mirror of
https://github.com/nihonbuzz/nihonbuzz-academy.git
synced 2026-01-26 13:32:07 +07:00
feat: Implement a new course learning system with dedicated layouts, lesson playback, and Spaced Repetition System (SRS) functionality.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user