diff --git a/app/routes/calendar/$id/report-winners.tsx b/app/routes/calendar/$id/report-winners.tsx index 3fc44a16e..f57d9cdb8 100644 --- a/app/routes/calendar/$id/report-winners.tsx +++ b/app/routes/calendar/$id/report-winners.tsx @@ -1,5 +1,5 @@ import { type ActionFunction, type LoaderArgs, json } from "@remix-run/node"; -import { Form, useLoaderData } from "@remix-run/react"; +import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { z } from "zod"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; @@ -7,14 +7,12 @@ import { CALENDAR_EVENT_RESULT } from "~/constants"; import { db } from "~/db"; import { requireUser } from "~/modules/auth"; import { canReportCalendarEventWinners } from "~/permissions"; -import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { - actualNumber, - id, - processMany, - safeJSONParse, - toArray, -} from "~/utils/zod"; + notFoundIfFalsy, + safeParseRequestFormData, + validate, +} from "~/utils/remix"; +import { actualNumber, id, safeJSONParse, toArray } from "~/utils/zod"; import * as React from "react"; import type { User } from "~/db/types"; import { Button } from "~/components/Button"; @@ -22,6 +20,28 @@ import clsx from "clsx"; import { UserCombobox } from "~/components/Combobox"; import { FormMessage } from "~/components/FormMessage"; +const playersSchema = z + .array( + z.union([ + z.string().min(1).max(CALENDAR_EVENT_RESULT.MAX_PLAYER_NAME_LENGTH), + z.object({ id }), + ]) + ) + .nonempty({ message: "Each team must have at least one player." }) + .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: "Can't have the same player twice in the same team.", + } + ); + const reportWinnersActionSchema = z.object({ participantsCount: z.preprocess( actualNumber, @@ -31,54 +51,47 @@ const reportWinnersActionSchema = z.object({ .positive() .max(CALENDAR_EVENT_RESULT.MAX_PARTICIPANTS_COUNT) ), - team: z - .array( - z.preprocess( - processMany(safeJSONParse, toArray), - 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: z - .array( - z.union([ - z - .string() - .min(1) - .max(CALENDAR_EVENT_RESULT.MAX_PLAYER_NAME_LENGTH), - z.object({ id }), - ]) - ) - .nonempty() - .max(CALENDAR_EVENT_RESULT.MAX_PLAYERS_LENGTH), - }) + 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, + }) + ) ) - ) - // don't allow repeating same team name - .refine( - (val) => val.length === new Set(val.map((team) => team.teamName)).size - ), + .refine( + (val) => val.length === new Set(val.map((team) => team.teamName)).size, + { message: "Each team needs a unique name." } + ) + ), }); export const action: ActionFunction = async ({ request }) => { - // xxx: return nice errors to frontend to render - // try { - // const data = await parseRequestFormData({ - // request, - // schema: reportWinnersActionSchema, - // }); - // } catch (e) { - // return null; - // } + const data = await safeParseRequestFormData({ + request, + schema: reportWinnersActionSchema, + }); + + if (!data.success) { + return { + errors: data.error.errors.map((error) => error.message), + }; + } return null; }; @@ -110,6 +123,9 @@ export const loader = async ({ request, params }: LoaderArgs) => { export default function ReportWinnersPage() { const data = useLoaderData(); + const actionData = useActionData(); + + console.log({ actionData }); return (
@@ -252,8 +268,9 @@ function Team({ id={teamNameId} value={results.teamName} onChange={handleTeamNameChange} - min={1} - max={CALENDAR_EVENT_RESULT.MAX_TEAM_NAME_LENGTH} + // xxx: why doesn't work? + minLength={1} + maxLength={CALENDAR_EVENT_RESULT.MAX_TEAM_NAME_LENGTH} />
diff --git a/app/utils/remix.ts b/app/utils/remix.ts index 8dd159de4..3fc2fdb53 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -38,6 +38,16 @@ export async function parseRequestFormData({ } } +export async function safeParseRequestFormData({ + request, + schema, +}: { + request: Request; + schema: T; +}): Promise, T>> { + return schema.safeParse(formDataToObject(await request.formData())); +} + function formDataToObject(formData: FormData) { const result: Record = {};