import { useEffect, useMemo, useState } from 'react'; import { animate, motion, MotionConfig, useReducedMotion } from 'motion/react'; import { FileText, HelpCircle, Gamepad2, Puzzle } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/hooks/use-i18n'; import { useStageStore } from '@/lib/store'; import type { Scene, SceneType } from '@/lib/types/stage'; import { summarizeScenes } from '@/lib/classroom/complete-summary'; import { readAnswersForSummary } from '@/lib/quiz/persistence'; const SCENE_TYPE_ICONS: Record = { slide: FileText, quiz: HelpCircle, interactive: Gamepad2, pbl: Puzzle, }; const TYPE_ORDER: SceneType[] = ['slide', 'quiz', 'interactive', 'pbl']; const CONFETTI_COLORS = [ '#fbbf24', '#f97316', '#ef4444', '#ec4899', '#a855f7', '#3b82f6', '#10b981', ]; function encouragementKey(pct: number): 'high' | 'mid' | 'low' { if (pct >= 90) return 'high'; if (pct >= 70) return 'mid'; return 'low'; } interface Particle { id: number; x: number; y: number; rotate: number; color: string; w: number; h: number; duration: number; delay: number; round: boolean; } function makeConfetti(count: number): Particle[] { const arr: Particle[] = []; for (let i = 0; i < count; i++) { const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.45; const distance = 180 + Math.random() * 280; const w = 6 + Math.random() * 6; arr.push({ id: i, x: Math.cos(angle) * distance, y: Math.sin(angle) * distance - 50, rotate: (Math.random() - 0.5) * 720, color: CONFETTI_COLORS[i % CONFETTI_COLORS.length], w, h: Math.random() > 0.3 ? w * 0.4 : w, duration: 1.0 + Math.random() * 0.9, delay: Math.random() * 0.12, round: Math.random() > 0.72, }); } return arr; } function Confetti() { const prefersReducedMotion = useReducedMotion(); const particles = useMemo(() => makeConfetti(55), []); if (prefersReducedMotion) return null; return (
{particles.map((p) => ( ))}
); } function Sparkle({ className }: { className?: string }) { return ( ); } function Sparkles() { const prefersReducedMotion = useReducedMotion(); const sparkles = useMemo( () => [ { x: -95, y: -40, size: 14, delay: 0.8, repeatDelay: 0.8 }, { x: 95, y: -20, size: 18, delay: 1.1, repeatDelay: 1.0 }, { x: -78, y: 55, size: 11, delay: 1.35, repeatDelay: 1.2 }, { x: 82, y: 62, size: 15, delay: 1.55, repeatDelay: 0.9 }, { x: 0, y: -88, size: 10, delay: 1.75, repeatDelay: 1.1 }, ], [], ); if (prefersReducedMotion) return null; return ( <> {sparkles.map((s, i) => ( ))} ); } function TrophySvg({ className }: { className?: string }) { return ( {/* Handles */} {/* Cup */} {/* Shine */} {/* Rim */} {/* Star */} {/* Stem */} {/* Base tiers */} ); } function AnimatedCounter({ value, delay = 0, duration = 0.9, }: { value: number; delay?: number; duration?: number; }) { const prefersReducedMotion = useReducedMotion(); const [display, setDisplay] = useState(0); useEffect(() => { if (prefersReducedMotion) return; const controls = animate(0, value, { delay, duration, ease: [0.16, 1, 0.3, 1], onUpdate: (v) => setDisplay(Math.round(v)), }); return () => controls.stop(); }, [value, delay, duration, prefersReducedMotion]); return <>{prefersReducedMotion ? value : display}; } function QuizRing({ pct, delay = 0 }: { pct: number; delay?: number }) { const prefersReducedMotion = useReducedMotion(); const radius = 34; const circumference = 2 * Math.PI * radius; return (
%
); } interface ClassroomCompletePageProps { readonly scenes: Scene[]; readonly title: string; } export function ClassroomCompletePage({ scenes, title }: ClassroomCompletePageProps) { const { t, locale } = useI18n(); const prefersReducedMotion = useReducedMotion(); // Computed once on mount: re-grading on every render would be wasteful and // the underlying localStorage values only change when the user revisits a // quiz scene (which unmounts this page). const summary = useMemo(() => summarizeScenes(scenes, readAnswersForSummary), [scenes]); const dateLabel = useMemo(() => { try { return new Intl.DateTimeFormat(locale).format(new Date()); } catch { return new Date().toLocaleDateString(); } }, [locale]); const trailItems = TYPE_ORDER.filter((type) => (summary.countsByType[type] ?? 0) > 0).map( (type) => ({ type, count: summary.countsByType[type] ?? 0, Icon: SCENE_TYPE_ICONS[type], label: t(`classroomComplete.trailLabels.${type}`), }), ); return (
{/* Single-shot announcement for screen readers — replaces the noisy outer aria-live region that used to wrap the live-updating counters. */} {t('classroomComplete.title')} {/* Base background */}
{/* Radial glow */} {/* Confetti */} {/* Content */}
{/* Trophy + halo + sparkles */}
{/* Ribbon */} {t('classroomComplete.title')} {/* Title + date */}

{title || t('classroomComplete.title')}

{dateLabel}

{/* Stats cards */} {trailItems.length > 0 && (
{trailItems.map(({ type, count, Icon, label }, idx) => { const cardDelay = 0.96 + idx * 0.08; return (
{label}
); })}
)} {/* Quiz card */} {summary.quiz && (
{t('classroomComplete.quizScoreLabel', { correct: summary.quiz.correct, total: summary.quiz.total, })}
{t(`classroomComplete.encouragement.${encouragementKey(summary.quiz.pct)}`)}
)}
); } export function ClassroomCompletePageConnected() { const stage = useStageStore((s) => s.stage); const scenes = useStageStore((s) => s.scenes); return ; }