From e4d6730848b5b287f80fa396814a3d2ad42f8503 Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Mon, 24 Aug 2026 21:53:52 +0530 Subject: [PATCH 1/3] interviewer --- src/components/InterviwerForm.tsx | 1121 +++++++++++++++++ .../supabase/actions/interviewers.actions.ts | 434 +++++++ src/pages/interviewer/index.tsx | 9 + src/types/index.d.ts | 13 + 4 files changed, 1577 insertions(+) create mode 100644 src/components/InterviwerForm.tsx create mode 100644 src/lib/supabase/actions/interviewers.actions.ts create mode 100644 src/pages/interviewer/index.tsx diff --git a/src/components/InterviwerForm.tsx b/src/components/InterviwerForm.tsx new file mode 100644 index 0000000..18a15b4 --- /dev/null +++ b/src/components/InterviwerForm.tsx @@ -0,0 +1,1121 @@ +"use client"; + +import { + useState, +} from "react"; + +import { + createInterviewer, + updateInterviewerPersonalInfo, +} from "@/lib/supabase/actions/interviewers.actions"; + +import type { + InterviewerType, +} from "@/types"; + +const availableDaysOptions = [ + "September 1", + "September 2", + "September 3", +]; + +export const INTERVIEWER_QUESTIONS = [ + { + id: "Q1", + text: "List the activities you've participated in over the last year.", + }, +]; + +const createEmptyResponses = + () => + INTERVIEWER_QUESTIONS.reduce( + ( + acc, + question + ) => { + acc[ + question.id + ] = ""; + + return acc; + }, + {} as Record< + string, + string + > + ); + +type FormState = { + name: string; + email: string; + sid: string; + + availableDays: string[]; + + responses: Record< + string, + string + >; +}; + +const initialForm: FormState = { + name: "", + email: "", + sid: "", + availableDays: [], + responses: + createEmptyResponses(), +}; + +export default function InterviewerForm() { + const [form, setForm] = + useState( + initialForm + ); + + const [ + application, + setApplication, + ] = + useState( + null + ); + + const [error, setError] = + useState(""); + + const [ + submitting, + setSubmitting, + ] = useState(false); + + const [ + savingPersonalInfo, + setSavingPersonalInfo, + ] = useState(false); + + /* + * --------------------------------------------------------- + * Field helper + * --------------------------------------------------------- + */ + + const updateField = < + K extends Exclude< + keyof FormState, + "responses" | "availableDays" + > + >( + field: K, + value: FormState[K] + ) => { + setForm( + (current) => ({ + ...current, + [field]: value, + }) + ); + }; + + /* + * --------------------------------------------------------- + * Available days + * --------------------------------------------------------- + */ + + const toggleAvailableDay = ( + day: string + ) => { + setForm( + (current) => { + const isSelected = + current.availableDays.includes( + day + ); + + return { + ...current, + + availableDays: + isSelected + ? current.availableDays.filter( + ( + d + ) => + d !== + day + ) + : [ + ...current.availableDays, + day, + ], + }; + } + ); + }; + + /* + * --------------------------------------------------------- + * Question response + * --------------------------------------------------------- + */ + + const updateResponse = ( + questionId: string, + value: string + ) => { + setForm( + (current) => ({ + ...current, + + responses: { + ...current.responses, + + [questionId]: + value, + }, + }) + ); + }; + + /* + * --------------------------------------------------------- + * Submit new application + * --------------------------------------------------------- + */ + + const handleSubmit = + async ( + event: React.FormEvent + ) => { + event.preventDefault(); + + setError(""); + + const name = + form.name.trim(); + + const email = + form.email.trim(); + + const sid = + form.sid.trim(); + + const availableDays = + form.availableDays; + + /* + * Required fields + */ + + if ( + !name || + !email || + !sid || + availableDays.length === + 0 + ) { + setError( + "Please fill in all the required fields and select at least one available day." + ); + + return; + } + + /* + * Email validation + */ + + if ( + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( + email + ) + ) { + setError( + "Please enter a valid email address." + ); + + return; + } + + /* + * SID validation + */ + + if ( + !/^\d{8}$/.test( + sid + ) + ) { + setError( + "SID must be exactly 8 digits." + ); + + return; + } + + /* + * Check questions + */ + + const missingResponses = + INTERVIEWER_QUESTIONS.some( + (question) => + !( + form.responses[ + question.id + ] || "" + ).trim() + ); + + if ( + missingResponses + ) { + setError( + "Please answer the question below." + ); + + return; + } + + /* + * Trim responses + */ + + const trimmedResponses = + Object.keys( + form.responses + ).reduce( + ( + acc, + key + ) => { + acc[key] = + ( + form.responses[ + key + ] || "" + ).trim(); + + return acc; + }, + {} as Record< + string, + string + > + ); + + setSubmitting(true); + + try { + const result = + await createInterviewer( + name, + sid, + email, + availableDays, + trimmedResponses + ); + + if ( + !result.success + ) { + if ( + result.reason === + "duplicate" + ) { + setError( + "You have already submitted an application." + ); + } else { + setError( + "We could not submit your application. Please try again." + ); + } + + return; + } + + /* + * Application successfully created. + */ + + setApplication( + result.applicant + ); + + setForm({ + name: + result + .applicant + .name || + "", + + email: + result + .applicant + .email || + "", + + sid: + result + .applicant + .sid || + "", + + availableDays: + result + .applicant + .availableDays || + [], + + responses: + result + .applicant + .responses || + createEmptyResponses(), + }); + } catch ( + submitError + ) { + console.error( + "Interviewer submission error:", + submitError + ); + + setError( + "We could not submit your application. Please try again." + ); + } finally { + setSubmitting( + false + ); + } + }; + + /* + * --------------------------------------------------------- + * Save personal information + * --------------------------------------------------------- + */ + + const handleSavePersonalInfo = + async () => { + if (!application) { + return; + } + + setError(""); + + const name = + form.name.trim(); + + const email = + form.email.trim(); + + const sid = + form.sid.trim(); + + const availableDays = + form.availableDays; + + /* + * Required fields + */ + + if ( + !name || + !email || + !sid || + availableDays.length === + 0 + ) { + setError( + "Please fill in all the personal information and select at least one available day." + ); + + return; + } + + /* + * Email validation + */ + + if ( + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( + email + ) + ) { + setError( + "Please enter a valid email address." + ); + + return; + } + + /* + * SID validation + */ + + if ( + !/^\d{8}$/.test( + sid + ) + ) { + setError( + "SID must be exactly 8 digits." + ); + + return; + } + + setSavingPersonalInfo( + true + ); + + try { + const result = + await updateInterviewerPersonalInfo( + application.id, + name, + email, + sid, + availableDays + ); + + 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; + } + + /* + * Update local application + */ + + setApplication( + result.applicant + ); + + /* + * Update local form + */ + + setForm( + ( + current + ) => ({ + ...current, + + name: + result + .applicant + .name, + + email: + result + .applicant + .email || + "", + + sid: + result + .applicant + .sid, + + availableDays: + result + .applicant + .availableDays || + [], + }) + ); + } catch ( + updateError + ) { + console.error( + "Interviewer update error:", + updateError + ); + + setError( + "Could not update your personal information. Please try again." + ); + } finally { + setSavingPersonalInfo( + false + ); + } + }; + + /* + * --------------------------------------------------------- + * 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 + Email */} + +
+
+ + + + 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( + "email", + 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" + /> +
+
+ + {/* SID */} + +
+
+ + + + 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" + /> +
+
+ + {/* Available days */} + +
+

+ Days available +

+ +
+ {availableDaysOptions.map( + ( + day + ) => ( + + ) + )} +
+
+ + {canEdit && ( + + )} +
+
+ + {/* Submitted answers */} + +
+

+ Submitted Application +

+ +

+ Your submitted answers cannot be changed. +

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

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

+ +
+

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

+
+
+ ) + )} +
+
+ + {error && ( +
+ {error} +
+ )} +
+ ); + } + + /* + * --------------------------------------------------------- + * New application + * --------------------------------------------------------- + */ + + return ( +
+ {/* Personal Information */} + +
+

+ Personal Information +

+ +

+ Tell us a little about yourself. +

+ +
+ {/* Name + Email */} + +
+
+ + + + 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( + "email", + event + .target + .value + ) + } + placeholder="you@example.com" + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none" + /> +
+
+ + {/* SID */} + +
+
+ + + + updateField( + "sid", + 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" + /> +
+
+ + {/* Available days */} + +
+

+ Days available +

+ +
+ {availableDaysOptions.map( + ( + day + ) => ( + + ) + )} +
+
+
+
+ + {/* Questions */} + +
+

+ Application Question +

+ +
+ {INTERVIEWER_QUESTIONS.map( + ( + question + ) => ( + + updateResponse( + question.id, + value + ) + } + /> + ) + )} +
+
+ + {error && ( +
+ {error} +
+ )} + + +
+ ); +} + +/* + * --------------------------------------------------------- + * Question component + * --------------------------------------------------------- + */ + +interface QuestionProps { + number: string; + question: string; + value: string; + onChange: ( + value: string + ) => void; +} + +function Question({ + number, + question, + value, + onChange, +}: QuestionProps) { + return ( +
+ + +