diff --git a/package-lock.json b/package-lock.json index 6ecf1750..6a84880d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.8.3", - "@ianpaschal/combat-command-game-systems": "^1.4.0", + "@ianpaschal/combat-command-components": "^1.8.4", + "@ianpaschal/combat-command-game-systems": "^1.4.1", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", "@react-hook/window-size": "^3.1.1", @@ -1500,9 +1500,9 @@ "license": "BSD-3-Clause" }, "node_modules/@ianpaschal/combat-command-components": { - "version": "1.8.3", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.8.3/79bc9bee5052e13cc94b32d715e54d321e798cd1", - "integrity": "sha512-gT71Lo9NRTZvg5O+mbidtXLhvGRCwG2GXHFy/hrSlHBFhSwldUmti901WwnyCS9aP91Zyon6sfkgbuTAdfHLXw==", + "version": "1.8.4", + "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-components/1.8.4/1707cf615f761a5d1151cb3c264901d57693eb4a", + "integrity": "sha512-D8K7TDE32YtfI2FSptFfrwWfs1A8G7Q+X/UqosxrCbE/104yTRE/0nOYfVpvlVe6V6iD+ApDyR01Fse7qSmV0A==", "license": "MIT", "dependencies": { "@base-ui/react": "^1.0.0", @@ -1558,9 +1558,9 @@ } }, "node_modules/@ianpaschal/combat-command-game-systems": { - "version": "1.4.0", - "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-game-systems/1.4.0/a432bfd116540fb818cff97712434ee965b127d0", - "integrity": "sha512-goLGRmnDnp+3UUyIDRS7otWOr45FsmttAdtI+SbKVpHLLMoxuAO9/lRy0o0NZDO6jxI3FxZ9b6KmrGDzf22gnA==", + "version": "1.4.1", + "resolved": "https://npm.pkg.github.com/download/@ianpaschal/combat-command-game-systems/1.4.1/f4bc4774eeb4a5532aab9125a5d499ea65b83934", + "integrity": "sha512-OpshG2PeUeE9RGeWORjhwZof4S9XoHXyalwlTHflBFbam4YIbo4lYiSTVfM8n0vgTasg+tQoaZq5ZZ/E0jg4Gg==", "license": "UNLICENSED", "dependencies": { "zod": "^3.25.76" diff --git a/package.json b/package.json index 541711a3..eda77ee9 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "@dnd-kit/sortable": "^10.0.0", "@fontsource/figtree": "^5.1.0", "@hookform/resolvers": "^3.9.0", - "@ianpaschal/combat-command-components": "^1.8.3", - "@ianpaschal/combat-command-game-systems": "^1.4.0", + "@ianpaschal/combat-command-components": "^1.8.4", + "@ianpaschal/combat-command-game-systems": "^1.4.1", "@mapbox/search-js-core": "^1.0.0-beta.25", "@radix-ui/colors": "^3.0.0", "@react-hook/window-size": "^3.1.1", diff --git a/src/components/MatchResultDetailFields/MatchResultDetailFields.tsx b/src/components/MatchResultDetailFields/MatchResultDetailFields.tsx index 3db5d4b4..6b839a36 100644 --- a/src/components/MatchResultDetailFields/MatchResultDetailFields.tsx +++ b/src/components/MatchResultDetailFields/MatchResultDetailFields.tsx @@ -9,11 +9,10 @@ import { CompatibleFormData } from './MatchResultDetailFields.types'; export interface MatchResultDetailFieldsProps { className?: string; + disableFactions?: boolean; } -export const MatchResultDetailFields = ({ - className, -}: MatchResultDetailFieldsProps): JSX.Element => { +export const MatchResultDetailFields = (props: MatchResultDetailFieldsProps): JSX.Element => { const { reset, watch, getFieldState } = useFormContext(); const gameSystem = watch('gameSystem'); @@ -36,20 +35,15 @@ export const MatchResultDetailFields = ({ */ const playerOptions = usePlayerOptions(); - const sharedProps = { - className, - playerOptions, - }; - if (gameSystem === GameSystem.FlamesOfWarV4) { return ( - + ); } if (gameSystem === GameSystem.TeamYankeeV2) { return ( - + ); } diff --git a/src/components/MatchResultDetailFields/gameSystems/FlamesOfWarV4MatchResultDetailFields/FlamesOfWarV4MatchResultDetailFields.tsx b/src/components/MatchResultDetailFields/gameSystems/FlamesOfWarV4MatchResultDetailFields/FlamesOfWarV4MatchResultDetailFields.tsx index 1044ae43..7b20d5c3 100644 --- a/src/components/MatchResultDetailFields/gameSystems/FlamesOfWarV4MatchResultDetailFields/FlamesOfWarV4MatchResultDetailFields.tsx +++ b/src/components/MatchResultDetailFields/gameSystems/FlamesOfWarV4MatchResultDetailFields/FlamesOfWarV4MatchResultDetailFields.tsx @@ -22,11 +22,13 @@ import styles from './FlamesOfWarV4MatchResultDetailFields.module.scss'; export interface FlamesOfWarV4MatchResultDetailFieldsProps { className?: string; playerOptions: InputSelectOption[]; + disableFactions?: boolean; } export const FlamesOfWarV4MatchResultDetailFields = ({ className, playerOptions, + disableFactions = false, }: FlamesOfWarV4MatchResultDetailFieldsProps): JSX.Element => { const [showScoreOverride, setShowScoreOverride] = useState(false); @@ -35,6 +37,8 @@ export const FlamesOfWarV4MatchResultDetailFields = ({ const { details } = values; const gameSystemConfig = validateGameSystemConfig(GameSystem.FlamesOfWarV4, values.gameSystemConfig); + const factionOptions = getFactionOptions(); + const battlePlanOptions = getBattlePlanOptions(); const missionOptions = useMissionOptions(gameSystemConfig, details.player0BattlePlan, details.player1BattlePlan); // TODO: Don't allow winner 'None' for certain outcome types. @@ -80,24 +84,22 @@ export const FlamesOfWarV4MatchResultDetailFields = ({ return (
- {/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */} - - + + - +
- {/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */} - - + + - + diff --git a/src/components/MatchResultDetailFields/gameSystems/TeamYankeeV2MatchResultDetailFields/TeamYankeeV2MatchResultDetailFields.tsx b/src/components/MatchResultDetailFields/gameSystems/TeamYankeeV2MatchResultDetailFields/TeamYankeeV2MatchResultDetailFields.tsx index 4c1037da..c7ca1903 100644 --- a/src/components/MatchResultDetailFields/gameSystems/TeamYankeeV2MatchResultDetailFields/TeamYankeeV2MatchResultDetailFields.tsx +++ b/src/components/MatchResultDetailFields/gameSystems/TeamYankeeV2MatchResultDetailFields/TeamYankeeV2MatchResultDetailFields.tsx @@ -22,11 +22,13 @@ import styles from './TeamYankeeV2MatchResultDetailFields.module.scss'; export interface TeamYankeeV2MatchResultDetailFieldsProps { className?: string; playerOptions: InputSelectOption[]; + disableFactions?: boolean; } export const TeamYankeeV2MatchResultDetailFields = ({ className, playerOptions, + disableFactions = false, }: TeamYankeeV2MatchResultDetailFieldsProps): JSX.Element => { const [showScoreOverride, setShowScoreOverride] = useState(false); @@ -35,6 +37,8 @@ export const TeamYankeeV2MatchResultDetailFields = ({ const { details } = values; const gameSystemConfig = validateGameSystemConfig(GameSystem.TeamYankeeV2, values.gameSystemConfig); + const factionOptions = getFactionOptions(); + const battlePlanOptions = getBattlePlanOptions(); const missionOptions = useMissionOptions(gameSystemConfig, details.player0BattlePlan, details.player1BattlePlan); // TODO: Don't allow winner 'None' for certain outcome types. @@ -80,24 +84,22 @@ export const TeamYankeeV2MatchResultDetailFields = ({ return (
- {/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */} - - + + - +
- {/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */} - - + + - + diff --git a/src/components/MatchResultForm/MatchResultForm.schema.ts b/src/components/MatchResultForm/MatchResultForm.schema.ts index e5f8f3a2..45ede6e2 100644 --- a/src/components/MatchResultForm/MatchResultForm.schema.ts +++ b/src/components/MatchResultForm/MatchResultForm.schema.ts @@ -1,32 +1,31 @@ import { DeepPartial } from 'react-hook-form'; -import { GameSystem } from '@ianpaschal/combat-command-game-systems/common'; +import { GameSystem, getGameSystem } from '@ianpaschal/combat-command-game-systems/common'; import { matchResultDetails as flamesOfWarV4MatchResultDetails } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4'; import { z } from 'zod'; import { MatchResult, UserId } from '~/api'; -import { gameSystemConfig, getGameSystemConfigDefaultValues } from '~/components/GameSystemConfigFields'; -import { matchResultDetails } from '~/components/MatchResultDetailFields'; - -export const matchResultFormSchema = z.object({ - - // Handled by or - player0Placeholder: z.optional(z.string()), - player0UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)), - player1Placeholder: z.optional(z.string()), - player1UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)), - - // Handled by - details: matchResultDetails, - - // Handled by - gameSystemConfig, - - // Non-editable - gameSystem: z.nativeEnum(GameSystem), - playedAt: z.union([z.string(), z.number()]), // TODO: not visible, enable later -}); +import { getGameSystemConfigDefaultValues } from '~/components/GameSystemConfigFields'; + +export const getMatchResultFormSchema = (gameSystem: GameSystem) => { + const { matchResultDetails, gameSystemConfig } = getGameSystem(gameSystem); + return z.object({ + + // Handled by or + player0Placeholder: z.optional(z.string()), + player0UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)), + player1Placeholder: z.optional(z.string()), + player1UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)), + + // Non-editable + gameSystem: z.nativeEnum(GameSystem), + playedAt: z.union([z.string(), z.number()]), // TODO: not visible, enable later + }).extend({ + details: matchResultDetails.schema, + gameSystemConfig: gameSystemConfig.schema, + }); +}; -export type MatchResultSubmitData = z.infer; +export type MatchResultSubmitData = z.infer>; export type MatchResultFormData = DeepPartial; diff --git a/src/components/MatchResultForm/MatchResultForm.tsx b/src/components/MatchResultForm/MatchResultForm.tsx index 63f4210d..a24156ec 100644 --- a/src/components/MatchResultForm/MatchResultForm.tsx +++ b/src/components/MatchResultForm/MatchResultForm.tsx @@ -26,6 +26,7 @@ import { Label } from '~/components/generic/Label'; import { Separator } from '~/components/generic/Separator'; import { MatchResultDetailFields } from '~/components/MatchResultDetailFields'; import { MatchResultDetails } from '~/components/MatchResultDetails'; +import { toast } from '~/components/ToastProvider'; import { useAsyncState } from '~/hooks/useAsyncState'; import { useCreateMatchResult, useUpdateMatchResult } from '~/services/matchResults'; import { useGetActiveTournamentPairingsByUser } from '~/services/tournamentPairings'; @@ -36,8 +37,8 @@ import { TournamentPlayerFields } from './components/TournamentPlayerFields'; import { usePlayerDisplayNames } from './MatchResultForm.hooks'; import { defaultValues, + getMatchResultFormSchema, MatchResultFormData, - matchResultFormSchema, } from './MatchResultForm.schema'; import styles from './MatchResultForm.module.scss'; @@ -109,7 +110,7 @@ export const MatchResultForm = ({ defaultValues: { ...defaultValues, ...(matchResult ? (() => { - const result = matchResultFormSchema.safeParse(matchResult); + const result = getMatchResultFormSchema(matchResult.gameSystem as GameSystem).safeParse(matchResult); if (!result.success) { console.error('MatchResultForm schema parsing failed:', result.error); console.error('MatchResult data:', matchResult); @@ -131,7 +132,14 @@ export const MatchResultForm = ({ const selectedGameSystem = form.watch('gameSystem'); const onSubmit: SubmitHandler = (formData): void => { - const data = validateForm(matchResultFormSchema, formData, form.setError); + if (!formData.gameSystem) { + toast.error('Cannot Submit Match Result', { + description: 'Data could not be validated because game system is not set.', + }); + return; + } + const schema = getMatchResultFormSchema(formData.gameSystem); + const data = validateForm(schema, formData, form.setError); if (data) { if (matchResult) { updateMatchResult({ ...data, id: matchResult._id, playedAt: matchResult.playedAt }); @@ -213,7 +221,10 @@ export const MatchResultForm = ({ )} {isTournament ? ( - + ) : ( <> diff --git a/src/components/MatchResultForm/components/TournamentPlayerFields/TournamentPlayerFields.tsx b/src/components/MatchResultForm/components/TournamentPlayerFields/TournamentPlayerFields.tsx index d958cabc..9159947f 100644 --- a/src/components/MatchResultForm/components/TournamentPlayerFields/TournamentPlayerFields.tsx +++ b/src/components/MatchResultForm/components/TournamentPlayerFields/TournamentPlayerFields.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useFormContext } from 'react-hook-form'; -import { TournamentPairingId } from '~/api'; +import { Tournament, TournamentPairingId } from '~/api'; import { FormField } from '~/components/generic/Form'; import { InputSelect } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; @@ -12,22 +12,34 @@ import styles from './TournamentPlayerFields.module.scss'; export interface TournamentPlayerFieldsProps { tournamentPairingId: TournamentPairingId; + tournament?: Tournament | null; } export const TournamentPlayerFields = ({ tournamentPairingId, + tournament, }: TournamentPlayerFieldsProps): JSX.Element => { const { setValue, watch } = useFormContext(); - const { player0UserId, player1UserId } = watch(); + const player0UserId = watch('player0UserId'); + const player1UserId = watch('player1UserId'); + const player0Faction = watch('details.player0Faction'); + const player1Faction = watch('details.player1Faction'); const { data: selectedPairing } = useGetTournamentPairing({ id: tournamentPairingId }); - const player0Label = selectedPairing?.tournamentCompetitor0 ? `${selectedPairing.tournamentCompetitor0.displayName} Player` : 'Player 1'; - const player1Label = selectedPairing?.tournamentCompetitor1 ? `${selectedPairing.tournamentCompetitor1.displayName} Player` : 'Bye Placeholder'; + const player0Label = tournament?.useTeams && selectedPairing?.tournamentCompetitor0 ? `${selectedPairing.tournamentCompetitor0.displayName} Player` : 'Player 1'; + const player1Label = tournament?.useTeams && selectedPairing?.tournamentCompetitor1 ? `${selectedPairing.tournamentCompetitor1.displayName} Player` : 'Player 2'; const player0Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor0); const player1Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor1); + const player0Registration = useMemo(() => ( + selectedPairing?.tournamentCompetitor0?.registrations.find((r) => r.userId === player0UserId) + ), [selectedPairing?.tournamentCompetitor0, player0UserId]); + const player1Registration = useMemo(() => ( + selectedPairing?.tournamentCompetitor1?.registrations.find((r) => r.userId === player1UserId) + ), [selectedPairing?.tournamentCompetitor1, player1UserId]); + // Automatically set "Player 1" if possible useEffect(() => { if (player0Options && player0Options.length === 1 && player0UserId !== player0Options[0].value) { @@ -35,6 +47,13 @@ export const TournamentPlayerFields = ({ } }, [player0Options, player0UserId, setValue]); + // Automatically set "Player 1" faction if possible + useEffect(() => { + if (player0Registration && player0Registration.factions.length > 0 && !player0Faction) { + setValue('details.player0Faction', player0Registration.factions[0]); + } + }, [player0Registration, player0Faction, setValue]); + // Automatically set "Player 2" if possible useEffect(() => { if (player1Options && player1Options.length === 1 && player1UserId !== player1Options[0].value) { @@ -42,6 +61,13 @@ export const TournamentPlayerFields = ({ } }, [player1Options, player1UserId, setValue]); + // Automatically set "Player 2" faction if possible + useEffect(() => { + if (player1Registration && player1Registration.factions.length > 0 && !player1Faction) { + setValue('details.player1Faction', player1Registration.factions[0]); + } + }, [player1Registration, player1Faction, setValue]); + if (!selectedPairing) { return
Loading...
; } diff --git a/src/utils/validateForm.ts b/src/utils/validateForm.ts index 102681c0..5d40656c 100644 --- a/src/utils/validateForm.ts +++ b/src/utils/validateForm.ts @@ -17,7 +17,7 @@ export const validateForm = { const fieldPath = issue.path.join('.') as Path; setError(fieldPath, { message: issue.message }); - toast.error('Error', { description: issue.message }); + toast.error('Error', { description: 'Please check the form for errors.' }); }); } return result.data;