Eget flashcard-sett (category: "fasttrack") som tester huskereglene, skillene og eksamensfellene fra de fem Fast-track-modulene — kort og konsist, uten ukesoverlappen. - flashcards.json: 30 nye kort (7/5/7/7/4 per modul), alle med kort front - flashcards.js: «Fast-track»-filter, categoryLabel, og fcInit(initialFilter) - app.js: ruter #/flashcards/<filter> for dyplenke - index.html: «Start flashcards →»-CTA på Fast-track-landingssiden - style.css: stil for test-CTA Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
6.4 KiB
JavaScript
204 lines
6.4 KiB
JavaScript
// =====================================================
|
|
// Flashcards — flippable cards with category filter
|
|
// =====================================================
|
|
|
|
let fcData = null;
|
|
let fcState = {
|
|
filter: 'all',
|
|
index: 0,
|
|
flipped: false,
|
|
cards: [],
|
|
known: new Set(), // localStorage-backed
|
|
hard: new Set()
|
|
};
|
|
|
|
async function fcLoad() {
|
|
if (fcData) return fcData;
|
|
try {
|
|
const res = await fetch('data/flashcards.json');
|
|
fcData = await res.json();
|
|
} catch (e) {
|
|
console.error('Klarte ikke laste flashcards:', e);
|
|
fcData = [];
|
|
}
|
|
// Load saved state
|
|
try {
|
|
const saved = JSON.parse(localStorage.getItem('smf-fc-state') || '{}');
|
|
if (saved.known) fcState.known = new Set(saved.known);
|
|
if (saved.hard) fcState.hard = new Set(saved.hard);
|
|
} catch {}
|
|
return fcData;
|
|
}
|
|
|
|
function fcSaveState() {
|
|
localStorage.setItem('smf-fc-state', JSON.stringify({
|
|
known: [...fcState.known],
|
|
hard: [...fcState.hard]
|
|
}));
|
|
}
|
|
|
|
function fcFilterCards(filter) {
|
|
if (filter === 'all') return fcData;
|
|
if (filter === 'hard') return fcData.filter(c => fcState.hard.has(c.id));
|
|
if (filter === 'new') return fcData.filter(c => !fcState.known.has(c.id) && !fcState.hard.has(c.id));
|
|
return fcData.filter(c => c.category === filter);
|
|
}
|
|
|
|
function fcSetFilter(filter) {
|
|
fcState.filter = filter;
|
|
fcState.cards = fcFilterCards(filter);
|
|
// shuffle once when filter changes
|
|
fcShuffle(fcState.cards);
|
|
fcState.index = 0;
|
|
fcState.flipped = false;
|
|
fcRender();
|
|
}
|
|
|
|
function fcShuffle(arr) {
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
}
|
|
}
|
|
|
|
function fcRender() {
|
|
const total = fcState.cards.length;
|
|
document.getElementById('fcCurrent').textContent = total > 0 ? (fcState.index + 1) : 0;
|
|
document.getElementById('fcTotal').textContent = total;
|
|
document.getElementById('fcKnown').textContent = fcState.known.size;
|
|
|
|
const card = document.getElementById('fcCard');
|
|
|
|
if (total === 0) {
|
|
document.getElementById('fcFront').textContent = 'Ingen kort i denne kategorien';
|
|
document.getElementById('fcBack').textContent = 'Prøv et annet filter';
|
|
document.getElementById('fcCategory').textContent = '—';
|
|
card.classList.remove('fc-card--flipped');
|
|
return;
|
|
}
|
|
|
|
const c = fcState.cards[fcState.index];
|
|
document.getElementById('fcFront').textContent = c.front;
|
|
document.getElementById('fcBack').innerHTML = c.back;
|
|
document.getElementById('fcCategory').textContent = (c.subcategory || categoryLabel(c.category));
|
|
|
|
card.classList.toggle('fc-card--flipped', fcState.flipped);
|
|
}
|
|
|
|
function categoryLabel(cat) {
|
|
return {
|
|
fasttrack: 'Fast-track',
|
|
etikk: 'Etikk',
|
|
baerekraft: 'Bærekraft',
|
|
samfunn: 'Samfunnsansvar',
|
|
verktoy: 'Verktøy',
|
|
case: 'Case'
|
|
}[cat] || cat;
|
|
}
|
|
|
|
function fcRenderFilters() {
|
|
const container = document.getElementById('fcFilters');
|
|
if (!container) return;
|
|
const filters = [
|
|
{ id: 'all', label: 'Alle' },
|
|
{ id: 'fasttrack', label: '» Fast-track' },
|
|
{ id: 'new', label: 'Nye' },
|
|
{ id: 'hard', label: 'Glemt' },
|
|
{ id: 'etikk', label: 'Etikk' },
|
|
{ id: 'baerekraft', label: 'Bærekraft' },
|
|
{ id: 'samfunn', label: 'Samfunnsansvar' },
|
|
{ id: 'verktoy', label: 'Verktøy' },
|
|
{ id: 'case', label: 'Case' }
|
|
];
|
|
container.innerHTML = filters.map(f => {
|
|
const active = fcState.filter === f.id ? 'fc-filter--active' : '';
|
|
return `<button class="fc-filter ${active}" data-filter="${f.id}">${f.label}</button>`;
|
|
}).join('');
|
|
}
|
|
|
|
function fcNext() {
|
|
if (!fcState.cards.length) return;
|
|
fcState.flipped = false;
|
|
fcState.index = (fcState.index + 1) % fcState.cards.length;
|
|
fcRender();
|
|
}
|
|
|
|
function fcPrev() {
|
|
if (!fcState.cards.length) return;
|
|
fcState.flipped = false;
|
|
fcState.index = (fcState.index - 1 + fcState.cards.length) % fcState.cards.length;
|
|
fcRender();
|
|
}
|
|
|
|
function fcMark(level) {
|
|
if (!fcState.cards.length) return;
|
|
const c = fcState.cards[fcState.index];
|
|
if (level === 'hard') {
|
|
fcState.hard.add(c.id);
|
|
fcState.known.delete(c.id);
|
|
} else if (level === 'good') {
|
|
fcState.hard.delete(c.id);
|
|
} else if (level === 'easy') {
|
|
fcState.known.add(c.id);
|
|
fcState.hard.delete(c.id);
|
|
}
|
|
fcSaveState();
|
|
fcNext();
|
|
}
|
|
|
|
async function fcInit(initialFilter) {
|
|
await fcLoad();
|
|
// Dyplenke, f.eks. #/flashcards/fasttrack — sett startfilter hvis gyldig
|
|
if (initialFilter) {
|
|
const valid = ['all', 'fasttrack', 'new', 'hard', 'etikk', 'baerekraft', 'samfunn', 'verktoy', 'case'];
|
|
if (valid.includes(initialFilter)) fcState.filter = initialFilter;
|
|
}
|
|
fcState.cards = fcFilterCards(fcState.filter);
|
|
fcShuffle(fcState.cards);
|
|
fcState.index = 0;
|
|
fcState.flipped = false;
|
|
|
|
fcRenderFilters();
|
|
fcRender();
|
|
|
|
// Card click → flip
|
|
const card = document.getElementById('fcCard');
|
|
card.addEventListener('click', () => {
|
|
fcState.flipped = !fcState.flipped;
|
|
fcRender();
|
|
});
|
|
|
|
document.getElementById('fcNext').addEventListener('click', (e) => { e.stopPropagation(); fcNext(); });
|
|
document.getElementById('fcPrev').addEventListener('click', (e) => { e.stopPropagation(); fcPrev(); });
|
|
document.getElementById('fcHard').addEventListener('click', (e) => { e.stopPropagation(); fcMark('hard'); });
|
|
document.getElementById('fcGood').addEventListener('click', (e) => { e.stopPropagation(); fcMark('good'); });
|
|
document.getElementById('fcEasy').addEventListener('click', (e) => { e.stopPropagation(); fcMark('easy'); });
|
|
|
|
document.getElementById('fcFilters').addEventListener('click', (e) => {
|
|
const btn = e.target.closest('[data-filter]');
|
|
if (btn) {
|
|
fcSetFilter(btn.dataset.filter);
|
|
fcRenderFilters();
|
|
}
|
|
});
|
|
|
|
// Keyboard
|
|
const handler = (e) => {
|
|
if (!location.hash.startsWith('#/flashcards')) {
|
|
document.removeEventListener('keydown', handler);
|
|
return;
|
|
}
|
|
const tag = document.activeElement?.tagName;
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
|
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); fcState.flipped = !fcState.flipped; fcRender(); }
|
|
else if (e.key === 'ArrowRight') { e.preventDefault(); fcNext(); }
|
|
else if (e.key === 'ArrowLeft') { e.preventDefault(); fcPrev(); }
|
|
else if (e.key === '1') { fcMark('hard'); }
|
|
else if (e.key === '2') { fcMark('good'); }
|
|
else if (e.key === '3') { fcMark('easy'); }
|
|
};
|
|
document.addEventListener('keydown', handler);
|
|
}
|
|
|
|
SMF.fcInit = fcInit;
|