'use client'; // Learner course catalog — the user-end home. Lists every published // courseware from the registry; clicking a card opens the learner player // (/learn/[coursewareId] → static playback + AI assistant). // // Read path only (GET /api/coursewares) — zero LLM, matches the "high-quality // original courseware, directly consumable" value proposition of the 3-tier // design. import { useCallback, useEffect, useState } from 'react'; import { BookOpen, ChevronRight, GraduationCap, Languages, Layers, ListChecks, RefreshCw, Sparkles, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useI18n } from '@/lib/hooks/use-i18n'; import { useRouter } from 'next/navigation'; export interface CatalogCourseware { coursewareId: string; version: number; title: string; language?: string; status: string; publishedAt: string; contentHash: string; sceneCount: number; quizSceneCount: number; bundleUrl: string; } /** Published large-course (large-course mode) — learned module by module. */ export interface CatalogCourse { courseId: string; version: number; title: string; summary: string; moduleCount: number; publishedAt: string; } export default function LearnCatalogPage() { const { t } = useI18n(); const router = useRouter(); const [items, setItems] = useState([]); const [courses, setCourses] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { const [coursewaresResponse, coursesResponse] = await Promise.all([ fetch('/api/coursewares'), fetch('/api/learn/courses'), ]); const coursewaresBody = (await coursewaresResponse.json()) as { items?: CatalogCourseware[] }; const coursesBody = (await coursesResponse.json()) as { items?: CatalogCourse[] }; if ( !coursewaresResponse.ok || !Array.isArray(coursewaresBody.items) || !coursesResponse.ok || !Array.isArray(coursesBody.items) ) { throw new Error('HTTP error'); } setItems(coursewaresBody.items); setCourses(coursesBody.items); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const formatDate = (iso: string) => new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }); return (
{/* Hero */}
{t('learner.catalogBrand')}

{t('learner.catalogTitle')}

{t('learner.catalogDesc')}

{/* Toolbar */}
{t('learner.catalogCount', { count: items.length })}
{loading && (

{t('learner.loading')}

)} {error && (
{t('learner.loadFailed')}: {error}
)} {!loading && !error && items.length === 0 && courses.length === 0 && (

{t('learner.catalogEmpty')}

)} {/* Large courses (large-course mode) — learned module by module */} {courses.length > 0 && (

{t('course.learner.catalogCourses')}

{courses.map((course) => ( ))}
)} {items.length > 0 && (

{t('course.learner.catalogCoursewares')}

)} {/* Course cards */}
{items.map((item) => ( ))}
); }