Files
Learn_System/frontend/collection.html
T
Maxim Dolgolyov fd29acbbdd feat: WebSocket real-time + rAF render gate + guest board + screen picker
Classroom performance:
- WebSocket server (ws-server.js) for low-latency cursor & stroke preview
  Replaces HTTP POST per event → eliminates per-message auth overhead
  Session member cache (30s TTL) avoids SQLite query per WS message
  Fallback to HTTP POST when WS not connected
- Cursor throttle reduced 100ms → 33ms (~30fps)
- Stroke preview throttle reduced 50ms → 20ms
- whiteboard.js: render() is now rAF-gated (_doRender/_rafPending)
  Multiple render() calls within one frame collapse into one repaint
  document.hidden check — zero CPU when tab is in background
  visibilitychange listener restores canvas on tab focus

Guest board:
- guestClassroom.js route: public token-based read-only access
- guest-board.html: name entry + read-only whiteboard + SSE
- SSE: addGuestClient/removeGuestClient/emitToGuests

Screen share picker:
- Discord-style modal with tab switching (screen/window/tab)
- Live video preview before confirming share
- useExistingScreenStream() in ClassroomRTC

Fullscreen exit overlay:
- #cr-fs-exit-overlay button inside cr-board-wrap
- Visible only via CSS :fullscreen selector (touchpad users)

File sharing from library:
- Teacher picks file from library, sends as styled card in chat
- crDownloadLibraryFile() fetches with Bearer auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:04:59 +03:00

553 lines
27 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Коллекция — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<style>
.sb-content { padding: 0; }
.col-wrap {
min-height: 100vh;
padding: 28px 24px 60px;
max-width: 1100px; margin: 0 auto;
}
/* Header */
.col-header { display:flex; align-items:center; gap:14px; margin-bottom:20px; }
.col-icon {
width:52px; height:52px; border-radius:14px; flex-shrink:0;
background: linear-gradient(135deg,rgba(249,199,79,.2),rgba(155,93,229,.15));
border:1.5px solid rgba(255,255,255,.1);
display:flex; align-items:center; justify-content:center;
}
.col-icon svg { width:26px; height:26px; stroke:#F9C74F; stroke-width:1.8; fill:none; }
.col-title { font-family:'Unbounded',sans-serif; font-size:1.3rem; font-weight:800; }
.col-sub { font-size:.82rem; color:var(--text-2); margin-top:2px; }
/* Summary bar */
.col-summary {
display:flex; gap:10px; flex-wrap:wrap; margin-bottom:20px;
}
.col-sum-card {
background:var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:14px; padding:12px 18px;
display:flex; align-items:center; gap:10px;
min-width:110px;
}
.col-sum-icon { width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.col-sum-icon svg { width:16px; height:16px; stroke:currentColor; fill:none; stroke-width:2; }
.col-sum-val { font-family:'Unbounded',sans-serif; font-size:1rem; font-weight:800; }
.col-sum-label { font-size:.72rem; color:var(--text-2); margin-top:1px; }
/* Tier legend */
.tier-legend { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; align-items:center; }
.tier-dot {
display:inline-flex; align-items:center; gap:5px;
font-size:.75rem; font-weight:600; color:var(--text-2); cursor:pointer;
padding:4px 10px; border-radius:99px; border:1.5px solid transparent; transition:all .15s;
}
.tier-dot.active { color:var(--text); }
.tier-dot-circle { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
/* Filter */
.col-filter { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px; align-items:center; }
.col-pill {
padding:5px 14px; border-radius:99px; border:1.5px solid rgba(255,255,255,.12);
font-size:.78rem; font-weight:600; cursor:pointer; transition:all .15s;
background:transparent; color:var(--text-2);
}
.col-pill:hover { border-color:rgba(249,199,79,.4); color:#F9C74F; }
.col-pill.active { background:rgba(249,199,79,.12); border-color:#F9C74F; color:#F9C74F; }
/* Search */
.col-search-wrap { position:relative; margin-left:auto; }
.col-search {
padding:6px 12px 6px 34px; border-radius:99px;
border:1.5px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.05); color:var(--text);
font-family:'Manrope',sans-serif; font-size:.82rem; outline:none; width:200px;
transition:border-color .2s;
}
.col-search:focus { border-color:rgba(249,199,79,.4); }
.col-search-icon {
position:absolute; left:10px; top:50%; transform:translateY(-50%);
color:var(--text-2); pointer-events:none;
}
.col-search-icon svg { width:14px; height:14px; stroke:currentColor; fill:none; stroke-width:2; }
/* Progress bar */
.col-progress-bar {
background:var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:14px; padding:14px 18px; margin-bottom:20px;
display:flex; align-items:center; gap:16px;
}
.col-prog-label { font-size:.82rem; color:var(--text-2); flex-shrink:0; }
.col-prog-bar { flex:1; height:8px; border-radius:99px; background:rgba(255,255,255,.08); overflow:hidden; }
.col-prog-fill { height:100%; border-radius:99px; background:linear-gradient(90deg,#9B5DE5,#F9C74F); transition:width .8s ease; }
.col-prog-pct { font-family:'Unbounded',sans-serif; font-size:.88rem; font-weight:800; color:#F9C74F; flex-shrink:0; }
/* Cards grid */
.col-grid {
display:grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap:12px;
}
/* Card */
.col-card {
position:relative; cursor:pointer;
perspective:800px;
height:200px;
}
.col-card-inner {
position:absolute; inset:0;
transition:transform .5s cubic-bezier(.4,0,.2,1);
transform-style:preserve-3d;
}
.col-card:hover .col-card-inner:not(.flipped) { transform:rotateY(10deg) scale(1.03); }
.col-card-inner.flipped { transform:rotateY(180deg); }
.col-card-front, .col-card-back {
position:absolute; inset:0;
border-radius:16px; padding:14px 12px;
backface-visibility:hidden;
display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:6px;
}
.col-card-back { transform:rotateY(180deg); }
/* Tier styles */
.tier-locked .col-card-front {
background:rgba(255,255,255,.04); border:1.5px solid rgba(255,255,255,.08);
filter:grayscale(1);
}
.tier-bronze .col-card-front {
background:linear-gradient(145deg,rgba(180,80,20,.3),rgba(120,50,10,.2));
border:1.5px solid rgba(205,127,50,.35);
box-shadow:0 0 20px rgba(205,127,50,.15);
}
.tier-silver .col-card-front {
background:linear-gradient(145deg,rgba(160,160,170,.25),rgba(100,100,110,.2));
border:1.5px solid rgba(192,192,192,.4);
box-shadow:0 0 20px rgba(192,192,192,.15);
}
.tier-gold .col-card-front {
background:linear-gradient(145deg,rgba(249,199,79,.2),rgba(200,150,30,.15));
border:1.5px solid rgba(249,199,79,.45);
box-shadow:0 0 24px rgba(249,199,79,.2);
}
.tier-platinum .col-card-front {
background:linear-gradient(145deg,rgba(6,214,224,.2),rgba(155,93,229,.2));
border:1.5px solid rgba(6,214,224,.45);
box-shadow:0 0 28px rgba(6,214,224,.2);
}
/* Platinum shimmer */
.tier-platinum .col-card-front::after {
content:''; position:absolute; inset:0; border-radius:16px;
background:linear-gradient(135deg,transparent 30%,rgba(255,255,255,.06) 50%,transparent 70%);
animation:shimmer 2.5s linear infinite;
background-size:200% 200%;
}
@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
.col-card-tier-badge {
position:absolute; top:8px; right:8px;
font-size:.6rem; font-weight:800; padding:2px 7px;
border-radius:99px; text-transform:uppercase; letter-spacing:.05em;
}
.tier-locked .col-card-tier-badge { background:rgba(255,255,255,.08); color:var(--text-2); }
.tier-bronze .col-card-tier-badge { background:rgba(205,127,50,.3); color:#CD7F32; }
.tier-silver .col-card-tier-badge { background:rgba(192,192,192,.3); color:#C0C0C0; }
.tier-gold .col-card-tier-badge { background:rgba(249,199,79,.25); color:#F9C74F; }
.tier-platinum .col-card-tier-badge { background:rgba(6,214,224,.25); color:#06D6E0; }
.col-card-icon { font-size:2rem; line-height:1; }
.tier-locked .col-card-icon { opacity:.3; }
.col-card-name {
font-family:'Unbounded',sans-serif; font-size:.7rem; font-weight:800;
text-align:center; line-height:1.3; word-break:break-word;
}
.tier-locked .col-card-name { color:var(--text-2); }
.col-card-subj {
font-size:.68rem; color:var(--text-2); text-align:center;
}
/* Stars rating */
.col-card-stars { display:flex; gap:2px; }
.col-card-stars svg { width:12px; height:12px; }
/* Back face */
.col-card-back {
background:var(--surface); border:1.5px solid rgba(255,255,255,.12);
}
.col-card-back-title {
font-family:'Unbounded',sans-serif; font-size:.72rem; font-weight:800;
text-align:center; margin-bottom:6px;
}
.col-card-back-stat { font-size:.72rem; color:var(--text-2); text-align:center; }
.col-card-back-pct {
font-family:'Unbounded',sans-serif; font-size:1.3rem; font-weight:800;
color:#F9C74F;
}
.col-card-back-bar { width:80%; height:5px; border-radius:99px; background:rgba(255,255,255,.1); overflow:hidden; }
.col-card-back-fill { height:100%; border-radius:99px; }
/* Lock icon */
.col-lock-icon {
width:36px; height:36px; border-radius:50%;
background:rgba(255,255,255,.06); display:flex; align-items:center; justify-content:center;
}
.col-lock-icon svg { width:18px; height:18px; stroke:var(--text-2); fill:none; stroke-width:2; }
/* Empty state */
.col-empty {
grid-column:1/-1;
text-align:center; padding:60px; color:var(--text-2);
font-size:.88rem;
}
@media (max-width:768px) {
.col-grid { grid-template-columns:repeat(auto-fill, minmax(130px, 1fr)); }
.col-search { width:150px; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar">
<div class="sb-brand">
<a href="/dashboard" class="sb-logo"><span class="sb-lbl">Learn<span>Space</span></span></a>
<button class="sb-toggle" title="Свернуть меню"><i data-lucide="chevron-left" class="sb-icon"></i></button>
</div>
<nav class="sb-nav">
<button class="sb-link" onclick="lsSearchOpen()" title="Ctrl+K"><i data-lucide="search" class="sb-icon"></i><span class="sb-lbl">Поиск</span></button>
<a href="/dashboard" class="sb-link"><i data-lucide="home" class="sb-icon"></i><span class="sb-lbl">Дашборд</span></a>
<a href="/board" class="sb-link" id="sbl-board" style="display:none"><i data-lucide="layout-dashboard" class="sb-icon"></i><span class="sb-lbl">Доска</span></a>
<a href="/classes" class="sb-link" id="sbl-classes" style="display:none"><i data-lucide="graduation-cap" class="sb-icon"></i><span class="sb-lbl">Классы</span></a>
<a href="/library" class="sb-link"><i data-lucide="book-open" class="sb-icon"></i><span class="sb-lbl">Библиотека</span></a>
<a href="/theory" class="sb-link"><i data-lucide="brain" class="sb-icon"></i><span class="sb-lbl">Теория</span></a>
<a href="/lab" class="sb-link"><i data-lucide="atom" class="sb-icon"></i><span class="sb-lbl">Лаборатория</span></a>
<a href="/biochem" class="sb-link"><i data-lucide="flask-conical" class="sb-icon"></i><span class="sb-lbl">Биохимия</span></a>
<a href="/hangman" class="sb-link"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Виселица</span></a>
<a href="/crossword" class="sb-link"><i data-lucide="grid-3x3" class="sb-icon"></i><span class="sb-lbl">Кроссворд</span></a>
<a href="/pet" class="sb-link"><i data-lucide="heart" class="sb-icon"></i><span class="sb-lbl">Питомец</span></a>
<span class="sb-link active"><i data-lucide="layers" class="sb-icon"></i><span class="sb-lbl">Коллекция</span></span>
<a href="/knowledge-map" class="sb-link"><i data-lucide="share-2" class="sb-icon"></i><span class="sb-lbl">Карта знаний</span></a>
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Красная книга</span></a>
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
<a href="/lesson-history" class="sb-link"><i data-lucide="archive" class="sb-icon"></i><span class="sb-lbl">Архив уроков</span></a>
<div class="sb-divider"></div>
<a href="/analytics" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="bar-chart-2" class="sb-icon"></i><span class="sb-lbl">Аналитика</span></a>
<a href="/question-bank" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="database" class="sb-icon"></i><span class="sb-lbl">Банк вопросов</span></a>
<a href="/live-quiz" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="radio" class="sb-icon"></i><span class="sb-lbl">Live-квиз</span></a>
<a href="/gradebook" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="table" class="sb-icon"></i><span class="sb-lbl">Журнал</span></a>
<a href="/admin" class="sb-link" id="sbl-admin" style="display:none"><i data-lucide="settings" class="sb-icon"></i><span class="sb-lbl">Управление</span></a>
</nav>
<div style="padding:4px 2px">
<div id="notif-wrap">
<button class="sb-link" id="notif-btn" onclick="LS.notif.toggle()">
<i data-lucide="bell" class="sb-icon"></i><span class="sb-lbl">Уведомления</span>
<span class="sb-badge" id="notif-badge" style="display:none"></span>
</button>
</div>
</div>
<div class="sb-foot">
<a href="/profile" class="sb-user-row" style="text-decoration:none">
<div class="sb-avatar" id="nav-avatar">?</div>
<div class="sb-user-info">
<div class="sb-user-name" id="nav-user"></div>
<span class="sb-logout" style="pointer-events:none">Мой профиль</span>
</div>
</a>
</div>
</aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<div class="col-wrap">
<div class="col-header">
<div class="col-icon">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div>
<div class="col-title">Коллекция карточек</div>
<div class="col-sub">Открывай карточки, прокачивая знания по темам</div>
</div>
</div>
<!-- Summary -->
<div class="col-summary" id="col-summary">
<div class="col-sum-card">
<div class="col-sum-icon" style="background:rgba(249,199,79,.12);color:#F9C74F">
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-total"></div>
<div class="col-sum-label">Всего тем</div>
</div>
</div>
<div class="col-sum-card">
<div class="col-sum-icon" style="background:rgba(56,217,90,.12);color:#38D95A">
<svg viewBox="0 0 24 24"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-unlocked"></div>
<div class="col-sum-label">Открыто</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('platinum')">
<div class="col-sum-icon" style="background:rgba(6,214,224,.12);color:#06D6E0">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-plat" style="color:#06D6E0"></div>
<div class="col-sum-label">Платина</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('gold')">
<div class="col-sum-icon" style="background:rgba(249,199,79,.12);color:#F9C74F">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-gold" style="color:#F9C74F"></div>
<div class="col-sum-label">Золото</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('silver')">
<div class="col-sum-icon" style="background:rgba(192,192,192,.15);color:#C0C0C0">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-silver" style="color:#C0C0C0"></div>
<div class="col-sum-label">Серебро</div>
</div>
</div>
<div class="col-sum-card" style="cursor:pointer" onclick="setTier('bronze')">
<div class="col-sum-icon" style="background:rgba(205,127,50,.15);color:#CD7F32">
<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div>
<div class="col-sum-val" id="sum-bronze" style="color:#CD7F32"></div>
<div class="col-sum-label">Бронза</div>
</div>
</div>
</div>
<!-- Progress -->
<div class="col-progress-bar">
<div class="col-prog-label">Прогресс коллекции</div>
<div class="col-prog-bar"><div class="col-prog-fill" id="col-prog-fill" style="width:0%"></div></div>
<div class="col-prog-pct" id="col-prog-pct">0%</div>
</div>
<!-- Filters -->
<div class="col-filter">
<button class="col-pill active" data-slug="" onclick="setSubject(this,'')">Все</button>
<button class="col-pill" data-slug="bio" onclick="setSubject(this,'bio')">Биология</button>
<button class="col-pill" data-slug="chem" onclick="setSubject(this,'chem')">Химия</button>
<button class="col-pill" data-slug="math" onclick="setSubject(this,'math')">Математика</button>
<button class="col-pill" data-slug="phys" onclick="setSubject(this,'phys')">Физика</button>
<div class="col-search-wrap" style="margin-left:auto">
<div class="col-search-icon"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>
<input class="col-search" id="col-search" placeholder="Поиск темы…" oninput="renderCards()" />
</div>
</div>
<!-- Tier filter -->
<div class="tier-legend" id="tier-legend">
<div class="tier-dot active" data-tier="" onclick="setTier('')">
<div class="tier-dot-circle" style="background:var(--text-2)"></div>Все
</div>
<div class="tier-dot" data-tier="platinum" onclick="setTier('platinum')">
<div class="tier-dot-circle" style="background:#06D6E0"></div>Платина
</div>
<div class="tier-dot" data-tier="gold" onclick="setTier('gold')">
<div class="tier-dot-circle" style="background:#F9C74F"></div>Золото
</div>
<div class="tier-dot" data-tier="silver" onclick="setTier('silver')">
<div class="tier-dot-circle" style="background:#C0C0C0"></div>Серебро
</div>
<div class="tier-dot" data-tier="bronze" onclick="setTier('bronze')">
<div class="tier-dot-circle" style="background:#CD7F32"></div>Бронза
</div>
<div class="tier-dot" data-tier="locked" onclick="setTier('locked')">
<div class="tier-dot-circle" style="background:rgba(255,255,255,.2)"></div>Закрыто
</div>
</div>
<!-- Cards -->
<div class="col-grid" id="col-grid">
<div class="col-empty">
<div style="font-size:2rem;margin-bottom:8px"><svg class="ic" viewBox="0 0 24 24"><path d="M5 22h14M5 2h14M17 22v-4.17a2 2 0 0 0-.59-1.41L12 12l-4.41 4.42A2 2 0 0 0 7 17.83V22M7 2v4.17a2 2 0 0 0 .59 1.41L12 12l4.41-4.42A2 2 0 0 0 17 6.17V2"/></svg></div>
Загружаем коллекцию…
</div>
</div>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script>
(async () => {
if (!LS.requireAuth()) return;
const user = LS.getUser();
LS.applyRoleSidebar(user);
if (user) {
document.getElementById('nav-avatar').textContent =
(user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
document.getElementById('nav-user').textContent = user.name || '—';
if (user.role === 'admin') document.getElementById('sbl-admin').style.display = '';
LS.showBoardIfAllowed();
if (user.role !== 'student') {
document.getElementById('sbl-classes').style.display = '';
}
}
LS.sidebar?.init();
lucide.createIcons();
await loadCollection();
})();
let _cards = [];
let _filterSubject = '';
let _filterTier = '';
/* ── Subject icons ── */
const SUBJ_ICONS = { bio:'<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg>', chem:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg>', math:'<svg class="ic" viewBox="0 0 24 24"><path d="m15 5 6.3 6.3a2.4 2.4 0 0 1 0 3.4L17 19"/><path d="M9.586 5.586A2 2 0 0 0 8.172 5H3a1 1 0 0 0-1 1v5.172a2 2 0 0 0 .586 1.414L8.29 18.29a2.426 2.426 0 0 0 3.42 0l3.58-3.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="6.5" cy="9.5" r=".5" fill="currentColor"/></svg>', phys:'<svg class="ic" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' };
/* ── Star SVG ── */
function starSVG(filled, color) {
return `<svg viewBox="0 0 24 24" fill="${filled ? color : 'none'}" stroke="${color}" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>`;
}
const TIER_COLOR = { platinum:'#06D6E0', gold:'#F9C74F', silver:'#C0C0C0', bronze:'#CD7F32', locked:'rgba(255,255,255,.2)' };
const TIER_LABEL = { platinum:'Платина', gold:'Золото', silver:'Серебро', bronze:'Бронза', locked:'Закрыто' };
const TIER_STARS = { platinum:5, gold:4, silver:3, bronze:1, locked:0 };
/* ── Load ── */
async function loadCollection() {
const data = await LS.api('/api/collection').catch(() => null);
if (!data) return;
_cards = data.cards;
document.getElementById('sum-total').textContent = data.totalTopics;
document.getElementById('sum-unlocked').textContent = data.unlockedTopics;
document.getElementById('sum-plat').textContent = data.platinumCount;
document.getElementById('sum-gold').textContent = data.goldCount;
document.getElementById('sum-silver').textContent = data.silverCount;
document.getElementById('sum-bronze').textContent = data.bronzeCount;
const pct = data.totalTopics > 0 ? Math.round(data.unlockedTopics / data.totalTopics * 100) : 0;
document.getElementById('col-prog-fill').style.width = pct + '%';
document.getElementById('col-prog-pct').textContent = pct + '%';
renderCards();
}
/* ── Filters ── */
function setSubject(el, slug) {
document.querySelectorAll('.col-pill').forEach(p => p.classList.remove('active'));
el.classList.add('active');
_filterSubject = slug;
renderCards();
}
function setTier(tier) {
document.querySelectorAll('.tier-dot').forEach(d => d.classList.remove('active'));
const el = document.querySelector(`.tier-dot[data-tier="${tier}"]`);
if (el) el.classList.add('active');
_filterTier = tier;
renderCards();
}
/* ── Render ── */
function renderCards() {
const q = document.getElementById('col-search').value.trim().toLowerCase();
let cards = _cards;
if (_filterSubject) cards = cards.filter(c => c.subjectSlug === _filterSubject);
if (_filterTier) cards = cards.filter(c => c.tier === _filterTier);
if (q) cards = cards.filter(c => c.topicName.toLowerCase().includes(q) || c.subjectName.toLowerCase().includes(q));
const grid = document.getElementById('col-grid');
if (!cards.length) {
grid.innerHTML = `<div class="col-empty"><div style="font-size:2rem;margin-bottom:8px"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>Ничего не найдено</div>`;
return;
}
// Sort: unlocked first, then by tier weight
const TIER_WEIGHT = { platinum:4, gold:3, silver:2, bronze:1, locked:0 };
cards = [...cards].sort((a,b) => (TIER_WEIGHT[b.tier]||0) - (TIER_WEIGHT[a.tier]||0));
grid.innerHTML = cards.map(card => buildCard(card)).join('');
}
function buildCard(c) {
const tierColor = TIER_COLOR[c.tier] || 'rgba(255,255,255,.2)';
const tierLbl = TIER_LABEL[c.tier] || '';
const stars = TIER_STARS[c.tier] || 0;
const icon = SUBJ_ICONS[c.subjectSlug] || '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>';
const starsHtml = [1,2,3,4,5].map(i =>
starSVG(i <= stars, tierColor)
).join('');
const frontContent = c.tier === 'locked'
? `<div class="col-lock-icon"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="col-card-name">${escHtml(c.topicName)}</div>
<div class="col-card-subj">${escHtml(c.subjectName)}</div>`
: `<div class="col-card-icon">${icon}</div>
<div class="col-card-name">${escHtml(c.topicName)}</div>
<div class="col-card-subj">${escHtml(c.subjectName)}</div>
<div class="col-card-stars">${starsHtml}</div>`;
const backContent = c.tier === 'locked'
? `<div class="col-card-back-title">${escHtml(c.topicName)}</div>
<div class="col-card-back-stat">Нет правильных ответов</div>
<div class="col-card-back-stat" style="margin-top:6px">Проходи тесты,<br>чтобы открыть карточку</div>`
: `<div class="col-card-back-title">${escHtml(c.topicName)}</div>
<div class="col-card-back-pct">${c.masteryPct}%</div>
<div class="col-card-back-stat">мастерство</div>
<div class="col-card-back-bar" style="margin-top:6px">
<div class="col-card-back-fill" style="width:${c.masteryPct}%;background:${tierColor}"></div>
</div>
<div class="col-card-back-stat" style="margin-top:6px">${c.correctCount}/${c.totalAttempts} попыток</div>`;
return `<div class="col-card tier-${c.tier}" onclick="flipCard(this)">
<div class="col-card-inner">
<div class="col-card-front">
<span class="col-card-tier-badge">${tierLbl}</span>
${frontContent}
</div>
<div class="col-card-back">${backContent}</div>
</div>
</div>`;
}
function flipCard(el) {
el.querySelector('.col-card-inner').classList.toggle('flipped');
}
function escHtml(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
LS.notif?.init();
</script>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>