mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 05:25:37 +07:00
219 lines
9.9 KiB
TypeScript
219 lines
9.9 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { Head, Link } from '@inertiajs/react';
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
CheckCircle,
|
|
FileText,
|
|
Download,
|
|
Maximize2
|
|
} from 'lucide-react';
|
|
import { Plyr } from 'plyr-react';
|
|
import 'plyr-react/plyr.css';
|
|
import CourseLayout from '@/Layouts/CourseLayout';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { Separator } from '@/Components/ui/separator';
|
|
import { Badge } from '@/Components/ui/badge';
|
|
|
|
// Interfaces (Should ideally be shared/exported)
|
|
interface Lesson {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
type: 'video' | 'text' | 'quiz' | 'pdf';
|
|
content?: string;
|
|
video_url?: string;
|
|
content_pdf?: string; // URL to PDF
|
|
is_completed: boolean;
|
|
duration_seconds: number;
|
|
next_lesson_slug?: string;
|
|
prev_lesson_slug?: string;
|
|
attachments?: Array<{ id: string, name: string, url: string, size: string }>;
|
|
}
|
|
|
|
interface PageProps {
|
|
course: any;
|
|
modules: any[];
|
|
lesson: Lesson;
|
|
}
|
|
|
|
// Helper to get YouTube video ID (Robust)
|
|
function getYouTubeId(url: string | null): string | null {
|
|
if (!url) return null;
|
|
try {
|
|
const urlObj = new URL(url.trim());
|
|
let id = null;
|
|
if (urlObj.hostname.includes('youtube.com')) {
|
|
id = urlObj.searchParams.get('v');
|
|
} else if (urlObj.hostname.includes('youtu.be')) {
|
|
id = urlObj.pathname.slice(1);
|
|
}
|
|
return id;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function Learn({ course, modules, lesson }: PageProps) {
|
|
const [isCompleted, setIsCompleted] = useState(lesson.is_completed);
|
|
|
|
const handleComplete = () => {
|
|
// Optimistic UI update
|
|
setIsCompleted(true);
|
|
// In real app: router.post(route('lessons.complete', lesson.id))
|
|
};
|
|
|
|
return (
|
|
<CourseLayout course={course} modules={modules} currentLesson={lesson}>
|
|
<Head title={`${lesson.title} - ${course.title}`} />
|
|
|
|
{/* Header Content */}
|
|
<div className="mb-8 space-y-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground text-sm uppercase tracking-wider font-bold">
|
|
<span className="bg-primary/10 text-primary px-2 py-0.5 rounded text-[10px]">{lesson.type}</span>
|
|
<span>•</span>
|
|
<span>{Math.ceil(lesson.duration_seconds / 60)} min read</span>
|
|
</div>
|
|
<h1 className="text-3xl md:text-4xl font-sans font-black tracking-tight text-foreground leading-tight">
|
|
{lesson.title}
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Main Content Render */}
|
|
<div className="space-y-8">
|
|
|
|
{lesson.type === 'video' && lesson.video_url && (
|
|
<div className="relative aspect-video rounded-2xl overflow-hidden shadow-2xl bg-black border border-border/50 group">
|
|
<Plyr
|
|
source={{
|
|
type: 'video',
|
|
sources: [{
|
|
src: getYouTubeId(lesson.video_url) || lesson.video_url || '',
|
|
provider: getYouTubeId(lesson.video_url) ? 'youtube' : 'html5',
|
|
}]
|
|
}}
|
|
options={{
|
|
youtube: {
|
|
noCookie: true,
|
|
rel: 0,
|
|
showinfo: 0,
|
|
iv_load_policy: 3,
|
|
modestbranding: 1
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{lesson.type === 'pdf' && lesson.content_pdf && (
|
|
<div className="rounded-2xl border border-border/50 overflow-hidden bg-card h-[80vh] flex flex-col shadow-sm">
|
|
<div className="bg-muted/30 border-b p-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<FileText size={16} />
|
|
<span className="truncate max-w-[200px]">{lesson.title}.pdf</span>
|
|
</div>
|
|
<Button size="sm" variant="ghost" asChild>
|
|
<a href={lesson.content_pdf} target="_blank" rel="noopener noreferrer">
|
|
<Maximize2 size={16} className="mr-2" />
|
|
Buka Full
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
<iframe
|
|
src={`${lesson.content_pdf}#toolbar=0`}
|
|
className="w-full h-full bg-white"
|
|
title="PDF Viewer"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Text Content (Notion-style blocks) */}
|
|
{lesson.content && (
|
|
<article className="prose prose-lg dark:prose-invert prose-headings:font-bold prose-headings:tracking-tight prose-p:leading-relaxed prose-img:rounded-xl max-w-none">
|
|
<div dangerouslySetInnerHTML={{ __html: lesson.content }} />
|
|
</article>
|
|
)}
|
|
|
|
{/* Attachments Section */}
|
|
{lesson.attachments && lesson.attachments.length > 0 && (
|
|
<div className="mt-12 pt-8 border-t">
|
|
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
|
<Download size={20} className="text-primary" />
|
|
Materi Pendukung
|
|
</h3>
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
{lesson.attachments.map((file) => (
|
|
<a
|
|
key={file.id}
|
|
href={file.url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="flex items-center gap-4 p-4 rounded-xl border bg-card hover:bg-muted/50 transition-all group"
|
|
>
|
|
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center group-hover:bg-primary/10 group-hover:text-primary transition-colors">
|
|
<FileText size={20} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium truncate">{file.name}</p>
|
|
<p className="text-xs text-muted-foreground">{file.size}</p>
|
|
</div>
|
|
<Download size={16} className="text-muted-foreground group-hover:text-foreground" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer / Navigation */}
|
|
<div className="mt-16 pt-8 border-t flex flex-col sm:flex-row items-center justify-between gap-6 pb-20">
|
|
<div className="w-full sm:w-auto">
|
|
{lesson.prev_lesson_slug ? (
|
|
<Button variant="ghost" asChild className="w-full sm:w-auto justify-start pl-0 hover:bg-transparent hover:text-primary">
|
|
<Link href={route('courses.learn', { course: course.slug, lesson: lesson.prev_lesson_slug })}>
|
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
<div className="flex flex-col items-start gap-0.5">
|
|
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-bold">Sebelumnya</span>
|
|
<span className="font-bold line-clamp-1 max-w-[150px]">Pelajaran Sebelumnya</span>
|
|
</div>
|
|
</Link>
|
|
</Button>
|
|
) : <div />}
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row items-center gap-4 w-full sm:w-auto">
|
|
{!isCompleted ? (
|
|
<Button
|
|
size="lg"
|
|
className="w-full sm:w-auto rounded-full font-bold shadow-lg shadow-primary/20 hover:shadow-primary/40 transition-all"
|
|
onClick={handleComplete}
|
|
>
|
|
<CheckCircle className="mr-2 h-5 w-5" />
|
|
Tandai Selesai
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="secondary"
|
|
size="lg"
|
|
className="w-full sm:w-auto rounded-full font-bold bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-900/50 cursor-default"
|
|
>
|
|
<CheckCircle className="mr-2 h-5 w-5" />
|
|
Sudah Selesai
|
|
</Button>
|
|
)}
|
|
|
|
{lesson.next_lesson_slug && (
|
|
<Button asChild className="w-full sm:w-auto rounded-full gap-2 font-bold px-6">
|
|
<Link href={route('courses.learn', { course: course.slug, lesson: lesson.next_lesson_slug })}>
|
|
Berikutnya
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CourseLayout>
|
|
);
|
|
}
|