feat(prep): фронтенд мастер-флага ЦТ — папка-коллекция карточек + тумблер у учителя
- flashcards.html: колоды коллекции рендерятся сворачиваемой папкой «Подготовка к ЦТ» (deckCardHtml вынесен, секции <details> по collection; метки из LS.prepListTracks) - classes.html: в таблице учеников колонка «ЦТ» с тумблером флага + кнопки «Весь класс → ЦТ»/ «Снять ЦТ» (LS.prepClassStatus/prepSetStudent/prepUnsetStudent/prepSetClass) Иконки — inline SVG, без эмодзи. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+92
-39
@@ -116,6 +116,21 @@
|
||||
.deck-add svg { transition: opacity .2s; opacity: .4; }
|
||||
.deck-add:hover svg { opacity: .9; }
|
||||
|
||||
/* ── коллекция-папка колод (напр. «Подготовка к ЦТ») ── */
|
||||
.deck-collection { border: 1px solid var(--border); border-radius: 18px; padding: 2px 14px 16px;
|
||||
background: rgba(123,97,255,.045); margin-top: 6px; }
|
||||
.deck-coll-head { display: flex; align-items: center; gap: 10px; cursor: pointer; list-style: none;
|
||||
padding: 13px 4px; font-weight: 700; color: var(--text); user-select: none; }
|
||||
.deck-coll-head::-webkit-details-marker { display: none; }
|
||||
.deck-coll-head > .ic { width: 19px; height: 19px; color: var(--violet); flex: none; }
|
||||
.deck-coll-title { font-size: 1.03rem; }
|
||||
.deck-coll-count { font-size: .82rem; font-weight: 600; color: #94a3b8; }
|
||||
.deck-coll-chev { margin-left: auto; transition: transform .2s; opacity: .55; width: 18px; height: 18px; }
|
||||
.deck-collection[open] .deck-coll-chev { transform: rotate(180deg); }
|
||||
.deck-coll-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 18px; }
|
||||
@media (max-width: 760px) { .deck-coll-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; } }
|
||||
@media (max-width: 480px) { .deck-coll-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ── card list (deck detail) ── */
|
||||
#view-cards { display: none; }
|
||||
.card-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px; }
|
||||
@@ -732,11 +747,17 @@ function bindStudyKeys() {
|
||||
});
|
||||
}
|
||||
|
||||
let _collLabels = null; // { collectionKey: label } для заголовков папок-коллекций
|
||||
async function loadDecks() {
|
||||
const [decks, stats] = await Promise.all([
|
||||
const [decks, stats, tracks] = await Promise.all([
|
||||
LS.api('/api/flashcards/decks').catch(()=>({decks:[]})),
|
||||
LS.api('/api/flashcards/stats').catch(()=>null),
|
||||
_collLabels ? Promise.resolve(null) : LS.prepListTracks().catch(()=>null),
|
||||
]);
|
||||
if (tracks && tracks.tracks) { // collection-ключ == ключ трека (1:1)
|
||||
_collLabels = {};
|
||||
tracks.tracks.forEach(t => { _collLabels[t.key] = t.label || t.title || t.key; });
|
||||
}
|
||||
_decks = decks.decks || [];
|
||||
renderStats(stats);
|
||||
renderDecks();
|
||||
@@ -769,6 +790,53 @@ function renderStats(s) {
|
||||
}
|
||||
|
||||
/* ════ Deck grid ════ */
|
||||
/* HTML одной карточки колоды (общий грид + папки-коллекции). */
|
||||
function deckCardHtml(d) {
|
||||
const due = d.due_count;
|
||||
const color = d.color || '#9B5DE5';
|
||||
const letter = (d.title || '?')[0].toUpperCase();
|
||||
const shadow = _hexAlpha(color, .22);
|
||||
const dueHtml = due > 0
|
||||
? `<span class="deck-badge due"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>${due} к повторению</span>`
|
||||
: `<span class="deck-badge zero"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>Актуально</span>`;
|
||||
// Общая колода (назначена мне учителем/направлением): бейдж владельца, без карандаша правки.
|
||||
const sharedHtml = d.shared
|
||||
? `<span class="deck-badge shared" title="Колода от учителя"><svg class="ic" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${esc(d.owner_name || 'учитель')}</span>`
|
||||
: '';
|
||||
return `<div class="deck-card${d.shared ? ' shared' : ''}" style="--dc-shadow:${shadow}">
|
||||
<div class="deck-head" style="background:${color}" onclick="openDeck(${d.id})">
|
||||
<div class="deck-head-letter">${letter}</div>
|
||||
<span class="deck-head-count">${d.card_count} карт.</span>
|
||||
</div>
|
||||
<div class="deck-body" onclick="openDeck(${d.id})">
|
||||
<div class="deck-name">${esc(d.title)}</div>
|
||||
${d.description ? `<div class="deck-desc">${esc(d.description)}</div>` : ''}
|
||||
<div class="deck-meta">${dueHtml}${sharedHtml}</div>
|
||||
</div>
|
||||
<div class="deck-actions">
|
||||
<button class="deck-btn-study" onclick="openDeckStudy(${d.id})" ${d.card_count===0?'disabled':''}>
|
||||
${due > 0
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Повторить (${due})`
|
||||
: d.card_count > 0
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Изучать`
|
||||
: 'Нет карточек'}
|
||||
</button>
|
||||
<button class="deck-btn-edit" onclick="openDeck(${d.id})" title="${d.shared ? 'Открыть' : 'Редактировать'}">
|
||||
${d.shared
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>`
|
||||
: `<svg class="ic" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _plDecks(n) {
|
||||
const a = n % 10, b = n % 100;
|
||||
const w = (a === 1 && b !== 11) ? 'колода'
|
||||
: (a >= 2 && a <= 4 && (b < 10 || b >= 20)) ? 'колоды' : 'колод';
|
||||
return n + ' ' + w;
|
||||
}
|
||||
|
||||
function renderDecks() {
|
||||
const grid = document.getElementById('deck-grid');
|
||||
if (!_decks.length) {
|
||||
@@ -780,47 +848,32 @@ function renderDecks() {
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = _decks.map(d => {
|
||||
const due = d.due_count;
|
||||
const color = d.color || '#9B5DE5';
|
||||
const letter = (d.title || '?')[0].toUpperCase();
|
||||
const shadow = _hexAlpha(color, .22);
|
||||
const dueHtml = due > 0
|
||||
? `<span class="deck-badge due"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>${due} к повторению</span>`
|
||||
: `<span class="deck-badge zero"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>Актуально</span>`;
|
||||
// Общая колода (назначена мне учителем): бейдж владельца, без карандаша правки.
|
||||
const sharedHtml = d.shared
|
||||
? `<span class="deck-badge shared" title="Колода от учителя"><svg class="ic" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>${esc(d.owner_name || 'учитель')}</span>`
|
||||
: '';
|
||||
return `<div class="deck-card${d.shared ? ' shared' : ''}" style="--dc-shadow:${shadow}">
|
||||
<div class="deck-head" style="background:${color}" onclick="openDeck(${d.id})">
|
||||
<div class="deck-head-letter">${letter}</div>
|
||||
<span class="deck-head-count">${d.card_count} карт.</span>
|
||||
</div>
|
||||
<div class="deck-body" onclick="openDeck(${d.id})">
|
||||
<div class="deck-name">${esc(d.title)}</div>
|
||||
${d.description ? `<div class="deck-desc">${esc(d.description)}</div>` : ''}
|
||||
<div class="deck-meta">${dueHtml}${sharedHtml}</div>
|
||||
</div>
|
||||
<div class="deck-actions">
|
||||
<button class="deck-btn-study" onclick="openDeckStudy(${d.id})" ${d.card_count===0?'disabled':''}>
|
||||
${due > 0
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Повторить (${due})`
|
||||
: d.card_count > 0
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>Изучать`
|
||||
: 'Нет карточек'}
|
||||
</button>
|
||||
<button class="deck-btn-edit" onclick="openDeck(${d.id})" title="${d.shared ? 'Открыть' : 'Редактировать'}">
|
||||
${d.shared
|
||||
? `<svg class="ic" viewBox="0 0 24 24"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>`
|
||||
: `<svg class="ic" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') + `<div class="deck-add" onclick="openNewDeckModal()">
|
||||
// Колоды без коллекции — обычный грид; колоды коллекций — отдельными папками снизу.
|
||||
const ungrouped = _decks.filter(d => !d.collection);
|
||||
const byColl = {};
|
||||
for (const d of _decks) if (d.collection) (byColl[d.collection] = byColl[d.collection] || []).push(d);
|
||||
|
||||
let html = ungrouped.map(deckCardHtml).join('') + `<div class="deck-add" onclick="openNewDeckModal()">
|
||||
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
|
||||
Новая колода
|
||||
</div>`;
|
||||
|
||||
for (const coll of Object.keys(byColl)) {
|
||||
const decks = byColl[coll];
|
||||
const label = (_collLabels && _collLabels[coll]) || coll;
|
||||
const totalDue = decks.reduce((s, d) => s + (d.due_count || 0), 0);
|
||||
const sub = `${_plDecks(decks.length)}${totalDue ? ` · ${totalDue} к повторению` : ''}`;
|
||||
html += `<details class="deck-collection" open style="grid-column:1/-1">
|
||||
<summary class="deck-coll-head">
|
||||
<svg class="ic" viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
||||
<span class="deck-coll-title">${esc(label)}</span>
|
||||
<span class="deck-coll-count">${sub}</span>
|
||||
<svg class="ic deck-coll-chev" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</summary>
|
||||
<div class="deck-coll-grid">${decks.map(deckCardHtml).join('')}</div>
|
||||
</details>`;
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ════ Open deck (card editor) ════ */
|
||||
|
||||
Reference in New Issue
Block a user