diff --git a/api/apiFunctions.ts b/api/apiFunctions.ts index 93041dd3..d9b31470 100644 --- a/api/apiFunctions.ts +++ b/api/apiFunctions.ts @@ -443,7 +443,6 @@ export async function getAllLeagues(): Promise { participants: league.participants, survivors: league.survivors, })); - return leagues; } catch (error) { throw new Error('Error getting all leagues', { cause: error }); diff --git a/app/(main)/league/[leagueId]/entry/Entries.interface.ts b/app/(main)/league/[leagueId]/entry/Entries.interface.ts index af624df8..5c324ff9 100644 --- a/app/(main)/league/[leagueId]/entry/Entries.interface.ts +++ b/app/(main)/league/[leagueId]/entry/Entries.interface.ts @@ -6,8 +6,8 @@ import { ILeague, INFLTeam, IUser } from '@/api/apiFunctions.interface'; export interface IEntry { $id: string; name: string; - user: IUser; - league: ILeague; + user: IUser['id']; + league: ILeague['leagueId']; selectedTeams: INFLTeam['teamName'][]; eliminated: boolean; } diff --git a/app/(main)/league/all/page.test.tsx b/app/(main)/league/all/page.test.tsx index b1a783c8..535e135c 100644 --- a/app/(main)/league/all/page.test.tsx +++ b/app/(main)/league/all/page.test.tsx @@ -8,264 +8,164 @@ import { import Leagues from './page'; import { useDataStore } from '@/store/dataStore'; import { getUserLeagues } from '@/utils/utils'; -import { getAllLeagues, addUserToLeague } from '@/api/apiFunctions'; +import { + getAllLeagues, + addUserToLeague, + getGameWeek, + getCurrentUserEntries, +} from '@/api/apiFunctions'; import { toast } from 'react-hot-toast'; import Alert from '@/components/AlertNotification/AlertNotification'; import { AlertVariants } from '@/components/AlertNotification/Alerts.enum'; -const mockUseAuthContext = { - isSignedIn: false, -}; - jest.mock('@/context/AuthContextProvider', () => ({ - useAuthContext() { - return { - ...mockUseAuthContext, - }; - }, + useAuthContext: () => ({ isSignedIn: true }), })); -jest.mock('@/store/dataStore', () => ({ - useDataStore: jest.fn(() => ({ - user: { - documentId: '123', - id: '1234', - email: 'test@test.com', - leagues: ['league1'], - }, - allLeagues: [ - { - leagueId: '123', - leagueName: 'Test League', - logo: 'logo.png', - participants: ['123456', '78'], - survivors: ['123456', '78'], - }, - ], - updateUser: jest.fn(), - })), -})); +jest.mock('@/store/dataStore'); +jest.mock('@/utils/utils'); +jest.mock('@/api/apiFunctions'); +jest.mock('react-hot-toast'); -jest.mock('@/utils/utils', () => ({ - getUserLeagues: jest.fn(() => Promise.resolve([])), - cn: jest.fn(), -})); +const mockUser = { + documentId: '123', + email: 'test@test.com', + id: '123', + leagues: [], +}; -jest.mock('@/api/apiFunctions', () => ({ - getAllLeagues: jest.fn(), - addUserToLeague: jest.fn(), -})); +const mockLeague = { + leagueId: '123', + leagueName: 'Test League', + logo: 'logo.png', + participants: [], + survivors: [], +}; -jest.mock('react-hot-toast', () => ({ - toast: { - custom: jest.fn(), - }, -})); +const setup = (initialStoreState = {}) => { + const mockStore = { + user: mockUser, + allLeagues: [mockLeague], + userLeagues: [], + updateUser: jest.fn(), + updateUserLeagues: jest.fn(), + updateAllLeagues: jest.fn(), + updateGameWeek: jest.fn(), + updateEntries: jest.fn(), + ...initialStoreState, + }; + + (useDataStore as unknown as jest.Mock).mockReturnValue(mockStore); + (getAllLeagues as jest.Mock).mockResolvedValue([mockLeague]); + (getUserLeagues as jest.Mock).mockResolvedValue([]); + (getCurrentUserEntries as jest.Mock).mockResolvedValue([]); + (getGameWeek as jest.Mock).mockResolvedValue({ id: '123', week: 1 }); + + return { mockStore }; +}; describe('Leagues Component', () => { - const mockUseDataStore = useDataStore as unknown as jest.Mock; - const mockGetUserLeagues = getUserLeagues as jest.Mock; - const mockGetAllLeagues = getAllLeagues as jest.Mock; - const mockAddUserToLeague = addUserToLeague as jest.Mock; - beforeEach(() => { jest.clearAllMocks(); }); - it('should render "You are not enrolled in any leagues" message when no leagues are found', async () => { - mockUseAuthContext.isSignedIn = true; - mockUseDataStore.mockReturnValue({ - user: { - documentId: '123', - email: 'test@test.com', - id: '123', - leagues: [], - }, - allLeagues: [], - }); - + it('should display GlobalSpinner while loading data', async () => { + setup(); render(); - + expect(screen.getByTestId('global-spinner')).toBeInTheDocument(); await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner')); - - await waitFor(() => { - const messageElement = screen.getByTestId('no-leagues-message'); - expect(messageElement).toBeInTheDocument(); - }); }); - it('should display GlobalSpinner while loading data', async () => { - mockUseAuthContext.isSignedIn = true; - - mockUseDataStore.mockReturnValueOnce({ - user: { - documentId: '123', - email: 'test@test.com', - id: '123', - leagues: [], - }, - allLeagues: [], - }); - + it('should render "You are not enrolled in any leagues" message when no leagues are found', async () => { + setup(); render(); - - expect(screen.getByTestId('global-spinner')).toBeInTheDocument(); + await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner')); + expect(screen.getByTestId('no-leagues-message')).toBeInTheDocument(); }); - it('should not display GlobalSpinner after loading data', async () => { - mockUseAuthContext.isSignedIn = true; - - mockUseDataStore.mockReturnValue({ - user: { - documentId: '123', - email: 'test@test.com', - id: '123', - leagues: [], - }, - allLeagues: [ - { - leagueId: '123', - leagueName: 'Test League', - logo: 'logo.png', - participants: ['123456', '78'], - survivors: ['123456', '78'], - }, - ], + it('should call getCurrentUserEntries and getGameWeek when component loads and user has leagues', async () => { + const userWithLeague = { ...mockUser, leagues: [mockLeague.leagueId] }; + const { mockStore } = setup({ + user: userWithLeague, + userLeagues: [mockLeague], }); - mockGetUserLeagues.mockResolvedValueOnce([]); + (getUserLeagues as jest.Mock).mockResolvedValue([mockLeague]); render(); await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner')); - expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); + await waitFor(() => { + expect(getAllLeagues).toHaveBeenCalled(); + expect(getUserLeagues).toHaveBeenCalledWith(userWithLeague.leagues); + expect(getCurrentUserEntries).toHaveBeenCalledWith( + userWithLeague.id, + mockLeague.leagueId, + ); + expect(getGameWeek).toHaveBeenCalled(); + expect(mockStore.updateAllLeagues).toHaveBeenCalled(); + expect(mockStore.updateUserLeagues).toHaveBeenCalled(); + expect(mockStore.updateEntries).toHaveBeenCalled(); + expect(mockStore.updateGameWeek).toHaveBeenCalled(); + }); }); it('should handle form submission to join a league', async () => { - mockUseAuthContext.isSignedIn = true; - - const user = { - documentId: '123', - email: 'test@test.com', - id: '123', - leagues: [], - }; - - const league = { - leagueId: '123', - leagueName: 'Test League', - logo: 'logo.png', - participants: [], - survivors: [], - }; - - const updateUser = jest.fn(); - - mockUseDataStore.mockReturnValue({ - user, - allLeagues: [league], - updateUser, + const { mockStore } = setup(); + (addUserToLeague as jest.Mock).mockResolvedValue({ + userDocumentId: mockUser.documentId, + selectedLeague: mockLeague.leagueId, + selectedLeagues: [mockLeague.leagueId], + participants: [mockUser.id], + survivors: [mockUser.id], }); - mockGetAllLeagues.mockResolvedValueOnce([league]); - mockAddUserToLeague.mockResolvedValue( - Promise.resolve({ - userDocumentId: user.documentId, - selectedLeague: league.leagueId, - selectedLeagues: [league.leagueId], - participants: [user.id], - survivors: [user.id], - }), - ); - render(); + await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner')); - await waitFor(() => { - expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('select-available-leagues'), { + target: { value: '123' }, }); - - const selectElement = screen.getByTestId('select-available-leagues'); - fireEvent.change(selectElement, { target: { value: '123' } }); fireEvent.click(screen.getByTestId('join-league-button')); await waitFor(() => { - expect(mockAddUserToLeague).toHaveBeenCalledWith({ - userDocumentId: user.documentId, - selectedLeague: league.leagueId, - selectedLeagues: [league.leagueId], - participants: [user.id], - survivors: [user.id], - }); - expect(updateUser).toHaveBeenCalledWith( - user.documentId, - user.id, - user.email, - [...user.leagues, league.leagueId], + expect(addUserToLeague).toHaveBeenCalledWith( + expect.objectContaining({ + userDocumentId: mockUser.documentId, + selectedLeague: mockLeague.leagueId, + }), + ); + expect(mockStore.updateAllLeagues).toHaveBeenCalledWith([mockLeague]); + expect(mockStore.updateUserLeagues).toHaveBeenCalledWith([mockLeague]); + expect(mockStore.updateUser).toHaveBeenCalledWith( + mockUser.documentId, + mockUser.id, + mockUser.email, + [mockLeague.leagueId], ); expect(toast.custom).toHaveBeenCalledWith( , ); }); }); it('should show error if adding to league fails', async () => { - mockUseAuthContext.isSignedIn = true; - - const user = { - documentId: '123', - email: 'test@test.com', - id: '123', - leagues: [], - }; - - const league = { - leagueId: '123', - leagueName: 'Test League', - logo: 'logo.png', - participants: [], - survivors: [], - }; - - mockUseDataStore.mockReturnValue({ - user, - allLeagues: [league], - }); - - mockGetUserLeagues.mockResolvedValueOnce([]); - mockGetAllLeagues.mockResolvedValueOnce([league]); - mockAddUserToLeague.mockResolvedValue( - Promise.resolve({ - userDocumentId: user.documentId, - selectedLeague: league.leagueId, - selectedLeagues: [league.leagueId], - participants: [user.id], - survivors: [user.id], - }), - ); + setup(); + (addUserToLeague as jest.Mock).mockRejectedValue(new Error()); render(); + await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner')); - await waitFor(() => { - expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('select-available-leagues'), { + target: { value: '123' }, }); - - const selectElement = screen.getByTestId('select-available-leagues'); - fireEvent.change(selectElement, { target: { value: '123' } }); fireEvent.click(screen.getByTestId('join-league-button')); await waitFor(() => { - expect(mockAddUserToLeague).toHaveBeenCalledWith({ - userDocumentId: user.documentId, - selectedLeague: league.leagueId, - selectedLeagues: [league.leagueId], - participants: [user.id], - survivors: [user.id], - }); - expect(toast.custom).toHaveBeenCalledWith( ; * @returns {JSX.Element} The rendered leagues component. */ const Leagues = (): JSX.Element => { - const [leagues, setLeagues] = useState([]); const [loadingData, setLoadingData] = useState(true); - const { user, updateUser, allLeagues, updateAllLeagues } = useDataStore( - (state) => state, - ); + const { + user, + updateUser, + allLeagues, + updateAllLeagues, + userLeagues, + updateUserLeagues, + updateEntries, + updateGameWeek, + } = useDataStore((state) => state); const { isSignedIn } = useAuthContext(); const { handleSubmit, control } = useForm({ resolver: zodResolver(leagueSchema), }); /** - * Fetches all leagues and leagues user is a part of from the database. + * Fetches all leagues, user leagues, and user entries */ - const fetchData = async (): Promise => { + const fetchGameData = async (): Promise => { try { - // Only fetch all leagues if they're not already in the store - if (allLeagues.length === 0) { - const fetchedLeagues = await getAllLeagues(); - updateAllLeagues(fetchedLeagues); - } + // fetch all leagues + const fetchedLeagues = await getAllLeagues(); + updateAllLeagues(fetchedLeagues); // Fetch user leagues const fetchedUserLeagues = await getUserLeagues(user.leagues); - setLeagues(fetchedUserLeagues); + updateUserLeagues(fetchedUserLeagues); + + // Fetch addition user data + fetchAdditionalUserData(fetchedUserLeagues); } catch (error) { - console.error('Error fetching leagues:', error); toast.custom( { } }; + /** + * Fetches entries per league and the current week. + * @param {ILeague[]} leagues - The leagues to fetch entries for. + * @returns {Promise} + */ + const fetchAdditionalUserData = async (leagues: ILeague[]): Promise => { + try { + const entryPromises = leagues.map((league) => + getCurrentUserEntries(user.id, league.leagueId), + ); + const gameWeekPromise = getGameWeek(); + + const [entries, currentWeek] = await Promise.all([ + Promise.all(entryPromises), + gameWeekPromise, + ]); + + updateEntries(entries.flat()); + updateGameWeek(currentWeek); + } catch (error) { + throw new Error('Error fetching entries and game week'); + } + }; + useEffect(() => { if (isSignedIn) { - fetchData(); + fetchGameData(); } }, [isSignedIn]); @@ -99,7 +134,7 @@ const Leagues = (): JSX.Element => { survivors: [...(league.survivors ?? []), user.id], }); - setLeagues([...leagues, league]); + updateUserLeagues([...userLeagues, league]); updateUser(user.documentId, user.id, user.email, [ ...user.leagues, league.leagueId, @@ -131,8 +166,8 @@ const Leagues = (): JSX.Element => { Your Leagues
- {leagues.length > 0 ? ( - leagues.map((league) => ( + {userLeagues.length > 0 ? ( + userLeagues.map((league) => ( { @@ -58,14 +72,14 @@ describe('Data Store', () => { act(() => { result.current.updateUser( userData.documentId, - userData.userId, - userData.userEmail, + userData.id, + userData.email, userData.leagues, ); }); - expect(result.current.user.id).toBe(userData.userId); - expect(result.current.user.email).toBe(userData.userEmail); + expect(result.current.user.id).toBe(userData.id); + expect(result.current.user.email).toBe(userData.email); }); it('Checks the reset user state matches default', () => { const { result } = renderHook(() => useDataStore()); @@ -73,8 +87,8 @@ describe('Data Store', () => { act(() => { result.current.updateUser( userData.documentId, - userData.userId, - userData.userEmail, + userData.id, + userData.email, userData.leagues, ); result.current.resetUser(); @@ -117,20 +131,20 @@ describe('Data Store', () => { userResults: { '123': { '456': { - 'entry1': { - teamName: 'New England Patriots', - correct:true - }, - 'entry2': { - teamName: 'Kansas City Chiefs', - correct:false + entry1: { + teamName: 'New England Patriots', + correct: true, + }, + entry2: { + teamName: 'Kansas City Chiefs', + correct: false, + }, + entry3: { + teamName: 'New England Patriots', + correct: false, + }, }, - 'entry3': { - teamName: 'New England Patriots', - correct:false - } - } - } + }, }, }; act(() => { @@ -143,27 +157,43 @@ describe('Data Store', () => { }); }); - describe('League Test', () => { + describe('All Leagues Test', () => { it('Check the default league state', () => { const { result } = renderHook(() => useDataStore()); - expect(result.current.league.leagueId).toBe(''); - expect(result.current.league.leagueName).toBe(''); - expect(result.current.league.logo).toBe(''); - expect(result.current.league.participants).toStrictEqual([]); - expect(result.current.league.survivors).toStrictEqual([]); + expect(result.current.allLeagues).toStrictEqual([]); }); it('Check the updated league state', () => { const { result } = renderHook(() => useDataStore()); act(() => { - result.current.updateLeague(league); + result.current.updateAllLeagues(league); }); - expect(result.current.league.leagueId).toBe(league.leagueId); - expect(result.current.league.leagueName).toBe(league.leagueName); - expect(result.current.league.logo).toBe(league.logo); - expect(result.current.league.participants).toBe(league.participants); - expect(result.current.league.survivors).toBe(league.survivors); + expect(result.current.allLeagues[0].leagueId).toBe(league[0].leagueId); + expect(result.current.allLeagues[0].leagueName).toBe( + league[0].leagueName, + ); + expect(result.current.allLeagues[0].logo).toBe(league[0].logo); + expect(result.current.allLeagues[0].participants).toBe( + league[0].participants, + ); + expect(result.current.allLeagues[0].survivors).toBe(league[0].survivors); + }); + }); + + describe('Entries Test', () => { + it('Check the default entries state', () => { + const { result } = renderHook(() => useDataStore()); + expect(result.current.entries).toStrictEqual([]); + }); + it('Check the updated entries state', () => { + const { result } = renderHook(() => useDataStore()); + + act(() => { + result.current.updateEntries(entries); + }); + + expect(result.current.entries).toStrictEqual(entries); }); }); @@ -192,10 +222,10 @@ xdescribe('getting all leagues test', () => { expect(result.current.allLeagues).toStrictEqual([]); }); it('check the updated allLeagues state', async () => { - const { result } = renderHook(() => useDataStore()); + const { result } = renderHook(() => useDataStore()); act(() => { result.current.updateAllLeagues(allLeagues); }); expect(result.current.allLeagues).toStrictEqual(allLeagues); - }) -}) + }); +}); diff --git a/store/dataStore.ts b/store/dataStore.ts index 1acc84e5..6ae95e92 100644 --- a/store/dataStore.ts +++ b/store/dataStore.ts @@ -10,6 +10,7 @@ import { ILeague, IGameWeek, } from '@/api/apiFunctions.interface'; +import { IEntry } from '@/app/(main)/league/[leagueId]/entry/Entries.interface'; //Define the shape of the state interface IDataStoreState { @@ -17,9 +18,10 @@ interface IDataStoreState { user: IUser; NFLTeams: INFLTeam[]; weeklyPicks: IWeeklyPicks; - league: ILeague; gameWeek: IGameWeek; + entries: IEntry[]; allLeagues: ILeague[]; + userLeagues: ILeague[]; } /* eslint-disable */ @@ -40,15 +42,10 @@ interface IDataStoreAction { gameWeekId, userResults, }: IWeeklyPicks) => void; - updateLeague: ({ - leagueId, - logo, - leagueName, - participants, - survivors, - }: ILeague) => void; updateGameWeek: (gameWeek: IGameWeek) => void; + updateEntries: (entries: IEntry[]) => void; updateAllLeagues: (allLeagues: ILeague[]) => void; + updateUserLeagues: (userLeagues: ILeague[]) => void; } /* eslint-disable */ @@ -69,18 +66,13 @@ const initialState: IDataStoreState = { gameWeekId: '', userResults: {}, }, - league: { - leagueId: '', - leagueName: '', - logo: '', - participants: [], - survivors: [], - }, gameWeek: { id: '', week: 0, }, + entries: [], allLeagues: [], + userLeagues: [], }; //create the store @@ -141,43 +133,29 @@ export const useDataStore = create((set) => ({ }), ), /** - * Update the game group + * Update the current week * @param props - props - * @param props.leagueId - The league id - * @param props.leagueName - The league name - * @param props.logo - The logo - * @param props.participants - The participants - * @param props.survivors - The survivors + * @param props.id - The id + * @param props.week - The week * @returns {void} */ - updateLeague: ({ - leagueId, - leagueName, - logo, - participants, - survivors, - }: ILeague): void => + updateGameWeek: ({ id, week }: IGameWeek): void => set( produce((state: IDataStoreState) => { - state.league.leagueId = leagueId; - state.league.leagueName = leagueName; - state.league.logo = logo; - state.league.participants = participants; - state.league.survivors = survivors; + state.gameWeek.id = id; + state.gameWeek.week = week; }), ), /** - * Update the current week + * Update the entries * @param props - props - * @param props.id - The id - * @param props.week - The week + * @param props.entries - all user entries from all leagues * @returns {void} */ - updateGameWeek: ({ id, week }: IGameWeek): void => + updateEntries: (entries: IEntry[]): void => set( produce((state: IDataStoreState) => { - state.gameWeek.id = id; - state.gameWeek.week = week; + state.entries = [...entries]; }), ), /** @@ -192,4 +170,26 @@ export const useDataStore = create((set) => ({ state.allLeagues = [...state.allLeagues, ...updatedLeagues]; }), ), + /** + * Updates user leagues in the data store. + * + * @param {ILeague[]} props - The league properties to update.. + * @returns {void} + */ + updateUserLeagues: (updatedUserLeagues: ILeague[]): void => + set( + produce((state: IDataStoreState) => { + // Create a Set of current league IDs to avoid duplicates + const existingLeagueIds = new Set( + state.userLeagues.map((league) => league.leagueId), + ); + + // Add only leagues that are not already in the state + const newLeagues = updatedUserLeagues.filter( + (league) => !existingLeagueIds.has(league.leagueId), + ); + + state.userLeagues = [...state.userLeagues, ...newLeagues]; + }), + ), })); diff --git a/utils/utils.ts b/utils/utils.ts index 51aee08e..5fc81181 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -127,6 +127,7 @@ export const parseUserPick = ( export const getUserLeagues = async ( leagues: IUser['leagues'], ): Promise => { + // TODO fix leagues.length === 0. If no leagues are found it returns an array with a length of 1 containing an empty string. if (!leagues || leagues.length === 0) { return []; }