p2/js/quiz.js
chriseo 6de1c79fa4 Add exam mode with post-quiz answer review
Adds a mode selector on the start screen:
- Directe feedback (existing): explanation shown after each question, timer pauses
- Examenmodus (new): all questions answered back-to-back with timer running, then a full review section on the result screen showing wrong answers (always) and correct answers (toggleable)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:17:26 +02:00

350 lines
12 KiB
JavaScript

/* ===== Language detection ===== */
const LANG_OPTIONS = [
{ key: 'nl', label: 'Nederlands', flag: '🇳🇱', data: window.QUESTIONS_NL },
{ key: 'en', label: 'English', flag: '🇬🇧', data: window.QUESTIONS_EN },
].filter(l => l.data && l.data.length > 0);
let ALL_QUESTIONS = [];
/* ===== Quiz state ===== */
let sessionQ = [];
let currentIdx = 0;
let score = 0;
let wrongIds = [];
let correctIds = [];
let chosenAnswers = []; // chosenAnswers[i] = chosen option index for question i
let selectedCount = 0;
let answered = false;
let quizMode = 'feedback'; // 'feedback' | 'exam'
let timerInterval = null;
let elapsedSeconds = 0;
let answeredCount = 0;
/* ===== Helpers ===== */
function shuffle(arr) {
const a = [...arr];
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 fmtTime(s) {
return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
}
function startTimer() {
stopTimer();
timerInterval = setInterval(() => { elapsedSeconds++; updateTimerDisplay(); }, 1000);
}
function stopTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
}
function updateTimerDisplay() {
const avg = answeredCount > 0 ? Math.round(elapsedSeconds / answeredCount) + 's' : '—';
document.getElementById('timer-display').textContent = fmtTime(elapsedSeconds) + ' | gem. ' + avg;
}
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function shuffleQuestion(q) {
const idx = [0, 1, 2, 3];
const si = shuffle(idx);
return {
exam: q.exam, qnum: q.qnum, q: q.q,
opts: si.map(i => q.opts[i]),
correct: si.indexOf(q.correct),
explanation: si.map(i => q.explanation[i]),
};
}
/* ===== Language screen ===== */
function initLangScreen() {
if (LANG_OPTIONS.length === 1) {
ALL_QUESTIONS = LANG_OPTIONS[0].data;
initStartScreen();
showScreen('start');
return;
}
const grid = document.getElementById('lang-grid');
grid.innerHTML = '';
LANG_OPTIONS.forEach(lang => {
const btn = document.createElement('button');
btn.className = 'lang-btn';
btn.innerHTML = '<span>' + lang.flag + '</span><span>' + lang.label + '</span>';
btn.onclick = () => {
ALL_QUESTIONS = lang.data;
initStartScreen();
showScreen('start');
};
grid.appendChild(btn);
});
showScreen('lang');
}
/* ===== Start screen ===== */
function initStartScreen() {
initCountButtons();
initModeButtons();
}
function initCountButtons() {
const counts = [5, 10, 20, 60, 9999];
const grid = document.getElementById('count-grid');
grid.innerHTML = '';
const total = ALL_QUESTIONS.length;
counts.forEach(n => {
const btn = document.createElement('button');
btn.className = 'count-btn';
const label = n === 9999 ? 'Alle ' + total : String(n);
btn.textContent = label;
btn.dataset.count = n;
const isDisabled = n !== 9999 && total < n;
btn.disabled = isDisabled;
btn.onclick = () => selectCount(n, btn);
grid.appendChild(btn);
});
document.getElementById('start-btn').disabled = true;
selectedCount = 0;
}
function initModeButtons() {
const modes = [
{ key: 'feedback', label: 'Directe feedback', desc: 'Antwoord zien na elke vraag' },
{ key: 'exam', label: 'Examenmodus', desc: 'Alle vragen eerst, daarna beoordeling' },
];
const grid = document.getElementById('mode-grid');
grid.innerHTML = '';
modes.forEach(m => {
const btn = document.createElement('button');
btn.className = 'mode-btn' + (m.key === quizMode ? ' selected' : '');
btn.innerHTML = '<span class="mode-label">' + m.label + '</span><span class="mode-desc">' + m.desc + '</span>';
btn.onclick = () => {
quizMode = m.key;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
};
grid.appendChild(btn);
});
}
function selectCount(n, btn) {
selectedCount = Math.min(n, ALL_QUESTIONS.length);
document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('start-btn').disabled = false;
}
function startQuiz() {
const pool = shuffle(ALL_QUESTIONS.map(shuffleQuestion));
sessionQ = pool.slice(0, selectedCount);
currentIdx = 0; score = 0; wrongIds = []; correctIds = []; chosenAnswers = []; answered = false;
elapsedSeconds = 0; answeredCount = 0;
updateTimerDisplay();
showScreen('quiz');
if (quizMode === 'exam') startTimer();
renderQuestion();
}
/* ===== Quiz screen ===== */
function renderQuestion() {
answered = false;
const q = sessionQ[currentIdx];
const total = sessionQ.length;
document.getElementById('progress-fill').style.width = ((currentIdx / total) * 100) + '%';
document.getElementById('q-meta').textContent = 'Vraag ' + (currentIdx + 1) + ' van ' + total;
document.getElementById('q-text').textContent = q.q;
const optDiv = document.getElementById('options');
optDiv.innerHTML = '';
['A', 'B', 'C', 'D'].forEach((ltr, i) => {
const btn = document.createElement('button');
btn.className = 'option-btn';
btn.innerHTML = '<span class="opt-letter">' + ltr + '.</span><span>' + q.opts[i] + '</span>';
btn.onclick = () => handleAnswer(i);
optDiv.appendChild(btn);
});
document.getElementById('explanation').style.display = 'none';
document.getElementById('next-btn').style.display = 'none';
if (quizMode === 'feedback') {
startTimer();
}
// In exam mode the timer is already running (started in startQuiz via first renderQuestion)
}
function handleAnswer(chosen) {
if (answered) return;
answered = true;
answeredCount++;
chosenAnswers[currentIdx] = chosen;
const q = sessionQ[currentIdx];
const isCorrect = chosen === q.correct;
if (isCorrect) { score++; correctIds.push(currentIdx); }
else { wrongIds.push(currentIdx); }
if (quizMode === 'feedback') {
stopTimer();
updateTimerDisplay();
showAnswerFeedback(q, chosen);
const nextBtn = document.getElementById('next-btn');
nextBtn.style.display = 'inline-flex';
nextBtn.textContent = currentIdx === sessionQ.length - 1 ? 'Bekijk resultaat' : 'Volgende vraag →';
} else {
// Exam mode: highlight answer, keep timer running, show minimal next
updateTimerDisplay();
document.querySelectorAll('.option-btn').forEach((btn, i) => {
btn.disabled = true;
if (i === q.correct) btn.classList.add('correct');
else if (i === chosen && !isCorrect) btn.classList.add('wrong');
});
const nextBtn = document.getElementById('next-btn');
nextBtn.style.display = 'inline-flex';
nextBtn.textContent = currentIdx === sessionQ.length - 1 ? 'Bekijk resultaat' : 'Volgende vraag →';
}
}
function showAnswerFeedback(q, chosen) {
document.querySelectorAll('.option-btn').forEach((btn, i) => {
btn.disabled = true;
if (i === q.correct) btn.classList.add('correct');
else if (i === chosen && chosen !== q.correct) btn.classList.add('wrong');
});
const expDiv = document.getElementById('explanation');
expDiv.style.display = 'block';
expDiv.innerHTML =
'<div class="explanation-source">Proefexamen ' + q.exam + ', vraag ' + q.qnum + '</div>' +
'<div class="explanation-label">Toelichting</div>' +
q.explanation.map((e, i) =>
'<p>' + e.replace(/^[A-D]\.\s*/, 'ABCD'[i] + '. ') + '</p>'
).join('');
}
function nextQuestion() {
currentIdx++;
if (currentIdx >= sessionQ.length) showResult();
else renderQuestion();
}
/* ===== Result screen ===== */
function showResult() {
stopTimer();
showScreen('result');
const total = sessionQ.length;
const pct = Math.round((score / total) * 100);
const circle = document.getElementById('score-circle');
circle.innerHTML = pct + '%<span>' + (pct >= 60 ? 'Geslaagd' : 'Niet geslaagd') + '</span>';
circle.className = 'score-circle ' + (pct >= 60 ? 'score-pass' : 'score-fail');
document.getElementById('result-label').textContent = pct >= 60 ? 'Geslaagd' : 'Niet geslaagd';
document.getElementById('result-sub').textContent = score + ' van de ' + total + ' vragen goed';
document.getElementById('stat-correct').textContent = score;
document.getElementById('stat-wrong').textContent = total - score;
document.getElementById('stat-time').textContent =
answeredCount > 0 ? Math.round(elapsedSeconds / answeredCount) + 's' : '—';
document.getElementById('retry-btn').style.display = wrongIds.length > 0 ? 'inline-flex' : 'none';
// In exam mode: render review section
const reviewSection = document.getElementById('review-section');
if (quizMode === 'exam') {
renderReview();
reviewSection.style.display = 'block';
} else {
reviewSection.style.display = 'none';
}
}
function renderReview() {
// Wrong questions (always shown)
const wrongContainer = document.getElementById('wrong-review');
wrongContainer.innerHTML = wrongIds.length === 0
? '<p class="review-empty">Geen fout beantwoorde vragen.</p>'
: wrongIds.map(i => buildReviewCard(sessionQ[i], chosenAnswers[i], true)).join('');
// Correct questions (hidden by default)
const correctContainer = document.getElementById('correct-review');
correctContainer.innerHTML = correctIds.length === 0
? '<p class="review-empty">Geen goed beantwoorde vragen.</p>'
: correctIds.map(i => buildReviewCard(sessionQ[i], chosenAnswers[i], false)).join('');
correctContainer.style.display = 'none';
// Toggle button
const toggleBtn = document.getElementById('toggle-correct-btn');
if (correctIds.length === 0) {
toggleBtn.style.display = 'none';
} else {
toggleBtn.style.display = 'inline-flex';
toggleBtn.textContent = 'Toon ook goed beantwoorde vragen (' + correctIds.length + ')';
toggleBtn.classList.remove('active');
}
}
function buildReviewCard(q, chosen, isWrong) {
const letters = 'ABCD';
const chosenLetter = letters[chosen];
const correctLetter = letters[q.correct];
const explanations = q.explanation.map((e, i) =>
'<p class="review-exp-item">' + e.replace(/^[A-D]\.\s*/, letters[i] + '. ') + '</p>'
).join('');
return '<div class="review-card review-card--' + (isWrong ? 'wrong' : 'correct') + '">' +
'<div class="review-card-head">' +
'<span class="review-badge review-badge--' + (isWrong ? 'wrong' : 'correct') + '">' +
(isWrong ? 'Fout' : 'Goed') +
'</span>' +
'<span class="review-qref">Proefexamen ' + q.exam + ', vraag ' + q.qnum + '</span>' +
'</div>' +
'<div class="review-question">' + q.q + '</div>' +
(isWrong
? '<div class="review-chosen">Jouw antwoord: ' + chosenLetter + '. ' + q.opts[chosen] + '</div>'
: '') +
'<div class="review-correct">Juist antwoord: ' + correctLetter + '. ' + q.opts[q.correct] + '</div>' +
'<div class="review-explanations">' + explanations + '</div>' +
'</div>';
}
function toggleCorrectReview() {
const container = document.getElementById('correct-review');
const btn = document.getElementById('toggle-correct-btn');
const visible = container.style.display !== 'none';
container.style.display = visible ? 'none' : 'block';
btn.textContent = visible
? 'Toon ook goed beantwoorde vragen (' + correctIds.length + ')'
: 'Verberg goed beantwoorde vragen';
btn.classList.toggle('active', !visible);
}
function goStart() {
stopTimer();
document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('selected'));
document.getElementById('start-btn').disabled = true;
selectedCount = 0;
showScreen('start');
}
function retryWrong() {
sessionQ = shuffle(wrongIds.map(i => shuffleQuestion(sessionQ[i])));
currentIdx = 0; score = 0; wrongIds = []; correctIds = []; chosenAnswers = []; answered = false;
elapsedSeconds = 0; answeredCount = 0;
updateTimerDisplay();
showScreen('quiz');
if (quizMode === 'exam') startTimer();
renderQuestion();
}
/* ===== Boot ===== */
document.addEventListener('DOMContentLoaded', initLangScreen);