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:
196
app/js/flashcards.js
Normal file
196
app/js/flashcards.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// =====================================================
|
||||
// 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 {
|
||||
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: '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() {
|
||||
await fcLoad();
|
||||
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 !== '#/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;
|
||||
Reference in New Issue
Block a user