Files
SMF/app/js/app.js
Sterister 8933e9501d 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>
2026-05-29 18:44:00 +02:00

180 lines
6.3 KiB
JavaScript

// =====================================================
// App — router, sidebar, theme toggle
// =====================================================
const view = document.getElementById('view');
const crumbCurrent = document.getElementById('crumbCurrent');
function parseRoute() {
const hash = location.hash.replace(/^#\/?/, '');
if (!hash || hash === '/') return { name: 'home' };
const parts = hash.split('/').filter(Boolean);
if (parts[0] === 'uke' && parts[1]) return { name: 'uke', weekId: parseInt(parts[1], 10) };
if (parts[0] === 'tema' && parts[1]) return { name: 'tema', temaId: parts[1] };
if (parts[0] === 'tldr') return { name: 'tldr' };
if (parts[0] === 'flashcards') return { name: 'flashcards' };
if (parts[0] === 'quiz') return { name: 'quiz' };
if (parts[0] === 'eksamen') return { name: 'eksamen' };
return { name: 'home' };
}
async function route() {
const r = parseRoute();
view.innerHTML = '';
let content;
let crumb = 'Oversikt';
switch (r.name) {
case 'home':
content = SMF.renderHome();
crumb = 'Oversikt';
break;
case 'uke':
content = await SMF.renderLesson(r.weekId);
const w = SMF.WEEKS.find(x => x.id === r.weekId);
crumb = w ? `Uke ${w.id} · ${w.title}` : 'Ukjent uke';
break;
case 'tema':
content = await SMF.renderTema(r.temaId);
const t = SMF.getTheme(r.temaId);
crumb = t ? t.label : 'Tema';
break;
case 'tldr':
content = await SMF.renderTldr();
crumb = 'tl;dr · i farta';
break;
case 'flashcards':
content = document.getElementById('t-flashcards').content.cloneNode(true);
crumb = 'Flashcards';
break;
case 'quiz':
content = document.getElementById('t-quiz').content.cloneNode(true);
crumb = 'Selvtest';
break;
case 'eksamen':
content = document.getElementById('t-eksamen').content.cloneNode(true);
crumb = 'Eksamenstrener';
break;
}
view.appendChild(content);
crumbCurrent.textContent = crumb;
// After DOM is in place, init relevant mode
if (r.name === 'flashcards') await SMF.fcInit();
else if (r.name === 'quiz') await SMF.quizInit();
else if (r.name === 'eksamen') await SMF.examInit();
updateActiveLinks(r);
closeSidebar();
window.scrollTo(0, 0);
}
function updateActiveLinks(r) {
document.querySelectorAll('.sidebar__link').forEach(a => {
a.classList.remove('sidebar__link--active');
});
if (r.name === 'home') {
document.querySelector('.sidebar__link[data-route="home"]')?.classList.add('sidebar__link--active');
} else if (r.name === 'tldr') {
document.querySelector('.sidebar__link[data-route="tldr"]')?.classList.add('sidebar__link--active');
} else if (r.name === 'flashcards') {
document.querySelector('.sidebar__link[data-route="flashcards"]')?.classList.add('sidebar__link--active');
} else if (r.name === 'quiz') {
document.querySelector('.sidebar__link[data-route="quiz"]')?.classList.add('sidebar__link--active');
} else if (r.name === 'eksamen') {
document.querySelector('.sidebar__link[data-route="eksamen"]')?.classList.add('sidebar__link--active');
} else if (r.name === 'uke') {
document.querySelector(`.sidebar__link[data-week="${r.weekId}"]`)?.classList.add('sidebar__link--active');
} else if (r.name === 'tema') {
// No specific active link for tema (could be added)
}
}
function renderWeekNav() {
const nav = document.getElementById('weekNav');
if (!nav) return;
nav.innerHTML = SMF.WEEKS.map(w => {
return `
<a href="#/uke/${w.id}" data-week="${w.id}" class="sidebar__link">
<span class="sidebar__link-num">${String(w.id).padStart(2, '0')}</span>
<span>${w.title}</span>
</a>
`;
}).join('');
}
// ============= Sidebar (mobile) =============
function openSidebar() {
document.getElementById('sidebar').classList.add('sidebar--open');
document.getElementById('sidebarScrim').classList.add('sidebar-scrim--open');
}
function closeSidebar() {
document.getElementById('sidebar').classList.remove('sidebar--open');
document.getElementById('sidebarScrim').classList.remove('sidebar-scrim--open');
}
document.getElementById('sidebarToggle')?.addEventListener('click', () => {
if (document.getElementById('sidebar').classList.contains('sidebar--open')) closeSidebar();
else openSidebar();
});
document.getElementById('sidebarScrim')?.addEventListener('click', closeSidebar);
// ============= Theme toggle =============
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
localStorage.setItem('smf-theme', theme);
// Update icon
const icon = document.getElementById('themeIcon');
if (theme === 'dark') {
icon.innerHTML = `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>`;
} else {
icon.innerHTML = `<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>`;
}
}
function initTheme() {
// dark default; only switch to light if explicitly saved
const saved = localStorage.getItem('smf-theme');
applyTheme(saved === 'light' ? 'light' : 'dark');
}
document.getElementById('themeBtn').addEventListener('click', () => {
const current = document.documentElement.dataset.theme;
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// ============= Countdown =============
function updateCountdown() {
const el = document.getElementById('examCountdown');
if (!el) return;
const c = SMF.daysToExam();
el.textContent = c.label;
}
// ============= Init =============
async function init() {
initTheme();
renderWeekNav();
updateCountdown();
SMF.attachSearch();
// Sidebar brand link
document.querySelectorAll('[data-route="home"]').forEach(a => {
a.addEventListener('click', (e) => {
e.preventDefault();
location.hash = '';
});
});
// Initial route
await route();
// Build search index in background
setTimeout(() => SMF.buildSearchIndex().catch(console.warn), 500);
}
window.addEventListener('hashchange', route);
window.addEventListener('DOMContentLoaded', init);
if (document.readyState !== 'loading') init();