Files
nihonbuzz-academy/resources/js/Layouts/CourseLayout.tsx

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>
);
}