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:
145
app/js/render.js
145
app/js/render.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user