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,14 +1,16 @@
import React, { useState } from 'react';
import { Head, Link } from '@inertiajs/react';
import {
ChevronLeft,
ChevronRight,
CheckCircle,
FileText,
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';
@@ -36,6 +38,23 @@ interface PageProps {
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);
@@ -48,7 +67,7 @@ export default function Learn({ course, modules, lesson }: PageProps) {
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">
@@ -63,21 +82,32 @@ export default function Learn({ course, modules, lesson }: PageProps) {
{/* 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">
<iframe
src={lesson.video_url}
className="w-full h-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
title={lesson.title}
<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="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} />
@@ -90,8 +120,8 @@ export default function Learn({ course, modules, lesson }: PageProps) {
</a>
</Button>
</div>
<iframe
src={`${lesson.content_pdf}#toolbar=0`}
<iframe
src={`${lesson.content_pdf}#toolbar=0`}
className="w-full h-full bg-white"
title="PDF Viewer"
/>
@@ -114,10 +144,10 @@ export default function Learn({ course, modules, lesson }: PageProps) {
</h3>
<div className="grid sm:grid-cols-2 gap-4">
{lesson.attachments.map((file) => (
<a
key={file.id}
href={file.url}
target="_blank"
<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"
>
@@ -154,8 +184,8 @@ export default function Learn({ course, modules, lesson }: PageProps) {
<div className="flex flex-col sm:flex-row items-center gap-4 w-full sm:w-auto">
{!isCompleted ? (
<Button
size="lg"
<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}
>
@@ -163,9 +193,9 @@ export default function Learn({ course, modules, lesson }: PageProps) {
Tandai Selesai
</Button>
) : (
<Button
<Button
variant="secondary"
size="lg"
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" />

View File

@@ -1,4 +1,4 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
@@ -28,7 +28,7 @@ interface Props {
export default function Library({ levels }: Props) {
return (
<AuthenticatedLayout
<DashboardLayout
header={
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">Galeri Kursus</h2>
@@ -58,7 +58,7 @@ export default function Library({ levels }: Props) {
</div>
)}
</div>
</AuthenticatedLayout>
</DashboardLayout>
);
}
@@ -72,13 +72,13 @@ function CourseCard({ course }: { course: Course }) {
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}
<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} />
@@ -107,8 +107,8 @@ function CourseCard({ course }: { course: Course }) {
</Link>
</Button>
) : (
<Button
onClick={handleEnroll}
<Button
onClick={handleEnroll}
disabled={processing}
className="w-full rounded-xl font-bold bg-primary hover:bg-primary/90 shadow-lg shadow-primary/20"
>

View File

@@ -1,31 +1,17 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { cn } from '@/lib/utils';
import CourseLayout from '@/Layouts/CourseLayout';
import { Head, router } from '@inertiajs/react';
import { useState, useEffect } from 'react';
import { Plyr } from 'plyr-react';
import 'plyr-react/plyr.css';
import {
CheckCircle2,
ChevronLeft,
Circle,
FileText,
Play,
Video,
Menu,
Volume2,
import {
BookOpen,
ArrowRight,
Trophy
Volume2,
Play
} from 'lucide-react';
import { Button } from '@/Components/ui/button';
import { Progress } from '@/Components/ui/progress';
import { FuriganaText } from '@/lib/furigana';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from "@/Components/ui/sheet";
interface VocabularyData {
id: string;
@@ -56,6 +42,7 @@ interface CourseData {
id: string;
title: string;
slug: string;
progress_percentage: number;
modules: ModuleData[];
}
@@ -69,85 +56,54 @@ interface CurrentLessonData {
content_pdf: string | null;
vocabularies: VocabularyData[];
is_completed?: boolean;
}
interface ProgressData {
completed_count: number;
total_count: number;
percentage: number;
duration_seconds?: number;
}
interface PlayerProps {
course: CourseData;
currentLesson: CurrentLessonData;
progress: ProgressData;
nextLesson?: LessonData | null;
previousLesson?: LessonData | null;
auth: {
user: any;
};
[key: string]: any;
}
// Helper to get YouTube video ID
// Helper to get YouTube video ID (Original Regex Logic)
// Helper to get YouTube video ID (Robust)
function getYouTubeId(url: string | null): string | null {
if (!url) return 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;
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) {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[2]) ? match[2] : null;
}
}
/**
* Sub-component for Navigation Content
*/
function NavigationContent({ course, currentLesson }: { course: CourseData, currentLesson: CurrentLessonData }) {
return (
<div className="space-y-6">
<h3 className="text-xs font-black text-gray-400 uppercase tracking-widest pl-1">Daftar Materi</h3>
<div className="space-y-4">
{course.modules.map((module) => (
<div key={module.id} className="space-y-2">
<div className="flex items-center gap-2 group cursor-default">
<div className="w-1 h-4 bg-nihonbuzz-red/30 rounded-full group-hover:bg-nihonbuzz-red transition-colors" />
<h4 className="font-black text-gray-800 text-xs uppercase tracking-tight">{module.title}</h4>
</div>
<ul className="space-y-1 ml-1">
{module.lessons.map((lesson) => (
<li key={lesson.id}>
<Link
// @ts-ignore
href={route('courses.learn', { course: course.slug, lesson: lesson.slug })}
className={`flex items-center gap-3 p-3 rounded-xl transition-all text-sm group ${
lesson.id === currentLesson.id
? 'bg-nihonbuzz-red text-white shadow-lg shadow-nihonbuzz-red/20 font-bold'
: 'hover:bg-gray-100 text-gray-600'
}`}
>
<div className="relative flex items-center justify-center shrink-0">
{lesson.is_completed ? (
<div className="bg-green-100 p-0.5 rounded-full">
<CheckCircle2 className="w-4 h-4 text-green-600" />
</div>
) : lesson.id === currentLesson.id ? (
<Play className="w-4 h-4 text-white fill-current" />
) : (
<Circle className="w-4 h-4 text-gray-300 group-hover:text-gray-400" />
)}
</div>
<span className="line-clamp-1">{lesson.title}</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}
export default function Player({ course, currentLesson, progress, auth }: PlayerProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
export default function Player({ course, currentLesson, nextLesson, previousLesson, auth }: PlayerProps) {
const [isPlaying, setIsPlaying] = useState<string | null>(null);
// ROBUST LOGIC:
// Extract ID first. If valid ID, force provider 'youtube'.
const youtubeId = getYouTubeId(currentLesson.video_url);
const videoSource = currentLesson.type === 'video' && currentLesson.video_url ? {
type: 'video' as const,
sources: [{
src: youtubeId || currentLesson.video_url,
provider: youtubeId ? 'youtube' as const : 'html5' as const,
}],
} : null;
// Learning Tracer - Heartbeat every 60 seconds
useEffect(() => {
const interval = setInterval(() => {
@@ -155,9 +111,9 @@ export default function Player({ course, currentLesson, progress, auth }: Player
router.post(route('lessons.heartbeat', { lesson: currentLesson.id }), {}, {
preserveScroll: true,
preserveState: true,
only: [], // Prevent any data reload to keep it "silent"
only: [],
// @ts-ignore
onFinish: () => {}
onFinish: () => { }
});
}, 60000);
@@ -176,204 +132,129 @@ export default function Player({ course, currentLesson, progress, auth }: Player
audio.onended = () => setIsPlaying(null);
};
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 gap-4 px-2 lg:px-0">
<div className="flex items-center gap-2 lg:gap-4 overflow-hidden">
<Link
// @ts-ignore
href={route('courses.index')}
className="p-2.5 rounded-xl bg-gray-50 hover:bg-white hover:shadow-md border border-gray-100 transition-all text-gray-500 hover:text-nihonbuzz-red shrink-0"
>
<ChevronLeft className="w-5 h-5" />
</Link>
<div className="overflow-hidden">
<h2 className="text-sm lg:text-base font-black text-gray-900 line-clamp-1 flex items-center gap-2">
<span className="text-nihonbuzz-red truncate">{course.title}</span>
</h2>
<div className="flex items-center gap-2 mt-0.5">
<Progress value={progress.percentage} className="w-20 lg:w-32 h-1.5" />
<span className="text-[10px] font-bold text-gray-400 whitespace-nowrap">{progress.percentage}%</span>
</div>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
{/* Mobile Navigation Trigger */}
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="lg:hidden rounded-xl border-gray-200">
<Menu className="w-5 h-5 text-gray-600" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="p-0 border-l border-gray-100">
<SheetHeader className="p-6 border-b border-gray-50 text-left">
<SheetTitle className="text-xl font-black">{course.title}</SheetTitle>
<p className="text-xs text-nihonbuzz-red font-bold uppercase tracking-widest">Kurikulum Kursus</p>
</SheetHeader>
<div className="p-6 overflow-y-auto h-[calc(100vh-120px)]">
<NavigationContent course={course} currentLesson={currentLesson} />
</div>
</SheetContent>
</Sheet>
<div className="hidden lg:flex items-center gap-2 px-4 py-2 bg-yellow-50 rounded-full border border-yellow-100">
<Trophy className="w-4 h-4 text-yellow-600" />
<span className="text-xs font-black text-yellow-700">{auth.user.xp_points} XP</span>
</div>
</div>
</div>
}
<CourseLayout
course={course}
modules={course.modules as any}
currentLesson={currentLesson as any}
nextLesson={nextLesson as any}
previousLesson={previousLesson as any}
>
<Head title={`${currentLesson.title} - ${course.title}`} />
<div className="flex min-h-[calc(100vh-80px)] bg-gray-50/30">
{/* Main Content Area */}
<div className={`flex-1 overflow-y-auto p-4 lg:p-10 transition-all ${sidebarOpen ? 'lg:mr-80' : ''}`}>
<div className="max-w-4xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Video Player */}
{currentLesson.type === 'video' && videoSource && (
<div className="relative group shadow-2xl rounded-3xl overflow-hidden ring-1 ring-black/5 bg-black aspect-video transition-transform hover:scale-[1.01] duration-500">
<Plyr source={videoSource} />
</div>
)}
<div className="max-w-4xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 py-6 px-4 lg:py-10">
{/* Title & Stats */}
<div className="bg-white p-6 lg:p-8 rounded-[32px] shadow-sm border border-gray-100 relative overflow-hidden group">
<div className="absolute top-0 right-0 w-32 h-32 bg-gray-50 -mr-16 -mt-16 rounded-full opacity-50 group-hover:scale-110 transition-transform duration-700" />
<div className="relative space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black tracking-widest text-nihonbuzz-red uppercase">
<span className="px-2 py-0.5 bg-nihonbuzz-red/10 rounded-full">{currentLesson.type}</span>
{currentLesson.is_completed && <span className="px-2 py-0.5 bg-green-100 text-green-600 rounded-full">Completed</span>}
</div>
<h1 className="text-2xl lg:text-3xl font-black text-gray-900 leading-tight">
<FuriganaText text={currentLesson.title || ''} />
</h1>
</div>
</div>
{/* Text Content */}
{currentLesson.type === 'text' && currentLesson.content && (
<div className="bg-white p-8 lg:p-12 rounded-[40px] shadow-sm border border-gray-100">
<article className="prose prose-nihonbuzz prose-lg lg:prose-xl max-w-none font-medium text-gray-700 leading-relaxed">
<FuriganaText text={currentLesson.content || ''} />
</article>
</div>
)}
{/* PDF Content */}
{currentLesson.type === 'pdf' && currentLesson.content_pdf && (
<div className="aspect-[3/4] w-full rounded-[40px] overflow-hidden shadow-2xl ring-1 ring-black/5">
<iframe
src={currentLesson.content_pdf}
className="w-full h-full border-none"
title={currentLesson.title}
/>
</div>
)}
{/* Vocabulary Section */}
{currentLesson.vocabularies && currentLesson.vocabularies.length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="bg-nihonbuzz-red/10 p-2 rounded-2xl">
<BookOpen className="w-5 h-5 text-nihonbuzz-red" />
</div>
<h2 className="text-xl font-black text-gray-900 tracking-tight">Kosa Kata Fokus</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{currentLesson.vocabularies.map((vocab) => (
<div key={vocab.id} className="bg-white p-5 rounded-3xl border border-gray-100 shadow-sm flex items-center justify-between group hover:border-nihonbuzz-red/30 transition-all hover:shadow-md">
<div className="flex items-center gap-4">
<div
onClick={() => vocab.audio_url && playAudio(vocab.audio_url, vocab.id)}
className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all cursor-pointer ${
isPlaying === vocab.id ? 'bg-nihonbuzz-red text-white scale-95 shadow-inner' : 'bg-gray-50 text-gray-400 group-hover:bg-nihonbuzz-red/5 group-hover:text-nihonbuzz-red'
}`}
>
{isPlaying === vocab.id ? <Volume2 className="w-5 h-5 animate-pulse" /> : <Volume2 className="w-5 h-5" />}
</div>
<div>
<FuriganaText text={vocab.word || ''} className="text-lg font-black text-gray-900" />
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs font-bold text-nihonbuzz-red">{vocab.reading || ''}</span>
<span className="text-[10px] text-gray-400 uppercase font-black">{vocab.type || ''}</span>
</div>
</div>
</div>
<div className="text-right pr-2">
<p className="font-bold text-gray-700">{vocab.meaning_id || ''}</p>
<p className="text-[11px] text-gray-400">{vocab.romaji || ''}</p>
</div>
</div>
))}
</div>
</div>
)}
{/* Navigation Footer */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 pt-10 border-t border-gray-100">
<div className="flex items-center gap-6">
<div className="flex -space-x-3 overflow-hidden">
<div className="w-10 h-10 rounded-full border-4 border-white bg-gray-100 flex items-center justify-center font-bold text-nihonbuzz-red text-xs">NB</div>
<div className="w-10 h-10 rounded-full border-4 border-white bg-red-50 flex items-center justify-center font-bold text-nihonbuzz-red text-xs">🇯🇵</div>
</div>
<div className="text-left">
<p className="text-sm font-black text-gray-900 tracking-tight">NihonBuzz Academy</p>
<p className="text-xs text-nihonbuzz-red font-bold uppercase tracking-widest">Master Japanese Effortlessly</p>
</div>
</div>
<Button
onClick={handleComplete}
size="lg"
className={`rounded-full px-12 py-7 text-base font-black shadow-2xl transition-all hover:scale-105 active:scale-95 group ${
currentLesson.is_completed
? 'bg-green-500 hover:bg-green-600 shadow-green-200 text-white'
: 'bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 shadow-nihonbuzz-red/20 text-white'
}`}
>
{currentLesson.is_completed ? (
<>Selesai! Tonton Lagi</>
) : (
<>Tandai Selesai <ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" /></>
)}
</Button>
{/* Video Player */}
{currentLesson.type === 'video' && videoSource && (
<div className="relative group shadow-2xl rounded-3xl overflow-hidden ring-1 ring-black/5 bg-black aspect-video transition-transform hover:scale-[1.01] duration-500">
<Plyr source={videoSource} />
</div>
)}
{/* Title & Stats */}
<div className="bg-white dark:bg-[#161618] p-6 lg:p-8 rounded-[32px] shadow-sm border border-black/5 dark:border-white/5 relative overflow-hidden group">
<div className="absolute top-0 right-0 w-32 h-32 bg-[#FF4500]/5 -mr-16 -mt-16 rounded-full opacity-50 group-hover:scale-110 transition-transform duration-700" />
<div className="relative space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black tracking-widest text-[#FF4500] uppercase">
<span className="px-2 py-0.5 bg-[#FF4500]/10 rounded-full">{currentLesson.type}</span>
{currentLesson.is_completed && <span className="px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded-full">Completed</span>}
</div>
<h1 className="text-2xl lg:text-3xl font-black text-gray-900 dark:text-white leading-tight">
<FuriganaText text={currentLesson.title || ''} />
</h1>
</div>
</div>
{/* Sidebar - Desktop Navigation */}
<aside className={`fixed right-0 top-16 bottom-0 w-80 bg-white border-l border-gray-50 overflow-y-auto transition-all shadow-[-10px_0_30px_rgba(0,0,0,0.02)] ${sidebarOpen ? 'translate-x-0' : 'translate-x-full'} hidden lg:block z-10`}>
<div className="p-8 h-full flex flex-col">
<NavigationContent course={course} currentLesson={currentLesson} />
<div className="mt-auto pt-10">
<div className="bg-nihonbuzz-red/5 p-6 rounded-[32px] border border-nihonbuzz-red/10 text-center space-y-3 relative overflow-hidden group">
<div className="absolute inset-0 bg-nihonbuzz-red/5 translate-y-full group-hover:translate-y-0 transition-transform duration-500" />
<div className="relative">
<div className="inline-flex items-center justify-center w-12 h-12 bg-nihonbuzz-red text-white rounded-2xl shadow-lg shadow-nihonbuzz-red/20 mb-2">
<Trophy className="w-6 h-6" />
{/* Text Content */}
{currentLesson.type === 'text' && currentLesson.content && (
<div className="bg-white dark:bg-[#161618] p-8 lg:p-12 rounded-[40px] shadow-sm border border-black/5 dark:border-white/5">
<article className="prose prose-nihonbuzz prose-lg lg:prose-xl max-w-none font-medium text-gray-700 dark:text-gray-300 leading-relaxed">
<FuriganaText text={currentLesson.content || ''} />
</article>
</div>
)}
{/* PDF Content */}
{currentLesson.type === 'pdf' && currentLesson.content_pdf && (
<div className="aspect-[3/4] w-full rounded-[40px] overflow-hidden shadow-2xl ring-1 ring-black/5">
<iframe
src={currentLesson.content_pdf}
className="w-full h-full border-none"
title={currentLesson.title}
/>
</div>
)}
{/* Vocabulary Section */}
{currentLesson.vocabularies && currentLesson.vocabularies.length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="bg-[#FF4500]/10 p-2 rounded-2xl">
<BookOpen className="w-5 h-5 text-[#FF4500]" />
</div>
<h2 className="text-xl font-black text-gray-900 dark:text-white tracking-tight">Kosa Kata Fokus</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{currentLesson.vocabularies.map((vocab) => (
<div key={vocab.id} className="bg-white dark:bg-[#161618] p-5 rounded-3xl border border-black/5 dark:border-white/5 shadow-sm flex items-center justify-between group hover:border-[#FF4500]/30 transition-all hover:shadow-md">
<div className="flex items-center gap-4">
<div
onClick={() => vocab.audio_url && playAudio(vocab.audio_url, vocab.id)}
className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all cursor-pointer ${isPlaying === vocab.id ? 'bg-[#FF4500] text-white scale-95 shadow-inner' : 'bg-gray-50 dark:bg-white/5 text-gray-400 dark:text-white/40 group-hover:bg-[#FF4500]/5 group-hover:text-[#FF4500]'
}`}
>
{isPlaying === vocab.id ? <Volume2 className="w-5 h-5 animate-pulse" /> : <Volume2 className="w-5 h-5" />}
</div>
<div>
<FuriganaText text={vocab.word || ''} className="text-lg font-black text-gray-900 dark:text-white" />
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs font-bold text-[#FF4500]">{vocab.reading || ''}</span>
<span className="text-[10px] text-gray-400 dark:text-gray-500 uppercase font-black">{vocab.type || ''}</span>
</div>
</div>
</div>
<div className="text-right pr-2">
<p className="font-bold text-gray-700 dark:text-gray-300">{vocab.meaning_id || ''}</p>
<p className="text-[11px] text-gray-400 dark:text-gray-600">{vocab.romaji || ''}</p>
</div>
<p className="text-sm font-black text-gray-900">Selesaikan untuk XP!</p>
<p className="text-[10px] font-bold text-nihonbuzz-red uppercase tracking-widest">+50 XP Per Materi</p>
</div>
</div>
))}
</div>
</div>
</aside>
)}
{/* Navigation Footer */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 pt-10 border-t border-black/5 dark:border-white/5">
<div className="flex items-center gap-6">
<div className="flex -space-x-3 overflow-hidden">
<div className="w-10 h-10 rounded-full border-4 border-white dark:border-[#161618] bg-gray-100 flex items-center justify-center font-bold text-[#FF4500] text-xs">NB</div>
<div className="w-10 h-10 rounded-full border-4 border-white dark:border-[#161618] bg-[#FF4500]/10 flex items-center justify-center font-bold text-[#FF4500] text-xs">🇯🇵</div>
</div>
<div className="text-left">
<p className="text-sm font-black text-gray-900 dark:text-white tracking-tight">NihonBuzz Academy</p>
<p className="text-xs text-[#FF4500] font-bold uppercase tracking-widest">Master Japanese Effortlessly</p>
</div>
</div>
<Button
onClick={handleComplete}
size="lg"
className={cn(
"rounded-full px-12 py-7 text-base font-black shadow-2xl transition-all hover:scale-105 active:scale-95 group",
currentLesson.is_completed
? "bg-green-500 hover:bg-green-600 shadow-green-500/20 text-white"
: "bg-[#FF4500] hover:bg-[#FF4500]/90 shadow-[#FF4500]/20 text-white"
)}
>
{currentLesson.is_completed ? (
<>Selesai! Tonton Lagi</>
) : (
<>Tandai Selesai <ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" /></>
)}
</Button>
</div>
</div>
</AuthenticatedLayout>
</CourseLayout>
);
}

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

View File

@@ -1,4 +1,4 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { PageProps } from '@/types';
import { Head } from '@inertiajs/react';
import DeleteUserForm from './Partials/DeleteUserForm';
@@ -10,7 +10,7 @@ export default function Edit({
status,
}: PageProps<{ mustVerifyEmail: boolean; status?: string }>) {
return (
<AuthenticatedLayout
<DashboardLayout
header={
<h2 className="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Profile
@@ -38,6 +38,6 @@ export default function Edit({
</div>
</div>
</div>
</AuthenticatedLayout>
</DashboardLayout>
);
}

View File

@@ -1,4 +1,4 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { Head, Link } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
@@ -12,7 +12,7 @@ interface SrsStats {
export default function SrsIndex({ stats }: { stats: SrsStats }) {
return (
<AuthenticatedLayout
<DashboardLayout
header={
<h2 className="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Vocabulary SRS
@@ -83,6 +83,6 @@ export default function SrsIndex({ stats }: { stats: SrsStats }) {
</div>
</div>
</div>
</AuthenticatedLayout>
</DashboardLayout>
);
}

View File

@@ -1,4 +1,4 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import DashboardLayout from '@/Layouts/DashboardLayout';
import { Head, router } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card } from '@/Components/ui/card';
@@ -32,7 +32,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
if (!isFlipped && !isFinished) {
setIsFlipped(true);
if (currentItem?.audio_url) {
new Audio(currentItem.audio_url).play().catch(() => {});
new Audio(currentItem.audio_url).play().catch(() => { });
}
}
}, [isFlipped, isFinished, currentItem]);
@@ -42,7 +42,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
// Optimistic UI update
const itemToSubmit = currentItem;
// Submit in background
router.post(route('srs.store'), {
vocabulary_id: itemToSubmit.id,
@@ -68,7 +68,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (isFinished) return;
if (e.code === 'Space') {
e.preventDefault();
if (!isFlipped) handleFlip();
@@ -85,7 +85,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
}, [handleFlip, handleGrade, isFinished, isFlipped]);
return (
<AuthenticatedLayout header={
<DashboardLayout header={
<div className="flex items-center justify-between w-full">
<div className="flex flex-col">
<h2 className="text-2xl font-black tracking-tight text-foreground">
@@ -119,9 +119,9 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
</div>
<h3 className="text-3xl font-black text-foreground mb-3">Otsukaresama! 🎉</h3>
<p className="text-muted-foreground font-medium mb-10">Sesi ulasan Anda telah selesai dengan luar biasa.</p>
<Button
onClick={() => router.visit(route('srs.index'))}
size="lg"
<Button
onClick={() => router.visit(route('srs.index'))}
size="lg"
className="w-full rounded-[1.5rem] h-14 text-lg font-bold bg-primary hover:bg-primary/90 shadow-xl shadow-primary/20 hover:scale-105 active:scale-95 transition-all"
>
Kembali ke Dashboard
@@ -131,7 +131,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
) : (
<div className="w-full max-w-2xl px-4 flex flex-col items-center">
{/* Flashcard Area */}
<div
<div
className="w-full aspect-[16/10] sm:aspect-[16/9] perspective-2000 cursor-pointer group mb-12"
onClick={handleFlip}
>
@@ -147,7 +147,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
{currentItem.type === 'new' ? 'New Discovery' : 'Active Review'}
</span>
</div>
<div className="flex flex-col items-center gap-4">
<h1 className="text-7xl md:text-9xl font-black text-foreground tracking-tighter drop-shadow-sm">
{currentItem.word}
@@ -162,14 +162,14 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
{/* Back Side */}
<Card className="absolute w-full h-full backface-hidden rotate-y-180 flex flex-col items-center justify-center p-12 bg-card/60 backdrop-blur-3xl border border-primary/20 rounded-[3rem] shadow-2xl">
<div className="absolute top-8 left-1/2 -translate-x-1/2 border-b-4 border-primary/30 w-12 rounded-full" />
<div className="flex flex-col items-center text-center space-y-6 w-full">
<h2 className="text-5xl md:text-6xl font-black text-primary tracking-tight">
{currentItem.reading}
</h2>
<div className="h-px w-24 bg-border/50" />
<div className="space-y-2">
<p className="text-3xl md:text-4xl text-foreground font-black tracking-tight">
{currentItem.meaning}
@@ -178,13 +178,13 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
</div>
{currentItem.audio_url && (
<Button
variant="secondary"
size="lg"
<Button
variant="secondary"
size="lg"
onClick={(e) => {
e.stopPropagation();
new Audio(currentItem.audio_url!).play();
}}
}}
className="mt-6 rounded-2xl h-14 w-14 p-0 bg-primary/10 text-primary hover:bg-primary hover:text-white transition-all shadow-lg shadow-primary/5"
>
<Volume2 size={24} />
@@ -206,9 +206,9 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
{ val: 3, label: 'Good', desc: 'Bagus', color: 'blue' },
{ val: 4, label: 'Easy', desc: 'Mudah', color: 'green' }
].map((btn) => (
<Button
<Button
key={btn.val}
variant="outline"
variant="outline"
className={cn(
"flex-1 min-w-[120px] h-20 flex flex-col gap-1 rounded-2xl border-2 transition-all hover:-translate-y-1",
btn.color === 'red' && "border-red-500/20 bg-red-500/5 hover:bg-red-500/10 text-red-600 hover:border-red-500/40",
@@ -223,7 +223,7 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
</Button>
))}
</div>
{/* Keyboard Tips */}
<div className={cn(
"mt-8 text-center transition-opacity duration-1000",
@@ -237,6 +237,6 @@ export default function SrsPractice({ items }: { items: ReviewItem[] }) {
)}
</AnimatePresence>
</div>
</AuthenticatedLayout>
</DashboardLayout>
);
}