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/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 = ({ - - + + diff --git a/src/components/TournamentCard/TournamentCard.tsx b/src/components/TournamentCard/TournamentCard.tsx index 0c5f3e77..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'; @@ -55,7 +56,7 @@ export const TournamentCard = ({ )}
-

{tournament.title}

+

{getTournamentDisplayName(tournament)}

{showContextMenu && ( diff --git a/src/components/TournamentForm/TournamentForm.schema.ts b/src/components/TournamentForm/TournamentForm.schema.ts index 9d74b901..fc5252bd 100644 --- a/src/components/TournamentForm/TournamentForm.schema.ts +++ b/src/components/TournamentForm/TournamentForm.schema.ts @@ -17,7 +17,8 @@ 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('')]), 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..0b12aca4 100644 --- a/src/components/TournamentForm/components/GeneralFields.module.scss +++ b/src/components/TournamentForm/components/GeneralFields.module.scss @@ -1,7 +1,22 @@ @use "/src/style/flex"; +@use "/src/style/text"; .GeneralFields { @include flex.column; + + &_TitleRow { + display: grid; + 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 437fa825..e01e3a9f 100644 --- a/src/components/TournamentForm/components/GeneralFields.tsx +++ b/src/components/TournamentForm/components/GeneralFields.tsx @@ -4,11 +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, 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'; @@ -21,16 +23,43 @@ 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: InputSelectOption[] = [{ + value: '0', + label: 'None', + }]; + for (let i = 2010; i < 2027; i += 1) { + options.push({ + value: i.toString(), + label: i.toString(), + }); + } + return options; + }; + + const tournament = watch(); + return (
- - - +
+ + + + + + +
+ {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; +};