mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-27 02:41:58 +07:00
262 lines
13 KiB
TypeScript
262 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Link, usePage } from '@inertiajs/react';
|
|
import {
|
|
ChevronLeft,
|
|
Menu,
|
|
CheckCircle,
|
|
Circle,
|
|
PlayCircle,
|
|
FileText,
|
|
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 { cn } from '@/lib/utils';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/Components/ui/avatar';
|
|
import { ModeToggle } from '@/Components/ModeToggle';
|
|
|
|
interface Lesson {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
type: 'video' | 'text' | 'quiz' | 'pdf';
|
|
is_completed: boolean;
|
|
duration_seconds: number;
|
|
}
|
|
|
|
interface Module {
|
|
id: string;
|
|
title: string;
|
|
lessons: Lesson[];
|
|
}
|
|
|
|
interface Course {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
progress_percentage: number;
|
|
}
|
|
|
|
interface CourseLayoutProps {
|
|
children: React.ReactNode;
|
|
course: Course;
|
|
modules: Module[];
|
|
currentLesson?: Lesson;
|
|
nextLesson?: Lesson | null;
|
|
previousLesson?: Lesson | null;
|
|
}
|
|
|
|
export default function CourseLayout({
|
|
children,
|
|
course,
|
|
modules = [],
|
|
currentLesson,
|
|
nextLesson,
|
|
previousLesson
|
|
}: CourseLayoutProps) {
|
|
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)
|
|
: [...prev, moduleId]
|
|
);
|
|
};
|
|
|
|
const getLessonIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'video': return <PlayCircle size={18} />;
|
|
case 'quiz': return <HelpCircle size={18} />;
|
|
case 'pdf': return <FileText size={18} />;
|
|
default: return <FileText size={18} />;
|
|
}
|
|
};
|
|
|
|
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>
|
|
|
|
<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="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"
|
|
>
|
|
<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")}
|
|
/>
|
|
</button>
|
|
|
|
{openModules.includes(module.id) && (
|
|
<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(
|
|
"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"
|
|
)}
|
|
>
|
|
{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={18} className="text-green-500" />
|
|
) : isActive ? (
|
|
<PlayCircle size={18} />
|
|
) : (
|
|
<Circle size={18} />
|
|
)}
|
|
</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>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<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>
|
|
);
|
|
}
|