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:
@@ -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 @@
|
||||
|
||||
<!-- Col 3: Progress -->
|
||||
<div class="widget" id="w-progress-col">
|
||||
<!-- Flashcard review (random card from pool) -->
|
||||
<div id="w-flashcard" style="display:none;margin-bottom:18px">
|
||||
<div class="w-head">
|
||||
<div class="w-title">Повтори карточку</div>
|
||||
<a class="w-more" href="/flashcards">Все карточки</a>
|
||||
</div>
|
||||
<div id="fcw-body"></div>
|
||||
</div>
|
||||
<!-- Combined Activity Widget (heatmap + streak calendar) -->
|
||||
<div id="w-activity" style="display:none">
|
||||
<div class="w-head">
|
||||
@@ -3888,8 +3929,76 @@
|
||||
loadContinueWidget();
|
||||
loadWeakWidget();
|
||||
loadTheoryWidget();
|
||||
loadFlashcardWidget();
|
||||
}
|
||||
|
||||
/* ══ WIDGET: Flashcard review (random card from pool) ════════════════ */
|
||||
let _fcwTotal = 0;
|
||||
async function loadFlashcardWidget() {
|
||||
if (isTeacher) return;
|
||||
const w = document.getElementById('w-flashcard');
|
||||
if (!w || w.dataset.cfgHidden) return;
|
||||
try {
|
||||
const r = await LS.api('/api/flashcards/random');
|
||||
renderFlashcardWidget(r);
|
||||
w.style.display = '';
|
||||
} catch { /* фича выключена или ошибка — оставляем скрытым */ }
|
||||
}
|
||||
|
||||
function renderFlashcardWidget(r) {
|
||||
const body = document.getElementById('fcw-body');
|
||||
if (!body) return;
|
||||
if (!r || !r.card) {
|
||||
_fcwTotal = 0;
|
||||
body.innerHTML = `<div class="fcw-empty">
|
||||
<p>Пока нет карточек. Создавай их в любой точке системы кнопкой внизу справа.</p>
|
||||
<button class="fcw-btn" type="button" onclick="document.getElementById('fc-fab')?.click()">
|
||||
${lci('plus','width:13px;height:13px')} Создать карточку
|
||||
</button>
|
||||
</div>`;
|
||||
reIcons();
|
||||
return;
|
||||
}
|
||||
_fcwTotal = r.total || 0;
|
||||
const c = r.card;
|
||||
const back = (c.back || '').trim() || '(ответ не заполнен)';
|
||||
const col = c.deck_color || '#9B5DE5';
|
||||
body.innerHTML = `
|
||||
<div class="fcw-card" onclick="fcwFlip(this)">
|
||||
<div class="fcw-inner">
|
||||
<div class="fcw-face fcw-front">
|
||||
<div class="fcw-deck" style="color:${esc(col)}">${esc(c.deck_title || 'Карточка')}</div>
|
||||
<div class="fcw-text">${esc(c.front)}</div>
|
||||
<div class="fcw-hint">${lci('rotate-cw','width:12px;height:12px')} нажми, чтобы перевернуть</div>
|
||||
</div>
|
||||
<div class="fcw-face fcw-back">
|
||||
<div class="fcw-deck">Ответ</div>
|
||||
<div class="fcw-text">${esc(back)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcw-actions">
|
||||
<button class="fcw-btn" type="button" onclick="fcwNext(event)">
|
||||
${lci('shuffle','width:13px;height:13px')} Другая
|
||||
</button>
|
||||
<span class="fcw-count">${_fcwTotal} ${_fcwTotal === 1 ? 'карточка' : (_fcwTotal % 10 >= 2 && _fcwTotal % 10 <= 4 && (_fcwTotal % 100 < 12 || _fcwTotal % 100 > 14) ? 'карточки' : 'карточек')} в пуле</span>
|
||||
</div>`;
|
||||
reIcons();
|
||||
}
|
||||
|
||||
function fcwFlip(el) { el.classList.toggle('flipped'); }
|
||||
|
||||
async function fcwNext(e) {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const r = await LS.api('/api/flashcards/random');
|
||||
renderFlashcardWidget(r);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Обновлять виджет, когда карточку добавили через глобальный FAB
|
||||
window.addEventListener('flashcard:added', () => { if (!isTeacher) loadFlashcardWidget(); });
|
||||
|
||||
/* ══ INIT ═════════════════════════════════════════════════════════════ */
|
||||
window.addEventListener('pageshow', e => { if (e.persisted) location.reload(); });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user