From 051d68f5650a2cdd8f57d753def2c85a7aa4256d Mon Sep 17 00:00:00 2001 From: Unchanted Date: Tue, 2 Sep 2025 16:27:21 +0530 Subject: [PATCH] feat(student): added parents field in signup --- apps/student/app/(main)/profile/page.tsx | 68 +++++++ apps/student/app/signup/action.ts | 5 + apps/student/app/signup/page.tsx | 180 ++++++++++++------ apps/student/app/signup/schema.ts | 99 +++++++--- .../app/signup/steps/PersonalDetailsStep.tsx | 75 +++++++- packages/db/schema.ts | 6 +- 6 files changed, 345 insertions(+), 88 deletions(-) diff --git a/apps/student/app/(main)/profile/page.tsx b/apps/student/app/(main)/profile/page.tsx index e7a4ac6..c328694 100644 --- a/apps/student/app/(main)/profile/page.tsx +++ b/apps/student/app/(main)/profile/page.tsx @@ -529,6 +529,74 @@ export default function ProfilePage() { )} +
+ + {editingSection === 'personal' ? ( + handleInputChange('mothersEmail', e.target.value)} + placeholder="mother@example.com" + /> + ) : ( + + )} +
+
+ + {editingSection === 'personal' ? ( + handleInputChange('mothersPhone', e.target.value)} + placeholder="10-digit number" + /> + ) : ( + + )} +
+
+ + {editingSection === 'personal' ? ( + handleInputChange('fathersName', e.target.value)} + /> + ) : ( + + )} +
+
+ + {editingSection === 'personal' ? ( + handleInputChange('fathersEmail', e.target.value)} + placeholder="father@example.com" + /> + ) : ( + + )} +
+
+ + {editingSection === 'personal' ? ( + handleInputChange('fathersPhone', e.target.value)} + placeholder="10-digit number" + /> + ) : ( + + )} +
{editingSection === 'personal' ? ( diff --git a/apps/student/app/signup/action.ts b/apps/student/app/signup/action.ts index fa53835..2c2db1e 100644 --- a/apps/student/app/signup/action.ts +++ b/apps/student/app/signup/action.ts @@ -37,6 +37,11 @@ export async function signupAction(data: StudentSignup) { middleName: student.middleName, lastName: student.lastName, mothersName: student.mothersName, + mothersEmail: student.mothersEmail && student.mothersEmail.length ? student.mothersEmail : null, + mothersPhone: student.mothersPhone && student.mothersPhone.length ? student.mothersPhone : null, + fathersName: student.fathersName, + fathersEmail: student.fathersEmail && student.fathersEmail.length ? student.fathersEmail : null, + fathersPhone: student.fathersPhone && student.fathersPhone.length ? student.fathersPhone : null, gender: student.gender, dob: student.dob, personalGmail: student.personalGmail, diff --git a/apps/student/app/signup/page.tsx b/apps/student/app/signup/page.tsx index fd7f6d6..af47eab 100644 --- a/apps/student/app/signup/page.tsx +++ b/apps/student/app/signup/page.tsx @@ -1,5 +1,5 @@ // Due to the length and complexity of the complete updated form, the full implementation is provided modularly. -// This file only includes the top-level form layout and updated schema logic. Other components (InternshipModal, ResumeModal, etc.) +// This file only includes the top-level form layout and updated schema logic. Other components (InternshipModal, ResumeModal, etc.) // should be created as separate files or extracted for cleanliness. 'use client'; @@ -34,6 +34,11 @@ const steps = [ 'firstName', 'lastName', 'mothersName', + 'mothersEmail', + 'mothersPhone', + 'fathersName', + 'fathersEmail', + 'fathersPhone', 'rollNumber', 'phoneNumber', 'address', @@ -67,6 +72,11 @@ export default function StudentRegistrationForm() { middleName: '', lastName: '', mothersName: '', + mothersEmail: '', + mothersPhone: '', + fathersName: '', + fathersEmail: '', + fathersPhone: '', rollNumber: '', phoneNumber: '', address: '', @@ -175,6 +185,11 @@ export default function StudentRegistrationForm() { middleName: 'Middle Name', lastName: 'Last Name', mothersName: "Mother's Name", + mothersEmail: "Mother's Email", + mothersPhone: "Mother's Phone Number", + fathersName: "Father's Name", + fathersEmail: "Father's Email", + fathersPhone: "Father's Phone Number", rollNumber: 'Roll Number', phoneNumber: 'Phone Number', address: 'Address', @@ -219,8 +234,13 @@ export default function StudentRegistrationForm() { {/* Welcoming heading - now always at the top */}
-

Welcome to Placement Portal

-

Register below to get started with your placement journey. Fill in your details step by step and let your career take off!

+

+ Welcome to Placement Portal +

+

+ Register below to get started with your placement journey. Fill in your details step by + step and let your career take off! +

@@ -233,7 +253,9 @@ export default function StudentRegistrationForm() { Step {currentStep} of {steps.length} - {steps[currentStep - 1]?.title} + + {steps[currentStep - 1]?.title} +
@@ -242,24 +264,25 @@ export default function StudentRegistrationForm() {
{ - // Get all error fields - const errorFields = extractErrorFields(form.formState.errors); - // Map to user-friendly names - const prettyFields = errorFields.map((key) => { - // Try to map top-level, or fallback to key - const topKey = String(key.split('.')[0]); - return fieldLabels[topKey] || key; - }); - alert( - 'Please fill all required fields before submitting.\n' + - (prettyFields.length > 0 - ? 'Missing/invalid: ' + prettyFields.join(', ') - : '') - ); - }) - : (e) => e.preventDefault() + onSubmit={ + currentStep === steps.length + ? form.handleSubmit(onSubmit, () => { + // Get all error fields + const errorFields = extractErrorFields(form.formState.errors); + // Map to user-friendly names + const prettyFields = errorFields.map((key) => { + // Try to map top-level, or fallback to key + const topKey = String(key.split('.')[0]); + return fieldLabels[topKey] || key; + }); + alert( + 'Please fill all required fields before submitting.\n' + + (prettyFields.length > 0 + ? 'Missing/invalid: ' + prettyFields.join(', ') + : ''), + ); + }) + : (e) => e.preventDefault() } className="space-y-8" onKeyDown={(e) => { @@ -284,11 +307,19 @@ export default function StudentRegistrationForm() { Previous {currentStep === steps.length ? ( - ) : ( - )} @@ -300,46 +331,75 @@ export default function StudentRegistrationForm() { {/* Animated gradient and blob styles */} + /* ====== BLOB MOTION (unchanged) ================================ */ + .animate-blob1 { + animation: blobMove1 18s ease-in-out infinite; + } + .animate-blob2 { + animation: blobMove2 22s ease-in-out infinite; + } + .animate-blob3 { + animation: blobMove3 20s ease-in-out infinite; + } + .animate-blob4 { + animation: blobMove4 26s ease-in-out infinite; + } + .animate-blob5 { + animation: blobMove5 24s ease-in-out infinite; + } + /* existing keyframes … */ + `} ); } diff --git a/apps/student/app/signup/schema.ts b/apps/student/app/signup/schema.ts index 39267bb..4bc8f64 100644 --- a/apps/student/app/signup/schema.ts +++ b/apps/student/app/signup/schema.ts @@ -26,32 +26,79 @@ export const resumeSchema = z.object({ link: z.string().url('Must be a valid URL'), }); -export const studentSignupSchema = z.object({ - rollNumber: z.string().min(1, 'Roll number is required').max(12), - firstName: z.string().min(1, 'First name is required').max(255), - middleName: z.string().max(255).optional(), - lastName: z.string().min(1, 'Last name is required').max(255), - mothersName: z.string().min(1, "Mother's name is required").max(255), - gender: z.string().min(1, 'Gender is required').max(10), - dob: z.coerce.date().refine((date) => date <= new Date(), { - message: 'Date of birth cannot be in the future', - }), - personalGmail: z.string().email('Must be a valid email'), - phoneNumber: z.string().min(10, 'Phone number must be at least 10 digits').max(10), - address: z.string().min(1, 'Address is required'), - degree: z.string().min(1, 'Degree is required'), - branch: z.string().min(1, 'Branch is required'), - year: z.string().min(1, 'Year is required'), - skills: z.array(z.string()), - linkedin: z.string(), - github: z.string(), - ssc: z.coerce.number().min(0).max(100), - hsc: z.coerce.number().min(0).max(100), - isDiploma: z.boolean(), - sgpi: z.array(sgpiSchema).length(8, 'Must provide grades for all 8 semesters'), - internships: z.array(internshipSchema), - resume: z.array(resumeSchema), -}); +export const studentSignupSchema = z + .object({ + rollNumber: z.string().min(1, 'Roll number is required').max(12), + firstName: z.string().min(1, 'First name is required').max(255), + middleName: z.string().max(255).optional(), + lastName: z.string().min(1, 'Last name is required').max(255), + // Parent details (all optional individually; validated via refinements below) + mothersName: z.string().max(255).optional(), + mothersEmail: z.string().email('Must be a valid email').optional().or(z.literal('')), + mothersPhone: z + .string() + .min(10, 'Phone must be 10 digits') + .max(10) + .optional() + .or(z.literal('')), + fathersName: z.string().max(255).optional(), + fathersEmail: z.string().email('Must be a valid email').optional().or(z.literal('')), + fathersPhone: z + .string() + .min(10, 'Phone must be 10 digits') + .max(10) + .optional() + .or(z.literal('')), + gender: z.string().min(1, 'Gender is required').max(10), + dob: z.coerce.date().refine((date) => date <= new Date(), { + message: 'Date of birth cannot be in the future', + }), + personalGmail: z.string().email('Must be a valid email'), + phoneNumber: z.string().min(10, 'Phone number must be at least 10 digits').max(10), + address: z.string().min(1, 'Address is required'), + degree: z.string().min(1, 'Degree is required'), + branch: z.string().min(1, 'Branch is required'), + year: z.string().min(1, 'Year is required'), + skills: z.array(z.string()), + linkedin: z.string(), + github: z.string(), + ssc: z.coerce.number().min(0).max(100), + hsc: z.coerce.number().min(0).max(100), + isDiploma: z.boolean(), + sgpi: z.array(sgpiSchema).length(8, 'Must provide grades for all 8 semesters'), + internships: z.array(internshipSchema), + resume: z.array(resumeSchema), + }) + // Require at least one parent provided + .refine( + (data) => { + const motherProvided = Boolean((data.mothersName && data.mothersName.trim()) || (data.mothersEmail && data.mothersEmail.trim()) || (data.mothersPhone && data.mothersPhone.trim())); + const fatherProvided = Boolean((data.fathersName && data.fathersName.trim()) || (data.fathersEmail && data.fathersEmail.trim()) || (data.fathersPhone && data.fathersPhone.trim())); + return motherProvided || fatherProvided; + }, + { + message: 'Provide details for at least one parent', + path: ['mothersName'], + }, + ) + // If mother's contact provided, require mother's name + .refine( + (data) => { + const motherContact = Boolean((data.mothersEmail && data.mothersEmail.trim()) || (data.mothersPhone && data.mothersPhone.trim())); + if (!motherContact) return true; + return Boolean(data.mothersName && data.mothersName.trim()); + }, + { message: "Mother's name is required when mother's contact is provided", path: ['mothersName'] }, + ) + // If father's contact provided, require father's name + .refine( + (data) => { + const fatherContact = Boolean((data.fathersEmail && data.fathersEmail.trim()) || (data.fathersPhone && data.fathersPhone.trim())); + if (!fatherContact) return true; + return Boolean(data.fathersName && data.fathersName.trim()); + }, + { message: "Father's name is required when father's contact is provided", path: ['fathersName'] }, + ); export type StudentSignup = z.infer; export type Internship = z.infer; diff --git a/apps/student/app/signup/steps/PersonalDetailsStep.tsx b/apps/student/app/signup/steps/PersonalDetailsStep.tsx index e7f0216..45bddc6 100644 --- a/apps/student/app/signup/steps/PersonalDetailsStep.tsx +++ b/apps/student/app/signup/steps/PersonalDetailsStep.tsx @@ -79,7 +79,7 @@ export default function PersonalDetailsStep({ form }: { form: any }) { name="mothersName" render={({ field }) => ( - Mother's Name * + Mother's Name @@ -89,6 +89,79 @@ export default function PersonalDetailsStep({ form }: { form: any }) { /> + {/* Parent contacts */} +
+ ( + + Mother's Email + + + + + + )} + /> + ( + + Mother's Phone + + + + + + )} + /> +
+
+ +
+ ( + + Father's Name + + + + + + )} + /> + ( + + Father's Email + + + + + + )} + /> + ( + + Father's Phone + + + + + + )} + /> +
+
new Date()) .notNull(), }); -