feat(flashcards): фаза 1 полировки — хоткеи, поиск, drag-reorder, честные интервалы
- study: хоткеи Space/стрелки=флип, 1-4/←→=оценка - превью интервалов = точная копия серверного SM-2 (было враньё «<1 мин») - поиск/фильтр карточек внутри колоды - drag-reorder карточек + endpoint PUT /decks/:id/reorder (requireOwnership) - flashcard_decks добавлен в ALLOWED_TABLES requireOwnership - эмодзи в empty-state → inline SVG .ic - deleteCard: нативный confirm() → LS.confirm Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,30 @@ function addCardsBulk(req, res) {
|
||||
res.json({ inserted });
|
||||
}
|
||||
|
||||
/* ── PUT /api/flashcards/decks/:id/reorder ─────────────────────────────────
|
||||
body: { order: [cardId, …] } — переписывает order_idx по позиции в массиве.
|
||||
Принимаются только карточки, реально принадлежащие колоде владельца. */
|
||||
function reorderCards(req, res) {
|
||||
const uid = req.user.id;
|
||||
const deck = db.prepare(`SELECT id FROM flashcard_decks WHERE id = ? AND user_id = ?`)
|
||||
.get(req.params.id, uid);
|
||||
if (!deck) return res.status(404).json({ error: 'Not found' });
|
||||
const { order } = req.body;
|
||||
if (!Array.isArray(order) || !order.length)
|
||||
return res.status(400).json({ error: 'order[] required' });
|
||||
|
||||
const owned = new Set(
|
||||
db.prepare(`SELECT id FROM flashcard_cards WHERE deck_id = ?`).all(deck.id).map(r => r.id)
|
||||
);
|
||||
const stmt = db.prepare(`UPDATE flashcard_cards SET order_idx = ? WHERE id = ? AND deck_id = ?`);
|
||||
const run = db.transaction(() => {
|
||||
let idx = 0;
|
||||
order.forEach(id => { if (owned.has(Number(id))) stmt.run(idx++, Number(id), deck.id); });
|
||||
});
|
||||
run();
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── PUT /api/flashcards/cards/:id ─────────────────────────────────────── */
|
||||
function updateCard(req, res) {
|
||||
const uid = req.user.id;
|
||||
@@ -298,7 +322,7 @@ function getRandom(req, res) {
|
||||
|
||||
module.exports = {
|
||||
listDecks, createDeck, updateDeck, deleteDeck,
|
||||
getCards, addCard, addCardsBulk, updateCard, deleteCard,
|
||||
getCards, addCard, addCardsBulk, updateCard, deleteCard, reorderCards,
|
||||
getStudySession, submitReview, getStats,
|
||||
quickAdd, getRandom,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ const db = require('../db/db');
|
||||
* paramKey — req.params key for the record ID (default: 'id')
|
||||
* adminBypass — admin role always passes (default: true)
|
||||
*/
|
||||
const ALLOWED_TABLES = new Set(['tests','classes','assignments','questions','courses','lessons','files','folders','shop_items','live_sessions']);
|
||||
const ALLOWED_TABLES = new Set(['tests','classes','assignments','questions','courses','lessons','files','folders','shop_items','live_sessions','flashcard_decks']);
|
||||
|
||||
function requireOwnership({ table, fetchFn, ownerField, paramKey = 'id', adminBypass = true }) {
|
||||
if (table && !ALLOWED_TABLES.has(table)) throw new Error(`requireOwnership: unknown table "${table}"`);
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const fc = require('../controllers/flashcardController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { requireOwnership } = require('../middleware/ownership');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
@@ -14,6 +15,7 @@ router.delete('/decks/:id', fc.deleteDeck);
|
||||
router.get ('/decks/:id/cards', fc.getCards);
|
||||
router.post ('/decks/:id/cards', fc.addCard);
|
||||
router.post ('/decks/:id/cards/bulk', fc.addCardsBulk);
|
||||
router.put ('/decks/:id/reorder', requireOwnership({ table: 'flashcard_decks', ownerField: 'user_id' }), fc.reorderCards);
|
||||
router.get ('/decks/:id/study', fc.getStudySession);
|
||||
router.put ('/cards/:id', fc.updateCard);
|
||||
router.delete('/cards/:id', fc.deleteCard);
|
||||
|
||||
Reference in New Issue
Block a user