diff --git a/api/apiFunctions.test.tsx b/api/apiFunctions.test.tsx index 7be6a988..33007e3d 100644 --- a/api/apiFunctions.test.tsx +++ b/api/apiFunctions.test.tsx @@ -5,6 +5,8 @@ import { resetPassword, resetRecoveredPassword, updateUserEmail, + getTotalEntries, + getAllLeagueEntries } from './apiFunctions'; import { IUser } from './apiFunctions.interface'; import { account, databases, ID } from './config'; @@ -497,4 +499,105 @@ describe('apiFunctions', () => { ); }); }); + + describe('getTotalEntries()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should return the total number of entries and alive entries in a league', async () => { + (databases.listDocuments as jest.Mock).mockResolvedValue({ + documents: [ + { + name: 'Entry 1', + user: '1234', + league: { + $id: 'league1', + survivors: ['123'], + participants: ['123', '1234'], + leagueName: 'League 1', + logo: '', + }, + eliminated: true, + selectedTeams: ['Browns', 'Bears'], + }, + { + name: 'Entry 2', + user: '1234', + league: { + $id: 'league1', + survivors: ['123'], + participants: ['123', '1234'], + leagueName: 'League 1', + logo: '', + }, + eliminated: false, + selectedTeams: ['Patriots', 'Eagles'], + }, + { + name: 'Entry 3', + user: '1234', + league: { + $id: 'league2', + survivors: ['123'], + participants: ['123', '1234'], + leagueName: 'League 2', + logo: '', + }, + eliminated: false, + selectedTeams: ['Bears', 'Cowboys'], + }, + ], + }); + + const leagues = ['league1', 'league2']; + + const result = await getTotalEntries({ leagues }); + + expect(result).toEqual([ + { + totalEntries: 2, + alive: 1, + }, + { + totalEntries: 1, + alive: 1, + }, + ]); + }); + }); + + describe('getAllLeagueEntries()', () => { + it('should return all the entry data for a given league', async () => { + (databases.listDocuments as jest.Mock).mockResolvedValue({ + documents: [ + { + name: 'Entry 1', + user: '1234', + league: { + $id: 'league1', + survivors: ['123'], + participants: ['123', '1234'], + leagueName: 'League 1', + logo: '', + }, + eliminated: true, + selectedTeams: ['Browns', 'Bears'], + }, + ], + }); + + const league = 'league1' + + const result = await getAllLeagueEntries({ leagueId: league }); + + expect(result).toEqual([ + { + entryName: 'Entry 1', + entryUser: '1234', + entrySelectedTeams: ['Browns', 'Bears'], + entryEliminated: true, + } + ]); + }); + }); }); diff --git a/api/apiFunctions.ts b/api/apiFunctions.ts index 2f3b072d..7308fa22 100644 --- a/api/apiFunctions.ts +++ b/api/apiFunctions.ts @@ -398,7 +398,6 @@ export async function createEntry({ } /** - * Update an entry * @param props - The entry data * @param props.entryId - The entry ID @@ -498,3 +497,81 @@ export async function addUserToLeague({ throw new Error('Error getting user document ID', { cause: error }); } } + +/** + * Grabs the length of entries in each league. + * @param props - Props being passed in. + * @param props.leagues - All user leagues. + * @returns {Promise} - Length of entries. + */ +export async function getTotalEntries({ + leagues, +}: { + leagues: string[]; +}): Promise<{ totalEntries: number; alive: number }[]> { + try { + const response = await databases.listDocuments( + appwriteConfig.databaseId, + Collection.ENTRIES, + [Query.limit(5000)], + ); + const entries = leagues.map((leagueId) => { + const leagueEntries = response.documents.filter( + (entry) => entry.league.$id === leagueId, + ); + const aliveEntries = leagueEntries.filter( + (aliveEntry) => aliveEntry.eliminated === false, + ); + + return { + totalEntries: leagueEntries.length, + alive: aliveEntries.length, + }; + }); + return entries; + } catch (error) { + throw error; + } +} + +/** + * To get all entry data for the player data table. + * @param props - Props being passed in. + * @param props.leagueId - ID of the league. + * @returns {Promise} - All the entries in that league. + */ +export async function getAllLeagueEntries({ + leagueId, +}: { + leagueId: string; +}): Promise< + { + entryName: string; + entryUser: string; + entrySelectedTeams: string[]; + entryEliminated: boolean; + }[] +> { + try { + const response = await databases.listDocuments( + appwriteConfig.databaseId, + Collection.ENTRIES, + [Query.limit(5000)], + ); + + const entries = response.documents.filter( + (entry) => entry.league.$id === leagueId, + ); + + const mappedEntries = entries.map((entry) => ({ + entryName: entry.name, + entryUser: entry.user, + entrySelectedTeams: entry.selectedTeams, + entryEliminated: entry.eliminated, + })); + + return mappedEntries; + } catch (error) { + throw error; + } +} diff --git a/app/(admin)/admin/leagues/page.test.tsx b/app/(admin)/admin/leagues/page.test.tsx index 467cfca8..9e690182 100644 --- a/app/(admin)/admin/leagues/page.test.tsx +++ b/app/(admin)/admin/leagues/page.test.tsx @@ -1,10 +1,46 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import AdminLeagues from './page'; +import { getUserLeagues } from '@/utils/utils'; +import { getTotalEntries } from '@/api/apiFunctions'; + +jest.mock('@/store/dataStore', () => ({ + useDataStore: jest.fn(() => ({ + user: { + documentId: '123', + id: '1234', + email: 'test@test.com', + leagues: ['league1', 'league2'], + }, + })), +})); + +jest.mock('@/utils/utils', () => ({ + getUserLeagues: jest.fn(() => Promise.resolve([])), + cn: jest.fn(), +})); + +jest.mock('@/api/apiFunctions', () => ({ + getTotalEntries: jest.fn(), +})); describe('AdminLeagues', () => { - it('should render the AdminLeagues page with LeagueCard components', () => { - render(); - const leagueCards = screen.getAllByTestId('LeagueCard'); - expect(leagueCards.length).toBe(3); + render(); + + const mockGetUserLeagues = getUserLeagues as jest.Mock; + const mockGetTotalEntries = getTotalEntries as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the League data table component', async () => { + mockGetTotalEntries.mockResolvedValue([]); + + mockGetUserLeagues.mockResolvedValue([]); + + await waitFor(() => { + const leagueTable = screen.getByTestId('data-table'); + expect(leagueTable).toBeInTheDocument(); + }); }); }); diff --git a/app/(admin)/admin/leagues/page.tsx b/app/(admin)/admin/leagues/page.tsx index dfd80f01..e7945f22 100644 --- a/app/(admin)/admin/leagues/page.tsx +++ b/app/(admin)/admin/leagues/page.tsx @@ -1,34 +1,48 @@ // Copyright (c) Gridiron Survivor. // Licensed under the MIT License. -import { JSX } from 'react'; -import { LeagueCard } from '@/components/LeagueCard/LeagueCard'; +'use client'; +import { getTotalEntries } from '@/api/apiFunctions'; +import { getUserLeagues } from '@/utils/utils'; +import { IEntryWithLeague } from '@/components/TableColumns/LeagueColumns/LeagueColumns.interface'; +import { JSX, useEffect, useState } from 'react'; +import { leagueColumns } from '@/components/TableColumns/LeagueColumns/LeagueColumns'; +import TableData from '@/components/TableData/TableData'; +import { useDataStore } from '@/store/dataStore'; /** * Renders the admin page. * @returns {JSX.Element} - The rendered Admin Leagues page. */ const AdminLeagues = (): JSX.Element => { + const [leaguesData, setLeaguesData] = useState([]); + const { user } = useDataStore((state) => state); + + /** + * Get all leagues the user is a part of. + */ + const fetchData = async (): Promise => { + const entries = await getTotalEntries({ leagues: user.leagues }); + const leagues = await getUserLeagues(user.leagues); + const combinedData = leagues.map((league, index) => ({ + aliveEntries: entries[index].alive, + leagueId: '', + leagueName: league.leagueName, + logo: '', + participants: league.participants, + survivors: league.survivors, + totalEntries: entries[index].totalEntries, + })); + setLeaguesData(combinedData); + }; + + useEffect(() => { + fetchData(); + }, [user.leagues]); + return (
- - - +
); }; diff --git a/app/(admin)/admin/players/page.test.tsx b/app/(admin)/admin/players/page.test.tsx index ba8483be..e5874e0f 100644 --- a/app/(admin)/admin/players/page.test.tsx +++ b/app/(admin)/admin/players/page.test.tsx @@ -1,15 +1,26 @@ -// /Users/ryanfurrer/Developer/GitHub/gridiron-survivor/app/(admin)/admin/notifications/page.test.tsx +import { render, screen, waitFor } from "@testing-library/react"; +import AdminPlayers from "./page"; +import { getAllLeagueEntries } from "@/api/apiFunctions"; -import { render, screen } from '@testing-library/react'; -import AdminPlayers from './page'; -import React from 'react'; +jest.mock('@/api/apiFunctions', () => ({ + getAllLeagueEntries: jest.fn(), +})); describe('Admin players page', () => { - it(`should render it's content`, () => { - render(); - const adminNotificationsContent = screen.getByTestId( - 'admin-players-content', - ); - expect(adminNotificationsContent).toBeInTheDocument(); + render(); + + const mockGetAllLeagueEntries = getAllLeagueEntries as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); }); + + it('should render the Player data table component', async () => { + mockGetAllLeagueEntries.mockResolvedValue({}); + + await waitFor(() => { + const playerTable = screen.getByTestId('data-table'); + expect(playerTable).toBeInTheDocument(); + }) + }) }); diff --git a/app/(admin)/admin/players/page.tsx b/app/(admin)/admin/players/page.tsx index d49399cc..402fda62 100644 --- a/app/(admin)/admin/players/page.tsx +++ b/app/(admin)/admin/players/page.tsx @@ -1,74 +1,40 @@ // Copyright (c) Gridiron Survivor. // Licensed under the MIT License. -import { JSX } from 'react'; +'use client'; +import { getAllLeagueEntries } from '@/api/apiFunctions'; +import { IPlayerEntryData } from '@/components/TableColumns/PlayerColumns/PlayerColumns.interface'; +import { playerColumns } from '@/components/TableColumns/PlayerColumns/PlayerColumns'; +import TableData from '@/components/TableData/TableData'; +import { JSX, useEffect, useState } from 'react'; /** * Renders the admin page. * @returns {JSX.Element} - The rendered Admin Players page. */ const AdminPlayers = (): JSX.Element => { + const [entryData, setEntryData] = useState([]); + /** + * Get all entries from the league provided. + */ + const fetchData = async (): Promise => { + const data = await getAllLeagueEntries({ + leagueId: '66e1cc9000160b10bf2c', + }); + + setEntryData(data); + }; + + useEffect(() => { + fetchData(); + }, []); + return (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NamePositionTeam
John DoeQuarterbackTeam A
Jane SmithRunning BackTeam B
Mike JohnsonWide ReceiverTeam C
Alice BrownTight EndTeam D
Bob WhiteLinebackerTeam E
Charlie GreenCornerbackTeam F
-
- - Page 1 of 10 - -
+
); }; diff --git a/components/Table/Table.tsx b/components/Table/Table.tsx index da30720e..f0b48d1d 100644 --- a/components/Table/Table.tsx +++ b/components/Table/Table.tsx @@ -12,6 +12,7 @@ const Table = React.forwardRef< ref={ref} className={cn('table-auto border border-border rounded-md', className)} {...props} + data-testid="data-table" /> )); Table.displayName = 'Table'; diff --git a/components/TableColumns/LeagueColumns/LeagueColumns.interface.ts b/components/TableColumns/LeagueColumns/LeagueColumns.interface.ts new file mode 100644 index 00000000..6ea48f6a --- /dev/null +++ b/components/TableColumns/LeagueColumns/LeagueColumns.interface.ts @@ -0,0 +1,9 @@ +// Copyright (c) Gridiron Survivor. +// Licensed under the MIT License. + +import { ILeague } from '@/api/apiFunctions.interface'; + +export interface IEntryWithLeague extends ILeague { + aliveEntries: number; + totalEntries: number; +} diff --git a/components/TableColumns/TableColumns.tsx b/components/TableColumns/LeagueColumns/LeagueColumns.tsx similarity index 67% rename from components/TableColumns/TableColumns.tsx rename to components/TableColumns/LeagueColumns/LeagueColumns.tsx index 67f5c4ef..5397e4a2 100644 --- a/components/TableColumns/TableColumns.tsx +++ b/components/TableColumns/LeagueColumns/LeagueColumns.tsx @@ -3,8 +3,7 @@ 'use client'; -import React, { JSX } from 'react'; -import { Button } from '../Button/Button'; +import { Button } from '../../Button/Button'; import { ColumnDef } from '@tanstack/react-table'; import { ChevronsUpDown, MoreHorizontal } from 'lucide-react'; import { @@ -12,14 +11,9 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from '../TableDropDownMenu/TableDropDownMenu'; - -export type LeagueHeader = { - text: string; - text2: string; - text3: string; - text4: string; -}; +} from '../../TableDropDownMenu/TableDropDownMenu'; +import { IEntryWithLeague } from './LeagueColumns.interface'; +import { JSX } from 'react'; export type LeagueDetailsHeader = { text: string; @@ -45,10 +39,10 @@ export const leagueDetailsColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text')}
, + cell: ({ row }) =>
{row.getValue('leagueName')}
, }, { - accessorKey: 'text2', + accessorKey: 'participants', /** * Value of row. @@ -74,10 +68,10 @@ export const leagueDetailsColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text2')}
, + cell: ({ row }) =>
{row.getValue('participants')}
, }, { - accessorKey: 'text3', + accessorKey: 'survivors', /** * Value of row. @@ -103,7 +97,7 @@ export const leagueDetailsColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text3')}
, + cell: ({ row }) =>
{row.getValue('survivors')}
, }, { accessorKey: 'text4', @@ -161,9 +155,9 @@ export const leagueDetailsColumns: ColumnDef[] = [ }, ]; -export const leagueColumns: ColumnDef[] = [ +export const leagueColumns: ColumnDef[] = [ { - accessorKey: 'text', + accessorKey: 'leagueName', header: 'League Name', /** * Value of row. @@ -171,10 +165,10 @@ export const leagueColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text')}
, + cell: ({ row }) => {row.getValue('leagueName')}, }, { - accessorKey: 'text2', + accessorKey: 'survivors', /** * Value of row. @@ -188,7 +182,7 @@ export const leagueColumns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - # of Participants + # of Survivors ); @@ -200,39 +194,24 @@ export const leagueColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text2')}
, - }, - { - accessorKey: 'text3', - - /** - * Value of row. - * @param {object} column - The column data. - * @param {object} column.column - The column definition - * @returns {JSX.Element} - The cell component. - */ - header: ({ column }): JSX.Element => { - return ( - - ); + cell: ({ row }): JSX.Element => { + const survivors = row.getValue('survivors') as string[]; + return {survivors.length}; }, - /** - * Value of row. - * @param {object} row - The row data. - * @param {object} row.row - The row definition - * @returns {JSX.Element} - The cell component. + * To be able to sort the row by numbers. + * @param rowA - Example 1 of survivors in a league. + * @param rowB - Example 2 of survivors in a league. + * @returns - Length of each participants league to be able to accurately sort. */ - cell: ({ row }) =>
{row.getValue('text3')}
, + sortingFn: (rowA, rowB): number => { + const lengthA = (rowA.getValue('survivors') as string[]).length; + const lengthB = (rowB.getValue('survivors') as string[]).length; + return lengthA - lengthB; + }, }, { - accessorKey: 'text4', + accessorKey: 'participants', /** * Value of row. @@ -246,7 +225,7 @@ export const leagueColumns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - # of Entries + # of Participants ); @@ -258,78 +237,24 @@ export const leagueColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text2')}
, - }, - { - id: 'actions', - - /** - * Admin action dropdown. - * @returns {JSX.Element} - The cell component. - */ - cell: (): JSX.Element => { - return ( - - - - - - View - Edit - Delete - - - ); + cell: ({ row }): JSX.Element => { + const participants = row.getValue('participants') as string[]; + return {participants.length}; }, - }, -]; - -export const playerColumns: ColumnDef[] = [ - { - accessorKey: 'text', - header: 'Picks Made', /** - * Value of row. - * @param {object} row - The row data. - * @param {object} row.row - The row definition - * @returns {JSX.Element} - The cell component. + * To be able to sort the row by numbers. + * @param rowA - Example 1 of participants in a league. + * @param rowB - Example 2 of participants in a league. + * @returns - Length of each participants league to be able to accurately sort. */ - cell: ({ row }) =>
{row.getValue('text')}
, - }, - { - accessorKey: 'text2', - - /** - * Value of row. - * @param {object} column - The column data. - * @param {object} column.column - The column definition - * @returns {JSX.Element} - The cell component. - */ - header: ({ column }): JSX.Element => { - return ( - - ); + sortingFn: (rowA, rowB): number => { + const lengthA = (rowA.getValue('participants') as string[]).length; + const lengthB = (rowB.getValue('participants') as string[]).length; + return lengthA - lengthB; }, - - /** - * Value of row. - * @param {object} row - The row data. - * @param {object} row.row - The row definition - * @returns {JSX.Element} - The cell component. - */ - cell: ({ row }) =>
{row.getValue('text2')}
, }, { - accessorKey: 'text3', + accessorKey: 'totalEntries', /** * Value of row. @@ -343,7 +268,7 @@ export const playerColumns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - Eliminated? + Total Entries ); @@ -355,10 +280,13 @@ export const playerColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text3')}
, + cell: ({ row }): JSX.Element => { + const totalEntries = row.getValue('totalEntries') as number; + return {totalEntries}; + }, }, { - accessorKey: 'text4', + accessorKey: 'aliveEntries', /** * Value of row. @@ -372,7 +300,7 @@ export const playerColumns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - Idk what to put here for header + Entries Still Alive ); @@ -384,7 +312,10 @@ export const playerColumns: ColumnDef[] = [ * @param {object} row.row - The row definition * @returns {JSX.Element} - The cell component. */ - cell: ({ row }) =>
{row.getValue('text2')}
, + cell: ({ row }): JSX.Element => { + const aliveEntries = row.getValue('aliveEntries') as number; + return {aliveEntries}; + }, }, { id: 'actions', diff --git a/components/TableColumns/PlayerColumns/PlayerColumns.interface.ts b/components/TableColumns/PlayerColumns/PlayerColumns.interface.ts new file mode 100644 index 00000000..dff27919 --- /dev/null +++ b/components/TableColumns/PlayerColumns/PlayerColumns.interface.ts @@ -0,0 +1,9 @@ +// Copyright (c) Gridiron Survivor. +// Licensed under the MIT License. + +export interface IPlayerEntryData { + entryName: string; + entryUser: string; + entrySelectedTeams: string[]; + entryEliminated: boolean; +} diff --git a/components/TableColumns/PlayerColumns/PlayerColumns.tsx b/components/TableColumns/PlayerColumns/PlayerColumns.tsx new file mode 100644 index 00000000..49fe6b89 --- /dev/null +++ b/components/TableColumns/PlayerColumns/PlayerColumns.tsx @@ -0,0 +1,143 @@ +// Copyright (c) Gridiron Survivor. +// Licensed under the MIT License. + +import { Button } from '../../Button/Button'; +import { ColumnDef } from '@tanstack/react-table'; +import { ChevronsUpDown, MoreHorizontal } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../../TableDropDownMenu/TableDropDownMenu'; +import { JSX } from 'react'; +import { IPlayerEntryData } from './PlayerColumns.interface'; + +export const playerColumns: ColumnDef[] = [ + { + accessorKey: 'entryUser', + header: 'User', + /** + * Value of row. + * @param {object} row - The row data. + * @param {object} row.row - The row definition + * @returns {JSX.Element} - The cell component. + */ + cell: ({ row }) => {row.getValue('entryUser')}, + }, + { + accessorKey: 'entryName', + + /** + * Value of row. + * @param {object} column - The column data. + * @param {object} column.column - The column definition + * @returns {JSX.Element} - The cell component. + */ + header: ({ column }): JSX.Element => { + return ( + + ); + }, + + /** + * Value of row. + * @param {object} row - The row data. + * @param {object} row.row - The row definition + * @returns {JSX.Element} - The cell component. + */ + cell: ({ row }) => {row.getValue('entryName')}, + }, + { + accessorKey: 'entrySelectedTeams', + + /** + * Value of row. + * @param {object} column - The column data. + * @param {object} column.column - The column definition + * @returns {JSX.Element} - The cell component. + */ + header: ({ column }): JSX.Element => { + return ( + + ); + }, + + /** + * Value of row. + * @param {object} row - The row data. + * @param {object} row.row - The row definition + * @returns {JSX.Element} - The cell component. + */ + cell: ({ row }) => {row.getValue('entrySelectedTeams')}, + }, + { + accessorKey: 'entryEliminated', + + /** + * Value of row. + * @param {object} column - The column data. + * @param {object} column.column - The column definition + * @returns {JSX.Element} - The cell component. + */ + header: ({ column }): JSX.Element => { + return ( + + ); + }, + + /** + * Value of row. + * @param {object} row - The row data. + * @param {object} row.row - The row definition + * @returns {JSX.Element} - The cell component. + */ + cell: ({ row }): JSX.Element => { + const eliminated = row.getValue('entryEliminated'); + return {eliminated ? 'True' : 'False'}; + }, + }, + { + id: 'actions', + + /** + * Admin action dropdown. + * @returns {JSX.Element} - The cell component. + */ + cell: (): JSX.Element => { + return ( + + + + + + View + Edit + Delete + + + ); + }, + }, +]; diff --git a/components/TableData/TableData.tsx b/components/TableData/TableData.tsx index 2c2b550f..04754314 100644 --- a/components/TableData/TableData.tsx +++ b/components/TableData/TableData.tsx @@ -66,21 +66,26 @@ const TableData = ({ ))} - {tableRows?.length ? ( - tableRows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + + {tableRows?.length ? ( + tableRows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + No results. - )) - ) : ( - - No results. - - )} + )} + ); };