From de969c80a98918f63bed1098ef6f15e498940473 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 10 Jul 2025 07:49:56 +0200 Subject: [PATCH 1/8] refactor: Improve tournament actions --- .../tournaments/_helpers/deepenTournament.ts | 14 +- convex/_model/tournaments/index.ts | 11 + .../queries/getTournamentOpenRound.ts | 1 + src/api.ts | 1 + .../ConfirmationDialog.hooks.ts | 7 +- .../ConfirmationDialog/ConfirmationDialog.tsx | 24 +- .../ConfirmationDialog.types.ts | 19 ++ src/components/ConfirmationDialog/index.ts | 5 +- .../ToastProvider/ToastProvider.store.ts | 2 +- .../TournamentActionsProvider.context.ts | 12 + .../TournamentActionsProvider.hooks.tsx | 229 ++++++++++++++++++ .../TournamentActionsProvider.tsx | 22 ++ .../TournamentActionsProvider/index.ts | 2 + .../utils/validateConfigureRound.tsx | 66 +++++ .../TournamentContextMenu.tsx | 150 +----------- .../TournamentContextMenu.utils.ts | 10 - .../ConfirmConfigureRoundDialog.module.scss | 5 - .../ConfirmConfigureRoundDialog.tsx | 95 -------- .../ConfirmConfigureRoundDialog.utils.tsx | 41 ---- .../ConfirmConfigureRoundDialog/index.ts | 5 - src/components/TournamentContextMenu/index.ts | 4 - .../Dialog/DialogDescription.module.scss | 4 + .../TournamentDetailPage.tsx | 71 +++--- .../TournamentPairingsCard.tsx | 31 +-- 24 files changed, 448 insertions(+), 383 deletions(-) create mode 100644 src/components/ConfirmationDialog/ConfirmationDialog.types.ts create mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts create mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx create mode 100644 src/components/TournamentActionsProvider/TournamentActionsProvider.tsx create mode 100644 src/components/TournamentActionsProvider/index.ts create mode 100644 src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx delete mode 100644 src/components/TournamentContextMenu/TournamentContextMenu.utils.ts delete mode 100644 src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.module.scss delete mode 100644 src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.tsx delete mode 100644 src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.utils.tsx delete mode 100644 src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/index.ts diff --git a/convex/_model/tournaments/_helpers/deepenTournament.ts b/convex/_model/tournaments/_helpers/deepenTournament.ts index e58a7098..7096b60f 100644 --- a/convex/_model/tournaments/_helpers/deepenTournament.ts +++ b/convex/_model/tournaments/_helpers/deepenTournament.ts @@ -36,22 +36,20 @@ export const deepenTournament = async ( ], [] as Id<'users'>[]); // Computed properties (easy to do, but used so frequently, it's nice to include them by default) - const playerCount = playerUserIds.length; - const activePlayerCount = activePlayerUserIds.length; - const maxPlayers = tournament.maxCompetitors * tournament.competitorSize; - const useTeams = tournament.competitorSize > 1; + const nextRound = (tournament.currentRound ?? tournament.lastRound ?? -1) + 1; return { ...tournament, logoUrl, bannerUrl, competitorCount, - activePlayerCount, - playerCount, + activePlayerCount: activePlayerUserIds.length, + playerCount: playerUserIds.length, playerUserIds, activePlayerUserIds, - maxPlayers, - useTeams, + maxPlayers : tournament.maxCompetitors * tournament.competitorSize, + useTeams: tournament.competitorSize > 1, + nextRound: nextRound < tournament.roundCount ? nextRound : undefined, }; }; diff --git a/convex/_model/tournaments/index.ts b/convex/_model/tournaments/index.ts index 68172dbe..38281fe7 100644 --- a/convex/_model/tournaments/index.ts +++ b/convex/_model/tournaments/index.ts @@ -11,6 +11,17 @@ export const tournamentsTable = defineTable({ .index('by_status', ['status']); export type TournamentId = Id<'tournaments'>; +export enum TournamentActionKey { + Edit = 'edit', + Delete = 'delete', + Publish = 'publish', + Cancel = 'cancel', + Start = 'start', + ConfigureRound = 'configureRound', + StartRound = 'startRound', + EndRound = 'endRound', + End = 'end', +} // Helpers export { checkTournamentAuth } from './_helpers/checkTournamentAuth'; diff --git a/convex/_model/tournaments/queries/getTournamentOpenRound.ts b/convex/_model/tournaments/queries/getTournamentOpenRound.ts index 2643fa5d..3f2bfb38 100644 --- a/convex/_model/tournaments/queries/getTournamentOpenRound.ts +++ b/convex/_model/tournaments/queries/getTournamentOpenRound.ts @@ -48,6 +48,7 @@ export const getTournamentOpenRound = async ( matchResultsProgress: { required: relevantPairingIds.length * tournament.competitorSize, submitted: relevantMatchResultIds.length, + remaining: (relevantPairingIds.length * tournament.competitorSize) - relevantMatchResultIds.length, }, // TODO: Get timer }; diff --git a/src/api.ts b/src/api.ts index f38f4fd8..2193ecae 100644 --- a/src/api.ts +++ b/src/api.ts @@ -52,6 +52,7 @@ export { // Tournaments export { type TournamentDeep as Tournament, + TournamentActionKey, type TournamentId, } from '../convex/_model/tournaments'; diff --git a/src/components/ConfirmationDialog/ConfirmationDialog.hooks.ts b/src/components/ConfirmationDialog/ConfirmationDialog.hooks.ts index 29d02da2..9ecefcd8 100644 --- a/src/components/ConfirmationDialog/ConfirmationDialog.hooks.ts +++ b/src/components/ConfirmationDialog/ConfirmationDialog.hooks.ts @@ -1,9 +1,4 @@ import { useModal } from '~/modals'; - -type ConfirmationDialogData = { - title?: string; - description?: string; - onConfirm: () => void; -}; +import { ConfirmationDialogData } from './ConfirmationDialog.types'; export const useConfirmationDialog = (id?: string) => useModal(id); diff --git a/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/src/components/ConfirmationDialog/ConfirmationDialog.tsx index 635231a6..e75ae3dc 100644 --- a/src/components/ConfirmationDialog/ConfirmationDialog.tsx +++ b/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; import clsx from 'clsx'; +import { ConfirmationDialogProps } from '~/components/ConfirmationDialog/ConfirmationDialog.types'; import { Button } from '~/components/generic/Button'; import { ControlledDialog, @@ -9,23 +9,10 @@ import { DialogHeader, } from '~/components/generic/Dialog'; import { ScrollArea } from '~/components/generic/ScrollArea'; -import { ElementIntent } from '~/types/componentLib'; import { useConfirmationDialog } from './ConfirmationDialog.hooks'; import styles from './ConfirmationDialog.module.scss'; -export interface ConfirmationDialogProps { - children?: ReactNode; - className?: string; - description?: string; - id: string; - intent?: ElementIntent; - onConfirm?: () => void; - title: string; - disabled?: boolean; - disablePadding?: boolean; -} - export const ConfirmationDialog = ({ children, className, @@ -36,6 +23,8 @@ export const ConfirmationDialog = ({ title, disabled = false, disablePadding = false, + cancelLabel = 'Cancel', + confirmLabel = 'Confirm', }: ConfirmationDialogProps): JSX.Element => { const { close, data } = useConfirmationDialog(id); const handleConfirm = (): void => { @@ -49,7 +38,7 @@ export const ConfirmationDialog = ({ }; return ( - + {(data?.description || description) && ( @@ -57,15 +46,16 @@ export const ConfirmationDialog = ({ )}
+ {data?.children} {children}
diff --git a/src/components/ConfirmationDialog/ConfirmationDialog.types.ts b/src/components/ConfirmationDialog/ConfirmationDialog.types.ts new file mode 100644 index 00000000..01fd77da --- /dev/null +++ b/src/components/ConfirmationDialog/ConfirmationDialog.types.ts @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; + +import { ElementIntent } from '~/types/componentLib'; + +export interface ConfirmationDialogProps { + children?: ReactNode; + className?: string; + description?: ReactNode; + id: string; + intent?: ElementIntent; + onConfirm?: () => void; + title?: string; + disabled?: boolean; + disablePadding?: boolean; + confirmLabel?: string; + cancelLabel?: string; +} + +export type ConfirmationDialogData = Partial; diff --git a/src/components/ConfirmationDialog/index.ts b/src/components/ConfirmationDialog/index.ts index 1fd5bb37..0fbd5f69 100644 --- a/src/components/ConfirmationDialog/index.ts +++ b/src/components/ConfirmationDialog/index.ts @@ -1,3 +1,6 @@ -export type { ConfirmationDialogProps } from './ConfirmationDialog'; export { ConfirmationDialog } from './ConfirmationDialog'; export { useConfirmationDialog } from './ConfirmationDialog.hooks'; +export { + type ConfirmationDialogData, + type ConfirmationDialogProps, +} from './ConfirmationDialog.types'; diff --git a/src/components/ToastProvider/ToastProvider.store.ts b/src/components/ToastProvider/ToastProvider.store.ts index 43a58833..09626817 100644 --- a/src/components/ToastProvider/ToastProvider.store.ts +++ b/src/components/ToastProvider/ToastProvider.store.ts @@ -1,7 +1,7 @@ import { Store } from '@tanstack/store'; interface ToastItem { - description?: string; + description?: string | string[]; icon?: JSX.Element; severity: ToastSeverity; title: string; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts b/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts new file mode 100644 index 00000000..123e1194 --- /dev/null +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.context.ts @@ -0,0 +1,12 @@ +import { createContext, MouseEvent } from 'react'; + +import { TournamentActionKey } from '~/api'; + +export type Action = { + handler: (e: MouseEvent) => void; + label: string; +}; + +export type TournamentActions = Partial>; + +export const TournamentActionsContext = createContext(null); diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx new file mode 100644 index 00000000..22eefc23 --- /dev/null +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx @@ -0,0 +1,229 @@ +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 { toast } from '~/components/ToastProvider'; +import { useTournament } from '~/components/TournamentProvider'; +import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; +import { useGetTournamentPairings } from '~/services/tournamentPairings'; +import { + useDeleteTournament, + useEndTournament, + useEndTournamentRound, + useGetTournamentOpenRound, + usePublishTournament, + useStartTournament, + useStartTournamentRound, +} from '~/services/tournaments'; +import { PATHS } from '~/settings'; +import { validateConfigureRound } from './utils/validateConfigureRound'; +import { + Action, + TournamentActions, + TournamentActionsContext, +} from './TournamentActionsProvider.context'; + +export const useTournamentActions = () => { + const context = useContext(TournamentActionsContext); + if (!context) { + throw Error('useTournamentActions must be used within a !'); + } + return context; +}; + +type ActionDefinition = Action & { + key: TournamentActionKey; + available: boolean; +}; + +export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): TournamentActions => { + const user = useAuth(); + const tournament = useTournament(); + + // ---- HANDLERS ---- + const navigate = useNavigate(); + const configureTournamentRound = (): void => { + navigate(generatePath(PATHS.tournamentPairings, { id: tournament._id })); + }; + const { mutation: deleteTournament } = useDeleteTournament({ + onSuccess: (): void => { + toast.success(`${tournament.title} deleted!`); + navigate(PATHS.tournaments); + }, + }); + + const { mutation: publishTournament } = usePublishTournament({ + onSuccess: (): void => { + toast.success(`${tournament.title} is now published!`); + }, + }); + + const { mutation: startTournament } = useStartTournament({ + onSuccess: (): void => { + toast.success(`${tournament.title} started!`); + }, + }); + + const { mutation: startTournamentRound } = useStartTournamentRound({ + onSuccess: (): void => { + toast.success(`Round ${currentRoundLabel} started!`); + }, + }); + + const { mutation: endTournament } = useEndTournament({ + onSuccess: (): void => { + toast.success(`${tournament.title} completed!`); + }, + }); + + const { mutation: endTournamentRound } = useEndTournamentRound({ + onSuccess: (): void => { + toast.success(`Round ${currentRoundLabel} completed!`); + }, + }); + + // ---- DATA ---- + const { data: nextRoundPairings } = useGetTournamentPairings({ + tournamentId: tournament._id, + round: tournament.nextRound, + }); + const { data: openRound } = useGetTournamentOpenRound({ + id: tournament._id, + }); + const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ + tournamentId: tournament._id, + }); + const isOrganizer = !!user && tournament.organizerUserIds.includes(user._id); + const isBetweenRounds = tournament.status === 'active' && !openRound; + const hasNextRound = tournament.nextRound !== undefined; + + // Labels for messages: + const nextRoundLabel = (tournament.nextRound ?? 0) + 1; + const currentRoundLabel = (tournament.currentRound ?? 0) + 1; + const remainingRoundsLabel = tournament.roundCount - ((tournament.lastRound ?? -1) + 1); + + // ---- ACTIONS ---- + const actions: ActionDefinition[] = [ + { + 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 }); + }, + }, + { + key: TournamentActionKey.Publish, + label: 'Publish', + available: isOrganizer && tournament.status === 'draft', + handler: () => { + // TODO: Implement confirmation dialog + publishTournament({ id: tournament._id }); + }, + }, + { + key: TournamentActionKey.Start, + label: 'Start', + available: isOrganizer && tournament.status === 'published', + handler: () => { + // TODO: Implement confirmation dialog + startTournament({ id: tournament._id }); + }, + }, + { + key: TournamentActionKey.ConfigureRound, + label: `Configure Round ${nextRoundLabel}`, + available: isOrganizer && isBetweenRounds && hasNextRound && nextRoundPairings?.length === 0, + handler: () => { + const { errors, warnings } = validateConfigureRound(tournament, tournamentCompetitors); + if (errors.length) { + return toast.error('Cannot Configure Round', { + description: errors, + }); + } + if (warnings.length) { + openDialog({ + title: `Configure Round ${nextRoundLabel}`, + children: warnings.map((warning, i) => ( + {warning} + )), + confirmLabel: 'Proceed', + onConfirm: () => configureTournamentRound(), + }); + } else { + configureTournamentRound(); + } + }, + }, + { + key: TournamentActionKey.StartRound, + label: `Start Round ${nextRoundLabel}`, + available: isOrganizer && isBetweenRounds && hasNextRound && (nextRoundPairings ?? []).length > 0, + handler: () => startTournamentRound({ id: tournament._id }), + }, + { + key: TournamentActionKey.EndRound, + label: `End Round ${currentRoundLabel}`, + available: isOrganizer && !!openRound, + handler: () => { + if (openRound && openRound.matchResultsProgress.remaining > 0) { + openDialog({ + title: 'Warning!', + description: ( + <> + {` + Are you sure you want to end round ${currentRoundLabel}? + There are still ${openRound.matchResultsProgress.remaining} + matches remaining to be checked in. + `} + Once the round is ended, it cannot be repeated! + + ), + confirmLabel: 'End Round', + onConfirm: () => configureTournamentRound(), + }); + } else { + endTournamentRound({ id: tournament._id }); + } + }, + }, + { + key: TournamentActionKey.End, + label: 'End Tournament', + available: isOrganizer && isBetweenRounds, + handler: () => { + if (tournament.nextRound !== undefined && tournament.nextRound < tournament.roundCount) { + openDialog({ + title: 'Warning!', + description: ( + <> + {`Are you sure you want to end ${tournament.title}? There are still ${remainingRoundsLabel} rounds remaining.`} + Once the tournament is ended, it cannot be restarted! + + ), + onConfirm: () => endTournament({ id: tournament._id }), + confirmLabel: 'End Tournament', + intent: 'danger', + }); + } else { + endTournament({ id: tournament._id }); + } + }, + }, + ]; + + return actions.filter(({ available }) => available).reduce((acc, { key, ...action }) => ({ + ...acc, + [key]: action, + }), {} as TournamentActions); +}; diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx new file mode 100644 index 00000000..0bf9d498 --- /dev/null +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; + +import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; +import { TournamentActionsContext } from './TournamentActionsProvider.context'; +import { useActions } from './TournamentActionsProvider.hooks'; + +export interface TournamentActionsProviderProps { + children: ReactNode; +} + +export const TournamentActionsProvider = ({ + children, +}: TournamentActionsProviderProps) => { + const { id, open } = useConfirmationDialog(); + const actions = useActions(open); + return ( + + {children} + + + ); +}; diff --git a/src/components/TournamentActionsProvider/index.ts b/src/components/TournamentActionsProvider/index.ts new file mode 100644 index 00000000..8321c11a --- /dev/null +++ b/src/components/TournamentActionsProvider/index.ts @@ -0,0 +1,2 @@ +export { TournamentActionsProvider } from './TournamentActionsProvider'; +export { useTournamentActions } from './TournamentActionsProvider.hooks'; diff --git a/src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx b/src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx new file mode 100644 index 00000000..08111e89 --- /dev/null +++ b/src/components/TournamentActionsProvider/utils/validateConfigureRound.tsx @@ -0,0 +1,66 @@ +import { ReactNode } from 'react'; + +import { Tournament, TournamentCompetitor } from '~/api'; +import { IdentityBadge } from '~/components/IdentityBadge'; +import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; + +export const validateConfigureRound = ( + tournament: Tournament, + tournamentCompetitors: TournamentCompetitor[] = [], +): { errors: string[], warnings: ReactNode[] } => { + const round = (tournament.lastRound ?? -1) + 2; + const { active, inactive } = tournamentCompetitors.reduce((acc, c) => { + const key = c.active ? 'active' : 'inactive'; + acc[key].push(c); + return acc; + }, { active: [] as TournamentCompetitor[], inactive: [] as TournamentCompetitor[] }); + + const errors: string[] = []; + const warnings: ReactNode[] = []; + + if (active.length < 2) { + errors.push('Please ensure at least 2 competitors are active.'); + } + if (active.length > tournament.maxCompetitors) { + errors.push(` + There are too many active competitors. + Please disable ${active.length - tournament.maxCompetitors} to proceed. + `); + } + for (const competitor of active) { + const activePlayers = competitor.players.filter(({ active }) => active); + if (activePlayers.length > tournament.competitorSize) { + errors.push(`${getTournamentCompetitorDisplayName(competitor)} has too many active players.`); + } + if (activePlayers.length < tournament.competitorSize) { + errors.push(`${getTournamentCompetitorDisplayName(competitor)} has too few active players.`); + } + } + if (inactive.length > 0) { + warnings.push( + <> +

+ {` + The following ${tournament.useTeams ? ('team' + ((inactive.length ?? 0) > 1 ? 's are' : ' is')) : 'player(s)'} + not listed as checked in and will not be included in the pairing process for round ${round}. + `} +

+ {inactive.map((tournamentCompetitor) => ( + + ))} + , + ); + } + if (active.length % 2) { + warnings.push( +

+ {` + There is an odd number of competitors, so one competitor will remain unpaired. As + tournament organizer, you will need to submit match results for the + ${tournament.useTeams ? 'team' : 'player'} with a bye, with the desired outcome. + `} +

, + ); + } + return { errors, warnings }; +}; diff --git a/src/components/TournamentContextMenu/TournamentContextMenu.tsx b/src/components/TournamentContextMenu/TournamentContextMenu.tsx index 751a81ce..0dbe0c83 100644 --- a/src/components/TournamentContextMenu/TournamentContextMenu.tsx +++ b/src/components/TournamentContextMenu/TournamentContextMenu.tsx @@ -1,26 +1,8 @@ -import { useRef } from 'react'; -import { generatePath, useNavigate } from 'react-router-dom'; import { Ellipsis } from 'lucide-react'; -import { useAuth } from '~/components/AuthProvider'; import { Button } from '~/components/generic/Button'; import { PopoverMenu } from '~/components/generic/PopoverMenu'; -import { toast } from '~/components/ToastProvider'; -import { ConfirmConfigureRoundDialog } from '~/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog'; -import { ConfirmConfigureRoundDialogHandle } from '~/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog'; -import { getRemainingRequiredMatchResults } from '~/components/TournamentContextMenu/TournamentContextMenu.utils'; -import { useTournament } from '~/components/TournamentProvider'; -import { useGetTournamentPairings } from '~/services/tournamentPairings'; -import { - useDeleteTournament, - useEndTournament, - useEndTournamentRound, - useGetTournamentOpenRound, - usePublishTournament, - useStartTournament, - useStartTournamentRound, -} from '~/services/tournaments'; -import { PATHS } from '~/settings'; +import { useTournamentActions } from '~/components/TournamentActionsProvider'; import { ElementSize } from '~/types/componentLib'; export interface TournamentContextMenuProps { @@ -34,127 +16,19 @@ export const TournamentContextMenu = ({ size = 'normal', variant = 'secondary', }: TournamentContextMenuProps): JSX.Element | null => { - const navigate = useNavigate(); - - const user = useAuth(); - const { - _id: id, - status, - currentRound, - lastRound, - title, - organizerUserIds, - } = useTournament(); - const nextRound = (lastRound ?? -1) + 1; - const nextRoundLabel = nextRound + 1; - const currentRoundLabel = (currentRound ?? 0) + 1; - - const { data: openRound } = useGetTournamentOpenRound({ id }); - const { data: nextRoundPairings } = useGetTournamentPairings({ - tournamentId: id, - round: nextRound, - }); - - const { mutation: deleteTournament } = useDeleteTournament({ - onSuccess: (): void => { - toast.success(`${title} deleted!`); - navigate(PATHS.tournaments); - }, - }); - - const { mutation: publishTournament } = usePublishTournament({ - onSuccess: (): void => { - toast.success(`${title} is now published!`); - }, - }); - - const { mutation: startTournament } = useStartTournament({ - onSuccess: (): void => { - toast.success(`${title} started!`); - }, - }); - - const { mutation: startTournamentRound } = useStartTournamentRound({ - onSuccess: (): void => { - toast.success(`Round ${currentRoundLabel} started!`); - }, - }); - - const { mutation: endTournament } = useEndTournament({ - onSuccess: (): void => { - toast.success(`${title} completed!`); - }, - }); - - const { mutation: endTournamentRound } = useEndTournamentRound({ - onSuccess: (): void => { - toast.success(`Round ${currentRoundLabel} completed!`); - }, - }); - - const confirmConfigureRoundDialogRef = useRef(null); - - const menuItems = [ - { - label: 'Edit', - onClick: () => navigate(generatePath(PATHS.tournamentEdit, { id })), - visible: status !== 'active' && status !== 'archived', - }, - { - label: 'Delete', - onClick: () => deleteTournament({ id }), - // TODO: Implement confirmation dialog - visible: status !== 'active' && status !== 'archived', - }, - { - label: 'Publish', - onClick: () => publishTournament({ id }), - // TODO: Implement confirmation dialog - visible: status === 'draft', - }, - { - label: 'Start', - onClick: () => startTournament({ id }), - visible: status === 'published', - }, - { - label: `Configure Round ${nextRoundLabel}`, - onClick: () => confirmConfigureRoundDialogRef.current?.open(), - visible: status === 'active' && !openRound && !nextRoundPairings?.length, - }, - { - label: `Start Round ${nextRoundLabel}`, - onClick: () => startTournamentRound({ id }), - visible: status === 'active' && !openRound && nextRoundPairings?.length, - }, - { - label: `End Round ${(currentRound ?? 0) + 1}`, - onClick: () => endTournamentRound({ id }), - visible: openRound && getRemainingRequiredMatchResults(openRound) === 0, - }, - { - label: 'End Tournament', - onClick: () => endTournament({ id }), - // TODO: More checks, show confirmation dialog if not complete - visible: status === 'active', - }, - ]; - - const visibleMenuItems = menuItems.filter((item) => item.visible); - - // TODO: Make better check for showing context menu - const showContextMenu = user && organizerUserIds.includes(user._id) && visibleMenuItems.length; - if (!showContextMenu) { + const actions = useTournamentActions(); + const visibleMenuItems = Object.values(actions).map(({ label, handler }) => ({ + label, + onClick: handler, + })); + if (!visibleMenuItems.length) { return null; } return ( - <> - - - - - + + + ); }; diff --git a/src/components/TournamentContextMenu/TournamentContextMenu.utils.ts b/src/components/TournamentContextMenu/TournamentContextMenu.utils.ts deleted file mode 100644 index 821a2059..00000000 --- a/src/components/TournamentContextMenu/TournamentContextMenu.utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TournamentOpenRound } from '~/services/tournaments'; - -export const getRemainingRequiredMatchResults = ( - openRound?: TournamentOpenRound, -): number | null => { - if (!openRound) { - return null; - } - return openRound.matchResultsProgress.required - openRound.matchResultsProgress.submitted; -}; diff --git a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.module.scss b/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.module.scss deleted file mode 100644 index 62aea60f..00000000 --- a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use "/src/style/flex"; - -.ConfirmConfigureRoundDialog { - @include flex.column; -} diff --git a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.tsx b/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.tsx deleted file mode 100644 index 546fe582..00000000 --- a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { - forwardRef, - ReactNode, - useImperativeHandle, - useMemo, -} from 'react'; -import { generatePath, useNavigate } from 'react-router-dom'; -import clsx from 'clsx'; - -import { ConfirmationDialog, useConfirmationDialog } from '~/components/ConfirmationDialog'; -import { Warning } from '~/components/generic/Warning'; -import { toast } from '~/components/ToastProvider'; -import { useTournament } from '~/components/TournamentProvider'; -import { useGetTournamentCompetitorsByTournament } from '~/services/tournamentCompetitors'; -import { PATHS } from '~/settings'; -import { getTournamentCompetitorDisplayName } from '~/utils/common/getTournamentCompetitorDisplayName'; -import { getWarnings, sortCompetitorsByActive } from './ConfirmConfigureRoundDialog.utils'; - -import styles from './ConfirmConfigureRoundDialog.module.scss'; - -export interface ConfirmConfigureRoundDialogProps { - className?: string; -} - -export interface ConfirmConfigureRoundDialogHandle { - open: () => void; -} - -export const ConfirmConfigureRoundDialog = forwardRef(({ - className, -}: ConfirmConfigureRoundDialogProps, ref): JSX.Element => { - const tournament = useTournament(); - const { data: tournamentCompetitors } = useGetTournamentCompetitorsByTournament({ - tournamentId: tournament._id, - }); - const navigate = useNavigate(); - const { id, open } = useConfirmationDialog(); - const proceed = (): void => { - navigate(generatePath(PATHS.tournamentPairings, { id: tournament._id })); - }; - - const { active: activeCompetitors } = sortCompetitorsByActive(tournamentCompetitors ?? []); - const warnings: ReactNode[] = useMemo(() => getWarnings(tournament, tournamentCompetitors ?? []), [ - tournament, - tournamentCompetitors, - ]); - - useImperativeHandle(ref, () => ({ - open: () => { - if (activeCompetitors.length < 2) { - return toast.error('Cannot Configure Round', { - description: 'Please ensure at least 2 competitors are active.', - }); - } - if (activeCompetitors.length > tournament.maxCompetitors) { - return toast.error('Cannot Configure Round', { - description: `There are too many active competitors. Please disable ${activeCompetitors.length - tournament.maxCompetitors} to proceed.`, - }); - } - for (const competitor of activeCompetitors) { - const activePlayers = competitor.players.filter(({ active }) => active); - if (activePlayers.length > tournament.competitorSize) { - return toast.error('Cannot Configure Round', { - description: `${getTournamentCompetitorDisplayName(competitor)} has too many active players.`, - }); - } - if (activePlayers.length < tournament.competitorSize) { - return toast.error('Cannot Configure Round', { - description: `${getTournamentCompetitorDisplayName(competitor)} has too few active players.`, - }); - } - } - - if (warnings.length) { - open(); - } else { - proceed(); - } - }, - })); - return ( - - {warnings.map((warning, i) => ( - - {warning} - - ))} - - ); -}); diff --git a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.utils.tsx b/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.utils.tsx deleted file mode 100644 index c7ee414b..00000000 --- a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/ConfirmConfigureRoundDialog.utils.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ReactNode } from 'react'; - -import { Tournament, TournamentCompetitor } from '~/api'; -import { IdentityBadge } from '~/components/IdentityBadge'; - -export const sortCompetitorsByActive = (tournamentCompetitors: TournamentCompetitor[]) => ( - tournamentCompetitors.reduce( - (acc, c) => { - const key = c.active ? 'active' : 'inactive'; - acc[key].push(c); - return acc; - }, - { active: [] as TournamentCompetitor[], inactive: [] as TournamentCompetitor[] }, - ) -); - -export const getWarnings = (tournament: Tournament, tournamentCompetitors: TournamentCompetitor[]): ReactNode[] => { - const round = (tournament.lastRound ?? -1) + 2; - const { active, inactive } = sortCompetitorsByActive(tournamentCompetitors); - const warnings: ReactNode[] = []; - if (inactive.length > 0) { - warnings.push( - <> -

- {`The following ${tournament.useTeams ? ('team' + ((inactive.length ?? 0) > 1 ? 's are' : ' is')) : 'player(s)'} not listed as checked in and will not be included in the pairing process for round ${round}.`} -

- {inactive.map((tournamentCompetitor) => ( - - ))} - , - ); - } - if (active.length % 2) { - warnings.push( -

- {`There is an odd number of competitors, so one competitor will remain unpaired. As tournament organizer, you will need to submit match results for the ${tournament.useTeams ? 'team' : 'player'} with a bye, with the desired outcome.`} -

, - ); - } - return warnings; -}; diff --git a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/index.ts b/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/index.ts deleted file mode 100644 index 61bf2d13..00000000 --- a/src/components/TournamentContextMenu/components/ConfirmConfigureRoundDialog/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - ConfirmConfigureRoundDialog, - type ConfirmConfigureRoundDialogHandle, - type ConfirmConfigureRoundDialogProps, -} from './ConfirmConfigureRoundDialog'; diff --git a/src/components/TournamentContextMenu/index.ts b/src/components/TournamentContextMenu/index.ts index 1ce08722..d1c00880 100644 --- a/src/components/TournamentContextMenu/index.ts +++ b/src/components/TournamentContextMenu/index.ts @@ -1,5 +1 @@ -export { - ConfirmConfigureRoundDialog, - type ConfirmConfigureRoundDialogHandle, -} from './components/ConfirmConfigureRoundDialog'; export { TournamentContextMenu } from './TournamentContextMenu'; diff --git a/src/components/generic/Dialog/DialogDescription.module.scss b/src/components/generic/Dialog/DialogDescription.module.scss index e181637b..602b5303 100644 --- a/src/components/generic/Dialog/DialogDescription.module.scss +++ b/src/components/generic/Dialog/DialogDescription.module.scss @@ -11,4 +11,8 @@ @include flex.column($gap: 0.5rem); padding: 0 var(--modal-inner-gutter); + + strong { + font-weight: 500; + } } diff --git a/src/pages/TournamentDetailPage/TournamentDetailPage.tsx b/src/pages/TournamentDetailPage/TournamentDetailPage.tsx index d9c26eb2..90563879 100644 --- a/src/pages/TournamentDetailPage/TournamentDetailPage.tsx +++ b/src/pages/TournamentDetailPage/TournamentDetailPage.tsx @@ -16,6 +16,7 @@ import { } from '~/components/generic/Tabs'; import { NotFoundView } from '~/components/NotFoundView'; import { PageWrapper } from '~/components/PageWrapper'; +import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; import { TournamentCompetitorsProvider } from '~/components/TournamentCompetitorsProvider'; import { TournamentContextMenu } from '~/components/TournamentContextMenu'; import { TournamentProvider } from '~/components/TournamentProvider'; @@ -105,40 +106,42 @@ export const TournamentDetailPage = (): JSX.Element => { return ( - - } bannerBackgroundUrl={tournament.bannerUrl}> -
- {showInfoSidebar && ( -
- -
- )} - -
- {tabs.length > 1 && ( - - )} - -
- - - - - - - - - - - - - - - -
-
-
-
+ + + } bannerBackgroundUrl={tournament.bannerUrl}> +
+ {showInfoSidebar && ( +
+ +
+ )} + +
+ {tabs.length > 1 && ( + + )} + +
+ + + + + + + + + + + + + + + +
+
+
+
+
); }; diff --git a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx index 9808731f..a9d2df5b 100644 --- a/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentPairingsCard/TournamentPairingsCard.tsx @@ -1,15 +1,11 @@ -import { - ReactElement, - useRef, - useState, -} from 'react'; +import { ReactElement, useState } from 'react'; import clsx from 'clsx'; import { Zap } from 'lucide-react'; import { Button } from '~/components/generic/Button'; import { InputSelect } from '~/components/generic/InputSelect'; import { Table } from '~/components/generic/Table'; -import { ConfirmConfigureRoundDialog, ConfirmConfigureRoundDialogHandle } from '~/components/TournamentContextMenu'; +import { useTournamentActions } from '~/components/TournamentActionsProvider/TournamentActionsProvider.hooks'; import { useTournament } from '~/components/TournamentProvider'; import { useGetTournamentPairings } from '~/services/tournamentPairings'; import { TournamentDetailCard } from '../TournamentDetailCard'; @@ -25,16 +21,19 @@ export interface TournamentPairingsCardProps { export const TournamentPairingsCard = ({ className, }: TournamentPairingsCardProps): JSX.Element => { - const { _id: tournamentId, lastRound } = useTournament(); + const { _id: tournamentId, lastRound, roundCount } = useTournament(); + const actions = useTournamentActions(); - const roundIndexes = lastRound !== undefined ? Array.from({ length: lastRound + 2 }, (_, i) => i) : [0]; + const roundIndexes = lastRound !== undefined ? Array.from({ + length: Math.min(lastRound + 2, roundCount), + }, (_, i) => i) : [0]; const [round, setRound] = useState(roundIndexes.length - 1); + const { data: tournamentPairings, loading } = useGetTournamentPairings({ tournamentId, round, }); - const confirmConfigureRoundDialogRef = useRef(null); const columns = getTournamentPairingTableConfig(); const rows = (tournamentPairings || []); @@ -55,10 +54,6 @@ export const TournamentPairingsCard = ({ />, ]; - const handleConfigure = (): void => { - confirmConfigureRoundDialogRef.current?.open(); - }; - return ( <> }> - + {actions?.configureRound && ( + + )} ) : ( ) )} - - ); }; From 6cf3d87ad96b2df2fe1e47e70bf31cb7bfc52e5a Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 10 Jul 2025 07:50:29 +0200 Subject: [PATCH 2/8] chore: Clean-up Convex errors --- convex/common/errors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/convex/common/errors.ts b/convex/common/errors.ts index ec67e853..de72e21f 100644 --- a/convex/common/errors.ts +++ b/convex/common/errors.ts @@ -10,7 +10,8 @@ export const errors = { CANNOT_MODIFY_ARCHIVED_TOURNAMENT: 'Cannot modify an archived tournament.', CANNOT_MODIFY_PUBLISHED_TOURNAMENT_COMPETITORS: 'Cannot modify competitor format for a published tournament.', CANNOT_REMOVE_ANOTHER_PLAYER: 'Cannot remove another player.', - CANNOT_REMOVE_COMPETITOR_FROM_ACTIVE_TOURNAMENT: 'Cannot add a competitor to an on-going tournament.', + CANNOT_REMOVE_COMPETITOR_FROM_ACTIVE_TOURNAMENT: 'Cannot remove a competitor from an on-going tournament.', + CANNOT_REMOVE_PLAYER_FROM_ACTIVE_TOURNAMENT: 'Cannot remove a player from an on-going tournament.', CANNOT_MODIFY_ANOTHER_TOURNAMENT_COMPETITOR: 'Cannot modify another tournament competitor.', // Tournament Lifecycle @@ -29,6 +30,7 @@ export const errors = { CANNOT_PUBLISH_ACTIVE_TOURNAMENT: 'Cannot publish a tournament which is already active.', CANNOT_END_PUBLISHED_TOURNAMENT: 'Cannot end a tournament which has not started.', CANNOT_END_DRAFT_TOURNAMENT: 'Cannot end a tournament which is still a draft.', + CANNOT_END_TOURNAMENT_MID_ROUND: 'Cannot end a tournament which mid-round.', TOURNAMENT_ALREADY_HAS_OPEN_ROUND: 'Tournament already has an open round.', TOURNAMENT_DOES_NOT_HAVE_OPEN_ROUND: 'Tournament does not have a currently open round.', @@ -43,8 +45,6 @@ export const errors = { TOURNAMENT_TIMER_ALREADY_PAUSED: 'Tournament timer is already paused.', TOURNAMENT_TIMER_ALREADY_RUNNING: 'Tournament timer is already running.', TOURNAMENT_TIMER_ALREADY_EXISTS: 'Tournament already has a timer for this round.', - CANNOT_SUBSTITUTE_ONLY_ONE_PLAYER: 'Cannot substitute only one player.', - INVALID_PAIRING_LENGTH: 'foo', // Missing docs FILE_NOT_FOUND: 'Could not find a file with that ID.', From a24308913172a50a6711935e0074cffa592f4f57 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 10 Jul 2025 07:50:59 +0200 Subject: [PATCH 3/8] fix: Do not try to clean up current round timer on tournament end --- convex/_model/tournaments/mutations/endTournament.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/convex/_model/tournaments/mutations/endTournament.ts b/convex/_model/tournaments/mutations/endTournament.ts index 54feafad..b529ec6f 100644 --- a/convex/_model/tournaments/mutations/endTournament.ts +++ b/convex/_model/tournaments/mutations/endTournament.ts @@ -6,7 +6,6 @@ import { import { MutationCtx } from '../../../_generated/server'; import { getErrorMessage } from '../../../common/errors'; -import { deleteTournamentTimerByTournament } from '../../tournamentTimers'; import { checkTournamentAuth } from '../_helpers/checkTournamentAuth'; export const endTournamentArgs = v.object({ @@ -45,14 +44,11 @@ export const endTournament = async ( if (tournament.status === 'archived') { throw new ConvexError(getErrorMessage('TOURNAMENT_ALREADY_ARCHIVED')); } + if (tournament.currentRound !== undefined) { + throw new ConvexError(getErrorMessage('CANNOT_END_TOURNAMENT_MID_ROUND')); + } // ---- PRIMARY ACTIONS ---- - // Clean up TournamentTimer: - await deleteTournamentTimerByTournament(ctx, { - tournamentId: tournament._id, - round: tournament.currentRound, - }); - // End the tournament: await ctx.db.patch(args.id, { status: 'archived', From 0585eb811eeaf23c70b1668e604d7d4ecaf14c9e Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 10 Jul 2025 07:51:23 +0200 Subject: [PATCH 4/8] fix: Don't allow players to be removed from tournament --- .../mutations/updateTournamentCompetitor.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/convex/_model/tournamentCompetitors/mutations/updateTournamentCompetitor.ts b/convex/_model/tournamentCompetitors/mutations/updateTournamentCompetitor.ts index c91a2292..a21eda45 100644 --- a/convex/_model/tournamentCompetitors/mutations/updateTournamentCompetitor.ts +++ b/convex/_model/tournamentCompetitors/mutations/updateTournamentCompetitor.ts @@ -34,6 +34,15 @@ export const updateTournamentCompetitor = async ( if (tournament.status === 'archived') { throw new ConvexError(getErrorMessage('CANNOT_MODIFY_ARCHIVED_TOURNAMENT')); } + // If a tournament is active, never allow existing players to be removed, as this will break tournament results/rankings: + if (tournament.status === 'active') { + const updatedUserIds = new Set(args.players.map((p) => p.userId)); + for (const player of tournamentCompetitor.players) { + if (!updatedUserIds.has(player.userId)) { + throw new ConvexError(getErrorMessage('CANNOT_REMOVE_PLAYER_FROM_ACTIVE_TOURNAMENT')); + } + } + } // ---- EXTENDED AUTH CHECK ---- /* These user IDs can make changes to this tournament competitor: From 18596ff635d3b98ff8a590381642f04a5540e1c0 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 10 Jul 2025 07:51:49 +0200 Subject: [PATCH 5/8] chore: Update test tournament banner image --- convex/_model/utils/createTestTournament.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex/_model/utils/createTestTournament.ts b/convex/_model/utils/createTestTournament.ts index 57c011cd..1c9c805f 100644 --- a/convex/_model/utils/createTestTournament.ts +++ b/convex/_model/utils/createTestTournament.ts @@ -60,7 +60,7 @@ export const createTestTournament = async ( playingTime: 3, }, logoStorageId: 'kg208wxmb55v36bh9qnkqc0c397j1rmb' as Id<'_storage'>, - bannerStorageId: 'kg21scbttz4t1hxxcs9qjcsvkh7j0nkh' as Id<'_storage'>, + bannerStorageId: 'kg250q9ezj209wpxgn0xqfca297kckc8' as Id<'_storage'>, }); // 3. Create competitors From 65baf27f2bec7c627c9f71338ad5713b7ae70306 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 14 Jul 2025 17:22:22 +0200 Subject: [PATCH 6/8] Update TournamentCard.tsx --- .../TournamentCard/TournamentCard.tsx | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/TournamentCard/TournamentCard.tsx b/src/components/TournamentCard/TournamentCard.tsx index 1f8e11fe..0c5f3e77 100644 --- a/src/components/TournamentCard/TournamentCard.tsx +++ b/src/components/TournamentCard/TournamentCard.tsx @@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react'; import { Tournament } from '~/api'; import { useAuth } from '~/components/AuthProvider'; import { Button } from '~/components/generic/Button'; +import { TournamentActionsProvider } from '~/components/TournamentActionsProvider'; import { TournamentContextMenu } from '~/components/TournamentContextMenu'; import { TournamentInfoBlock } from '~/components/TournamentInfoBlock/'; import { TournamentProvider } from '~/components/TournamentProvider'; @@ -43,32 +44,34 @@ export const TournamentCard = ({ return ( -
-
- {tournament?.logoUrl && ( - {tournament.title} - )} -
-
-

{tournament.title}

-
- {showContextMenu && ( - + +
+
+ {tournament?.logoUrl && ( + {tournament.title} )} -
+
+

{tournament.title}

+
+ {showContextMenu && ( + )} - - + +
+ +
- - -
+ ); }; From 58d35c474cc0776889a963254a21c132e33d8ed9 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 14 Jul 2025 20:49:20 +0200 Subject: [PATCH 7/8] fix: Ensure round 0 rankings can be included --- .../queries/getTournamentCompetitorsByTournament.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts index 2cf22a39..1b17b134 100644 --- a/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts +++ b/convex/_model/tournamentCompetitors/queries/getTournamentCompetitorsByTournament.ts @@ -17,7 +17,7 @@ export const getTournamentCompetitorsByTournament = async ( const tournamentCompetitors = await ctx.db.query('tournamentCompetitors') .withIndex('by_tournament_id', (q) => q.eq('tournamentId', args.tournamentId)) .collect(); - const rankings = args.includeRankings && args.includeRankings > -1 ? await getTournamentRankings(ctx, { + const rankings = args.includeRankings !== undefined && args.includeRankings > -1 ? await getTournamentRankings(ctx, { tournamentId: args.tournamentId, round: args.includeRankings, }) : undefined; From 815ac390bc9e31dce4ef0b6248b4c66b9bc2bac8 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Tue, 15 Jul 2025 08:21:03 +0200 Subject: [PATCH 8/8] fix: Fix end tournament round context menu behavior --- .../TournamentActionsProvider.hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx index 22eefc23..e2110bd4 100644 --- a/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx +++ b/src/components/TournamentActionsProvider/TournamentActionsProvider.hooks.tsx @@ -190,7 +190,7 @@ export const useActions = (openDialog: (data?: ConfirmationDialogData) => void): ), confirmLabel: 'End Round', - onConfirm: () => configureTournamentRound(), + onConfirm: () => endTournamentRound({ id: tournament._id }), }); } else { endTournamentRound({ id: tournament._id });