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);
|
||||
|
||||
+181
-26
@@ -80,6 +80,14 @@
|
||||
.card-item { background: #fff; border: 1.5px solid var(--border); border-radius: 12px;
|
||||
display: flex; gap: 0; overflow: hidden; }
|
||||
.card-item.editing { border-color: var(--violet); }
|
||||
.card-item.dragging { opacity: .45; }
|
||||
.card-item.drag-over-top { box-shadow: inset 0 3px 0 0 var(--violet); }
|
||||
.card-item.drag-over-bottom { box-shadow: inset 0 -3px 0 0 var(--violet); }
|
||||
.card-drag { display: flex; align-items: center; padding: 0 6px; cursor: grab;
|
||||
color: var(--text-3); flex-shrink: 0; border-right: 1px solid var(--border); }
|
||||
.card-drag:active { cursor: grabbing; }
|
||||
.card-drag:hover { color: var(--violet); background: var(--surface-2); }
|
||||
.card-drag .ic { width: 18px; height: 18px; }
|
||||
.card-side { flex: 1; padding: 12px 14px; min-width: 0; }
|
||||
.card-divider { width: 1px; background: var(--border); flex-shrink: 0; }
|
||||
.card-side-lbl { font-size: .68rem; font-weight: 700; text-transform: uppercase;
|
||||
@@ -101,6 +109,15 @@
|
||||
color: var(--text); outline: none; transition: .15s; }
|
||||
.card-add-input:focus { border-color: var(--violet); }
|
||||
|
||||
/* card search */
|
||||
.card-search-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
|
||||
background: #fff; border: 1.5px solid var(--border); border-radius: 10px; padding: 0 12px; }
|
||||
.card-search-bar:focus-within { border-color: var(--violet); }
|
||||
.card-search-ic { width: 16px; height: 16px; color: var(--text-3); flex-shrink: 0; }
|
||||
.card-search-input { flex: 1; border: none; outline: none; background: transparent; padding: 9px 0;
|
||||
font-family: 'Manrope', sans-serif; font-size: .86rem; color: var(--text); }
|
||||
.card-search-count { font-size: .72rem; color: var(--text-3); font-weight: 600; flex-shrink: 0; }
|
||||
|
||||
/* ── study mode ── */
|
||||
#view-study { display: none; }
|
||||
.study-wrap { max-width: 600px; margin: 0 auto; }
|
||||
@@ -145,7 +162,12 @@
|
||||
.sq-btn { padding: 10px 20px; border-radius: 10px; border: 2px solid transparent;
|
||||
cursor: pointer; font-family: 'Manrope', sans-serif; font-size: .84rem; font-weight: 700;
|
||||
transition: .18s; display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
||||
.sq-btn .sq-top { display: flex; align-items: center; gap: 6px; }
|
||||
.sq-btn .sq-days { font-size: .66rem; font-weight: 600; opacity: .65; }
|
||||
.fc-kbd { font-family: 'Manrope', sans-serif; font-size: .62rem; font-weight: 800; line-height: 1;
|
||||
padding: 2px 5px; border-radius: 5px; background: rgba(0,0,0,.08);
|
||||
border: 1px solid rgba(0,0,0,.12); color: inherit; opacity: .8; }
|
||||
.study-hint .fc-kbd { opacity: .9; }
|
||||
.sq-btn-again { background: #FEE2E2; border-color: #FECACA; color: #DC2626; }
|
||||
.sq-btn-again:hover { background: #FECACA; }
|
||||
.sq-btn-hard { background: #FEF3C7; border-color: #FDE68A; color: #D97706; }
|
||||
@@ -253,6 +275,11 @@
|
||||
<button class="fc-btn fc-btn-ghost" onclick="openBulkModal()">Добавить список</button>
|
||||
<button class="fc-btn fc-btn-primary" id="cards-study-btn" onclick="startStudy()">Начать изучение</button>
|
||||
</div>
|
||||
<div class="card-search-bar" id="card-search-bar" style="display:none">
|
||||
<svg class="ic card-search-ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input class="card-search-input" id="card-search" placeholder="Поиск по карточкам…" oninput="onCardSearch(this.value)" />
|
||||
<span class="card-search-count" id="card-search-count"></span>
|
||||
</div>
|
||||
<div class="card-list" id="card-list"></div>
|
||||
<!-- Add card row -->
|
||||
<div class="card-add-bar" style="margin-bottom:14px">
|
||||
@@ -293,13 +320,13 @@
|
||||
<span class="swipe-indicator swipe-left-ind" id="ind-left">ЕЩЁ РАЗ <svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="study-hint" id="study-flip-hint">Нажмите, чтобы увидеть ответ</div>
|
||||
<div class="study-hint" id="study-flip-hint">Нажмите или <kbd class="fc-kbd">Пробел</kbd>, чтобы увидеть ответ</div>
|
||||
|
||||
<div class="study-btns" id="study-btns">
|
||||
<button class="sq-btn sq-btn-again" onclick="answer(0)">Снова<span class="sq-days" id="sq-days-0"><1 мин</span></button>
|
||||
<button class="sq-btn sq-btn-hard" onclick="answer(3)">Трудно<span class="sq-days" id="sq-days-3">—</span></button>
|
||||
<button class="sq-btn sq-btn-good" onclick="answer(4)">Знаю<span class="sq-days" id="sq-days-4">—</span></button>
|
||||
<button class="sq-btn sq-btn-easy" onclick="answer(5)">Легко<span class="sq-days" id="sq-days-5">—</span></button>
|
||||
<button class="sq-btn sq-btn-again" onclick="answer(0)"><span class="sq-top">Снова<kbd class="fc-kbd">1</kbd></span><span class="sq-days" id="sq-days-0">1 день</span></button>
|
||||
<button class="sq-btn sq-btn-hard" onclick="answer(3)"><span class="sq-top">Трудно<kbd class="fc-kbd">2</kbd></span><span class="sq-days" id="sq-days-3">—</span></button>
|
||||
<button class="sq-btn sq-btn-good" onclick="answer(4)"><span class="sq-top">Знаю<kbd class="fc-kbd">3</kbd></span><span class="sq-days" id="sq-days-4">—</span></button>
|
||||
<button class="sq-btn sq-btn-easy" onclick="answer(5)"><span class="sq-top">Легко<kbd class="fc-kbd">4</kbd></span><span class="sq-days" id="sq-days-5">—</span></button>
|
||||
</div>
|
||||
|
||||
<!-- done screen -->
|
||||
@@ -379,6 +406,7 @@ let _curDeck = null;
|
||||
let _cards = [];
|
||||
let _editingDeckId = null;
|
||||
let _deckColor = '#9B5DE5';
|
||||
let _cardFilter = '';
|
||||
|
||||
(async () => {
|
||||
/* ── auth ── */
|
||||
@@ -402,9 +430,33 @@ let _deckColor = '#9B5DE5';
|
||||
/* ════ Init ════ */
|
||||
async function init() {
|
||||
buildColorPicker();
|
||||
bindStudyKeys();
|
||||
await loadDecks();
|
||||
}
|
||||
|
||||
/* ── keyboard shortcuts in study mode ──
|
||||
Space/Enter/↑/↓ — перевернуть; после переворота 1-4 или ←/→ — оценка. */
|
||||
function bindStudyKeys() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (document.getElementById('view-study')?.style.display !== 'block') return;
|
||||
if (document.getElementById('study-done')?.style.display === 'block') return;
|
||||
const t = e.target;
|
||||
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
||||
|
||||
const flip = ['Space', 'Enter', 'ArrowUp', 'ArrowDown'];
|
||||
if (!_studyFlipped) {
|
||||
if (flip.includes(e.code) || e.key === ' ') { e.preventDefault(); flipCard(); }
|
||||
else if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { e.preventDefault(); flipCard(); }
|
||||
return;
|
||||
}
|
||||
// flipped → grade
|
||||
const map = { Digit1: 0, Digit2: 3, Digit3: 4, Digit4: 5,
|
||||
Numpad1: 0, Numpad2: 3, Numpad3: 4, Numpad4: 5,
|
||||
ArrowLeft: 0, ArrowRight: 5 };
|
||||
if (e.code in map) { e.preventDefault(); answer(map[e.code]); }
|
||||
});
|
||||
}
|
||||
|
||||
async function loadDecks() {
|
||||
const [decks, stats] = await Promise.all([
|
||||
LS.api('/api/flashcards/decks').catch(()=>({decks:[]})),
|
||||
@@ -442,7 +494,7 @@ function renderDecks() {
|
||||
const grid = document.getElementById('deck-grid');
|
||||
if (!_decks.length) {
|
||||
grid.innerHTML = `<div class="fc-empty" style="grid-column:1/-1">
|
||||
<div class="fc-empty-icon">🃏</div>
|
||||
<div class="fc-empty-icon"><svg class="ic" style="width:46px;height:46px" viewBox="0 0 24 24"><rect x="3" y="5" width="13" height="15" rx="2"/><path d="M8 5V3.5A1.5 1.5 0 0 1 9.5 2h9A1.5 1.5 0 0 1 20 3.5V16a1.5 1.5 0 0 1-1.5 1.5H16"/></svg></div>
|
||||
<h3>Нет колод</h3>
|
||||
<p>Создайте первую колоду карточек</p>
|
||||
<button class="fc-btn fc-btn-primary" onclick="openNewDeckModal()">+ Создать колоду</button>
|
||||
@@ -485,6 +537,8 @@ async function openDeck(id) {
|
||||
document.getElementById('cards-deck-title').textContent = _curDeck.title;
|
||||
const data = await LS.api(`/api/flashcards/decks/${id}/cards`).catch(()=>({cards:[]}));
|
||||
_cards = data.cards || [];
|
||||
_cardFilter = '';
|
||||
const si = document.getElementById('card-search'); if (si) si.value = '';
|
||||
renderCardList();
|
||||
document.getElementById('view-decks').style.display = 'none';
|
||||
document.getElementById('view-cards').style.display = 'block';
|
||||
@@ -511,18 +565,48 @@ function showCards() {
|
||||
}
|
||||
|
||||
/* ════ Card list ════ */
|
||||
function onCardSearch(v) {
|
||||
_cardFilter = (v || '').trim().toLowerCase();
|
||||
renderCardList();
|
||||
}
|
||||
|
||||
function renderCardList() {
|
||||
const list = document.getElementById('card-list');
|
||||
const bar = document.getElementById('card-search-bar');
|
||||
// строка поиска появляется, когда карточек достаточно для фильтрации
|
||||
if (bar) bar.style.display = _cards.length > 4 ? 'flex' : 'none';
|
||||
|
||||
if (!_cards.length) {
|
||||
if (bar) bar.style.display = 'none';
|
||||
list.innerHTML = `<div class="fc-empty" style="padding:30px 0">
|
||||
<div class="fc-empty-icon"><svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>
|
||||
<div class="fc-empty-icon"><svg class="ic" style="width:40px;height:40px" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>
|
||||
<h3>Нет карточек</h3>
|
||||
<p>Добавьте первую карточку ниже</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = _cards.map((c, i) => `
|
||||
<div class="card-item" id="ci-${c.id}">
|
||||
|
||||
const q = _cardFilter;
|
||||
const shown = q
|
||||
? _cards.filter(c => (c.front || '').toLowerCase().includes(q) || (c.back || '').toLowerCase().includes(q))
|
||||
: _cards;
|
||||
|
||||
const cnt = document.getElementById('card-search-count');
|
||||
if (cnt) cnt.textContent = q ? `${shown.length} из ${_cards.length}` : `${_cards.length} карточек`;
|
||||
|
||||
if (!shown.length) {
|
||||
list.innerHTML = `<div class="fc-empty" style="padding:24px 0">
|
||||
<h3>Ничего не найдено</h3>
|
||||
<p>По запросу «${esc(q)}» карточек нет</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = shown.map((c) => `
|
||||
<div class="card-item" id="ci-${c.id}" data-id="${c.id}">
|
||||
${q ? '' : `<div class="card-drag" title="Перетащите для сортировки">
|
||||
<svg class="ic" viewBox="0 0 24 24"><circle cx="9" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="18" r="1"/></svg>
|
||||
</div>`}
|
||||
<div class="card-side">
|
||||
<div class="card-side-lbl">Вопрос</div>
|
||||
<textarea class="card-textarea" rows="2"
|
||||
@@ -540,6 +624,71 @@ function renderCardList() {
|
||||
</button>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
if (!q) bindCardDrag();
|
||||
}
|
||||
|
||||
/* ── drag-reorder карточек (только без активного фильтра) ── */
|
||||
let _dragId = null;
|
||||
function bindCardDrag() {
|
||||
const list = document.getElementById('card-list');
|
||||
if (!list) return;
|
||||
list.querySelectorAll('.card-item').forEach(el => {
|
||||
const handle = el.querySelector('.card-drag');
|
||||
if (!handle) return;
|
||||
// перетаскивание стартует только с ручки — textarea остаётся редактируемой
|
||||
handle.addEventListener('mousedown', () => el.setAttribute('draggable', 'true'));
|
||||
handle.addEventListener('touchstart', () => el.setAttribute('draggable', 'true'), { passive: true });
|
||||
|
||||
el.addEventListener('dragstart', (e) => {
|
||||
_dragId = +el.dataset.id;
|
||||
el.classList.add('dragging');
|
||||
try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(_dragId)); } catch {}
|
||||
});
|
||||
el.addEventListener('dragend', () => {
|
||||
el.classList.remove('dragging'); el.removeAttribute('draggable');
|
||||
list.querySelectorAll('.drag-over-top,.drag-over-bottom')
|
||||
.forEach(x => x.classList.remove('drag-over-top', 'drag-over-bottom'));
|
||||
_dragId = null;
|
||||
});
|
||||
el.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
const r = el.getBoundingClientRect();
|
||||
const after = (e.clientY - r.top) > r.height / 2;
|
||||
el.classList.toggle('drag-over-bottom', after);
|
||||
el.classList.toggle('drag-over-top', !after);
|
||||
});
|
||||
el.addEventListener('dragleave', () => el.classList.remove('drag-over-top', 'drag-over-bottom'));
|
||||
el.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
el.classList.remove('drag-over-top', 'drag-over-bottom');
|
||||
const targetId = +el.dataset.id;
|
||||
if (_dragId == null || _dragId === targetId) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const after = (e.clientY - r.top) > r.height / 2;
|
||||
moveCard(_dragId, targetId, after);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function moveCard(dragId, targetId, after) {
|
||||
const from = _cards.findIndex(c => c.id === dragId);
|
||||
if (from < 0) return;
|
||||
const item = _cards.splice(from, 1)[0];
|
||||
let to = _cards.findIndex(c => c.id === targetId);
|
||||
if (to < 0) { _cards.splice(from, 0, item); return; }
|
||||
if (after) to += 1;
|
||||
_cards.splice(to, 0, item);
|
||||
renderCardList();
|
||||
persistCardOrder();
|
||||
}
|
||||
|
||||
async function persistCardOrder() {
|
||||
if (!_curDeck) return;
|
||||
const order = _cards.map(c => c.id);
|
||||
await LS.api(`/api/flashcards/decks/${_curDeck.id}/reorder`, {
|
||||
method: 'PUT', body: JSON.stringify({ order })
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
async function addCard() {
|
||||
@@ -572,7 +721,7 @@ async function saveCard(id, field, value) {
|
||||
}
|
||||
|
||||
async function deleteCard(id) {
|
||||
if (!confirm('Удалить карточку?')) return;
|
||||
if (!await LS.confirm('Удалить карточку?', { title: 'Удаление карточки', confirmText: 'Удалить', danger: true })) return;
|
||||
await LS.api(`/api/flashcards/cards/${id}`, { method: 'DELETE' }).catch(()=>{});
|
||||
_cards = _cards.filter(c => c.id !== id);
|
||||
renderCardList();
|
||||
@@ -726,25 +875,31 @@ function finishStudy() {
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
/* ── estimated next interval preview for sq buttons ── */
|
||||
function updateSQDays(card) {
|
||||
/* ── estimated next interval preview for sq buttons ──
|
||||
ВАЖНО: точная копия серверного sm2() (flashcardController.js), иначе
|
||||
превью врёт. В чистом SM-2 интервал для q>=3 НЕ зависит от значения q
|
||||
(q влияет только на ease factor), поэтому «Трудно/Знаю/Легко» при первых
|
||||
повторениях дают одинаковый интервал — это корректно.
|
||||
(Дифференциация интервалов по кнопкам — кандидат на Фазу 4.) */
|
||||
function fcNextInterval(card, q) {
|
||||
const ef = card.ease_factor || 2.5;
|
||||
const iv = card.interval_days || 1;
|
||||
const rep = card.repetitions || 0;
|
||||
const preview = (q) => {
|
||||
if (q < 3) return '<1 мин';
|
||||
let niv;
|
||||
if (rep === 0) niv = 1;
|
||||
else if (rep === 1) niv = 6;
|
||||
else niv = Math.round(iv * ef);
|
||||
const nef = Math.max(1.3, ef + 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02));
|
||||
let n2 = (q === 3 ? Math.max(1, niv - 2) : q === 4 ? niv : Math.round(niv * nef));
|
||||
return n2 <= 1 ? '1 день' : n2 + ' дн.';
|
||||
};
|
||||
document.getElementById('sq-days-0').textContent = '<1 мин';
|
||||
document.getElementById('sq-days-3').textContent = preview(3);
|
||||
document.getElementById('sq-days-4').textContent = preview(4);
|
||||
document.getElementById('sq-days-5').textContent = preview(5);
|
||||
if (q < 3) return 1;
|
||||
if (rep === 0) return 1;
|
||||
if (rep === 1) return 6;
|
||||
return Math.round(iv * ef);
|
||||
}
|
||||
function fcDaysLabel(n) {
|
||||
if (n <= 1) return '1 день';
|
||||
if (n < 5) return n + ' дня';
|
||||
return n + ' дн.';
|
||||
}
|
||||
function updateSQDays(card) {
|
||||
document.getElementById('sq-days-0').textContent = fcDaysLabel(fcNextInterval(card, 0));
|
||||
document.getElementById('sq-days-3').textContent = fcDaysLabel(fcNextInterval(card, 3));
|
||||
document.getElementById('sq-days-4').textContent = fcDaysLabel(fcNextInterval(card, 4));
|
||||
document.getElementById('sq-days-5').textContent = fcDaysLabel(fcNextInterval(card, 5));
|
||||
}
|
||||
|
||||
/* ── touch/mouse swipe ── */
|
||||
|
||||
Reference in New Issue
Block a user