fd29acbbdd
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>
553 lines
27 KiB
HTML
553 lines
27 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
LS.notif?.init();
|
||
</script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/search.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
</body>
|
||
</html>
|