Skip to content

Commit d822fa2

Browse files
authored
#134 feat: Add edition year to tournaments (#147)
* feat: Add edition year to tournaments * fix: Replace accidentally removed mission matrix options
1 parent 9cb868d commit d822fa2

9 files changed

Lines changed: 73 additions & 18 deletions

File tree

convex/_model/tournaments/fields.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const editableFields = {
2222
requireRealNames: v.boolean(),
2323
organizerUserIds: v.array(v.id('users')),
2424
rulesPackUrl: v.optional(v.string()),
25+
editionYear: v.optional(v.number()),
2526

2627
// Denormalized so that we can filter tournaments by game system, and all related fields.
2728
// The duplicate data is worth the efficiency in querying.

src/components/FowV4MatchResultForm/components/GameConfigFields.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
fowV4EraOptions,
66
fowV4LessonsFromTheFrontVersionOptions,
77
fowV4MissionPackOptions,
8-
getFowV4MissionsByMissionPackId,
8+
getFowV4MissionMatrixOptionsByMissionPackId,
99
} from '~/api';
1010
import { Animate } from '~/components/generic/Animate';
1111
import { FormField } from '~/components/generic/Form';
@@ -35,10 +35,7 @@ export const GameConfigFields = ({
3535

3636
const missionPackId = watch(`${formPath}.missionPackId`);
3737

38-
const missionOptions = (getFowV4MissionsByMissionPackId(missionPackId) ?? []).map((mission) => ({
39-
label: mission.displayName,
40-
value: mission.id,
41-
}));
38+
const missionMatrixOptions = getFowV4MissionMatrixOptionsByMissionPackId(missionPackId);
4239

4340
return (
4441
<div className={styles.Root}>
@@ -67,8 +64,8 @@ export const GameConfigFields = ({
6764
<FormField name={`${formPath}.missionPackId`} label="Mission Pack" disabled={fowV4MissionPackOptions.length < 2}>
6865
<InputSelect options={fowV4MissionPackOptions} />
6966
</FormField>
70-
<FormField name={`${formPath}.missionMatrixId`} label="Mission Matrix" disabled={missionOptions.length < 2}>
71-
<InputSelect options={missionOptions} />
67+
<FormField name={`${formPath}.missionMatrixId`} label="Mission Matrix" disabled={missionMatrixOptions.length < 2}>
68+
<InputSelect options={missionMatrixOptions} />
7269
</FormField>
7370
<FormField name={`${formPath}.useExperimentalMissions`} label="Prefer experimental missions">
7471
<Switch />

src/components/TournamentCard/TournamentCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TournamentInfoBlock } from '~/components/TournamentInfoBlock/';
1010
import { TournamentProvider } from '~/components/TournamentProvider';
1111
import { useElementSize } from '~/hooks/useElementSize';
1212
import { MIN_WIDTH_TABLET, PATHS } from '~/settings';
13+
import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName';
1314

1415
import styles from './TournamentCard.module.scss';
1516

@@ -55,7 +56,7 @@ export const TournamentCard = ({
5556
)}
5657
</div>
5758
<div className={styles.TournamentCard_Title}>
58-
<h2>{tournament.title}</h2>
59+
<h2>{getTournamentDisplayName(tournament)}</h2>
5960
<div className={styles.TournamentCard_Buttons}>
6061
{showContextMenu && (
6162
<TournamentContextMenu />

src/components/TournamentForm/TournamentForm.schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import { fowV4GameSystemConfigDefaultValues, fowV4GameSystemConfigFormSchema } f
1717
export const tournamentFormSchema = z.object({
1818

1919
// General
20-
title: z.string().min(5, 'Title must be at least 5 characters.').max(40, 'Titles are limited to 50 characters.'),
20+
title: z.string().min(3, 'Title must be at least 3 characters.').max(40, 'Titles are limited to 40 characters.'),
21+
editionYear: z.coerce.number(),
2122
description: z.string().min(10, 'Please add a description.').max(1000, 'Descriptions are limited to 1000 characters.'),
2223
rulesPackUrl: z.union([z.string().url('Please provide a valid URL.'), z.literal('')]),
2324
location: z.object({
@@ -112,4 +113,5 @@ export const defaultValues: DeepPartial<TournamentFormData> = {
112113
rankingFactors: ['total_wins'],
113114
logoStorageId: '',
114115
bannerStorageId: '',
116+
editionYear: 2025,
115117
};

src/components/TournamentForm/components/GeneralFields.module.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
@use "/src/style/flex";
2+
@use "/src/style/text";
23

34
.GeneralFields {
45
@include flex.column;
6+
7+
&_TitleRow {
8+
display: grid;
9+
grid-template-columns: 1fr 6rem;
10+
gap: 1rem;
11+
}
12+
13+
&_Preview {
14+
@include flex.column($gap: 0.25rem);
15+
16+
&_Description {
17+
@include text.ui($muted: true);
18+
}
19+
}
520
}
621

722
.Stackable {

src/components/TournamentForm/components/GeneralFields.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import clsx from 'clsx';
44
import { FormField } from '~/components/generic/Form';
55
import { InputDateTime } from '~/components/generic/InputDateTime';
66
import { InputLocation } from '~/components/generic/InputLocation';
7+
import { InputSelect, InputSelectOption } from '~/components/generic/InputSelect';
78
import { InputText } from '~/components/generic/InputText';
89
import { InputTextArea } from '~/components/generic/InputTextArea';
910
import { Separator } from '~/components/generic/Separator';
1011
import { InputSingleFile } from '~/components/InputSingleFile/InputSingleFile';
1112
import { TournamentFormData } from '~/components/TournamentForm/TournamentForm.schema';
13+
import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName';
1214

1315
import styles from './GeneralFields.module.scss';
1416

@@ -21,16 +23,43 @@ export const GeneralFields = ({
2123
className,
2224
status = 'draft',
2325
}: GeneralFieldsProps): JSX.Element => {
24-
const { resetField } = useFormContext<TournamentFormData>();
26+
const { resetField, watch } = useFormContext<TournamentFormData>();
2527

2628
// Once a tournament is active, lock some fields
2729
const disableFields = !['draft', 'published'].includes(status);
2830

31+
const getYearOptions = () => {
32+
const options: InputSelectOption<string>[] = [{
33+
value: '0',
34+
label: 'None',
35+
}];
36+
for (let i = 2010; i < 2027; i += 1) {
37+
options.push({
38+
value: i.toString(),
39+
label: i.toString(),
40+
});
41+
}
42+
return options;
43+
};
44+
45+
const tournament = watch();
46+
2947
return (
3048
<div className={clsx(styles.GeneralFields, className)}>
31-
<FormField name="title" label="Title" description="Avoid including points and other rules in the title." disabled={disableFields}>
32-
<InputText type="text" />
33-
</FormField>
49+
<div className={styles.GeneralFields_TitleRow}>
50+
<FormField name="title" label="Title" description="Avoid including points and other rules in the title." disabled={disableFields}>
51+
<InputText type="text" />
52+
</FormField>
53+
<FormField name="editionYear" label="Year" disabled={disableFields}>
54+
<InputSelect options={getYearOptions()} />
55+
</FormField>
56+
</div>
57+
{tournament.editionYear > 0 && (
58+
<div className={styles.GeneralFields_Preview}>
59+
<p className={styles.GeneralFields_Preview_Description}>Your tournament's name will render as:</p>
60+
<h2>{getTournamentDisplayName(tournament)}</h2>
61+
</div>
62+
)}
3463
<FormField name="description" label="Description" disabled={disableFields}>
3564
<InputTextArea />
3665
</FormField>

src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import { useTournament } from '~/components/TournamentProvider';
22
import { TournamentTimer } from '~/components/TournamentTimer';
33
import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize';
4+
import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName';
45

56
import styles from './TournamentDetailBanner.module.scss';
67

78
export const TournamentDetailBanner = (): JSX.Element => {
8-
const { title, logoUrl, currentRound, status } = useTournament();
9+
const tournament = useTournament();
910
const [deviceSize] = useDeviceSize();
1011

11-
const showTimer = status === 'active' && currentRound !== undefined;
12+
const showTimer = tournament.status === 'active' && tournament.currentRound !== undefined;
1213
const compact = deviceSize < DeviceSize.Default;
1314

1415
return (
1516
<div className={styles.TournamentDetailBanner} data-compact={compact} data-timer={showTimer}>
1617
<div className={styles.TournamentDetailBanner_Title}>
17-
{logoUrl && (
18-
<img className={styles.TournamentDetailBanner_Logo} src={logoUrl} />
18+
{tournament.logoUrl && (
19+
<img className={styles.TournamentDetailBanner_Logo} src={tournament.logoUrl} />
1920
)}
20-
<h1>{title}</h1>
21+
<h1>{getTournamentDisplayName(tournament)}</h1>
2122
</div>
2223
{showTimer && (
2324
<div className={styles.TournamentDetailBanner_TimerSection}>

src/style/_text.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
}
1212

1313
@if $muted == true {
14+
font-weight: 300;
1415
color: var(--text-color-muted);
1516
}
1617

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Tournament } from '~/api';
2+
3+
export const getTournamentDisplayName = (tournament: Pick<Tournament, 'editionYear'|'title'>): string => {
4+
if (tournament.editionYear) {
5+
return `${tournament.title} ${tournament.editionYear}`;
6+
}
7+
return tournament.title;
8+
};

0 commit comments

Comments
 (0)