import { useState, useEffect, useRef, useCallback } from 'react'; import { CheckSquare, Loader, AlertCircle, Trophy, ArrowRight, Clock, CheckCircle, XCircle } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { Link } from 'react-router-dom'; import Card from '../components/ui/Card'; import Button from '../components/ui/Button'; import Tag from '../components/ui/Tag'; import { useApp } from '../store/AppContext'; import { generateWeeklyQuiz, saveTestResult, getTestResult } from '../lib/testService'; import { getCurriculumWeek, getCurriculumCycle } from '../lib/curriculumService'; import { storage } from '../lib/storage'; const TIMER_SECONDS = 300; // 5 minutes function setQuizActive(userId, active) { if (!userId) return; if (active) storage.set(`quiz:active:${userId}`, true); else storage.remove(`quiz:active:${userId}`); try { window.dispatchEvent(new CustomEvent('respellion:quiz-state', { detail: { active } })); } catch { // ignore } } // ─── Helper: format mm:ss ────────────────────────────────── function formatTime(sec) { const m = Math.floor(sec / 60); const s = sec % 60; return `${m}:${s.toString().padStart(2, '0')}`; } const Testen = () => { const { state } = useApp(); const { currentUser, weekNumber } = state; const [phase, setPhase] = useState('intro'); // intro | loading | quiz | review | results const [quiz, setQuiz] = useState(null); const [answers, setAnswers] = useState({}); // { questionId: selectedIndex } const [currentQ, setCurrentQ] = useState(0); const [showFeedback, setShowFeedback] = useState(false); const [timeLeft, setTimeLeft] = useState(TIMER_SECONDS); const [result, setResult] = useState(null); const [error, setError] = useState(null); const timerRef = useRef(null); // ── Check for existing result ── useEffect(() => { if (currentUser) { const check = async () => { const existing = await getTestResult(currentUser.id, weekNumber); if (existing) { setResult(existing); setPhase('results'); } }; check(); } }, [currentUser, weekNumber]); // ── Timer ── const finishQuizRef = useRef(null); useEffect(() => { if (phase === 'quiz') { timerRef.current = setInterval(() => { setTimeLeft(prev => { if (prev <= 1) { clearInterval(timerRef.current); finishQuizRef.current?.(); return 0; } return prev - 1; }); }, 1000); } return () => clearInterval(timerRef.current); }, [phase]); // ── Clear quiz-active flag whenever we leave the quiz phase or unmount ── useEffect(() => { if (phase !== 'quiz' && currentUser) { setQuizActive(currentUser.id, false); } }, [phase, currentUser]); useEffect(() => { return () => { if (currentUser) setQuizActive(currentUser.id, false); }; }, [currentUser]); // ── Start quiz ── const startQuiz = async () => { setPhase('loading'); setError(null); try { const q = await generateWeeklyQuiz(currentUser.id, weekNumber); if (!q?.questions?.length) { throw new Error('No questions could be generated for this week. Please try again.'); } setQuiz(q); setCurrentQ(0); setAnswers({}); setShowFeedback(false); setTimeLeft(TIMER_SECONDS); setQuizActive(currentUser.id, true); setPhase('quiz'); } catch (e) { setError(e.message); setPhase('intro'); } }; // ── Select answer ── const selectAnswer = (questionId, optionIndex) => { if (showFeedback) return; // locked setAnswers(prev => ({ ...prev, [questionId]: optionIndex })); setShowFeedback(true); }; // ── Next question / finish ── const nextQuestion = () => { setShowFeedback(false); if (currentQ < quiz.questions.length - 1) { setCurrentQ(prev => prev + 1); } else { finishQuiz(); } }; // ── Finish quiz & score ── const finishQuiz = useCallback(async () => { clearInterval(timerRef.current); if (!quiz) return; const questions = quiz.questions; let score = 0; const breakdown = questions.map(q => { const selected = answers[q.id]; const correct = selected === q.correctIndex; if (correct) score++; return { questionId: q.id, question: q.question, topicLabel: q.topicLabel, selected, correctIndex: q.correctIndex, correct, explanation: q.explanation, options: q.options, }; }); const testResult = { score, total: questions.length, percentage: Math.round((score / questions.length) * 100), timeUsed: TIMER_SECONDS - timeLeft, completedAt: new Date().toISOString(), breakdown, }; const { pointsEarned } = await saveTestResult(currentUser.id, weekNumber, testResult); testResult.pointsEarned = pointsEarned; setResult(testResult); setPhase('results'); }, [quiz, answers, timeLeft, currentUser, weekNumber]); useEffect(() => { finishQuizRef.current = finishQuiz; }, [finishQuiz]); // ─── Intro / Start screen ──────────────────────────────── if (phase === 'intro') { return (

Theme Test

Cycle {getCurriculumCycle(weekNumber)} · Week {getCurriculumWeek(weekNumber)} of 26

Test your knowledge of this week's theme with 5 AI-generated questions. You have 5 minutes to complete the test. Good luck!

{error && (

{error}

)}
5 questions 5 minutes 20 pts max
); } // ─── Loading ────────────────────────────────────────────── if (phase === 'loading') { return (

AI is generating your test...

Preparing 5 questions based on your learning topics.

); } // ─── Results ────────────────────────────────────────────── if (phase === 'results' && result) { const isPerfect = result.percentage === 100; const isGood = result.percentage >= 70; return (
{/* Score Header */}
{result.percentage}%

{isPerfect ? 'Perfect score!' : isGood ? 'Well done!' : 'Keep learning!'}

You scored {result.score}/{result.total} in {formatTime(result.timeUsed)}. {result.pointsEarned && +{result.pointsEarned} pts}

{/* Summary Cards */}
{result.score}
Correct
{result.total - result.score}
Incorrect
{formatTime(result.timeUsed)}
Time Used
{/* Review toggle */}

Question Review

{/* Question breakdown */}
{result.breakdown.map((item, i) => (
{i + 1}

{item.question}

{item.topicLabel}

{item.options.map((opt, oi) => { const isCorrect = oi === item.correctIndex; const wasSelected = oi === item.selected; return (
{isCorrect && } {wasSelected && !isCorrect && } {opt}
); })}

{item.explanation}

))}
); } // ─── Active Quiz ────────────────────────────────────────── if (phase === 'quiz' && quiz) { const q = quiz.questions[currentQ]; // Safety guard — should never happen, but prevents a crash if questions array is empty if (!q) { return (

Something went wrong loading this question.

); } const selectedAnswer = answers[q.id]; const isCorrect = selectedAnswer === q.correctIndex; const progress = ((currentQ + (showFeedback ? 1 : 0)) / quiz.questions.length) * 100; const timerDanger = timeLeft < 60; return (
{/* Header bar */}
Question {currentQ + 1} of {quiz.questions.length} {formatTime(timeLeft)}
{/* Progress bar */}
{/* Question card */}
{q.topicLabel}
{q.question}
{/* Options */}
{q.options.map((option, oi) => { let optionClass = 'border-bg-warm bg-paper hover:border-teal/50 hover:bg-teal/5 cursor-pointer'; if (showFeedback) { if (oi === q.correctIndex) { optionClass = 'border-teal bg-teal/10 text-teal'; } else if (oi === selectedAnswer && !isCorrect) { optionClass = 'border-red-300 bg-red-50 text-red-700'; } else { optionClass = 'border-bg-warm text-fg-muted opacity-50'; } } else if (selectedAnswer === oi) { optionClass = 'border-teal bg-teal/5'; } return ( ); })}
{/* Feedback */} {showFeedback && (
{isCorrect ? : }

{isCorrect ? 'Correct!' : 'Incorrect'}

{q.explanation}

)}
); } return null; }; export default Testen;