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;
+};