diff --git a/docs/agile docs/Sprint 5/Sprint Journal 5.md b/docs/agile docs/Sprint 5/Sprint Journal 5.md index c8e7feff..e0e50b8e 100644 --- a/docs/agile docs/Sprint 5/Sprint Journal 5.md +++ b/docs/agile docs/Sprint 5/Sprint Journal 5.md @@ -1 +1,41 @@ # Sprint Journal 5 + +# 1. Adoption Plan + +**1) Date that you wrote this journal entry.** + +Friday, May 2 + +**2) Is your product ready for adoption?** + +- *Describe the current state of your product's development at the time of writing this journal entry.* +- *Give a comprehensive risk/benefit analysis of your product by stating at least 5 risks or benefits of using your product* +- *Is it right to encourage people to adopt your product in its current state? Or will you continue developing instead? Explain your decision.* + +--- + +Our product is not ready for adoption. Currently, our product displays the frontend calendar and supports adding/removing events. However, we still need to work on managing events, adding/removing/managing deadlines, and scheduling time to work on deadlines. Ideally, we also want to have certain stretch goals, such as calendar integration. Since we do not have our planned features fully implemented yet, our product is not adoptable right now. + +Below are 5 risks and benefits for using the product. + +- **Benefit**: Users can think more intentionally about work habits when using our product to create a schedule. +- **Risk**: We do not have a mobile-friendly way to use the product now, since calendaring is largely done on mobile nowadays. +- **Risk**: We have not confirmed whether our product works at scale. +- **Risk**: The product does not have any ways to incentivize users following their schedules. +- **Risk**: The product lacks notification functionality, which is an important aspect for keeping users accountable to the plans they make on the app. + +Given these risks and the missing features, it is not right to encourage people to adopt our product. Many features are not fully working and the optimization of our product at a large scale is unconfirmed. We seek to complete the feature set initially planned and make the software user-friendly and mobile-friendly before encouraging users to adopt it. + +**3b) Continued development plan (if not ready for adoption)** + +- *Will your product be ready for adoption after some future work? Why or why not? (Don't be too hard on yourself if you decide not. Many software products make their most important impact as a learning experience for the development team rather than by reaching target users. It's totally fine if that's the case for your product. We are responsible to only encourage others to use our software when it is right to do so.)* +- *If you answered yes to the previous question, what future work is needed before adoption? Aim high. You will not be evaluated on completing all the future work.* +- *If you answered no, what future work is needed for you to tie off the loose ends of your product and feel satisfied in your learning?* + +--- + +We are not sure right now about whether our product is ready for adoption. We initially planned on making the scheduling algorithm as an integral part of our product. However, we realized we also lacked skills in the user-facing and business-logic aspects, which are fundamental to all software products today. This meant that we spent a significant amount of time learning how to implement the basic frontend and backend logic of our app, preventing us from engaging with the scheduling algorithm significantly. Therefore, the readiness for adoption of our product would depend on the feasibility to complete our scheduling algorithm. + +If the scheduling algorithm is found to be doable without significant research, or if this is a solved problem, then our product will be ready for adoption. Future work at this point would likely involve creating a frontend that is user-friendly and accessible while providing users access to the scheduling algorithm from a calendar interface. This would include adding the remaining features we plan to implement, including managing events and implementing deadlines. Furthermore, we would want to make our application usable from mobile devices. We would also need to ensure reliability through expanding the scope of our testing and ensuring our product performs at scale. + +Otherwise, we may determine our product is not ready for adoption after some future work, because the future work is too significant. We can tie up loose ends through delivering a generic calendar app without a polished scheduling algorithm. In such an app, we would further develop our user interface so that it is consistent with industry standards, completing our implementation of event and deadline management. We would also improve our code documentation and test coverage. This will make us satisfied in our learning through gaining the fundamental understanding needed to create a full-stack web application, which will be useful for future projects or jobs we pursue individually. 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}

)}