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>
350 lines
12 KiB
JavaScript
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);
|