diff --git a/backend/src/controllers/flashcardController.js b/backend/src/controllers/flashcardController.js index b5a3109..aeb2be4 100644 --- a/backend/src/controllers/flashcardController.js +++ b/backend/src/controllers/flashcardController.js @@ -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, }; diff --git a/backend/src/routes/flashcards.js b/backend/src/routes/flashcards.js index 13d9a65..3966276 100644 --- a/backend/src/routes/flashcards.js +++ b/backend/src/routes/flashcards.js @@ -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); diff --git a/frontend/dashboard.html b/frontend/dashboard.html index a3079ec..2ee98a4 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -245,6 +245,39 @@ .cont-sub { font-size: 0.72rem; color: var(--text-3); margin-top: 2px; } .cont-pct { font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800; color: var(--violet); } + /* ── flashcard review widget ── */ + .fcw-card { perspective: 1000px; cursor: pointer; } + .fcw-inner { + position: relative; transform-style: preserve-3d; + transition: transform 0.5s cubic-bezier(.34,1.1,.64,1); min-height: 118px; + } + .fcw-card.flipped .fcw-inner { transform: rotateY(180deg); } + .fcw-face { + position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; + border-radius: 14px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; + border: 1.5px solid rgba(15,23,42,0.08); box-sizing: border-box; + } + .fcw-front { background: linear-gradient(135deg, rgba(155,93,229,0.06), rgba(6,214,224,0.05)); } + .fcw-back { background: linear-gradient(135deg, rgba(6,214,100,0.07), rgba(6,214,224,0.05)); transform: rotateY(180deg); } + .fcw-deck { font-size: 0.66rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--violet); } + .fcw-back .fcw-deck { color: #059652; } + .fcw-text { flex: 1; font-size: 0.92rem; font-weight: 600; color: var(--text); line-height: 1.35; + display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } + .fcw-hint { font-size: 0.68rem; color: var(--text-3); display: flex; align-items: center; gap: 5px; } + .fcw-hint svg { width: 12px; height: 12px; } + .fcw-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; } + .fcw-count { font-size: 0.72rem; color: var(--text-3); font-weight: 600; } + .fcw-btn { + display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 99px; + border: 1.5px solid rgba(155,93,229,0.3); background: rgba(155,93,229,0.06); color: var(--violet); + font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 700; cursor: pointer; + transition: all 0.15s; text-decoration: none; + } + .fcw-btn:hover { background: rgba(155,93,229,0.14); border-color: var(--violet); } + .fcw-btn svg { width: 13px; height: 13px; stroke: currentColor; } + .fcw-empty { text-align: center; padding: 16px 12px; color: var(--text-3); } + .fcw-empty p { font-size: 0.82rem; margin-bottom: 10px; } + /* ── subjects progress bars ── */ .subj-prog-row { display: flex; align-items: center; gap: 14px; @@ -1460,6 +1493,14 @@
+ +