mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 05:25:37 +07:00
287 lines
16 KiB
TypeScript
287 lines
16 KiB
TypeScript
import { Link, usePage } from '@inertiajs/react';
|
|
import { PropsWithChildren, ReactNode } from 'react';
|
|
import {
|
|
Menu,
|
|
LogOut,
|
|
User as UserIcon,
|
|
LayoutDashboard,
|
|
BookOpen,
|
|
Calendar,
|
|
Settings,
|
|
Users,
|
|
Search,
|
|
Brain
|
|
} from 'lucide-react';
|
|
import { Sheet, SheetContent, SheetTrigger } from '@/Components/ui/sheet';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/Components/ui/dropdown-menu';
|
|
import { Input } from '@/Components/ui/input';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/Components/ui/avatar';
|
|
import { cn } from '@/lib/utils';
|
|
import { ScrollArea } from '@/Components/ui/scroll-area';
|
|
import { ModeToggle } from '@/Components/ModeToggle';
|
|
import { useState, useEffect } from 'react';
|
|
import { ChevronLeft, ChevronRight, Trophy as TrophyIcon } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
export default function Authenticated({
|
|
header,
|
|
children,
|
|
}: PropsWithChildren<{ header?: ReactNode }>) {
|
|
const user = usePage().props.auth.user;
|
|
const currentRoute = route().current() as string;
|
|
|
|
const [isCollapsed, setIsCollapsed] = useState(() => {
|
|
if (typeof window !== 'undefined') {
|
|
return localStorage.getItem('sidebar-collapsed') === 'true';
|
|
}
|
|
return false;
|
|
});
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('sidebar-collapsed', String(isCollapsed));
|
|
}, [isCollapsed]);
|
|
|
|
const navItems = [
|
|
{ label: 'Dashboard', icon: LayoutDashboard, href: route('dashboard'), active: route().current('dashboard') },
|
|
{ label: 'Galeri Kursus', icon: BookOpen, href: route('courses.index'), active: route().current('courses.index') },
|
|
{ label: 'SRS Practice', icon: Brain, href: route('srs.index'), active: route().current('srs.*') },
|
|
{ label: 'Jadwal Belajar', icon: Calendar, href: '#', active: false },
|
|
{ label: 'Komunitas', icon: Users, href: '#', active: false },
|
|
{ label: 'Pengaturan', icon: Settings, href: route('profile.edit'), active: route().current('profile.edit') },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background font-sans selection:bg-primary selection:text-primary-foreground">
|
|
|
|
{/* Desktop Sidebar Island */}
|
|
<aside className={cn(
|
|
"hidden lg:flex flex-col fixed left-4 top-4 bottom-4 z-50 bg-card/40 backdrop-blur-2xl border border-border/40 transition-all duration-500 ease-in-out shadow-2xl overflow-hidden",
|
|
isCollapsed ? "w-20 rounded-[2rem]" : "w-64 rounded-[2.5rem]"
|
|
)}>
|
|
{/* Collapse Toggle */}
|
|
<button
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className="absolute -right-0 top-20 bg-primary text-white p-1 rounded-l-md shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-50 hover:bg-primary/90 hidden"
|
|
>
|
|
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
|
|
</button>
|
|
|
|
<div className={cn("h-16 flex items-center transition-all duration-500", isCollapsed ? "justify-center px-0" : "px-6")}>
|
|
<Link href="/" className="flex items-center gap-2 group">
|
|
{isCollapsed ? (
|
|
<>
|
|
<img src="/brand/Nihonbuzz-Academy-Light.png" alt="NB" className="h-10 w-10 dark:hidden transition-transform group-hover:rotate-12" />
|
|
<img src="/brand/Nihonbuzz-Academy-Dark.png" alt="NB" className="h-10 w-10 hidden dark:block transition-transform group-hover:rotate-12" />
|
|
</>
|
|
) : (
|
|
<div className="flex items-center gap-3">
|
|
<img src="/brand/Nihonbuzz-Academy-Light.png" alt="NB" className="h-10 w-10 dark:hidden" />
|
|
<img src="/brand/Nihonbuzz-Academy-Dark.png" alt="NB" className="h-10 w-10 hidden dark:block" />
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-black tracking-tighter leading-none dark:text-white text-gray-900">NIHONBUZZ</span>
|
|
<span className="text-[10px] font-bold tracking-[0.2em] text-primary leading-none mt-1">ACADEMY</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1 py-6 px-3">
|
|
<nav className="space-y-2">
|
|
{navItems.map((item, index) => (
|
|
<Link
|
|
key={index}
|
|
href={item.href}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-3 rounded-2xl text-sm font-medium transition-all duration-300 group relative overflow-hidden",
|
|
item.active
|
|
? "bg-primary/10 text-primary shadow-sm"
|
|
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
|
|
isCollapsed && "justify-center px-0"
|
|
)}
|
|
>
|
|
<item.icon size={20} className={cn(
|
|
"transition-all duration-300",
|
|
item.active ? "text-primary scale-110" : "text-muted-foreground group-hover:text-foreground group-hover:scale-110"
|
|
)} />
|
|
{!isCollapsed && <span className="truncate">{item.label}</span>}
|
|
{item.active && !isCollapsed && (
|
|
<motion.div layoutId="activeNav" className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r-full" />
|
|
)}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
{!isCollapsed && (
|
|
<div className="mt-8 px-3 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<h4 className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/50 mb-3 px-2">Shortcut</h4>
|
|
<div className="space-y-2">
|
|
<div className="p-4 rounded-[1.5rem] bg-gradient-to-br from-nihonbuzz-red to-orange-500 text-white shadow-xl shadow-nihonbuzz-red/20 relative overflow-hidden group hover:shadow-nihonbuzz-red/30 transition-all hover:-translate-y-0.5">
|
|
<div className="absolute -top-2 -right-2 p-2 opacity-10 group-hover:opacity-20 transition-opacity rotate-12">
|
|
<TrophyIcon size={64} />
|
|
</div>
|
|
<p className="text-[10px] font-bold opacity-80 mb-0.5">Current Level</p>
|
|
<p className="text-xl font-black">N5 Basic</p>
|
|
<Button size="sm" variant="secondary" className="w-full mt-3 bg-white/20 hover:bg-white/30 text-white border-none h-7 text-[10px] font-bold uppercase tracking-wider backdrop-blur-md">
|
|
Sertifikat
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
|
|
<div className="p-3 border-t border-border/20">
|
|
<button
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className={cn(
|
|
"flex items-center gap-3 w-full px-3 py-2.5 text-xs font-bold text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-xl transition-all",
|
|
isCollapsed && "justify-center"
|
|
)}
|
|
>
|
|
{isCollapsed ? <ChevronRight size={18} /> : <><ChevronLeft size={18} /> <span>Sembunyikan</span></>}
|
|
</button>
|
|
<Link href={route('logout')} method="post" as="button" className={cn("flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-xl transition-colors mt-1", isCollapsed && "justify-center")}>
|
|
<LogOut size={18} />
|
|
{!isCollapsed && "Keluar"}
|
|
</Link>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content Wrapper */}
|
|
<div className={cn(
|
|
"min-h-screen flex flex-col transition-all duration-500 ease-in-out",
|
|
isCollapsed ? "lg:pl-24" : "lg:pl-72"
|
|
)}>
|
|
|
|
{/* Island Topbar Pill */}
|
|
<header className="sticky top-4 z-40 h-16 flex items-center justify-between px-4 sm:px-6 mx-4 rounded-[1.25rem] bg-background/40 backdrop-blur-2xl border border-border/40 shadow-xl shadow-black/5 transition-all duration-500">
|
|
<div className="flex items-center gap-4 lg:hidden">
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="rounded-lg -ml-2">
|
|
<Menu size={20} className="text-muted-foreground" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-[280px] p-0 border-r-0 bg-card">
|
|
<div className="h-16 flex items-center px-6 border-b border-border">
|
|
<img
|
|
src="/brand/Nihonbuzz-Academy-Light-LS-Regular.png"
|
|
alt="Nihonbuzz Academy"
|
|
className="h-6 w-auto dark:hidden"
|
|
/>
|
|
<img
|
|
src="/brand/Nihonbuzz-Academy-Dark-LS-Regular.png"
|
|
alt="Nihonbuzz Academy"
|
|
className="h-6 w-auto hidden dark:block"
|
|
/>
|
|
</div>
|
|
<div className="p-4 space-y-1">
|
|
{navItems.map((item, index) => (
|
|
<Link
|
|
key={index}
|
|
href={item.href}
|
|
className={cn(
|
|
"flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors",
|
|
item.active
|
|
? "bg-primary/10 text-primary"
|
|
: "text-muted-foreground hover:bg-muted"
|
|
)}
|
|
>
|
|
<item.icon size={18} />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
<Link href="/" className="lg:hidden">
|
|
<img src="/brand/logo-symbol.svg" alt="Logo" className="h-8 w-8" />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Search Bar (Desktop) */}
|
|
<div className="hidden md:flex items-center max-w-sm w-full relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
<Input
|
|
placeholder="Cari materi..."
|
|
className="pl-10 h-9 rounded-full border-border/50 bg-muted/30 focus:bg-background transition-all w-full text-sm placeholder:text-muted-foreground/50"
|
|
/>
|
|
</div>
|
|
|
|
{/* Right Profile Menu */}
|
|
<div className="flex items-center gap-3">
|
|
<ModeToggle />
|
|
|
|
<DropdownMenu modal={false}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="relative h-9 w-9 rounded-full ring-2 ring-transparent hover:ring-primary/20 transition-all p-0 overflow-hidden">
|
|
<Avatar className="h-9 w-9">
|
|
<AvatarImage src={user.avatar ?? ''} alt={user.name} />
|
|
<AvatarFallback className="bg-gradient-to-br from-nihonbuzz-red to-orange-500 text-white font-bold text-xs">
|
|
{user.name.charAt(0)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56 rounded-xl p-1" align="end" forceMount>
|
|
<div className="px-2 py-1.5 text-sm font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-bold leading-none">{user.name}</p>
|
|
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
<DropdownMenuSeparator className="my-1 bg-border/50" />
|
|
<DropdownMenuItem asChild>
|
|
<Link href={route('profile.edit')} className="cursor-pointer rounded-lg font-medium text-foreground focus:text-primary focus:bg-primary/5">
|
|
<UserIcon className="mr-2 h-4 w-4" />
|
|
<span>Profile</span>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href={route('logout')} method="post" as="button" className="cursor-pointer w-full rounded-lg font-medium text-destructive focus:text-destructive focus:bg-destructive/10">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>Keluar</span>
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 p-4 sm:p-6 lg:p-8">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Helper component for Trophy icon which was missing in imports
|
|
function Trophy({ size = 24, ...props }: { size?: number, [key: string]: any }) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
{...props}
|
|
>
|
|
<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6" />
|
|
<path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18" />
|
|
<path d="M4 22h16" />
|
|
<path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" />
|
|
<path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" />
|
|
<path d="M18 2H6v7a6 6 0 0 0 12 0V2Z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|