Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 6 additions & 8 deletions convex/_model/tournaments/_helpers/deepenTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
11 changes: 11 additions & 0 deletions convex/_model/tournaments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 3 additions & 7 deletions convex/_model/tournaments/mutations/endTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
2 changes: 1 addition & 1 deletion convex/_model/utils/createTestTournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions convex/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.',

Expand All @@ -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.',
Expand Down
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export {
// Tournaments
export {
type TournamentDeep as Tournament,
TournamentActionKey,
type TournamentId,
} from '../convex/_model/tournaments';

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ConfirmationDialogData>(id);
24 changes: 7 additions & 17 deletions src/components/ConfirmationDialog/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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 => {
Expand All @@ -49,23 +38,24 @@ export const ConfirmationDialog = ({
};
return (
<ControlledDialog id={id} width="small" className={clsx(className)}>
<DialogHeader title={data?.title ?? title} onCancel={close} />
<DialogHeader title={data?.title ?? title ?? 'Confirmation'} onCancel={close} />
<ScrollArea>
{(data?.description || description) && (
<DialogDescription>
{data?.description || description}
</DialogDescription>
)}
<div className={styles.ConfirmationDialog_Body} data-padding={!disablePadding}>
{data?.children}
{children}
</div>
</ScrollArea>
<DialogActions>
<Button variant="secondary" onClick={close}>
Cancel
{data?.cancelLabel ?? cancelLabel}
</Button>
<Button intent={intent} onClick={handleConfirm} disabled={disabled}>
Confirm
{data?.confirmLabel ?? confirmLabel}
</Button>
</DialogActions>
</ControlledDialog>
Expand Down
19 changes: 19 additions & 0 deletions src/components/ConfirmationDialog/ConfirmationDialog.types.ts
Original file line number Diff line number Diff line change
@@ -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<ConfirmationDialogProps>;
5 changes: 4 additions & 1 deletion src/components/ConfirmationDialog/index.ts
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion src/components/ToastProvider/ToastProvider.store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Store } from '@tanstack/store';

interface ToastItem {
description?: string;
description?: string | string[];
icon?: JSX.Element;
severity: ToastSeverity;
title: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Record<TournamentActionKey, Action>>;

export const TournamentActionsContext = createContext<TournamentActions | null>(null);
Loading