From 51e5dc29e1abb1c083819683523aecb6f3edef52 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 13:14:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(flashcards):=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B8=20=D0=B2=20=D0=BC=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=BC=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=D0=B5=20=C2=AB=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - модалка в 2 шага: текст -> предпросмотр карточек, к каждой стороне можно прикрепить картинку перед импортом - addCardsBulk принимает front_image/back_image (через safeImg) и теперь санитизит front/back (stripTags) — раньше bulk пропускал теги - общий ensureImgPicker() переиспользуется редактором и предпросмотром Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/controllers/flashcardController.js | 10 +- frontend/flashcards.html | 153 +++++++++++++++--- 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/backend/src/controllers/flashcardController.js b/backend/src/controllers/flashcardController.js index 50f50a2..4a43a44 100644 --- a/backend/src/controllers/flashcardController.js +++ b/backend/src/controllers/flashcardController.js @@ -125,12 +125,16 @@ function addCardsBulk(req, res) { if (!Array.isArray(cards) || !cards.length) return res.status(400).json({ error: 'cards[] required' }); const maxIdx = db.prepare(`SELECT MAX(order_idx) AS m FROM flashcard_cards WHERE deck_id = ?`) .get(deck.id)?.m ?? -1; - const stmt = db.prepare(`INSERT INTO flashcard_cards (deck_id, front, back, order_idx) VALUES (?,?,?,?)`); + const stmt = db.prepare(`INSERT INTO flashcard_cards (deck_id, front, back, front_image, back_image, order_idx) VALUES (?,?,?,?,?,?)`); const inserted = []; const ins = db.transaction(() => { cards.forEach((c, i) => { - const r = stmt.run(deck.id, c.front || '', c.back || '', maxIdx + 1 + i); - inserted.push({ id: r.lastInsertRowid, front: c.front, back: c.back }); + const front = stripTags((c.front || '').slice(0, 5000)); + const back = stripTags((c.back || '').slice(0, 5000)); + const fImg = safeImg(c.front_image); + const bImg = safeImg(c.back_image); + const r = stmt.run(deck.id, front, back, fImg, bImg, maxIdx + 1 + i); + inserted.push({ id: r.lastInsertRowid, front, back, front_image: fImg, back_image: bImg }); }); }); ins(); diff --git a/frontend/flashcards.html b/frontend/flashcards.html index 9078d8c..1f00004 100644 --- a/frontend/flashcards.html +++ b/frontend/flashcards.html @@ -164,6 +164,19 @@ #new-card-imgs { display: none; gap: 14px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; } .new-img-lbl { font-size: .7rem; font-weight: 700; color: var(--text-3); margin-right: 4px; } + /* bulk import preview */ + .bulk-preview-list { max-height: 56vh; overflow-y: auto; display: flex; flex-direction: column; + gap: 8px; margin-bottom: 6px; padding-right: 4px; } + .bulk-row { display: grid; grid-template-columns: 24px 1fr 1fr; gap: 8px; align-items: start; + background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; } + .bulk-row-n { font-size: .7rem; font-weight: 800; color: var(--text-3); padding-top: 3px; } + .bulk-row-side { display: flex; flex-direction: column; gap: 6px; min-width: 0; } + .bulk-row-txt { font-size: .8rem; color: var(--text); white-space: pre-wrap; word-break: break-word; line-height: 1.35; } + .bulk-row-empty { color: var(--text-3); font-style: italic; } + .bulk-img-wrap { position: relative; display: inline-block; } + .bulk-img-thumb { max-width: 110px; max-height: 64px; border-radius: 6px; display: block; + border: 1px solid var(--border); object-fit: cover; } + .card-add-bar { display: flex; gap: 10px; align-items: center; } .card-add-input { flex: 1; padding: 10px 14px; border: 1.5px solid var(--border); border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: .88rem; background: #fff; @@ -463,16 +476,30 @@