mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 13:32:07 +07:00
feat: Implement a new course learning system with dedicated layouts, lesson playback, and Spaced Repetition System (SRS) functionality.
This commit is contained in:
@@ -1,286 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, Head, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
Menu,
|
||||
Search,
|
||||
CheckCircle,
|
||||
Circle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
import React, { useState } from 'react';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
Menu,
|
||||
CheckCircle,
|
||||
Circle,
|
||||
PlayCircle,
|
||||
FileText,
|
||||
HelpCircle
|
||||
HelpCircle,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Download,
|
||||
Lock,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
X,
|
||||
LayoutDashboard
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { ScrollArea } from '@/Components/ui/scroll-area';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/Components/ui/sheet';
|
||||
import { Progress } from '@/Components/ui/progress';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/Components/ui/avatar';
|
||||
import { ModeToggle } from '@/Components/ModeToggle';
|
||||
|
||||
// Interfaces for Props
|
||||
interface Lesson {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -48,240 +51,95 @@ interface CourseLayoutProps {
|
||||
course: Course;
|
||||
modules: Module[];
|
||||
currentLesson?: Lesson;
|
||||
nextLesson?: Lesson | null;
|
||||
previousLesson?: Lesson | null;
|
||||
}
|
||||
|
||||
export default function CourseLayout({
|
||||
children,
|
||||
course,
|
||||
modules = [],
|
||||
currentLesson
|
||||
currentLesson,
|
||||
nextLesson,
|
||||
previousLesson
|
||||
}: CourseLayoutProps) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [openModules, setOpenModules] = useState<string[]>([]);
|
||||
const { url } = usePage();
|
||||
|
||||
// Default open all modules or just the active one
|
||||
useEffect(() => {
|
||||
if (modules.length > 0) {
|
||||
// Find module containing current lesson
|
||||
const activeModule = modules.find(m => m.lessons.some(l => l.slug === currentLesson?.slug));
|
||||
if (activeModule) {
|
||||
setOpenModules(prev => [...new Set([...prev, activeModule.id])]);
|
||||
} else {
|
||||
// If no active lesson (e.g. course home), open first module
|
||||
setOpenModules([modules[0].id]);
|
||||
}
|
||||
}
|
||||
}, [modules, currentLesson]);
|
||||
const user = usePage().props.auth.user;
|
||||
const [openModules, setOpenModules] = useState<string[]>(modules.map(m => m.id)); // Default open all
|
||||
|
||||
const toggleModule = (moduleId: string) => {
|
||||
setOpenModules(prev =>
|
||||
prev.includes(moduleId)
|
||||
? prev.filter(id => id !== moduleId)
|
||||
setOpenModules(prev =>
|
||||
prev.includes(moduleId)
|
||||
? prev.filter(id => id !== moduleId)
|
||||
: [...prev, moduleId]
|
||||
);
|
||||
};
|
||||
|
||||
const getLessonIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'video': return <PlayCircle size={14} />;
|
||||
case 'quiz': return <HelpCircle size={14} />;
|
||||
case 'pdf': return <FileText size={14} />;
|
||||
default: return <FileText size={14} />;
|
||||
case 'video': return <PlayCircle size={18} />;
|
||||
case 'quiz': return <HelpCircle size={18} />;
|
||||
case 'pdf': return <FileText size={18} />;
|
||||
default: return <FileText size={18} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex font-sans text-foreground">
|
||||
{/* Mobile Sheet Sidebar */}
|
||||
<Sheet>
|
||||
<div className="lg:hidden fixed top-0 left-0 right-0 h-14 border-b bg-background/95 backdrop-blur z-40 flex items-center px-4 justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="-ml-2">
|
||||
<Menu size={20} />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<span className="font-semibold text-sm truncate max-w-[200px]">{course.title}</span>
|
||||
</div>
|
||||
<Link href={route('dashboard')}>
|
||||
<Button variant="ghost" size="sm" className="text-xs">Keluar</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<SheetContent side="left" className="p-0 w-[85vw] max-w-[320px]">
|
||||
<SidebarContent
|
||||
course={course}
|
||||
modules={modules}
|
||||
currentLesson={currentLesson}
|
||||
openModules={openModules}
|
||||
toggleModule={toggleModule}
|
||||
getLessonIcon={getLessonIcon}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
"hidden lg:flex flex-col w-80 border-r bg-muted/30 fixed inset-y-0 left-0 z-30 transition-all duration-300",
|
||||
!isSidebarOpen && "-ml-80"
|
||||
)}
|
||||
>
|
||||
<SidebarContent
|
||||
course={course}
|
||||
modules={modules}
|
||||
currentLesson={currentLesson}
|
||||
openModules={openModules}
|
||||
toggleModule={toggleModule}
|
||||
getLessonIcon={getLessonIcon}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main
|
||||
className={cn(
|
||||
"flex-1 flex flex-col min-h-screen transition-all duration-300 bg-background",
|
||||
isSidebarOpen ? "lg:ml-80" : "lg:ml-0",
|
||||
"pt-14 lg:pt-0" // Mobile padding
|
||||
)}
|
||||
>
|
||||
{/* Desktop Topbar */}
|
||||
<header className="hidden lg:flex h-14 items-center justify-between px-6 border-b bg-background sticky top-0 z-20">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={isSidebarOpen ? "Close Sidebar" : "Open Sidebar"}
|
||||
>
|
||||
<Menu size={18} />
|
||||
</Button>
|
||||
<nav className="flex items-center text-sm text-muted-foreground">
|
||||
<Link href={route('dashboard')} className="hover:text-foreground transition-colors">
|
||||
Dashboard
|
||||
</Link>
|
||||
<ChevronRight size={14} className="mx-2" />
|
||||
<Link href={route('courses.index')} className="hover:text-foreground transition-colors">
|
||||
Courses
|
||||
</Link>
|
||||
<ChevronRight size={14} className="mx-2" />
|
||||
<span className="font-medium text-foreground truncate max-w-[300px]">
|
||||
{currentLesson?.title || course.title}
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-end mr-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Progress</span>
|
||||
<span className="text-xs font-bold">{course.progress_percentage}%</span>
|
||||
</div>
|
||||
<Progress value={course.progress_percentage} className="w-32 h-1.5" />
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Slot */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="max-w-4xl mx-auto p-6 lg:p-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ course, modules, currentLesson, openModules, toggleModule, getLessonIcon }: any) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b bg-background/50 backdrop-blur-sm">
|
||||
<Link
|
||||
href={route('dashboard')}
|
||||
className="flex items-center text-xs font-medium text-muted-foreground hover:text-primary mb-4 transition-colors group"
|
||||
>
|
||||
<ChevronLeft size={14} className="mr-1 group-hover:-translate-x-1 transition-transform" />
|
||||
Kembali ke Dashboard
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-nihonbuzz-red to-orange-500 flex items-center justify-center text-white font-bold text-sm shrink-0 shadow-lg shadow-orange-500/20">
|
||||
{course.title.charAt(0)}
|
||||
</div>
|
||||
<h2 className="font-bold leading-tight text-sm line-clamp-2">{course.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-3">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<input
|
||||
className="w-full h-8 pl-8 pr-3 rounded-md border border-input bg-background/50 text-xs focus:outline-none focus:ring-1 focus:ring-primary transition-all placeholder:text-muted-foreground"
|
||||
placeholder="Cari materi..."
|
||||
/>
|
||||
</div>
|
||||
const SidebarContent = () => (
|
||||
<div className="flex flex-col h-full bg-white dark:bg-[#0a0a0b] border-l border-gray-200 dark:border-white/10 transition-colors duration-300">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
|
||||
<h3 className="font-bold text-sm text-gray-900 dark:text-white uppercase tracking-widest">Course Content</h3>
|
||||
<span className="text-xs text-gray-500 dark:text-white/50">{course.progress_percentage}% Complete</span>
|
||||
</div>
|
||||
|
||||
{/* Curriculum List */}
|
||||
<ScrollArea className="flex-1 px-3 py-4">
|
||||
<div className="space-y-4">
|
||||
{modules.map((module: any, index: number) => (
|
||||
<div key={module.id} className="space-y-1">
|
||||
<button
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="flex flex-col">
|
||||
{modules.map((module) => (
|
||||
<div key={module.id} className="flex flex-col">
|
||||
<button
|
||||
onClick={() => toggleModule(module.id)}
|
||||
className="flex items-center w-full text-left gap-2 px-2 py-1.5 text-xs font-bold uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors group"
|
||||
className="px-4 py-3 bg-gray-50 dark:bg-[#161618]/50 flex items-center justify-between cursor-pointer border-b border-gray-200 dark:border-white/5 hover:bg-gray-100 dark:hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
className={cn(
|
||||
"transition-transform bg-muted rounded-sm",
|
||||
openModules.includes(module.id) && "rotate-90"
|
||||
)}
|
||||
<span className="text-xs font-bold text-gray-500 dark:text-white/60 uppercase tracking-tighter group-hover:text-gray-900 dark:group-hover:text-white transition-colors">{module.title}</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn("text-gray-400 dark:text-white/40 transition-transform", !openModules.includes(module.id) && "-rotate-90")}
|
||||
/>
|
||||
<span className="line-clamp-1 group-hover:underline decoration-border underline-offset-4">{module.title}</span>
|
||||
</button>
|
||||
|
||||
{openModules.includes(module.id) && (
|
||||
<div className="space-y-0.5 ml-1 pl-2 border-l border-border/40">
|
||||
{module.lessons.map((lesson: any) => {
|
||||
<div className="flex flex-col">
|
||||
{module.lessons.map((lesson) => {
|
||||
const isActive = currentLesson?.slug === lesson.slug;
|
||||
return (
|
||||
<Link
|
||||
key={lesson.id}
|
||||
href={route('courses.learn', { course: course.slug, lesson: lesson.slug })}
|
||||
className={cn(
|
||||
"flex items-start gap-2.5 px-3 py-2 rounded-md text-sm transition-all duration-200 group relative",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
|
||||
"group flex items-center gap-3 px-4 py-3 cursor-pointer transition-all border-l-2",
|
||||
isActive
|
||||
? "bg-[#FF4500]/10 border-[#FF4500] relative overflow-hidden"
|
||||
: "hover:bg-gray-100 dark:hover:bg-white/5 border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="mt-0.5 shrink-0 transition-colors group-hover:text-foreground">
|
||||
{isActive && <div className="absolute inset-0 bg-[#FF4500]/5 blur-xl pointer-events-none" />}
|
||||
|
||||
<div className={cn("text-[20px]", isActive ? "text-[#FF4500]" : "text-gray-300 dark:text-white/40")}>
|
||||
{lesson.is_completed ? (
|
||||
<CheckCircle size={14} className="text-green-500" />
|
||||
<CheckCircle size={18} className="text-green-500" />
|
||||
) : isActive ? (
|
||||
<PlayCircle size={18} />
|
||||
) : (
|
||||
<Circle size={14} className={cn("text-muted-foreground/30", isActive && "text-primary/30")} />
|
||||
<Circle size={18} />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<span className="line-clamp-2 leading-snug">{lesson.title}</span>
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground/50 font-medium">
|
||||
<span className="flex items-center gap-1">
|
||||
{getLessonIcon(lesson.type)}
|
||||
<span className="capitalize">{lesson.type}</span>
|
||||
</span>
|
||||
{lesson.duration_seconds > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{Math.ceil(lesson.duration_seconds / 60)} min</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 relative z-10">
|
||||
<p className={cn("text-sm font-semibold truncate", isActive ? "text-gray-900 dark:text-white" : "text-gray-500 dark:text-white/60 group-hover:text-gray-900 dark:group-hover:text-white/80")}>
|
||||
{lesson.title}
|
||||
</p>
|
||||
<p className={cn("text-[10px]", isActive ? "text-[#FF4500]/80" : "text-gray-400 dark:text-white/40")}>
|
||||
{isActive ? "Current Lesson" : `${Math.ceil(lesson.duration_seconds / 60)} min`}
|
||||
</p>
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-full bg-primary rounded-r-full" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -291,11 +149,113 @@ function SidebarContent({ course, modules, currentLesson, openModules, toggleMod
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Footer User Info */}
|
||||
<div className="p-4 border-t bg-background/50 text-[10px] text-center text-muted-foreground">
|
||||
Built for <span className="font-bold text-foreground">Future Japan Enthusisasts</span>
|
||||
|
||||
<div className="p-4 bg-gray-50 dark:bg-[#161618] border-t border-gray-200 dark:border-white/10">
|
||||
<Button variant="outline" className="w-full border-gray-200 dark:border-white/10 text-gray-500 dark:text-white/60 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5 text-xs h-9">
|
||||
<Download size={14} className="mr-2" />
|
||||
Lesson Materials (PDF)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-white dark:bg-[#0a0a0b] text-gray-900 dark:text-white font-sans selection:bg-[#FF4500]/30 transition-colors duration-300">
|
||||
{/* Top Navigation */}
|
||||
<header className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-white/10 px-6 bg-white dark:bg-[#0a0a0b] z-20 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-8 bg-[#FF4500] rounded flex items-center justify-center">
|
||||
<span className="font-bold text-white text-lg">N</span>
|
||||
</div>
|
||||
<h2 className="text-gray-900 dark:text-white text-lg font-extrabold tracking-tight hidden sm:block">
|
||||
Nihonbuzz <span className="font-light text-gray-400 dark:text-white/40">Academy</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="h-6 w-px bg-gray-200 dark:bg-white/10 mx-2 hidden sm:block"></div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-white/40 hidden md:flex">
|
||||
<span className="truncate max-w-[150px]">{course.title}</span>
|
||||
<ChevronRight size={14} />
|
||||
<span className="text-gray-900 dark:text-white font-medium truncate max-w-[200px]">{currentLesson?.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={route('dashboard')}>
|
||||
<Button variant="ghost" size="sm" className="text-gray-500 dark:text-white/60 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5 hidden sm:flex">
|
||||
<LayoutDashboard size={16} className="mr-2" />
|
||||
Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<ModeToggle />
|
||||
|
||||
<div className="flex items-center gap-2 p-1 bg-gray-50 dark:bg-[#161618] border border-gray-200 dark:border-white/10 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar Trigger */}
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="lg:hidden text-gray-500 dark:text-white/60">
|
||||
<Menu size={20} />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="p-0 w-80 bg-white dark:bg-[#0a0a0b] border-l border-gray-200 dark:border-white/10">
|
||||
<SidebarContent />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Layout Area */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
|
||||
{/* Center Content (Video/Text) */}
|
||||
<main className="flex-1 overflow-y-auto bg-[#f8f6f5] dark:bg-[#0a0a0b] relative scroll-smooth pb-20">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Right Sidebar (Desktop) */}
|
||||
<aside className="hidden lg:flex w-80 flex-col border-l border-gray-200 dark:border-white/10 bg-white dark:bg-[#0a0a0b]">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation Bar */}
|
||||
<footer className="fixed bottom-0 left-0 right-0 h-16 bg-white/80 dark:bg-[#0a0a0b]/80 backdrop-blur-md border-t border-gray-200 dark:border-white/10 flex items-center justify-between px-6 z-30">
|
||||
<div className="flex items-center gap-4">
|
||||
{previousLesson && (
|
||||
<Link href={route('courses.learn', { course: course.slug, lesson: previousLesson.slug })}>
|
||||
<Button variant="ghost" className="text-gray-500 dark:text-white/60 hover:text-gray-900 dark:hover:text-white gap-2 pl-0 hover:bg-transparent group">
|
||||
<ArrowLeft size={18} className="group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="hidden sm:inline">Previous Lesson</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{nextLesson && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-end hidden md:flex">
|
||||
<span className="text-[10px] text-gray-400 dark:text-white/40 uppercase font-bold tracking-widest">Next Up</span>
|
||||
<span className="text-xs text-gray-900 dark:text-white font-medium truncate max-w-[200px]">{nextLesson.title}</span>
|
||||
</div>
|
||||
<Link href={route('courses.learn', { course: course.slug, lesson: nextLesson.slug })}>
|
||||
<Button className="bg-[#FF4500] hover:bg-[#FF4500]/90 text-white font-bold shadow-[0_0_15px_rgba(255,68,0,0.3)] transition-all active:scale-95 gap-2">
|
||||
Next Lesson
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
169
resources/js/Layouts/DashboardLayout.tsx
Normal file
169
resources/js/Layouts/DashboardLayout.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
BookOpen,
|
||||
Layers,
|
||||
Trophy,
|
||||
User,
|
||||
Settings,
|
||||
LogOut,
|
||||
Menu,
|
||||
Search,
|
||||
Bell,
|
||||
Plus,
|
||||
Play
|
||||
} from 'lucide-react';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/Components/ui/sheet';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/Components/ui/avatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollArea } from '@/Components/ui/scroll-area';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/Components/ui/dropdown-menu';
|
||||
import { ModeToggle } from '@/Components/ModeToggle';
|
||||
|
||||
export default function DashboardLayout({
|
||||
header,
|
||||
children,
|
||||
}: PropsWithChildren<{ header?: ReactNode }>) {
|
||||
const user = usePage().props.auth.user;
|
||||
const { url } = usePage();
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: LayoutDashboard, href: route('dashboard'), active: route().current('dashboard') },
|
||||
{ label: 'Study (SRS)', icon: BookOpen, href: route('srs.index'), active: route().current('srs.*') },
|
||||
{ label: 'Decks', icon: Layers, href: route('courses.index'), active: route().current('courses.index') },
|
||||
{ label: 'Leaderboard', icon: Trophy, href: '#', active: false },
|
||||
{ label: 'Profile', icon: User, href: route('profile.edit'), active: route().current('profile.edit') },
|
||||
];
|
||||
|
||||
const SidebarContent = () => (
|
||||
<div className="flex flex-col h-full bg-white dark:bg-[#0a0a0b] border-r border-gray-200 dark:border-white/5 p-6 transition-colors duration-300">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="size-10 bg-[#FF4500] rounded flex items-center justify-center text-white font-bold text-xl shadow-[0_0_15px_rgba(255,68,0,0.3)]">
|
||||
N
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-gray-900 dark:text-white text-base font-bold leading-none tracking-tight">NIHONBUZZ</h1>
|
||||
<p className="text-[#FF4500] text-[10px] font-bold tracking-[0.2em] uppercase mt-0.5">Academy</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav Links */}
|
||||
<nav className="space-y-2 flex-1">
|
||||
{navItems.map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200",
|
||||
item.active
|
||||
? "bg-[#FF4500]/10 text-[#FF4500] border border-[#FF4500]/20"
|
||||
: "text-gray-500 dark:text-white/60 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<item.icon size={20} className={cn(item.active ? "text-[#FF4500]" : "text-gray-400 dark:text-white/60")} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Profile Section (Bottom) */}
|
||||
<div className="space-y-4 pt-6 border-t border-gray-200 dark:border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10 border border-gray-200 dark:border-white/10 rounded">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback className="bg-[#FF4500] text-white">
|
||||
{user.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-bold text-gray-900 dark:text-white truncate">{user.name}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-white/40 truncate">Level 12 Scholar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mini Progress Bar */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between text-[10px] font-bold uppercase tracking-wider text-gray-500 dark:text-white/40">
|
||||
<span>XP Progress</span>
|
||||
<span>85%</span>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-gray-100 dark:bg-white/5 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#FF4500]" style={{ width: '85%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={route('profile.edit')}
|
||||
className="flex items-center gap-2 text-gray-500 dark:text-white/40 hover:text-gray-900 dark:hover:text-white text-xs font-medium transition-colors cursor-pointer w-full"
|
||||
>
|
||||
<Settings size={14} />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f8f6f5] dark:bg-[#0a0a0b] font-sans selection:bg-[#FF4500]/30 flex transition-colors duration-300">
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
<aside className="hidden lg:block w-64 flex-shrink-0 fixed inset-y-0 left-0 z-50">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
{/* Mobile Header / Sheet */}
|
||||
<header className="lg:hidden fixed top-0 left-0 right-0 h-16 bg-white/80 dark:bg-[#0a0a0b]/80 backdrop-blur-md border-b border-gray-200 dark:border-white/5 flex items-center justify-between px-4 z-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="text-gray-900 dark:text-white">
|
||||
<Menu size={20} />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 border-r-0 bg-white dark:bg-[#0a0a0b] w-64">
|
||||
<SidebarContent />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="font-bold text-gray-900 dark:text-white tracking-tight">NIHONBUZZ</div>
|
||||
</div>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar} />
|
||||
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 lg:pl-64 flex flex-col min-h-screen pt-16 lg:pt-0 transition-all">
|
||||
|
||||
{/* Desktop Topbar */}
|
||||
<header className="hidden lg:flex h-16 border-b border-gray-200 dark:border-white/5 items-center justify-between px-8 bg-white/50 dark:bg-[#0a0a0b]/50 backdrop-blur-md sticky top-0 z-40">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-white/40">
|
||||
<span>Dashboard</span>
|
||||
<span className="text-gray-300 dark:text-white/20">/</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">Home</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button className="bg-[#FF4500] hover:bg-[#FF4500]/90 text-white font-bold h-9 rounded shadow-[0_0_15px_rgba(255,68,0,0.3)] transition-all">
|
||||
<Plus size={16} className="mr-2" />
|
||||
Quick Add
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-gray-200 dark:border-white/10 bg-transparent text-gray-500 dark:text-white/60 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-white/5">
|
||||
<Bell size={20} />
|
||||
</Button>
|
||||
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto w-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user