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:
parent
bb3eed1e7d
commit
6de1c79fa4
3 changed files with 323 additions and 13 deletions
177
css/style.css
177
css/style.css
|
|
@ -460,6 +460,183 @@ button {
|
|||
|
||||
#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 {
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
<h2>PRINCE2 7 Foundation Quiz</h2>
|
||||
<p>Kies hoeveel vragen je wilt oefenen</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -70,6 +71,14 @@
|
|||
<button onclick="goStart()">Nieuwe ronde</button>
|
||||
<button onclick="retryWrong()" id="retry-btn" style="display:none">Fout gegane vragen</button>
|
||||
</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>
|
||||
|
|
|
|||
148
js/quiz.js
148
js/quiz.js
|
|
@ -11,8 +11,11 @@ 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;
|
||||
|
|
@ -66,7 +69,7 @@ function shuffleQuestion(q) {
|
|||
function initLangScreen() {
|
||||
if (LANG_OPTIONS.length === 1) {
|
||||
ALL_QUESTIONS = LANG_OPTIONS[0].data;
|
||||
initCountButtons();
|
||||
initStartScreen();
|
||||
showScreen('start');
|
||||
return;
|
||||
}
|
||||
|
|
@ -79,7 +82,7 @@ function initLangScreen() {
|
|||
btn.innerHTML = '<span>' + lang.flag + '</span><span>' + lang.label + '</span>';
|
||||
btn.onclick = () => {
|
||||
ALL_QUESTIONS = lang.data;
|
||||
initCountButtons();
|
||||
initStartScreen();
|
||||
showScreen('start');
|
||||
};
|
||||
grid.appendChild(btn);
|
||||
|
|
@ -89,6 +92,11 @@ function initLangScreen() {
|
|||
}
|
||||
|
||||
/* ===== Start screen ===== */
|
||||
function initStartScreen() {
|
||||
initCountButtons();
|
||||
initModeButtons();
|
||||
}
|
||||
|
||||
function initCountButtons() {
|
||||
const counts = [5, 10, 20, 60, 9999];
|
||||
const grid = document.getElementById('count-grid');
|
||||
|
|
@ -111,6 +119,26 @@ function initCountButtons() {
|
|||
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'));
|
||||
|
|
@ -121,10 +149,11 @@ function selectCount(n, btn) {
|
|||
function startQuiz() {
|
||||
const pool = shuffle(ALL_QUESTIONS.map(shuffleQuestion));
|
||||
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;
|
||||
updateTimerDisplay();
|
||||
showScreen('quiz');
|
||||
if (quizMode === 'exam') startTimer();
|
||||
renderQuestion();
|
||||
}
|
||||
|
||||
|
|
@ -149,23 +178,52 @@ function renderQuestion() {
|
|||
|
||||
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;
|
||||
stopTimer();
|
||||
answeredCount++;
|
||||
updateTimerDisplay();
|
||||
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');
|
||||
});
|
||||
if (chosen === q.correct) score++;
|
||||
else wrongIds.push(currentIdx);
|
||||
|
||||
const expDiv = document.getElementById('explanation');
|
||||
expDiv.style.display = 'block';
|
||||
|
|
@ -175,10 +233,6 @@ function handleAnswer(chosen) {
|
|||
q.explanation.map((e, i) =>
|
||||
'<p>' + e.replace(/^[A-D]\.\s*/, 'ABCD'[i] + '. ') + '</p>'
|
||||
).join('');
|
||||
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
nextBtn.style.display = 'inline-flex';
|
||||
nextBtn.textContent = currentIdx === sessionQ.length - 1 ? 'Bekijk resultaat' : 'Volgende vraag →';
|
||||
}
|
||||
|
||||
function nextQuestion() {
|
||||
|
|
@ -203,6 +257,75 @@ function showResult() {
|
|||
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() {
|
||||
|
|
@ -215,10 +338,11 @@ function goStart() {
|
|||
|
||||
function retryWrong() {
|
||||
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;
|
||||
updateTimerDisplay();
|
||||
showScreen('quiz');
|
||||
if (quizMode === 'exam') startTimer();
|
||||
renderQuestion();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue