From 99c404f809d4ec7fadce74a7053d7f17bd556496 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Mon, 30 Jun 2025 20:53:37 +0200 Subject: [PATCH] feat: #99 Improve tournament competitor edit dialog --- .../TournamentCompetitorEditDialog.tsx | 22 +++++---- .../TournamentCompetitorForm.module.scss | 4 ++ .../TournamentCompetitorForm.schema.ts | 45 ++++++++----------- .../TournamentCompetitorForm.tsx | 40 ++++++++++++++--- .../CompetitorActions/CompetitorActions.tsx | 6 +-- 5 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx index fd36e82a..e7870637 100644 --- a/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx +++ b/src/components/TournamentCompetitorEditDialog/TournamentCompetitorEditDialog.tsx @@ -4,6 +4,7 @@ import { DialogActions, DialogHeader, } from '~/components/generic/Dialog'; +import { ScrollArea } from '~/components/generic/ScrollArea'; import { TournamentCompetitorForm, TournamentCompetitorSubmitData } from '~/components/TournamentCompetitorForm'; import { useUpdateTournamentCompetitor } from '~/services/tournamentCompetitors'; import { useTournamentCompetitorEditDialog } from './TournamentCompetitorEditDialog.hooks'; @@ -25,22 +26,27 @@ export const TournamentCompetitorEditDialog = (): JSX.Element => { if (!data) { return; } + + const { players, ...restData } = formData; updateTournamentCompetitor({ id: data?.tournamentCompetitor._id, - ...formData, + ...restData, + players: players.filter((player) => player.userId), }); }; return ( - + + + diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss index c5348d3d..f35e4bff 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.module.scss @@ -11,6 +11,10 @@ row-gap: 0.5rem; column-gap: 1rem; align-items: center; + + &_AddButton { + grid-column: 1/-1; + } } &_SinglePlayer { diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts index 299dc51d..31563df4 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.schema.ts @@ -17,21 +17,19 @@ export const createSchema = ( ), }).superRefine((data, ctx) => { const activeCount = data.players.filter((player) => player.active).length; - if (status === 'active') { - if (activeCount < competitorSize) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `At least ${competitorSize} player(s) must be active.`, - path: ['players'], - }); - } - if (activeCount > competitorSize) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Only ${competitorSize} player(s) may be active.`, - path: ['players'], - }); - } + if (activeCount < competitorSize && status === 'active') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `At least ${competitorSize} players must be active.`, + path: ['players'], + }); + } + if (activeCount > competitorSize) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Only ${competitorSize} players may be active.`, + path: ['players'], + }); } if (otherCompetitors.find((c) => c.teamName?.toLowerCase() === data.teamName.trim().toLowerCase())) { ctx.addIssue({ @@ -49,17 +47,10 @@ export const defaultValues: DeepPartial = { players: [], }; -export const getDefaultValues = (competitorSize: number, existingCompetitor?: TournamentCompetitor): DeepPartial => { - const emptyPlayers = Array.from({ length: competitorSize }, () => ({ - active: true, - userId: '', - })); - const existingPlayers = (existingCompetitor?.players || []).map(({ active, user }) => ({ +export const getDefaultValues = (competitorSize: number, existingCompetitor?: TournamentCompetitor): DeepPartial => ({ + teamName: existingCompetitor?.teamName ?? '', + players: (existingCompetitor?.players || []).map(({ active, user }) => ({ active, userId: user._id, - })); - return { - teamName: existingCompetitor?.teamName ?? '', - players: existingPlayers.length ? existingPlayers : emptyPlayers, - }; -}; + })), +}); diff --git a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx index f28cc6b8..5506b273 100644 --- a/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx +++ b/src/components/TournamentCompetitorForm/TournamentCompetitorForm.tsx @@ -1,3 +1,4 @@ +import { MouseEvent } from 'react'; import { Fragment } from 'react/jsx-runtime'; import { SubmitHandler, @@ -7,6 +8,7 @@ import { } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import clsx from 'clsx'; +import { Plus } from 'lucide-react'; import { TournamentCompetitor, @@ -14,6 +16,7 @@ import { UserId, } from '~/api'; import { useAuth } from '~/components/AuthProvider'; +import { Button } from '~/components/generic/Button'; import { Form, FormField } from '~/components/generic/Form'; import { InputSelect } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; @@ -70,7 +73,7 @@ export const TournamentCompetitorForm = ({ defaultValues: getDefaultValues(competitorSize, tournamentCompetitor), mode: 'onSubmit', }); - const { fields } = useFieldArray({ + const { fields, append } = useFieldArray({ control: form.control, name: 'players', rules: { @@ -91,11 +94,25 @@ export const TournamentCompetitorForm = ({ form.setValue(`players.${i}.userId`, userId); } }; + const handleChangePlayerActive = (i: number, active: boolean): void => { form.setValue(`players.${i}.active`, active); }; - const handleSubmit: SubmitHandler = async (formData): Promise => { - onSubmit({ tournamentId, ...formData }); + + const handleAddPlayer = (e: MouseEvent): void => { + e.preventDefault(); + append({ + active: players.filter((p) => p.active).length < competitorSize, + userId: '' as UserId, + }); + }; + + const handleSubmit: SubmitHandler = async ({ players, ...formData }): Promise => { + onSubmit({ + tournamentId, + players: players.filter((p) => p.userId.length), + ...formData, + }); }; const excludedUserIds = [ @@ -105,6 +122,8 @@ export const TournamentCompetitorForm = ({ const isOrganizer = user && organizerUserIds.includes(user._id); + const emptyPlayerSlots = players.filter((player) => !player.userId.length).length; + return (
{useTeams && ( @@ -124,20 +143,29 @@ export const TournamentCompetitorForm = ({ handleChangePlayerActive(i, value)} disabled={loading} /> handleChangeUser(i, value)} excludedUserIds={excludedUserIds} - disabled={loading} + disabled={loading || (!!players[i]?.userId && status !== 'published')} allowPlaceholder={false} /> ))} +
+ +
) : ( <> diff --git a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx index 25137975..c139505e 100644 --- a/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx +++ b/src/components/TournamentRoster/components/CompetitorActions/CompetitorActions.tsx @@ -76,17 +76,17 @@ export const CompetitorActions = ({ playerUserId: user!._id, tournamentCompetitorId: competitor._id, }), - visible: user && isPlayer && tournament.status !== 'archived', + visible: isPlayer && !['active', 'archived'].includes(tournament.status), }, { label: 'Edit', onClick: () => openEditDialog({ tournamentCompetitor: competitor }), - visible: user && (isOrganizer || (isPlayer && tournament.useTeams)) && tournament.status !== 'archived' && tournament.currentRound === undefined, + visible: (isOrganizer || (isPlayer && tournament.useTeams)) && tournament.status !== 'archived' && tournament.currentRound === undefined, }, { label: 'Delete', onClick: () => openConfirmDeleteDialog(), - visible: user && isOrganizer && tournament.status === 'published', + visible: isOrganizer && tournament.status === 'published', }, ];