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:
179
app/js/app.js
Normal file
179
app/js/app.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// =====================================================
|
||||
// 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();
|
||||
Reference in New Issue
Block a user