/** * Quiz Page * Displays quiz questions and handles submission */ import React, { useEffect, useState, useMemo } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { ArrowLeft, ArrowRight, CheckCircle, XCircle, Clock, Award, Loader2, AlertCircle, Play, RotateCcw, Home, Trophy, Zap, } from 'lucide-react'; import { useEducationStore } from '../../../stores/educationStore'; import type { Quiz as QuizType, QuizQuestion } from '../../../types/education.types'; type QuizState = 'intro' | 'in_progress' | 'submitted'; export default function Quiz() { const { courseSlug, lessonId, quizId } = useParams<{ courseSlug: string; lessonId: string; quizId?: string; }>(); const navigate = useNavigate(); const { currentQuiz, currentAttempt, quizResult, loadingQuiz, submittingQuiz, error, fetchQuiz, startQuizAttempt, submitQuiz, resetQuizState, } = useEducationStore(); const [quizState, setQuizState] = useState('intro'); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [answers, setAnswers] = useState>(new Map()); const [timeRemaining, setTimeRemaining] = useState(null); const [startTime, setStartTime] = useState(null); // Load quiz useEffect(() => { if (lessonId) { fetchQuiz(lessonId); } return () => { resetQuizState(); }; }, [lessonId, fetchQuiz, resetQuizState]); // Timer useEffect(() => { if (quizState !== 'in_progress' || !currentQuiz?.timeLimitMinutes) return; const interval = setInterval(() => { if (startTime && currentQuiz.timeLimitMinutes) { const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000); const remaining = currentQuiz.timeLimitMinutes * 60 - elapsed; if (remaining <= 0) { handleSubmit(); } else { setTimeRemaining(remaining); } } }, 1000); return () => clearInterval(interval); }, [quizState, startTime, currentQuiz?.timeLimitMinutes]); const currentQuestion = useMemo(() => { if (!currentQuiz?.questions) return null; return currentQuiz.questions[currentQuestionIndex]; }, [currentQuiz, currentQuestionIndex]); const totalQuestions = currentQuiz?.questions?.length || 0; const answeredCount = answers.size; const progress = totalQuestions > 0 ? (answeredCount / totalQuestions) * 100 : 0; const handleStart = async () => { if (!currentQuiz) return; try { await startQuizAttempt(currentQuiz.id); setQuizState('in_progress'); setStartTime(new Date()); if (currentQuiz.timeLimitMinutes) { setTimeRemaining(currentQuiz.timeLimitMinutes * 60); } } catch (err) { console.error('Error starting quiz:', err); } }; const handleAnswer = (questionId: string, answer: string | string[]) => { setAnswers((prev) => { const next = new Map(prev); next.set(questionId, answer); return next; }); }; const handleSubmit = async () => { if (!currentAttempt) return; const answersArray = Array.from(answers.entries()).map(([questionId, answer]) => ({ questionId, answer, })); try { await submitQuiz(currentAttempt.id, answersArray); setQuizState('submitted'); } catch (err) { console.error('Error submitting quiz:', err); } }; const handleRetry = () => { setQuizState('intro'); setCurrentQuestionIndex(0); setAnswers(new Map()); setTimeRemaining(null); setStartTime(null); resetQuizState(); if (lessonId) { fetchQuiz(lessonId); } }; const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; // Loading state if (loadingQuiz) { return (
); } // Error state if (error || !currentQuiz) { return (

Quiz no encontrado

{error || 'El quiz no existe o no tienes acceso'}

Volver a la Lección
); } // Intro State if (quizState === 'intro') { return (

{currentQuiz.title}

{currentQuiz.description && (

{currentQuiz.description}

)}

{totalQuestions}

Preguntas

{currentQuiz.passingScore}%

Para aprobar

{currentQuiz.timeLimitMinutes && (

{currentQuiz.timeLimitMinutes}

Minutos

)} {currentQuiz.maxAttempts && (

{currentQuiz.maxAttempts}

Intentos máx.

)}
Volver a la Lección
); } // Results State if (quizState === 'submitted' && quizResult) { return (
{quizResult.passed ? ( ) : ( )}

{quizResult.passed ? 'Felicidades!' : 'Sigue intentando'}

{quizResult.passed ? 'Has aprobado el quiz exitosamente' : 'No alcanzaste el puntaje mínimo para aprobar'}

{quizResult.percentage.toFixed(0)}%

Tu puntaje

{quizResult.score}/{quizResult.maxScore}

Puntos

{quizResult.xpAwarded && quizResult.xpAwarded > 0 && (
+{quizResult.xpAwarded} XP ganados
)} {/* Question Results */} {quizResult.results && (

Resumen de respuestas:

{quizResult.results.map((result, index) => (
{result.isCorrect ? ( ) : ( )} Pregunta {index + 1}: {result.pointsEarned}/{result.maxPoints} pts
))}
)}
{!quizResult.passed && ( )} Volver a la Lección Ir al Curso
); } // In Progress State return (
{/* Header */}

{currentQuiz.title}

Pregunta {currentQuestionIndex + 1} de {totalQuestions}

{timeRemaining !== null && (
{formatTime(timeRemaining)}
)}
{answeredCount}/{totalQuestions} respondidas
{/* Progress Bar */}
{/* Question */}
{currentQuestion && ( handleAnswer(currentQuestion.id, answer)} /> )}
{/* Navigation */}
{/* Question Dots */}
{Array.from({ length: totalQuestions }, (_, i) => { const questionId = currentQuiz.questions[i]?.id; const isAnswered = questionId && answers.has(questionId); const isCurrent = i === currentQuestionIndex; return ( ); })}
{currentQuestionIndex < totalQuestions - 1 ? ( ) : ( )}
); } // Question Card Component interface QuestionCardProps { question: QuizQuestion; answer?: string | string[]; onAnswer: (answer: string | string[]) => void; } function QuestionCard({ question, answer, onAnswer }: QuestionCardProps) { const handleOptionClick = (optionId: string) => { if (question.questionType === 'multiple_answer') { const currentAnswers = Array.isArray(answer) ? answer : []; if (currentAnswers.includes(optionId)) { onAnswer(currentAnswers.filter((a) => a !== optionId)); } else { onAnswer([...currentAnswers, optionId]); } } else { onAnswer(optionId); } }; const isOptionSelected = (optionId: string) => { if (Array.isArray(answer)) { return answer.includes(optionId); } return answer === optionId; }; return (

{question.questionText}

{question.questionType === 'short_answer' ? ( onAnswer(e.target.value)} placeholder="Escribe tu respuesta..." className="w-full px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:border-purple-500 focus:outline-none" /> ) : (
{question.options?.map((option) => ( ))}
)} {question.questionType === 'multiple_answer' && (

Selecciona todas las respuestas correctas

)}
); }