Pratham200Rajbhar
first commit
78013c4
Raw
History Blame Contribute Delete
12.4 kB
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard, Users, GraduationCap, BookOpen, Building2, ScanSearch,
ScrollText, Radio, ClipboardCheck, BarChart3, Shield, Sliders, ChevronDown, ChevronRight,
FileText, Smartphone
} from "lucide-react";
import { useAuthStore } from "@/store/authStore";
import api from "@/lib/api";
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
}
const adminLinks: NavItem[] = [
{ label: "Overview", href: "/admin/dashboard", icon: <LayoutDashboard size={18} /> },
{ label: "Students", href: "/admin/users/students", icon: <GraduationCap size={18} /> },
{ label: "Teachers", href: "/admin/users/teachers", icon: <Users size={18} /> },
{ label: "Classes", href: "/admin/classes", icon: <BookOpen size={18} /> },
{ label: "AI Scanner", href: "/admin/scanner", icon: <ScanSearch size={18} /> },
{ label: "Audit Log", href: "/admin/audit", icon: <ScrollText size={18} /> },
];
const setupLinks: NavItem[] = [
{ label: "Departments", href: "/admin/setup/departments", icon: <Building2 size={16} /> },
{ label: "Subjects", href: "/admin/setup/subjects", icon: <BookOpen size={16} /> },
{ label: "Classrooms", href: "/admin/setup/classrooms", icon: <Sliders size={16} /> },
{ label: "Designations", href: "/admin/setup/designations", icon: <Users size={16} /> },
{ label: "Verification Settings", href: "/admin/setup/verification-settings", icon: <Shield size={16} /> },
];
const teacherLinks: NavItem[] = [
{ label: "Overview", href: "/teacher/dashboard", icon: <LayoutDashboard size={18} /> },
{ label: "My Classes", href: "/teacher/classes", icon: <BookOpen size={18} /> },
{ label: "Sessions", href: "/teacher/sessions", icon: <Radio size={18} /> },
{ label: "Review Queue", href: "/teacher/review", icon: <ClipboardCheck size={18} /> },
{ label: "Leave Requests", href: "/teacher/leaves", icon: <FileText size={18} /> },
{ label: "Device Changes", href: "/teacher/device-changes", icon: <Smartphone size={18} /> },
];
const teacherReportLinks: NavItem[] = [
{ label: "Class Analytics", href: "/teacher/analytics", icon: <BarChart3 size={18} /> },
{ label: "Attendance History", href: "/teacher/history", icon: <ScrollText size={18} /> },
];
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
}
export default function Sidebar({ isOpen, onClose }: SidebarProps): React.ReactElement {
const pathname = usePathname();
const user = useAuthStore((s) => s.user);
const role = user?.role;
const isSetupActive = pathname.startsWith("/admin/setup");
const [setupOpen, setSetupOpen] = useState(isSetupActive);
const [prevIsSetupActive, setPrevIsSetupActive] = useState(isSetupActive);
const [flaggedCount, setFlaggedCount] = useState(0);
if (isSetupActive !== prevIsSetupActive) {
setPrevIsSetupActive(isSetupActive);
if (isSetupActive) {
setSetupOpen(true);
}
}
useEffect(() => {
if (role !== "TEACHER") return;
let isMounted = true;
async function fetchFlagged(): Promise<void> {
try {
const { data } = await api.get<unknown[]>("/teacher/attendance/flagged");
if (isMounted) {
setFlaggedCount(Array.isArray(data) ? data.length : 0);
}
} catch {
// Ignore errors in background polling
}
}
void fetchFlagged();
const interval = setInterval(() => {
void fetchFlagged();
}, 60_000);
return () => {
isMounted = false;
clearInterval(interval);
};
}, [role]);
return (
<>
{isOpen && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-30 md:hidden animate-fade-in-up" onClick={onClose} />
)}
<aside className={`sidebar ${isOpen ? "open" : ""}`}>
{}
<div className="p-5 border-b border-white/5">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-gradient-to-tr from-emerald-500/10 to-teal-500/10 border border-white/10 shadow-[0_4px_12px_rgba(0,0,0,0.5)]">
<Shield size={20} className="text-emerald-400" />
</div>
<div>
<h2 className="text-base font-extrabold text-slate-100 leading-tight tracking-wide font-[Outfit]">Smart Attendance</h2>
<p className="text-[12px] text-slate-500 font-semibold tracking-wide">System Dashboard</p>
</div>
</div>
</div>
{}
<nav className="flex-1 py-4 overflow-y-auto space-y-1">
{role === "ADMIN" ? (
<>
<p className="px-6 mb-2 text-[10px] font-bold text-slate-500 uppercase tracking-widest">
Administration
</p>
<div className="space-y-1">
{adminLinks.map((link) => {
const isActive = pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
onClick={onClose}
className={`sidebar-link relative group ${isActive ? "sidebar-link-active" : ""}`}
>
<span className={`transition-colors duration-200 ${isActive ? "text-slate-200" : "text-slate-400 group-hover:text-slate-200"}`}>
{link.icon}
</span>
<span className="font-semibold text-sm tracking-wide">{link.label}</span>
{isActive && (
<span className="absolute left-0 top-[20%] bottom-[20%] w-[3px] rounded-r bg-emerald-400 shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
)}
</Link>
);
})}
</div>
<div className="mt-4 border-t border-white/5 pt-4">
<button
onClick={() => setSetupOpen(!setupOpen)}
className={`w-full sidebar-link justify-between group ${isSetupActive ? "text-slate-300 font-semibold" : "text-slate-400 hover:text-slate-200"}`}
>
<div className="flex items-center gap-3">
<Sliders size={18} className={isSetupActive ? "text-slate-300" : "text-slate-400 group-hover:text-slate-200"} />
<span className="font-semibold text-sm tracking-wide">System Setup</span>
</div>
{setupOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
{setupOpen && (
<div className="pl-6 mt-1 space-y-0.5 transition-all duration-300">
{setupLinks.map((link) => {
const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
return (
<Link
key={link.href}
href={link.href}
onClick={onClose}
className={`sidebar-link py-2 relative group ${isActive
? "sidebar-link-active text-slate-200 bg-white/5 font-semibold"
: "text-slate-400 hover:text-slate-200"
}`}
>
<span className={`transition-colors duration-200 ${isActive ? "text-slate-200" : "text-slate-500 group-hover:text-slate-300"}`}>
{link.icon}
</span>
<span className="text-[13px] font-medium tracking-wide">{link.label}</span>
{isActive && (
<span className="absolute left-0 top-[20%] bottom-[20%] w-[3px] rounded-r bg-emerald-400 shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
)}
</Link>
);
})}
</div>
)}
</div>
</>
) : (
<>
{}
<p className="px-6 mb-2 text-[10px] font-bold text-slate-500 uppercase tracking-widest">
Teaching Operations
</p>
<div className="space-y-1">
{teacherLinks.map((link) => {
const isActive = pathname.startsWith(link.href);
const isReviewLink = link.href === "/teacher/review";
return (
<Link
key={link.href}
href={link.href}
onClick={onClose}
className={`sidebar-link relative group ${isActive ? "sidebar-link-active" : ""}`}
>
<span className={`transition-colors duration-200 ${isActive ? "text-slate-200" : "text-slate-400 group-hover:text-slate-200"}`}>
{link.icon}
</span>
<span className="font-semibold text-sm tracking-wide">{link.label}</span>
{isReviewLink && flaggedCount > 0 && (
<span className="ml-auto flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 text-[10px] font-bold shadow-[0_4px_12px_rgba(0,0,0,0.5)]">
{flaggedCount > 99 ? "99+" : flaggedCount}
</span>
)}
{isActive && !isReviewLink && (
<span className="absolute left-0 top-[20%] bottom-[20%] w-[3px] rounded-r bg-emerald-400 shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
)}
</Link>
);
})}
</div>
{}
<div className="mt-4 border-t border-white/5 pt-4">
<p className="px-6 mb-2 text-[10px] font-bold text-slate-500 uppercase tracking-widest">
Reports & Archives
</p>
<div className="space-y-1">
{teacherReportLinks.map((link) => {
const isActive = pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
onClick={onClose}
className={`sidebar-link relative group ${isActive ? "sidebar-link-active" : ""}`}
>
<span className={`transition-colors duration-200 ${isActive ? "text-slate-200" : "text-slate-400 group-hover:text-slate-200"}`}>
{link.icon}
</span>
<span className="font-semibold text-sm tracking-wide">{link.label}</span>
{isActive && (
<span className="absolute left-0 top-[20%] bottom-[20%] w-[3px] rounded-r bg-emerald-400 shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
)}
</Link>
);
})}
</div>
</div>
</>
)}
</nav>
{}
{user && (
<div className="mx-4 mb-2 p-3 rounded-xl bg-white/[0.01] border border-white/5 flex items-center gap-3 shadow-inner hover:bg-white/[0.03] transition-all duration-300">
<div className="w-8 h-8 rounded-lg bg-gradient-to-tr from-emerald-500/10 to-teal-500/10 border border-emerald-500/20 flex items-center justify-center font-bold text-emerald-400 text-xs shadow-sm">
{user.email ? user.email[0].toUpperCase() : "U"}
</div>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-bold text-slate-200 truncate leading-normal">{user.email || "User"}</p>
<p className="text-[10px] text-slate-500 truncate uppercase font-bold tracking-wider leading-none mt-0.5">{user.role || "Guest"}</p>
</div>
</div>
)}
<div className="p-3 border-t border-white/5">
<p className="text-[10px] font-semibold text-slate-600 text-center tracking-wider">SAS v1.0.0</p>
</div>
</aside>
</>
);
}