app/registration/page.tsx and is a single-page client component. It uses React Hook Form for state management, Zod for validation, and writes directly to Supabase on submit.
Libraries used
| Library | Version constraint | Purpose |
|---|---|---|
react-hook-form | — | Form state and submission |
@hookform/resolvers/zod | — | Connects Zod schema to RHF |
zod | — | Runtime validation schema |
react-phone-number-input | — | International phone field with dial-code picker |
react-day-picker | — | Date range picker (wrapped in LazyDatePicker) |
date-fns | — | Date arithmetic used by the picker |
framer-motion | — | Entry animations on card and title |
Zod validation schema
The full schema is defined at the top ofapp/registration/page.tsx:
Email validation
Email is validated through a shared schema inlib/email-validation.ts. It applies three checks in sequence:
global-tld-list package, which mirrors the IANA root zone database. This catches common typos such as .clm instead of .com.
Form fields reference
Personal details
Registrant’s first name. Must contain letters only (
/^[A-Za-z]+$/). Stored as first_name.Optional middle name. Must contain letters only if provided. Stored as
middle_name (null when omitted).Registrant’s last name. Must contain letters only. Stored as
last_name.Age as a string input, parsed to an integer (1–99) before storage. Stored as
age (integer).Ghaam name. Must contain letters only. Stored as
ghaam.Location
Country of origin. Accepted values:
australia, canada, england, india, kenya, usa. Stored as lowercase. Changing this field resets or auto-fills mandal.Mandal affiliation. For India, Australia, Canada, and Kenya this is auto-set and the field is disabled. For England and USA a dropdown is shown. Stored as kebab-case (e.g.
new-jersey). Changing country clears this field.Contact
Email address validated against the IANA TLD list via
lib/email-validation.ts. Stored as email.International phone number entered via
react-phone-number-input. Validated with isValidPhoneNumber(). The national number is extracted with parsePhoneNumber() before storage. Stored as mobile_number.E.164 country calling code (e.g.
+1). Set automatically from the parsed phone number. Stored as phone_country_code.Travel dates
Arrival date. Must be non-null. Minimum selectable date:
2026-07-23 (from lib/registration-date-range.ts). Stored as an ISO 8601 string in arrival_date.Departure date. Must be on or after
dateRange.start. Maximum selectable date: 2026-08-08. Stored as departure_date.React Hook Form setup
onBlur — fields are validated when the user leaves them, not on every keystroke.
All inputs are wired via the <Controller> render-prop pattern so that custom components (phone input, date picker, selects) integrate cleanly with RHF’s controlled field API.
Phone input integration
LazyPhoneInput wraps react-phone-number-input and is loaded lazily to keep the initial bundle small. When the user selects a country the phone field’s default dial code updates automatically:
parsePhoneNumber() splits the value into country code and national number. The country code is written to phoneCountryCode and only the national number is stored in Supabase.
Date picker integration
LazyDatePicker wraps react-day-picker with a range-selection mode. The selectable window is defined in lib/registration-date-range.ts:
CalendarDate objects (from @internationalized/date). Their .toString() method produces an ISO 8601 string that is stored directly in Supabase.
Submission flow
Submit handler in full
Error handling
| Scenario | Behaviour |
|---|---|
| Zod validation fails | Inline error messages appear beneath each field after blur |
| Supabase returns an error object | Red toast with error.message |
| Network or uncaught exception | Red toast: “Please check your connection and try again.” |
| Success (new record) | Green toast: “Successfully registered, [firstName]!” |
| Success (updated record) | Green toast: “Updated existing registration, [firstName]!” |
Loader2 icon and the text “Please wait” while isSubmitting is true. It is disabled during this period to prevent double submission.
Styling
The page imports@/styles/registration-theme.css which defines CSS custom properties under :root for all registration UI elements:
| Class | Purpose |
|---|---|
.reg-page-bg | Orange-to-white-to-red gradient page background |
.reg-card | Frosted-glass card with orange border |
.reg-input | 3.5 rem tall input with backdrop blur |
.reg-button | Orange-to-red gradient submit button |
.reg-error-text | Red inline validation error |
.reg-label | Semi-bold label text |
FAQ
Why is the page rendered client-side?
Why is the page rendered client-side?
The component is marked
"use client" because it uses useState, useEffect, and browser-only libraries (react-day-picker, react-phone-number-input). The mounted guard prevents a hydration mismatch by returning null until after the first render on the client.Why does the form write to Supabase directly instead of an API route?
Why does the form write to Supabase directly instead of an API route?
The registration table allows anonymous inserts via Supabase Row Level Security policies, so the client SDK can write directly without an API route. This keeps the submission path simple and reduces latency by one network hop.
How does the mandal field handle countries with only one mandal?
How does the mandal field handle countries with only one mandal?
When the user selects India, Australia, Canada, or Kenya,
setValue('mandal', ...) is called immediately with the fixed mandal name and the field is rendered as a disabled <Input> showing that value. For England and USA a <Select> is rendered with the appropriate list.What happens if the date picker returns null?
What happens if the date picker returns null?
The Zod schema for
dateRange.start and dateRange.end uses .refine((val) => val !== null, ...). If either value is null when the form is submitted, the submit handler is blocked and the error appears in the date picker component.How is the phone number stored?
How is the phone number stored?
The full E.164 string (e.g.
+12015551234) is validated with isValidPhoneNumber() from react-phone-number-input. Before writing to Supabase, parsePhoneNumber() splits the value and only the national number (e.g. 2015551234) is stored in mobile_number. The country code (e.g. +1) is stored separately in phone_country_code.