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:
125
app/js/eksamen.js
Normal file
125
app/js/eksamen.js
Normal 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;
|
||||
Reference in New Issue
Block a user