Student page ui changes + modal
This commit is contained in:
@@ -1,44 +1,189 @@
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { createSelectSchema, students } from '@workspace/db';
|
||||
import * as z from 'zod/v4';
|
||||
// Remove server-specific imports to avoid issues in client bundle
|
||||
// import { createSelectSchema, students } from '@workspace/db';
|
||||
// import * as z from 'zod/v4';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
// import { Button } from '@workspace/ui/components/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar';
|
||||
import { Eye, Mail, Phone, MapPin, Calendar, GraduationCap } from 'lucide-react';
|
||||
|
||||
const studentSelectSchema = createSelectSchema(students);
|
||||
export type Student = z.infer<typeof studentSelectSchema>;
|
||||
// Define the Student interface locally to avoid importing server-side code
|
||||
export interface Student {
|
||||
id: number;
|
||||
email: string;
|
||||
rollNumber: string | null;
|
||||
verified: boolean;
|
||||
firstName: string | null;
|
||||
middleName: string | null;
|
||||
lastName: string | null;
|
||||
mothersName?: string | null;
|
||||
gender?: string | null;
|
||||
dob?: Date | null;
|
||||
personalGmail?: string | null;
|
||||
phoneNumber?: string | null;
|
||||
address?: string | null;
|
||||
profilePicture?: string | null;
|
||||
degree?: string | null;
|
||||
branch?: string | null;
|
||||
year?: string | null;
|
||||
skills?: string[] | null;
|
||||
ssc?: number | null;
|
||||
hsc?: number | null;
|
||||
isDiploma?: boolean | null;
|
||||
linkedin?: string | null;
|
||||
github?: string | null;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Student>[] = [
|
||||
{
|
||||
accessorKey: 'firstName',
|
||||
header: 'First Name',
|
||||
filterFn: 'includesString',
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="w-10 h-10">
|
||||
<AvatarImage src={student.profilePicture || undefined} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
|
||||
{student.firstName ? student.firstName.charAt(0).toUpperCase() :
|
||||
student.email ? student.email.charAt(0).toUpperCase() : 'S'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-900">#{student.id}</span>
|
||||
<Badge
|
||||
variant={student.verified ? "default" : "secondary"}
|
||||
className={`text-xs ${student.verified ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}
|
||||
>
|
||||
{student.verified ? 'Verified' : 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastName',
|
||||
header: 'Last Name',
|
||||
filterFn: 'includesString',
|
||||
},
|
||||
{
|
||||
accessorKey: 'rollNumber',
|
||||
header: 'Roll Number',
|
||||
accessorKey: 'name',
|
||||
header: 'Student Name',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
const fullName = [
|
||||
student.firstName,
|
||||
student.middleName,
|
||||
student.lastName
|
||||
].filter(Boolean).join(' ') || 'Not provided';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-gray-900">{fullName}</span>
|
||||
<span className="text-sm text-gray-500">{student.rollNumber || 'No Roll Number'}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
filterFn: 'includesString',
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
header: 'Contact Information',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Mail className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-900">{student.email}</span>
|
||||
</div>
|
||||
{student.phoneNumber && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Phone className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-600">{student.phoneNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
{student.personalGmail && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Mail className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-600">{student.personalGmail}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
filterFn: 'includesString',
|
||||
},
|
||||
{
|
||||
accessorKey: 'yearOfGraduation',
|
||||
header: 'Year of Graduation',
|
||||
filterFn: 'includesString',
|
||||
accessorKey: 'academic',
|
||||
header: 'Academic Details',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<GraduationCap className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-900">{student.degree || 'Not specified'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-600">{student.branch || 'Branch not specified'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-600">{student.year || 'Year not specified'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'degree',
|
||||
header: 'Degree',
|
||||
filterFn: 'includesString',
|
||||
accessorKey: 'location',
|
||||
header: 'Location',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MapPin className="w-3 h-3 text-gray-400" />
|
||||
<span className="text-gray-600">{student.address || 'Address not provided'}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'branch',
|
||||
header: 'Branch',
|
||||
filterFn: 'includesString',
|
||||
accessorKey: 'skills',
|
||||
header: 'Skills',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
const skills = student.skills || [];
|
||||
|
||||
if (skills.length === 0) {
|
||||
return <span className="text-sm text-gray-500">No skills listed</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{skills.slice(0, 3).map((skill, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{skill}
|
||||
</Badge>
|
||||
))}
|
||||
{skills.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{skills.length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => {
|
||||
const student = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-blue-600">
|
||||
<Eye className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">View Details</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user