diff --git a/docs/dev docs/general bugs fixed/frontend-turbo-failing.md b/docs/dev docs/bugs-fixed-report/frontend-turbo-failing.md similarity index 100% rename from docs/dev docs/general bugs fixed/frontend-turbo-failing.md rename to docs/dev docs/bugs-fixed-report/frontend-turbo-failing.md diff --git a/docs/dev docs/general bugs fixed/port-already-in-use.md b/docs/dev docs/bugs-fixed-report/port-already-in-use.md similarity index 100% rename from docs/dev docs/general bugs fixed/port-already-in-use.md rename to docs/dev docs/bugs-fixed-report/port-already-in-use.md diff --git a/eslint.config.mjs b/eslint.config.mjs index 5bad6edd..5571e808 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,9 @@ export default [ React: 'readonly', exports: 'readonly', HTMLInputElement: "readonly", - HTMLFormElement: "readonly" + HTMLFormElement: "readonly", + setInterval: "readonly", + clearInterval: "readonly", }, }, plugins: { diff --git a/frontend/package.json b/frontend/package.json index 92b0dd60..441191f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,9 @@ "test": "vitest" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "@fullcalendar/core": "^6.1.17", "@fullcalendar/daygrid": "^6.1.17", "@fullcalendar/interaction": "^6.1.17", diff --git a/frontend/src/app/dashboard/calendar/page.tsx b/frontend/src/app/dashboard/calendar/page.tsx index cf308b5e..4a13a143 100644 --- a/frontend/src/app/dashboard/calendar/page.tsx +++ b/frontend/src/app/dashboard/calendar/page.tsx @@ -30,6 +30,7 @@ import axios from 'axios'; import { backendBaseUrl } from '@/lib/utils'; interface Event { + start_time?: Date | string; id: number | string; // your backend sometimes uses uuid string, sometimes number title: string; end_time: Date | string | null; @@ -102,7 +103,7 @@ export default function Home() { const extractedEvents = response.data.map((event: Event) => ({ id: event.id, title: event.title, - start: event.start ? new Date(event.start) : undefined, + start: event.start_time ? new Date(event.start_time) : undefined, end: event.end_time ? new Date(event.end_time) : undefined, allDay: event.allDay ?? false, // default to false if undefined // Optional: You could add more fields here if FullCalendar needs diff --git a/frontend/src/components/Help.tsx b/frontend/src/components/Help.tsx index 2c2524e7..1f2e5199 100644 --- a/frontend/src/components/Help.tsx +++ b/frontend/src/components/Help.tsx @@ -43,6 +43,7 @@ export const Help = () => { return (
- {/* Sign-in */} {/* Sign-in or Dashboard Redirect */}
- {user ? ( + {verified ? (

You're already signed in. @@ -102,33 +94,6 @@ const Home: React.FC = () => { )}

- - {/*
- -
*/} - {/*
*/} - {/*
*/} - {/*

Sign In

*/} - {/*
*/} - {/* */} - {/* */} - {/* router.push('/dashboard')}*/} - {/* className="w-full rounded-full py-3 font-semibold bg-gradient-to-r from-purple-500 to-indigo-500 text-white hover:from-purple-400 hover:to-indigo-400 transition"*/} - {/* >*/} - {/* Sign In*/} - {/* */} - {/*
*/} - {/*
*/} - {/*
*/} ); }; diff --git a/frontend/src/components/UserPreferencesForm.tsx b/frontend/src/components/UserPreferencesForm.tsx index 197dd5c6..7158ae63 100644 --- a/frontend/src/components/UserPreferencesForm.tsx +++ b/frontend/src/components/UserPreferencesForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -21,6 +21,7 @@ import { Card, CardHeader, CardContent } from './ui/card'; import axios from 'axios'; import { useAuth } from '@/components/context/auth/AuthContext'; import { backendBaseUrl, minutesToTime } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; //Define form shema export const formSchema = z .object({ @@ -75,6 +76,7 @@ export function UserPreferencesForm({ defaultValues, }: UserPreferencesFormProps) { const router = useRouter(); + const [loading, setLoading] = useState(false); const { user } = useAuth(); // 1. Define your form. const form = useForm>({ @@ -97,6 +99,7 @@ export function UserPreferencesForm({ // 2. Define a submit handler. function onSubmit(values: z.infer) { + setLoading(true); // Do something with the form values. // This will be type-safe and validated. console.log(values); @@ -118,6 +121,7 @@ export function UserPreferencesForm({ axios .post(backendBaseUrl + `/api/user/surveyresults/${user.uid}`, outputs) .then((response) => { + setLoading(false); console.log('Successfully posted answers for user: ', user!.uid); console.log(response); router.push('/dashboard'); @@ -242,8 +246,21 @@ export function UserPreferencesForm({ )} /> - diff --git a/frontend/src/components/UserProfile.tsx b/frontend/src/components/UserProfile.tsx index da9a9d7a..21ea3252 100644 --- a/frontend/src/components/UserProfile.tsx +++ b/frontend/src/components/UserProfile.tsx @@ -68,7 +68,20 @@ export default function ProfilePage() { // Handle the save preference logic function handleSavePreferences(values: z.infer): void { - console.log('Updated preferences:', values); + console.log( + 'handleSavePreferences', + values as ReturnType + ); + console.log('current: ', userPreferences); + console.log(values === userPreferences); + console.log(JSON.stringify(values) === JSON.stringify(userPreferences)); + + if (JSON.stringify(values) === JSON.stringify(userPreferences)) { + window.alert("You didn't make any changes!"); + setOpen(false); + return; + } + const outputs = []; for (const question in values) { outputs.push({ @@ -76,23 +89,26 @@ export default function ProfilePage() { answer: String(values[question as keyof z.infer]), }); } - console.log(outputs); + // Send the data to our backend if (!user?.uid) return; axios .put(backendBaseUrl + `/api/user/surveyresults/${user.uid}`, outputs) .then((response) => { + // Update state of user preference + setUserPreferences(values); + console.log('Successfully updated answers for user: ', user!.uid); console.log(response); }) .catch((error) => { console.log(error); + }) + .finally(() => { + // Close the dialog after save + setOpen(false); }); - // Update state of user preference - setUserPreferences(values); - // Close the dialog after save - setOpen(false); } if (loading || !userPreferences) { @@ -153,10 +169,6 @@ export default function ProfilePage() { )} - {/*

*/} - {/* {displayName}*/} - {/*

*/} - {/* User Preferences */}
diff --git a/frontend/src/components/context/auth/AuthContext.tsx b/frontend/src/components/context/auth/AuthContext.tsx index a6a2b8e7..f79a5354 100644 --- a/frontend/src/components/context/auth/AuthContext.tsx +++ b/frontend/src/components/context/auth/AuthContext.tsx @@ -7,19 +7,28 @@ import { User, signInWithEmailAndPassword, createUserWithEmailAndPassword, + updateProfile, + sendEmailVerification, + sendPasswordResetEmail, } from 'firebase/auth'; type AuthContextType = { user: User | null; + displayName: string | null; + verified: boolean; loading: boolean; signIn: (email: string, password: string) => Promise; signOut: () => Promise; - signUp: (email: string, password: string) => Promise; + signUp: ( + email: string, + password: string, + displayName: string + ) => Promise; + forgotPassword: (email: string) => Promise; }; const AuthContext = createContext(undefined); -// Custom hook to use authentication context export const useAuth = () => { const context = useContext(AuthContext); if (!context) { @@ -28,42 +37,73 @@ export const useAuth = () => { return context; }; -// Define the type for children prop interface AuthProviderProps { - children: React.ReactNode; // This ensures the AuthProvider can accept any valid React child + children: React.ReactNode; } export const AuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + const [verified, setVerified] = useState(false); - // Listen for authentication state changes useEffect(() => { - const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { - setUser(user); + const unsubscribe = onAuthStateChanged(firebaseAuth, (currentUser) => { + setUser(currentUser); + setVerified(currentUser?.emailVerified ?? false); setLoading(false); }); return () => unsubscribe(); }, []); - // SignIn function const signIn = async (email: string, password: string) => { - await signInWithEmailAndPassword(firebaseAuth, email, password); + setLoading(true); + await signInWithEmailAndPassword(firebaseAuth, email, password).finally( + () => setLoading(false) + ); }; - // SignUp function - const signUp = async (email: string, password: string) => { - await createUserWithEmailAndPassword(firebaseAuth, email, password); + const signUp = async ( + email: string, + password: string, + displayName: string + ) => { + setLoading(true); + try { + const { user } = await createUserWithEmailAndPassword( + firebaseAuth, + email, + password + ); + + await updateProfile(user, { displayName }); + await sendEmailVerification(user); + } finally { + setLoading(false); + } }; - // SignOut function const signOut = async () => { await firebaseAuth.signOut(); }; + const forgotPassword = async (email: string) => { + await sendPasswordResetEmail(firebaseAuth, email); + }; + return ( - + {children} ); diff --git a/frontend/src/components/ui/ProtectedRoutes.tsx b/frontend/src/components/ui/ProtectedRoutes.tsx index 0157f2eb..4dd6b27e 100644 --- a/frontend/src/components/ui/ProtectedRoutes.tsx +++ b/frontend/src/components/ui/ProtectedRoutes.tsx @@ -5,18 +5,18 @@ import { useEffect } from 'react'; import { useAuth } from '@/components/context/auth/AuthContext'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { - const { user, loading } = useAuth(); + const { verified, loading } = useAuth(); const router = useRouter(); const pathname = usePathname(); useEffect(() => { // Only redirect if not loading and no user - if (!loading && !user && pathname !== '/') { + if (!loading && !verified && pathname !== '/') { router.push('/'); } - }, [user, loading, pathname, router]); + }, [verified, loading, pathname, router]); - if (loading || (!user && pathname !== '/')) { + if (loading || (!verified && pathname !== '/')) { return
Loading...
; } diff --git a/frontend/src/components/ui/auth/SignIn.tsx b/frontend/src/components/ui/auth/SignIn.tsx index 0c4d0f8d..6523fd42 100644 --- a/frontend/src/components/ui/auth/SignIn.tsx +++ b/frontend/src/components/ui/auth/SignIn.tsx @@ -3,13 +3,37 @@ import { useAuth } from '@/components/context/auth/AuthContext'; import { getAuth } from 'firebase/auth'; import axios from 'axios'; import { backendBaseUrl } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; export default function SignIn() { const [email, setEmail] = useState(''); - const { signIn, loading } = useAuth(); + const [visible, setVisible] = useState(false); + const { signIn, forgotPassword, loading } = useAuth(); const [password, setPassword] = useState(''); const [errorMessage, setErrorMessage] = useState(''); + const handleForgotPassword = async (e: React.MouseEvent) => { + e.preventDefault(); + + try { + await forgotPassword(email); + setErrorMessage('Please check your email for password reset link!'); + } catch (error: unknown) { + if (error instanceof Error) { + if (error.message === 'Firebase: Error (auth/missing-email).') { + setErrorMessage( + 'Please fill out the email so we can send you a password reset link!' + ); + } else { + setErrorMessage(error.message); + } + } else { + setErrorMessage('Something went wrong.'); + } + } + }; + const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); @@ -37,15 +61,25 @@ export default function SignIn() { } } catch (error: unknown) { if (error instanceof Error) { - setErrorMessage(error.message); + if (error.message === 'Firebase: Error (auth/invalid-email).') { + setErrorMessage('Please provide a valid email address!'); + } else if ( + error.message === 'Firebase: Error (auth/missing-password).' + ) { + setErrorMessage('Please fill out your password!'); + } else if ( + error.message === 'Firebase: Error (auth/invalid-credential).' + ) { + setErrorMessage('Wrong email or password!'); + } else { + setErrorMessage(error.message); + } } else { setErrorMessage('Something went wrong.'); } } }; - if (loading) return
Loading...
; - return (
@@ -56,24 +90,63 @@ export default function SignIn() { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" + required className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} /> - setPassword(e.target.value)} - placeholder="Password" - className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" - /> + +
+ setPassword(e.target.value)} + placeholder="Password" + required + className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} + /> + + { + setVisible(!visible); + }} + /> + +
+ {errorMessage && (

{errorMessage}

)} - +
+ + +
diff --git a/frontend/src/components/ui/auth/SignUp.tsx b/frontend/src/components/ui/auth/SignUp.tsx index bbcede8b..875e5f6c 100644 --- a/frontend/src/components/ui/auth/SignUp.tsx +++ b/frontend/src/components/ui/auth/SignUp.tsx @@ -1,17 +1,71 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useAuth } from '@/components/context/auth/AuthContext'; -import { getAuth, updateProfile } from 'firebase/auth'; +import { getAuth } from 'firebase/auth'; import axios from 'axios'; import { backendBaseUrl } from '@/lib/utils'; +import { Loader2 } from 'lucide-react'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +library.add(faEye, faEyeSlash); export default function SignUpForm() { const [email, setEmail] = useState(''); - const { signUp, user, loading } = useAuth(); + const { signUp, loading } = useAuth(); + const [visible, setVisible] = useState(false); + const [visibleConfirm, setVisibleConfirm] = useState(false); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const [displayName, setDisplayName] = useState(''); + const [displayNameText, setDisplayNameText] = useState(''); const [errorMessage, setErrorMessage] = useState(''); + const [authenticated, setAuthenticated] = useState(false); + const [polling, setPolling] = useState(false); + useEffect(() => { + if (authenticated) { + window.location.href = '/preference'; + } + }, [authenticated]); + + // Purpose: check for email verification and send token + const startVerificationPolling = () => { + if (polling) return; + setPolling(true); + + const interval = setInterval(async () => { + const currentUser = getAuth().currentUser; + if (!currentUser) return; + + await currentUser.reload(); // refreshes auth info + + if (currentUser.emailVerified) { + clearInterval(interval); + setPolling(false); + + try { + const idToken = await currentUser.getIdToken(true); + + const response = await axios.post( + backendBaseUrl + '/api/auth/signup', + { idToken } + ); + + if (response.status === 201) { + setAuthenticated(true); + } else { + setErrorMessage('Could not complete signup with backend.'); + } + } catch (err) { + console.error(err); + setErrorMessage( + 'Something went wrong communicating with the server.' + ); + } + } + }, 3000); // check every 3 seconds + }; + + // Handling clicking Sign Up button const handleSignUp = async (e: React.FormEvent) => { e.preventDefault(); @@ -21,41 +75,24 @@ export default function SignUpForm() { } try { - await signUp(email, password); - - if (!user) { - setErrorMessage('Creating your account...'); - } - - // 3. Get the Firebase ID token - const idToken = await getAuth().currentUser?.getIdToken(true); - - if (!idToken) { - console.log(`${idToken} not found`); - throw new Error('Failed to retrieve ID token'); - } + await signUp(email, password, displayNameText).then(() => + setErrorMessage( + 'Please verify your account by following the instructions emailed to you!' + ) + ); - // 4. Send the ID token to the backend API for user creation - const response = await axios.post(backendBaseUrl + '/api/auth/signup', { - idToken, - }); - - // 5. Handle response from your API - if (response.status === 201) { - if (user) { - await updateProfile(user, { - displayName: displayName, - }).then(() => { - console.log('Display name updated: ', user.displayName); - }); - } - window.location.href = '/preference'; // Redirect after successful signup - } else { - setErrorMessage('Cannot create new account'); - } + startVerificationPolling(); } catch (error: unknown) { if (error instanceof Error) { - setErrorMessage(error.message); + if (error.message === 'Firebase: Error (auth/invalid-email).') { + setErrorMessage('Please provide a valid email address!'); + } else if ( + error.message === 'Firebase: Error (auth/missing-password).' + ) { + setErrorMessage('Please fill out your password!'); + } else { + setErrorMessage(error.message); + } } else { setErrorMessage( 'Cannot create your account right now. Please try again other time.' @@ -64,8 +101,6 @@ export default function SignUpForm() { } }; - if (loading) return
Loading...
; - return (
@@ -77,36 +112,76 @@ export default function SignUpForm() { onChange={(e) => setEmail(e.target.value)} placeholder="Email" className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} /> - setPassword(e.target.value)} - placeholder="Password" - className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" - /> - setConfirmPassword(e.target.value)} - placeholder="Confirm Password" - className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" - /> +
+ setPassword(e.target.value)} + placeholder="Password" + className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} + /> + + { + { + setVisible(!visible); + }} + /> + } + +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="Confirm Password" + className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} + /> + + { + { + setVisibleConfirm(!visibleConfirm); + }} + /> + } + +
+ setDisplayName(e.target.value)} + value={displayNameText ? displayNameText : ''} + onChange={(e) => setDisplayNameText(e.target.value)} placeholder="Display Name" className="w-full p-3 rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + disabled={loading} /> {errorMessage && (

{errorMessage}

)}