2026-04-02 23:11:07 +02:00
|
|
|
'use strict';
|
|
|
|
|
// NOTE: innerHTML usage is intentional - content comes from our own API/DB only (admin-imported Markdown).
|
|
|
|
|
// This is an internal training tool for 2-5 known users. No user-generated content.
|
|
|
|
|
|
|
|
|
|
var Flashcards = {
|
|
|
|
|
session: null,
|
|
|
|
|
|
|
|
|
|
render: function(params) {
|
|
|
|
|
var el = document.getElementById('content');
|
|
|
|
|
|
|
|
|
|
if (params[0] === 'study' && params[1]) {
|
|
|
|
|
this.renderStudy(el, params[1]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el.textContent = 'Lade Decks...';
|
|
|
|
|
var self = this;
|
|
|
|
|
API.get('/decks').then(function(data) {
|
|
|
|
|
self.renderDeckList(el, data.decks);
|
|
|
|
|
}).catch(function(e) {
|
|
|
|
|
el.textContent = e.message;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderDeckList: function(el, decks) {
|
|
|
|
|
var total = decks.reduce(function(s, d) { return s + (d.card_count || 0); }, 0);
|
|
|
|
|
var totalDue = decks.reduce(function(s, d) { return s + (parseInt(d.due) || 0); }, 0);
|
|
|
|
|
|
2026-04-03 09:38:10 +02:00
|
|
|
var h = '<div class="page-header" style="display:flex;justify-content:space-between;align-items:start;">';
|
|
|
|
|
h += '<div><h1>Flashcards</h1>';
|
2026-04-02 23:11:07 +02:00
|
|
|
h += '<p>' + total + ' Karten in ' + decks.length + ' Decks';
|
|
|
|
|
if (totalDue > 0) h += ' — <strong style="color:var(--orange)">' + totalDue + ' faellig</strong>';
|
2026-04-03 09:38:10 +02:00
|
|
|
h += '</p></div>';
|
|
|
|
|
h += '<a href="/api/decks/export-anki" class="btn btn-small" style="white-space:nowrap;margin-top:4px;" download>Anki-Export</a>';
|
|
|
|
|
h += '</div><div class="deck-grid">';
|
2026-04-02 23:11:07 +02:00
|
|
|
|
|
|
|
|
decks.forEach(function(d) {
|
|
|
|
|
var reviewed = parseInt(d.reviewed) || 0;
|
|
|
|
|
var ct = d.card_count || 0;
|
|
|
|
|
var due = parseInt(d.due) || 0;
|
|
|
|
|
var pct = ct > 0 ? Math.round((reviewed / ct) * 100) : 0;
|
|
|
|
|
|
|
|
|
|
h += '<div class="deck-card" data-slug="' + d.slug + '">';
|
|
|
|
|
h += '<div class="deck-name">' + Markdown.esc(d.name) + '</div>';
|
|
|
|
|
h += '<div class="deck-info">' + ct + ' Karten — ' + pct + '% gelernt</div>';
|
|
|
|
|
if (due > 0) h += '<div class="deck-due">' + due + ' Karten faellig</div>';
|
|
|
|
|
h += '<div class="progress-bar"><div class="fill fill-green" style="width:' + pct + '%"></div></div>';
|
|
|
|
|
h += '</div>';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
h += '</div>';
|
|
|
|
|
el.innerHTML = h; // safe: data from own API only
|
|
|
|
|
el.querySelectorAll('.deck-card').forEach(function(card) {
|
|
|
|
|
card.addEventListener('click', function() {
|
|
|
|
|
location.hash = '#/flashcards/study/' + card.dataset.slug;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderStudy: function(el, slug) {
|
|
|
|
|
el.textContent = 'Lade Karten...';
|
|
|
|
|
var self = this;
|
|
|
|
|
API.get('/cards/due?deck=' + slug + '&limit=50').then(function(data) {
|
|
|
|
|
if (data.cards.length === 0) {
|
|
|
|
|
return API.get('/cards/deck/' + slug).then(function(allData) {
|
|
|
|
|
var h = '<a href="#/flashcards" class="back-link">← Zurueck zu Decks</a>';
|
|
|
|
|
h += '<div class="empty-state"><h2>Keine faelligen Karten</h2>';
|
|
|
|
|
h += '<p>Alle Karten sind gelernt. Komm spaeter wieder!</p><br>';
|
|
|
|
|
h += '<button class="btn btn-primary" id="start-all-btn">Alle ' + allData.cards.length + ' Karten ueben</button></div>';
|
|
|
|
|
el.innerHTML = h; // safe: static text + number
|
|
|
|
|
document.getElementById('start-all-btn').addEventListener('click', function() {
|
|
|
|
|
self.startSession(el, slug, allData.cards);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
self.startSession(el, slug, data.cards);
|
|
|
|
|
}).catch(function(e) { el.textContent = e.message; });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
startSession: function(el, slug, cards) {
|
|
|
|
|
this.session = { slug: slug, cards: cards, index: 0, correct: 0, wrong: 0, showAnswer: false };
|
|
|
|
|
this.renderCard(el);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderCard: function(el) {
|
|
|
|
|
var s = this.session;
|
|
|
|
|
if (s.index >= s.cards.length) { this.renderSummary(el); return; }
|
|
|
|
|
|
|
|
|
|
var card = s.cards[s.index];
|
|
|
|
|
var open = s.cards.length - s.index - 1;
|
|
|
|
|
var deckName = card.deck_name || s.slug;
|
|
|
|
|
var lastReview = card.last_reviewed_at
|
|
|
|
|
? 'Zuletzt: ' + new Date(card.last_reviewed_at).toLocaleDateString('de-DE')
|
|
|
|
|
: 'Noch nie gelernt';
|
|
|
|
|
|
|
|
|
|
var h = '<a href="#/flashcards" class="back-link">← Zurueck zu Decks</a>';
|
|
|
|
|
h += '<div class="study-header"><span class="study-deck-name">' + Markdown.esc(deckName) + '</span>';
|
|
|
|
|
h += '<div class="study-stats"><span class="stat-correct">' + s.correct + ' richtig</span>';
|
|
|
|
|
h += '<span class="stat-wrong">' + s.wrong + ' falsch</span>';
|
|
|
|
|
h += '<span class="stat-open">' + open + ' offen</span></div>';
|
|
|
|
|
h += '<span class="study-counter">Karte ' + (s.index + 1) + ' / ' + s.cards.length + '</span></div>';
|
|
|
|
|
h += '<div class="progress-segmented"><div class="seg-correct" style="flex:' + s.correct + '"></div>';
|
|
|
|
|
h += '<div class="seg-wrong" style="flex:' + s.wrong + '"></div>';
|
|
|
|
|
h += '<div class="seg-open" style="flex:' + (open + 1) + '"></div></div><br>';
|
|
|
|
|
h += '<div class="flashcard"><div class="card-meta"><span class="card-level">' + (card.level || 'basis') + '</span>';
|
|
|
|
|
h += '<span class="card-last-review">' + lastReview + '</span></div>';
|
|
|
|
|
h += '<div class="card-question">' + Markdown.render(card.question) + '</div>';
|
|
|
|
|
|
|
|
|
|
if (s.showAnswer) {
|
|
|
|
|
h += '<div class="card-answer"><div class="card-answer-label">Antwort</div>' + Markdown.render(card.answer) + '</div>';
|
|
|
|
|
h += '<div class="rating-buttons">';
|
|
|
|
|
h += '<button class="btn btn-green" id="rate-easy">Leicht</button>';
|
|
|
|
|
h += '<button class="btn btn-orange" id="rate-medium">Mittel</button>';
|
|
|
|
|
h += '<button class="btn btn-red" id="rate-hard">Schwer</button>';
|
|
|
|
|
h += '<button class="btn btn-muted" id="rate-wrong">Falsch</button></div>';
|
|
|
|
|
} else {
|
|
|
|
|
h += '<div class="card-answer-hidden"><button class="btn btn-primary" id="show-answer-btn">Antwort zeigen</button></div>';
|
|
|
|
|
}
|
|
|
|
|
h += '</div>';
|
|
|
|
|
el.innerHTML = h; // safe: Markdown content from own DB
|
|
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
|
if (s.showAnswer) {
|
|
|
|
|
document.getElementById('rate-easy').addEventListener('click', function() { self.rate('easy'); });
|
|
|
|
|
document.getElementById('rate-medium').addEventListener('click', function() { self.rate('medium'); });
|
|
|
|
|
document.getElementById('rate-hard').addEventListener('click', function() { self.rate('hard'); });
|
|
|
|
|
document.getElementById('rate-wrong').addEventListener('click', function() { self.rate('wrong'); });
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById('show-answer-btn').addEventListener('click', function() {
|
|
|
|
|
s.showAnswer = true;
|
|
|
|
|
self.renderCard(el);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
rate: function(rating) {
|
|
|
|
|
var s = this.session;
|
|
|
|
|
var card = s.cards[s.index];
|
|
|
|
|
if (rating === 'easy' || rating === 'medium') s.correct++;
|
|
|
|
|
else s.wrong++;
|
|
|
|
|
API.post('/cards/' + card.id + '/review', { rating: rating }).catch(function(e) { console.error('Review error:', e); });
|
|
|
|
|
s.index++;
|
|
|
|
|
s.showAnswer = false;
|
|
|
|
|
this.renderCard(document.getElementById('content'));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderSummary: function(el) {
|
|
|
|
|
var s = this.session;
|
|
|
|
|
var total = s.correct + s.wrong;
|
|
|
|
|
var pct = total > 0 ? Math.round((s.correct / total) * 100) : 0;
|
|
|
|
|
var h = '<a href="#/flashcards" class="back-link">← Zurueck zu Decks</a>';
|
|
|
|
|
h += '<div style="text-align:center;padding:48px 0;"><h2>Session abgeschlossen!</h2><br>';
|
|
|
|
|
h += '<div class="study-stats" style="justify-content:center;font-size:18px;">';
|
|
|
|
|
h += '<span class="stat-correct">' + s.correct + ' richtig</span>';
|
|
|
|
|
h += '<span class="stat-wrong">' + s.wrong + ' falsch</span></div><br>';
|
|
|
|
|
h += '<p style="color:var(--text-secondary);font-size:18px;">' + pct + '% korrekt</p><br>';
|
|
|
|
|
h += '<a href="#/flashcards" class="btn btn-primary">Zurueck zu Decks</a></div>';
|
|
|
|
|
el.innerHTML = h; // safe: only session stats
|
|
|
|
|
}
|
|
|
|
|
};
|