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:
2026-05-29 18:44:00 +02:00
commit 8933e9501d
52 changed files with 8796 additions and 0 deletions

125
app/js/eksamen.js Normal file
View File

@@ -0,0 +1,125 @@
// =====================================================
// Eksamenstrener — practice questions with guided answers
// =====================================================
let examData = null;
let examFilter = 'all';
async function examLoad() {
if (examData) return examData;
try {
const res = await fetch('data/exam.json');
if (!res.ok) throw new Error('Not ready');
examData = await res.json();
} catch (e) {
console.warn('Eksamen-data ikke klar ennå:', e.message);
examData = [];
}
return examData;
}
// Normalize category names — agents sometimes used "samfunnsansvar"
function normalizeCat(cat) {
if (cat === 'samfunnsansvar') return 'samfunn';
return cat;
}
function examFilteredQuestions() {
if (examFilter === 'all') return examData;
return examData.filter(q => normalizeCat(q.category) === examFilter);
}
function examCategoryLabel(cat) {
return {
etikk: 'Oppgave I · Etikk',
baerekraft: 'Oppgave II · Bærekraft',
samfunn: 'Oppgave III · Samfunnsansvar',
case: 'Oppgave IV · Case-drøfting'
}[normalizeCat(cat)] || cat;
}
function examCategoryColor(cat) {
return {
etikk: 'var(--theme-etikk)',
baerekraft: 'var(--theme-baerekraft)',
samfunn: 'var(--theme-samfunn)',
case: 'var(--theme-verktoy)'
}[normalizeCat(cat)] || 'var(--accent)';
}
function examRender() {
const container = document.getElementById('examQuestions');
if (!container) return;
// Filter chips at top
container.innerHTML = `
<div class="fc-filters reveal" id="examFilters" style="margin-bottom:var(--sp-7)">
<button class="fc-filter ${examFilter==='all'?'fc-filter--active':''}" data-filter="all">Alle</button>
<button class="fc-filter ${examFilter==='etikk'?'fc-filter--active':''}" data-filter="etikk">Etikk</button>
<button class="fc-filter ${examFilter==='baerekraft'?'fc-filter--active':''}" data-filter="baerekraft">Bærekraft</button>
<button class="fc-filter ${examFilter==='samfunn'?'fc-filter--active':''}" data-filter="samfunn">Samfunnsansvar</button>
<button class="fc-filter ${examFilter==='case'?'fc-filter--active':''}" data-filter="case">Case</button>
</div>
<div id="examList"></div>
`;
const list = document.getElementById('examList');
const questions = examFilteredQuestions();
let counter = 0;
questions.forEach(q => {
counter++;
const num = String(counter).padStart(2, '0');
const color = examCategoryColor(q.category);
const div = document.createElement('div');
div.className = 'exam-q reveal';
div.innerHTML = `
<div class="exam-q__head">
<div class="exam-q__num" style="color:${color}">${num}</div>
<div class="exam-q__head-info">
<span class="exam-q__category">${examCategoryLabel(q.category)}</span>
<span class="exam-q__weight">${q.title || ''}</span>
</div>
</div>
<p class="exam-q__sub">${q.question}</p>
${q.checklist ? `
<h4 style="margin-top:var(--sp-5); font-family:var(--f-mono); font-size:var(--s-0); letter-spacing:0.15em; text-transform:uppercase; color:var(--muted); font-weight:600">Hva må svaret inneholde</h4>
<ul style="margin-top:var(--sp-2); padding-left:var(--sp-5)">
${q.checklist.map(c => `<li>${c}</li>`).join('')}
</ul>
` : ''}
<button class="exam-q__toggle" data-toggle>Vis veiledet svar ↓</button>
<div class="exam-q__reveal" style="display:none" data-answer>
<div class="exam-q__reveal-label">Veiledet svar</div>
<div class="markdown-content">${SMF.renderMarkdown(q.guidedAnswer || '')}</div>
${q.tips ? `<div style="margin-top:var(--sp-4); padding-top:var(--sp-3); border-top:1px solid var(--line); font-family:var(--f-display); font-style:italic; font-size:var(--s-2); color:var(--ink-2)"><strong style="font-family:var(--f-mono); font-size:0.6875rem; letter-spacing:0.15em; text-transform:uppercase; color:var(--accent); font-weight:600; font-style:normal; display:block; margin-bottom:6px">Tips</strong>${q.tips}</div>` : ''}
</div>
`;
list.appendChild(div);
});
// Attach toggles
list.querySelectorAll('[data-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
const reveal = btn.nextElementSibling;
const isOpen = reveal.style.display !== 'none';
reveal.style.display = isOpen ? 'none' : 'block';
btn.textContent = isOpen ? 'Vis veiledet svar ↓' : 'Skjul veiledet svar ↑';
});
});
// Attach filters
document.getElementById('examFilters').addEventListener('click', (e) => {
const f = e.target.closest('[data-filter]');
if (f) {
examFilter = f.dataset.filter;
examRender();
}
});
}
async function examInit() {
await examLoad();
examRender();
}
SMF.examInit = examInit;