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