diff --git a/edu/api/cards.php b/edu/api/cards.php new file mode 100644 index 0000000..1e4b729 --- /dev/null +++ b/edu/api/cards.php @@ -0,0 +1,92 @@ +prepare(" + SELECT c.id, c.question, c.answer, c.level, c.sort_order, + cp.next_review, cp.last_rating, cp.last_reviewed_at, + cp.ease_factor, cp.interval_days, cp.repetitions, + d.name AS deck_name, d.slug AS deck_slug + FROM cards c + JOIN decks d ON d.id = c.deck_id AND d.slug = :slug + LEFT JOIN card_progress cp ON cp.card_id = c.id AND cp.user_id = :uid + ORDER BY c.sort_order + "); + $stmt->execute([':slug' => $slug, ':uid' => $user_id]); + json_ok(['cards' => $stmt->fetchAll()]); +} + +// GET /api/cards/due?deck={slug}&limit=N +if (get_method() === 'GET' && ($segments[1] ?? '') === 'due') { + $slug = get_param('deck', ''); + $limit = max(1, min(50, (int) get_param('limit', 20))); + + $query = " + SELECT c.id, c.question, c.answer, c.level, c.sort_order, c.deck_id, + d.slug AS deck_slug, d.name AS deck_name, + cp.next_review, cp.last_rating, cp.last_reviewed_at, + cp.ease_factor, cp.interval_days, cp.repetitions + FROM cards c + JOIN decks d ON d.id = c.deck_id + LEFT JOIN card_progress cp ON cp.card_id = c.id AND cp.user_id = :uid + WHERE (cp.next_review IS NULL OR cp.next_review <= CURRENT_DATE) + "; + $params = [':uid' => $user_id]; + + if ($slug) { + $query .= " AND d.slug = :slug"; + $params[':slug'] = $slug; + } + + $query .= " ORDER BY cp.next_review ASC NULLS FIRST, c.sort_order LIMIT " . $limit; + + $stmt = $pdo->prepare($query); + $stmt->execute($params); + json_ok(['cards' => $stmt->fetchAll()]); +} + +// POST /api/cards/{id}/review +if (get_method() === 'POST' && is_numeric($segments[1] ?? '') && ($segments[2] ?? '') === 'review') { + $card_id = (int) $segments[1]; + $body = get_json_body(); + $rating = $body['rating'] ?? ''; + + if (!in_array($rating, ['easy', 'medium', 'hard', 'wrong'])) { + json_error('Ungueltige Bewertung. Erlaubt: easy, medium, hard, wrong'); + } + + $stmt = $pdo->prepare("SELECT id FROM cards WHERE id = :id"); + $stmt->execute([':id' => $card_id]); + if (!$stmt->fetch()) json_error('Karte nicht gefunden', 404); + + $stmt = $pdo->prepare("SELECT * FROM card_progress WHERE user_id = :uid AND card_id = :cid"); + $stmt->execute([':uid' => $user_id, ':cid' => $card_id]); + $progress = $stmt->fetch(); + + $ease = $progress['ease_factor'] ?? 2.5; + $interval = $progress['interval_days'] ?? 0; + $reps = $progress['repetitions'] ?? 0; + + $result = sm2_calculate($rating, $ease, $interval, $reps); + + $stmt = $pdo->prepare(" + INSERT INTO card_progress (user_id, card_id, ease_factor, interval_days, repetitions, next_review, last_rating, last_reviewed_at) + VALUES (:uid, :cid, :ease, :intv, :reps, :next, :rating, NOW()) + ON CONFLICT (user_id, card_id) DO UPDATE SET + ease_factor = :ease, interval_days = :intv, repetitions = :reps, + next_review = :next, last_rating = :rating, last_reviewed_at = NOW() + "); + $stmt->execute([ + ':uid' => $user_id, ':cid' => $card_id, + ':ease' => $result['ease_factor'], ':intv' => $result['interval_days'], + ':reps' => $result['repetitions'], ':next' => $result['next_review'], + ':rating' => $rating + ]); + + json_ok(['progress' => $result]); +} + +json_error('Unbekannter Cards-Endpunkt', 404); diff --git a/edu/api/decks.php b/edu/api/decks.php new file mode 100644 index 0000000..329b8fe --- /dev/null +++ b/edu/api/decks.php @@ -0,0 +1,30 @@ +prepare(" + SELECT d.id, d.slug, d.name, d.description, d.card_count, + COUNT(cp.id) AS reviewed, + COUNT(cp.id) FILTER (WHERE cp.last_rating IN ('easy','medium')) AS correct, + COUNT(cp.id) FILTER (WHERE cp.next_review <= CURRENT_DATE) AS due + FROM decks d + LEFT JOIN cards c ON c.deck_id = d.id + LEFT JOIN card_progress cp ON cp.card_id = c.id AND cp.user_id = :uid + GROUP BY d.id + ORDER BY d.sort_order, d.name + "); + $stmt->execute([':uid' => $user_id]); + json_ok(['decks' => $stmt->fetchAll()]); +} + +// GET /api/decks/{slug} +if (get_method() === 'GET' && !empty($segments[1])) { + $stmt = $pdo->prepare("SELECT * FROM decks WHERE slug = :s"); + $stmt->execute([':s' => $segments[1]]); + $deck = $stmt->fetch(); + if (!$deck) json_error('Deck nicht gefunden', 404); + json_ok(['deck' => $deck]); +} + +json_error('Methode nicht erlaubt', 405); diff --git a/edu/js/flashcards.js b/edu/js/flashcards.js new file mode 100644 index 0000000..4701b75 --- /dev/null +++ b/edu/js/flashcards.js @@ -0,0 +1,157 @@ +'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); + + var h = '
' + total + ' Karten in ' + decks.length + ' Decks'; + if (totalDue > 0) h += ' — ' + totalDue + ' faellig'; + h += '
Alle Karten sind gelernt. Komm spaeter wieder!
' + pct + '% korrekt