Skip to content

Commit 126b299

Browse files
authored
#287 Auto-fill faction field on match results if player faction is known (#299)
1 parent 86867df commit 126b299

9 files changed

Lines changed: 103 additions & 69 deletions

File tree

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"@dnd-kit/sortable": "^10.0.0",
3030
"@fontsource/figtree": "^5.1.0",
3131
"@hookform/resolvers": "^3.9.0",
32-
"@ianpaschal/combat-command-components": "^1.8.3",
33-
"@ianpaschal/combat-command-game-systems": "^1.4.0",
32+
"@ianpaschal/combat-command-components": "^1.8.4",
33+
"@ianpaschal/combat-command-game-systems": "^1.4.1",
3434
"@mapbox/search-js-core": "^1.0.0-beta.25",
3535
"@radix-ui/colors": "^3.0.0",
3636
"@react-hook/window-size": "^3.1.1",

src/components/MatchResultDetailFields/MatchResultDetailFields.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import { CompatibleFormData } from './MatchResultDetailFields.types';
99

1010
export interface MatchResultDetailFieldsProps {
1111
className?: string;
12+
disableFactions?: boolean;
1213
}
1314

14-
export const MatchResultDetailFields = ({
15-
className,
16-
}: MatchResultDetailFieldsProps): JSX.Element => {
15+
export const MatchResultDetailFields = (props: MatchResultDetailFieldsProps): JSX.Element => {
1716
const { reset, watch, getFieldState } = useFormContext<CompatibleFormData>();
1817
const gameSystem = watch('gameSystem');
1918

@@ -36,20 +35,15 @@ export const MatchResultDetailFields = ({
3635
*/
3736
const playerOptions = usePlayerOptions();
3837

39-
const sharedProps = {
40-
className,
41-
playerOptions,
42-
};
43-
4438
if (gameSystem === GameSystem.FlamesOfWarV4) {
4539
return (
46-
<FlamesOfWarV4MatchResultDetailFields {...sharedProps} />
40+
<FlamesOfWarV4MatchResultDetailFields {...props} playerOptions={playerOptions} />
4741
);
4842
}
4943

5044
if (gameSystem === GameSystem.TeamYankeeV2) {
5145
return (
52-
<TeamYankeeV2MatchResultDetailFields {...sharedProps} />
46+
<TeamYankeeV2MatchResultDetailFields {...props} playerOptions={playerOptions} />
5347
);
5448
}
5549

src/components/MatchResultDetailFields/gameSystems/FlamesOfWarV4MatchResultDetailFields/FlamesOfWarV4MatchResultDetailFields.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import styles from './FlamesOfWarV4MatchResultDetailFields.module.scss';
2222
export interface FlamesOfWarV4MatchResultDetailFieldsProps {
2323
className?: string;
2424
playerOptions: InputSelectOption<number>[];
25+
disableFactions?: boolean;
2526
}
2627

2728
export const FlamesOfWarV4MatchResultDetailFields = ({
2829
className,
2930
playerOptions,
31+
disableFactions = false,
3032
}: FlamesOfWarV4MatchResultDetailFieldsProps): JSX.Element => {
3133
const [showScoreOverride, setShowScoreOverride] = useState<boolean>(false);
3234

@@ -35,6 +37,8 @@ export const FlamesOfWarV4MatchResultDetailFields = ({
3537
const { details } = values;
3638
const gameSystemConfig = validateGameSystemConfig(GameSystem.FlamesOfWarV4, values.gameSystemConfig);
3739

40+
const factionOptions = getFactionOptions();
41+
const battlePlanOptions = getBattlePlanOptions();
3842
const missionOptions = useMissionOptions(gameSystemConfig, details.player0BattlePlan, details.player1BattlePlan);
3943

4044
// TODO: Don't allow winner 'None' for certain outcome types.
@@ -80,24 +84,22 @@ export const FlamesOfWarV4MatchResultDetailFields = ({
8084
return (
8185
<div className={clsx(styles.FlamesOfWarV4MatchResultDetailFields, className)}>
8286
<div className={styles.FlamesOfWarV4MatchResultDetailFields_Player0Section}>
83-
{/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */}
84-
<FormField name="details.player0Faction" label="Faction">
85-
<InputSelect options={getFactionOptions()} />
87+
<FormField name="details.player0Faction" label="Faction" disabled={disableFactions}>
88+
<InputSelect options={factionOptions} />
8689
</FormField>
8790
<FormField name="details.player0BattlePlan" label="Battle Plan">
88-
<InputSelect options={getBattlePlanOptions()} />
91+
<InputSelect options={battlePlanOptions} />
8992
</FormField>
9093
<FormField name="details.player0UnitsLost" label="Units Lost">
9194
<InputText type="number" />
9295
</FormField>
9396
</div>
9497
<div className={styles.FlamesOfWarV4MatchResultDetailFields_Player1Section}>
95-
{/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */}
96-
<FormField name="details.player1Faction" label="Faction">
97-
<InputSelect options={getFactionOptions()} />
98+
<FormField name="details.player1Faction" label="Faction" disabled={disableFactions}>
99+
<InputSelect options={factionOptions} />
98100
</FormField>
99101
<FormField name="details.player1BattlePlan" label="Battle Plan">
100-
<InputSelect options={getBattlePlanOptions()} />
102+
<InputSelect options={battlePlanOptions} />
101103
</FormField>
102104
<FormField name="details.player1UnitsLost" label="Units Lost">
103105
<InputText type="number" />

src/components/MatchResultDetailFields/gameSystems/TeamYankeeV2MatchResultDetailFields/TeamYankeeV2MatchResultDetailFields.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import styles from './TeamYankeeV2MatchResultDetailFields.module.scss';
2222
export interface TeamYankeeV2MatchResultDetailFieldsProps {
2323
className?: string;
2424
playerOptions: InputSelectOption<number>[];
25+
disableFactions?: boolean;
2526
}
2627

2728
export const TeamYankeeV2MatchResultDetailFields = ({
2829
className,
2930
playerOptions,
31+
disableFactions = false,
3032
}: TeamYankeeV2MatchResultDetailFieldsProps): JSX.Element => {
3133
const [showScoreOverride, setShowScoreOverride] = useState<boolean>(false);
3234

@@ -35,6 +37,8 @@ export const TeamYankeeV2MatchResultDetailFields = ({
3537
const { details } = values;
3638
const gameSystemConfig = validateGameSystemConfig(GameSystem.TeamYankeeV2, values.gameSystemConfig);
3739

40+
const factionOptions = getFactionOptions();
41+
const battlePlanOptions = getBattlePlanOptions();
3842
const missionOptions = useMissionOptions(gameSystemConfig, details.player0BattlePlan, details.player1BattlePlan);
3943

4044
// TODO: Don't allow winner 'None' for certain outcome types.
@@ -80,24 +84,22 @@ export const TeamYankeeV2MatchResultDetailFields = ({
8084
return (
8185
<div className={clsx(styles.TeamYankeeV2MatchResultDetailFields, className)}>
8286
<div className={styles.TeamYankeeV2MatchResultDetailFields_Player0Section}>
83-
{/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */}
84-
<FormField name="details.player0Faction" label="Faction">
85-
<InputSelect options={getFactionOptions()} />
87+
<FormField name="details.player0Faction" label="Faction" disabled={disableFactions}>
88+
<InputSelect options={factionOptions} />
8689
</FormField>
8790
<FormField name="details.player0BattlePlan" label="Battle Plan">
88-
<InputSelect options={getBattlePlanOptions()} />
91+
<InputSelect options={battlePlanOptions} />
8992
</FormField>
9093
<FormField name="details.player0UnitsLost" label="Units Lost">
9194
<InputText type="number" />
9295
</FormField>
9396
</div>
9497
<div className={styles.TeamYankeeV2MatchResultDetailFields_Player1Section}>
95-
{/* TODO: AUTO-FILTER OPTIONS TO 1 USING LIST INFO */}
96-
<FormField name="details.player1Faction" label="Faction">
97-
<InputSelect options={getFactionOptions()} />
98+
<FormField name="details.player1Faction" label="Faction" disabled={disableFactions}>
99+
<InputSelect options={factionOptions} />
98100
</FormField>
99101
<FormField name="details.player1BattlePlan" label="Battle Plan">
100-
<InputSelect options={getBattlePlanOptions()} />
102+
<InputSelect options={battlePlanOptions} />
101103
</FormField>
102104
<FormField name="details.player1UnitsLost" label="Units Lost">
103105
<InputText type="number" />

src/components/MatchResultForm/MatchResultForm.schema.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
import { DeepPartial } from 'react-hook-form';
2-
import { GameSystem } from '@ianpaschal/combat-command-game-systems/common';
2+
import { GameSystem, getGameSystem } from '@ianpaschal/combat-command-game-systems/common';
33
import { matchResultDetails as flamesOfWarV4MatchResultDetails } from '@ianpaschal/combat-command-game-systems/flamesOfWarV4';
44
import { z } from 'zod';
55

66
import { MatchResult, UserId } from '~/api';
7-
import { gameSystemConfig, getGameSystemConfigDefaultValues } from '~/components/GameSystemConfigFields';
8-
import { matchResultDetails } from '~/components/MatchResultDetailFields';
9-
10-
export const matchResultFormSchema = z.object({
11-
12-
// Handled by <TournamentPlayersForm /> or <SingleMatchPlayersForm />
13-
player0Placeholder: z.optional(z.string()),
14-
player0UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)),
15-
player1Placeholder: z.optional(z.string()),
16-
player1UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)),
17-
18-
// Handled by <MatchResultDetailsForm />
19-
details: matchResultDetails,
20-
21-
// Handled by <GameSystemConfigForm />
22-
gameSystemConfig,
23-
24-
// Non-editable
25-
gameSystem: z.nativeEnum(GameSystem),
26-
playedAt: z.union([z.string(), z.number()]), // TODO: not visible, enable later
27-
});
7+
import { getGameSystemConfigDefaultValues } from '~/components/GameSystemConfigFields';
8+
9+
export const getMatchResultFormSchema = (gameSystem: GameSystem) => {
10+
const { matchResultDetails, gameSystemConfig } = getGameSystem(gameSystem);
11+
return z.object({
12+
13+
// Handled by <TournamentPlayersForm /> or <SingleMatchPlayersForm />
14+
player0Placeholder: z.optional(z.string()),
15+
player0UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)),
16+
player1Placeholder: z.optional(z.string()),
17+
player1UserId: z.optional(z.string().transform((val) => val.length ? val as UserId : undefined)),
18+
19+
// Non-editable
20+
gameSystem: z.nativeEnum(GameSystem),
21+
playedAt: z.union([z.string(), z.number()]), // TODO: not visible, enable later
22+
}).extend({
23+
details: matchResultDetails.schema,
24+
gameSystemConfig: gameSystemConfig.schema,
25+
});
26+
};
2827

29-
export type MatchResultSubmitData = z.infer<typeof matchResultFormSchema>;
28+
export type MatchResultSubmitData = z.infer<ReturnType<typeof getMatchResultFormSchema>>;
3029

3130
export type MatchResultFormData = DeepPartial<MatchResultSubmitData>;
3231

src/components/MatchResultForm/MatchResultForm.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Label } from '~/components/generic/Label';
2626
import { Separator } from '~/components/generic/Separator';
2727
import { MatchResultDetailFields } from '~/components/MatchResultDetailFields';
2828
import { MatchResultDetails } from '~/components/MatchResultDetails';
29+
import { toast } from '~/components/ToastProvider';
2930
import { useAsyncState } from '~/hooks/useAsyncState';
3031
import { useCreateMatchResult, useUpdateMatchResult } from '~/services/matchResults';
3132
import { useGetActiveTournamentPairingsByUser } from '~/services/tournamentPairings';
@@ -36,8 +37,8 @@ import { TournamentPlayerFields } from './components/TournamentPlayerFields';
3637
import { usePlayerDisplayNames } from './MatchResultForm.hooks';
3738
import {
3839
defaultValues,
40+
getMatchResultFormSchema,
3941
MatchResultFormData,
40-
matchResultFormSchema,
4142
} from './MatchResultForm.schema';
4243

4344
import styles from './MatchResultForm.module.scss';
@@ -109,7 +110,7 @@ export const MatchResultForm = ({
109110
defaultValues: {
110111
...defaultValues,
111112
...(matchResult ? (() => {
112-
const result = matchResultFormSchema.safeParse(matchResult);
113+
const result = getMatchResultFormSchema(matchResult.gameSystem as GameSystem).safeParse(matchResult);
113114
if (!result.success) {
114115
console.error('MatchResultForm schema parsing failed:', result.error);
115116
console.error('MatchResult data:', matchResult);
@@ -131,7 +132,14 @@ export const MatchResultForm = ({
131132
const selectedGameSystem = form.watch('gameSystem');
132133

133134
const onSubmit: SubmitHandler<MatchResultFormData> = (formData): void => {
134-
const data = validateForm(matchResultFormSchema, formData, form.setError);
135+
if (!formData.gameSystem) {
136+
toast.error('Cannot Submit Match Result', {
137+
description: 'Data could not be validated because game system is not set.',
138+
});
139+
return;
140+
}
141+
const schema = getMatchResultFormSchema(formData.gameSystem);
142+
const data = validateForm(schema, formData, form.setError);
135143
if (data) {
136144
if (matchResult) {
137145
updateMatchResult({ ...data, id: matchResult._id, playedAt: matchResult.playedAt });
@@ -213,7 +221,10 @@ export const MatchResultForm = ({
213221
</>
214222
)}
215223
{isTournament ? (
216-
<TournamentPlayerFields tournamentPairingId={tournamentPairingId} />
224+
<TournamentPlayerFields
225+
tournament={tournament}
226+
tournamentPairingId={tournamentPairingId}
227+
/>
217228
) : (
218229
<>
219230
<FormField name="gameSystem" label="Game System" disabled={gameSystemOptions.length < 2}>

src/components/MatchResultForm/components/TournamentPlayerFields/TournamentPlayerFields.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useMemo } from 'react';
22
import { useFormContext } from 'react-hook-form';
33

4-
import { TournamentPairingId } from '~/api';
4+
import { Tournament, TournamentPairingId } from '~/api';
55
import { FormField } from '~/components/generic/Form';
66
import { InputSelect } from '~/components/generic/InputSelect';
77
import { InputText } from '~/components/generic/InputText';
@@ -12,36 +12,62 @@ import styles from './TournamentPlayerFields.module.scss';
1212

1313
export interface TournamentPlayerFieldsProps {
1414
tournamentPairingId: TournamentPairingId;
15+
tournament?: Tournament | null;
1516
}
1617

1718
export const TournamentPlayerFields = ({
1819
tournamentPairingId,
20+
tournament,
1921
}: TournamentPlayerFieldsProps): JSX.Element => {
2022
const { setValue, watch } = useFormContext();
21-
const { player0UserId, player1UserId } = watch();
23+
const player0UserId = watch('player0UserId');
24+
const player1UserId = watch('player1UserId');
25+
const player0Faction = watch('details.player0Faction');
26+
const player1Faction = watch('details.player1Faction');
2227

2328
const { data: selectedPairing } = useGetTournamentPairing({ id: tournamentPairingId });
2429

25-
const player0Label = selectedPairing?.tournamentCompetitor0 ? `${selectedPairing.tournamentCompetitor0.displayName} Player` : 'Player 1';
26-
const player1Label = selectedPairing?.tournamentCompetitor1 ? `${selectedPairing.tournamentCompetitor1.displayName} Player` : 'Bye Placeholder';
30+
const player0Label = tournament?.useTeams && selectedPairing?.tournamentCompetitor0 ? `${selectedPairing.tournamentCompetitor0.displayName} Player` : 'Player 1';
31+
const player1Label = tournament?.useTeams && selectedPairing?.tournamentCompetitor1 ? `${selectedPairing.tournamentCompetitor1.displayName} Player` : 'Player 2';
2732

2833
const player0Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor0);
2934
const player1Options = getCompetitorPlayerOptions(selectedPairing?.tournamentCompetitor1);
3035

36+
const player0Registration = useMemo(() => (
37+
selectedPairing?.tournamentCompetitor0?.registrations.find((r) => r.userId === player0UserId)
38+
), [selectedPairing?.tournamentCompetitor0, player0UserId]);
39+
const player1Registration = useMemo(() => (
40+
selectedPairing?.tournamentCompetitor1?.registrations.find((r) => r.userId === player1UserId)
41+
), [selectedPairing?.tournamentCompetitor1, player1UserId]);
42+
3143
// Automatically set "Player 1" if possible
3244
useEffect(() => {
3345
if (player0Options && player0Options.length === 1 && player0UserId !== player0Options[0].value) {
3446
setValue('player0UserId', player0Options[0].value);
3547
}
3648
}, [player0Options, player0UserId, setValue]);
3749

50+
// Automatically set "Player 1" faction if possible
51+
useEffect(() => {
52+
if (player0Registration && player0Registration.factions.length > 0 && !player0Faction) {
53+
setValue('details.player0Faction', player0Registration.factions[0]);
54+
}
55+
}, [player0Registration, player0Faction, setValue]);
56+
3857
// Automatically set "Player 2" if possible
3958
useEffect(() => {
4059
if (player1Options && player1Options.length === 1 && player1UserId !== player1Options[0].value) {
4160
setValue('player1UserId', player1Options[0].value);
4261
}
4362
}, [player1Options, player1UserId, setValue]);
4463

64+
// Automatically set "Player 2" faction if possible
65+
useEffect(() => {
66+
if (player1Registration && player1Registration.factions.length > 0 && !player1Faction) {
67+
setValue('details.player1Faction', player1Registration.factions[0]);
68+
}
69+
}, [player1Registration, player1Faction, setValue]);
70+
4571
if (!selectedPairing) {
4672
return <div>Loading...</div>;
4773
}

src/utils/validateForm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const validateForm = <T extends ZodTypeAny, TFieldValues extends FieldVal
1717
result.error.issues.forEach((issue) => {
1818
const fieldPath = issue.path.join('.') as Path<TFieldValues>;
1919
setError(fieldPath, { message: issue.message });
20-
toast.error('Error', { description: issue.message });
20+
toast.error('Error', { description: 'Please check the form for errors.' });
2121
});
2222
}
2323
return result.data;

0 commit comments

Comments
 (0)