From c531aeb663f51dc51f5e8704e1ee6a0854577833 Mon Sep 17 00:00:00 2001 From: DhairyaSood Date: Sat, 15 Aug 2026 14:04:56 +0530 Subject: [PATCH] feat: improve registration and application editing --- src/components/ApplicationForm.tsx | 912 ++++++++++++++++-- .../supabase/actions/applicants.actions.ts | 566 ++++++++--- src/types/index.d.ts | 2 + .../sync-application-to-sheets/index.ts | 388 ++++++-- 4 files changed, 1521 insertions(+), 347 deletions(-) diff --git a/src/components/ApplicationForm.tsx b/src/components/ApplicationForm.tsx index 19cfd69..046c94b 100644 --- a/src/components/ApplicationForm.tsx +++ b/src/components/ApplicationForm.tsx @@ -1,5 +1,15 @@ -import { useState } from "react"; -import { createApplicant } from "@/lib/supabase/actions/applicants.actions"; +import { + useEffect, + useState, +} from "react"; + +import { + createApplicant, + fetchMyApplication, + updateApplicantPersonalInfo, +} from "@/lib/supabase/actions/applicants.actions"; + +import type { ApplicantType } from "@/types"; const branches = [ "Aerospace Engineering", @@ -37,21 +47,54 @@ export const APPLICATION_QUESTIONS = [ }, ]; +const createEmptyResponses = () => + APPLICATION_QUESTIONS.reduce( + (acc, question) => { + acc[question.id] = ""; + return acc; + }, + {} as Record + ); + const initialForm = { name: "", + phone: "", sid: "", branch: "", - responses: APPLICATION_QUESTIONS.reduce((acc, q) => { - acc[q.id] = ""; - return acc; - }, {} as Record), + responses: + createEmptyResponses(), }; export default function ApplicationForm() { - const [form, setForm] = useState(initialForm); - const [error, setError] = useState(""); - const [success, setSuccess] = useState(false); - const [submitting, setSubmitting] = useState(false); + const [form, setForm] = + useState(initialForm); + + const [application, setApplication] = + useState( + null + ); + + const [ + checkingApplication, + setCheckingApplication, + ] = useState(true); + + const [error, setError] = + useState(""); + + const [submitting, setSubmitting] = + useState(false); + + const [ + savingPersonalInfo, + setSavingPersonalInfo, + ] = useState(false); + + /* + * --------------------------------------------------------- + * Field helpers + * --------------------------------------------------------- + */ const updateField = ( field: keyof typeof initialForm, @@ -63,7 +106,10 @@ export default function ApplicationForm() { })); }; - const updateResponse = (questionId: string, value: string) => { + const updateResponse = ( + questionId: string, + value: string + ) => { setForm((current) => ({ ...current, responses: { @@ -73,58 +119,173 @@ export default function ApplicationForm() { })); }; + /* + * --------------------------------------------------------- + * Load existing application + * --------------------------------------------------------- + */ + + useEffect(() => { + let active = true; + + const loadApplication = + async () => { + const existing = + await fetchMyApplication(); + + if (!active) { + return; + } + + if (existing) { + setApplication( + existing + ); + + setForm({ + name: + existing.name || + "", + phone: + existing.phone || + "", + sid: + existing.sid || + "", + branch: + existing.branch || + "", + responses: + existing.responses || + createEmptyResponses(), + }); + } + + setCheckingApplication( + false + ); + }; + + void loadApplication(); + + return () => { + active = false; + }; + }, []); + + /* + * --------------------------------------------------------- + * Create new application + * --------------------------------------------------------- + */ + const handleSubmit = async ( event: React.FormEvent ) => { event.preventDefault(); setError(""); - setSuccess(false); - - const name = form.name.trim(); - const sid = form.sid.trim(); - const branch = form.branch; - - // Ensure all required fields and questions are filled - if (!name || !sid || !branch) { - setError("Please fill in all the required personal fields."); + + const name = + form.name.trim(); + + const phone = + form.phone.trim(); + + const sid = + form.sid.trim(); + + const branch = + form.branch; + + if ( + !name || + !phone || + !sid || + !branch + ) { + setError( + "Please fill in all the required personal fields." + ); + return; } - const missingResponses = APPLICATION_QUESTIONS.some( - (q) => !form.responses[q.id].trim() - ); + if ( + !/^[6-9]\d{9}$/.test( + phone + ) + ) { + setError( + "Please enter a valid 10-digit phone number." + ); - if (missingResponses) { - setError("Please answer all the application questions."); return; } - if (!/^\d{8}$/.test(sid)) { - setError("SID must be exactly 8 digits."); + if ( + !/^\d{8}$/.test(sid) + ) { + setError( + "SID must be exactly 8 digits." + ); + + return; + } + + const missingResponses = + APPLICATION_QUESTIONS.some( + (question) => + !form.responses[ + question.id + ].trim() + ); + + if (missingResponses) { + setError( + "Please answer all the application questions." + ); + return; } + const trimmedResponses = + Object.keys( + form.responses + ).reduce( + (acc, key) => { + acc[key] = + form.responses[ + key + ].trim(); + + return acc; + }, + {} as Record< + string, + string + > + ); + setSubmitting(true); try { - // Trim responses - const trimmedResponses = Object.keys(form.responses).reduce((acc, key) => { - acc[key] = form.responses[key].trim(); - return acc; - }, {} as Record); - - const result = await createApplicant( - name, - sid, - branch, - trimmedResponses - ); + const result = + await createApplicant( + name, + sid, + phone, + branch, + trimmedResponses + ); if (!result.success) { - if (result.reason === "duplicate") { + if ( + result.reason === + "duplicate" + ) { setError( - "An application with this SID has already been submitted." + "You have already submitted an application." ); } else { setError( @@ -135,10 +296,33 @@ export default function ApplicationForm() { return; } - setSuccess(true); - setForm(initialForm); - } catch (error) { - console.error("Unexpected application submission error:", error); + setApplication( + result.applicant + ); + + setForm({ + name: + result.applicant.name || + "", + phone: + result.applicant.phone || + "", + sid: + result.applicant.sid || + "", + branch: + result.applicant.branch || + "", + responses: + result.applicant + .responses || + createEmptyResponses(), + }); + } catch (submissionError) { + console.error( + "Unexpected application submission error:", + submissionError + ); setError( "We could not submit your application. Please try again." @@ -148,30 +332,443 @@ export default function ApplicationForm() { } }; - if (success) { + /* + * --------------------------------------------------------- + * Save personal information + * --------------------------------------------------------- + */ + + const handleSavePersonalInfo = + async () => { + if (!application) { + return; + } + + setError(""); + + const name = + form.name.trim(); + + const phone = + form.phone.trim(); + + const sid = + form.sid.trim(); + + const branch = + form.branch; + + if ( + !name || + !phone || + !sid || + !branch + ) { + setError( + "Please fill in all the personal information." + ); + + return; + } + + if ( + !/^[6-9]\d{9}$/.test( + phone + ) + ) { + setError( + "Please enter a valid 10-digit phone number." + ); + + return; + } + + if ( + !/^\d{8}$/.test(sid) + ) { + setError( + "SID must be exactly 8 digits." + ); + + return; + } + + setSavingPersonalInfo( + true + ); + + try { + const result = + await updateApplicantPersonalInfo( + application.id, + name, + phone, + sid, + branch + ); + + if (!result.success) { + setError( + result.reason === + "not_found" + ? "Your application could not be found or can no longer be edited." + : "Could not update your personal information. Please try again." + ); + + return; + } + + setApplication( + result.applicant + ); + + setForm( + (current) => ({ + ...current, + name: + result.applicant + .name, + phone: + result.applicant + .phone || + "", + sid: + result.applicant + .sid, + branch: + result.applicant + .branch || + "", + }) + ); + } catch (updateError) { + console.error( + "Error updating personal information:", + updateError + ); + + setError( + "Could not update your personal information. Please try again." + ); + } finally { + setSavingPersonalInfo( + false + ); + } + }; + + /* + * --------------------------------------------------------- + * Loading state + * --------------------------------------------------------- + */ + + if (checkingApplication) { return (
-
- ✓ +

+ Checking your application... +

+
+ ); + } + + /* + * --------------------------------------------------------- + * Existing application + * --------------------------------------------------------- + */ + + if (application) { + const canEdit = + application.status === + "pending"; + + return ( +
+ {/* Personal Information */} +
+
+
+

+ Personal Information +

+ +

+ {canEdit + ? "You can update your personal information while your application is pending." + : "Your personal information is locked because your application has been reviewed."} +

+
+ + + {application.status + .charAt( + 0 + ) + .toUpperCase() + + application.status.slice( + 1 + )} + +
+ +
+ {/* Name + Phone */} +
+
+ + + + updateField( + "name", + event + .target + .value + ) + } + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60" + /> +
+ +
+ + + + updateField( + "phone", + event.target.value.replace( + /\D/g, + "" + ) + ) + } + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60" + /> +
+
+ + {/* SID + Branch */} +
+
+ + + + updateField( + "sid", + event.target.value.replace( + /\D/g, + "" + ) + ) + } + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60" + /> +
+ +
+ + + +
+
+ + {canEdit && ( + + )} +
-

- Application Submitted -

+ {/* Read-only answers */} +
+

+ Submitted Application +

-

- Your application has been successfully submitted - to the Robotics Society. -

+

+ Your submitted answers + cannot be changed. +

-

- Please wait for further communication regarding - the selection process. -

+
+ {APPLICATION_QUESTIONS.map( + ( + question + ) => ( +
+

+ + { + question.id + }. + + + { + question.text + } +

+ +
+

+ {application + .responses?.[ + question.id + ] || + "No answer provided."} +

+
+
+ ) + )} +
+
+ + {error && ( +
+ {error} +
+ )}
); } + /* + * --------------------------------------------------------- + * New application form + * --------------------------------------------------------- + */ + return (
- {/* Name */} -
- - - - updateField("name", e.target.value) - } - placeholder="Enter your full name" - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring" - /> + {/* Name + Phone */} +
+
+ + + + updateField( + "name", + event + .target + .value + ) + } + placeholder="Enter your full name" + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none" + /> +
+ +
+ + + + updateField( + "phone", + event.target.value.replace( + /\D/g, + "" + ) + ) + } + placeholder="10-digit phone number" + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none" + /> +
{/* SID + Branch */} @@ -223,16 +865,25 @@ export default function ApplicationForm() { id="sid" type="text" inputMode="numeric" - maxLength={8} - value={form.sid} - onChange={(e) => + maxLength={ + 8 + } + value={ + form.sid + } + onChange={( + event + ) => updateField( "sid", - e.target.value.replace(/\D/g, "") + event.target.value.replace( + /\D/g, + "" + ) ) } placeholder="8-digit SID" - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring" + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none" />
@@ -246,24 +897,43 @@ export default function ApplicationForm() {
@@ -277,15 +947,37 @@ export default function ApplicationForm() {
- {APPLICATION_QUESTIONS.map((q) => ( - updateResponse(q.id, value)} - /> - ))} + {APPLICATION_QUESTIONS.map( + ( + question + ) => ( + + updateResponse( + question.id, + value + ) + } + /> + ) + )}
@@ -312,7 +1004,9 @@ interface QuestionProps { number: string; question: string; value: string; - onChange: (value: string) => void; + onChange: ( + value: string + ) => void; } function Question({ @@ -337,10 +1031,14 @@ function Question({