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:
@@ -1935,3 +1935,302 @@ blockquote {
|
||||
.app { grid-template-columns: 1fr; }
|
||||
.canvas { padding: 0; }
|
||||
}
|
||||
|
||||
/* ===================================================
|
||||
Fast-track — kondensert læringsspor
|
||||
=================================================== */
|
||||
.sidebar__link--fasttrack .sidebar__link-num {
|
||||
color: var(--accent-2);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.ft-hero {
|
||||
padding: var(--sp-7) 0 var(--sp-6);
|
||||
border-bottom: 1px solid var(--line);
|
||||
margin-bottom: var(--sp-7);
|
||||
}
|
||||
.ft-hero__badge {
|
||||
display: inline-block;
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-2);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
padding: 4px 10px;
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
.ft-hero__title {
|
||||
font-family: var(--f-display);
|
||||
font-weight: 300;
|
||||
font-size: var(--s-7);
|
||||
line-height: 1.05;
|
||||
color: var(--ink);
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
.ft-hero__title em { font-style: italic; color: var(--accent-2); }
|
||||
.ft-hero__sub {
|
||||
font-size: var(--s-3);
|
||||
line-height: 1.6;
|
||||
color: var(--ink-2);
|
||||
max-width: 60ch;
|
||||
}
|
||||
.ft-hero__sub strong { color: var(--ink); }
|
||||
|
||||
/* Progress */
|
||||
.ft-progress { margin-top: var(--sp-6); max-width: 480px; }
|
||||
.ft-progress__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: var(--sp-2);
|
||||
}
|
||||
.ft-progress__label {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.ft-progress__count {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-1);
|
||||
color: var(--accent-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ft-progress__track {
|
||||
height: 6px;
|
||||
background: var(--surface-2);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ft-progress__bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(90deg, var(--accent-2), var(--accent));
|
||||
border-radius: 99px;
|
||||
transition: width 0.6s var(--ease);
|
||||
}
|
||||
|
||||
/* Module grid */
|
||||
.ft-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
.ft-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--sp-5);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-top: 3px solid var(--ft-color, var(--accent));
|
||||
border-radius: var(--radius-lg);
|
||||
transition: transform 0.25s var(--ease), border-color 0.25s, background 0.25s;
|
||||
position: relative;
|
||||
}
|
||||
.ft-card:hover {
|
||||
transform: translateY(-4px);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.ft-card__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--sp-3);
|
||||
}
|
||||
.ft-card__num {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-4);
|
||||
color: var(--ft-color, var(--accent));
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ft-card__check {
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--line-strong);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: var(--s-0);
|
||||
color: transparent;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.ft-card--done .ft-card__check {
|
||||
background: var(--ft-color, var(--accent));
|
||||
border-color: var(--ft-color, var(--accent));
|
||||
color: var(--bg);
|
||||
}
|
||||
.ft-card__eyebrow {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ft-card__title {
|
||||
font-family: var(--f-display);
|
||||
font-weight: 400;
|
||||
font-size: var(--s-5);
|
||||
line-height: 1.15;
|
||||
color: var(--ink);
|
||||
margin-bottom: var(--sp-3);
|
||||
}
|
||||
.ft-card__desc {
|
||||
font-size: var(--s-1);
|
||||
line-height: 1.55;
|
||||
color: var(--ink-2);
|
||||
flex: 1;
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
.ft-card__foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: var(--sp-3);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.ft-card__mins {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
color: var(--subtle);
|
||||
}
|
||||
.ft-card__arrow {
|
||||
font-size: var(--s-1);
|
||||
color: var(--ft-color, var(--accent));
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s var(--ease);
|
||||
}
|
||||
.ft-card:hover .ft-card__arrow { transform: translateX(5px); }
|
||||
|
||||
/* Mark-as-read button */
|
||||
.ft-done-btn {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 99px;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ft-done-btn:hover { color: var(--ink); border-color: var(--ink-2); }
|
||||
.ft-done-btn--active {
|
||||
color: var(--bg);
|
||||
background: var(--accent-2);
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
|
||||
/* Module body callouts — passed through as raw HTML from markdown */
|
||||
.ft-body .callout {
|
||||
display: flex;
|
||||
gap: var(--sp-4);
|
||||
margin: var(--sp-5) 0;
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--accent);
|
||||
font-size: var(--s-1);
|
||||
line-height: 1.6;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.ft-body .callout > div { color: var(--ink-2); }
|
||||
.ft-body .callout strong { color: var(--ink); }
|
||||
.ft-body .callout__label {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--f-mono);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
padding-top: 2px;
|
||||
min-width: 7.5em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.ft-body .callout--huske {
|
||||
border-left-color: var(--accent-2);
|
||||
background: color-mix(in srgb, var(--accent-2) 7%, var(--surface));
|
||||
}
|
||||
.ft-body .callout--huske .callout__label { color: var(--accent-2); }
|
||||
.ft-body .callout--eksempel {
|
||||
border-left-color: var(--theme-baerekraft);
|
||||
background: color-mix(in srgb, var(--theme-baerekraft) 8%, var(--surface));
|
||||
}
|
||||
.ft-body .callout--eksempel .callout__label { color: var(--theme-baerekraft); }
|
||||
.ft-body .callout--felle {
|
||||
border-left-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--surface));
|
||||
}
|
||||
.ft-body .callout--felle .callout__label { color: var(--accent); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ft-body .callout { flex-direction: column; gap: var(--sp-2); }
|
||||
.ft-body .callout__label { min-width: 0; }
|
||||
}
|
||||
|
||||
/* Home promo */
|
||||
.ft-promo {
|
||||
padding: var(--sp-7) 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.ft-promo__card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-5);
|
||||
padding: var(--sp-5) var(--sp-6);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: transform 0.25s var(--ease), background 0.25s, border-color 0.25s;
|
||||
}
|
||||
.ft-promo__card:hover {
|
||||
transform: translateY(-3px);
|
||||
background: var(--surface-2);
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
.ft-promo__icon {
|
||||
font-family: var(--f-display);
|
||||
font-size: var(--s-8);
|
||||
line-height: 1;
|
||||
color: var(--accent-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ft-promo__body { flex: 1; }
|
||||
.ft-promo__label {
|
||||
font-family: var(--f-mono);
|
||||
font-size: var(--s-0);
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-2);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ft-promo__title {
|
||||
font-family: var(--f-display);
|
||||
font-weight: 400;
|
||||
font-size: var(--s-5);
|
||||
color: var(--ink);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ft-promo__title em { font-style: italic; color: var(--accent-2); }
|
||||
.ft-promo__desc {
|
||||
font-size: var(--s-1);
|
||||
line-height: 1.55;
|
||||
color: var(--ink-2);
|
||||
max-width: 64ch;
|
||||
}
|
||||
.ft-promo__arrow {
|
||||
font-size: var(--s-6);
|
||||
color: var(--accent-2);
|
||||
transition: transform 0.2s var(--ease);
|
||||
}
|
||||
.ft-promo__card:hover .ft-promo__arrow { transform: translateX(8px); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ft-promo__card { flex-direction: column; align-items: flex-start; gap: var(--sp-3); }
|
||||
.ft-promo__arrow { align-self: flex-end; }
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
<span class="sidebar__link-num">00</span>
|
||||
<span>Oversikt</span>
|
||||
</a>
|
||||
<a href="#/fast-track" data-route="fasttrack" class="sidebar__link sidebar__link--fasttrack">
|
||||
<span class="sidebar__link-num">»</span>
|
||||
<span>Fast-track</span>
|
||||
</a>
|
||||
<a href="#/tldr" data-route="tldr" class="sidebar__link sidebar__link--tldr">
|
||||
<span class="sidebar__link-num">!</span>
|
||||
<span>tl;dr — i farta</span>
|
||||
@@ -221,6 +225,18 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ft-promo reveal">
|
||||
<a href="#/fast-track" class="ft-promo__card" data-route="fasttrack">
|
||||
<div class="ft-promo__icon">»</div>
|
||||
<div class="ft-promo__body">
|
||||
<div class="ft-promo__label">Fast-track</div>
|
||||
<div class="ft-promo__title">Mye <em>repetisjon</em> mellom ukene? Hopp over den.</div>
|
||||
<div class="ft-promo__desc">Hele pensum kondensert til fem moduler — hvert begrep lært én gang, med huskeregler, eksempler og eksamensfeller. Følg fremdriften din underveis.</div>
|
||||
</div>
|
||||
<div class="ft-promo__arrow">→</div>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section class="tldr-promo reveal">
|
||||
<a href="#/tldr" class="tldr-promo__card" data-route="tldr">
|
||||
<div class="tldr-promo__icon">!</div>
|
||||
@@ -346,6 +362,40 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="t-fasttrack-home">
|
||||
<div class="page page--narrow">
|
||||
<header class="ft-hero reveal">
|
||||
<div class="ft-hero__badge">Fast-track</div>
|
||||
<h1 class="ft-hero__title">Hele pensum,<br><em>uten gjentakelsen</em></h1>
|
||||
<div class="ft-hero__sub">Faget gjentar seg på tvers av 12 uker. Her lærer du hvert begrep <strong>én gang</strong> — kondensert til fem moduler etter eksamensstrukturen, med huskeregler, eksempler og fellene som trekker ned.</div>
|
||||
<div class="ft-progress reveal">
|
||||
<div class="ft-progress__head">
|
||||
<span class="ft-progress__label">Din fremdrift</span>
|
||||
<span class="ft-progress__count" id="ftProgressCount">0 / 5 moduler</span>
|
||||
</div>
|
||||
<div class="ft-progress__track"><div class="ft-progress__bar" id="ftProgressBar"></div></div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ft-grid" id="ftGrid"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="t-fasttrack-module">
|
||||
<div class="page page--narrow">
|
||||
<header class="lesson-header reveal">
|
||||
<div class="lesson-header__num" id="ftModNum">Fast-track</div>
|
||||
<h1 class="lesson-header__title" id="ftModTitle">—</h1>
|
||||
<div class="lesson-header__meta">
|
||||
<button class="ft-done-btn" id="ftDoneBtn">Marker som lest</button>
|
||||
</div>
|
||||
</header>
|
||||
<article class="lesson tldr-body ft-body" id="ftModBody">
|
||||
<div class="is-loading" style="height: 400px;"></div>
|
||||
</article>
|
||||
<nav class="lesson-pager" id="ftModPager"></nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="t-tldr">
|
||||
<div class="page page--narrow tldr-page">
|
||||
<header class="tldr-hero reveal">
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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