Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c2e6371
feat: direct join-us button to a new page. Install react hook form.
TheSKBroook Apr 29, 2026
2696665
Update and merge main branch into this branch, fixed the conflict on
TheSKBroook May 5, 2026
04ceeb0
feat: add react-hook-form and basic form structure
TheSKBroook May 5, 2026
7df77df
feat: add all input fields and install zod
TheSKBroook May 5, 2026
eeb4eab
feat: create join us form schema and enum
TheSKBroook May 5, 2026
74ad26d
feat: add a temporary ui components for form inputs
TheSKBroook May 5, 2026
3f5fd81
feat: add an error message and a helper function to allow empty
TheSKBroook May 6, 2026
322c8d4
fix: fix the Enum class in Degree and Uni
TheSKBroook May 6, 2026
650a002
test: create a supabase client for testing the database
TheSKBroook May 6, 2026
cf99c90
feat: add responsive change to the university field
TheSKBroook May 6, 2026
254ab20
fix: add university_other when user selects other
TheSKBroook May 6, 2026
fa72440
feat: make the input fields be reactive depends on the University field
TheSKBroook May 6, 2026
37edfd5
fix: fix UI center and change background colour to white
TheSKBroook May 6, 2026
5472b82
fix: fix issue on number is being parse as string on type number
TheSKBroook May 6, 2026
949de6d
fix: comment out sending to backend code for now
TheSKBroook May 6, 2026
b8cced7
feat: add other university field when OTHER chosen in university field
TheSKBroook May 6, 2026
5b176f8
fix: fix schema alignment and supebase connection issue
TheSKBroook May 7, 2026
05454d2
refactor: rename join-us component to registration and move supabase …
TheSKBroook May 7, 2026
7c5f8ba
feat: create function in registration service for join us page
TheSKBroook May 7, 2026
fa79eab
fix: turn id field to only allow 0- 9.
TheSKBroook May 7, 2026
ec19549
refactor: delete unused file
TheSKBroook May 7, 2026
7460b55
feat: add jsdoc for join-us related files
TheSKBroook May 7, 2026
b8008f2
chore: add testing-library and vitest and jsdom
TheSKBroook May 8, 2026
2b2f5ce
feat: add test for registration form components
TheSKBroook May 8, 2026
0bfec29
fix: turn fields into null when changing University Type, and prompt …
TheSKBroook May 8, 2026
2e55473
fix: add some comments for readability
TheSKBroook May 8, 2026
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
312 changes: 301 additions & 11 deletions bun.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
"start": "next start"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@prisma/adapter-pg": "^7.6.0",
"@prisma/client": "^7.6.0",
"@radix-ui/react-slot": "^1.2.3",
"@reown/appkit": "^1.8.19",
"@reown/appkit-adapter-wagmi": "^1.8.19",
"@supabase/supabase-js": "^2.105.3",
"@tanstack/react-query": "^5.95.2",
"@types/pg": "^8.20.0",
"class-variance-authority": "^0.7.1",
Expand All @@ -28,13 +30,18 @@
"prisma": "^7.6.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.75.0",
"tailwind-merge": "^3.3.1",
"viem": "^2.47.6",
"wagmi": "^3.6.0"
"wagmi": "^3.6.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@tailwindcss/postcss": "^4.1.14",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
Expand All @@ -44,9 +51,12 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"i": "^0.3.7",
"jsdom": "^29.1.1",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0"
"typescript-eslint": "^8.45.0",
"vitest": "^4.1.5"
}
}
35 changes: 35 additions & 0 deletions src/app/pages/join-us/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { RegistrationForm } from "../../../components/registration-form";

export default function JoinUs() {
return (
<section
id="join-us"
className="py-24 md:py-32 bg-background border-t border-border/50"
>
<div className="container mx-auto px-4">
{/* Section header */}
<div className=" space-y-4 max-w-2xl mx-auto text-center">
<span className="text-sm font-bold uppercase tracking-[0.2em] text-primary">
/ Join Us
</span>
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-foreground">
Join our Web3 Club
</h2>
<p className="text-lg text-muted-foreground leading-relaxed">
A student-led club at the University of Auckland for anyone
interested in blockchain, AI, and the wider Web3 space. Sign up to
learn, connect, and stay updated on events.
<br />
<br />
Bitcoin runs on Proof of Work. We run on Proof of Learn ✅
</p>
</div>

{/* Sign up grid */}
<div className="max-w-2xl mx-auto w-full">
<RegistrationForm />
</div>
</div>
</section>
);
}
8 changes: 1 addition & 7 deletions src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,7 @@ export function Navbar() {
className="rounded-xl px-6 py-5 font-bold shadow-md hover:shadow-lg transition-transform hover:-translate-y-0.5"
asChild
>
<Link
href="https://forms.gle/vzRb7t46SPBUwi7v8"
target="_blank"
rel="noopener noreferrer"
>
Join Us
</Link>
<Link href="/pages/join-us">Join Us</Link>
</Button>
</div>

Expand Down
259 changes: 259 additions & 0 deletions src/components/registration-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
"use client";

import { useEffect } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import {
RegistrationSchema,
RegistrationData,
} from "../lib/schemas/registration";
import {
UniversityType,
DegreeType,
FacultyType,
} from "../lib/schemas/registration";
import { zodResolver } from "@hookform/resolvers/zod";
import { FormInput, FormSelect } from "./ui/form-field";
import { RegistrationService } from "../services/registrations/registrations-service";

/**
* Renders a registration form for new members to join the team.
* This form handles user input for personal details, university information,
* and a goal statement. It integrates with `react-hook-form` for validation
* and form state management, and interacts with `RegistrationService` to
* check email availability and submit registration data.
*
* @returns {JSX.Element} The registration form component.
*/
export function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors },
reset,
watch,
setValue,
clearErrors,
} = useForm<RegistrationData>({
resolver: zodResolver(RegistrationSchema),
});

const selectedUniversity = watch("university");

useEffect(() => {
const clearField = (
field:
| "university_other"
| "upi"
| "student_id"
| "degree_type"
| "faculty",
) => {
clearErrors(field);
setValue(field, null as never, {
shouldDirty: true,
shouldTouch: false,
shouldValidate: false,
});
};

if (!selectedUniversity) {
return;
}

if (selectedUniversity === UniversityType.None) {
clearField("university_other");
clearField("upi");
clearField("student_id");
clearField("degree_type");
clearField("faculty");
return;
}

if (selectedUniversity === UniversityType.UOA) {
clearField("university_other");
return;
}

if (selectedUniversity === UniversityType.AUT) {
clearField("university_other");
clearField("upi");
return;
}

if (selectedUniversity === UniversityType.Other) {
clearField("upi");
clearField("student_id");
}
}, [clearErrors, selectedUniversity, setValue]);

/**
* Handles the form submission.
* Validates the form data, checks if the email is already taken,
* submits the registration data, and resets the form.
*
* @param {RegistrationData} data - The validated form data.
*/
const onSubmit: SubmitHandler<RegistrationData> = async (data) => {
console.log(data);

const emailTaken = await RegistrationService.isEmailTaken(data.email);
if (emailTaken) {
alert("Email is already taken");
return;
}

await RegistrationService.submitRegistration(data);

reset();
alert("Form submitted successfully!");
};

// Storing the shared styles in a variable keeps the JSX clean!
const inputClass =
"w-full px-4 py-3 border border-gray-300 rounded-lg bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all";

return (
<div className="min-h-screen flex items-center justify-center p-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-lg bg-white p-8 rounded-xl shadow-xl/20 flex flex-col gap-4"
noValidate
>
<h2 className="text-2xl font-bold text-gray-800 mb-2 text-center">
Join Our Team
</h2>

<FormInput
className={inputClass}
placeholder="First Name"
error={errors.first_name}
{...register("first_name")}
/>
<FormInput
className={inputClass}
placeholder="Last Name"
error={errors.last_name}
{...register("last_name")}
/>
<FormInput
type="email"
className={inputClass}
placeholder="Email"
error={errors.email}
{...register("email")}
/>

<FormSelect
className={inputClass}
defaultValue=""
error={errors.university}
{...register("university")}
>
<option value="" disabled>
Select University
</option>
<option value={UniversityType.UOA}>UOA</option>
<option value={UniversityType.AUT}>AUT</option>
<option value={UniversityType.Other}>Other</option>
<option value={UniversityType.None}>None</option>
</FormSelect>

{selectedUniversity && selectedUniversity !== UniversityType.None && (
<div className="flex flex-col gap-4">
{selectedUniversity &&
selectedUniversity !== UniversityType.Other && (
<div className="flex flex-row gap-4">
{selectedUniversity &&
selectedUniversity !== UniversityType.AUT && (
<div className="flex-1">
<FormInput
className={inputClass}
placeholder="UPI"
error={errors.upi}
{...register("upi")}
/>
</div>
)}

<div className="flex-1">
<FormInput
type="text"
className={inputClass}
numericOnly={true}
placeholder="Student ID"
inputMode="numeric"
pattern="[0-9]*"
error={errors.student_id}
{...register("student_id")}
/>
</div>
</div>
)}

{selectedUniversity &&
selectedUniversity === UniversityType.Other && (
<div className="flex-1">
<FormInput
type="text"
className={inputClass}
placeholder="Other University"
error={errors.university_other}
{...register("university_other")}
/>
</div>
)}

<FormSelect
className={inputClass}
defaultValue=""
error={errors.degree_type}
{...register("degree_type")}
>
<option value="" disabled>
Select Degree Type
</option>
<option value={DegreeType.FirstYear}>First Year</option>
<option value={DegreeType.SecondYear}>Second Year</option>
<option value={DegreeType.ThirdYear}>Third Year</option>
<option value={DegreeType.FourthYear}>Fourth Year</option>
<option value={DegreeType.FourthYearAndBeyond}>
Fourth Year and Beyond
</option>
<option value={DegreeType.Masters}>Masters</option>
<option value={DegreeType.PhD}>PhD</option>
</FormSelect>

<FormSelect
className={inputClass}
defaultValue=""
error={errors.faculty}
{...register("faculty")}
>
<option value="" disabled>
Select Faculty
</option>
<option value={FacultyType.Science}>Science</option>
<option value={FacultyType.Arts}>Arts</option>
<option value={FacultyType.Engineering}>Engineering</option>
<option value={FacultyType.Commerce}>Commerce</option>
<option value={FacultyType.Other}>Other</option>
</FormSelect>
</div>
)}

<textarea
className={inputClass}
placeholder="Goal Statement"
{...register("goal_statement")}
/>

<button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition-colors mt-4 shadow-sm"
>
Submit
</button>
</form>
</div>
);
}
Loading
Loading