Student page ui changes + modal
This commit is contained in:
@@ -1,44 +1,189 @@
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { createSelectSchema, students } from '@workspace/db';
|
// Remove server-specific imports to avoid issues in client bundle
|
||||||
import * as z from 'zod/v4';
|
// 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);
|
// Define the Student interface locally to avoid importing server-side code
|
||||||
export type Student = z.infer<typeof studentSelectSchema>;
|
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>[] = [
|
export const columns: ColumnDef<Student>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'firstName',
|
accessorKey: 'id',
|
||||||
header: 'First Name',
|
header: 'ID',
|
||||||
filterFn: 'includesString',
|
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',
|
accessorKey: 'name',
|
||||||
header: 'Last Name',
|
header: 'Student Name',
|
||||||
filterFn: 'includesString',
|
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>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'rollNumber',
|
|
||||||
header: 'Roll Number',
|
|
||||||
filterFn: 'includesString',
|
filterFn: 'includesString',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'email',
|
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',
|
filterFn: 'includesString',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'yearOfGraduation',
|
accessorKey: 'academic',
|
||||||
header: 'Year of Graduation',
|
header: 'Academic Details',
|
||||||
filterFn: 'includesString',
|
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',
|
accessorKey: 'location',
|
||||||
header: 'Degree',
|
header: 'Location',
|
||||||
filterFn: 'includesString',
|
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',
|
accessorKey: 'skills',
|
||||||
header: 'Branch',
|
header: 'Skills',
|
||||||
filterFn: 'includesString',
|
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>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
@@ -16,13 +17,18 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@workspace/ui/components/table';
|
} from '@workspace/ui/components/table';
|
||||||
|
import { Student } from './columns';
|
||||||
|
import { StudentDetailsModal } from './student-details-modal.tsx';
|
||||||
|
import { columns } from './columns';
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
data: Student[];
|
||||||
data: TData[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
export function DataTable({ data }: DataTableProps) {
|
||||||
|
const [selectedStudent, setSelectedStudent] = useState<Student | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
@@ -30,15 +36,26 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
|
|||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleRowClick = (student: Student) => {
|
||||||
|
setSelectedStudent(student);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setSelectedStudent(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<>
|
||||||
|
<div className="rounded-md border bg-white">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id} className="bg-gray-50">
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<TableHead key={header.id}>
|
<TableHead key={header.id} className="font-semibold text-gray-700">
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
@@ -51,9 +68,14 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows?.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
|
className="cursor-pointer hover:bg-gray-50 transition-colors duration-150"
|
||||||
|
onClick={() => handleRowClick(row.original as Student)}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={cell.id} className="py-4">
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
@@ -61,13 +83,47 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center text-gray-500">
|
||||||
No results.
|
No students found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex items-center justify-between space-x-2 py-4">
|
||||||
|
<div className="flex-1 text-sm text-gray-700">
|
||||||
|
Showing {table.getFilteredRowModel().rows.length} of{' '}
|
||||||
|
{table.getFilteredRowModel().rows.length} results
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Student Details Modal */}
|
||||||
|
{selectedStudent && (
|
||||||
|
<StudentDetailsModal
|
||||||
|
student={selectedStudent}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
import { columns, Student } from './columns';
|
import { Student } from './columns';
|
||||||
import { DataTable } from './data-table';
|
import { DataTable } from './data-table';
|
||||||
import { db, students } from '@workspace/db';
|
import { db, students } from '@workspace/db';
|
||||||
import { Input } from '@workspace/ui/components/input';
|
import { Input } from '@workspace/ui/components/input';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { eq } from '@workspace/db/drizzle';
|
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<Student[]> {
|
async function getData(): Promise<Student[]> {
|
||||||
|
try {
|
||||||
const data = await db.select().from(students);
|
const data = await db.select().from(students);
|
||||||
return data;
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addStudent(formData: FormData) {
|
async function addStudent(formData: FormData) {
|
||||||
@@ -28,42 +53,159 @@ async function StudentsTable() {
|
|||||||
const data = await getData();
|
const data = await getData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-10">
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
<div className="space-y-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Add Student */}
|
{/* Header Section */}
|
||||||
<Card className="p-6 shadow-md border border-border bg-card">
|
<section className="mb-8">
|
||||||
<h2 className="text-2xl font-bold mb-4 text-primary">Add Student</h2>
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
<form action={addStudent} className="flex gap-2 items-end">
|
<div>
|
||||||
<Input name="email" type="email" placeholder="Student email" className="max-w-sm" required />
|
<h1 className="text-4xl font-bold text-gray-800 mb-2">Students Management</h1>
|
||||||
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Add Student</Button>
|
<p className="text-gray-600 text-lg">Manage student profiles and track their progress</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="outline" className="flex items-center gap-2">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="flex items-center gap-2">
|
||||||
|
<Filter className="w-4 h-4" />
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<Card className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Total Students</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-800">{data.length}</p>
|
||||||
|
</div>
|
||||||
|
<Users className="w-8 h-8 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Verified</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-800">{data.filter(s => s.verified).length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Pending</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-800">{data.filter(s => !s.verified).length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||||
|
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Active</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-800">{data.length}</p>
|
||||||
|
</div>
|
||||||
|
<User className="w-8 h-8 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Add Student Section */}
|
||||||
|
<section className="mb-8">
|
||||||
|
<Card className="bg-white shadow-sm border border-gray-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-xl font-semibold text-gray-800">
|
||||||
|
<Plus className="w-5 h-5 text-blue-600" />
|
||||||
|
Add New Student
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form action={addStudent} className="flex flex-col sm:flex-row gap-4 items-end">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-2 block">Student Email</label>
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter student email address"
|
||||||
|
className="h-11"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="h-11 px-6 bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors duration-200 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Add Student
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Students Table */}
|
{/* Students Table Section */}
|
||||||
<Card className="p-6 shadow-md border border-border bg-card">
|
<section>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<Card className="bg-white shadow-sm border border-gray-200">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight text-accent">Students</h1>
|
<CardHeader>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
{data.length} {data.length === 1 ? 'student' : 'students'} total
|
<div>
|
||||||
|
<CardTitle className="text-xl font-semibold text-gray-800 mb-1">Student Directory</CardTitle>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{data.length} {data.length === 1 ? 'student' : 'students'} in the system
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search students..."
|
||||||
|
className="pl-10 h-9 w-64"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<div className="text-center text-muted-foreground">No students yet. Add your first student above!</div>
|
<div className="text-center py-12">
|
||||||
) : (
|
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
<DataTable columns={columns} data={data} />
|
<h3 className="text-lg font-medium text-gray-700 mb-2">No students yet</h3>
|
||||||
)}
|
<p className="text-gray-500 mb-6">Get started by adding your first student above</p>
|
||||||
</Card>
|
<Button className="flex items-center gap-2 mx-auto">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Add First Student
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DataTable data={data} />
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{/* Toast placeholder for feedback */}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StudentsPage() {
|
export default function StudentsPage() {
|
||||||
return (
|
return <StudentsTable />;
|
||||||
<StudentsTable />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
331
apps/admin/app/(main)/students/student-details-modal.tsx
Normal file
331
apps/admin/app/(main)/students/student-details-modal.tsx
Normal file
@@ -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 (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<DialogTitle className="text-2xl font-bold text-gray-800">
|
||||||
|
Student Details
|
||||||
|
</DialogTitle>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="flex items-start gap-6 p-6 bg-gradient-to-r from-blue-50 to-purple-50 rounded-xl">
|
||||||
|
<Avatar className="w-20 h-20">
|
||||||
|
<AvatarImage src={student.profilePicture || undefined} />
|
||||||
|
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-2xl font-bold">
|
||||||
|
{student.firstName ? student.firstName.charAt(0).toUpperCase() :
|
||||||
|
student.email ? student.email.charAt(0).toUpperCase() : 'S'}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800">{fullName}</h2>
|
||||||
|
<Badge
|
||||||
|
variant={student.verified ? "default" : "secondary"}
|
||||||
|
className={`${student.verified ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}
|
||||||
|
>
|
||||||
|
{student.verified ? 'Verified' : 'Pending Verification'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||||
|
<span className="font-medium">ID: #{student.id}</span>
|
||||||
|
{student.rollNumber && (
|
||||||
|
<span className="font-medium">Roll: {student.rollNumber}</span>
|
||||||
|
)}
|
||||||
|
<span className="font-medium">
|
||||||
|
Joined: {formatDate(new Date(Number(student.createdAt)))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Personal Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5 text-blue-600" />
|
||||||
|
Personal Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Mail className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Email</p>
|
||||||
|
<p className="text-gray-900">{student.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{student.personalGmail && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Mail className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Personal Gmail</p>
|
||||||
|
<p className="text-gray-900">{student.personalGmail}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.phoneNumber && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Phone className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Phone Number</p>
|
||||||
|
<p className="text-gray-900">{student.phoneNumber}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.address && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<MapPin className="w-4 h-4 text-gray-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Address</p>
|
||||||
|
<p className="text-gray-900">{student.address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.dob && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Calendar className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Date of Birth</p>
|
||||||
|
<p className="text-gray-900">{formatDate(student.dob)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.gender && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<User className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Gender</p>
|
||||||
|
<p className="text-gray-900">{student.gender}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.mothersName && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<User className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Mother's Name</p>
|
||||||
|
<p className="text-gray-900">{student.mothersName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Academic Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<GraduationCap className="w-5 h-5 text-green-600" />
|
||||||
|
Academic Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{student.degree && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<GraduationCap className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Degree</p>
|
||||||
|
<p className="text-gray-900">{student.degree}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.branch && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<BookOpen className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Branch</p>
|
||||||
|
<p className="text-gray-900">{student.branch}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{student.year && (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Calendar className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Year</p>
|
||||||
|
<p className="text-gray-900">{student.year}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Award className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-700">Diploma Student</p>
|
||||||
|
<p className="text-gray-900">{student.isDiploma ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Academic Scores */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-md font-semibold text-gray-700">Academic Scores</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<p className="text-sm font-medium text-gray-700">SSC Score</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900">
|
||||||
|
{formatPercentage(Number(student.ssc))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 p-3 rounded-lg">
|
||||||
|
<p className="text-sm font-medium text-gray-700">HSC Score</p>
|
||||||
|
<p className="text-lg font-bold text-gray-900">
|
||||||
|
{formatPercentage(Number(student.hsc))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skills Section */}
|
||||||
|
{student.skills && student.skills.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<Award className="w-5 h-5 text-orange-600" />
|
||||||
|
Skills & Expertise
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{student.skills.map((skill, index) => (
|
||||||
|
<Badge key={index} variant="outline" className="text-sm">
|
||||||
|
{skill}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Professional Links */}
|
||||||
|
{(student.linkedin || student.github) && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
|
||||||
|
<ExternalLink className="w-5 h-5 text-blue-600" />
|
||||||
|
Professional Links
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{student.linkedin && (
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<Linkedin className="w-4 h-4" />
|
||||||
|
LinkedIn
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{student.github && (
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<Separator />
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button className="flex items-center gap-2">
|
||||||
|
<Share2 className="w-4 h-4" />
|
||||||
|
Share Profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@tailwindcss/postcss": "^4.0.8",
|
"@tailwindcss/postcss": "^4.0.8",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@workspace/db": "workspace:*",
|
"@workspace/db": "workspace:*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-card/90 text-card-foreground flex flex-col gap-6 rounded-2xl border border-border/50 py-8 shadow-xl shadow-black/5 backdrop-blur-sm hover:shadow-2xl hover:shadow-black/10 transition-all duration-300 hover:-translate-y-1',
|
'bg-card/90 text-card-foreground flex flex-col gap-6 rounded-2xl border border-border/50 py-8 shadow-xl shadow-black/5 backdrop-blur-sm hover:shadow-2xl hover:shadow-black/10 transition-all duration-300',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1(react-hook-form@7.59.0(react@19.1.0))
|
version: 5.1.1(react-hook-form@7.59.0(react@19.1.0))
|
||||||
|
'@radix-ui/react-avatar':
|
||||||
|
specifier: ^1.1.10
|
||||||
|
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.1.11
|
version: 4.1.11
|
||||||
@@ -247,6 +250,9 @@ importers:
|
|||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-avatar':
|
||||||
|
specifier: ^1.1.10
|
||||||
|
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.3.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -1011,6 +1017,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.10':
|
||||||
|
resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-checkbox@1.3.2':
|
'@radix-ui/react-checkbox@1.3.2':
|
||||||
resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==}
|
resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1309,6 +1328,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-is-hydrated@0.1.0':
|
||||||
|
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4150,6 +4178,19 @@ snapshots:
|
|||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
|
'@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
'@types/react-dom': 19.1.6(@types/react@19.1.8)
|
||||||
|
|
||||||
'@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-checkbox@1.3.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
@@ -4455,6 +4496,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.8
|
'@types/react': 19.1.8
|
||||||
|
|
||||||
|
'@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.8)(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
use-sync-external-store: 1.5.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.8
|
||||||
|
|
||||||
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)':
|
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user