diff --git a/convex/_model/fowV4/aggregateFowV4TournamentData.ts b/convex/_model/fowV4/aggregateFowV4TournamentData.ts index f2e80e01..c1154a8b 100644 --- a/convex/_model/fowV4/aggregateFowV4TournamentData.ts +++ b/convex/_model/fowV4/aggregateFowV4TournamentData.ts @@ -1,9 +1,6 @@ import { Id } from '../../_generated/dataModel'; import { QueryCtx } from '../../_generated/server'; import { getRange, Range } from '../common/_helpers/getRange'; -import { getMatchResultsByTournament } from '../matchResults/queries/getMatchResultsByTournament'; -import { getTournamentCompetitorsByTournament } from '../tournamentCompetitors'; -import { getTournamentPairings } from '../tournamentPairings'; import { createFowV4TournamentExtendedStatMap } from './createFowV4TournamentExtendedStatMap'; import { createTournamentCompetitorMetaMap } from './createTournamentCompetitorMetaMap'; import { divideFowV4BaseStats } from './divideFowV4BaseStats'; @@ -30,9 +27,15 @@ export const aggregateFowV4TournamentData = async ( range?: Range | number, ) => { // ---- 1. Gather base data ---- - const tournamentCompetitors = await getTournamentCompetitorsByTournament(ctx, { tournamentId }); // TODO: No reason to get them not-by-tournament - const tournamentPairings = await getTournamentPairings(ctx, { tournamentId }); - const matchResults = await getMatchResultsByTournament(ctx, { tournamentId }); + const tournamentCompetitors = await ctx.db.query('tournamentCompetitors') + .withIndex('by_tournament_id', (q) => q.eq('tournamentId', tournamentId)) + .collect(); + const tournamentPairings = await ctx.db.query('tournamentPairings') + .withIndex('by_tournament_id', (q) => q.eq('tournamentId', tournamentId)) + .collect(); + const matchResults = await ctx.db.query('matchResults') + .withIndex('by_tournament_id', (q) => q.eq('tournamentId', tournamentId)) + .collect(); // ---- End of async portion ---- @@ -40,7 +43,7 @@ export const aggregateFowV4TournamentData = async ( const tournamentCompetitorIds = tournamentCompetitors.map((c) => c._id); const tournamentPlayerIds = Array.from(new Set(tournamentCompetitors.reduce((acc, c) => [ ...acc, - ...c.players.map((p) => p.user._id), + ...c.players.map((p) => p.userId), ], [] as Id<'users'>[]))); // TODO: Replace the above with a re-usable function @@ -112,14 +115,14 @@ export const aggregateFowV4TournamentData = async ( // ---- 7. Compute stats for each competitor ---- for (const tournamentCompetitor of tournamentCompetitors) { const id = tournamentCompetitor._id; - const gamesPlayed = tournamentCompetitor.players.reduce((acc, { user }) => ( - acc + playerStats[user._id].gamesPlayed + const gamesPlayed = tournamentCompetitor.players.reduce((acc, { userId }) => ( + acc + playerStats[userId].gamesPlayed ), 0); - const total = sumFowV4BaseStats(tournamentCompetitor.players.map(({ user }) => ( - playerStats[user._id].total + const total = sumFowV4BaseStats(tournamentCompetitor.players.map(({ userId }) => ( + playerStats[userId].total ))); - const total_opponent = sumFowV4BaseStats(tournamentCompetitor.players.map(({ user }) => ( - playerStats[user._id].total_opponent + const total_opponent = sumFowV4BaseStats(tournamentCompetitor.players.map(({ userId }) => ( + playerStats[userId].total_opponent ))); competitorStats[id] = { total, diff --git a/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts b/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts index 23e6f08a..7b89c6cb 100644 --- a/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts +++ b/convex/_model/fowV4/extractFowV4MatchResultBaseStats.ts @@ -1,5 +1,4 @@ import { Doc } from '../../_generated/dataModel'; -import { DeepMatchResult } from '../matchResults'; import { calculateFowV4MatchResultScore } from './calculateFowV4MatchResultScore'; import { FowV4BaseStats } from './types'; @@ -10,7 +9,7 @@ import { FowV4BaseStats } from './types'; * @returns */ -export const extractFowV4MatchResultBaseStats = (matchResult: Doc<'matchResults'> | DeepMatchResult): [FowV4BaseStats, FowV4BaseStats] => { +export const extractFowV4MatchResultBaseStats = (matchResult: Doc<'matchResults'>): [FowV4BaseStats, FowV4BaseStats] => { const score = calculateFowV4MatchResultScore(matchResult); return [ { diff --git a/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts b/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts index 3b31c6b8..9436326a 100644 --- a/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts +++ b/convex/_model/matchResults/_helpers/checkMatchResultBattlePlanVisibility.ts @@ -2,8 +2,6 @@ import { getAuthUserId } from '@convex-dev/auth/server'; import { Doc } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; -import { getTournamentShallow } from '../../../_model/tournaments'; -import { deepenTournamentPairing } from '../../tournamentPairings'; /** * Checks if a match result's battle plans should be visible or not. @@ -23,17 +21,15 @@ export const checkMatchResultBattlePlanVisibility = async ( return true; } - const tournamentPairing = await ctx.db.get(matchResult.tournamentPairingId); - // If the match result's pairing has gone missing, treat it the same as a single match: + const tournamentPairing = await ctx.db.get(matchResult.tournamentPairingId); if (!tournamentPairing) { return true; } - const deepTournamentPairing = await deepenTournamentPairing(ctx, tournamentPairing); - const tournament = await getTournamentShallow(ctx, deepTournamentPairing.tournamentId); // If the match result is not from an on-going tournament, battle plans should be visible: - if (tournament?.status !== 'active') { + const tournament = await ctx.db.get(tournamentPairing.tournamentId); + if (!tournament || tournament?.status !== 'active') { return true; } @@ -45,7 +41,7 @@ export const checkMatchResultBattlePlanVisibility = async ( } // If the requesting user is a player within that pairing, battle plans should be visible: - if (deepTournamentPairing.playerUserIds.includes(userId)) { + if (matchResult.player0UserId === userId || matchResult.player1UserId === userId) { return true; } } diff --git a/convex/_model/matchResults/index.ts b/convex/_model/matchResults/index.ts index d8a0a47c..4e9ec3f0 100644 --- a/convex/_model/matchResults/index.ts +++ b/convex/_model/matchResults/index.ts @@ -44,6 +44,7 @@ export { } from './queries/getMatchResult'; export { getMatchResults, + getMatchResultsArgs, } from './queries/getMatchResults'; export { getMatchResultsByTournament, diff --git a/convex/_model/matchResults/queries/getMatchResults.ts b/convex/_model/matchResults/queries/getMatchResults.ts index 6381ab63..bc43fce5 100644 --- a/convex/_model/matchResults/queries/getMatchResults.ts +++ b/convex/_model/matchResults/queries/getMatchResults.ts @@ -1,11 +1,22 @@ +import { paginationOptsValidator, PaginationResult } from 'convex/server'; +import { Infer, v } from 'convex/values'; + import { QueryCtx } from '../../../_generated/server'; import { deepenMatchResult, DeepMatchResult } from '../_helpers/deepenMatchResult'; +export const getMatchResultsArgs = v.object({ + paginationOpts: paginationOptsValidator, +}); + export const getMatchResults = async ( ctx: QueryCtx, -): Promise => { - const matchResults = await ctx.db.query('matchResults').order('desc').collect(); - return await Promise.all(matchResults.map( - async (item) => await deepenMatchResult(ctx, item), - )); + args: Infer, +): Promise> => { + const results = await ctx.db.query('matchResults').order('desc').paginate(args.paginationOpts); + return { + ...results, + page: await Promise.all(results.page.map( + async (item) => await deepenMatchResult(ctx, item), + )), + }; }; diff --git a/convex/_model/tournaments/index.ts b/convex/_model/tournaments/index.ts index a4ba9dbc..a8d9e67f 100644 --- a/convex/_model/tournaments/index.ts +++ b/convex/_model/tournaments/index.ts @@ -21,6 +21,7 @@ export enum TournamentActionKey { StartRound = 'startRound', EndRound = 'endRound', End = 'end', + SubmitMatchResult = 'submitMatchResult', } // Helpers diff --git a/convex/matchResults.ts b/convex/matchResults.ts index 84788268..8d22aed1 100644 --- a/convex/matchResults.ts +++ b/convex/matchResults.ts @@ -7,7 +7,7 @@ export const getMatchResult = query({ }); export const getMatchResults = query({ - args: {}, + args: model.getMatchResultsArgs, handler: model.getMatchResults, }); diff --git a/src/components/MatchResultCreateDialog/MatchResultCreateDialog.hooks.tsx b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.hooks.tsx new file mode 100644 index 00000000..b6e35eb8 --- /dev/null +++ b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.hooks.tsx @@ -0,0 +1,3 @@ +import { useModal } from '~/modals'; + +export const useMatchResultCreateDialog = () => useModal('match-result-create-dialog'); diff --git a/src/components/MatchResultCreateDialog/MatchResultCreateDialog.module.scss b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.module.scss new file mode 100644 index 00000000..756547f5 --- /dev/null +++ b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.module.scss @@ -0,0 +1,9 @@ +@use "/src/style/flex"; + +.MatchResultCreateDialog { + @include flex.column($gap: 0); +} + +.Form { + padding: 1rem var(--modal-inner-gutter); +} diff --git a/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx new file mode 100644 index 00000000..b8c09d4f --- /dev/null +++ b/src/components/MatchResultCreateDialog/MatchResultCreateDialog.tsx @@ -0,0 +1,34 @@ +import { FowV4MatchResultForm } from '~/components/FowV4MatchResultForm'; +import { Button } from '~/components/generic/Button'; +import { + ControlledDialog, + DialogActions, + DialogHeader, +} from '~/components/generic/Dialog'; +import { ScrollArea } from '~/components/generic/ScrollArea'; +import { Separator } from '~/components/generic/Separator'; +import { useMatchResultCreateDialog } from './MatchResultCreateDialog.hooks'; + +import styles from './MatchResultCreateDialog.module.scss'; + +export const MatchResultCreateDialog = (): JSX.Element => { + const { id, close } = useMatchResultCreateDialog(); + return ( + + + + + + + + + + + + + ); +}; diff --git a/src/components/MatchResultCreateDialog/index.ts b/src/components/MatchResultCreateDialog/index.ts new file mode 100644 index 00000000..b84ace14 --- /dev/null +++ b/src/components/MatchResultCreateDialog/index.ts @@ -0,0 +1,2 @@ +export { MatchResultCreateDialog } from './MatchResultCreateDialog'; +export { useMatchResultCreateDialog } from './MatchResultCreateDialog.hooks'; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx index e2110bd4..95c847ed 100644 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx @@ -5,6 +5,7 @@ import { TournamentActionKey } from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { ConfirmationDialogData } from '~/components/ConfirmationDialog'; import { Warning } from '~/components/generic/Warning'; +import { useMatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; import { toast } from '~/components/ToastProvider'; import { useTournament } from '~/components/TournamentProvider'; import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; @@ -48,6 +49,7 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): const configureTournamentRound = (): void => { navigate(generatePath(PATHS.tournamentPairings, { id: tournament._id })); }; + const { open: openMatchResultCreateDialog } = useMatchResultCreateDialog(); const { mutation: deleteTournament } = useDeleteTournament({ onSuccess: (): void => { toast.success(`${tournament.title} deleted!`); @@ -171,6 +173,12 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): available: isOrganizer && isBetweenRounds && hasNextRound && (nextRoundPairings ?? []).length > 0, handler: () => startTournamentRound({ id: tournament._id }), }, + { + key: TournamentActionKey.SubmitMatchResult, + label: 'Submit Match Result', + available: !!openRound, + handler: () => openMatchResultCreateDialog(), + }, { key: TournamentActionKey.EndRound, label: `End Round ${currentRoundLabel}`, diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx index 0bf9d498..766596e1 100644 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; +import { MatchResultCreateDialog } from '~/components/MatchResultCreateDialog'; import { TournamentActionsContext } from './TournamentActionsProvider.context'; import { useActions } from './TournamentActionsProvider.hooks'; @@ -17,6 +18,7 @@ export const TournamentActionsProvider = ({ {children} + ); }; diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx index 207ec00b..320a7fcf 100644 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -30,8 +30,8 @@ export const DashboardPage = (): JSX.Element => { {Object.entries(tabs).map(([key, icon]) => ( -
-
diff --git a/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx index 20086ed2..9c95dfa5 100644 --- a/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx +++ b/src/pages/DashboardPage/components/MatchResultsCard/MatchResultsCard.tsx @@ -52,14 +52,12 @@ export const MatchResultsCard = ({ {(matchResults ?? []).length ? (
- {(matchResults ?? []).slice(0, 5).map((matchResult) => ( - + {(matchResults ?? []).map((matchResult) => ( + ))} - {/* {(tournaments ?? []).length > 5 && ( */}
- {/* )} */}
) : ( diff --git a/src/pages/MatchResultsPage/MatchResultsPage.module.scss b/src/pages/MatchResultsPage/MatchResultsPage.module.scss index a9712236..88c9cff2 100644 --- a/src/pages/MatchResultsPage/MatchResultsPage.module.scss +++ b/src/pages/MatchResultsPage/MatchResultsPage.module.scss @@ -29,4 +29,10 @@ grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); grid-template-rows: min-content; gap: 1rem; + + &_LoadMoreButton { + @include flex.centered; + + padding: 2rem; + } } diff --git a/src/pages/MatchResultsPage/MatchResultsPage.tsx b/src/pages/MatchResultsPage/MatchResultsPage.tsx index 79af2fc1..50abed37 100644 --- a/src/pages/MatchResultsPage/MatchResultsPage.tsx +++ b/src/pages/MatchResultsPage/MatchResultsPage.tsx @@ -23,7 +23,10 @@ export const MatchResultsPage = (): JSX.Element => { const showFilters = false; const showAddMatchResultButton = !!user; const showButtonText = useWindowWidth() > MIN_WIDTH_TABLET; - const { data: matchResults } = useGetMatchResults({}); + const { data: matchResults, loadMore } = useGetMatchResults({}); + const handleLoadMore = (): void => { + loadMore(10); + }; return ( {showFilters && ( @@ -51,6 +54,9 @@ export const MatchResultsPage = (): JSX.Element => { ))} +
+ +
{showAddMatchResultButton && ( diff --git a/src/services/matchResults.ts b/src/services/matchResults.ts index cac5b6d5..80dad147 100644 --- a/src/services/matchResults.ts +++ b/src/services/matchResults.ts @@ -1,9 +1,13 @@ import { api } from '~/api'; -import { createMutationHook, createQueryHook } from '~/services/utils'; +import { + createMutationHook, + createPaginatedQueryHook, + createQueryHook, +} from '~/services/utils'; // Basic Queries export const useGetMatchResult = createQueryHook(api.matchResults.getMatchResult); -export const useGetMatchResults = createQueryHook(api.matchResults.getMatchResults); +export const useGetMatchResults = createPaginatedQueryHook(api.matchResults.getMatchResults); // Special Queries export const useGetMatchResultsByTournament = createQueryHook(api.matchResults.getMatchResultsByTournament); diff --git a/src/services/utils/createPaginatedQueryHook.ts b/src/services/utils/createPaginatedQueryHook.ts new file mode 100644 index 00000000..b4ad9802 --- /dev/null +++ b/src/services/utils/createPaginatedQueryHook.ts @@ -0,0 +1,35 @@ +import { useRef } from 'react'; +import { usePaginatedQuery } from 'convex/react'; +import { + BetterOmit, + Expand, + FunctionArgs, + FunctionReference, +} from 'convex/server'; + +type QueryFn = FunctionReference<'query'>; + +export const createPaginatedQueryHook = (queryFn: T) => { + function isArgs(args: unknown): args is 'skip' | Expand, 'paginationOpts'>> { + return args !== 'skip' && args !== undefined && args !== null; + } + return (args: Omit | 'skip') => { + if (!isArgs(args)) { + return { + data: undefined, + loading: false, + loadMore: (_n: number) => undefined, + }; + } + const { results: data, isLoading, loadMore } = usePaginatedQuery(queryFn, args, { initialNumItems: 10 }); + const stored = useRef(data); + if (data !== undefined) { + stored.current = data; + } + return { + data: stored.current, + loading: isLoading || stored.current === undefined, + loadMore, + }; + }; +}; diff --git a/src/services/utils/index.ts b/src/services/utils/index.ts index 08b78b00..3cefc300 100644 --- a/src/services/utils/index.ts +++ b/src/services/utils/index.ts @@ -3,4 +3,5 @@ export { type MutationFn, type MutationHookConfig, } from './createMutationHook'; +export { createPaginatedQueryHook } from './createPaginatedQueryHook'; export { createQueryHook } from './createQueryHook';