// Parcours 2 — Interface examen (CRITIQUE) + soumission const ExamInterface = () => { const D = window.STUDYHUB_DATA; const questions = D.examQuestions; const [qIdx, setQIdx] = React.useState(0); const [answers, setAnswers] = React.useState({}); const [marked, setMarked] = React.useState({}); const [timeLeft, setTimeLeft] = React.useState(2700); // 45 min const [savedAt, setSavedAt] = React.useState(0); const [tabAlert, setTabAlert] = React.useState(false); const [showSubmitModal, setShowSubmitModal] = React.useState(false); const [transition, setTransition] = React.useState(null); React.useEffect(() => { const t = setInterval(() => setTimeLeft(x => Math.max(0, x - 8)), 1000); // accelerated for demo return () => clearInterval(t); }, []); React.useEffect(() => { const t = setInterval(() => setSavedAt(s => s + 1), 3000); return () => clearInterval(t); }, []); // simulate tab-change alert once React.useEffect(() => { const t = setTimeout(() => { setTabAlert(true); setTimeout(() => setTabAlert(false), 4500); }, 14000); return () => clearTimeout(t); }, []); const fmt = (s) => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; const timerColor = timeLeft < 60 ? '#EF4444' : timeLeft < 300 ? '#F59E0B' : '#6ECFB5'; const timerPulse = timeLeft < 60 ? { animation: 'pulse 1s infinite' } : {}; const q = questions[qIdx]; const goTo = (newIdx) => { if (newIdx === qIdx) return; setTransition(newIdx > qIdx ? 'right' : 'left'); setTimeout(() => { setQIdx(newIdx); setTransition(null); }, 180); }; const setAnswer = (val) => setAnswers({ ...answers, [q.id]: val }); // Question status const statusOf = (i) => { const qq = questions[i]; if (i === qIdx) return 'current'; if (marked[qq.id]) return 'marked'; if (answers[qq.id] !== undefined) return 'answered'; return 'unseen'; }; return (
{/* Tab alert */} {tabAlert && (
Sortie d'onglet détectée. Un rapport a été transmis à l'enseignant. Cette infraction est enregistrée.
)} {/* Header */}
Algorithmique — Complexité & récursivité
INF201 · Lucas Benhamou
Question {qIdx + 1} / {questions.length}
{fmt(timeLeft)}
{/* Main */}
{{ 'qcm-single': 'QCM simple', 'qcm-multi': 'QCM multiple', 'matching': 'Association', 'cloze': 'Texte à trous', 'essay': 'Question ouverte', 'short': 'Réponse courte', 'ordering': 'Ordonnancement' }[q.type]} Q{qIdx + 1} · {q.points} pt{q.points > 1 ? 's' : ''}

{q.text}

{/* Render by type */} {(q.type === 'qcm-multi' || q.type === 'qcm-single') && (
{q.options.map((opt, i) => { const sel = q.type === 'qcm-single' ? answers[q.id] === opt.id : (answers[q.id] || []).includes(opt.id); return (
{ if (q.type === 'qcm-single') setAnswer(opt.id); else { const cur = answers[q.id] || []; setAnswer(cur.includes(opt.id) ? cur.filter(x => x !== opt.id) : [...cur, opt.id]); } }} className={'exam-option' + (sel ? ' selected' : '')}>
{sel && }
{opt.text}
{String.fromCharCode(65 + i)}
); })}
)} {q.type === 'cloze' && (
{q.parts.map((p, i) => p.type === 'text' ? {p.value} : ( setAnswer({ ...(answers[q.id] || {}), [p.id]: e.target.value })}/> ))}
)} {q.type === 'ordering' && } {q.type === 'matching' && } {q.type === 'essay' && (