feat(flashcards): глобальный quick-add FAB + виджет «повтори карточку»

Backend:
- POST /api/flashcards/quick — добавить карточку из любой точки; колода по
  выбору или автоколода «Быстрые карточки» (создаётся при первом обращении)
- GET /api/flashcards/random — случайная карточка из всего пула пользователя

Frontend:
- /js/flashcard-fab.js — плавающая кнопка «запомнить» на всех страницах
  (учебник, лаборатория, симуляция…). Поповер: вопрос/ответ/колода, Ctrl+Enter.
  Гейт по фиче-флагу flashcards; исключены classroom/login/error/сама /flashcards.
  Загружается лениво из sidebar.js (на 45 страницах с шапкой).
- dashboard: виджет #w-flashcard в колонке прогресса — флип-карта (вопрос↔ответ),
  кнопка «Другая», счётчик пула, CTA при пустом пуле; слушает событие
  flashcard:added для авто-обновления.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-31 09:38:23 +03:00
parent d4ab7993c5
commit 1dcc4cbf6e
5 changed files with 388 additions and 0 deletions
@@ -241,8 +241,64 @@ function getStats(req, res) {
res.json({ decks_count, cards_count, due_count, reviewed_today });
}
/* ── POST /api/flashcards/quick — быстрое добавление из любой точки ──────
Кладёт карточку в указанную колоду (deckId) либо в личную колоду
«Быстрые карточки» (создаётся при первом обращении). */
const QUICK_DECK_TITLE = 'Быстрые карточки';
function quickAdd(req, res) {
const uid = req.user.id;
const front = stripTags((req.body.front || '').slice(0, 5000)).trim();
const back = stripTags((req.body.back || '').slice(0, 5000)).trim();
if (!front) return res.status(400).json({ error: 'Лицевая сторона обязательна' });
let deck = null;
const deckId = Number(req.body.deckId) || 0;
if (deckId) {
deck = db.prepare(`SELECT id, title, color FROM flashcard_decks WHERE id = ? AND user_id = ?`)
.get(deckId, uid);
}
if (!deck) {
deck = db.prepare(`SELECT id, title, color FROM flashcard_decks WHERE user_id = ? AND title = ? ORDER BY id LIMIT 1`)
.get(uid, QUICK_DECK_TITLE);
if (!deck) {
const r = db.prepare(
`INSERT INTO flashcard_decks (user_id, title, description, color) VALUES (?,?,?,?)`
).run(uid, QUICK_DECK_TITLE, 'Карточки, добавленные на лету', '#9B5DE5');
deck = { id: r.lastInsertRowid, title: QUICK_DECK_TITLE, color: '#9B5DE5', created: true };
}
}
const maxIdx = db.prepare(`SELECT MAX(order_idx) AS m FROM flashcard_cards WHERE deck_id = ?`)
.get(deck.id)?.m ?? -1;
const r = db.prepare(`INSERT INTO flashcard_cards (deck_id, front, back, order_idx) VALUES (?,?,?,?)`)
.run(deck.id, front, back, maxIdx + 1);
res.json({ id: r.lastInsertRowid, deck_id: deck.id, deck_title: deck.title, deck_color: deck.color, front, back });
}
/* ── GET /api/flashcards/random — случайная карточка из всего пула ───────
Для дашборд-виджета «повтори карточку». */
function getRandom(req, res) {
const uid = req.user.id;
const total = db.prepare(`
SELECT COUNT(*) AS n FROM flashcard_cards c
JOIN flashcard_decks d ON d.id = c.deck_id WHERE d.user_id = ?
`).get(uid).n;
if (!total) return res.json({ card: null, total: 0 });
const card = db.prepare(`
SELECT c.id, c.front, c.back, c.deck_id,
d.title AS deck_title, d.color AS deck_color
FROM flashcard_cards c
JOIN flashcard_decks d ON d.id = c.deck_id
WHERE d.user_id = ?
ORDER BY RANDOM() LIMIT 1
`).get(uid);
res.json({ card, total });
}
module.exports = {
listDecks, createDeck, updateDeck, deleteDeck,
getCards, addCard, addCardsBulk, updateCard, deleteCard,
getStudySession, submitReview, getStats,
quickAdd, getRandom,
};
+2
View File
@@ -5,6 +5,8 @@ const { authMiddleware } = require('../middleware/auth');
router.use(authMiddleware);
router.post ('/quick', fc.quickAdd);
router.get ('/random', fc.getRandom);
router.get ('/decks', fc.listDecks);
router.post ('/decks', fc.createDeck);
router.put ('/decks/:id', fc.updateDeck);