diff --git a/.gitignore b/.gitignore index 5ef6a52..b610f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# utils +/agents/ \ No newline at end of file diff --git a/agents/app-form.md b/agents/app-form.md new file mode 100644 index 0000000..cca2420 --- /dev/null +++ b/agents/app-form.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/applications/apply/hacker/application-form-skeleton.tsx b/app/applications/apply/hacker/application-form-skeleton.tsx new file mode 100644 index 0000000..fbb5554 --- /dev/null +++ b/app/applications/apply/hacker/application-form-skeleton.tsx @@ -0,0 +1,97 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +function SectionSkeleton({ fields = 3 }: { fields?: number }) { + return ( + + + + + + {Array.from({ length: fields }).map((_, i) => ( +
+ + +
+ ))} +
+
+ ); +} + +export default function ApplicationFormSkeleton() { + return ( +
+ {/* Header */} +
+ + +
+ +
+ {/* Personal Information */} + + + {/* Academic Information */} + + + {/* Essays */} + + + + + + {Array.from({ length: 3 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ + {/* Logistics */} + + + {/* Socials */} + + + {/* Communications */} + + + + + + {Array.from({ length: 2 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ + {/* MLH & Sponsor Agreements */} + + + + + + {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ + {/* Action buttons */} +
+ + +
+
+
+ ); +} diff --git a/app/applications/apply/hacker/application-form.tsx b/app/applications/apply/hacker/application-form.tsx new file mode 100644 index 0000000..a184f78 --- /dev/null +++ b/app/applications/apply/hacker/application-form.tsx @@ -0,0 +1,320 @@ +"use client"; + +import { use, useEffect, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; + +import { + HackerApplicationFormData, + hackerApplicationSchema, +} from "@/lib/types/applications"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import AcademicInformation from "./components/academic-information"; +import PersonalInformation from "./components/personal-information"; +import Essays from "./components/essays"; +import Logistics from "./components/logistics"; +import Socials from "./components/socials"; +import Communications from "./components/communications"; +import { submitHackerApplication } from "@/lib/actions/application-form.server.actions"; + +const STORAGE_KEY = "mhacks-application-draft"; + +export default function ApplyPage({ + profileIdPromise, +}: { + profileIdPromise: Promise; +}) { + const profileId = use(profileIdPromise); + 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(hackerApplicationSchema), + 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, + }, + }); + + // 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 HackerApplicationFormData, 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 HackerApplicationFormData] === undefined) { + delete toSave[key as keyof HackerApplicationFormData]; + } + }); + localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); + }); + return () => subscription.unsubscribe(); + }, [watch]); + + const onSubmit = async (data: HackerApplicationFormData) => { + setIsSubmitting(true); + try { + await submitHackerApplication(profileId, data); + // In a real app, we'd upload the resume here + 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 */} + + + {/* Academic Inforamtion */} + + + {/* Essays */} + + + {/* Logistics */} + + + {/* Socials */} + + + {/* Communications */} + + + {/* MLH & Sponsor Agreements */} + + + MLH & Sponsor Agreements + + +
+ ( + + )} + /> + +
+ {errors.mlhCodeOfConduct && ( +

+ {errors.mlhCodeOfConduct.message} +

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

+ {errors.mlhPrivacyPolicy.message} +

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

+ {errors.mlhEmails.message} +

+ )} + +
+ ( + + )} + /> + +
+
+
+ +
+ + +
+ +
+ ); +} diff --git a/app/applications/apply/hacker/components/academic-information.tsx b/app/applications/apply/hacker/components/academic-information.tsx new file mode 100644 index 0000000..1fde8bf --- /dev/null +++ b/app/applications/apply/hacker/components/academic-information.tsx @@ -0,0 +1,241 @@ +import { Controller, useWatch } from "react-hook-form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + countries, + degreeOptions, + majorOptions, + universities, +} from "../form-options"; +import { FormField } from "../utils"; + +const currentYear = new Date().getFullYear(); +const graduationYears = Array.from({ length: 10 }, (_, i) => currentYear + i); + +const AcademicInformation = ({ errors, register, control, setValue }: any) => { + const university = useWatch({ control, name: "university" }); + const country = useWatch({ control, name: "country" }); + const degree = useWatch({ control, name: "degree" }); + const major = useWatch({ control, name: "major" }); + return ( + <> + {" "} + {/* 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) +

+
+
+
+ + ); +}; + +export default AcademicInformation; diff --git a/app/applications/apply/hacker/components/communications.tsx b/app/applications/apply/hacker/components/communications.tsx new file mode 100644 index 0000000..1b5e4e7 --- /dev/null +++ b/app/applications/apply/hacker/components/communications.tsx @@ -0,0 +1,39 @@ +import { Controller } from "react-hook-form"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +const Communications = ({ control, errors }: any) => { + return ( + + + Communications + + +
+ ( + + )} + /> + +
+
+
+ ); +}; + +export default Communications; diff --git a/app/applications/apply/hacker/components/essays.tsx b/app/applications/apply/hacker/components/essays.tsx new file mode 100644 index 0000000..ddaffba --- /dev/null +++ b/app/applications/apply/hacker/components/essays.tsx @@ -0,0 +1,80 @@ +import { Textarea } from "@/components/ui/textarea"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { FormField } from "../utils"; + +const Essays = ({ register, errors }: any) => { + return ( + + + Essays + + Please write 100-1000 characters for each response + + + + +