From aac237b63752aad6d7eb944706f2eefe0d79f783 Mon Sep 17 00:00:00 2001 From: Danielle Lindblom <114356705+Danielle254@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:16:47 -0700 Subject: [PATCH 1/3] Danielle/658-fix-console-errors-on-forms (#658) Closes #555 --- app/(main)/account/recovery/page.tsx | 2 -- app/(main)/login/page.tsx | 6 ++++-- app/(main)/recover-password/page.tsx | 1 - app/(main)/register/Register.tsx | 8 +++++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/(main)/account/recovery/page.tsx b/app/(main)/account/recovery/page.tsx index aba1469d..05192427 100644 --- a/app/(main)/account/recovery/page.tsx +++ b/app/(main)/account/recovery/page.tsx @@ -106,7 +106,6 @@ const ResetPassword = (): React.JSX.Element => { const password: string = useWatch({ control: form.control, name: 'password', - defaultValue: '', }); /** @@ -116,7 +115,6 @@ const ResetPassword = (): React.JSX.Element => { const confirmPassword: string = useWatch({ control: form.control, name: 'confirmPassword', - defaultValue: '', }); /** diff --git a/app/(main)/login/page.tsx b/app/(main)/login/page.tsx index 7aa5d925..c524d599 100644 --- a/app/(main)/login/page.tsx +++ b/app/(main)/login/page.tsx @@ -64,18 +64,20 @@ const Login = (): React.JSX.Element => { const form = useForm({ resolver: zodResolver(LoginUserSchema), + defaultValues: { + email: '', + password: '', + }, }); const email = useWatch({ control: form.control, name: 'email', - defaultValue: '', }); const password = useWatch({ control: form.control, name: 'password', - defaultValue: '', }); /** diff --git a/app/(main)/recover-password/page.tsx b/app/(main)/recover-password/page.tsx index 6eca9c91..81937e63 100644 --- a/app/(main)/recover-password/page.tsx +++ b/app/(main)/recover-password/page.tsx @@ -67,7 +67,6 @@ const RecoverPassword = (): React.JSX.Element => { const email = useWatch({ control: form.control, name: 'email', - defaultValue: '', }); /** diff --git a/app/(main)/register/Register.tsx b/app/(main)/register/Register.tsx index 6c8d02b9..9338dc2d 100644 --- a/app/(main)/register/Register.tsx +++ b/app/(main)/register/Register.tsx @@ -67,6 +67,11 @@ const Register = (): JSX.Element => { const form = useForm({ resolver: zodResolver(RegisterUserSchema), + defaultValues: { + email: '', + password: '', + confirmPassword: '' + }, }); /** @@ -76,7 +81,6 @@ const Register = (): JSX.Element => { const email: string = useWatch({ control: form.control, name: 'email', - defaultValue: '', }); /** @@ -86,7 +90,6 @@ const Register = (): JSX.Element => { const password: string = useWatch({ control: form.control, name: 'password', - defaultValue: '', }); /** @@ -96,7 +99,6 @@ const Register = (): JSX.Element => { const confirmPassword: string = useWatch({ control: form.control, name: 'confirmPassword', - defaultValue: '', }); /** From 491d856fa2fc01679eec7c3b7964936d065a73bd Mon Sep 17 00:00:00 2001 From: Danielle Lindblom <114356705+Danielle254@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:07:29 -0700 Subject: [PATCH 2/3] Danielle/homepage-flickers-as-it-checks-authentication (#654) Closes #635 Issue: If user is logged in and goes to gridironsurvivor.com or gridironsurvivor.com/login, the login page displays briefly before the user is redirected to the leagues landing page. Desired behavior: A user who is already logged in should see the global spinner (pulsing logo) while authentication takes place, then is taken straight to leagues landing page. Login page should not display at all. For a user who is not logged in, the global spinner should display while authentication is checked, then see the login page. Solution: - updated the `isSignedIn` variable to have 3 states instead of the previous 2 - null, true, false. Null will satisfy the condition where authentication is being checked. If the user is logged in, state goes to true. If not logged in, state goes to false. - wrapped the page in the `GlobalSpinner` and added conditional rendering so that, if the user is logged in, they only see the spinner followed by the page redirect - updated the tests to reflect the spinner displaying while authentication is checked, then either redirecting the logged in user to the leagues landing page or loading the login page if the user is not logged in --- app/(main)/login/page.test.tsx | 51 ++++++--- app/(main)/login/page.tsx | 197 +++++++++++++++++--------------- context/AuthContextProvider.tsx | 5 +- context/AuthHelper.interface.ts | 2 +- 4 files changed, 144 insertions(+), 111 deletions(-) diff --git a/app/(main)/login/page.test.tsx b/app/(main)/login/page.test.tsx index 7ea58670..8c3dabc9 100644 --- a/app/(main)/login/page.test.tsx +++ b/app/(main)/login/page.test.tsx @@ -17,7 +17,13 @@ let continueButton: HTMLElement, emailInput: HTMLInputElement, passwordInput: HTMLInputElement; -const mockUseAuthContext = { +interface MockUseAuthContext { + getUser: jest.Mock; + isSignedIn: boolean | null; + login: jest.Mock; +} + +const mockUseAuthContext: MockUseAuthContext = { getUser, isSignedIn: false, login: mockLogin, @@ -46,20 +52,45 @@ describe('Login', () => { jest .spyOn(React, 'useState') .mockImplementation(() => [false, setIsLoading]); + }); + + it('should display GlobalSpinner while authenticating the user', async () => { + mockUseAuthContext.isSignedIn = null; + + render(); + + expect(screen.getByTestId('global-spinner')).toBeInTheDocument(); + }); + + it('should not display GlobalSpinner once authentication is complete', async () => { + mockUseAuthContext.isSignedIn = false; + + render(); + + expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); + }); + + it('should render the login page if the user is not logged in', () => { + mockUseAuthContext.isSignedIn = false; render(); continueButton = screen.getByTestId('continue-button'); emailInput = screen.getByTestId('email'); passwordInput = screen.getByTestId('password'); - }); - it('should render the login page', () => { + expect(continueButton).toBeInTheDocument(); expect(emailInput).toBeInTheDocument(); expect(passwordInput).toBeInTheDocument(); }); it('should update email and password fields and submit form', async () => { + mockUseAuthContext.isSignedIn = false; + + render(); + + const emailInput = screen.getByTestId('email'); + const passwordInput = screen.getByTestId('password'); const form = screen.getByTestId('login-form'); await act(async () => { @@ -80,16 +111,7 @@ describe('Login', () => { }); }); - it('redirects to /weeklyPicks when the button is clicked', () => { - mockUseAuthContext.isSignedIn = true; - - render(); - expect(mockUseAuthContext.getUser).toHaveBeenCalled(); - - mockUseAuthContext.isSignedIn = false; - }); - - it('redirects to /league/all when user navigates to /login', async () => { + it('redirects to /league/all when user navigates to /login and is logged in', async () => { mockUseAuthContext.isSignedIn = true; act(() => { @@ -106,6 +128,8 @@ describe('Login', () => { describe('Login loading spinner', () => { it('should show the loading spinner', async () => { + mockUseAuthContext.isSignedIn = false; + (useStateMock as jest.Mock).mockImplementation((init: boolean) => [ true, setIsLoading, @@ -117,6 +141,7 @@ describe('Login loading spinner', () => { expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument(); }); }); + it('should not show the loading spinner', async () => { (useStateMock as jest.Mock).mockImplementation((init: boolean) => [ false, diff --git a/app/(main)/login/page.tsx b/app/(main)/login/page.tsx index c524d599..66b6fd9a 100644 --- a/app/(main)/login/page.tsx +++ b/app/(main)/login/page.tsx @@ -11,6 +11,7 @@ import { FormItem, FormMessage, } from '../../../components/Form/Form'; +import GlobalSpinner from '@/components/GlobalSpinner/GlobalSpinner'; import { Input } from '@/components/Input/Input'; import { useAuthContext } from '@/context/AuthContextProvider'; import { useRouter } from 'next/navigation'; @@ -51,16 +52,15 @@ type LoginUserSchemaType = z.infer; */ const Login = (): React.JSX.Element => { const router = useRouter(); - const { login, isSignedIn, getUser } = useAuthContext(); - const [isLoading, setIsLoading] = useState(false); + const { login, isSignedIn } = useAuthContext(); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (isSignedIn) { - getUser(); + if (isSignedIn === true) { router.push('/league/all'); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSignedIn, getUser]); + }, [isSignedIn]); const form = useForm({ resolver: zodResolver(LoginUserSchema), @@ -99,98 +99,105 @@ const Login = (): React.JSX.Element => { }; return ( -
-
- -
-

- Thank you... fantasy football draft, for letting me know that even - in my fantasies, I am bad at sports. -

-

Jimmy Fallon

+
+ {(isSignedIn === null || isSignedIn === true) && + + } + {isSignedIn === false && + <> +
+ +
+

+ Thank you... fantasy football draft, for letting me know that even + in my fantasies, I am bad at sports. +

+

Jimmy Fallon

+
-
-
-
-

- Join Gridiron Survivor -

-

- Log in to your existing account or{' '} - sign up to get started - with a league -

+
+
+

+ Join Gridiron Survivor +

+

+ Log in to your existing account or{' '} + sign up to get started + with a league +

+
+
+ + } + name="email" + render={({ field }) => ( + + + + + {form.formState.errors?.email && ( + + {form.formState.errors.email.message} + + )} + + )} + /> + } + name="password" + render={({ field }) => ( + + + + + {form.formState.errors?.password && ( + + {form.formState.errors?.password.message} + + )} + + )} + /> +
-
- - } - name="email" - render={({ field }) => ( - - - - - {form.formState.errors?.email && ( - - {form.formState.errors.email.message} - - )} - - )} - /> - } - name="password" - render={({ field }) => ( - - - - - {form.formState.errors?.password && ( - - {form.formState.errors?.password.message} - - )} - - )} - /> -
+ + }
); }; diff --git a/context/AuthContextProvider.tsx b/context/AuthContextProvider.tsx index eb1adb9b..6e6964e5 100644 --- a/context/AuthContextProvider.tsx +++ b/context/AuthContextProvider.tsx @@ -23,7 +23,7 @@ type AuthContextType = { getUser: () => Promise; login: (user: UserCredentials) => Promise; // eslint-disable-line no-unused-vars logoutAccount: () => Promise; - isSignedIn: boolean; + isSignedIn: boolean | null; }; export const AuthContext = createContext(null); @@ -39,7 +39,7 @@ export const AuthContextProvider = ({ }: { children: React.ReactNode; }): JSX.Element => { - const [isSignedIn, setIsSignedIn] = useState(false); + const [isSignedIn, setIsSignedIn] = useState(null); const { updateUser, resetUser, user } = useDataStore( (state) => state, ); @@ -86,6 +86,7 @@ export const AuthContextProvider = ({ */ const getUser = async (): Promise => { if (!isSessionInLocalStorage()) { + setIsSignedIn(false); if (isAuthRequiredPath(pathname)) { router.push('/login'); } diff --git a/context/AuthHelper.interface.ts b/context/AuthHelper.interface.ts index 44791ed4..d62abed6 100644 --- a/context/AuthHelper.interface.ts +++ b/context/AuthHelper.interface.ts @@ -7,5 +7,5 @@ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.share export interface ILogoutType { resetUser: React.Dispatch>; router: AppRouterInstance; - setIsSignedIn: React.Dispatch>; + setIsSignedIn: React.Dispatch>; } From f92114fbe79c3e6b31d9f50b4b141f3a4eddd98a Mon Sep 17 00:00:00 2001 From: Carlos <54788134+Clue355@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:39:37 -0600 Subject: [PATCH 3/3] clue/FEE: Implement User and Admin Menu Dropdowns #392 (#487) Feat: - added a menu for the user settings in the /admin route that includes edit profile and sign out buttons - the sign out button logs a user out and redirects to /login page - the edit profile button directs user to /admin/edit-profile page --------- Co-authored-by: Ryan Furrer Co-authored-by: Chris Nowicki <102450568+chris-nowicki@users.noreply.github.com> Co-authored-by: Mai Vang <100221733+vmaineng@users.noreply.github.com> Co-authored-by: Alex Appleget Co-authored-by: Jennifer Tieu <41343727+jennifertieu@users.noreply.github.com> Co-authored-by: Shashi Lo <362527+shashilo@users.noreply.github.com> Co-authored-by: Cody Epstein Co-authored-by: Danielle Lindblom <114356705+Danielle254@users.noreply.github.com> --- components/AdminNav/AdminNav.test.tsx | 23 ++++- .../AdminUserSettings.test.tsx | 90 ++++++++++++++++++- .../AdminUserSettings/AdminUserSettings.tsx | 80 +++++++++++++---- 3 files changed, 175 insertions(+), 18 deletions(-) diff --git a/components/AdminNav/AdminNav.test.tsx b/components/AdminNav/AdminNav.test.tsx index b0f3c7ad..c39a39e2 100644 --- a/components/AdminNav/AdminNav.test.tsx +++ b/components/AdminNav/AdminNav.test.tsx @@ -1,10 +1,31 @@ import { AdminNav } from './AdminNav'; +import { AuthContextProvider } from '@/context/AuthContextProvider'; import { render, screen } from '@testing-library/react'; import React from 'react'; +const mockPush = jest.fn(); +const mockUsePathname = jest.fn(); + +jest.mock('next/navigation', () => ({ + useRouter() { + return { + push: mockPush, + }; + }, + usePathname() { + return mockUsePathname(); + }, +})); + describe('AdminNav Component', () => { beforeEach(() => { - render(); + jest.clearAllMocks(); + + render( + + + , + ); }); it('should render the navigation links with correct href attributes', () => { diff --git a/components/AdminUserSettings/AdminUserSettings.test.tsx b/components/AdminUserSettings/AdminUserSettings.test.tsx index e2048e49..6cf07921 100644 --- a/components/AdminUserSettings/AdminUserSettings.test.tsx +++ b/components/AdminUserSettings/AdminUserSettings.test.tsx @@ -1,12 +1,98 @@ import { AdminUserSettings } from './AdminUserSettings'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; +const mockPush = jest.fn(); +const mockUsePathname = jest.fn(); +const mockLogoutAccount = jest.fn(); + +const mockUseAuthContext = { + logoutAccount: mockLogoutAccount, +}; + +jest.mock('../../context/AuthContextProvider', () => ({ + useAuthContext() { + return { + ...mockUseAuthContext, + }; + }, +})); + +jest.mock('next/navigation', () => ({ + useRouter() { + return { + push: mockPush, + }; + }, + usePathname() { + return mockUsePathname(); + }, +})); + describe('AdminUserSettings Component', () => { - it('should render the component', () => { + beforeEach(() => { + jest.clearAllMocks(); + render(); + }); + + it('should render the component', async () => { const adminUserSettings = screen.getByTestId('admin-user-settings'); expect(adminUserSettings).toBeInTheDocument(); }); + + it('should show user options when clicked', () => { + const adminUserSettings = screen.getByTestId('admin-user-settings'); + + fireEvent.click(adminUserSettings); + + waitFor(() => { + expect(screen.getByTestId('edit-profile-link')).toBeInTheDocument(); + expect(screen.getByTestId('sign-out-button')).toBeInTheDocument(); + }); + }); + + it('should not show user options when closed', () => { + const adminUserSettings = screen.getByTestId('admin-user-settings'); + + fireEvent.click(adminUserSettings); + + fireEvent.click(adminUserSettings); + + waitFor(() => { + expect(screen.getByTestId('edit-profile-link')).not.toBeInTheDocument(); + expect(screen.getByTestId('sign-out-button')).not.toBeInTheDocument(); + }); + }); + + it('should direct to /account/settings route when the edit profile button is clicked', () => { + const adminUserSettings = screen.getByTestId('admin-user-settings'); + + fireEvent.click(adminUserSettings); + + waitFor(() => { + const editProfileButton = screen.getByTestId('edit-profile-link'); + fireEvent.click(editProfileButton); + }); + + waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/account/settings'); + }); + }); + + it('should direct to /login route when the sign out button is clicked', () => { + const adminUserSettings = screen.getByTestId('admin-user-settings'); + + fireEvent.click(adminUserSettings); + + waitFor(() => { + const signOutButton = screen.getByTestId('sign-out-button'); + fireEvent.click(signOutButton); + }); + + waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/login'); + }); + }); }); diff --git a/components/AdminUserSettings/AdminUserSettings.tsx b/components/AdminUserSettings/AdminUserSettings.tsx index e13e77cb..58bd6302 100644 --- a/components/AdminUserSettings/AdminUserSettings.tsx +++ b/components/AdminUserSettings/AdminUserSettings.tsx @@ -1,25 +1,75 @@ // Copyright (c) Gridiron Survivor. // Licensed under the MIT License. +'use client'; +import React, { JSX } from 'react'; +import { Button } from '../Button/Button'; +import { useDataStore } from '@/store/dataStore'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../TableDropDownMenu/TableDropDownMenu'; import { LucideChevronsUpDown } from 'lucide-react'; -import React from 'react'; +import { useAuthContext } from '@/context/AuthContextProvider'; +import { useRouter } from 'next/navigation'; +import LinkCustom from '../LinkCustom/LinkCustom'; /** - * The admin user settings component. - * @returns The rendered admin user settings. + * Renders admin user settings. + * @returns {JSX.Element} The rendered admin user settings component. */ -export const AdminUserSettings = (): React.JSX.Element => { +export const AdminUserSettings = (): JSX.Element => { + const router = useRouter(); + const { logoutAccount } = useAuthContext(); + const { user } = useDataStore((state) => state); + + /** + * Handles the logout. + * @returns {Promise} The logout promise. + */ + const handleLogout = async (): Promise => { + try { + await logoutAccount(); + router.push('/login'); + } catch (error) { + throw error; + } + }; + return ( -
- -

Users Name

- -
+ + +
+ +

{user.email}

+ +
+
+ + + + Edit Profile + + + +