first commit

This commit is contained in:
2026-01-23 17:28:21 +07:00
commit 29ff8992b9
331 changed files with 30545 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import InputError from '@/Components/InputError';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
export default function ConfirmPassword() {
const { data, setData, post, processing, errors, reset } = useForm({
password: '',
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('password.confirm'), {
onFinish: () => reset('password'),
});
};
return (
<GuestLayout>
<Head title="Confirm Password" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md bg-white p-8 rounded-3xl shadow-2xl border border-gray-100">
<div className="mb-6 text-sm text-gray-600 dark:text-gray-400">
This is a secure area of the application. Please confirm your
password before continuing.
</div>
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
name="password"
value={data.password}
className="block w-full"
onChange={(e) => setData('password', e.target.value)}
/>
<InputError message={errors.password} />
</div>
<div className="flex items-center justify-end">
<Button className="w-full bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold py-3 rounded-xl" disabled={processing}>
Confirm
</Button>
</div>
</form>
</div>
</div>
</GuestLayout>
);
}

View File

@@ -0,0 +1,63 @@
import InputError from '@/Components/InputError';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
export default function ForgotPassword({ status }: { status?: string }) {
const { data, setData, post, processing, errors } = useForm({
email: '',
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('password.email'));
};
return (
<GuestLayout>
<Head title="Forgot Password" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md bg-white p-8 rounded-3xl shadow-2xl border border-gray-100">
<div className="mb-6 text-sm text-gray-600 dark:text-gray-400">
Forgot your password? No problem. Just let us know your email
address and we will email you a password reset link that will
allow you to choose a new one.
</div>
{status && (
<div className="mb-4 text-sm font-medium text-green-600 dark:text-green-400">
{status}
</div>
)}
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
value={data.email}
className="block w-full"
onChange={(e) => setData('email', e.target.value)}
/>
<InputError message={errors.email} />
</div>
<div className="flex items-center justify-end">
<Button className="w-full bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold py-3 rounded-xl" disabled={processing}>
Email Password Reset Link
</Button>
</div>
</form>
</div>
</div>
</GuestLayout>
);
}

View File

@@ -0,0 +1,162 @@
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Label } from '@/Components/ui/label';
import { Input } from '@/Components/ui/input';
import { Button } from '@/Components/ui/button';
import { Checkbox } from '@/Components/ui/checkbox';
import InputError from '@/Components/InputError';
export default function Login({
status,
canResetPassword,
}: {
status?: string;
canResetPassword: boolean;
}) {
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('login'), {
onFinish: () => reset('password'),
});
};
return (
<GuestLayout>
<Head title="Log in" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<Card className="w-full max-w-md border-gray-100 shadow-2xl rounded-3xl overflow-hidden">
<CardHeader className="space-y-1 text-center pt-8">
<CardTitle className="text-3xl font-extrabold tracking-tight">Selamat Datang Kembali</CardTitle>
<CardDescription className="text-gray-500">
Masuk ke akun NihonBuzz Academy anda
</CardDescription>
</CardHeader>
<CardContent className="pb-8">
{status && (
<div className="mb-4 text-sm font-medium text-green-600">
{status}
</div>
)}
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
value={data.email}
className="block w-full"
autoComplete="username"
placeholder="nama@email.com"
onChange={(e) => setData('email', e.target.value)}
/>
<InputError message={errors.email} />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
{canResetPassword && (
<Link
href={route('password.request')}
className="text-xs text-nihonbuzz-red hover:underline font-medium"
>
Lupa password?
</Link>
)}
</div>
<Input
id="password"
type="password"
name="password"
value={data.password}
className="block w-full"
autoComplete="current-password"
onChange={(e) => setData('password', e.target.value)}
/>
<InputError message={errors.password} />
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="remember"
checked={data.remember}
onCheckedChange={(checked) => setData('remember', checked as boolean)}
/>
<Label htmlFor="remember" className="text-sm text-gray-600 font-normal cursor-pointer">
Ingat saya
</Label>
</div>
<div className="pt-2">
<Button className="w-full py-6 rounded-2xl text-base font-bold shadow-lg shadow-nihonbuzz-red/20 bg-nihonbuzz-red hover:bg-nihonbuzz-red/90" disabled={processing}>
Masuk Sekarang
</Button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-100" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-gray-500 font-medium">Atau masuk dengan</span>
</div>
</div>
<div className="grid grid-cols-1 gap-3">
<Button
type="button"
variant="outline"
className="w-full py-6 rounded-2xl text-sm font-bold border-gray-100 hover:bg-gray-50 flex items-center justify-center gap-3 transition-all"
onClick={() => window.location.href = route('social.redirect', { provider: 'google' })}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Google
</Button>
</div>
<div className="text-center text-sm text-gray-500">
Belum punya akun?{' '}
<Link
href={route('register')}
className="text-nihonbuzz-red font-bold hover:underline"
>
Daftar Gratis
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
</GuestLayout>
);
}

View File

@@ -0,0 +1,124 @@
import InputError from '@/Components/InputError';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Label } from '@/Components/ui/label';
import { Input } from '@/Components/ui/input';
import { Button } from '@/Components/ui/button';
export default function Register() {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('register'), {
onFinish: () => reset('password', 'password_confirmation'),
});
};
return (
<GuestLayout>
<Head title="Register" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<Card className="w-full max-w-md border-gray-100 shadow-2xl rounded-3xl overflow-hidden">
<CardHeader className="space-y-1 text-center pt-8">
<CardTitle className="text-3xl font-extrabold tracking-tight">Buat Akun Baru</CardTitle>
<CardDescription className="text-gray-500">
Mulai petualangan belajarmu hari ini
</CardDescription>
</CardHeader>
<CardContent className="pb-8">
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Nama Lengkap</Label>
<Input
id="name"
name="name"
value={data.name}
className="block w-full"
autoComplete="name"
placeholder="Nama Lengkap Anda"
onChange={(e) => setData('name', e.target.value)}
required
/>
<InputError message={errors.name} />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
value={data.email}
className="block w-full"
autoComplete="username"
placeholder="nama@email.com"
onChange={(e) => setData('email', e.target.value)}
required
/>
<InputError message={errors.email} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
name="password"
value={data.password}
className="block w-full"
autoComplete="new-password"
onChange={(e) => setData('password', e.target.value)}
required
/>
<InputError message={errors.password} />
</div>
<div className="space-y-2">
<Label htmlFor="password_confirmation">Konfirmasi</Label>
<Input
id="password_confirmation"
type="password"
name="password_confirmation"
value={data.password_confirmation}
className="block w-full"
autoComplete="new-password"
onChange={(e) => setData('password_confirmation', e.target.value)}
required
/>
<InputError message={errors.password_confirmation} />
</div>
</div>
<div className="pt-2">
<Button className="w-full py-6 rounded-2xl text-base font-bold shadow-lg shadow-nihonbuzz-red/20 bg-nihonbuzz-red hover:bg-nihonbuzz-red/90" disabled={processing}>
Daftar Sekarang
</Button>
</div>
<div className="text-center text-sm text-gray-500">
Sudah punya akun?{' '}
<Link
href={route('login')}
className="text-nihonbuzz-red font-bold hover:underline"
>
Masuk Saja
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
</GuestLayout>
);
}

View File

@@ -0,0 +1,92 @@
import InputError from '@/Components/InputError';
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
export default function ResetPassword({
token,
email,
}: {
token: string;
email: string;
}) {
const { data, setData, post, processing, errors, reset } = useForm({
token: token,
email: email,
password: '',
password_confirmation: '',
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('password.store'), {
onFinish: () => reset('password', 'password_confirmation'),
});
};
return (
<GuestLayout>
<Head title="Reset Password" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md bg-white p-8 rounded-3xl shadow-2xl border border-gray-100">
<h2 className="text-2xl font-bold mb-6 text-center">Reset Password</h2>
<form onSubmit={submit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
value={data.email}
className="block w-full"
autoComplete="username"
onChange={(e) => setData('email', e.target.value)}
/>
<InputError message={errors.email} />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
name="password"
value={data.password}
className="block w-full"
autoComplete="new-password"
onChange={(e) => setData('password', e.target.value)}
/>
<InputError message={errors.password} />
</div>
<div className="space-y-2">
<Label htmlFor="password_confirmation">Confirm Password</Label>
<Input
type="password"
name="password_confirmation"
value={data.password_confirmation}
className="block w-full"
autoComplete="new-password"
onChange={(e) => setData('password_confirmation', e.target.value)}
/>
<InputError message={errors.password_confirmation} />
</div>
<div className="flex items-center justify-end">
<Button className="w-full bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold py-3 rounded-xl" disabled={processing}>
Reset Password
</Button>
</div>
</form>
</div>
</div>
</GuestLayout>
);
}

View File

@@ -0,0 +1,53 @@
import GuestLayout from '@/Layouts/GuestLayout';
import { Head, Link, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Button } from '@/Components/ui/button';
export default function VerifyEmail({ status }: { status?: string }) {
const { post, processing } = useForm({});
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('verification.send'));
};
return (
<GuestLayout>
<Head title="Email Verification" />
<div className="flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md bg-white p-8 rounded-3xl shadow-2xl border border-gray-100 text-center">
<div className="mb-6 text-sm text-gray-600 dark:text-gray-400">
Thanks for signing up! Before getting started, could you verify
your email address by clicking on the link we just emailed to
you? If you didn't receive the email, we will gladly send you
another.
</div>
{status === 'verification-link-sent' && (
<div className="mb-6 text-sm font-medium text-green-600 dark:text-green-400">
A new verification link has been sent to the email address
you provided during registration.
</div>
)}
<form onSubmit={submit} className="space-y-4">
<Button className="w-full bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold py-3 rounded-xl" disabled={processing}>
Resend Verification Email
</Button>
<Link
href={route('logout')}
method="post"
as="button"
className="text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100"
>
Log Out
</Link>
</form>
</div>
</div>
</GuestLayout>
);
}

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

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

View File

@@ -0,0 +1,186 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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';
interface DashboardProps {
stats: {
xp_points: number;
current_streak: number;
active_courses: number;
certificates: number;
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;
};
}
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' }
}: 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 };
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>
}
>
<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="border-border/50 bg-card/50 backdrop-blur-sm">
<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="border-border/50 bg-card/50 backdrop-blur-sm">
<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="border-border/50 bg-card/50 backdrop-blur-sm">
<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="border-border/50 bg-card/50 backdrop-blur-sm">
<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 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>
</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" />
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}

View File

@@ -0,0 +1,43 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { PageProps } from '@/types';
import { Head } from '@inertiajs/react';
import DeleteUserForm from './Partials/DeleteUserForm';
import UpdatePasswordForm from './Partials/UpdatePasswordForm';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm';
export default function Edit({
mustVerifyEmail,
status,
}: PageProps<{ mustVerifyEmail: boolean; status?: string }>) {
return (
<AuthenticatedLayout
header={
<h2 className="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Profile
</h2>
}
>
<Head title="Profile" />
<div className="py-12">
<div className="mx-auto max-w-7xl space-y-6 sm:px-6 lg:px-8">
<div className="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800">
<UpdateProfileInformationForm
mustVerifyEmail={mustVerifyEmail}
status={status}
className="max-w-xl"
/>
</div>
<div className="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800">
<UpdatePasswordForm className="max-w-xl" />
</div>
<div className="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800">
<DeleteUserForm className="max-w-xl" />
</div>
</div>
</div>
</AuthenticatedLayout>
);
}

View File

@@ -0,0 +1,117 @@
import InputError from '@/Components/InputError';
import { useForm } from '@inertiajs/react';
import { FormEventHandler, useRef, useState } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog"
export default function DeleteUserForm({
className = '',
}: {
className?: string;
}) {
const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false);
const passwordInput = useRef<HTMLInputElement>(null);
const {
data,
setData,
delete: destroy,
processing,
reset,
errors,
clearErrors,
} = useForm({
password: '',
});
const confirmUserDeletion = () => {
setConfirmingUserDeletion(true);
};
const deleteUser: FormEventHandler = (e) => {
e.preventDefault();
destroy(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.current?.focus(),
onFinish: () => reset(),
});
};
const closeModal = () => {
setConfirmingUserDeletion(false);
clearErrors();
reset();
};
return (
<section className={`space-y-6 ${className}`}>
<header>
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Delete Account
</h2>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Once your account is deleted, all of its resources and data
will be permanently deleted. Before deleting your account,
please download any data or information that you wish to
retain.
</p>
</header>
<Button variant="destructive" onClick={confirmUserDeletion}>
Delete Account
</Button>
<Dialog open={confirmingUserDeletion} onOpenChange={setConfirmingUserDeletion}>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources and
data will be permanently deleted. Please enter your
password to confirm you would like to permanently delete
your account.
</DialogDescription>
</DialogHeader>
<form onSubmit={deleteUser} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password" className="sr-only">Password</Label>
<Input
id="password"
type="password"
name="password"
ref={passwordInput}
value={data.password}
onChange={(e) => setData('password', e.target.value)}
className="block w-full"
placeholder="Password"
/>
<InputError message={errors.password} />
</div>
<DialogFooter>
<Button type="button" variant="secondary" onClick={closeModal}>
Cancel
</Button>
<Button variant="destructive" disabled={processing}>
Delete Account
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</section>
);
}

View File

@@ -0,0 +1,124 @@
import InputError from '@/Components/InputError';
import { Transition } from '@headlessui/react';
import { useForm } from '@inertiajs/react';
import { FormEventHandler, useRef } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
export default function UpdatePasswordForm({
className = '',
}: {
className?: string;
}) {
const passwordInput = useRef<HTMLInputElement>(null);
const currentPasswordInput = useRef<HTMLInputElement>(null);
const {
data,
setData,
errors,
put,
reset,
processing,
recentlySuccessful,
} = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const updatePassword: FormEventHandler = (e) => {
e.preventDefault();
put(route('password.update'), {
preserveScroll: true,
onSuccess: () => reset(),
onError: (errors) => {
if (errors.password) {
reset('password', 'password_confirmation');
passwordInput.current?.focus();
}
if (errors.current_password) {
reset('current_password');
currentPasswordInput.current?.focus();
}
},
});
};
return (
<section className={className}>
<header>
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Update Password
</h2>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Ensure your account is using a long, random password to stay
secure.
</p>
</header>
<form onSubmit={updatePassword} className="mt-6 space-y-6 max-w-xl">
<div className="space-y-2">
<Label htmlFor="current_password">Current Password</Label>
<Input
id="current_password"
ref={currentPasswordInput}
value={data.current_password}
onChange={(e) => setData('current_password', e.target.value)}
type="password"
className="block w-full"
autoComplete="current-password"
/>
<InputError message={errors.current_password} />
</div>
<div className="space-y-2">
<Label htmlFor="password">New Password</Label>
<Input
id="password"
ref={passwordInput}
value={data.password}
onChange={(e) => setData('password', e.target.value)}
type="password"
className="block w-full"
autoComplete="new-password"
/>
<InputError message={errors.password} />
</div>
<div className="space-y-2">
<Label htmlFor="password_confirmation">Confirm Password</Label>
<Input
id="password_confirmation"
value={data.password_confirmation}
onChange={(e) => setData('password_confirmation', e.target.value)}
type="password"
className="block w-full"
autoComplete="new-password"
/>
<InputError message={errors.password_confirmation} />
</div>
<div className="flex items-center gap-4">
<Button disabled={processing} className="bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold">Save</Button>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
Saved.
</p>
</Transition>
</div>
</form>
</section>
);
}

View File

@@ -0,0 +1,114 @@
import InputError from '@/Components/InputError';
import { Transition } from '@headlessui/react';
import { Link, useForm, usePage } from '@inertiajs/react';
import { FormEventHandler } from 'react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
export default function UpdateProfileInformation({
mustVerifyEmail,
status,
className = '',
}: {
mustVerifyEmail: boolean;
status?: string;
className?: string;
}) {
const user = usePage().props.auth.user;
const { data, setData, patch, errors, processing, recentlySuccessful } =
useForm({
name: user.name,
email: user.email,
});
const submit: FormEventHandler = (e) => {
e.preventDefault();
patch(route('profile.update'));
};
return (
<section className={className}>
<header>
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Profile Information
</h2>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Update your account's profile information and email address.
</p>
</header>
<form onSubmit={submit} className="mt-6 space-y-6 max-w-xl">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
required
autoComplete="name"
className="block w-full"
/>
<InputError message={errors.name} />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
required
autoComplete="username"
className="block w-full"
/>
<InputError message={errors.email} />
</div>
{mustVerifyEmail && user.email_verified_at === null && (
<div>
<p className="mt-2 text-sm text-gray-800 dark:text-gray-200">
Your email address is unverified.
<Link
href={route('verification.send')}
method="post"
as="button"
className="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
>
Click here to re-send the verification email.
</Link>
</p>
{status === 'verification-link-sent' && (
<div className="mt-2 text-sm font-medium text-green-600 dark:text-green-400">
A new verification link has been sent to your
email address.
</div>
)}
</div>
)}
<div className="flex items-center gap-4">
<Button disabled={processing} className="bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 text-white font-bold">Save</Button>
<Transition
show={recentlySuccessful}
enter="transition ease-in-out"
enterFrom="opacity-0"
leave="transition ease-in-out"
leaveTo="opacity-0"
>
<p className="text-sm text-gray-600 dark:text-gray-400">
Saved.
</p>
</Transition>
</div>
</form>
</section>
);
}

View File

@@ -0,0 +1,88 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { BookOpen, Brain, TrendingUp } from 'lucide-react';
interface SrsStats {
due: number;
new: number;
total_learned: number;
}
export default function SrsIndex({ stats }: { stats: SrsStats }) {
return (
<AuthenticatedLayout
header={
<h2 className="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Vocabulary SRS
</h2>
}
>
<Head title="Vocabulary Flashcards" />
<div className="py-12">
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
{/* Due Reviews Card */}
<Card className="bg-orange-50 border-orange-200">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-orange-900">
Review Due
</CardTitle>
<Brain className="h-4 w-4 text-orange-600" />
</CardHeader>
<CardContent>
<div className="text-4xl font-bold text-orange-700">{stats.due}</div>
<p className="text-xs text-orange-600 mt-1">Cards ready to review</p>
</CardContent>
</Card>
{/* New Cards Card */}
<Card className="bg-blue-50 border-blue-200">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-blue-900">
New Cards
</CardTitle>
<BookOpen className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-4xl font-bold text-blue-700">{stats.new}</div>
<p className="text-xs text-blue-600 mt-1">Ready to learn</p>
</CardContent>
</Card>
{/* Total Learned Card */}
<Card className="bg-green-50 border-green-200">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-green-900">
Total Learned
</CardTitle>
<TrendingUp className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-4xl font-bold text-green-700">{stats.total_learned}</div>
<p className="text-xs text-green-600 mt-1">Vocabulary mastered</p>
</CardContent>
</Card>
</div>
<div className="flex justify-center">
{stats.due + stats.new > 0 ? (
<Link href={route('srs.practice')}>
<Button size="lg" className="w-full md:w-auto px-12 py-6 text-lg font-bold bg-nihonbuzz-red hover:bg-nihonbuzz-red/90 shadow-xl shadow-nihonbuzz-red/20 rounded-2xl transition-all hover:scale-105 active:scale-95">
Start Session ({stats.due + stats.new})
</Button>
</Link>
) : (
<div className="text-center p-12 bg-gray-50 rounded-3xl border border-gray-100">
<h3 className="text-xl font-bold text-gray-800 mb-2">All Caught Up! 🎉</h3>
<p className="text-gray-500">You have no cards due for review. Come back later!</p>
</div>
)}
</div>
</div>
</div>
</AuthenticatedLayout>
);
}

View File

@@ -0,0 +1,242 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, router } from '@inertiajs/react';
import { Button } from '@/Components/ui/button';
import { Card } from '@/Components/ui/card';
import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Check, X, Volume2, RotateCw } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Progress } from '@/Components/ui/progress';
interface ReviewItem {
type: 'review' | 'new';
id: string; // Vocabulary ID
srs_id?: string;
word: string;
reading: string;
meaning: string;
audio_url?: string;
}
export default function SrsPractice({ items }: { items: ReviewItem[] }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isFlipped, setIsFlipped] = useState(false);
const [sessionQueue, setSessionQueue] = useState(items);
const [completedCount, setCompletedCount] = useState(0);
const currentItem = sessionQueue[currentIndex];
const isFinished = !currentItem;
const progress = items.length > 0 ? (completedCount / items.length) * 100 : 0;
const handleFlip = useCallback(() => {
if (!isFlipped && !isFinished) {
setIsFlipped(true);
if (currentItem?.audio_url) {
new Audio(currentItem.audio_url).play().catch(() => {});
}
}
}, [isFlipped, isFinished, currentItem]);
const handleGrade = useCallback((grade: number) => {
if (!isFlipped || isFinished) return;
// Optimistic UI update
const itemToSubmit = currentItem;
// Submit in background
router.post(route('srs.store'), {
vocabulary_id: itemToSubmit.id,
grade: grade
}, {
preserveScroll: true,
preserveState: true, // Keep our local state intact
onSuccess: () => {
// Determine if we should requeue (Again/1) or remove
// For simplicity in this version, we move to next regardless,
// but real apps might requeue failed cards.
}
});
// Move to next
setIsFlipped(false);
setCompletedCount(prev => prev + 1);
setCurrentIndex(prev => prev + 1);
}, [isFlipped, isFinished, currentItem]);
// Keyboard Shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (isFinished) return;
if (e.code === 'Space') {
e.preventDefault();
if (!isFlipped) handleFlip();
} else if (isFlipped) {
if (e.key === '1') handleGrade(1);
if (e.key === '2') handleGrade(2);
if (e.key === '3') handleGrade(3);
if (e.key === '4') handleGrade(4);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleFlip, handleGrade, isFinished, isFlipped]);
return (
<AuthenticatedLayout 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">
SRS Practice
</h2>
<p className="text-xs text-muted-foreground font-medium">Power up your Japanese memory</p>
</div>
<div className="w-1/3 lg:w-1/4">
<div className="flex justify-between items-end mb-1.5">
<span className="text-[10px] font-bold uppercase tracking-widest text-primary">Progress</span>
<span className="text-[10px] font-bold text-muted-foreground">{completedCount} / {items.length}</span>
</div>
<Progress value={progress} className="h-1.5 bg-primary/10" />
</div>
</div>
}>
<Head title="SRS Practice" />
<div className="max-w-4xl mx-auto w-full py-8 lg:py-12 flex flex-col items-center min-h-[70vh]">
<AnimatePresence mode="wait">
{isFinished ? (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="text-center w-full max-w-md"
>
<Card className="bg-card/40 backdrop-blur-2xl border-border/40 p-12 rounded-[3rem] shadow-2xl flex flex-col items-center relative overflow-hidden">
<div className="absolute top-0 inset-x-0 h-2 bg-gradient-to-r from-transparent via-green-500 to-transparent opacity-50" />
<div className="h-24 w-24 bg-green-500/10 text-green-500 rounded-full flex items-center justify-center mb-8 ring-8 ring-green-500/5">
<Check size={48} strokeWidth={3} />
</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"
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
</Button>
</Card>
</motion.div>
) : (
<div className="w-full max-w-2xl px-4 flex flex-col items-center">
{/* Flashcard Area */}
<div
className="w-full aspect-[16/10] sm:aspect-[16/9] perspective-2000 cursor-pointer group mb-12"
onClick={handleFlip}
>
<motion.div
className="relative w-full h-full transform-style-3d shadow-2xl rounded-[3rem]"
animate={{ rotateY: isFlipped ? 180 : 0 }}
transition={{ type: "spring", stiffness: 150, damping: 25 }}
>
{/* Front Side */}
<Card className="absolute w-full h-full backface-hidden flex flex-col items-center justify-center p-12 bg-card/40 backdrop-blur-3xl border border-white/20 dark:border-white/5 rounded-[3rem] shadow-[0_32px_64px_-16px_rgba(0,0,0,0.1)]">
<div className="absolute top-8 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full bg-primary/10 border border-primary/20">
<span className="text-[10px] font-black text-primary uppercase tracking-[0.2em]">
{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}
</h1>
<div className="mt-8 flex items-center gap-2 text-muted-foreground/40 font-bold text-xs uppercase tracking-widest animate-pulse">
<span>Klik untuk Membalik</span>
<RotateCw className="w-3 h-3" />
</div>
</div>
</Card>
{/* 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}
</p>
<p className="text-sm font-medium text-muted-foreground/60 italic">Kosa Kata N5</p>
</div>
{currentItem.audio_url && (
<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} />
</Button>
)}
</div>
</Card>
</motion.div>
</div>
{/* Controls */}
<div className={cn(
"flex flex-wrap justify-center gap-4 w-full transition-all duration-500",
isFlipped ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 translate-y-8 pointer-events-none"
)}>
{[
{ val: 1, label: 'Again', desc: 'Lupakan', color: 'red' },
{ val: 2, label: 'Hard', desc: 'Sulit', color: 'orange' },
{ val: 3, label: 'Good', desc: 'Bagus', color: 'blue' },
{ val: 4, label: 'Easy', desc: 'Mudah', color: 'green' }
].map((btn) => (
<Button
key={btn.val}
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",
btn.color === 'orange' && "border-orange-500/20 bg-orange-500/5 hover:bg-orange-500/10 text-orange-600 hover:border-orange-500/40",
btn.color === 'blue' && "border-blue-500/20 bg-blue-500/5 hover:bg-blue-500/10 text-blue-600 hover:border-blue-500/40",
btn.color === 'green' && "border-green-500/20 bg-green-500/5 hover:bg-green-500/10 text-green-600 hover:border-green-500/40"
)}
onClick={() => handleGrade(btn.val)}
>
<span className="text-base font-black uppercase tracking-wider">{btn.label}</span>
<span className="text-[10px] font-bold opacity-60 uppercase tracking-tighter">{btn.desc}</span>
</Button>
))}
</div>
{/* Keyboard Tips */}
<div className={cn(
"mt-8 text-center transition-opacity duration-1000",
isFlipped ? "opacity-40" : "opacity-0"
)}>
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
Tips: Gunakan angka <span className="text-foreground">1 - 4</span> di keyboard Anda
</p>
</div>
</div>
)}
</AnimatePresence>
</div>
</AuthenticatedLayout>
);
}

View File

@@ -0,0 +1,13 @@
import { PageProps } from '@/types';
import { Head } from '@inertiajs/react';
import GuestLayout from '@/Layouts/GuestLayout';
import { Hero } from '@/Components/Landing/Hero';
export default function Welcome({ auth }: PageProps) {
return (
<GuestLayout>
<Head title="Ecosystem Belajar Bahasa Jepang Modern" />
<Hero />
</GuestLayout>
);
}