From c2ea1e25173241e0ccb2fe545957b3b3978524fa Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 3 Aug 2025 11:26:27 +0200 Subject: [PATCH 1/3] feat: Add edition year to tournaments --- convex/_model/tournaments/fields.ts | 1 + .../TournamentCard/TournamentCard.tsx | 9 +++++++- .../TournamentForm/TournamentForm.schema.ts | 2 ++ .../components/GeneralFields.module.scss | 6 +++++ .../components/GeneralFields.tsx | 23 ++++++++++++++++--- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/convex/_model/tournaments/fields.ts b/convex/_model/tournaments/fields.ts index 91a9e73f..a09ada12 100644 --- a/convex/_model/tournaments/fields.ts +++ b/convex/_model/tournaments/fields.ts @@ -22,6 +22,7 @@ export const editableFields = { requireRealNames: v.boolean(), organizerUserIds: v.array(v.id('users')), rulesPackUrl: v.optional(v.string()), + editionYear: v.optional(v.number()), // Denormalized so that we can filter tournaments by game system, and all related fields. // The duplicate data is worth the efficiency in querying. diff --git a/src/components/TournamentCard/TournamentCard.tsx b/src/components/TournamentCard/TournamentCard.tsx index 0c5f3e77..25bdfedf 100644 --- a/src/components/TournamentCard/TournamentCard.tsx +++ b/src/components/TournamentCard/TournamentCard.tsx @@ -42,6 +42,13 @@ export const TournamentCard = ({ const showContextMenu = user && tournament.organizerUserIds.includes(user._id); + const getTournamentTitle = (): string => { + if (tournament.editionYear) { + return `${tournament.title} ${tournament.editionYear}`; + } + return tournament.title; + }; + return ( @@ -55,7 +62,7 @@ export const TournamentCard = ({ )}
-

{tournament.title}

+

{getTournamentTitle()}

{showContextMenu && ( diff --git a/src/components/TournamentForm/TournamentForm.schema.ts b/src/components/TournamentForm/TournamentForm.schema.ts index 9d74b901..67c07124 100644 --- a/src/components/TournamentForm/TournamentForm.schema.ts +++ b/src/components/TournamentForm/TournamentForm.schema.ts @@ -18,6 +18,7 @@ export const tournamentFormSchema = z.object({ // General title: z.string().min(5, 'Title must be at least 5 characters.').max(40, 'Titles are limited to 50 characters.'), + editionYear: z.coerce.number(), description: z.string().min(10, 'Please add a description.').max(1000, 'Descriptions are limited to 1000 characters.'), rulesPackUrl: z.union([z.string().url('Please provide a valid URL.'), z.literal('')]), location: z.object({ @@ -112,4 +113,5 @@ export const defaultValues: DeepPartial = { rankingFactors: ['total_wins'], logoStorageId: '', bannerStorageId: '', + editionYear: 2025, }; diff --git a/src/components/TournamentForm/components/GeneralFields.module.scss b/src/components/TournamentForm/components/GeneralFields.module.scss index 7288b774..422f04a1 100644 --- a/src/components/TournamentForm/components/GeneralFields.module.scss +++ b/src/components/TournamentForm/components/GeneralFields.module.scss @@ -2,6 +2,12 @@ .GeneralFields { @include flex.column; + + &_TitleRow { + display: grid; + grid-template-columns: 1fr 6rem; + gap: 1rem; + } } .Stackable { diff --git a/src/components/TournamentForm/components/GeneralFields.tsx b/src/components/TournamentForm/components/GeneralFields.tsx index 437fa825..fc63c035 100644 --- a/src/components/TournamentForm/components/GeneralFields.tsx +++ b/src/components/TournamentForm/components/GeneralFields.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import { FormField } from '~/components/generic/Form'; import { InputDateTime } from '~/components/generic/InputDateTime'; import { InputLocation } from '~/components/generic/InputLocation'; +import { InputSelect } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; import { InputTextArea } from '~/components/generic/InputTextArea'; import { Separator } from '~/components/generic/Separator'; @@ -26,11 +27,27 @@ export const GeneralFields = ({ // Once a tournament is active, lock some fields const disableFields = !['draft', 'published'].includes(status); + const getYearOptions = () => { + const options = []; + for (let i = 2010; i < 2027; i += 1) { + options.push({ + value: i, + label: i.toString(), + }); + } + return options; + }; + return (
- - - +
+ + + + + + +
From edde7870e5b7dee54053cf48cc6f16f4650cfeda Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 3 Aug 2025 16:14:31 +0200 Subject: [PATCH 2/3] feat: Improve edition year handling --- .../TournamentCard/TournamentCard.tsx | 10 ++-------- .../TournamentForm/TournamentForm.schema.ts | 2 +- .../components/GeneralFields.module.scss | 9 +++++++++ .../components/GeneralFields.tsx | 20 +++++++++++++++---- .../TournamentDetailBanner.tsx | 11 +++++----- src/style/_text.scss | 1 + src/utils/common/getTournamentDisplayName.ts | 8 ++++++++ 7 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 src/utils/common/getTournamentDisplayName.ts diff --git a/src/components/TournamentCard/TournamentCard.tsx b/src/components/TournamentCard/TournamentCard.tsx index 25bdfedf..59d62a47 100644 --- a/src/components/TournamentCard/TournamentCard.tsx +++ b/src/components/TournamentCard/TournamentCard.tsx @@ -10,6 +10,7 @@ import { TournamentInfoBlock } from '~/components/TournamentInfoBlock/'; import { TournamentProvider } from '~/components/TournamentProvider'; import { useElementSize } from '~/hooks/useElementSize'; import { MIN_WIDTH_TABLET, PATHS } from '~/settings'; +import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import styles from './TournamentCard.module.scss'; @@ -42,13 +43,6 @@ export const TournamentCard = ({ const showContextMenu = user && tournament.organizerUserIds.includes(user._id); - const getTournamentTitle = (): string => { - if (tournament.editionYear) { - return `${tournament.title} ${tournament.editionYear}`; - } - return tournament.title; - }; - return ( @@ -62,7 +56,7 @@ export const TournamentCard = ({ )}
-

{getTournamentTitle()}

+

{getTournamentDisplayName(tournament)}

{showContextMenu && ( diff --git a/src/components/TournamentForm/TournamentForm.schema.ts b/src/components/TournamentForm/TournamentForm.schema.ts index 67c07124..fc5252bd 100644 --- a/src/components/TournamentForm/TournamentForm.schema.ts +++ b/src/components/TournamentForm/TournamentForm.schema.ts @@ -17,7 +17,7 @@ import { fowV4GameSystemConfigDefaultValues, fowV4GameSystemConfigFormSchema } f export const tournamentFormSchema = z.object({ // General - title: z.string().min(5, 'Title must be at least 5 characters.').max(40, 'Titles are limited to 50 characters.'), + title: z.string().min(3, 'Title must be at least 3 characters.').max(40, 'Titles are limited to 40 characters.'), editionYear: z.coerce.number(), description: z.string().min(10, 'Please add a description.').max(1000, 'Descriptions are limited to 1000 characters.'), rulesPackUrl: z.union([z.string().url('Please provide a valid URL.'), z.literal('')]), diff --git a/src/components/TournamentForm/components/GeneralFields.module.scss b/src/components/TournamentForm/components/GeneralFields.module.scss index 422f04a1..0b12aca4 100644 --- a/src/components/TournamentForm/components/GeneralFields.module.scss +++ b/src/components/TournamentForm/components/GeneralFields.module.scss @@ -1,4 +1,5 @@ @use "/src/style/flex"; +@use "/src/style/text"; .GeneralFields { @include flex.column; @@ -8,6 +9,14 @@ grid-template-columns: 1fr 6rem; gap: 1rem; } + + &_Preview { + @include flex.column($gap: 0.25rem); + + &_Description { + @include text.ui($muted: true); + } + } } .Stackable { diff --git a/src/components/TournamentForm/components/GeneralFields.tsx b/src/components/TournamentForm/components/GeneralFields.tsx index fc63c035..e01e3a9f 100644 --- a/src/components/TournamentForm/components/GeneralFields.tsx +++ b/src/components/TournamentForm/components/GeneralFields.tsx @@ -4,12 +4,13 @@ import clsx from 'clsx'; import { FormField } from '~/components/generic/Form'; import { InputDateTime } from '~/components/generic/InputDateTime'; import { InputLocation } from '~/components/generic/InputLocation'; -import { InputSelect } from '~/components/generic/InputSelect'; +import { InputSelect, InputSelectOption } from '~/components/generic/InputSelect'; import { InputText } from '~/components/generic/InputText'; import { InputTextArea } from '~/components/generic/InputTextArea'; import { Separator } from '~/components/generic/Separator'; import { InputSingleFile } from '~/components/InputSingleFile/InputSingleFile'; import { TournamentFormData } from '~/components/TournamentForm/TournamentForm.schema'; +import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import styles from './GeneralFields.module.scss'; @@ -22,22 +23,27 @@ export const GeneralFields = ({ className, status = 'draft', }: GeneralFieldsProps): JSX.Element => { - const { resetField } = useFormContext(); + const { resetField, watch } = useFormContext(); // Once a tournament is active, lock some fields const disableFields = !['draft', 'published'].includes(status); const getYearOptions = () => { - const options = []; + const options: InputSelectOption[] = [{ + value: '0', + label: 'None', + }]; for (let i = 2010; i < 2027; i += 1) { options.push({ - value: i, + value: i.toString(), label: i.toString(), }); } return options; }; + const tournament = watch(); + return (
@@ -48,6 +54,12 @@ export const GeneralFields = ({
+ {tournament.editionYear > 0 && ( +
+

Your tournament's name will render as:

+

{getTournamentDisplayName(tournament)}

+
+ )} diff --git a/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.tsx b/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.tsx index d25c4786..6492872d 100644 --- a/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.tsx +++ b/src/pages/TournamentDetailPage/components/TournamentDetailBanner/TournamentDetailBanner.tsx @@ -1,23 +1,24 @@ import { useTournament } from '~/components/TournamentProvider'; import { TournamentTimer } from '~/components/TournamentTimer'; import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; +import { getTournamentDisplayName } from '~/utils/common/getTournamentDisplayName'; import styles from './TournamentDetailBanner.module.scss'; export const TournamentDetailBanner = (): JSX.Element => { - const { title, logoUrl, currentRound, status } = useTournament(); + const tournament = useTournament(); const [deviceSize] = useDeviceSize(); - const showTimer = status === 'active' && currentRound !== undefined; + const showTimer = tournament.status === 'active' && tournament.currentRound !== undefined; const compact = deviceSize < DeviceSize.Default; return (
- {logoUrl && ( - + {tournament.logoUrl && ( + )} -

{title}

+

{getTournamentDisplayName(tournament)}

{showTimer && (
diff --git a/src/style/_text.scss b/src/style/_text.scss index c635e6e6..c607fa6a 100644 --- a/src/style/_text.scss +++ b/src/style/_text.scss @@ -11,6 +11,7 @@ } @if $muted == true { + font-weight: 300; color: var(--text-color-muted); } diff --git a/src/utils/common/getTournamentDisplayName.ts b/src/utils/common/getTournamentDisplayName.ts new file mode 100644 index 00000000..cfe021bd --- /dev/null +++ b/src/utils/common/getTournamentDisplayName.ts @@ -0,0 +1,8 @@ +import { Tournament } from '~/api'; + +export const getTournamentDisplayName = (tournament: Pick): string => { + if (tournament.editionYear) { + return `${tournament.title} ${tournament.editionYear}`; + } + return tournament.title; +}; From d6306222b60aa6d38f9de2941e0b1f345fc624d1 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Sun, 3 Aug 2025 16:28:53 +0200 Subject: [PATCH 3/3] fix: Replace accidentally removed mission matrix options --- .../components/GameConfigFields.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/FowV4MatchResultForm/components/GameConfigFields.tsx b/src/components/FowV4MatchResultForm/components/GameConfigFields.tsx index a8a7de70..ea3d490e 100644 --- a/src/components/FowV4MatchResultForm/components/GameConfigFields.tsx +++ b/src/components/FowV4MatchResultForm/components/GameConfigFields.tsx @@ -5,7 +5,7 @@ import { fowV4EraOptions, fowV4LessonsFromTheFrontVersionOptions, fowV4MissionPackOptions, - getFowV4MissionsByMissionPackId, + getFowV4MissionMatrixOptionsByMissionPackId, } from '~/api'; import { Animate } from '~/components/generic/Animate'; import { FormField } from '~/components/generic/Form'; @@ -35,10 +35,7 @@ export const GameConfigFields = ({ const missionPackId = watch(`${formPath}.missionPackId`); - const missionOptions = (getFowV4MissionsByMissionPackId(missionPackId) ?? []).map((mission) => ({ - label: mission.displayName, - value: mission.id, - })); + const missionMatrixOptions = getFowV4MissionMatrixOptionsByMissionPackId(missionPackId); return (
@@ -67,8 +64,8 @@ export const GameConfigFields = ({ - - + +