diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 2409c14c..3ef653ab 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -113,6 +113,8 @@ import type * as _model_tournaments__helpers_checkTournamentAuth from "../_model import type * as _model_tournaments__helpers_checkTournamentVisibility from "../_model/tournaments/_helpers/checkTournamentVisibility.js"; import type * as _model_tournaments__helpers_deepenTournament from "../_model/tournaments/_helpers/deepenTournament.js"; import type * as _model_tournaments__helpers_getTournamentDeep from "../_model/tournaments/_helpers/getTournamentDeep.js"; +import type * as _model_tournaments__helpers_getTournamentNextRound from "../_model/tournaments/_helpers/getTournamentNextRound.js"; +import type * as _model_tournaments__helpers_getTournamentPlayerUserIds from "../_model/tournaments/_helpers/getTournamentPlayerUserIds.js"; import type * as _model_tournaments__helpers_getTournamentShallow from "../_model/tournaments/_helpers/getTournamentShallow.js"; import type * as _model_tournaments__helpers_getTournamentUserIds from "../_model/tournaments/_helpers/getTournamentUserIds.js"; import type * as _model_tournaments_fields from "../_model/tournaments/fields.js"; @@ -125,6 +127,7 @@ import type * as _model_tournaments_mutations_publishTournament from "../_model/ import type * as _model_tournaments_mutations_startTournament from "../_model/tournaments/mutations/startTournament.js"; import type * as _model_tournaments_mutations_startTournamentRound from "../_model/tournaments/mutations/startTournamentRound.js"; import type * as _model_tournaments_mutations_updateTournament from "../_model/tournaments/mutations/updateTournament.js"; +import type * as _model_tournaments_queries_getAvailableTournamentActions from "../_model/tournaments/queries/getAvailableTournamentActions.js"; import type * as _model_tournaments_queries_getTournament from "../_model/tournaments/queries/getTournament.js"; import type * as _model_tournaments_queries_getTournamentOpenRound from "../_model/tournaments/queries/getTournamentOpenRound.js"; import type * as _model_tournaments_queries_getTournamentRankings from "../_model/tournaments/queries/getTournamentRankings.js"; @@ -301,6 +304,8 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/_helpers/checkTournamentVisibility": typeof _model_tournaments__helpers_checkTournamentVisibility; "_model/tournaments/_helpers/deepenTournament": typeof _model_tournaments__helpers_deepenTournament; "_model/tournaments/_helpers/getTournamentDeep": typeof _model_tournaments__helpers_getTournamentDeep; + "_model/tournaments/_helpers/getTournamentNextRound": typeof _model_tournaments__helpers_getTournamentNextRound; + "_model/tournaments/_helpers/getTournamentPlayerUserIds": typeof _model_tournaments__helpers_getTournamentPlayerUserIds; "_model/tournaments/_helpers/getTournamentShallow": typeof _model_tournaments__helpers_getTournamentShallow; "_model/tournaments/_helpers/getTournamentUserIds": typeof _model_tournaments__helpers_getTournamentUserIds; "_model/tournaments/fields": typeof _model_tournaments_fields; @@ -313,6 +318,7 @@ declare const fullApi: ApiFromModules<{ "_model/tournaments/mutations/startTournament": typeof _model_tournaments_mutations_startTournament; "_model/tournaments/mutations/startTournamentRound": typeof _model_tournaments_mutations_startTournamentRound; "_model/tournaments/mutations/updateTournament": typeof _model_tournaments_mutations_updateTournament; + "_model/tournaments/queries/getAvailableTournamentActions": typeof _model_tournaments_queries_getAvailableTournamentActions; "_model/tournaments/queries/getTournament": typeof _model_tournaments_queries_getTournament; "_model/tournaments/queries/getTournamentOpenRound": typeof _model_tournaments_queries_getTournamentOpenRound; "_model/tournaments/queries/getTournamentRankings": typeof _model_tournaments_queries_getTournamentRankings; diff --git a/convex/_model/tournaments/_helpers/deepenTournament.ts b/convex/_model/tournaments/_helpers/deepenTournament.ts index 7096b60f..fa62f304 100644 --- a/convex/_model/tournaments/_helpers/deepenTournament.ts +++ b/convex/_model/tournaments/_helpers/deepenTournament.ts @@ -1,6 +1,7 @@ import { Doc, Id } from '../../../_generated/dataModel'; import { QueryCtx } from '../../../_generated/server'; import { getStorageUrl } from '../../common/_helpers/getStorageUrl'; +import { getTournamentNextRound } from './getTournamentNextRound'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ /** @@ -35,9 +36,6 @@ export const deepenTournament = async ( ...c.players.filter((p) => p.active).map((p) => p.userId), ], [] as Id<'users'>[]); - // Computed properties (easy to do, but used so frequently, it's nice to include them by default) - const nextRound = (tournament.currentRound ?? tournament.lastRound ?? -1) + 1; - return { ...tournament, logoUrl, @@ -49,7 +47,7 @@ export const deepenTournament = async ( activePlayerUserIds, maxPlayers : tournament.maxCompetitors * tournament.competitorSize, useTeams: tournament.competitorSize > 1, - nextRound: nextRound < tournament.roundCount ? nextRound : undefined, + nextRound: getTournamentNextRound(tournament), }; }; diff --git a/convex/_model/tournaments/_helpers/getTournamentNextRound.ts b/convex/_model/tournaments/_helpers/getTournamentNextRound.ts new file mode 100644 index 00000000..ac1af029 --- /dev/null +++ b/convex/_model/tournaments/_helpers/getTournamentNextRound.ts @@ -0,0 +1,14 @@ +import { Doc } from '../../../_generated/dataModel'; + +/** + * Gets the next round of a tournament. + * + * @param tournament - Raw Tournament document + * @returns The next round if there will be one, otherwise undefined + */ +export const getTournamentNextRound = ( + tournament: Doc<'tournaments'>, +): number | undefined => { + const nextRound = (tournament.currentRound ?? tournament.lastRound ?? -1) + 1; + return nextRound < tournament.roundCount ? nextRound : undefined; +}; diff --git a/convex/_model/tournaments/_helpers/getTournamentPlayerUserIds.ts b/convex/_model/tournaments/_helpers/getTournamentPlayerUserIds.ts new file mode 100644 index 00000000..0fdc4fcb --- /dev/null +++ b/convex/_model/tournaments/_helpers/getTournamentPlayerUserIds.ts @@ -0,0 +1,25 @@ +import { Id } from '../../../_generated/dataModel'; +import { QueryCtx } from '../../../_generated/server'; + +/** + * Get a list of user IDs for the players of a tournament. + * + * @param ctx - Convex query context + * @param tournamentId - ID of the tournament + * @param activeOnly - Limit to active players + * @returns Array of user IDs + */ +export const getTournamentPlayerUserIds = async ( + ctx: QueryCtx, + tournamentId: Id<'tournaments'>, + activeOnly = false, +): Promise[]> => { + const tournamentCompetitors = await ctx.db.query('tournamentCompetitors').withIndex( + 'by_tournament_id', + (q) => q.eq('tournamentId', tournamentId), + ).collect(); + return tournamentCompetitors.reduce((acc, c) => [ + ...acc, + ...c.players.filter((p) => activeOnly ? p.active : true).map((p) => p.userId), + ], [] as Id<'users'>[]); +}; diff --git a/convex/_model/tournaments/index.ts b/convex/_model/tournaments/index.ts index a8d9e67f..1b5d40ec 100644 --- a/convex/_model/tournaments/index.ts +++ b/convex/_model/tournaments/index.ts @@ -66,6 +66,10 @@ export { } from './mutations/updateTournament'; // Queries +export { + getAvailableTournamentActions, + getAvailableTournamentActionsArgs, +} from './queries/getAvailableTournamentActions'; export { getTournament, getTournamentArgs, diff --git a/convex/_model/tournaments/queries/getAvailableTournamentActions.ts b/convex/_model/tournaments/queries/getAvailableTournamentActions.ts new file mode 100644 index 00000000..e55fe23a --- /dev/null +++ b/convex/_model/tournaments/queries/getAvailableTournamentActions.ts @@ -0,0 +1,91 @@ +import { getAuthUserId } from '@convex-dev/auth/server'; +import { Infer, v } from 'convex/values'; + +import { QueryCtx } from '../../../_generated/server'; +import { TournamentActionKey } from '..'; +import { checkTournamentVisibility } from '../_helpers/checkTournamentVisibility'; +import { getTournamentNextRound } from '../_helpers/getTournamentNextRound'; +import { getTournamentPlayerUserIds } from '../_helpers/getTournamentPlayerUserIds'; + +export const getAvailableTournamentActionsArgs = v.object({ + id: v.id('tournaments'), +}); + +/** + * Gets a list of tournament actions which are available to a user. + * + * @param ctx - Convex query context + * @param args - Convex query args + * @param args.id - ID of the tournament + * @returns An array of TournamentActionKey(s) + */ +export const getAvailableTournamentActions = async ( + ctx: QueryCtx, + args: Infer, +): Promise => { + const tournament = await ctx.db.get(args.id); + if (!tournament) { + return []; + } + + // --- CHECK AUTH ---- + const userId = await getAuthUserId(ctx); + if (!(await checkTournamentVisibility(ctx, tournament))) { + return []; + } + + // ---- GATHER DATA ---- + const nextRound = getTournamentNextRound(tournament); + const nextRoundPairings = await ctx.db.query('tournamentPairings') + .withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.id)) + .collect(); + const nextRoundPairingCount = (nextRoundPairings ?? []).length; + const playerUserIds = await getTournamentPlayerUserIds(ctx, tournament._id); + + const isOrganizer = !!userId && tournament.organizerUserIds.includes(userId); + const isPlayer = !!userId && playerUserIds.includes(userId); + + const hasCurrentRound = tournament.currentRound !== undefined; + const hasNextRound = nextRound !== undefined; + + // ---- PRIMARY ACTIONS ---- + const actions: TournamentActionKey[] = []; + + if (isOrganizer && ['draft', 'published'].includes(tournament.status)) { + actions.push(TournamentActionKey.Edit); + } + + if (isOrganizer && tournament.status === 'draft') { + actions.push(TournamentActionKey.Delete); + } + + if (isOrganizer && tournament.status === 'draft') { + actions.push(TournamentActionKey.Publish); + } + + if (isOrganizer && tournament.status === 'published') { // TODO: Check for at least 2 competitors + actions.push(TournamentActionKey.Start); + } + + if (isOrganizer && !hasCurrentRound && hasNextRound && nextRoundPairingCount === 0) { + actions.push(TournamentActionKey.ConfigureRound); + } + + if (isOrganizer && !hasCurrentRound && hasNextRound && nextRoundPairingCount > 0) { + actions.push(TournamentActionKey.StartRound); + } + + if ((isOrganizer || isPlayer) && hasCurrentRound) { // TODO: Don't show if all matches checked in + actions.push(TournamentActionKey.SubmitMatchResult); + } + + if (isOrganizer && hasCurrentRound) { + actions.push(TournamentActionKey.EndRound); + } + + if (isOrganizer && !hasCurrentRound) { + actions.push(TournamentActionKey.End); + } + + return actions; +}; diff --git a/convex/tournaments.ts b/convex/tournaments.ts index 9c50a783..69f72f22 100644 --- a/convex/tournaments.ts +++ b/convex/tournaments.ts @@ -31,6 +31,11 @@ export const getTournamentRankings = query({ handler: model.getTournamentRankings, }); +export const getAvailableTournamentActions = query({ + args: model.getAvailableTournamentActionsArgs, + handler: model.getAvailableTournamentActions, +}); + export const createTournament = mutation({ args: model.createTournamentArgs, handler: model.createTournament, diff --git a/package-lock.json b/package-lock.json index 56300639..71c5addf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "combat-command", - "version": "0.0.0", + "version": "1.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "combat-command", - "version": "0.0.0", + "version": "1.8.5", "dependencies": { "@convex-dev/auth": "^0.0.80", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index d961a5d3..9d42ef45 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "combat-command", "private": true, - "version": "0.0.0", + "version": "1.8.5", "type": "module", "scripts": { "dev": "npm-run-all --parallel dev:frontend dev:backend", diff --git a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.hooks.ts b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.hooks.ts index 243b5356..c1592c2a 100644 --- a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.hooks.ts +++ b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.hooks.ts @@ -1,6 +1,3 @@ -import { useQuery } from 'convex/react'; - -import { api } from '~/api'; import { Identity } from '~/components/IdentityBadge'; import { getUserDisplayNameString } from '~/utils/common/getUserDisplayNameString'; import { FowV4MatchResultDetailsData } from './FowV4MatchResultDetails.types'; @@ -15,14 +12,9 @@ export const usePlayerName = ( identity: Identity, loading?: boolean, ): UserPlayerNameResult => { - const { user, userId, placeholder } = identity; - - // TODO: Replace with a service hook - const queryUser = useQuery(api.users.getUser, userId ? { - id: userId, - } : 'skip'); + const { user, placeholder } = identity; - if (loading || (userId && !queryUser)) { + if (loading) { return { loading: true, }; @@ -34,13 +26,6 @@ export const usePlayerName = ( displayName: getUserDisplayNameString(user), }; } - - if (userId && queryUser) { - return { - loading: false, - displayName: getUserDisplayNameString(queryUser), - }; - } if (placeholder) { return { @@ -62,14 +47,12 @@ export const usePlayerNames = ( ): UserPlayerNamesResult => { const { displayName: player0DisplayName, loading: player0Loading } = usePlayerName({ user: matchResult.player0User, - userId: matchResult.player0UserId, placeholder: matchResult.player0Placeholder ? { displayName: matchResult.player0Placeholder, } : undefined, }); const { displayName: player1DisplayName, loading: player1Loading } = usePlayerName({ user: matchResult.player1User, - userId: matchResult.player1UserId, placeholder: matchResult.player1Placeholder ? { displayName: matchResult.player1Placeholder, } : undefined, diff --git a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.types.ts b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.types.ts index 4ecf00f2..fe059936 100644 --- a/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.types.ts +++ b/src/components/FowV4MatchResultDetails/FowV4MatchResultDetails.types.ts @@ -1,6 +1,6 @@ import { MatchResult, User } from '~/api'; -export type FowV4MatchResultDetailsData = Pick & { +export type FowV4MatchResultDetailsData = Pick & { player0User?: User; player1User?: User; details: Omit & { diff --git a/src/components/IdentityBadge/IdentityBadge.hooks.tsx b/src/components/IdentityBadge/IdentityBadge.hooks.tsx index a38de72e..714ff005 100644 --- a/src/components/IdentityBadge/IdentityBadge.hooks.tsx +++ b/src/components/IdentityBadge/IdentityBadge.hooks.tsx @@ -4,8 +4,6 @@ import { Ghost, HelpCircle } from 'lucide-react'; import { TournamentCompetitor } from '~/api'; import { Avatar } from '~/components/generic/Avatar'; import { FlagCircle } from '~/components/generic/FlagCircle'; -import { useGetTournamentCompetitor } from '~/services/tournamentCompetitors'; -import { useGetUser } from '~/services/users'; import { getCountryName } from '~/utils/common/getCountryName'; import { getUserDisplayNameString } from '~/utils/common/getUserDisplayNameString'; import { Identity } from './IdentityBadge.types'; @@ -39,13 +37,10 @@ const getCompetitorDisplayName = (competitor: TournamentCompetitor): ReactElemen }; export const useIdentityElements = (identity: Identity, loading?: boolean): ReactElement[] => { - const { user, userId, competitor, competitorId, placeholder } = identity; + const { user, competitor, placeholder } = identity; - const { data: queryUser } = useGetUser(userId ? { id: userId } : 'skip'); - const { data: queryCompetitor } = useGetTournamentCompetitor(competitorId ? { id: competitorId } : 'skip'); - - // Render loading skeleton if explicitly loading or an ID was provided and it is fetching - if (loading || (userId && !queryUser) || competitorId && !queryCompetitor) { + // Render loading skeleton if explicitly loading + if (loading) { return [ , Loading..., @@ -59,13 +54,6 @@ export const useIdentityElements = (identity: Identity, loading?: boolean): Reac ]; } - if (userId && queryUser) { - return [ - , - {getUserDisplayNameString(queryUser)}, - ]; - } - if (competitor) { return [ getCompetitorAvatar(competitor), @@ -73,13 +61,6 @@ export const useIdentityElements = (identity: Identity, loading?: boolean): Reac ]; } - if (competitorId && queryCompetitor) { - return [ - getCompetitorAvatar(queryCompetitor), - getCompetitorDisplayName(queryCompetitor), - ]; - } - if (placeholder) { return [ } muted />, diff --git a/src/components/IdentityBadge/IdentityBadge.tsx b/src/components/IdentityBadge/IdentityBadge.tsx index 41333e89..9b180822 100644 --- a/src/components/IdentityBadge/IdentityBadge.tsx +++ b/src/components/IdentityBadge/IdentityBadge.tsx @@ -1,12 +1,7 @@ import { cloneElement } from 'react'; import clsx from 'clsx'; -import { - TournamentCompetitor, - TournamentCompetitorId, - User, - UserId, -} from '~/api'; +import { TournamentCompetitor, User } from '~/api'; import { ElementSize } from '~/types/componentLib'; import { useIdentityElements } from './IdentityBadge.hooks'; import { IdentityBadgePlaceholder } from './IdentityBadge.types'; @@ -23,32 +18,26 @@ const sizeClasses: Record = { export interface IdentityBadgeProps { className?: string; competitor?: TournamentCompetitor; - competitorId?: TournamentCompetitorId; flipped?: boolean; loading?: boolean; placeholder?: IdentityBadgePlaceholder; size?: ElementSize; user?: User; - userId?: UserId; } export const IdentityBadge = ({ className, competitor, - competitorId, flipped = false, loading = false, placeholder, size = 'normal', user, - userId, }: IdentityBadgeProps): JSX.Element | null => { const [displayAvatar, displayName] = useIdentityElements({ user, competitor, placeholder, - competitorId, - userId, }, loading); const elements = [ cloneElement(displayAvatar, { className: styles.IdentityBadge_Avatar, key: 'avatar' }), diff --git a/src/components/IdentityBadge/IdentityBadge.types.ts b/src/components/IdentityBadge/IdentityBadge.types.ts index e7bacc00..904f9743 100644 --- a/src/components/IdentityBadge/IdentityBadge.types.ts +++ b/src/components/IdentityBadge/IdentityBadge.types.ts @@ -1,23 +1,10 @@ import { ReactElement } from 'react'; -import { - TournamentCompetitor, - TournamentCompetitorId, - User, - UserId, -} from '~/api'; - -export type IdentityBadgeInput = { - user?: User; - competitor?: TournamentCompetitor; - placeholder?: IdentityBadgePlaceholder; -}; +import { TournamentCompetitor, User } from '~/api'; export type Identity = { - userId?: UserId; user?: User; competitor?: TournamentCompetitor; - competitorId?: TournamentCompetitorId; placeholder?: IdentityBadgePlaceholder; }; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx index 7d510865..fe668e5f 100644 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx @@ -2,18 +2,17 @@ import { useContext } from 'react'; import { generatePath, useNavigate } from 'react-router-dom'; 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'; -import { useGetTournamentPairings } from '~/services/tournamentPairings'; import { useDeleteTournament, useEndTournament, useEndTournamentRound, + useGetAvailableTournamentActions, useGetTournamentOpenRound, usePublishTournament, useStartTournament, @@ -37,11 +36,9 @@ export const useTournamentActions = () => { type ActionDefinition = Action & { key: TournamentActionKey; - available: boolean; }; export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): TournamentActions => { - const user = useAuth(); const tournament = useTournament(); // ---- HANDLERS ---- @@ -88,9 +85,8 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): }); // ---- DATA ---- - const { data: nextRoundPairings } = useGetTournamentPairings({ - tournamentId: tournament._id, - round: tournament.nextRound, + const { data: availableActions } = useGetAvailableTournamentActions({ + id: tournament._id, }); const { data: openRound } = useGetTournamentOpenRound({ id: tournament._id, @@ -98,10 +94,6 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ tournamentId: tournament._id, }); - const isOrganizer = !!user && tournament.organizerUserIds.includes(user._id); - const isPlayer = !!user && tournament.playerUserIds.includes(user._id); - const isBetweenRounds = tournament.status === 'active' && !openRound; - const hasNextRound = tournament.nextRound !== undefined; // Labels for messages: const nextRoundLabel = (tournament.nextRound ?? 0) + 1; @@ -113,13 +105,11 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.Edit, label: 'Edit', - available: isOrganizer && ['draft', 'published'].includes(tournament.status), handler: () => navigate(generatePath(PATHS.tournamentEdit, { id: tournament._id })), }, { key: TournamentActionKey.Delete, label: 'Delete', - available: isOrganizer && tournament.status === 'draft', handler: () => { // TODO: Implement confirmation dialog deleteTournament({ id: tournament._id }); @@ -128,7 +118,6 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.Publish, label: 'Publish', - available: isOrganizer && tournament.status === 'draft', handler: () => { // TODO: Implement confirmation dialog publishTournament({ id: tournament._id }); @@ -137,7 +126,6 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.Start, label: 'Start', - available: isOrganizer && tournament.status === 'published', handler: () => { // TODO: Implement confirmation dialog startTournament({ id: tournament._id }); @@ -146,7 +134,6 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.ConfigureRound, label: `Configure Round ${nextRoundLabel}`, - available: isOrganizer && isBetweenRounds && hasNextRound && nextRoundPairings?.length === 0, handler: () => { const { errors, warnings } = validateConfigureRound(tournament, tournamentCompetitors); if (errors.length) { @@ -171,19 +158,16 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.StartRound, label: `Start Round ${nextRoundLabel}`, - available: isOrganizer && isBetweenRounds && hasNextRound && (nextRoundPairings ?? []).length > 0, handler: () => startTournamentRound({ id: tournament._id }), }, { key: TournamentActionKey.SubmitMatchResult, label: 'Submit Match Result', - available: !!openRound && (isOrganizer || isPlayer), handler: () => openMatchResultCreateDialog(), }, { key: TournamentActionKey.EndRound, label: `End Round ${currentRoundLabel}`, - available: isOrganizer && !!openRound, handler: () => { if (openRound && openRound.matchResultsProgress.remaining > 0) { openDialog({ @@ -209,7 +193,6 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): { key: TournamentActionKey.End, label: 'End Tournament', - available: isOrganizer && isBetweenRounds, handler: () => { if (tournament.nextRound !== undefined && tournament.nextRound < tournament.roundCount) { openDialog({ @@ -231,7 +214,7 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): }, ]; - return actions.filter(({ available }) => available).reduce((acc, { key, ...action }) => ({ + return actions.filter(({ key }) => (availableActions ?? []).includes(key)).reduce((acc, { key, ...action }) => ({ ...acc, [key]: action, }), {} as TournamentActions); diff --git a/src/components/TournamentPairingRow/TournamentPairingRow.tsx b/src/components/TournamentPairingRow/TournamentPairingRow.tsx index 37d1aeb5..c07e4032 100644 --- a/src/components/TournamentPairingRow/TournamentPairingRow.tsx +++ b/src/components/TournamentPairingRow/TournamentPairingRow.tsx @@ -1,15 +1,17 @@ import { useWindowWidth } from '@react-hook/window-size/throttled'; import clsx from 'clsx'; -import { DraftTournamentPairing, TournamentPairing } from '~/api'; +import { TournamentPairing } from '~/api'; import { IdentityBadge } from '~/components/IdentityBadge'; import { MOBILE_BREAKPOINT } from '~/settings'; import { getIdentityBadgeProps } from './TournamentPairingRow.utils'; import styles from './TournamentPairingRow.module.scss'; +export type TournamentPairingRowData = Pick; + export interface TournamentPairingRowProps { - pairing?: TournamentPairing | DraftTournamentPairing; + pairing?: TournamentPairingRowData; loading?: boolean; className?: string; } diff --git a/src/components/TournamentPairingRow/TournamentPairingRow.utils.tsx b/src/components/TournamentPairingRow/TournamentPairingRow.utils.tsx index 9e60cfff..ea8d7ad4 100644 --- a/src/components/TournamentPairingRow/TournamentPairingRow.utils.tsx +++ b/src/components/TournamentPairingRow/TournamentPairingRow.utils.tsx @@ -1,49 +1,22 @@ import { ChevronRight } from 'lucide-react'; -import { DraftTournamentPairing, TournamentPairing } from '~/api'; +import { TournamentPairing } from '~/api'; import { IdentityBadgeProps } from '~/components/IdentityBadge'; -import { TournamentPairingFormItem } from '~/pages/TournamentPairingsPage/TournamentPairingsPage.schema'; - -export function isUnassignedPairingInput(pairing: unknown): pairing is DraftTournamentPairing { - return typeof pairing === 'object' && - pairing !== null && - 'tournamentCompetitor0Id' in pairing; -} - -export function isTournamentPairing(pairing: unknown): pairing is TournamentPairing { - return typeof pairing === 'object' && - pairing !== null && - 'tournamentCompetitor0' in pairing; -} export const getIdentityBadgeProps = ( - pairing?: TournamentPairing | TournamentPairingFormItem | DraftTournamentPairing, + pairing?: Pick, ): [Partial, Partial] => { - if (isUnassignedPairingInput(pairing)) { - if (pairing.tournamentCompetitor1Id) { - return [ - { competitorId: pairing.tournamentCompetitor0Id }, - { competitorId: pairing.tournamentCompetitor1Id }, - ]; - } - return [ - { competitorId: pairing.tournamentCompetitor0Id }, - { placeholder: { displayName: 'Bye', icon: } }, - ]; + if (!pairing) { + return [{}, {}]; } - - if (isTournamentPairing(pairing)) { - if (pairing.tournamentCompetitor1) { - return [ - { competitor: pairing.tournamentCompetitor0 }, - { competitor: pairing.tournamentCompetitor1 }, - ]; - } + if (pairing.tournamentCompetitor1) { return [ { competitor: pairing.tournamentCompetitor0 }, - { placeholder: { displayName: 'Bye', icon: } }, + { competitor: pairing.tournamentCompetitor1 }, ]; } - - return [{}, {}]; + return [ + { competitor: pairing.tournamentCompetitor0 }, + { placeholder: { displayName: 'Bye', icon: } }, + ]; }; diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx index 6b76880b..ffe80774 100644 --- a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.tsx @@ -15,6 +15,7 @@ import { TournamentActionsProvider } from '~/components/TournamentActionsProvide import { TournamentContextMenu } from '~/components/TournamentContextMenu'; import { TournamentProvider } from '~/components/TournamentProvider'; import { TournamentTimer } from '~/components/TournamentTimer'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; import { PATHS } from '~/settings'; import { Header } from '../Header'; import { @@ -41,6 +42,11 @@ export const ActiveTournament = ({ const navigate = useNavigate(); const user = useAuth(); + const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ + tournamentId: tournament._id, + includeRankings: tournament.lastRound, + }); + const opponent = getOpponent(user?._id, pairing); const handleViewMore = (): void => { @@ -103,7 +109,7 @@ export const ActiveTournament = ({ )} - {showRankings && renderRankings(tournament, rankedCompetitors)} + {showRankings && renderRankings(tournament, tournamentCompetitors)} ); diff --git a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx index 3ca2ca41..3c0a9e8b 100644 --- a/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx +++ b/src/pages/DashboardPage/components/ActiveTournament/ActiveTournament.utils.tsx @@ -4,7 +4,6 @@ import { Trophy } from 'lucide-react'; import { Tournament, TournamentCompetitor, - TournamentCompetitorRanked, TournamentPairing, UserId, } from '~/api'; @@ -50,7 +49,7 @@ export const renderTitle = (title: string): ReactElement => ( export const renderRankings = ( tournament: Tournament, - competitors: TournamentCompetitorRanked[] = [], + competitors: TournamentCompetitor[] = [], ): ReactElement => ( !competitors.length ? ( } /> @@ -65,19 +64,19 @@ export const renderRankings = ( label: 'Rank', width: 40, align: 'center', - renderCell: (r) => ( + renderCell: ({ rank }) => (
- {tournament.lastRound !== undefined ? r.rank + 1 : '-'} + {tournament.lastRound !== undefined && rank !== undefined ? rank + 1 : '-'}
), }, { key: 'identity', label: tournament?.useTeams ? 'Team' : 'Player', - renderCell: (r) => ( + renderCell: (competitor) => ( ), diff --git a/src/pages/TournamentPairingDetailPage/TournamentPairingDetailPage.tsx b/src/pages/TournamentPairingDetailPage/TournamentPairingDetailPage.tsx index 83fd3c7d..2f8ae5d0 100644 --- a/src/pages/TournamentPairingDetailPage/TournamentPairingDetailPage.tsx +++ b/src/pages/TournamentPairingDetailPage/TournamentPairingDetailPage.tsx @@ -35,7 +35,7 @@ export const TournamentPairingDetailPage = (): JSX.Element => { const isPlayer = user && tournamentPairing?.playerUserIds.includes(user._id); const isOrganizer = user && tournament?.organizerUserIds.includes(user._id); const isComplete = tournamentPairing && tournamentPairing.matchResultsProgress.submitted === tournamentPairing?.matchResultsProgress.required; - const showAddMatchResult = (isPlayer || isOrganizer) && !isComplete; + const showAddMatchResult = (isPlayer || isOrganizer) && !isComplete && tournament?.currentRound !== undefined; return ( - +
{rankedCompetitor.rank !== undefined ? rankedCompetitor.rank + 1 : '-'}
diff --git a/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx b/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx index fdf0efb9..a11c38e8 100644 --- a/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx +++ b/src/pages/TournamentPairingsPage/components/ConfirmPairingsDialog/ConfirmPairingsDialog.tsx @@ -1,11 +1,10 @@ import { DraftTournamentPairing, TournamentCompetitor } from '~/api'; import { ConfirmationDialog } from '~/components/ConfirmationDialog'; -import { ColumnDef, Table } from '~/components/generic/Table'; +import { Table } from '~/components/generic/Table'; import { Warning } from '~/components/generic/Warning'; -import { TournamentPairingRow } from '~/components/TournamentPairingRow'; import { useTournament } from '~/components/TournamentProvider'; import { TournamentPairingFormItem } from '../../TournamentPairingsPage.schema'; -import { assignTables } from './ConfirmPairingsDialog.utils'; +import { assignTables, getTableColumns } from './ConfirmPairingsDialog.utils'; import styles from './ConfirmPairingsDialog.module.scss'; @@ -38,30 +37,7 @@ export const ConfirmPairingsDialog = ({ onConfirm(assignedPairings); }; - const columns: ColumnDef[] = [ - { - key: 'table', - label: 'Table', - width: 40, - align: 'center', - renderCell: (r) => ( -
- {r.table === null ? '-' : r.table + 1} -
- ), - }, - { - key: 'pairing', - label: 'Pairing', - align: 'center', - renderCell: (r) => ( - - ), - }, - ]; + const columns = getTableColumns(competitors); return ( [] => [ + { + key: 'table', + label: 'Table', + width: 40, + align: 'center', + renderCell: (r) => ( +
+ {r.table === null ? '-' : r.table + 1} +
+ ), + }, + { + key: 'pairing', + label: 'Pairing', + align: 'center', + renderCell: (r) => { + const tournamentCompetitor0 = competitors.find((c) => c._id === r.tournamentCompetitor0Id) ?? null; + const tournamentCompetitor1 = competitors.find((c) => c._id === r.tournamentCompetitor1Id) ?? null; + if (!tournamentCompetitor0) { + return null; + } + return ( + + ); + }, + }, +]; + export const assignTables = ( pairings: (TournamentPairingFormItem & { playedTables: (number | null)[]; diff --git a/src/services/tournaments.ts b/src/services/tournaments.ts index a7c18a48..4d3480cc 100644 --- a/src/services/tournaments.ts +++ b/src/services/tournaments.ts @@ -6,6 +6,7 @@ export const useGetTournament = createQueryHook(api.tournaments.getTournament); export const useGetTournaments = createQueryHook(api.tournaments.getTournaments); // Special Queries +export const useGetAvailableTournamentActions = createQueryHook(api.tournaments.getAvailableTournamentActions); export const useGetTournamentOpenRound = createQueryHook(api.tournaments.getTournamentOpenRound); export type TournamentOpenRound = typeof api.tournaments.getTournamentOpenRound._returnType; // TODO: Move to back-end export const useGetTournamentRankings = createQueryHook(api.tournaments.getTournamentRankings);