diff --git a/package-lock.json b/package-lock.json index 507577c..7ebbedd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1157,7 +1157,6 @@ "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1197,7 +1196,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1542,7 +1540,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1593,7 +1590,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -1811,7 +1807,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2162,7 +2157,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3278,7 +3272,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3306,7 +3299,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3440,7 +3432,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3457,7 +3448,6 @@ "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" @@ -3786,7 +3776,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.12.tgz", "integrity": "sha512-d1R+3pFa39LXoHCsxHmV//D2pSFZlEMlnxCVQ54TlrQv+4o5pewJO0/Pc5MUp+j71PJrOrPJHTvREZJHn+ymDQ==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3880,8 +3869,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -3969,7 +3957,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4032,7 +4019,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/lib/registration.ts b/src/lib/registration.ts new file mode 100644 index 0000000..b0191d0 --- /dev/null +++ b/src/lib/registration.ts @@ -0,0 +1,88 @@ +export type RegistrationForm = { + firstName: string; + lastName: string; + + school: string; + + major: string; + gradYear: string; + + email: string; + dob: string; + + allergies: string[]; + allergiesOther: string; + + tshirtSize: string; + + projectIdea: string; +}; + +export type ValidationResult = { + ok: boolean; + errors: Record; + data?: RegistrationForm; +}; + + +export function countWords(text: string) { + return text.trim().split(/\s+/).filter(Boolean).length; +} + +export function validateRegistration(payload: RegistrationForm): ValidationResult { + const errors: Record = {}; + + const reqStr = (key: keyof RegistrationForm) => { + const v = payload?.[key]; + if (typeof v !== "string" || v.trim().length === 0) errors[String(key)] = "Required"; + }; + + reqStr("firstName"); + reqStr("lastName"); + reqStr("email"); + reqStr("dob"); + reqStr("major"); + reqStr("gradYear"); + reqStr("school"); + reqStr("tshirtSize"); + reqStr("projectIdea"); + + if (typeof payload?.email === "string" && !/^\S+@\S+\.\S+$/.test(payload.email)) { + errors.email = "Invalid email"; + } + + if (payload?.school === "__OTHER__") { + errors.schoolOther = "Please type your school"; + } + + if (payload?.allergies != null) { + if (!Array.isArray(payload.allergies) || payload.allergies.some((x: any) => typeof x !== "string")) { + errors.allergies = "Invalid allergies"; + } + } + + if (typeof payload?.projectIdea === "string") { + const words = countWords(payload.projectIdea); + if (words > 250) errors.projectIdea = `Too long (${words}/250 words)`; + } + + if (Object.keys(errors).length > 0) { + return { ok: false, errors }; + } + + const data: RegistrationForm = { + firstName: payload.firstName.trim(), + lastName: payload.lastName.trim(), + school: payload.school.trim(), + major: payload.major.trim(), + gradYear: payload.gradYear.trim(), + email: payload.email.trim(), + dob: payload.dob.trim(), + allergies: Array.isArray(payload.allergies) ? payload.allergies : [], + allergiesOther: (payload.allergiesOther ?? "").trim(), + tshirtSize: payload.tshirtSize.trim(), + projectIdea: payload.projectIdea.trim() + }; + + return { ok: true, errors: {}, data }; +} \ No newline at end of file diff --git a/src/routes/api/register/+server.ts b/src/routes/api/register/+server.ts new file mode 100644 index 0000000..f0b993b --- /dev/null +++ b/src/routes/api/register/+server.ts @@ -0,0 +1,34 @@ +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; +import { validateRegistration } from "$lib/registration"; + +const submissions: any[] = []; + +export const POST: RequestHandler = async ({ request }) => { + let body: any; + try { + body = await request.json(); + } catch { + return json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } + + const result = validateRegistration(body); + if (!result.ok) { + return json({ ok: false, errors: result.errors }, { status: 400 }); + } + + const submission = { + ...result.data, + id: crypto.randomUUID(), + createdAt: new Date().toISOString() + }; + + submissions.push(submission); + + return json({ ok: true, id: submission.id }); +}; + +export const GET: RequestHandler = async () => { + // return last 20 for debugging + return json({ ok: true, count: submissions.length, submissions: submissions.slice(-20).reverse() }); +}; diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte new file mode 100644 index 0000000..4a9c1b9 --- /dev/null +++ b/src/routes/register/+page.svelte @@ -0,0 +1,609 @@ + + + + Registration Form + + +
+
+

Registration Form

+
+ +
+
+
+
+
+ + + + + + + +
+
+ +
+ {images[0].image.alt} +
+
+ +
+
+ {images[1].image.alt} +
+ +
+ {#if schoolsError} +

+ Couldn’t load schools list: {schoolsError}
+ You can still select “Other”. +

+ {/if} + +
+ + + {#if otherSelected } + + {/if} + + + + +
+
+
+ +
+
+
+ Food allergies (select all that apply) + +
+ {#each commonAllergies as a} + + {/each} +
+ + +
+ + +
+ +
+ {images[2].image.alt} +
+
+
+ + +
+
+ + +
+ + {projectWordCount}/{MAX_WORDS} words + + {#if projectTooLong} + Please shorten this to {MAX_WORDS} words or fewer. + {/if} +
+
+
+ +
+ +
+
+
+ + \ No newline at end of file diff --git a/static/images/hack-food.png b/static/images/hack-food.png new file mode 100644 index 0000000..72c5e28 --- /dev/null +++ b/static/images/hack-food.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6097c2c520b1255136be0e3c502ffbdf576b68991d9b09782f36ea470303bc0d +size 8667129 diff --git a/static/images/hack-identity.png b/static/images/hack-identity.png new file mode 100644 index 0000000..72c5e28 --- /dev/null +++ b/static/images/hack-identity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6097c2c520b1255136be0e3c502ffbdf576b68991d9b09782f36ea470303bc0d +size 8667129 diff --git a/static/images/hack-school.png b/static/images/hack-school.png new file mode 100644 index 0000000..72c5e28 --- /dev/null +++ b/static/images/hack-school.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6097c2c520b1255136be0e3c502ffbdf576b68991d9b09782f36ea470303bc0d +size 8667129