React 18 + TypeScript + Vite 5 + Tailwind CSS + shadcn/ui frontend: - Multi-role auth: student, teacher, admin, corporate, agent, developer - Student portal: dashboard, courses, adaptive learning, exams, placement - Teacher portal: courses, assignments, grading, attendance, AI workbench - Admin portal: 60+ management pages (LMS, exams, users, entities, AI) - AI-powered features: study coach, generation page, gap analysis, TTS - Exam system: session, auto-save, results, grading queue - Full IELTS workflow: reading/listening/writing/speaking modules Made-with: Cursor
146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
import { Outlet, useLocation, Link, useNavigate } from "react-router-dom";
|
|
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
import { AppSidebar } from "@/components/AppSidebar";
|
|
import {
|
|
Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,
|
|
BreadcrumbPage, BreadcrumbSeparator,
|
|
} from "@/components/ui/breadcrumb";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
|
DropdownMenuSeparator, DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Ticket, Settings, User, LogOut, HelpCircle } from "lucide-react";
|
|
import React from "react";
|
|
import AiAssistantDrawer from "@/components/ai/AiAssistantDrawer";
|
|
import AiSearchBar from "@/components/ai/AiSearchBar";
|
|
|
|
const routeLabels: Record<string, string> = {
|
|
"dashboard": "Dashboard",
|
|
"admin": "Admin",
|
|
"assignments": "Assignments",
|
|
"examsList": "Exams List",
|
|
"exam-structures": "Exam Structures",
|
|
"rubrics": "Rubrics",
|
|
"generation": "Exam Generation",
|
|
"approval-workflows": "Approval Workflows",
|
|
"users": "Users",
|
|
"entities": "Entities",
|
|
"classrooms": "Classrooms",
|
|
"student-performance": "Student Performance",
|
|
"stats-corporate": "Stats Corporate",
|
|
"record": "Record",
|
|
"training": "Training",
|
|
"vocabulary": "Vocabulary",
|
|
"grammar": "Grammar",
|
|
"payment-record": "Payment Record",
|
|
"tickets": "Tickets",
|
|
"settings": "Settings",
|
|
"profile": "Profile",
|
|
"exam": "Exam",
|
|
"tutor": "Tutor",
|
|
};
|
|
|
|
function AppBreadcrumbs() {
|
|
const location = useLocation();
|
|
const segments = location.pathname.split("/").filter(Boolean);
|
|
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink asChild>
|
|
<Link to="/dashboard/admin">Home</Link>
|
|
</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
{segments.map((seg, i) => {
|
|
const path = "/" + segments.slice(0, i + 1).join("/");
|
|
const label = routeLabels[seg] || seg.charAt(0).toUpperCase() + seg.slice(1);
|
|
const isLast = i === segments.length - 1;
|
|
return (
|
|
<React.Fragment key={path}>
|
|
<BreadcrumbSeparator />
|
|
<BreadcrumbItem>
|
|
{isLast ? (
|
|
<BreadcrumbPage>{label}</BreadcrumbPage>
|
|
) : (
|
|
<BreadcrumbLink asChild>
|
|
<Link to={path}>{label}</Link>
|
|
</BreadcrumbLink>
|
|
)}
|
|
</BreadcrumbItem>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
);
|
|
}
|
|
|
|
export default function AppLayout() {
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<div className="min-h-screen flex w-full">
|
|
<AppSidebar />
|
|
<div className="flex-1 flex flex-col min-w-0">
|
|
<header className="h-14 border-b bg-card flex items-center justify-between px-4 shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<SidebarTrigger />
|
|
<AppBreadcrumbs />
|
|
</div>
|
|
<AiSearchBar />
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="ghost" size="icon" onClick={() => navigate("/tickets")} className="text-muted-foreground hover:text-foreground">
|
|
<Ticket className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" onClick={() => navigate("/settings")} className="text-muted-foreground hover:text-foreground">
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="rounded-full">
|
|
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center">
|
|
<span className="text-xs font-semibold text-primary-foreground">AU</span>
|
|
</div>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<div className="px-3 py-2">
|
|
<p className="text-sm font-medium">Admin User</p>
|
|
<p className="text-xs text-muted-foreground">admin@encoach.com</p>
|
|
</div>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => navigate("/profile")}>
|
|
<User className="mr-2 h-4 w-4" /> Profile
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => navigate("/settings")}>
|
|
<Settings className="mr-2 h-4 w-4" /> Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => navigate("/login")}>
|
|
<LogOut className="mr-2 h-4 w-4" /> Logout
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1 overflow-auto p-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<AiAssistantDrawer />
|
|
{/* Floating help button */}
|
|
<Link
|
|
to="/tickets"
|
|
className="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full bg-primary px-4 py-2.5 text-primary-foreground shadow-lg hover:opacity-90 transition-opacity"
|
|
>
|
|
<HelpCircle className="h-4 w-4" />
|
|
<span className="text-sm font-medium">Need help?</span>
|
|
</Link>
|
|
</SidebarProvider>
|
|
);
|
|
}
|