mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-26 09:20:24 -05:00
293 lines
7.7 KiB
TypeScript
293 lines
7.7 KiB
TypeScript
import { z } from "zod";
|
|
import { type CalendarEventTag, TOURNAMENT_STAGE_TYPES } from "~/db/tables";
|
|
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
|
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
|
import * as Swiss from "~/features/tournament-bracket/core/Swiss";
|
|
import {
|
|
array,
|
|
checkboxGroup,
|
|
numberFieldOptional,
|
|
radioGroup,
|
|
textFieldOptional,
|
|
toggle,
|
|
userSearchOptional,
|
|
} from "~/form/fields";
|
|
import { gamesShort, versusShort } from "~/modules/in-game-lists/games";
|
|
import { modesShortWithSpecial } from "~/modules/in-game-lists/modes";
|
|
import {
|
|
actualNumber,
|
|
gamesShortSchema,
|
|
id,
|
|
modeShortWithSpecial,
|
|
safeJSONParse,
|
|
toArray,
|
|
} from "~/utils/zod";
|
|
import { CALENDAR_EVENT, CALENDAR_EVENT_RESULT } from "./calendar-constants";
|
|
import * as CalendarEvent from "./core/CalendarEvent";
|
|
|
|
export const calendarEventTagSchema = z
|
|
.string()
|
|
.refine((val) => CALENDAR_EVENT.TAGS.includes(val as CalendarEventTag));
|
|
|
|
const calendarFiltersPlainStringArr = z.array(z.string().max(100)).max(10);
|
|
const calendarFiltersIdsArr = z.array(id).max(10);
|
|
const calendarFilterGamesArr = z.array(gamesShortSchema).min(1).max(3);
|
|
const preferredStartTime = z.enum(["ANY", "EU", "NA", "AU"]);
|
|
const preferredVersus = z
|
|
.array(z.enum(versusShort))
|
|
.min(1)
|
|
.max(versusShort.length);
|
|
const modeArr = z
|
|
.array(modeShortWithSpecial)
|
|
.min(1)
|
|
.max(modesShortWithSpecial.length);
|
|
|
|
export const calendarFiltersSearchParamsSchema = z.object({
|
|
preferredStartTime: preferredStartTime.catch("ANY"),
|
|
tagsIncluded: z.array(calendarEventTagSchema).catch([]),
|
|
tagsExcluded: z.array(calendarEventTagSchema).catch([]),
|
|
isSendou: z.boolean().catch(false),
|
|
isRanked: z.boolean().catch(false),
|
|
orgsIncluded: calendarFiltersPlainStringArr.catch([]),
|
|
orgsExcluded: calendarFiltersPlainStringArr.catch([]),
|
|
authorIdsExcluded: calendarFiltersIdsArr.catch([]),
|
|
games: calendarFilterGamesArr.catch([...gamesShort]),
|
|
preferredVersus: preferredVersus.catch([...versusShort]),
|
|
modes: modeArr.catch([...modesShortWithSpecial]),
|
|
modesExact: z.boolean().catch(false),
|
|
minTeamCount: z.coerce.number().int().nonnegative().catch(0),
|
|
});
|
|
|
|
const TAGS_TO_OMIT: CalendarEventTag[] = [
|
|
"CARDS",
|
|
"SR",
|
|
"S1",
|
|
"S2",
|
|
"SZ",
|
|
"TW",
|
|
"ONES",
|
|
"DUOS",
|
|
"TRIOS",
|
|
];
|
|
|
|
const filterTags = CALENDAR_EVENT.TAGS.filter(
|
|
(tag) => !TAGS_TO_OMIT.includes(tag),
|
|
);
|
|
|
|
const tagItems = filterTags.map((tag) => ({
|
|
label: `options.tag.${tag}` as const,
|
|
value: tag,
|
|
}));
|
|
|
|
export const calendarFiltersFormSchema = z
|
|
.object({
|
|
modes: checkboxGroup({
|
|
label: "labels.buildModes",
|
|
items: [
|
|
{ label: "modes.TW", value: "TW" },
|
|
{ label: "modes.SZ", value: "SZ" },
|
|
{ label: "modes.TC", value: "TC" },
|
|
{ label: "modes.RM", value: "RM" },
|
|
{ label: "modes.CB", value: "CB" },
|
|
{ label: () => "Salmon Run", value: "SR" },
|
|
{ label: () => "Tricolor", value: "TB" },
|
|
],
|
|
minLength: 1,
|
|
}),
|
|
modesExact: toggle({
|
|
label: "labels.modesExact",
|
|
bottomText: "bottomTexts.modesExact",
|
|
}),
|
|
games: checkboxGroup({
|
|
label: "labels.games",
|
|
items: [
|
|
{ label: "options.game.S1", value: "S1" },
|
|
{ label: "options.game.S2", value: "S2" },
|
|
{ label: "options.game.S3", value: "S3" },
|
|
],
|
|
minLength: 1,
|
|
}),
|
|
preferredVersus: checkboxGroup({
|
|
label: "labels.vs",
|
|
items: [
|
|
{ label: () => "4v4", value: "4v4" },
|
|
{ label: () => "3v3", value: "3v3" },
|
|
{ label: () => "2v2", value: "2v2" },
|
|
{ label: () => "1v1", value: "1v1" },
|
|
],
|
|
minLength: 1,
|
|
}),
|
|
preferredStartTime: radioGroup({
|
|
label: "labels.startTime",
|
|
items: [
|
|
{ label: "options.startTime.any", value: "ANY" },
|
|
{ label: "options.startTime.eu", value: "EU" },
|
|
{ label: "options.startTime.na", value: "NA" },
|
|
{ label: "options.startTime.au", value: "AU" },
|
|
],
|
|
}),
|
|
tagsIncluded: checkboxGroup({
|
|
label: "labels.tagsIncluded",
|
|
items: tagItems,
|
|
}),
|
|
tagsExcluded: checkboxGroup({
|
|
label: "labels.tagsExcluded",
|
|
items: tagItems,
|
|
}),
|
|
isSendou: toggle({ label: "labels.onlySendouEvents" }),
|
|
isRanked: toggle({ label: "labels.onlyRankedEvents" }),
|
|
minTeamCount: numberFieldOptional({
|
|
label: "labels.minTeamCount",
|
|
}),
|
|
orgsIncluded: array({
|
|
label: "labels.orgsIncluded",
|
|
field: textFieldOptional({ maxLength: 100 }),
|
|
max: 10,
|
|
}),
|
|
orgsExcluded: array({
|
|
label: "labels.orgsExcluded",
|
|
field: textFieldOptional({ maxLength: 100 }),
|
|
max: 10,
|
|
}),
|
|
authorIdsExcluded: array({
|
|
label: "labels.authorIdsExcluded",
|
|
field: userSearchOptional({}),
|
|
max: 10,
|
|
}),
|
|
})
|
|
.superRefine((filters, ctx) => {
|
|
if (
|
|
filters.tagsIncluded.some((tag) => filters.tagsExcluded.includes(tag))
|
|
) {
|
|
ctx.addIssue({
|
|
path: ["tagsExcluded"],
|
|
message: "Can't include and exclude the same tag",
|
|
code: z.ZodIssueCode.custom,
|
|
});
|
|
}
|
|
|
|
if (filters.orgsIncluded.length > 0 && filters.orgsExcluded.length > 0) {
|
|
ctx.addIssue({
|
|
path: ["orgsExcluded"],
|
|
message: "Can't both include and exclude organizations",
|
|
code: z.ZodIssueCode.custom,
|
|
});
|
|
}
|
|
});
|
|
export const calendarFiltersSearchParamsObject = z.object({
|
|
filters: z
|
|
.preprocess(safeJSONParse, calendarFiltersSearchParamsSchema)
|
|
.catch(CalendarEvent.defaultFilters()),
|
|
});
|
|
|
|
const playersSchema = z
|
|
.array(
|
|
z.union([
|
|
z.string().min(1).max(CALENDAR_EVENT_RESULT.MAX_PLAYER_NAME_LENGTH),
|
|
z.object({ id }),
|
|
]),
|
|
)
|
|
.nonempty({ message: "forms.errors.emptyTeam" })
|
|
.max(CALENDAR_EVENT_RESULT.MAX_PLAYERS_LENGTH)
|
|
.refine(
|
|
(val) => {
|
|
const userIds = val.flatMap((user) =>
|
|
typeof user === "string" ? [] : user.id,
|
|
);
|
|
|
|
return userIds.length === new Set(userIds).size;
|
|
},
|
|
{
|
|
message: "forms.errors.duplicatePlayer",
|
|
},
|
|
);
|
|
|
|
export const reportWinnersActionSchema = z.object({
|
|
participantCount: z.preprocess(
|
|
actualNumber,
|
|
z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(CALENDAR_EVENT_RESULT.MAX_PARTICIPANTS_COUNT),
|
|
),
|
|
team: z.preprocess(
|
|
toArray,
|
|
z
|
|
.array(
|
|
z.preprocess(
|
|
safeJSONParse,
|
|
z.object({
|
|
teamName: z
|
|
.string()
|
|
.min(1)
|
|
.max(CALENDAR_EVENT_RESULT.MAX_TEAM_NAME_LENGTH),
|
|
placement: z.preprocess(
|
|
actualNumber,
|
|
z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(CALENDAR_EVENT_RESULT.MAX_TEAM_PLACEMENT),
|
|
),
|
|
players: playersSchema,
|
|
}),
|
|
),
|
|
)
|
|
.refine(
|
|
(val) => val.length === new Set(val.map((team) => team.teamName)).size,
|
|
{ message: "forms.errors.uniqueTeamName" },
|
|
),
|
|
),
|
|
});
|
|
|
|
export const bracketProgressionSchema = z.preprocess(
|
|
safeJSONParse,
|
|
z
|
|
.array(
|
|
z.object({
|
|
type: z.enum(TOURNAMENT_STAGE_TYPES),
|
|
name: z.string().min(1).max(TOURNAMENT.BRACKET_NAME_MAX_LENGTH),
|
|
settings: z
|
|
.object({
|
|
thirdPlaceMatch: z.boolean().optional(),
|
|
teamsPerGroup: z.number().int().optional(),
|
|
groupCount: z.number().int().optional(),
|
|
roundCount: z.number().int().optional(),
|
|
advanceThreshold: z.number().int().optional(),
|
|
})
|
|
.refine(
|
|
(settings) => {
|
|
if (settings.advanceThreshold) {
|
|
return Swiss.isValidAdvanceThreshold({
|
|
roundCount:
|
|
settings.roundCount ?? TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
|
advanceThreshold: settings.advanceThreshold,
|
|
});
|
|
}
|
|
return true;
|
|
},
|
|
{
|
|
message: "Invalid advance threshold for the given round count",
|
|
path: ["advanceThreshold"],
|
|
},
|
|
),
|
|
requiresCheckIn: z.boolean(),
|
|
startTime: z.number().optional(),
|
|
sources: z
|
|
.array(
|
|
z.object({
|
|
bracketIdx: z.number(),
|
|
placements: z.array(z.number()),
|
|
}),
|
|
)
|
|
.optional(),
|
|
}),
|
|
)
|
|
.refine(
|
|
(progression) =>
|
|
Progression.bracketsToValidationError(progression) === null,
|
|
"Invalid bracket progression",
|
|
),
|
|
);
|