diff --git a/app/db/models/calendar.server.ts b/app/db/models/calendar.server.ts index d39e6964f..67662fd2b 100644 --- a/app/db/models/calendar.server.ts +++ b/app/db/models/calendar.server.ts @@ -7,6 +7,7 @@ import type { Badge, CalendarEventTag, CalendarEventBadge, + CalendarEventWinner, } from "../types"; const createStm = sql.prepare(` @@ -130,6 +131,53 @@ export const update = sql.transaction( } ); +const updateCalendarEventParticipantsCountStm = sql.prepare(` + update "CalendarEvent" + set "participantCount" = $participantCount + where "id" = $eventId +`); + +const deleteCalendarEventWinnersByEventIdStm = sql.prepare(` + delete from "CalendarEventWinner" + where "eventId" = $eventId +`); + +const insertCalendarEventWinnerStm = sql.prepare(` + insert into "CalendarEventWinner" ( + "eventId", + "teamName", + "placement", + "userId", + "name" + ) values ( + $eventId, + $teamName, + $placement, + $userId, + $name + ) +`); +export const upsertReportedScores = sql.transaction( + ({ + eventId, + participantCount, + winners, + }: { + eventId: CalendarEvent["id"]; + participantCount: CalendarEvent["participantCount"]; + winners: Array< + Pick + >; + }) => { + updateCalendarEventParticipantsCountStm.run({ eventId, participantCount }); + deleteCalendarEventWinnersByEventIdStm.run({ eventId }); + + for (const winner of winners) { + insertCalendarEventWinnerStm.run({ ...winner, eventId }); + } + } +); + const findAllBetweenTwoTimestampsStm = sql.prepare(` select "CalendarEvent"."name", diff --git a/app/routes/calendar/$id/report-winners.tsx b/app/routes/calendar/$id/report-winners.tsx index bdb9d25a7..e8ef6120c 100644 --- a/app/routes/calendar/$id/report-winners.tsx +++ b/app/routes/calendar/$id/report-winners.tsx @@ -82,18 +82,36 @@ const reportWinnersActionSchema = z.object({ ), }); -export const action: ActionFunction = async ({ request }) => { - const data = await safeParseRequestFormData({ +const reportWinnersParamsSchema = z.object({ + id: z.preprocess(actualNumber, id), +}); + +export const action: ActionFunction = async ({ request, params }) => { + const parsedParams = reportWinnersParamsSchema.parse(params); + const parsedInput = await safeParseRequestFormData({ request, schema: reportWinnersActionSchema, }); - if (!data.success) { + if (!parsedInput.success) { return { - errors: data.error.errors.map((error) => error.message), + errors: parsedInput.errors, }; } + db.calendarEvents.upsertReportedScores({ + eventId: parsedParams.id, + participantCount: parsedInput.data.participantsCount, + winners: parsedInput.data.team.flatMap((t) => + t.players.map((player) => ({ + teamName: t.teamName, + placement: t.placement, + userId: typeof player !== "string" ? player.id : null, + name: typeof player === "string" ? player : null, + })) + ), + }); + return null; }; @@ -102,9 +120,7 @@ export const handle = { }; export const loader = async ({ request, params }: LoaderArgs) => { - const parsedParams = z - .object({ id: z.preprocess(actualNumber, id) }) - .parse(params); + const parsedParams = reportWinnersParamsSchema.parse(params); const user = await requireUser(request); const event = notFoundIfFalsy(db.calendarEvents.findById(parsedParams.id)); @@ -267,8 +283,7 @@ function Team({ id={teamNameId} value={results.teamName} onChange={handleTeamNameChange} - // xxx: why doesn't work? - minLength={1} + required={!hidden} maxLength={CALENDAR_EVENT_RESULT.MAX_TEAM_NAME_LENGTH} /> @@ -279,7 +294,7 @@ function Team({ value={results.placement} type="number" onChange={handlePlacementChange} - min={1} + required={!hidden} max={CALENDAR_EVENT_RESULT.MAX_TEAM_PLACEMENT} className="w-24" /> diff --git a/app/utils/remix.ts b/app/utils/remix.ts index 3fc2fdb53..2f381cef4 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -44,8 +44,23 @@ export async function safeParseRequestFormData({ }: { request: Request; schema: T; -}): Promise, T>> { - return schema.safeParse(formDataToObject(await request.formData())); +}): Promise< + { success: true; data: z.infer } | { success: false; errors: string[] } +> { + const parsed = schema.safeParse(formDataToObject(await request.formData())); + + // this implementation is somewhat redundant but it's the only way I got types to work nice + if (!parsed.success) { + return { + success: false, + errors: parsed.error.errors.map((error) => error.message), + }; + } + + return { + success: true, + data: parsed.data, + }; } function formDataToObject(formData: FormData) {