// Quiz.jsx — motor de cuestionarios del curso. window.Quiz const { useState: useStateQ, useMemo: useMemoQ } = React; function shuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function sample(bank, n) { return shuffle(bank).slice(0, Math.min(n, bank.length)); } function gradeOne(p, ans) { if (p.type === 'vf') return ans === p.correcta; if (p.type === 'abierta_num') { if (ans === '' || ans === null || ans === undefined) return false; const v = parseFloat(String(ans).replace(',', '.')); if (isNaN(v)) return false; const tol = p.tolerancia || 0; return Math.abs(v - p.correcta) <= tol + 1e-9; } return ans === p.correcta; // multiple → índice } function Quiz({ title, subtitle, bank, n, min, onResult, onClose }) { const [round, setRound] = useStateQ(0); const qs = useMemoQ(() => sample(bank, n), [round, bank, n]); const [answers, setAnswers] = useStateQ({}); const [graded, setGraded] = useStateQ(false); const [result, setResult] = useStateQ(null); const setAns = (i, v) => { if (graded) return; setAnswers(a => ({ ...a, [i]: v })); }; const submit = () => { let score = 0; qs.forEach((p, i) => { if (gradeOne(p, answers[i])) score++; }); const passed = score * 2 >= qs.length; // 50% de aciertos setGraded(true); setResult({ score, total: qs.length, passed }); if (onResult) onResult({ score, total: qs.length, passed }); document.querySelector('#quiz-top')?.scrollIntoView({ block: 'start' }); }; const retry = () => { setAnswers({}); setGraded(false); setResult(null); setRound(r => r + 1); window.scrollTo(0, 0); }; const answeredCount = Object.keys(answers).filter(k => answers[k] !== '' && answers[k] !== undefined && answers[k] !== null).length; const notaTxt = result ? (Math.round((result.score / result.total) * 100) / 10).toFixed(1).replace('.', ',') : ''; return (
{/* barra superior */}
{subtitle}
{title}
{qs.length} preguntas · aprobado al 50%
{/* resultado */} {graded && result && (
{result.passed ? '¡Aprobado!' : 'No superado'}
{result.score} de {result.total} aciertos · nota {notaTxt} · necesitas el 50% (≥ {Math.ceil(result.total / 2)} aciertos).
{!result.passed &&
Repasa la explicación y vuelve a intentarlo: saldrán preguntas distintas.
} {result.passed &&
Se ha desbloqueado el siguiente paso. Pulsa “Continuar”.
}
)} {qs.map((p, i) => { const ok = graded && gradeOne(p, answers[i]); const bad = graded && !ok; return (
{i + 1}.
{p.type === 'multiple' && (
{p.opciones.map((opt, oi) => { const sel = answers[i] === oi; const showCorrect = graded && oi === p.correcta; const showWrong = graded && sel && oi !== p.correcta; return ( ); })}
)} {p.type === 'vf' && (
{[['Verdadero', true], ['Falso', false]].map(([lab, val]) => { const sel = answers[i] === val; const showCorrect = graded && p.correcta === val; const showWrong = graded && sel && p.correcta !== val; return ( ); })}
)} {p.type === 'abierta_num' && ( setAns(i, e.target.value)} disabled={graded} placeholder="Tu respuesta…" style={{ width: 160, padding: '9px 12px', borderRadius: 'var(--r-md)', border: '1px solid', borderColor: ok ? 'var(--success)' : (bad ? 'var(--danger)' : 'var(--line)'), font: 'var(--type-body)', color: 'var(--ink)', background: 'var(--bg-surface)' }} /> )} {graded && (
{ok ? '✓ Correcto' : '✗ Incorrecto'} {p.explica && }
)}
); })} {/* acciones */}
{!graded && ( )} {graded && !result.passed && ( )} {graded && result.passed && ( )} {graded && ( )}
); } window.Quiz = Quiz;