From a3d0455e6e5a3d0a5102e40c3ef320c9d14a5ed4 Mon Sep 17 00:00:00 2001 From: hujalex Date: Fri, 27 Mar 2026 14:12:49 -0400 Subject: [PATCH 1/6] app form mvp --- agent.md | 112 + app/apply/page.tsx | 892 +++ app/page.tsx | 58 +- components/ui/button.tsx | 22 +- components/ui/card.tsx | 103 + components/ui/checkbox.tsx | 33 + components/ui/input.tsx | 19 + components/ui/label.tsx | 24 + components/ui/select.tsx | 192 + components/ui/textarea.tsx | 18 + lib/schemas/application.ts | 63 + package-lock.json | 11491 +++++++++++++++++++++++++++++++++++ package.json | 5 +- 13 files changed, 12971 insertions(+), 61 deletions(-) create mode 100644 agent.md create mode 100644 app/apply/page.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/schemas/application.ts create mode 100644 package-lock.json diff --git a/agent.md b/agent.md new file mode 100644 index 0000000..cca2420 --- /dev/null +++ b/agent.md @@ -0,0 +1,112 @@ +This will be the form that hackers use to apply. + +## Requirements + +- Use shadcn components for building the form UI +- Use React Hook Form for form state management and validation +- Allow users to save progress locally and return later to complete the application +- Validate submissions both in the browser and on the server for correctness +- ** For the resume upload, use a dummy function for now. We will set up S3 later. + +## Design + +- Any simple form design would suffice +- Example from shadcn docs https://ui.shadcn.com/docs/forms/react-hook-form#complex-forms + + ![Screenshot 2026-03-08 at 6.15.35 PM.png](attachment:b798e4b7-33c0-4637-a0c6-d4a815b480d0:Screenshot_2026-03-08_at_6.15.35_PM.png) + +- Additional features like partitioning the form into paged sections or fancy UI elements (e.g., map for location input) would be fun, but beyond MVP + +## Questions + +**Personal Information** + +- Age: Number input (min: 18) +- Gender: Single-select dropdown + - Male + - Female + - Other (please describe) +- Ethnicity: Single-select dropdown + - American Indian or Alaska Native + - Asian + - Black or African American + - Hispanic or Latino / Latina / Latinx + - Middle Eastern or North African + - Native Hawaiian or Pacific Islander + - White + - Multiracial (please describe) + +**Academic Information** + +- University: Single-select dropdown + - Find a list of universities + - Other (please describe) +- Country: Single-select dropdown + - Find a list of countries + - Other (describe) +- Degree: Single-select dropdown + - High School + - Associate's + - Bachelor's + - Master's + - PhD + - Other (please describe) +- Graduation Year: Number input (min: 2026) +- Number of Previous Hackathons: Number input +- Major(s): Single-select dropdown + - Computer Science + - Computer Engineering + - Electrical Engineering + - Data Science + - Statistics + - Mathematics + - Business + - Other or multiple majors (please describe) +- Resume: PDF File upload + +**Essays** (min/max character limits apply) + +- Why do you want to attend MHacks? +- Describe a technical challenge you've faced and how you solved it. +- Tell us about a project you're proud of. +- Anything else you'd like us to know? (optional) + +**Logistics** + +- Transportation Type: Single-select dropdown + - Driving + - Flying + - Bus + - Train + - Local +- Where Are You Coming From?: Text input +- Shirt Size: Single-select dropdown + - XS + - S + - M + - L + - XL + - XXL +- Do you have any allergies or dietary restrictions?: Checkbox + - When checked provide textbox with “Please describe” +- Will you require travel reimbursement to attend? Yes / No +- If yes: If travel reimbursement cannot be provided, would you still be interested in attending MHacks? + +**Socials** + +- GitHub (optional): URL input +- LinkedIn (optional): URL input +- Personal Site (optional): URL input + +**Communications** + +- Did you follow us on Instagram (@mhacks_)? (optional): Checkbox + +**MLH & Sponsor Agreements** + +- I have read and agree to the MLH Code of Conduct: Checkbox (must be checked) +- I authorize you to share my application/registration information with Major League Hacking for event administration, ranking, and MLH administration in-line with the MLH Privacy Policy. I further agree to the terms of both the MLH Contest Terms and Conditions and the MLH Privacy Policy: Checkbox (must be checked) +- I authorize MLH to send me occasional emails about relevant events, career opportunities, and community announcements: Checkbox (must be checked) +- I agree to receive emails from event sponsors about relevant opportunities and updates: Checkbox (optional) + +from amy: make sure all are present: https://guide.mlh.io/general-information/managing-registrations/registrations diff --git a/app/apply/page.tsx b/app/apply/page.tsx new file mode 100644 index 0000000..f3546b2 --- /dev/null +++ b/app/apply/page.tsx @@ -0,0 +1,892 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useRouter } from "next/navigation"; + +import { applicationSchema, type ApplicationFormData } from "@/lib/schemas/application"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +const STORAGE_KEY = "mhacks-application-draft"; + +const genderOptions = [ + { value: "male", label: "Male" }, + { value: "female", label: "Female" }, + { value: "other", label: "Other (please describe)" }, +]; + +const ethnicityOptions = [ + { value: "american-indian", label: "American Indian or Alaska Native" }, + { value: "asian", label: "Asian" }, + { value: "black", label: "Black or African American" }, + { value: "hispanic", label: "Hispanic or Latino / Latina / Latinx" }, + { value: "mena", label: "Middle Eastern or North African" }, + { value: "pacific-islander", label: "Native Hawaiian or Pacific Islander" }, + { value: "white", label: "White" }, + { value: "multiracial", label: "Multiracial (please describe)" }, +]; + +const universities = [ + { value: "umich", label: "University of Michigan" }, + { value: "mit", label: "Massachusetts Institute of Technology" }, + { value: "stanford", label: "Stanford University" }, + { value: "cmu", label: "Carnegie Mellon University" }, + { value: "gatech", label: "Georgia Institute of Technology" }, + { value: "uiuc", label: "University of Illinois at Urbana-Champaign" }, + { value: "ucberkeley", label: "University of California, Berkeley" }, + { value: "uw", label: "University of Washington" }, + { value: "utexas", label: "University of Texas at Austin" }, + { value: "cornell", label: "Cornell University" }, + { value: "other", label: "Other" }, +]; + +const countries = [ + { value: "us", label: "United States" }, + { value: "canada", label: "Canada" }, + { value: "uk", label: "United Kingdom" }, + { value: "mexico", label: "Mexico" }, + { value: "other", label: "Other" }, +]; + +const degreeOptions = [ + { value: "high-school", label: "High School" }, + { value: "associates", label: "Associate's" }, + { value: "bachelors", label: "Bachelor's" }, + { value: "masters", label: "Master's" }, + { value: "phd", label: "PhD" }, + { value: "other", label: "Other" }, +]; + +const majorOptions = [ + { value: "cs", label: "Computer Science" }, + { value: "ce", label: "Computer Engineering" }, + { value: "ee", label: "Electrical Engineering" }, + { value: "datasci", label: "Data Science" }, + { value: "stats", label: "Statistics" }, + { value: "math", label: "Mathematics" }, + { value: "business", label: "Business" }, + { value: "other", label: "Other or multiple majors" }, +]; + +const transportationOptions = [ + { value: "driving", label: "Driving" }, + { value: "flying", label: "Flying" }, + { value: "bus", label: "Bus" }, + { value: "train", label: "Train" }, + { value: "local", label: "Local" }, +]; + +const shirtSizeOptions = [ + { value: "xs", label: "XS" }, + { value: "s", label: "S" }, + { value: "m", label: "M" }, + { value: "l", label: "L" }, + { value: "xl", label: "XL" }, + { value: "xxl", label: "XXL" }, +]; + +const currentYear = new Date().getFullYear(); +const graduationYears = Array.from({ length: 10 }, (_, i) => currentYear + i); + +function FormField({ + label, + required, + children, +}: { + label: string; + required?: boolean; + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} + +export default function ApplyPage() { + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitSuccess, setSubmitSuccess] = useState(false); + + const { + register, + handleSubmit, + control, + watch, + setValue, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(applicationSchema), + mode: "onChange", + defaultValues: { + age: undefined, + gender: "", + genderOther: "", + ethnicity: "", + ethnicityOther: "", + university: "", + universityOther: "", + country: "", + countryOther: "", + degree: "", + degreeOther: "", + graduationYear: undefined, + previousHackathons: undefined, + major: "", + majorOther: "", + resume: undefined, + whyAttend: "", + technicalChallenge: "", + proudProject: "", + anythingElse: "", + transportationType: "", + comingFrom: "", + shirtSize: "", + hasAllergies: false, + allergiesDescription: "", + needsTravelReimbursement: false, + wouldAttendWithoutReimbursement: undefined, + github: "", + linkedin: "", + personalSite: "", + followsInstagram: false, + mlhCodeOfConduct: false, + mlhPrivacyPolicy: false, + mlhEmails: false, + sponsorEmails: false, + }, + }); + + const hasAllergies = watch("hasAllergies"); + const needsTravelReimbursement = watch("needsTravelReimbursement"); + const gender = watch("gender"); + const ethnicity = watch("ethnicity"); + const university = watch("university"); + const country = watch("country"); + const degree = watch("degree"); + const major = watch("major"); + + // Load saved progress + useEffect(() => { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const data = JSON.parse(saved); + Object.entries(data).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== "") { + setValue(key as keyof ApplicationFormData, value as any); + } + }); + } catch (e) { + console.error("Failed to load saved progress:", e); + } + } + }, [setValue]); + + // Save progress on change + useEffect(() => { + const subscription = watch((data) => { + const toSave = { ...data }; + // Don't save undefined values + Object.keys(toSave).forEach((key) => { + if (toSave[key as keyof ApplicationFormData] === undefined) { + delete toSave[key as keyof ApplicationFormData]; + } + }); + localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); + }); + return () => subscription.unsubscribe(); + }, [watch]); + + const onSubmit = async (data: ApplicationFormData) => { + setIsSubmitting(true); + try { + // Dummy resume upload function + const uploadResume = async (file: File): Promise => { + // TODO: Implement S3 upload + console.log("Uploading resume:", file.name); + return "dummy-resume-url"; + }; + + // In a real app, we'd upload the resume here + console.log("Submitting application:", data); + localStorage.removeItem(STORAGE_KEY); + setSubmitSuccess(true); + } catch (error) { + console.error("Submission error:", error); + } finally { + setIsSubmitting(false); + } + }; + + if (submitSuccess) { + return ( +
+ + + Application Submitted! + + Thank you for applying to MHacks. We'll be in touch soon. + + + + + + +
+ ); + } + + return ( +
+
+

MHacks Application

+

+ Fill out the form below to apply for MHacks +

+
+ +
+ {/* Personal Information */} + + + Personal Information + + +
+ + + {errors.age && ( +

{errors.age.message}

+ )} +
+ + + ( + + )} + /> + {errors.gender && ( +

{errors.gender.message}

+ )} +
+
+ + {gender === "other" && ( + + + + )} + + + ( + + )} + /> + {errors.ethnicity && ( +

{errors.ethnicity.message}

+ )} +
+ + {ethnicity === "multiracial" && ( + + + + )} +
+
+ + {/* Academic Information */} + + + Academic Information + + +
+ + ( + + )} + /> + {errors.university && ( +

{errors.university.message}

+ )} +
+ + {university === "other" && ( + + + + )} + + + ( + + )} + /> + {errors.country && ( +

{errors.country.message}

+ )} +
+ + {country === "other" && ( + + + + )} + + + ( + + )} + /> + {errors.degree && ( +

{errors.degree.message}

+ )} +
+ + {degree === "other" && ( + + + + )} + + + ( + + )} + /> + {errors.graduationYear && ( +

{errors.graduationYear.message}

+ )} +
+ + + + {errors.previousHackathons && ( +

{errors.previousHackathons.message}

+ )} +
+ + + ( + + )} + /> + {errors.major && ( +

{errors.major.message}

+ )} +
+ + {major === "other" && ( + + + + )} +
+ + + { + const file = e.target.files?.[0]; + if (file) { + setValue("resume", file.name); + } + }} + /> +

+ Upload your resume as a PDF (dummy upload for now) +

+
+
+
+ + {/* Essays */} + + + Essays + + Please write 100-1000 characters for each response + + + + +