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 (
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}
AI is generating your test...
Preparing 5 questions based on your learning topics.
You scored {result.score}/{result.total} in {formatTime(result.timeUsed)}. {result.pointsEarned && +{result.pointsEarned} pts}
{item.question}
{item.topicLabel}
{item.explanation}
Something went wrong loading this question.
{isCorrect ? 'Correct!' : 'Incorrect'}
{q.explanation}