untested pushhhhh
This commit is contained in:
@@ -1,24 +1,25 @@
|
|||||||
'use server'
|
'use server';
|
||||||
import { db, students, grades, internships as internshipsTable, resumes as resumesTable } from '@workspace/db';
|
import {
|
||||||
|
db,
|
||||||
|
students,
|
||||||
|
grades,
|
||||||
|
internships as internshipsTable,
|
||||||
|
resumes as resumesTable,
|
||||||
|
} from '@workspace/db';
|
||||||
import { eq } from '@workspace/db/drizzle';
|
import { eq } from '@workspace/db/drizzle';
|
||||||
import { studentSignupSchema } from './schema';
|
import { studentSignupSchema, StudentSignup } from './schema';
|
||||||
import { auth } from '@/auth';
|
import { auth } from '@/auth';
|
||||||
|
|
||||||
export async function signupAction(data: FormData) {
|
export async function signupAction(data: StudentSignup) {
|
||||||
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const studentId = session?.user?.studentId;
|
const studentId = session?.user?.studentId;
|
||||||
if (!studentId) {
|
if (!studentId) {
|
||||||
return { error: 'Student ID not found in session.' };
|
return { error: 'Student ID not found in session.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = Object.fromEntries(data.entries());
|
// Validate data using schema
|
||||||
// Parse arrays/objects from formData if sent as JSON strings
|
const parsedData = await studentSignupSchema.safeParseAsync(data);
|
||||||
if (typeof formData.skills === 'string') formData.skills = JSON.parse(formData.skills);
|
|
||||||
if (typeof formData.sgpi === 'string') formData.sgpi = JSON.parse(formData.sgpi);
|
|
||||||
if (typeof formData.internships === 'string') formData.internships = JSON.parse(formData.internships);
|
|
||||||
if (typeof formData.resume === 'string') formData.resume = JSON.parse(formData.resume);
|
|
||||||
|
|
||||||
const parsedData = await studentSignupSchema.safeParseAsync(formData);
|
|
||||||
|
|
||||||
if (!parsedData.success) {
|
if (!parsedData.success) {
|
||||||
return { error: parsedData.error.issues };
|
return { error: parsedData.error.issues };
|
||||||
@@ -26,8 +27,12 @@ export async function signupAction(data: FormData) {
|
|||||||
|
|
||||||
const student = parsedData.data;
|
const student = parsedData.data;
|
||||||
|
|
||||||
|
// Use a transaction to ensure all operations succeed or fail together
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
// Update student table
|
// Update student table
|
||||||
await db.update(students).set({
|
await tx
|
||||||
|
.update(students)
|
||||||
|
.set({
|
||||||
rollNumber: student.rollNumber,
|
rollNumber: student.rollNumber,
|
||||||
firstName: student.firstName,
|
firstName: student.firstName,
|
||||||
middleName: student.middleName,
|
middleName: student.middleName,
|
||||||
@@ -47,33 +52,32 @@ export async function signupAction(data: FormData) {
|
|||||||
ssc: String(student.ssc),
|
ssc: String(student.ssc),
|
||||||
hsc: String(student.hsc),
|
hsc: String(student.hsc),
|
||||||
isDiploma: student.isDiploma,
|
isDiploma: student.isDiploma,
|
||||||
}).where(eq(students.id, studentId));
|
})
|
||||||
|
.where(eq(students.id, studentId));
|
||||||
|
|
||||||
// Upsert grades (sgpi)
|
// Clear existing grades for this student
|
||||||
|
await tx.delete(grades).where(eq(grades.studentId, studentId));
|
||||||
|
|
||||||
|
// Insert grades (sgpi)
|
||||||
if (Array.isArray(student.sgpi)) {
|
if (Array.isArray(student.sgpi)) {
|
||||||
for (const grade of student.sgpi) {
|
for (const grade of student.sgpi) {
|
||||||
await db.insert(grades).values({
|
await tx.insert(grades).values({
|
||||||
studentId: studentId,
|
studentId: studentId,
|
||||||
sem: grade.sem,
|
sem: grade.sem,
|
||||||
sgpi: String(grade.sgpi),
|
sgpi: String(grade.sgpi),
|
||||||
isKT: grade.kt,
|
isKT: grade.kt,
|
||||||
deadKT: grade.ktDead,
|
deadKT: grade.ktDead,
|
||||||
}).onConflictDoUpdate({
|
|
||||||
target: [grades.studentId, grades.sem],
|
|
||||||
set: {
|
|
||||||
sgpi: String(grade.sgpi),
|
|
||||||
isKT: grade.kt,
|
|
||||||
deadKT: grade.ktDead,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert internships
|
// Clear existing internships for this student
|
||||||
|
await tx.delete(internshipsTable).where(eq(internshipsTable.studentId, studentId));
|
||||||
|
|
||||||
|
// Insert internships
|
||||||
if (Array.isArray(student.internships)) {
|
if (Array.isArray(student.internships)) {
|
||||||
for (const internship of student.internships) {
|
for (const internship of student.internships) {
|
||||||
await db.insert(internshipsTable).values({
|
await tx.insert(internshipsTable).values({
|
||||||
studentId,
|
studentId,
|
||||||
title: internship.title,
|
title: internship.title,
|
||||||
company: internship.company,
|
company: internship.company,
|
||||||
@@ -81,36 +85,30 @@ export async function signupAction(data: FormData) {
|
|||||||
location: internship.location,
|
location: internship.location,
|
||||||
startDate: internship.startDate,
|
startDate: internship.startDate,
|
||||||
endDate: internship.endDate,
|
endDate: internship.endDate,
|
||||||
}).onConflictDoUpdate({
|
|
||||||
target: [internshipsTable.studentId, internshipsTable.title, internshipsTable.company],
|
|
||||||
set: {
|
|
||||||
description: internship.description,
|
|
||||||
location: internship.location,
|
|
||||||
startDate: internship.startDate,
|
|
||||||
endDate: internship.endDate,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert resumes
|
// Clear existing resumes for this student
|
||||||
|
await tx.delete(resumesTable).where(eq(resumesTable.studentId, studentId));
|
||||||
|
|
||||||
|
// Insert resumes
|
||||||
if (Array.isArray(student.resume)) {
|
if (Array.isArray(student.resume)) {
|
||||||
for (const resume of student.resume) {
|
for (const resume of student.resume) {
|
||||||
await db.insert(resumesTable).values({
|
await tx.insert(resumesTable).values({
|
||||||
studentId,
|
studentId,
|
||||||
title: resume.title,
|
title: resume.title,
|
||||||
link: resume.link,
|
link: resume.link,
|
||||||
}).onConflictDoUpdate({
|
|
||||||
target: [resumesTable.studentId, resumesTable.title],
|
|
||||||
set: {
|
|
||||||
link: resume.link,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signup action error:', error);
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : 'An unexpected error occurred during signup.',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -13,6 +13,7 @@ import { Button } from '@workspace/ui/components/button';
|
|||||||
import { Progress } from '@workspace/ui/components/progress';
|
import { Progress } from '@workspace/ui/components/progress';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||||
import { Form } from '@workspace/ui/components/form';
|
import { Form } from '@workspace/ui/components/form';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { studentSignupSchema, StudentSignup } from './schema';
|
import { studentSignupSchema, StudentSignup } from './schema';
|
||||||
import PersonalDetailsStep from './steps/PersonalDetailsStep';
|
import PersonalDetailsStep from './steps/PersonalDetailsStep';
|
||||||
@@ -22,9 +23,29 @@ import AdditionalDetailsStep from './steps/AdditionalDetailsStep';
|
|||||||
import InternshipStep from './steps/InternshipStep';
|
import InternshipStep from './steps/InternshipStep';
|
||||||
import ResumeStep from './steps/ResumeStep';
|
import ResumeStep from './steps/ResumeStep';
|
||||||
|
|
||||||
|
import { signupAction } from './action';
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ id: 1, title: 'Personal Details', fields: ['firstName', 'lastName', 'mothersName', 'rollNumber', 'phoneNumber', 'address', 'gender', 'dob', 'personalGmail'] },
|
{
|
||||||
{ id: 2, title: 'Academic Details', fields: ['degree', 'year', 'branch', 'ssc', 'hsc', 'isDiploma'] },
|
id: 1,
|
||||||
|
title: 'Personal Details',
|
||||||
|
fields: [
|
||||||
|
'firstName',
|
||||||
|
'lastName',
|
||||||
|
'mothersName',
|
||||||
|
'rollNumber',
|
||||||
|
'phoneNumber',
|
||||||
|
'address',
|
||||||
|
'gender',
|
||||||
|
'dob',
|
||||||
|
'personalGmail',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Academic Details',
|
||||||
|
fields: ['degree', 'year', 'branch', 'ssc', 'hsc', 'isDiploma'],
|
||||||
|
},
|
||||||
{ id: 3, title: 'Semester Grades', fields: ['sgpi'] },
|
{ id: 3, title: 'Semester Grades', fields: ['sgpi'] },
|
||||||
{ id: 4, title: 'Additional Details', fields: ['linkedin', 'github', 'skills'] },
|
{ id: 4, title: 'Additional Details', fields: ['linkedin', 'github', 'skills'] },
|
||||||
{ id: 5, title: 'Internships', fields: ['internships'] },
|
{ id: 5, title: 'Internships', fields: ['internships'] },
|
||||||
@@ -34,6 +55,8 @@ const steps = [
|
|||||||
export default function StudentRegistrationForm() {
|
export default function StudentRegistrationForm() {
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const form = useForm<StudentSignup>({
|
const form = useForm<StudentSignup>({
|
||||||
resolver: zodResolver(studentSignupSchema),
|
resolver: zodResolver(studentSignupSchema),
|
||||||
@@ -57,7 +80,12 @@ export default function StudentRegistrationForm() {
|
|||||||
ssc: 0,
|
ssc: 0,
|
||||||
hsc: 0,
|
hsc: 0,
|
||||||
isDiploma: false,
|
isDiploma: false,
|
||||||
sgpi: Array.from({ length: 8 }, (_, i) => ({ sem: i + 1, sgpi: 0, kt: false, ktDead: false })),
|
sgpi: Array.from({ length: 8 }, (_, i) => ({
|
||||||
|
sem: i + 1,
|
||||||
|
sgpi: 0,
|
||||||
|
kt: false,
|
||||||
|
ktDead: false,
|
||||||
|
})),
|
||||||
internships: [],
|
internships: [],
|
||||||
resume: [],
|
resume: [],
|
||||||
},
|
},
|
||||||
@@ -68,13 +96,21 @@ export default function StudentRegistrationForm() {
|
|||||||
const validateCurrentStep = async () => {
|
const validateCurrentStep = async () => {
|
||||||
const current = steps.find((s) => s.id === currentStep);
|
const current = steps.find((s) => s.id === currentStep);
|
||||||
if (!current) return false;
|
if (!current) return false;
|
||||||
// Cast fields to the correct type for react-hook-form
|
|
||||||
return await form.trigger(current.fields as Parameters<typeof form.trigger>[0]);
|
try {
|
||||||
|
const result = await form.trigger(current.fields as (keyof StudentSignup)[]);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Validation error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextStep = async () => {
|
const nextStep = async () => {
|
||||||
const isValid = await validateCurrentStep();
|
const isValid = await validateCurrentStep();
|
||||||
if (isValid && currentStep < steps.length) setCurrentStep((prev) => prev + 1);
|
if (isValid && currentStep < steps.length) {
|
||||||
|
setCurrentStep((prev) => prev + 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevStep = () => {
|
const prevStep = () => {
|
||||||
@@ -84,13 +120,24 @@ export default function StudentRegistrationForm() {
|
|||||||
const onSubmit = async (data: StudentSignup) => {
|
const onSubmit = async (data: StudentSignup) => {
|
||||||
// Only submit if on the last step
|
// Only submit if on the last step
|
||||||
if (currentStep !== steps.length) return;
|
if (currentStep !== steps.length) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await new Promise((res) => setTimeout(res, 2000));
|
const result = await signupAction(data);
|
||||||
console.log('Form submitted:', data);
|
if (result && result.success) {
|
||||||
alert('Form submitted successfully!');
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result && result.error) {
|
||||||
|
const errorMessage = Array.isArray(result.error)
|
||||||
|
? result.error.map((e) => e.message || e).join(', ')
|
||||||
|
: result.error;
|
||||||
|
alert('Submission failed: ' + errorMessage);
|
||||||
|
} else {
|
||||||
|
alert('Submission failed. Try again.');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error('Submission error:', err);
|
||||||
alert('Submission failed. Try again.');
|
alert('Submission failed. Try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
@@ -128,7 +175,9 @@ export default function StudentRegistrationForm() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-sm text-muted-foreground">
|
<div className="flex justify-between text-sm text-muted-foreground">
|
||||||
<span>Step {currentStep} of {steps.length}</span>
|
<span>
|
||||||
|
Step {currentStep} of {steps.length}
|
||||||
|
</span>
|
||||||
<span>{steps[currentStep - 1]?.title}</span>
|
<span>{steps[currentStep - 1]?.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={progress} className="w-full" />
|
<Progress value={progress} className="w-full" />
|
||||||
@@ -139,7 +188,7 @@ export default function StudentRegistrationForm() {
|
|||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
onKeyDown={e => {
|
onKeyDown={(e) => {
|
||||||
if (
|
if (
|
||||||
e.key === 'Enter' &&
|
e.key === 'Enter' &&
|
||||||
e.target instanceof HTMLElement &&
|
e.target instanceof HTMLElement &&
|
||||||
@@ -151,16 +200,17 @@ export default function StudentRegistrationForm() {
|
|||||||
>
|
>
|
||||||
{renderStep()}
|
{renderStep()}
|
||||||
<div className="flex justify-between pt-6">
|
<div className="flex justify-between pt-6">
|
||||||
<Button type="button" variant="outline" onClick={prevStep} disabled={currentStep === 1}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={prevStep}
|
||||||
|
disabled={currentStep === 1}
|
||||||
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
{currentStep === steps.length ? (
|
{currentStep === steps.length ? (
|
||||||
<Button
|
<Button type="submit" disabled={isSubmitting || isPending}>
|
||||||
type="button"
|
{isSubmitting || isPending ? 'Submitting...' : 'Submit'}
|
||||||
disabled={isSubmitting}
|
|
||||||
onClick={() => form.handleSubmit(onSubmit)()}
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="button" onClick={nextStep}>
|
<Button type="button" onClick={nextStep}>
|
||||||
|
|||||||
@@ -7,43 +7,50 @@ export const sgpiSchema = z.object({
|
|||||||
ktDead: z.boolean(),
|
ktDead: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const internshipSchema = z.object({
|
export const internshipSchema = z
|
||||||
title: z.string(),
|
.object({
|
||||||
company: z.string(),
|
title: z.string().min(1, 'Title is required'),
|
||||||
description: z.string(),
|
company: z.string().min(1, 'Company is required'),
|
||||||
location: z.string(),
|
description: z.string().min(1, 'Description is required'),
|
||||||
|
location: z.string().min(1, 'Location is required'),
|
||||||
startDate: z.coerce.date(),
|
startDate: z.coerce.date(),
|
||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
});
|
})
|
||||||
|
.refine((data) => data.endDate >= data.startDate, {
|
||||||
|
message: 'End date must be after start date',
|
||||||
|
path: ['endDate'],
|
||||||
|
});
|
||||||
|
|
||||||
export const resumeSchema = z.object({
|
export const resumeSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string().min(1, 'Title is required'),
|
||||||
link: z.string().url(),
|
link: z.string().url('Must be a valid URL'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const studentSignupSchema = z.object({
|
export const studentSignupSchema = z.object({
|
||||||
rollNumber: z.string().max(12),
|
rollNumber: z.string().min(1, 'Roll number is required').max(12),
|
||||||
firstName: z.string().max(255),
|
firstName: z.string().min(1, 'First name is required').max(255),
|
||||||
middleName: z.string().max(255),
|
middleName: z.string().max(255).optional(),
|
||||||
lastName: z.string().max(255),
|
lastName: z.string().min(1, 'Last name is required').max(255),
|
||||||
mothersName: z.string().max(255),
|
mothersName: z.string().min(1, "Mother's name is required").max(255),
|
||||||
gender: z.string().max(10),
|
gender: z.string().min(1, 'Gender is required').max(10),
|
||||||
dob: z.coerce.date(),
|
dob: z.coerce.date().refine((date) => date <= new Date(), {
|
||||||
personalGmail: z.string().email(),
|
message: 'Date of birth cannot be in the future',
|
||||||
phoneNumber: z.string().max(10),
|
}),
|
||||||
address: z.string(),
|
personalGmail: z.string().email('Must be a valid email'),
|
||||||
degree: z.string(),
|
phoneNumber: z.string().min(10, 'Phone number must be at least 10 digits').max(10),
|
||||||
branch: z.string(),
|
address: z.string().min(1, 'Address is required'),
|
||||||
year: z.string(),
|
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()),
|
skills: z.array(z.string()),
|
||||||
linkedin: z.string(),
|
linkedin: z.string(),
|
||||||
github: z.string(),
|
github: z.string(),
|
||||||
ssc: z.coerce.number(),
|
ssc: z.coerce.number().min(0).max(100),
|
||||||
hsc: z.coerce.number(),
|
hsc: z.coerce.number().min(0).max(100),
|
||||||
isDiploma: z.boolean(),
|
isDiploma: z.boolean(),
|
||||||
sgpi: z.array(sgpiSchema),
|
sgpi: z.array(sgpiSchema).length(8, 'Must provide grades for all 8 semesters'),
|
||||||
internships: z.array(internshipSchema).optional(),
|
internships: z.array(internshipSchema),
|
||||||
resume: z.array(resumeSchema).optional(),
|
resume: z.array(resumeSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type StudentSignup = z.infer<typeof studentSignupSchema>;
|
export type StudentSignup = z.infer<typeof studentSignupSchema>;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Degree *</FormLabel>
|
<FormLabel>Degree *</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select your degree" />
|
<SelectValue placeholder="Select your degree" />
|
||||||
@@ -64,7 +64,7 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Year *</FormLabel>
|
<FormLabel>Year *</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Year of graduation" />
|
<SelectValue placeholder="Year of graduation" />
|
||||||
@@ -89,7 +89,7 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Branch *</FormLabel>
|
<FormLabel>Branch *</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select your branch" />
|
<SelectValue placeholder="Select your branch" />
|
||||||
@@ -119,7 +119,12 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>SSC % *</FormLabel>
|
<FormLabel>SSC % *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="10th percentage" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="10th percentage"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -133,7 +138,12 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>HSC % *</FormLabel>
|
<FormLabel>HSC % *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="12th percentage" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="12th percentage"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -146,7 +156,11 @@ export default function AcademicDetailsStep({ form }: { form: any }) {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex items-center space-x-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<input type="checkbox" checked={field.value} onChange={(e) => field.onChange(e.target.checked)} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={field.value}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="!m-0">Diploma Holder?</FormLabel>
|
<FormLabel className="!m-0">Diploma Holder?</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ export default function AdditionalDetailsStep({ form }: { form: any }) {
|
|||||||
name="linkedin"
|
name="linkedin"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>LinkedIn Profile *</FormLabel>
|
<FormLabel>LinkedIn Profile</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="url" placeholder="https://linkedin.com/in/yourprofile" {...field} />
|
<Input type="url" placeholder="https://linkedin.com/in/yourprofile" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>Optional</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -37,10 +38,11 @@ export default function AdditionalDetailsStep({ form }: { form: any }) {
|
|||||||
name="github"
|
name="github"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>GitHub Profile *</FormLabel>
|
<FormLabel>GitHub Profile</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="url" placeholder="https://github.com/yourusername" {...field} />
|
<Input type="url" placeholder="https://github.com/yourusername" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormDescription>Optional</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -57,10 +59,20 @@ export default function AdditionalDetailsStep({ form }: { form: any }) {
|
|||||||
<Textarea
|
<Textarea
|
||||||
placeholder="JavaScript, React, Node.js, Python"
|
placeholder="JavaScript, React, Node.js, Python"
|
||||||
className="resize-none"
|
className="resize-none"
|
||||||
value={field.value ? (Array.isArray(field.value) ? field.value.join(", ") : field.value) : ""}
|
value={
|
||||||
onChange={e => {
|
field.value
|
||||||
|
? Array.isArray(field.value)
|
||||||
|
? field.value.join(', ')
|
||||||
|
: field.value
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
field.onChange(value.split(',').map(s => s.trim()).filter(Boolean));
|
const skills = value
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
field.onChange(skills);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ import {
|
|||||||
import { Input } from '@workspace/ui/components/input';
|
import { Input } from '@workspace/ui/components/input';
|
||||||
import { Textarea } from '@workspace/ui/components/textarea';
|
import { Textarea } from '@workspace/ui/components/textarea';
|
||||||
import { Separator } from '@workspace/ui/components/separator';
|
import { Separator } from '@workspace/ui/components/separator';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
} from '@workspace/ui/components/select';
|
||||||
|
import { Calendar } from '@workspace/ui/components/calendar';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
|
||||||
export default function PersonalDetailsStep({ form }: { form: any }) {
|
export default function PersonalDetailsStep({ form }: { form: any }) {
|
||||||
return (
|
return (
|
||||||
@@ -76,6 +89,66 @@ export default function PersonalDetailsStep({ form }: { form: any }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="gender"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Gender *</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select your gender" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="male">Male</SelectItem>
|
||||||
|
<SelectItem value="female">Female</SelectItem>
|
||||||
|
<SelectItem value="other">Other</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dob"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Date of Birth *</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'w-full pl-3 text-left font-normal',
|
||||||
|
!field.value && 'text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ? format(field.value, 'PPP') : <span>Pick a date</span>}
|
||||||
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={field.value}
|
||||||
|
onSelect={field.onChange}
|
||||||
|
disabled={(date) => date > new Date() || date < new Date('1900-01-01')}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare module 'next-auth' {
|
|||||||
role?: 'ADMIN' | 'USER';
|
role?: 'ADMIN' | 'USER';
|
||||||
adminId?: number;
|
adminId?: number;
|
||||||
studentId?: number;
|
studentId?: number;
|
||||||
completedProfile?: boolean;
|
// completedProfile?: boolean; // Removed from JWT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,41 +33,43 @@ const authConfig: NextAuthConfig = {
|
|||||||
providers: [Google],
|
providers: [Google],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, user }) {
|
async jwt({ token, account, user }) {
|
||||||
// Only check DB on first sign in
|
// Only set role, adminId, studentId, and email in JWT
|
||||||
if (account && user && user.email) {
|
const email = user?.email || token?.email;
|
||||||
const admin = await db.select().from(admins).where(eq(admins.email, user.email)).limit(1);
|
if (!email) return token;
|
||||||
|
|
||||||
|
const admin = await db.select().from(admins).where(eq(admins.email, email)).limit(1);
|
||||||
if (admin.length > 0 && admin[0]) {
|
if (admin.length > 0 && admin[0]) {
|
||||||
token.role = 'ADMIN';
|
token.role = 'ADMIN';
|
||||||
token.adminId = admin[0].id;
|
token.adminId = admin[0].id;
|
||||||
token.completedProfile = true;
|
token.email = email;
|
||||||
} else {
|
return token;
|
||||||
token.role = 'USER';
|
}
|
||||||
const student = await db
|
|
||||||
.select()
|
let student = await db.select().from(students).where(eq(students.email, email)).limit(1);
|
||||||
.from(students)
|
|
||||||
.where(eq(students.email, user.email))
|
|
||||||
.limit(1);
|
|
||||||
if (student.length > 0 && student[0]) {
|
if (student.length > 0 && student[0]) {
|
||||||
|
token.role = 'USER';
|
||||||
token.studentId = student[0].id;
|
token.studentId = student[0].id;
|
||||||
token.completedProfile = student[0].rollNumber ? true : false;
|
token.email = email;
|
||||||
} else {
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
const nameParts = user.name?.split(' ') ?? [];
|
const nameParts = user.name?.split(' ') ?? [];
|
||||||
const firstName = nameParts[0] || '';
|
const firstName = nameParts[0] || '';
|
||||||
const lastName = nameParts.slice(1).join(' ') || '';
|
const lastName = nameParts.slice(1).join(' ') || '';
|
||||||
const newStudent = await db
|
const newStudent = await db
|
||||||
.insert(students)
|
.insert(students)
|
||||||
.values({
|
.values({
|
||||||
email: user.email,
|
email: email,
|
||||||
firstName: firstName,
|
firstName: firstName,
|
||||||
lastName: lastName,
|
lastName: lastName,
|
||||||
profilePicture: user.image,
|
profilePicture: user.image,
|
||||||
})
|
})
|
||||||
.returning({ id: students.id });
|
.returning({ id: students.id });
|
||||||
if (newStudent[0]) {
|
if (newStudent[0]) {
|
||||||
|
token.role = 'USER';
|
||||||
token.studentId = newStudent[0].id;
|
token.studentId = newStudent[0].id;
|
||||||
token.completedProfile = false;
|
token.email = email;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
@@ -82,8 +84,12 @@ const authConfig: NextAuthConfig = {
|
|||||||
if (token?.studentId) {
|
if (token?.studentId) {
|
||||||
session.user.studentId = token.studentId as number;
|
session.user.studentId = token.studentId as number;
|
||||||
}
|
}
|
||||||
if (token?.completedProfile !== undefined) {
|
// Fetch completedProfile from DB for students only
|
||||||
session.user.completedProfile = token.completedProfile as boolean;
|
if (token?.role === 'USER' && token?.studentId) {
|
||||||
|
const student = await db.select().from(students).where(eq(students.id, token.studentId as number)).limit(1);
|
||||||
|
session.user.completedProfile = student[0]?.rollNumber ? true : false;
|
||||||
|
} else if (token?.role === 'ADMIN') {
|
||||||
|
session.user.completedProfile = true;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
"@workspace/ui": "workspace:*",
|
"@workspace/ui": "workspace:*",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^12.22.0",
|
"framer-motion": "^12.22.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^15.3.4",
|
"next": "^15.3.4",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -126,6 +126,9 @@ importers:
|
|||||||
'@workspace/ui':
|
'@workspace/ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/ui
|
version: link:../../packages/ui
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.22.0
|
specifier: ^12.22.0
|
||||||
version: 12.22.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 12.22.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|||||||
Reference in New Issue
Block a user