From 9aa7e61d34438e2d6f91cab6692b5950673b50f3 Mon Sep 17 00:00:00 2001 From: Anushlinux Date: Sat, 5 Jul 2025 01:01:41 +0530 Subject: [PATCH] Student page ui changes + modal --- apps/admin/app/(main)/students/columns.tsx | 193 ++++++++-- apps/admin/app/(main)/students/data-table.tsx | 138 +++++--- apps/admin/app/(main)/students/page.tsx | 206 +++++++++-- .../(main)/students/student-details-modal.tsx | 331 ++++++++++++++++++ apps/admin/package.json | 1 + packages/ui/package.json | 1 + packages/ui/src/components/card.tsx | 2 +- pnpm-lock.yaml | 48 +++ 8 files changed, 822 insertions(+), 98 deletions(-) create mode 100644 apps/admin/app/(main)/students/student-details-modal.tsx diff --git a/apps/admin/app/(main)/students/columns.tsx b/apps/admin/app/(main)/students/columns.tsx index 4d6fda6..908ae0e 100644 --- a/apps/admin/app/(main)/students/columns.tsx +++ b/apps/admin/app/(main)/students/columns.tsx @@ -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; +// 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[] = [ { - accessorKey: 'firstName', - header: 'First Name', - filterFn: 'includesString', + accessorKey: 'id', + header: 'ID', + cell: ({ row }) => { + const student = row.original; + return ( +
+ + + + {student.firstName ? student.firstName.charAt(0).toUpperCase() : + student.email ? student.email.charAt(0).toUpperCase() : 'S'} + + +
+ #{student.id} + + {student.verified ? 'Verified' : 'Pending'} + +
+
+ ); + }, }, { - 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 ( +
+ {fullName} + {student.rollNumber || 'No Roll Number'} +
+ ); + }, filterFn: 'includesString', }, { accessorKey: 'email', - header: 'Email', + header: 'Contact Information', + cell: ({ row }) => { + const student = row.original; + return ( +
+
+ + {student.email} +
+ {student.phoneNumber && ( +
+ + {student.phoneNumber} +
+ )} + {student.personalGmail && ( +
+ + {student.personalGmail} +
+ )} +
+ ); + }, filterFn: 'includesString', }, { - accessorKey: 'yearOfGraduation', - header: 'Year of Graduation', - filterFn: 'includesString', + accessorKey: 'academic', + header: 'Academic Details', + cell: ({ row }) => { + const student = row.original; + return ( +
+
+ + {student.degree || 'Not specified'} +
+
+ {student.branch || 'Branch not specified'} +
+
+ + {student.year || 'Year not specified'} +
+
+ ); + }, }, { - accessorKey: 'degree', - header: 'Degree', - filterFn: 'includesString', + accessorKey: 'location', + header: 'Location', + cell: ({ row }) => { + const student = row.original; + return ( +
+ + {student.address || 'Address not provided'} +
+ ); + }, }, { - 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 No skills listed; + } + + return ( +
+ {skills.slice(0, 3).map((skill, index) => ( + + {skill} + + ))} + {skills.length > 3 && ( + + +{skills.length - 3} more + + )} +
+ ); + }, + }, + { + id: 'actions', + header: 'Actions', + cell: ({ row }) => { + const student = row.original; + return ( +
+ + View Details +
+ ); + }, }, ]; diff --git a/apps/admin/app/(main)/students/data-table.tsx b/apps/admin/app/(main)/students/data-table.tsx index fc097df..46e8b66 100644 --- a/apps/admin/app/(main)/students/data-table.tsx +++ b/apps/admin/app/(main)/students/data-table.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { ColumnDef, flexRender, @@ -16,13 +17,18 @@ import { TableHeader, TableRow, } from '@workspace/ui/components/table'; +import { Student } from './columns'; +import { StudentDetailsModal } from './student-details-modal.tsx'; +import { columns } from './columns'; -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; +interface DataTableProps { + data: Student[]; } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ data }: DataTableProps) { + const [selectedStudent, setSelectedStudent] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const table = useReactTable({ data, columns, @@ -30,44 +36,94 @@ export function DataTable({ columns, data }: DataTableProps { + setSelectedStudent(student); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedStudent(null); + }; + return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + <> +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
-
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + handleRowClick(row.original as Student)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No students found. + + + )} + + + + + {/* Pagination */} +
+
+ Showing {table.getFilteredRowModel().rows.length} of{' '} + {table.getFilteredRowModel().rows.length} results +
+
+ + +
+
+ + {/* Student Details Modal */} + {selectedStudent && ( + + )} + ); } diff --git a/apps/admin/app/(main)/students/page.tsx b/apps/admin/app/(main)/students/page.tsx index 3d9c605..ec78791 100644 --- a/apps/admin/app/(main)/students/page.tsx +++ b/apps/admin/app/(main)/students/page.tsx @@ -1,15 +1,40 @@ -import { columns, Student } from './columns'; +import { Student } from './columns'; import { DataTable } from './data-table'; import { db, students } from '@workspace/db'; import { Input } from '@workspace/ui/components/input'; import { Button } from '@workspace/ui/components/button'; import { revalidatePath } from 'next/cache'; import { eq } from '@workspace/db/drizzle'; -import { Card } from '@workspace/ui/components/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; +import { Badge } from '@workspace/ui/components/badge'; +import { Separator } from '@workspace/ui/components/separator'; +import { + Users, + Plus, + Search, + Filter, + Download, + Mail, + Phone, + MapPin, + Calendar, + GraduationCap, + User, + Linkedin, + Github, + FileText, + Award, + BookOpen +} from 'lucide-react'; async function getData(): Promise { - const data = await db.select().from(students); - return data; + try { + const data = await db.select().from(students); + return data; + } catch (error) { + console.error('Database error:', error); + return []; + } } async function addStudent(formData: FormData) { @@ -28,42 +53,159 @@ async function StudentsTable() { const data = await getData(); return ( -
-
- {/* Add Student */} - -

Add Student

-
- - -
-
- - {/* Students Table */} - -
-

Students

-
- {data.length} {data.length === 1 ? 'student' : 'students'} total +
+
+ {/* Header Section */} +
+
+
+

Students Management

+

Manage student profiles and track their progress

+
+
+ +
- {data.length === 0 ? ( -
No students yet. Add your first student above!
- ) : ( - - )} - + + {/* Stats Cards */} +
+ + +
+
+

Total Students

+

{data.length}

+
+ +
+
+
+ + +
+
+

Verified

+

{data.filter(s => s.verified).length}

+
+
+
+
+
+
+
+ + +
+
+

Pending

+

{data.filter(s => !s.verified).length}

+
+
+
+
+
+
+
+ + +
+
+

Active

+

{data.length}

+
+ +
+
+
+
+
+ + {/* Add Student Section */} +
+ + + + + Add New Student + + + +
+
+ + +
+ +
+
+
+
+ + {/* Students Table Section */} +
+ + +
+
+ Student Directory +

+ {data.length} {data.length === 1 ? 'student' : 'students'} in the system +

+
+
+
+ + +
+
+
+
+ + {data.length === 0 ? ( +
+ +

No students yet

+

Get started by adding your first student above

+ +
+ ) : ( + + )} +
+
+
- {/* Toast placeholder for feedback */} -
); } export default function StudentsPage() { - return ( - - ); + return ; } export const dynamic = 'force-dynamic'; \ No newline at end of file diff --git a/apps/admin/app/(main)/students/student-details-modal.tsx b/apps/admin/app/(main)/students/student-details-modal.tsx new file mode 100644 index 0000000..f90b128 --- /dev/null +++ b/apps/admin/app/(main)/students/student-details-modal.tsx @@ -0,0 +1,331 @@ +'use client'; + +import { Student } from './columns'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@workspace/ui/components/dialog'; +import { Badge } from '@workspace/ui/components/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar'; +import { Separator } from '@workspace/ui/components/separator'; +import { Button } from '@workspace/ui/components/button'; +import { + Mail, + Phone, + MapPin, + Calendar, + GraduationCap, + User, + Linkedin, + Github, + FileText, + Award, + BookOpen, + ExternalLink, + X, + Edit, + Download, + Share2 +} from 'lucide-react'; + +interface StudentDetailsModalProps { + student: Student; + isOpen: boolean; + onClose: () => void; +} + +export function StudentDetailsModal({ student, isOpen, onClose }: StudentDetailsModalProps) { + const fullName = [ + student.firstName, + student.middleName, + student.lastName + ].filter(Boolean).join(' ') || 'Name not provided'; + + const formatDate = (date: Date | null) => { + if (!date) return 'Not provided'; + return new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + }; + + const formatPercentage = (value: number | null) => { + if (value === null) return 'Not provided'; + return `${value}%`; + }; + + return ( + + + +
+ + Student Details + + +
+
+ +
+ {/* Header Section */} +
+ + + + {student.firstName ? student.firstName.charAt(0).toUpperCase() : + student.email ? student.email.charAt(0).toUpperCase() : 'S'} + + + +
+
+

{fullName}

+ + {student.verified ? 'Verified' : 'Pending Verification'} + +
+ +
+ ID: #{student.id} + {student.rollNumber && ( + Roll: {student.rollNumber} + )} + + Joined: {formatDate(new Date(Number(student.createdAt)))} + +
+
+ +
+ + +
+
+ +
+ {/* Personal Information */} +
+

+ + Personal Information +

+ +
+
+ +
+

Email

+

{student.email}

+
+
+ + {student.personalGmail && ( +
+ +
+

Personal Gmail

+

{student.personalGmail}

+
+
+ )} + + {student.phoneNumber && ( +
+ +
+

Phone Number

+

{student.phoneNumber}

+
+
+ )} + + {student.address && ( +
+ +
+

Address

+

{student.address}

+
+
+ )} + + {student.dob && ( +
+ +
+

Date of Birth

+

{formatDate(student.dob)}

+
+
+ )} + + {student.gender && ( +
+ +
+

Gender

+

{student.gender}

+
+
+ )} + + {student.mothersName && ( +
+ +
+

Mother's Name

+

{student.mothersName}

+
+
+ )} +
+
+ + {/* Academic Information */} +
+

+ + Academic Information +

+ +
+ {student.degree && ( +
+ +
+

Degree

+

{student.degree}

+
+
+ )} + + {student.branch && ( +
+ +
+

Branch

+

{student.branch}

+
+
+ )} + + {student.year && ( +
+ +
+

Year

+

{student.year}

+
+
+ )} + +
+ +
+

Diploma Student

+

{student.isDiploma ? 'Yes' : 'No'}

+
+
+
+ + + + {/* Academic Scores */} +
+

Academic Scores

+ +
+
+

SSC Score

+

+ {formatPercentage(Number(student.ssc))} +

+
+ +
+

HSC Score

+

+ {formatPercentage(Number(student.hsc))} +

+
+
+
+
+
+ + {/* Skills Section */} + {student.skills && student.skills.length > 0 && ( +
+

+ + Skills & Expertise +

+
+ {student.skills.map((skill, index) => ( + + {skill} + + ))} +
+
+ )} + + {/* Professional Links */} + {(student.linkedin || student.github) && ( +
+

+ + Professional Links +

+
+ {student.linkedin && ( + + )} + {student.github && ( + + )} +
+
+ )} + + {/* Footer Actions */} + +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/admin/package.json b/apps/admin/package.json index 2c68218..0b5283e 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -15,6 +15,7 @@ "@headlessui/react": "^2.2.4", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.1.1", + "@radix-ui/react-avatar": "^1.1.10", "@tailwindcss/postcss": "^4.0.8", "@tanstack/react-table": "^8.21.3", "@workspace/db": "workspace:*", diff --git a/packages/ui/package.json b/packages/ui/package.json index a13302a..fd2241f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,6 +9,7 @@ "dependencies": { "@hookform/resolvers": "^5.1.1", "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-label": "^2.1.0", diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index dd6dbdd..e736f26 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {