Module Update
Some checks failed
CI / build-and-test (push) Failing after 1m31s
CI / deploy (push) Has been skipped

This commit is contained in:
SpecialX
2025-12-30 14:42:30 +08:00
parent f1797265b2
commit e7c902e8e1
148 changed files with 19317 additions and 113 deletions

View File

@@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { ChevronRight } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/shared/components/ui/collapsible"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/shared/components/ui/tooltip"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { cn } from "@/shared/lib/utils"
import { useSidebar } from "./sidebar-provider"
import { NAV_CONFIG, Role } from "../config/navigation"
interface AppSidebarProps {
mode?: "mobile" | "desktop"
}
export function AppSidebar({ mode }: AppSidebarProps) {
const { expanded, toggleSidebar, isMobile } = useSidebar()
const pathname = usePathname()
// MOCK ROLE: In real app, get this from auth context / session
const [currentRole, setCurrentRole] = React.useState<Role>("admin")
const navItems = NAV_CONFIG[currentRole]
// Ensure consistent state for hydration
if (!expanded && mode === 'mobile') return null
return (
<div className="flex h-full flex-col gap-4">
{/* Sidebar Header */}
<div className={cn("flex h-16 items-center border-b px-4 transition-all duration-300", !expanded && !isMobile ? "justify-center px-2" : "justify-between")}>
{expanded || isMobile ? (
<Link href="/" className="flex items-center gap-2 font-bold">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-lg">
NE
</div>
<span className="truncate text-lg">Next_Edu</span>
</Link>
) : (
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-lg">
NE
</div>
)}
</div>
{/* Role Switcher (Dev Only - for Demo) */}
{(expanded || isMobile) && (
<div className="px-4">
<label className="text-muted-foreground mb-2 block text-xs font-medium uppercase">
View As (Dev Mode)
</label>
<Select value={currentRole} onValueChange={(v) => setCurrentRole(v as Role)}>
<SelectTrigger>
<SelectValue placeholder="Select Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="teacher">Teacher</SelectItem>
<SelectItem value="student">Student</SelectItem>
<SelectItem value="parent">Parent</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Navigation */}
<ScrollArea className="flex-1 px-3">
<nav className="flex flex-col gap-2 py-4">
<TooltipProvider delayDuration={0}>
{navItems.map((item, index) => {
const isActive = pathname.startsWith(item.href)
const hasChildren = item.items && item.items.length > 0
if (!expanded && !isMobile) {
// Collapsed Mode (Icon Only + Tooltip)
return (
<Tooltip key={index}>
<TooltipTrigger asChild>
<Link
href={item.href}
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex size-10 items-center justify-center rounded-md transition-colors",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
>
<item.icon className="size-5" />
<span className="sr-only">{item.title}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">{item.title}</TooltipContent>
</Tooltip>
)
}
// Expanded Mode
if (hasChildren) {
return (
<Collapsible key={index} defaultOpen={isActive} className="group/collapsible">
<CollapsibleTrigger asChild>
<button
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex w-full items-center justify-between rounded-md p-2 text-sm font-medium transition-colors",
isActive && "text-sidebar-accent-foreground"
)}
>
<div className="flex items-center gap-2">
<item.icon className="size-4" />
<span>{item.title}</span>
</div>
<ChevronRight className="text-muted-foreground size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</button>
</CollapsibleTrigger>
<CollapsibleContent className="data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up overflow-hidden">
<div className="ml-6 mt-1 flex flex-col gap-1 border-l pl-2">
{item.items?.map((subItem, subIndex) => (
<Link
key={subIndex}
href={subItem.href}
className={cn(
"text-muted-foreground hover:text-foreground block rounded-md px-2 py-1 text-sm transition-colors",
pathname === subItem.href && "text-foreground font-medium"
)}
>
{subItem.title}
</Link>
))}
</div>
</CollapsibleContent>
</Collapsible>
)
}
return (
<Link
key={index}
href={item.href}
className={cn(
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex items-center gap-2 rounded-md p-2 text-sm font-medium transition-colors",
isActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
>
<item.icon className="size-4" />
<span>{item.title}</span>
</Link>
)
})}
</TooltipProvider>
</nav>
</ScrollArea>
{/* Sidebar Footer */}
<div className="p-4">
{!isMobile && (
<button
onClick={toggleSidebar}
className="hover:bg-sidebar-accent text-sidebar-foreground flex w-full items-center justify-center rounded-md border p-2 text-sm transition-colors"
>
{expanded ? "Collapse" : <ChevronRight className="size-4" />}
</button>
)}
</div>
</div>
)
}
AppSidebar.displayName = "AppSidebar"

View File

@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import { Menu } from "lucide-react"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/shared/components/ui/sheet"
import { cn } from "@/shared/lib/utils"
type SidebarContextType = {
expanded: boolean
setExpanded: (expanded: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextType | undefined>(
undefined
)
export function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider")
}
return context
}
interface SidebarProviderProps {
children: React.ReactNode
sidebar: React.ReactNode
}
export function SidebarProvider({ children, sidebar }: SidebarProviderProps) {
const [expanded, setExpanded] = React.useState(true)
const [isMobile, setIsMobile] = React.useState(false)
const [openMobile, setOpenMobile] = React.useState(false)
React.useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 768
setIsMobile(mobile)
if (mobile) {
setExpanded(true)
}
}
checkMobile()
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
const toggleSidebar = () => {
if (isMobile) {
setOpenMobile(!openMobile)
} else {
setExpanded(!expanded)
}
}
return (
<SidebarContext.Provider
value={{ expanded, setExpanded, isMobile, toggleSidebar }}
>
<div className="flex min-h-screen flex-col md:flex-row bg-background">
{/* Mobile Trigger & Sheet */}
{isMobile && (
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent side="left" className="w-[80%] p-0 sm:w-[300px]">
<SheetHeader className="sr-only">
<SheetTitle>Navigation Menu</SheetTitle>
</SheetHeader>
<div className="h-full py-4">
{sidebar}
</div>
</SheetContent>
</Sheet>
)}
{/* Desktop Sidebar Wrapper */}
{!isMobile && (
<aside
className={cn(
"bg-sidebar border-sidebar-border text-sidebar-foreground sticky top-0 hidden h-screen flex-col border-r transition-[width] duration-300 ease-in-out md:flex",
expanded ? "w-64" : "w-16"
)}
>
{sidebar}
</aside>
)}
{/* Main Content Wrapper - Right Side */}
<div className="flex-1 flex flex-col min-w-0 transition-[margin] duration-300 ease-in-out h-screen overflow-hidden">
{children}
</div>
</div>
</SidebarContext.Provider>
)
}

View File

@@ -0,0 +1,105 @@
"use client"
import * as React from "react"
import { Bell, Menu, Search } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Separator } from "@/shared/components/ui/separator"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/shared/components/ui/breadcrumb"
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { useSidebar } from "./sidebar-provider"
export function SiteHeader() {
const { toggleSidebar, isMobile } = useSidebar()
return (
<header className="bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-16 items-center border-b px-4 backdrop-blur-sm">
<div className="flex flex-1 items-center gap-4">
{/* Mobile Toggle */}
{isMobile && (
<Button variant="ghost" size="icon" onClick={toggleSidebar} className="mr-2">
<Menu className="size-5" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)}
<Separator orientation="vertical" className="mr-2 hidden h-6 md:block" />
{/* Breadcrumbs */}
<Breadcrumb className="hidden md:flex">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Overview</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex items-center gap-4">
{/* Global Search */}
<div className="relative hidden md:block">
<Search className="text-muted-foreground absolute top-2.5 left-2.5 size-4" />
<Input
type="search"
placeholder="Search... (Cmd+K)"
className="w-[200px] pl-9 lg:w-[300px]"
/>
</div>
{/* Notifications */}
<Button variant="ghost" size="icon" className="text-muted-foreground">
<Bell className="size-5" />
<span className="sr-only">Notifications</span>
</Button>
{/* User Nav */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative size-8 rounded-full">
<Avatar className="size-8">
<AvatarImage src="/avatars/01.png" alt="@user" />
<AvatarFallback>AD</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">Admin User</p>
<p className="text-muted-foreground text-xs leading-none">admin@nextedu.com</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:bg-destructive/10">
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
)
}

View File

@@ -0,0 +1,176 @@
import {
BarChart,
BookOpen,
Calendar,
GraduationCap,
LayoutDashboard,
Settings,
Users,
FileText,
MessageSquare,
Shield,
CreditCard,
FileQuestion,
ClipboardList,
Library,
PenTool
} from "lucide-react"
export type NavItem = {
title: string
icon: any
href: string
items?: { title: string; href: string }[]
}
export type Role = "admin" | "teacher" | "student" | "parent"
export const NAV_CONFIG: Record<Role, NavItem[]> = {
admin: [
{
title: "Dashboard",
icon: LayoutDashboard,
href: "/admin/dashboard",
},
{
title: "School Management",
icon: Shield,
href: "/admin/school",
items: [
{ title: "Departments", href: "/admin/school/departments" },
{ title: "Classrooms", href: "/admin/school/classrooms" },
{ title: "Academic Year", href: "/admin/school/academic-year" },
]
},
{
title: "Users",
icon: Users,
href: "/admin/users",
items: [
{ title: "Teachers", href: "/admin/users/teachers" },
{ title: "Students", href: "/admin/users/students" },
{ title: "Parents", href: "/admin/users/parents" },
{ title: "Staff", href: "/admin/users/staff" },
]
},
{
title: "Courses",
icon: BookOpen,
href: "/courses",
items: [
{ title: "Course Catalog", href: "/courses/catalog" },
{ title: "Schedules", href: "/courses/schedules" },
]
},
{
title: "Reports",
icon: BarChart,
href: "/reports",
},
{
title: "Finance",
icon: CreditCard,
href: "/finance",
},
{
title: "Settings",
icon: Settings,
href: "/settings",
},
],
teacher: [
{
title: "Dashboard",
icon: LayoutDashboard,
href: "/dashboard",
},
{
title: "Textbooks",
icon: Library,
href: "/teacher/textbooks",
},
{
title: "Exams",
icon: FileQuestion,
href: "/teacher/exams",
items: [
{ title: "All Exams", href: "/teacher/exams/all" },
{ title: "Create Exam", href: "/teacher/exams/create" },
{ title: "Grading", href: "/teacher/exams/grading" },
]
},
{
title: "Homework",
icon: PenTool,
href: "/teacher/homework",
items: [
{ title: "Assignments", href: "/teacher/homework/assignments" },
{ title: "Submissions", href: "/teacher/homework/submissions" },
]
},
{
title: "Question Bank",
icon: ClipboardList,
href: "/teacher/questions",
},
{
title: "Class Management",
icon: Users,
href: "/teacher/classes",
items: [
{ title: "My Classes", href: "/teacher/classes/my" },
{ title: "Students", href: "/teacher/classes/students" },
{ title: "Schedule", href: "/teacher/classes/schedule" },
]
},
],
student: [
{
title: "Dashboard",
icon: LayoutDashboard,
href: "/dashboard",
},
{
title: "My Learning",
icon: BookOpen,
href: "/student/learning",
items: [
{ title: "Courses", href: "/student/learning/courses" },
{ title: "Assignments", href: "/student/learning/assignments" },
{ title: "Grades", href: "/student/learning/grades" },
]
},
{
title: "Schedule",
icon: Calendar,
href: "/student/schedule",
},
{
title: "Resources",
icon: FileText,
href: "/student/resources",
},
],
parent: [
{
title: "Dashboard",
icon: LayoutDashboard,
href: "/parent/dashboard",
},
{
title: "Children",
icon: Users,
href: "/parent/children",
},
{
title: "Tuition",
icon: CreditCard,
href: "/parent/tuition",
},
{
title: "Messages",
icon: MessageSquare,
href: "/messages",
},
]
}