Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion client/src/app/dashboard/(application)/extra/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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"
Expand All @@ -36,7 +37,9 @@ type ExtraDetailsFormFields = {
tShirtSize: string
hackathonExperience: string
dietaryRequirements: string[]
pizzaFlavors: string[]
accessRequirements: string
midnightSnack: string
}

const extraDetailsFormSchema = z.object({
Expand All @@ -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(),
})

Expand All @@ -72,6 +77,8 @@ function ExtraDetailsForm({ application }: { application: Application }) {
hackathonExperience: application.hackathonExperience ?? "",
dietaryRequirements: application.dietaryRequirements ?? [],
accessRequirements: application.accessRequirements ?? "",
midnightSnack: application.midnightSnack ?? "",
pizzaFlavors: application.pizzaFlavors ?? [],
},
})

Expand All @@ -81,6 +88,13 @@ function ExtraDetailsForm({ application }: { application: Application }) {
if (application.tShirtSize == null) router.push("/dashboard/education")
}

const [snackChoice, setSnackChoice] = useState<string>(application.midnightSnack ?? "")

function handleSnackChoice(value: string): void {
setSnackChoice(value)
form.setValue("midnightSnack", value)
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
Expand Down Expand Up @@ -169,6 +183,53 @@ function ExtraDetailsForm({ application }: { application: Application }) {
/>
</div>

<div className="mb-4">
<FormField
control={form.control}
name="midnightSnack"
render={({ field: { ref, ...field } }) => (
<FormItem>
<FormLabel>Midnight Snack (on us)</FormLabel>
<FormDescription>
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!
</FormDescription>
<Select onValueChange={handleSnackChoice} {...field}>
<FormControl>
<SelectTrigger ref={ref}>
<SelectValueViewport>
<SelectValue placeholder="Select..." />
</SelectValueViewport>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="pizza">Pizza</SelectItem>
<SelectItem value="alternative">Alternative</SelectItem>
<SelectItem value="nothing">Nothing</SelectItem>
</SelectContent>
</Select>

<FormMessage />
</FormItem>
)}
/>

{snackChoice === "pizza" && (
<FormField
control={form.control}
name="pizzaFlavors"
render={({ field }) => (
<FormItem>
<FormDescription>Please choose your preferred pizza toppings/flavours</FormDescription>
<FormControl>
<MultiSelect {...field} options={pizzaFlavourOptions} hidePlaceholderWhenSelected />
</FormControl>
</FormItem>
)}
/>
)}
</div>

<div className="mb-4">
<FormField
control={form.control}
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/profile/[userId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function UserAttribute({ children, className, ...props }: React.HTMLAttributes<H
}

export default async function ProfilePage(props: { params: Promise<{ userId: string }> }) {
const params = React.use(props.params);
const params = React.use(props.params)
const { toast } = useToast()

const {
Expand Down
19 changes: 19 additions & 0 deletions common/src/input/pizza-flavor.ts
Original file line number Diff line number Diff line change
@@ -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<typeof pizzaFlavorSchema>

const PizzaFlavourMetadata: Record<PizzaFlavor, { label: string }> = {
margherita: { label: "Margherita" },
pepperoni: { label: "Pepperoni" },
}

export const pizzaFlavourOptions = recordEntries(PizzaFlavourMetadata).map(([value, metadata]) => ({
value,
label: metadata.label,
})) satisfies Array<{
value: PizzaFlavor
label: string
}>
3 changes: 3 additions & 0 deletions common/src/types/application.ts
Original file line number Diff line number Diff line change
@@ -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 }

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "MidnightSnack" AS ENUM ('pizza', 'alternative', 'nothing');

-- AlterTable
ALTER TABLE "UserInfo" ADD COLUMN "midnight_snack" "MidnightSnack";
7 changes: 7 additions & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
}
30 changes: 27 additions & 3 deletions server/src/routes/application/application-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(),
})

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 })),
Expand Down Expand Up @@ -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({
Expand All @@ -337,6 +353,14 @@ class ApplicationHandlers {
},
}),
),
...payload.pizzaFlavors.map((item) =>
prisma.userFlag.create({
data: {
userId: user.keycloakUserId,
flagName: `pizza-flavor:${item}`,
},
}),
),
])
response.sendStatus(200)
}
Expand Down