feat: Implement a new course learning system with dedicated layouts, lesson playback, and Spaced Repetition System (SRS) functionality.

This commit is contained in:
2026-01-25 18:17:26 +07:00
parent 74e5c2893d
commit 97547521ad
17 changed files with 881 additions and 990 deletions

View File

@@ -1,10 +1,10 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { Head, Link } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/Components/ui/card";
import { Card } from "@/Components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/Components/ui/avatar";
import { Button } from "@/Components/ui/button";
import { BookOpen, Flame, GraduationCap, Trophy, Play } from "lucide-react";
import CourseCard from '@/Components/CourseCard';
import { BookOpen, Flame, Trophy, Play, Plus, Brain } from "lucide-react";
import { cn } from "@/lib/utils";
interface DashboardProps {
stats: {
@@ -15,172 +15,156 @@ interface DashboardProps {
srs_due: number;
srs_new: number;
};
activeCourses: Array<{
id: string;
title: string;
thumbnail: string;
level: string;
progress: number;
lessonsCount: number;
completedLessons: number;
slug: string;
}>;
user: {
name: string;
avatar: string;
rank: string;
xp_points?: number;
};
activeCourses: Array<any>;
user: any;
}
export default function Dashboard({
stats: propStats = { xp_points: 0, current_streak: 0, active_courses: 0, certificates: 0, srs_due: 0, srs_new: 0 },
activeCourses: propCourses = [],
user: propUser = { name: 'Student', avatar: '', rank: 'Genin' }
export default function Dashboard({
stats = { xp_points: 0, current_streak: 0, active_courses: 0, certificates: 0, srs_due: 0, srs_new: 0 },
activeCourses = [],
user
}: Partial<DashboardProps>) {
const activeCourses = propCourses ?? [];
const userData = propUser ?? { name: 'Student', avatar: '', rank: 'Genin' };
const stats = propStats ?? { xp_points: 0, current_streak: 0, active_courses: 0, certificates: 0, srs_due: 0, srs_new: 0 };
// Stitch Heatmap Simulation
const heatmapIntensity = ['bg-gray-200 dark:bg-white/5', 'bg-[#FF4500]/20', 'bg-[#FF4500]/40', 'bg-[#FF4500]/60', 'bg-[#FF4500]'];
const heatmapCells = Array.from({ length: 84 }, () => Math.floor(Math.random() * 5));
return (
<AuthenticatedLayout
header={
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">Dashboard Siswa</h2>
<p className="text-muted-foreground text-sm">Selamat datang kembali! Yuk, lanjutkan progres belajarmu hari ini.</p>
</div>
}
>
<DashboardLayout>
<Head title="Dashboard" />
<div className="space-y-8 animate-in fade-in duration-700">
{/* Stats Grid */}
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
<Card className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Total XP</CardTitle>
<Trophy className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-black">{stats.xp_points.toLocaleString()}</div>
</CardContent>
</Card>
<Card className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Streak</CardTitle>
<Flame className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-black">{stats.current_streak} Hari</div>
</CardContent>
</Card>
<Card className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Kursus</CardTitle>
<BookOpen className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-black">{stats.active_courses} Aktif</div>
</CardContent>
</Card>
<Card className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Sertifikat</CardTitle>
<GraduationCap className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-black">{stats.certificates} Diraih</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-12 gap-6 animate-in fade-in duration-500">
<div className="grid gap-8 lg:grid-cols-12">
{/* Main Content: Course List */}
<div className="lg:col-span-8 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold tracking-tight">Lanjutkan Belajar</h3>
<Link href="#" className="text-xs font-semibold text-primary hover:underline">Lihat Semua</Link>
</div>
<div className="grid sm:grid-cols-2 gap-6">
{activeCourses.length > 0 ? activeCourses.map((course, i) => (
<CourseCard key={i} {...course} />
)) : (
<div className="col-span-full border-2 border-dashed border-border/50 rounded-3xl py-12 flex flex-col items-center justify-center text-center px-6">
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<BookOpen className="text-muted-foreground" size={32} />
</div>
<h4 className="font-bold text-foreground">Belum ada kursus aktif</h4>
<p className="text-sm text-muted-foreground max-w-xs mt-1 mb-6">Mulai perjalanan belajarmu sekarang dengan memilih paket kursus yang tersedia.</p>
<Button className="rounded-full px-8">Explorasi Kursus</Button>
</div>
)}
</div>
{/* SRS Review Card (Large) */}
<div className="col-span-12 lg:col-span-8 bg-white/60 dark:bg-[#161618]/60 backdrop-blur-md border border-gray-200 dark:border-white/5 rounded-xl p-8 relative overflow-hidden group shadow-sm transition-colors duration-300">
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
<Brain size={120} className="text-[#FF4500]" />
</div>
{/* Sidebar: Profile & SRS */}
<div className="lg:col-span-4 space-y-6">
<Card className="overflow-hidden border-border/50 bg-card/50 backdrop-blur-xl">
<div className="h-24 bg-gradient-to-br from-primary/20 via-primary/10 to-transparent dark:from-primary/30 dark:via-primary/5" />
<CardContent className="relative pt-0 px-6 pb-6">
<Avatar className="h-20 w-20 absolute -top-10 border-4 border-background ring-1 ring-border/20">
<AvatarImage src={userData.avatar} />
<AvatarFallback className="bg-primary text-primary-foreground font-bold">{userData.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="mt-12">
<h3 className="font-bold text-xl">{userData.name}</h3>
<p className="text-sm text-muted-foreground font-medium">Rank: <span className="text-primary italic font-bold">{userData.rank}</span></p>
</div>
<div className="mt-6 pt-6 border-t border-border/50 grid grid-cols-2 gap-4 text-center">
<div className="space-y-0.5">
<p className="text-xl font-black">{stats.xp_points.toLocaleString()}</p>
<p className="text-[10px] text-muted-foreground uppercase font-bold tracking-wider">Total XP</p>
</div>
<div className="space-y-0.5">
<p className="text-xl font-black">{stats.current_streak}</p>
<p className="text-[10px] text-muted-foreground uppercase font-bold tracking-wider">Streak</p>
</div>
</div>
<Button variant="outline" className="w-full mt-6 rounded-xl text-xs font-bold border-border/50">Edit Profil</Button>
</CardContent>
</Card>
{/* SRS Reminder */}
<Card className="bg-primary text-primary-foreground border-none shadow-xl shadow-primary/20 relative overflow-hidden group">
<div className="absolute top-0 right-0 p-8 opacity-10 transform translate-x-4 -translate-y-4 group-hover:scale-110 transition-transform duration-700">
<Flame size={120} />
</div>
<CardHeader className="relative z-10">
<CardTitle className="flex items-center gap-2 text-lg">
Hafalan (SRS)
</CardTitle>
<CardDescription className="text-primary-foreground/80 font-medium">
{stats.srs_due > 0 ? (
<>Ada <span className="font-black text-white">{stats.srs_due} kata</span> yang perlu diulas hari ini.</>
) : stats.srs_new > 0 ? (
<>Tidak ada ulasan, tapi ada <span className="font-black text-white">{stats.srs_new} kata baru</span> siap dipelajari!</>
) : (
<>Luar biasa! Semua hafalanmu sudah selesai untuk hari ini.</>
)}
</CardDescription>
</CardHeader>
<CardContent className="relative z-10">
<Button
variant="secondary"
className="w-full font-black text-primary rounded-xl h-11"
size="lg"
onClick={() => window.location.href = route('srs.index')}
>
{stats.srs_due > 0 ? 'Mulai Sesi Review' : 'Buka Koleksi Kata'}
<Play className="ml-2 w-4 h-4 fill-current" />
<div className="relative z-10 flex flex-col h-full justify-between gap-8">
<div>
<h2 className="text-[#FF4500] font-bold uppercase tracking-widest text-xs mb-2">SRS Status</h2>
<p className="text-5xl font-bold text-gray-900 dark:text-white tracking-tight">{stats.srs_due} Reviews Due</p>
<p className="text-gray-500 dark:text-white/60 mt-2 max-w-md">
{stats.srs_due > 0
? "You have Kanji and Vocabulary items waiting. Keep your streak alive!"
: "All caught up! Why not learn some new words?"}
</p>
</div>
<div className="flex items-center gap-4">
<Link href={route('srs.index')}>
<Button className="bg-[#FF4500] hover:bg-[#FF4500]/90 text-white px-8 py-6 rounded-lg font-bold text-base shadow-[0_0_15px_rgba(255,68,0,0.3)] hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center gap-2">
Start Session
<Play size={20} className="fill-current" />
</Button>
</CardContent>
</Card>
</Link>
<div className="text-xs font-medium text-gray-500 dark:text-white/40 flex flex-col">
<span>Estimated time</span>
<span className="text-gray-900 dark:text-white">~{Math.ceil(stats.srs_due * 0.5)} minutes</span>
</div>
</div>
</div>
</div>
{/* Streak Widget */}
<div className="col-span-12 md:col-span-6 lg:col-span-4 bg-[#FF4500]/5 dark:bg-[#FF4500]/[0.03] border border-[#FF4500]/20 backdrop-blur-md rounded-xl p-6 flex flex-col justify-between shadow-sm transition-colors duration-300">
<div className="flex justify-between items-start">
<div className="size-12 rounded bg-[#FF4500]/20 flex items-center justify-center">
<Flame size={24} className="text-[#FF4500] fill-current" />
</div>
<span className="text-[10px] font-bold uppercase tracking-widest text-[#FF4500] bg-[#FF4500]/10 px-2 py-1 rounded">Daily Goal Met</span>
</div>
<div className="mt-4">
<p className="text-4xl font-bold text-gray-900 dark:text-white tracking-tighter">{stats.current_streak} Day Streak</p>
<p className="text-gray-500 dark:text-white/40 text-sm mt-1">Don't break the chain. Keep going!</p>
</div>
<div className="mt-6 flex gap-1">
{[1, 1, 1, 1, 0, 0, 0].map((active, i) => (
<div
key={i}
className={cn(
"h-1.5 flex-1 rounded-full",
active ? "bg-[#FF4500] shadow-[0_0_8px_rgba(255,68,0,0.4)]" : "bg-gray-200 dark:bg-white/10"
)}
/>
))}
</div>
</div>
{/* Study Heatmap */}
<div className="col-span-12 lg:col-span-8 bg-white/60 dark:bg-[#161618]/60 backdrop-blur-md border border-gray-200 dark:border-white/5 rounded-xl p-6 shadow-sm transition-colors duration-300">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-gray-900 dark:text-white font-bold">Study Activity</h3>
<p className="text-gray-500 dark:text-white/40 text-xs">Consistent effort yields results</p>
</div>
<div className="flex gap-2 items-center text-[10px] text-gray-400 dark:text-white/40">
<span>Less</span>
{heatmapIntensity.map((bg, i) => (
<div key={i} className={cn("size-3 rounded-sm", bg)} />
))}
<span>More</span>
</div>
</div>
<div className="grid grid-cols-12 gap-2">
<div className="col-span-12 flex gap-1.5 flex-wrap">
{heatmapCells.map((level, i) => (
<div key={i} className={cn("w-[11px] h-[11px] rounded-sm", heatmapIntensity[level])} />
))}
</div>
</div>
<div className="mt-4 flex justify-between text-[11px] font-medium text-gray-400 dark:text-white/40 uppercase tracking-widest">
<span>October</span>
<span>November</span>
<span>December</span>
<span>January</span>
</div>
</div>
{/* Level Progress (Circular) */}
<div className="col-span-12 md:col-span-6 lg:col-span-4 bg-white/60 dark:bg-[#161618]/60 backdrop-blur-md border border-gray-200 dark:border-white/5 rounded-xl p-6 flex flex-col items-center justify-center text-center shadow-sm transition-colors duration-300">
<div className="relative size-32 flex items-center justify-center">
<svg className="absolute inset-0 size-full -rotate-90">
<circle className="text-gray-200 dark:text-white/5" cx="64" cy="64" fill="transparent" r="58" stroke="currentColor" strokeWidth="8"></circle>
<circle className="text-[#FF4500]" cx="64" cy="64" fill="transparent" r="58" stroke="currentColor" strokeDasharray="364.4" strokeDashoffset="127.5" strokeLinecap="round" strokeWidth="8"></circle>
</svg>
<div className="flex flex-col items-center">
<span className="text-3xl font-bold text-gray-900 dark:text-white">65%</span>
<span className="text-[10px] font-bold text-gray-500 dark:text-white/40 uppercase">N5 Progress</span>
</div>
</div>
<div className="mt-6">
<h4 className="text-gray-900 dark:text-white font-bold text-lg">JLPT N5 Path</h4>
<p className="text-gray-500 dark:text-white/40 text-sm mt-1">242 / 800 Kanji mastered</p>
</div>
<Link href={route('courses.index')} className="w-full mt-4">
<Button variant="outline" className="w-full border-gray-200 dark:border-white/10 hover:bg-gray-100 dark:hover:bg-white/5 text-xs font-bold text-gray-700 dark:text-white/80">
VIEW CURRICULUM
</Button>
</Link>
</div>
{/* Quick Stats Footer */}
<div className="col-span-12 grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white/60 dark:bg-[#161618]/60 border border-gray-200 dark:border-white/5 p-4 rounded-lg shadow-sm">
<p className="text-[10px] font-bold text-gray-500 dark:text-white/40 uppercase tracking-widest">Items Learned</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">1,402</p>
</div>
<div className="bg-white/60 dark:bg-[#161618]/60 border border-gray-200 dark:border-white/5 p-4 rounded-lg shadow-sm">
<p className="text-[10px] font-bold text-gray-500 dark:text-white/40 uppercase tracking-widest">Retention Rate</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">88.4%</p>
</div>
<div className="bg-white/60 dark:bg-[#161618]/60 border border-gray-200 dark:border-white/5 p-4 rounded-lg shadow-sm">
<p className="text-[10px] font-bold text-gray-500 dark:text-white/40 uppercase tracking-widest">Global Rank</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">#412</p>
</div>
<div className="bg-white/60 dark:bg-[#161618]/60 border border-gray-200 dark:border-white/5 p-4 rounded-lg shadow-sm">
<p className="text-[10px] font-bold text-gray-500 dark:text-white/40 uppercase tracking-widest">Total XP</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">{stats.xp_points.toLocaleString()}</p>
</div>
</div>
</div>
</AuthenticatedLayout>
</DashboardLayout>
);
}