mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 13:32:07 +07:00
first commit
This commit is contained in:
121
resources/js/Pages/Courses/Library.tsx
Normal file
121
resources/js/Pages/Courses/Library.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/Components/ui/card";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { BookOpen, CheckCircle2, Play } from "lucide-react";
|
||||
|
||||
interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: string;
|
||||
slug: string;
|
||||
modulesCount: number;
|
||||
isEnrolled: boolean;
|
||||
}
|
||||
|
||||
interface Level {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
courses: Course[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
levels: Level[];
|
||||
}
|
||||
|
||||
export default function Library({ levels }: Props) {
|
||||
return (
|
||||
<AuthenticatedLayout
|
||||
header={
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Galeri Kursus</h2>
|
||||
<p className="text-muted-foreground text-sm">Pilih materi yang ingin kamu pelajari hari ini.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Head title="Galeri Kursus" />
|
||||
|
||||
<div className="space-y-12 animate-in fade-in duration-700">
|
||||
{levels.length > 0 ? levels.map((level) => (
|
||||
<div key={level.id} className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-1 bg-primary rounded-full" />
|
||||
<h3 className="text-xl font-black tracking-tight">{level.name} ({level.code})</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{level.courses.map((course) => (
|
||||
<CourseCard key={course.id} course={course} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-muted-foreground">Belum ada materi tersedia saat ini.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function CourseCard({ course }: { course: Course }) {
|
||||
const { post, processing } = useForm();
|
||||
|
||||
const handleEnroll = () => {
|
||||
post(route('courses.enroll', course.slug));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="group border-border/40 bg-card/40 backdrop-blur-xl overflow-hidden hover:shadow-2xl hover:shadow-primary/5 transition-all duration-500 hover:-translate-y-1 flex flex-col h-full">
|
||||
<div className="relative aspect-video overflow-hidden">
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="object-cover w-full h-full transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
{course.isEnrolled && (
|
||||
<Badge className="absolute top-3 right-3 bg-green-500 hover:bg-green-600 text-white border-none gap-1">
|
||||
<CheckCircle2 size={12} />
|
||||
Terdaftar
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CardHeader className="p-5 flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Badge variant="outline" className="text-[10px] font-bold uppercase tracking-widest border-primary/20 text-primary">
|
||||
{course.modulesCount} Materi
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-lg font-bold group-hover:text-primary transition-colors">{course.title}</CardTitle>
|
||||
<CardDescription className="line-clamp-2 text-xs mt-2 font-medium leading-relaxed">
|
||||
{course.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardFooter className="p-5 pt-0 mt-auto">
|
||||
{course.isEnrolled ? (
|
||||
<Button asChild className="w-full rounded-xl font-bold bg-primary/10 hover:bg-primary/20 text-primary shadow-none border-none">
|
||||
<Link href={route('courses.learn', course.slug)}>
|
||||
Lanjutkan Belajar <Play className="ml-2 w-3 h-3 fill-current" />
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleEnroll}
|
||||
disabled={processing}
|
||||
className="w-full rounded-xl font-bold bg-primary hover:bg-primary/90 shadow-lg shadow-primary/20"
|
||||
>
|
||||
{processing ? 'Memproses...' : 'Daftar Sekarang'}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
183
resources/js/Pages/Courses/Player.tsx
Normal file
183
resources/js/Pages/Courses/Player.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { Plyr } from 'plyr-react';
|
||||
import { CheckCircle2, ChevronLeft, Circle, FileText, Play, Video } from 'lucide-react';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { Progress } from '@/Components/ui/progress';
|
||||
|
||||
interface LessonData {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
type: 'video' | 'text' | 'pdf';
|
||||
is_completed: boolean;
|
||||
}
|
||||
|
||||
interface ModuleData {
|
||||
id: string;
|
||||
title: string;
|
||||
lessons: LessonData[];
|
||||
}
|
||||
|
||||
interface CourseData {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
modules: ModuleData[];
|
||||
}
|
||||
|
||||
interface CurrentLessonData {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
type: 'video' | 'text' | 'pdf';
|
||||
content: string | null;
|
||||
video_url: string | null;
|
||||
content_pdf: string | null;
|
||||
}
|
||||
|
||||
interface ProgressData {
|
||||
completed_count: number;
|
||||
total_count: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
interface PlayerProps {
|
||||
course: CourseData;
|
||||
currentLesson: CurrentLessonData;
|
||||
progress: ProgressData;
|
||||
}
|
||||
|
||||
// Helper to get YouTube video ID
|
||||
function getYouTubeId(url: string): string | null {
|
||||
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return (match && match[2].length === 11) ? match[2] : null;
|
||||
}
|
||||
|
||||
export default function Player({ course, currentLesson, progress }: PlayerProps) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
|
||||
const handleComplete = () => {
|
||||
router.post(route('lessons.complete', { lesson: currentLesson.id }));
|
||||
};
|
||||
|
||||
const videoSource = currentLesson.video_url ? {
|
||||
type: 'video' as const,
|
||||
sources: [{
|
||||
src: getYouTubeId(currentLesson.video_url) || currentLesson.video_url,
|
||||
provider: currentLesson.video_url.includes('youtube') ? 'youtube' as const : 'html5' as const,
|
||||
}],
|
||||
} : null;
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout
|
||||
header={
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={route('dashboard')} className="p-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors">
|
||||
<ChevronLeft className="w-5 h-5 text-gray-600" />
|
||||
</Link>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-900 line-clamp-1">{course.title}</h2>
|
||||
<p className="text-xs text-gray-500">{progress.completed_count}/{progress.total_count} Materi Selesai</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={progress.percentage} className="w-32 h-2" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Head title={`${currentLesson.title} - ${course.title}`} />
|
||||
|
||||
<div className="flex h-[calc(100vh-80px)]">
|
||||
{/* Main Content Area */}
|
||||
<div className={`flex-1 overflow-y-auto p-6 lg:p-10 transition-all ${sidebarOpen ? 'lg:mr-80' : ''}`}>
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Lesson Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
{currentLesson.type === 'video' && <Video className="w-6 h-6 text-nihonbuzz-red" />}
|
||||
{currentLesson.type === 'text' && <FileText className="w-6 h-6 text-blue-500" />}
|
||||
{currentLesson.type === 'pdf' && <FileText className="w-6 h-6 text-orange-500" />}
|
||||
<h1 className="text-2xl font-black text-gray-900">{currentLesson.title}</h1>
|
||||
</div>
|
||||
|
||||
{/* Video Player */}
|
||||
{currentLesson.type === 'video' && videoSource && (
|
||||
<div className="aspect-video rounded-2xl overflow-hidden shadow-2xl bg-black">
|
||||
<Plyr source={videoSource} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Content */}
|
||||
{currentLesson.type === 'text' && currentLesson.content && (
|
||||
<div
|
||||
className="prose prose-lg max-w-none bg-white p-8 rounded-2xl shadow-sm border border-gray-100"
|
||||
dangerouslySetInnerHTML={{ __html: currentLesson.content }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* PDF Content */}
|
||||
{currentLesson.type === 'pdf' && currentLesson.content_pdf && (
|
||||
<div className="aspect-[3/4] w-full">
|
||||
<iframe
|
||||
src={currentLesson.content_pdf}
|
||||
className="w-full h-full rounded-2xl shadow-lg border border-gray-200"
|
||||
title={currentLesson.title}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mark as Complete Button */}
|
||||
<div className="flex justify-center pt-6">
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
size="lg"
|
||||
className="bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold rounded-full px-10 py-6 text-base shadow-xl shadow-nihonbuzz-red/30"
|
||||
>
|
||||
<CheckCircle2 className="w-5 h-5 mr-2" />
|
||||
Tandai Selesai & Lanjutkan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar - Module & Lesson Navigation */}
|
||||
<aside className={`fixed right-0 top-16 bottom-0 w-80 bg-white border-l border-gray-100 overflow-y-auto transition-transform ${sidebarOpen ? 'translate-x-0' : 'translate-x-full'} hidden lg:block`}>
|
||||
<div className="p-6 space-y-6">
|
||||
<h3 className="text-sm font-black text-gray-400 uppercase tracking-widest">Daftar Materi</h3>
|
||||
|
||||
{course.modules.map((module) => (
|
||||
<div key={module.id} className="space-y-2">
|
||||
<h4 className="font-bold text-gray-700 text-sm">{module.title}</h4>
|
||||
<ul className="space-y-1">
|
||||
{module.lessons.map((lesson) => (
|
||||
<li key={lesson.id}>
|
||||
<Link
|
||||
href={route('courses.learn', { course: course.slug, lesson: lesson.slug })}
|
||||
className={`flex items-center gap-3 p-3 rounded-xl transition-all text-sm ${
|
||||
lesson.id === currentLesson.id
|
||||
? 'bg-nihonbuzz-red/10 text-nihonbuzz-red font-bold'
|
||||
: 'hover:bg-gray-50 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{lesson.is_completed ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : lesson.id === currentLesson.id ? (
|
||||
<Play className="w-5 h-5 text-nihonbuzz-red shrink-0 fill-current" />
|
||||
) : (
|
||||
<Circle className="w-5 h-5 text-gray-300 shrink-0" />
|
||||
)}
|
||||
<span className="line-clamp-1">{lesson.title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user