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>
This commit is contained in:
chriseo 2026-06-18 13:17:26 +02:00
parent bb3eed1e7d
commit 6de1c79fa4
3 changed files with 323 additions and 13 deletions

View file

@ -460,6 +460,183 @@ button {
#retry-btn:hover { opacity: .9; } #retry-btn:hover { opacity: .9; }
/* ===== Mode selector ===== */
.mode-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.625rem;
max-width: 480px;
margin: 0 auto 2rem;
}
.mode-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
min-height: 64px;
padding: 0.75rem 0.75rem;
border: 2px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
box-shadow: var(--shadow);
transition: border-color .15s, background .15s;
cursor: pointer;
}
.mode-btn:hover {
border-color: var(--accent);
background: var(--accent-light);
}
.mode-btn.selected {
border-color: var(--accent);
background: var(--accent-light);
color: var(--accent);
}
.mode-label {
font-size: 0.9375rem;
font-weight: 600;
}
.mode-desc {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1.3;
}
.mode-btn.selected .mode-desc {
color: var(--accent);
opacity: .8;
}
/* ===== Review section (exam mode) ===== */
.review-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
margin: 2rem 0 1rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.review-header h3 {
font-size: 1rem;
font-weight: 700;
color: var(--text);
}
.toggle-correct-btn {
min-height: 36px;
padding: 0.375rem 0.875rem;
border-radius: var(--radius-sm);
border: 2px solid var(--border);
background: var(--surface);
color: var(--text-muted);
font-size: 0.8125rem;
font-weight: 500;
box-shadow: var(--shadow);
transition: border-color .15s, color .15s, background .15s;
}
.toggle-correct-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.toggle-correct-btn.active {
border-color: var(--accent);
background: var(--accent-light);
color: var(--accent);
}
.review-empty {
font-size: 0.875rem;
color: var(--text-muted);
text-align: center;
padding: 1rem 0;
}
.review-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.125rem;
margin-bottom: 0.75rem;
box-shadow: var(--shadow);
text-align: left;
}
.review-card--wrong { border-left: 3px solid var(--wrong-border); }
.review-card--correct { border-left: 3px solid var(--correct-border); }
.review-card-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.review-badge {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
.review-badge--wrong { background: var(--wrong-bg); color: var(--wrong-text); }
.review-badge--correct { background: var(--correct-bg); color: var(--correct-text); }
.review-qref {
font-size: 0.75rem;
color: var(--text-muted);
}
.review-question {
font-size: 0.9375rem;
font-weight: 500;
line-height: 1.5;
margin-bottom: 0.625rem;
}
.review-chosen {
font-size: 0.8125rem;
color: var(--wrong-text);
background: var(--wrong-bg);
border-radius: var(--radius-sm);
padding: 0.375rem 0.625rem;
margin-bottom: 0.375rem;
}
.review-correct {
font-size: 0.8125rem;
color: var(--correct-text);
background: var(--correct-bg);
border-radius: var(--radius-sm);
padding: 0.375rem 0.625rem;
margin-bottom: 0.75rem;
}
.review-explanations {
border-top: 1px solid var(--border);
padding-top: 0.625rem;
}
.review-exp-item {
font-size: 0.8125rem;
line-height: 1.6;
color: var(--text);
margin-bottom: 0.375rem;
}
.review-exp-item:last-child { margin-bottom: 0; }
/* ===== Copyright footer ===== */ /* ===== Copyright footer ===== */
.copyright { .copyright {
text-align: center; text-align: center;

View file

@ -25,6 +25,7 @@
<h2>PRINCE2 7 Foundation Quiz</h2> <h2>PRINCE2 7 Foundation Quiz</h2>
<p>Kies hoeveel vragen je wilt oefenen</p> <p>Kies hoeveel vragen je wilt oefenen</p>
<div class="count-grid" id="count-grid"></div> <div class="count-grid" id="count-grid"></div>
<div class="mode-grid" id="mode-grid"></div>
<button class="start-btn" onclick="startQuiz()" disabled id="start-btn">Start</button> <button class="start-btn" onclick="startQuiz()" disabled id="start-btn">Start</button>
</div> </div>
</div> </div>
@ -70,6 +71,14 @@
<button onclick="goStart()">Nieuwe ronde</button> <button onclick="goStart()">Nieuwe ronde</button>
<button onclick="retryWrong()" id="retry-btn" style="display:none">Fout gegane vragen</button> <button onclick="retryWrong()" id="retry-btn" style="display:none">Fout gegane vragen</button>
</div> </div>
<div id="review-section" style="display:none">
<div class="review-header">
<h3>Beoordeling</h3>
<button class="toggle-correct-btn" id="toggle-correct-btn" onclick="toggleCorrectReview()"></button>
</div>
<div id="wrong-review"></div>
<div id="correct-review"></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,8 +11,11 @@ let sessionQ = [];
let currentIdx = 0; let currentIdx = 0;
let score = 0; let score = 0;
let wrongIds = []; let wrongIds = [];
let correctIds = [];
let chosenAnswers = []; // chosenAnswers[i] = chosen option index for question i
let selectedCount = 0; let selectedCount = 0;
let answered = false; let answered = false;
let quizMode = 'feedback'; // 'feedback' | 'exam'
let timerInterval = null; let timerInterval = null;
let elapsedSeconds = 0; let elapsedSeconds = 0;
@ -66,7 +69,7 @@ function shuffleQuestion(q) {
function initLangScreen() { function initLangScreen() {
if (LANG_OPTIONS.length === 1) { if (LANG_OPTIONS.length === 1) {
ALL_QUESTIONS = LANG_OPTIONS[0].data; ALL_QUESTIONS = LANG_OPTIONS[0].data;
initCountButtons(); initStartScreen();
showScreen('start'); showScreen('start');
return; return;
} }
@ -79,7 +82,7 @@ function initLangScreen() {
btn.innerHTML = '<span>' + lang.flag + '</span><span>' + lang.label + '</span>'; btn.innerHTML = '<span>' + lang.flag + '</span><span>' + lang.label + '</span>';
btn.onclick = () => { btn.onclick = () => {
ALL_QUESTIONS = lang.data; ALL_QUESTIONS = lang.data;
initCountButtons(); initStartScreen();
showScreen('start'); showScreen('start');
}; };
grid.appendChild(btn); grid.appendChild(btn);
@ -89,6 +92,11 @@ function initLangScreen() {
} }
/* ===== Start screen ===== */ /* ===== Start screen ===== */
function initStartScreen() {
initCountButtons();
initModeButtons();
}
function initCountButtons() { function initCountButtons() {
const counts = [5, 10, 20, 60, 9999]; const counts = [5, 10, 20, 60, 9999];
const grid = document.getElementById('count-grid'); const grid = document.getElementById('count-grid');
@ -111,6 +119,26 @@ function initCountButtons() {
selectedCount = 0; 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) { function selectCount(n, btn) {
selectedCount = Math.min(n, ALL_QUESTIONS.length); selectedCount = Math.min(n, ALL_QUESTIONS.length);
document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('selected')); document.querySelectorAll('.count-btn').forEach(b => b.classList.remove('selected'));
@ -121,10 +149,11 @@ function selectCount(n, btn) {
function startQuiz() { function startQuiz() {
const pool = shuffle(ALL_QUESTIONS.map(shuffleQuestion)); const pool = shuffle(ALL_QUESTIONS.map(shuffleQuestion));
sessionQ = pool.slice(0, selectedCount); sessionQ = pool.slice(0, selectedCount);
currentIdx = 0; score = 0; wrongIds = []; answered = false; currentIdx = 0; score = 0; wrongIds = []; correctIds = []; chosenAnswers = []; answered = false;
elapsedSeconds = 0; answeredCount = 0; elapsedSeconds = 0; answeredCount = 0;
updateTimerDisplay(); updateTimerDisplay();
showScreen('quiz'); showScreen('quiz');
if (quizMode === 'exam') startTimer();
renderQuestion(); renderQuestion();
} }
@ -149,23 +178,52 @@ function renderQuestion() {
document.getElementById('explanation').style.display = 'none'; document.getElementById('explanation').style.display = 'none';
document.getElementById('next-btn').style.display = 'none'; document.getElementById('next-btn').style.display = 'none';
if (quizMode === 'feedback') {
startTimer(); startTimer();
}
// In exam mode the timer is already running (started in startQuiz via first renderQuestion)
} }
function handleAnswer(chosen) { function handleAnswer(chosen) {
if (answered) return; if (answered) return;
answered = true; answered = true;
stopTimer();
answeredCount++; answeredCount++;
updateTimerDisplay(); chosenAnswers[currentIdx] = chosen;
const q = sessionQ[currentIdx]; 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) => { document.querySelectorAll('.option-btn').forEach((btn, i) => {
btn.disabled = true; btn.disabled = true;
if (i === q.correct) btn.classList.add('correct'); if (i === q.correct) btn.classList.add('correct');
else if (i === chosen && chosen !== q.correct) btn.classList.add('wrong'); else if (i === chosen && chosen !== q.correct) btn.classList.add('wrong');
}); });
if (chosen === q.correct) score++;
else wrongIds.push(currentIdx);
const expDiv = document.getElementById('explanation'); const expDiv = document.getElementById('explanation');
expDiv.style.display = 'block'; expDiv.style.display = 'block';
@ -175,10 +233,6 @@ function handleAnswer(chosen) {
q.explanation.map((e, i) => q.explanation.map((e, i) =>
'<p>' + e.replace(/^[A-D]\.\s*/, 'ABCD'[i] + '. ') + '</p>' '<p>' + e.replace(/^[A-D]\.\s*/, 'ABCD'[i] + '. ') + '</p>'
).join(''); ).join('');
const nextBtn = document.getElementById('next-btn');
nextBtn.style.display = 'inline-flex';
nextBtn.textContent = currentIdx === sessionQ.length - 1 ? 'Bekijk resultaat' : 'Volgende vraag →';
} }
function nextQuestion() { function nextQuestion() {
@ -203,6 +257,75 @@ function showResult() {
document.getElementById('stat-time').textContent = document.getElementById('stat-time').textContent =
answeredCount > 0 ? Math.round(elapsedSeconds / answeredCount) + 's' : '—'; answeredCount > 0 ? Math.round(elapsedSeconds / answeredCount) + 's' : '—';
document.getElementById('retry-btn').style.display = wrongIds.length > 0 ? 'inline-flex' : 'none'; 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() { function goStart() {
@ -215,10 +338,11 @@ function goStart() {
function retryWrong() { function retryWrong() {
sessionQ = shuffle(wrongIds.map(i => shuffleQuestion(sessionQ[i]))); sessionQ = shuffle(wrongIds.map(i => shuffleQuestion(sessionQ[i])));
currentIdx = 0; score = 0; wrongIds = []; answered = false; currentIdx = 0; score = 0; wrongIds = []; correctIds = []; chosenAnswers = []; answered = false;
elapsedSeconds = 0; answeredCount = 0; elapsedSeconds = 0; answeredCount = 0;
updateTimerDisplay(); updateTimerDisplay();
showScreen('quiz'); showScreen('quiz');
if (quizMode === 'exam') startTimer();
renderQuestion(); renderQuestion();
} }