Legg til Fast-track: kondensert læringsspor uten gjentakelse

Nytt studiemodus som samler hele pensum i 5 moduler etter
eksamensstrukturen, slik at hvert begrep læres én gang i stedet
for å gjentas på tvers av ukene. Hver modul har huskeregler,
konkrete eksempler og «eksamensfeller», med fremdriftsmåler
(lest-markering lagret i localStorage), modulkort og pager.

- notes/fast-track.md: innhold i 5 moduler med HTML-callouts
- data.js: FASTTRACK-moduler + getFastTrack()
- render.js: renderFastTrackHome/-Module + modul-ekstraktor
- app.js: ruter #/fast-track og #/fast-track/N
- index.html: templates, sidebar-lenke, forside-promo
- style.css: kort, callouts, fremdrift, promo

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:42:16 +02:00
parent 9a7a7b9ef1
commit 3f7f5d86b1
8 changed files with 764 additions and 2 deletions

View File

@@ -11,6 +11,9 @@ function parseRoute() {
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] === 'fast-track') {
return parts[1] ? { name: 'fasttrack-module', moduleId: parseInt(parts[1], 10) } : { name: 'fasttrack' };
}
if (parts[0] === 'tldr') return { name: 'tldr' };
if (parts[0] === 'flashcards') return { name: 'flashcards' };
if (parts[0] === 'quiz') return { name: 'quiz' };
@@ -39,6 +42,15 @@ async function route() {
const t = SMF.getTheme(r.temaId);
crumb = t ? t.label : 'Tema';
break;
case 'fasttrack':
content = await SMF.renderFastTrackHome();
crumb = 'Fast-track';
break;
case 'fasttrack-module':
content = await SMF.renderFastTrackModule(r.moduleId);
const m = SMF.getFastTrack(r.moduleId);
crumb = m ? `Fast-track · ${m.title}` : 'Fast-track';
break;
case 'tldr':
content = await SMF.renderTldr();
crumb = 'tl;dr · i farta';
@@ -78,6 +90,8 @@ function updateActiveLinks(r) {
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 === 'fasttrack' || r.name === 'fasttrack-module') {
document.querySelector('.sidebar__link[data-route="fasttrack"]')?.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') {

View File

@@ -122,6 +122,15 @@ const THEMES = {
}
};
// Fast-track — kondensert læringsspor, gjentakelsesfritt, etter eksamensstruktur
const FASTTRACK = [
{ id: 1, theme: 'etikk', tag: 'Oppgave I', title: 'Etikk — grunnmuren', mins: 8, desc: 'Moral vs. etikk vs. jus, dilemma-typene, og de seks prinsippene som er verktøykassen.' },
{ id: 2, theme: 'etikk', tag: 'Oppgave I', title: 'Etikk — teorier & verktøy', mins: 9, desc: 'De tre teoriene (Mill · Kant · Aristoteles), Kants imperativ og Kvalnes-hjulet.' },
{ id: 3, theme: 'baerekraft', tag: 'Oppgave II', title: 'Bærekraft', mins: 10, desc: 'Brundtland, Triple Bottom Line, tålegrenser, smultring, sirkulær økonomi og degrowth.' },
{ id: 4, theme: 'samfunn', tag: 'Oppgave III', title: 'Samfunnsansvar', mins: 12, desc: 'CSR vs. CSV, Carrolls pyramide, Friedman↔Freeman, Mitchell, eksternaliteter, sosial pilar.' },
{ id: 5, theme: 'verktoy', tag: 'Oppgave IV · Case', title: 'Verktøy, implementering & case', mins: 10, desc: 'Standarder, CSRD & dobbel vesentlighet, de fem stegene + ferdig case-oppskrift.' }
];
// Mapping from theme to color name
const THEME_COLORS = {
etikk: { color: '#D89AA2', soft: '#4A2027', bg: '#2A1A1D' },
@@ -151,9 +160,15 @@ function daysToExam() {
return { days, hours, label: days === 0 ? `${hours}t` : days === 1 ? '1 dag' : `${days} dager` };
}
function getFastTrack(id) {
return FASTTRACK.find(m => m.id === id);
}
window.SMF = window.SMF || {};
SMF.COURSE = COURSE;
SMF.WEEKS = WEEKS;
SMF.FASTTRACK = FASTTRACK;
SMF.getFastTrack = getFastTrack;
SMF.THEMES = THEMES;
SMF.THEME_COLORS = THEME_COLORS;
SMF.themeOf = themeOf;

View File

@@ -238,6 +238,149 @@ async function renderTema(temaId) {
return tpl;
}
// ============= Fast-track =============
const FT_PROGRESS_KEY = 'smf-fasttrack-done';
function ftGetDone() {
try { return new Set(JSON.parse(localStorage.getItem(FT_PROGRESS_KEY) || '[]')); }
catch { return new Set(); }
}
function ftSetDone(set) {
localStorage.setItem(FT_PROGRESS_KEY, JSON.stringify([...set]));
}
// Extract a single module from fast-track.md by its <!--MODULE:n--> sentinel
function extractFastTrackModule(markdown, moduleId) {
const startRe = new RegExp(`<!--MODULE:${moduleId}-->`);
const lines = markdown.split('\n');
let start = lines.findIndex(l => startRe.test(l));
if (start === -1) return null;
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (/<!--MODULE:\d+-->/.test(lines[i])) { end = i; break; }
}
let section = lines.slice(start + 1, end).join('\n').trim();
// Strip the leading "## ..." heading — it's shown in the module header
section = section.replace(/^##\s+[^\n]+\n+/, '');
return section;
}
async function renderFastTrackHome() {
const tpl = document.getElementById('t-fasttrack-home').content.cloneNode(true);
const grid = tpl.getElementById('ftGrid');
const done = ftGetDone();
const total = SMF.FASTTRACK.length;
const completed = SMF.FASTTRACK.filter(m => done.has(m.id)).length;
// Progress bar
const bar = tpl.getElementById('ftProgressBar');
if (bar) bar.style.width = `${Math.round((completed / total) * 100)}%`;
const count = tpl.getElementById('ftProgressCount');
if (count) count.textContent = `${completed} / ${total} moduler`;
SMF.FASTTRACK.forEach(m => {
const colors = SMF.THEME_COLORS[m.theme] || SMF.THEME_COLORS.all;
const isDone = done.has(m.id);
const a = document.createElement('a');
a.href = `#/fast-track/${m.id}`;
a.className = 'ft-card reveal' + (isDone ? ' ft-card--done' : '');
a.style.setProperty('--ft-color', colors.color);
a.style.setProperty('--ft-bg', colors.bg);
a.innerHTML = `
<div class="ft-card__top">
<span class="ft-card__num">${String(m.id).padStart(2, '0')}</span>
<span class="ft-card__check" aria-hidden="true">✓</span>
</div>
<div class="ft-card__eyebrow">${m.tag}</div>
<h3 class="ft-card__title">${m.title}</h3>
<p class="ft-card__desc">${m.desc}</p>
<div class="ft-card__foot">
<span class="ft-card__mins">~${m.mins} min</span>
<span class="ft-card__arrow">${isDone ? 'Repetér' : 'Start'} →</span>
</div>`;
grid.appendChild(a);
});
return tpl;
}
async function renderFastTrackModule(moduleId) {
const tpl = document.getElementById('t-fasttrack-module').content.cloneNode(true);
const module = SMF.getFastTrack(moduleId);
if (!module) {
tpl.getElementById('ftModNum').textContent = 'Ikke funnet';
tpl.getElementById('ftModTitle').textContent = 'Ukjent modul';
return tpl;
}
const colors = SMF.THEME_COLORS[module.theme] || SMF.THEME_COLORS.all;
const num = tpl.getElementById('ftModNum');
num.textContent = `Fast-track · Modul ${String(moduleId).padStart(2, '0')} · ${module.tag}`;
num.style.color = colors.color;
tpl.getElementById('ftModTitle').innerHTML = formatTitleWithEm(module.title);
const body = tpl.getElementById('ftModBody');
try {
const md = await loadNote('fast-track.md');
const section = extractFastTrackModule(md, moduleId);
body.innerHTML = section
? renderMarkdown(section)
: '<p>Fant ikke modulinnholdet.</p>';
} catch (e) {
body.innerHTML = `<p>Klarte ikke laste modulen: ${e.message}</p>`;
}
// "Mark as read" toggle
const doneBtn = tpl.getElementById('ftDoneBtn');
function syncDoneBtn() {
const done = ftGetDone();
const isDone = done.has(moduleId);
doneBtn.classList.toggle('ft-done-btn--active', isDone);
doneBtn.textContent = isDone ? '✓ Markert som lest' : 'Marker som lest';
}
doneBtn.addEventListener('click', () => {
const done = ftGetDone();
if (done.has(moduleId)) done.delete(moduleId); else done.add(moduleId);
ftSetDone(done);
syncDoneBtn();
});
syncDoneBtn();
// Pager
const idx = SMF.FASTTRACK.findIndex(m => m.id === moduleId);
const prev = idx > 0 ? SMF.FASTTRACK[idx - 1] : null;
const next = idx < SMF.FASTTRACK.length - 1 ? SMF.FASTTRACK[idx + 1] : null;
const pager = tpl.getElementById('ftModPager');
if (prev) {
const a = document.createElement('a');
a.href = `#/fast-track/${prev.id}`;
a.className = 'pager-link pager-link--prev';
a.innerHTML = `<div class="pager-link__dir">← Modul ${prev.id}</div><div class="pager-link__title">${prev.title}</div>`;
pager.appendChild(a);
} else {
const a = document.createElement('a');
a.href = '#/fast-track';
a.className = 'pager-link pager-link--prev';
a.innerHTML = `<div class="pager-link__dir">← Oversikt</div><div class="pager-link__title">Alle moduler</div>`;
pager.appendChild(a);
}
if (next) {
const a = document.createElement('a');
a.href = `#/fast-track/${next.id}`;
a.className = 'pager-link pager-link--next';
a.innerHTML = `<div class="pager-link__dir">Modul ${next.id} →</div><div class="pager-link__title">${next.title}</div>`;
pager.appendChild(a);
} else {
const a = document.createElement('a');
a.href = '#/tldr';
a.className = 'pager-link pager-link--next';
a.innerHTML = `<div class="pager-link__dir">Ferdig! → tl;dr</div><div class="pager-link__title">Siste-minutts-repetisjon</div>`;
pager.appendChild(a);
}
return tpl;
}
// ============= TL;DR view =============
async function renderTldr() {
const tpl = document.getElementById('t-tldr').content.cloneNode(true);
@@ -257,6 +400,8 @@ SMF.renderHome = renderHome;
SMF.renderLesson = renderLesson;
SMF.renderTema = renderTema;
SMF.renderTldr = renderTldr;
SMF.renderFastTrackHome = renderFastTrackHome;
SMF.renderFastTrackModule = renderFastTrackModule;
SMF.loadNote = loadNote;
SMF.renderMarkdown = renderMarkdown;
SMF.extractWeekSection = extractWeekSection;