diff --git a/client/src/app/dashboard/(application)/extra/page.tsx b/client/src/app/dashboard/(application)/extra/page.tsx index 6aade8a6..9071ff19 100644 --- a/client/src/app/dashboard/(application)/extra/page.tsx +++ b/client/src/app/dashboard/(application)/extra/page.tsx @@ -1,6 +1,7 @@ "use client" import { dietaryRequirementOptions, dietaryRequirementSchema } from "@durhack/durhack-common/input/dietary-requirement" +import { pizzaFlavorSchema, pizzaFlavourOptions } from "@durhack/durhack-common/input/pizza-flavor" import { Form, FormControl, @@ -22,9 +23,9 @@ import { import { Textarea } from "@durhack/web-components/ui/textarea" import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" +import { useState } from "react" import { useForm } from "react-hook-form" import { z } from "zod/v4" - import { FormSkeleton } from "@/components/dashboard/form-skeleton" import { FormSubmitButton } from "@/components/dashboard/form-submit-button" import type { Application } from "@/hooks/use-application" @@ -36,7 +37,9 @@ type ExtraDetailsFormFields = { tShirtSize: string hackathonExperience: string dietaryRequirements: string[] + pizzaFlavors: string[] accessRequirements: string + midnightSnack: string } const extraDetailsFormSchema = z.object({ @@ -52,6 +55,8 @@ const extraDetailsFormSchema = z.object({ ) return mutuallyExclusivePreferences.length <= 1 }, "Please select at most one of 'vegan', 'vegetarian', 'pescatarian'."), + midnightSnack: z.enum(["pizza", "alternative", "nothing"], { message: "Please select a midnight snack." }), + pizzaFlavors: z.array(pizzaFlavorSchema), accessRequirements: z.string().trim(), }) @@ -72,6 +77,8 @@ function ExtraDetailsForm({ application }: { application: Application }) { hackathonExperience: application.hackathonExperience ?? "", dietaryRequirements: application.dietaryRequirements ?? [], accessRequirements: application.accessRequirements ?? "", + midnightSnack: application.midnightSnack ?? "", + pizzaFlavors: application.pizzaFlavors ?? [], }, }) @@ -81,6 +88,13 @@ function ExtraDetailsForm({ application }: { application: Application }) { if (application.tShirtSize == null) router.push("/dashboard/education") } + const [snackChoice, setSnackChoice] = useState(application.midnightSnack ?? "") + + function handleSnackChoice(value: string): void { + setSnackChoice(value) + form.setValue("midnightSnack", value) + } + return (
@@ -169,6 +183,53 @@ function ExtraDetailsForm({ application }: { application: Application }) { /> +
+ ( + + Midnight Snack (on us) + + We normally offer pizza as a midnight snack. If you are not able to have pizza, select "alternative" + and we will get in touch with you! + + + + + + )} + /> + + {snackChoice === "pizza" && ( + ( + + Please choose your preferred pizza toppings/flavours + + + + + )} + /> + )} +
+
}) { - const params = React.use(props.params); + const params = React.use(props.params) const { toast } = useToast() const { diff --git a/common/src/input/pizza-flavor.ts b/common/src/input/pizza-flavor.ts new file mode 100644 index 00000000..ec8f913e --- /dev/null +++ b/common/src/input/pizza-flavor.ts @@ -0,0 +1,19 @@ +import { z } from "zod/v4" +import { recordEntries } from "@/util/record-entries" + +export const pizzaFlavorSchema = z.enum(["margherita", "pepperoni"]) + +export type PizzaFlavor = z.output + +const PizzaFlavourMetadata: Record = { + margherita: { label: "Margherita" }, + pepperoni: { label: "Pepperoni" }, +} + +export const pizzaFlavourOptions = recordEntries(PizzaFlavourMetadata).map(([value, metadata]) => ({ + value, + label: metadata.label, +})) satisfies Array<{ + value: PizzaFlavor + label: string +}> diff --git a/common/src/types/application.ts b/common/src/types/application.ts index a39d249c..9ffaf1d1 100644 --- a/common/src/types/application.ts +++ b/common/src/types/application.ts @@ -1,5 +1,6 @@ import type { DisciplineOfStudy } from "@/input/discipline-of-study" import type { DietaryRequirement } from "@/input/dietary-requirement" +import { PizzaFlavor } from "@/input/pizza-flavor"; export type { DisciplineOfStudy, DietaryRequirement } @@ -23,6 +24,8 @@ export type Application = { university: string | null graduationYear: number | null disciplinesOfStudy: null | DisciplineOfStudy[] + midnightSnack: null | "pizza" | "alternative" | "nothing" + pizzaFlavors: null | PizzaFlavor[] levelOfStudy: | null | "secondary" diff --git a/server/prisma/migrations/20250728121812_midnight_snack/migration.sql b/server/prisma/migrations/20250728121812_midnight_snack/migration.sql new file mode 100644 index 00000000..044982ac --- /dev/null +++ b/server/prisma/migrations/20250728121812_midnight_snack/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "MidnightSnack" AS ENUM ('pizza', 'alternative', 'nothing'); + +-- AlterTable +ALTER TABLE "UserInfo" ADD COLUMN "midnight_snack" "MidnightSnack"; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 0ea8d237..fb73d806 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -62,6 +62,7 @@ model UserInfo { ethnicity Ethnicity? hackathonExperience HackathonExperience? @map("hackathon_experience") accessRequirements String? @map("access_requirements") @db.Text() + midnightSnack MidnightSnack? @map("midnight_snack") updatedAt DateTime @updatedAt @map("updated_at") } @@ -151,3 +152,9 @@ enum HackathonExperience { threeToSeven @map("three_to_seven") // "hack wizard" eightOrMore @map("eight_or_more") // "hackathon guru" } + +enum MidnightSnack { + pizza + alternative + nothing +} diff --git a/server/src/routes/application/application-handlers.ts b/server/src/routes/application/application-handlers.ts index 2dbbc257..101d88af 100644 --- a/server/src/routes/application/application-handlers.ts +++ b/server/src/routes/application/application-handlers.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict" import { parse as parsePath } from "node:path/posix" import { type DietaryRequirement, dietaryRequirementSchema } from "@durhack/durhack-common/input/dietary-requirement" import { type DisciplineOfStudy, disciplineOfStudySchema } from "@durhack/durhack-common/input/discipline-of-study" +import { type PizzaFlavor, pizzaFlavorSchema } from "@durhack/durhack-common/input/pizza-flavor" import type { Application } from "@durhack/durhack-common/types/application" import { ClientError, HttpStatus } from "@otterhttp/errors" import type { ContentType, ParsedFormFieldFile } from "@otterhttp/parsec" @@ -96,6 +97,10 @@ const extraDetailsFormSchema = z.object({ ) return mutuallyExclusivePreferences.length <= 1 }, "Please select at most one of 'vegan', 'vegetarian', 'pescatarian'."), + midnightSnack: z.enum(["pizza", "alternative", "nothing"], { + message: "Please select your midnight snack preference.", + }), + pizzaFlavors: z.array(pizzaFlavorSchema).min(1, { message: "Please select at least one pizza flavor." }), accessRequirements: z.string().trim(), }) @@ -154,6 +159,9 @@ class ApplicationHandlers { const dietaryRequirements = userFlags .filter((flag) => flag.flagName.startsWith("dietary-requirement:")) .map((flag) => flag.flagName.slice(20) as DietaryRequirement) + const pizzaFlavors = userFlags + .filter((flag) => flag.flagName.startsWith("pizza-flavor:")) + .map((flag) => flag.flagName.slice(13) as PizzaFlavor) const { phone_number, @@ -190,6 +198,8 @@ class ApplicationHandlers { disciplinesOfStudy: disciplinesOfStudy, tShirtSize: (userInfo?.tShirtSize?.trimEnd() as Application["tShirtSize"] | null | undefined) ?? null, dietaryRequirements: dietaryRequirements, + midnightSnack: userInfo?.midnightSnack ?? null, + pizzaFlavors: pizzaFlavors, accessRequirements: userInfo?.accessRequirements ?? null, countryOfResidence: userInfo?.countryOfResidence ?? null, consents: userConsents.map((consent) => ({ name: consent.consentName, choice: consent.choice })), @@ -307,15 +317,21 @@ class ApplicationHandlers { tShirtSize: payload.tShirtSize, hackathonExperience: payload.hackathonExperience, accessRequirements: payload.accessRequirements || undefined, + midnightSnack: payload.midnightSnack, + } + + if (payload.midnightSnack !== "pizza") { + prisma.userFlag.deleteMany({ + where: { userId: user.keycloakUserId, flagName: { startsWith: "pizza-flavor:" } }, + }) + payload.pizzaFlavors = [] } await prisma.$transaction([ prisma.userFlag.deleteMany({ where: { userId: user.keycloakUserId, - flagName: { - startsWith: "dietary-requirement:", - }, + OR: [{ flagName: { startsWith: "dietary-requirement:" } }, { flagName: { startsWith: "pizza-flavor:" } }], }, }), prisma.user.update({ @@ -337,6 +353,14 @@ class ApplicationHandlers { }, }), ), + ...payload.pizzaFlavors.map((item) => + prisma.userFlag.create({ + data: { + userId: user.keycloakUserId, + flagName: `pizza-flavor:${item}`, + }, + }), + ), ]) response.sendStatus(200) }