Initial commit: SMF2290 studie-app for eksamen vår 2026
Komplett pensumstudie-app med: - 120 flashcards, 49 quiz-spørsmål, 16 eksamenstrener-oppgaver - Sammendrag av alle 12 forelesninger - tl;dr-side for siste-minutts pugging - Søk gjennom hele pensumet - Dark/light mode, mobile-vennlig Cross-platform launchere (Start.sh + Start.bat) med auto-detect og auto-install av HTTP-server. PowerShell-fallback for Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
225
app/js/quiz.js
Normal file
225
app/js/quiz.js
Normal file
@@ -0,0 +1,225 @@
|
||||
// =====================================================
|
||||
// Quiz — multiple choice with explanations
|
||||
// =====================================================
|
||||
|
||||
let quizData = null;
|
||||
let quizState = {
|
||||
filter: 'all',
|
||||
questions: [],
|
||||
current: 0,
|
||||
selected: null,
|
||||
answered: false,
|
||||
correct: 0,
|
||||
wrong: 0,
|
||||
finished: false
|
||||
};
|
||||
|
||||
async function quizLoad() {
|
||||
if (quizData) return quizData;
|
||||
try {
|
||||
const res = await fetch('data/quiz.json');
|
||||
quizData = await res.json();
|
||||
} catch (e) {
|
||||
console.error('Klarte ikke laste quiz:', e);
|
||||
quizData = [];
|
||||
}
|
||||
return quizData;
|
||||
}
|
||||
|
||||
function quizFilterQuestions(filter) {
|
||||
if (filter === 'all') return [...quizData];
|
||||
return quizData.filter(q => q.category === filter);
|
||||
}
|
||||
|
||||
function quizShuffle(arr) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function quizRenderStart() {
|
||||
const stage = document.getElementById('quizStage');
|
||||
const counts = {
|
||||
all: quizData.length,
|
||||
etikk: quizData.filter(q => q.category === 'etikk').length,
|
||||
baerekraft: quizData.filter(q => q.category === 'baerekraft').length,
|
||||
samfunn: quizData.filter(q => q.category === 'samfunn').length,
|
||||
verktoy: quizData.filter(q => q.category === 'verktoy').length
|
||||
};
|
||||
stage.innerHTML = `
|
||||
<header class="lesson-header reveal" style="text-align:center; border:none; padding-bottom:var(--sp-5)">
|
||||
<div class="lesson-header__num">Studiemodus</div>
|
||||
<h1 class="lesson-header__title"><em>Selvtest</em></h1>
|
||||
<div class="lesson-header__meta" style="justify-content:center"><span>Velg kategori — flervalg med forklaringer</span></div>
|
||||
</header>
|
||||
<div class="themes__grid reveal" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-4); margin-top: var(--sp-7)">
|
||||
<button class="theme-card" data-cat="all" style="--theme-color: var(--accent); --theme-color-bg: transparent; text-align: left">
|
||||
<div class="theme-card__eyebrow">Mix</div>
|
||||
<h3 class="theme-card__title">Alle spørsmål</h3>
|
||||
<div class="theme-card__foot"><span>${counts.all} spørsmål</span><span class="theme-card__arrow">Start →</span></div>
|
||||
</button>
|
||||
<button class="theme-card theme-card--etikk" data-cat="etikk">
|
||||
<div class="theme-card__eyebrow">I</div>
|
||||
<h3 class="theme-card__title"><em>Etikk</em></h3>
|
||||
<div class="theme-card__foot"><span>${counts.etikk} spørsmål</span><span class="theme-card__arrow">Start →</span></div>
|
||||
</button>
|
||||
<button class="theme-card theme-card--baerekraft" data-cat="baerekraft">
|
||||
<div class="theme-card__eyebrow">II</div>
|
||||
<h3 class="theme-card__title"><em>Bærekraft</em></h3>
|
||||
<div class="theme-card__foot"><span>${counts.baerekraft} spørsmål</span><span class="theme-card__arrow">Start →</span></div>
|
||||
</button>
|
||||
<button class="theme-card theme-card--samfunn" data-cat="samfunn">
|
||||
<div class="theme-card__eyebrow">III</div>
|
||||
<h3 class="theme-card__title">Samfunns<em>ansvar</em></h3>
|
||||
<div class="theme-card__foot"><span>${counts.samfunn} spørsmål</span><span class="theme-card__arrow">Start →</span></div>
|
||||
</button>
|
||||
<button class="theme-card theme-card--verktoy" data-cat="verktoy">
|
||||
<div class="theme-card__eyebrow">IV</div>
|
||||
<h3 class="theme-card__title"><em>Verktøy</em></h3>
|
||||
<div class="theme-card__foot"><span>${counts.verktoy} spørsmål</span><span class="theme-card__arrow">Start →</span></div>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
stage.querySelectorAll('[data-cat]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
quizStart(btn.dataset.cat);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function quizStart(category) {
|
||||
quizState.filter = category;
|
||||
quizState.questions = quizShuffle(quizFilterQuestions(category));
|
||||
quizState.current = 0;
|
||||
quizState.selected = null;
|
||||
quizState.answered = false;
|
||||
quizState.correct = 0;
|
||||
quizState.wrong = 0;
|
||||
quizState.finished = false;
|
||||
quizRenderCurrent();
|
||||
}
|
||||
|
||||
function quizRenderCurrent() {
|
||||
const stage = document.getElementById('quizStage');
|
||||
if (quizState.finished) return quizRenderResult();
|
||||
const total = quizState.questions.length;
|
||||
const q = quizState.questions[quizState.current];
|
||||
if (!q) return;
|
||||
const progress = ((quizState.current) / total) * 100;
|
||||
const letters = ['A', 'B', 'C', 'D', 'E'];
|
||||
|
||||
stage.innerHTML = `
|
||||
<div class="quiz-bar"><div class="quiz-bar__fill" style="width:${progress}%"></div></div>
|
||||
<div class="quiz-meta">
|
||||
<span><strong>${quizState.current + 1}</strong> / ${total}</span>
|
||||
<span>${categoryLabelQuiz(q.category)}</span>
|
||||
<span>Riktig: <strong>${quizState.correct}</strong></span>
|
||||
</div>
|
||||
<h2 class="quiz-question reveal">${q.question}</h2>
|
||||
<div class="quiz-options" id="quizOptions">
|
||||
${q.options.map((opt, i) => `
|
||||
<button class="quiz-option reveal" data-i="${i}">
|
||||
<span class="quiz-option__letter">${letters[i]}</span>
|
||||
<span>${opt}</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div id="quizExplanation"></div>
|
||||
<div class="quiz-actions">
|
||||
<button class="btn btn--ghost" id="quizCancel">Avslutt</button>
|
||||
<button class="btn btn--primary" id="quizNext" disabled>Neste →</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
stage.querySelectorAll('.quiz-option').forEach(opt => {
|
||||
opt.addEventListener('click', () => {
|
||||
if (quizState.answered) return;
|
||||
const i = parseInt(opt.dataset.i, 10);
|
||||
quizSelect(i);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('quizNext').addEventListener('click', quizAdvance);
|
||||
document.getElementById('quizCancel').addEventListener('click', () => {
|
||||
if (confirm('Avslutte testen?')) quizRenderStart();
|
||||
});
|
||||
}
|
||||
|
||||
function quizSelect(i) {
|
||||
const q = quizState.questions[quizState.current];
|
||||
quizState.selected = i;
|
||||
quizState.answered = true;
|
||||
const opts = document.querySelectorAll('.quiz-option');
|
||||
opts.forEach((opt, idx) => {
|
||||
opt.classList.add('quiz-option--disabled');
|
||||
if (idx === q.correct) opt.classList.add('quiz-option--correct');
|
||||
if (idx === i && i !== q.correct) opt.classList.add('quiz-option--wrong');
|
||||
if (idx === i) opt.classList.add('quiz-option--selected');
|
||||
});
|
||||
if (i === q.correct) quizState.correct++;
|
||||
else quizState.wrong++;
|
||||
|
||||
const exp = document.getElementById('quizExplanation');
|
||||
exp.innerHTML = `
|
||||
<div class="quiz-explanation reveal">
|
||||
<div class="quiz-explanation__label">${i === q.correct ? 'Riktig — ' : 'Forklaring — '}</div>
|
||||
<div>${q.explanation}</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('quizNext').disabled = false;
|
||||
}
|
||||
|
||||
function quizAdvance() {
|
||||
quizState.current++;
|
||||
quizState.selected = null;
|
||||
quizState.answered = false;
|
||||
if (quizState.current >= quizState.questions.length) {
|
||||
quizState.finished = true;
|
||||
}
|
||||
quizRenderCurrent();
|
||||
}
|
||||
|
||||
function quizRenderResult() {
|
||||
const stage = document.getElementById('quizStage');
|
||||
const total = quizState.questions.length;
|
||||
const pct = total ? Math.round((quizState.correct / total) * 100) : 0;
|
||||
let verdict, vColor;
|
||||
if (pct >= 90) { verdict = 'Fremragende.'; vColor = 'var(--theme-baerekraft)'; }
|
||||
else if (pct >= 80) { verdict = 'Meget god.'; vColor = 'var(--theme-baerekraft)'; }
|
||||
else if (pct >= 65) { verdict = 'Solid forståelse.'; vColor = 'var(--theme-samfunn)'; }
|
||||
else if (pct >= 50) { verdict = 'Greit, men trener videre.'; vColor = 'var(--theme-samfunn)'; }
|
||||
else { verdict = 'Repeter mer av dette temaet.'; vColor = 'var(--theme-etikk)'; }
|
||||
|
||||
stage.innerHTML = `
|
||||
<div class="quiz-result reveal">
|
||||
<div class="quiz-result__score">${quizState.correct}<span style="color:var(--muted)">/${total}</span></div>
|
||||
<div class="quiz-result__label">${pct} % riktig</div>
|
||||
<div class="quiz-result__verdict" style="color:${vColor}">${verdict}</div>
|
||||
<div class="quiz-actions" style="justify-content:center; margin-top: var(--sp-7)">
|
||||
<button class="btn" id="quizRestart">Test på nytt</button>
|
||||
<button class="btn btn--primary" id="quizMore">Andre kategori</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('quizRestart').addEventListener('click', () => quizStart(quizState.filter));
|
||||
document.getElementById('quizMore').addEventListener('click', () => quizRenderStart());
|
||||
}
|
||||
|
||||
function categoryLabelQuiz(cat) {
|
||||
return {
|
||||
etikk: 'Etikk',
|
||||
baerekraft: 'Bærekraft',
|
||||
samfunn: 'Samfunnsansvar',
|
||||
verktoy: 'Verktøy & implementering'
|
||||
}[cat] || cat;
|
||||
}
|
||||
|
||||
async function quizInit() {
|
||||
await quizLoad();
|
||||
quizRenderStart();
|
||||
}
|
||||
|
||||
SMF.quizInit = quizInit;
|
||||
Reference in New Issue
Block a user