be4d43105e
Node.js/Express backend + vanilla JS frontend. Features: real-time collaborative whiteboard (SSE), multi-page support, LaTeX formulas, shapes/connectors, coordinate systems, number lines, compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with rotation & resize controls, minimap navigation overlay, auto-measurements, multi-page thumbnails sidebar, PNG export, page templates. Student/teacher workflows: classes, assignments, library, dashboard. Mobile responsive. SQLite (better-sqlite3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
779 lines
41 KiB
HTML
779 lines
41 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" />
|
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
<style>
|
|
.sb-content { background: #f4f5f8; }
|
|
|
|
/* ── page header ── */
|
|
.page-header {
|
|
background: linear-gradient(140deg, #0a0818 0%, #1a1040 50%, #0d1a30 100%);
|
|
padding: 40px 28px 36px;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.page-header::before {
|
|
content: '';
|
|
position: absolute; inset: 0;
|
|
background-image: radial-gradient(circle, rgba(255,255,255,0.04) 1px, transparent 1px);
|
|
background-size: 22px 22px; pointer-events: none;
|
|
}
|
|
.page-header::after {
|
|
content: '';
|
|
position: absolute; width: 400px; height: 400px; border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(155,93,229,0.25), transparent 65%);
|
|
top: -180px; right: -60px; pointer-events: none;
|
|
}
|
|
.ph-inner {
|
|
position: relative; z-index: 1;
|
|
max-width: 960px; margin: 0 auto;
|
|
display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap;
|
|
}
|
|
.ph-title {
|
|
font-family: 'Unbounded', sans-serif; font-size: 1.6rem; font-weight: 800;
|
|
color: #fff; letter-spacing: -0.03em; margin-bottom: 6px;
|
|
}
|
|
.ph-sub { font-size: 0.88rem; color: rgba(255,255,255,0.5); font-weight: 500; }
|
|
.btn-new-course {
|
|
padding: 10px 22px; border: none; border-radius: 999px;
|
|
background: var(--violet); color: #fff;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700;
|
|
cursor: pointer; display: flex; align-items: center; gap: 8px;
|
|
box-shadow: 0 4px 18px rgba(155,93,229,0.4);
|
|
transition: all 0.18s; flex-shrink: 0;
|
|
}
|
|
.btn-new-course:hover { background: #8a47d8; box-shadow: 0 6px 24px rgba(155,93,229,0.5); }
|
|
|
|
/* ── content ── */
|
|
.container { max-width: 960px; margin: 0 auto; padding: 28px 24px 80px; }
|
|
|
|
/* ── subject filter ── */
|
|
.subj-filter {
|
|
display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 28px;
|
|
}
|
|
.sf-btn {
|
|
padding: 8px 18px; border-radius: 999px;
|
|
border: 1.5px solid rgba(15,23,42,0.1);
|
|
background: #fff; color: #6B7A8E;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
|
cursor: pointer; transition: all 0.15s;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.sf-btn:hover { border-color: var(--violet); color: var(--violet); }
|
|
.sf-btn.active { background: var(--violet); color: #fff; border-color: var(--violet); }
|
|
|
|
/* ── course card grid ── */
|
|
.courses-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.course-card {
|
|
background: #fff;
|
|
border: 1.5px solid rgba(15,23,42,0.07);
|
|
border-radius: 20px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
text-decoration: none; color: inherit;
|
|
transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s;
|
|
display: flex; flex-direction: column;
|
|
box-shadow: 0 2px 8px rgba(15,23,42,0.05);
|
|
}
|
|
.course-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 12px 36px rgba(15,23,42,0.12);
|
|
border-color: rgba(155,93,229,0.2);
|
|
}
|
|
.cc-banner {
|
|
height: 80px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 2.8rem;
|
|
position: relative;
|
|
}
|
|
.cc-banner-bio { background: linear-gradient(135deg, #1a0a3c 0%, #2d0856 100%); }
|
|
.cc-banner-chem { background: linear-gradient(135deg, #062a20 0%, #064a38 100%); }
|
|
.cc-banner-math { background: linear-gradient(135deg, #061828 0%, #062848 100%); }
|
|
.cc-banner-phys { background: linear-gradient(135deg, #1c1400 0%, #3a2800 100%); }
|
|
.cc-banner-other { background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%); }
|
|
.cc-draft-badge {
|
|
position: absolute; top: 8px; right: 10px;
|
|
font-size: 0.64rem; font-weight: 700; font-family: 'Manrope', sans-serif;
|
|
color: rgba(255,255,255,0.5);
|
|
background: rgba(0,0,0,0.35); padding: 2px 8px; border-radius: 99px;
|
|
}
|
|
.cc-del-btn {
|
|
position: absolute; top: 8px; left: 10px; z-index: 2;
|
|
width: 28px; height: 28px; border-radius: 8px; border: none;
|
|
background: rgba(0,0,0,0.35); color: rgba(255,255,255,0.5);
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
opacity: 0; transition: all 0.15s;
|
|
}
|
|
.course-card:hover .cc-del-btn { opacity: 1; }
|
|
.cc-del-btn:hover { background: rgba(239,71,111,0.7); color: #fff; }
|
|
.cc-body { padding: 18px 18px 16px; flex: 1; display: flex; flex-direction: column; }
|
|
.cc-subj {
|
|
font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
|
|
margin-bottom: 6px;
|
|
}
|
|
.cc-subj-bio { color: #9B5DE5; }
|
|
.cc-subj-chem { color: #06D6A0; }
|
|
.cc-subj-math { color: #06B6D4; }
|
|
.cc-subj-phys { color: #F59E0B; }
|
|
.cc-subj-other { color: #8898AA; }
|
|
.cc-title {
|
|
font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 800;
|
|
color: #0F172A; margin-bottom: 8px; line-height: 1.35;
|
|
}
|
|
.cc-desc {
|
|
font-size: 0.8rem; color: #6B7A8E; line-height: 1.6;
|
|
flex: 1; margin-bottom: 14px;
|
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
}
|
|
.cc-footer {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
border-top: 1px solid rgba(15,23,42,0.07); padding-top: 12px;
|
|
}
|
|
.cc-meta { font-size: 0.76rem; color: #8898AA; display: flex; align-items: center; gap: 5px; }
|
|
.cc-progress-bar {
|
|
height: 4px; border-radius: 99px;
|
|
background: rgba(15,23,42,0.07); flex: 1; max-width: 80px;
|
|
}
|
|
.cc-progress-fill { height: 100%; border-radius: 99px; background: var(--violet); transition: width 0.4s; }
|
|
.cc-pct { font-size: 0.72rem; font-weight: 700; color: var(--violet); }
|
|
|
|
/* ── nav active ── */
|
|
.nav-active {
|
|
background: rgba(155,93,229,0.08) !important;
|
|
border-color: var(--violet) !important;
|
|
color: var(--violet) !important;
|
|
cursor: default; pointer-events: none;
|
|
}
|
|
|
|
/* ── notif ── */
|
|
.notif-bell { position: relative; }
|
|
.notif-badge { position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 4px; background: var(--pink); color: #fff; border-radius: 99px; font-size: 0.65rem; font-weight: 700; display: flex; align-items: center; justify-content: center; line-height: 1; }
|
|
.notif-drop-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px 10px; border-bottom: 1px solid var(--border); }
|
|
.notif-drop-title { font-family: 'Unbounded', sans-serif; font-size: 0.78rem; font-weight: 800; }
|
|
.notif-read-all { background: none; border: none; font-size: 0.74rem; color: var(--violet); cursor: pointer; font-family: 'Manrope', sans-serif; font-weight: 600; }
|
|
.notif-item { display: flex; gap: 10px; padding: 11px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background var(--tr); text-decoration: none; color: inherit; }
|
|
.notif-item:last-child { border-bottom: none; }
|
|
.notif-item:hover { background: rgba(155,93,229,0.04); }
|
|
.notif-item.unread { background: rgba(155,93,229,0.05); }
|
|
.notif-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--violet); flex-shrink: 0; margin-top: 5px; }
|
|
.notif-dot.read { background: transparent; border: 1.5px solid var(--border-h); }
|
|
.notif-msg { font-size: 0.80rem; line-height: 1.45; flex: 1; }
|
|
.notif-time { font-size: 0.70rem; color: var(--text-3); margin-top: 2px; }
|
|
.notif-empty { padding: 28px 16px; text-align: center; color: var(--text-3); font-size: 0.84rem; }
|
|
|
|
/* ── new course modal ── */
|
|
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,0.4); backdrop-filter: blur(6px); z-index: 200; display: none; align-items: center; justify-content: center; padding: 20px; }
|
|
.modal-overlay.open { display: flex; }
|
|
.modal { background: #fff; border-radius: 24px; padding: 36px; width: 100%; max-width: 460px; box-shadow: 0 32px 80px rgba(15,23,42,0.22); }
|
|
.modal-title { font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800; margin-bottom: 22px; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-label { display: block; font-size: 0.8rem; font-weight: 700; color: #3D4F6B; margin-bottom: 6px; }
|
|
.form-input { width: 100%; padding: 11px 14px; border: 1.5px solid rgba(15,23,42,0.15); border-radius: 12px; font-family: 'Manrope', sans-serif; font-size: 0.92rem; color: var(--text); background: #f8f9fc; transition: border-color 0.2s; box-sizing: border-box; }
|
|
.form-input:focus { outline: none; border-color: var(--violet); background: #fff; }
|
|
select.form-input { cursor: pointer; }
|
|
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 22px; }
|
|
.btn-cancel { padding: 10px 22px; border: 1.5px solid rgba(15,23,42,0.15); border-radius: 999px; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: #8898AA; cursor: pointer; transition: all 0.18s; }
|
|
.btn-cancel:hover { border-color: rgba(15,23,42,0.3); color: var(--text); }
|
|
.btn-primary { padding: 10px 28px; border: none; border-radius: 999px; background: var(--violet); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700; cursor: pointer; box-shadow: 0 2px 10px rgba(155,93,229,0.3); transition: all 0.18s; }
|
|
.btn-primary:hover { background: #8a47d8; }
|
|
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
|
|
|
|
/* ── search ── */
|
|
.search-wrap {
|
|
position: relative; max-width: 400px;
|
|
}
|
|
.search-input {
|
|
width: 100%; padding: 9px 16px 9px 38px;
|
|
border: 1.5px solid rgba(255,255,255,0.15); border-radius: 999px;
|
|
background: rgba(255,255,255,0.09); color: #fff;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.88rem;
|
|
backdrop-filter: blur(10px); box-sizing: border-box;
|
|
transition: border-color 0.2s, background 0.2s;
|
|
}
|
|
.search-input::placeholder { color: rgba(255,255,255,0.35); }
|
|
.search-input:focus { outline: none; border-color: rgba(155,93,229,0.6); background: rgba(255,255,255,0.13); }
|
|
.search-icon {
|
|
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
|
pointer-events: none; color: rgba(255,255,255,0.35);
|
|
}
|
|
.search-results-count {
|
|
font-size: 0.76rem; color: rgba(255,255,255,0.4); margin-top: 6px;
|
|
}
|
|
|
|
/* ── continue banner ── */
|
|
.continue-banner {
|
|
background: linear-gradient(135deg, rgba(155,93,229,0.12), rgba(6,182,212,0.08));
|
|
border: 1.5px solid rgba(155,93,229,0.2); border-radius: 18px;
|
|
padding: 18px 20px; margin-bottom: 24px;
|
|
display: flex; align-items: center; gap: 16px;
|
|
text-decoration: none; color: inherit;
|
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
}
|
|
.continue-banner:hover { border-color: rgba(155,93,229,0.4); box-shadow: 0 4px 20px rgba(155,93,229,0.12); }
|
|
.cb-icon {
|
|
width: 42px; height: 42px; border-radius: 12px; flex-shrink: 0;
|
|
background: linear-gradient(135deg, var(--violet), #06B6D4);
|
|
display: flex; align-items: center; justify-content: center; font-size: 1.25rem;
|
|
}
|
|
.cb-body { flex: 1; }
|
|
.cb-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--violet); margin-bottom: 3px; }
|
|
.cb-title { font-size: 0.92rem; font-weight: 700; color: #0F172A; }
|
|
.cb-course { font-size: 0.76rem; color: #6B7A8E; margin-top: 2px; }
|
|
.cb-arrow { color: #CBD5E1; }
|
|
.continue-banner:hover .cb-arrow { color: var(--violet); }
|
|
|
|
/* ── Mobile adaptation ── */
|
|
@media (max-width: 768px) {
|
|
/* Page header: compact padding, smaller title */
|
|
.page-header { padding: 20px 16px 20px; }
|
|
.ph-title { font-size: 1.15rem; }
|
|
.ph-sub { font-size: 0.8rem; }
|
|
.ph-inner { gap: 12px; }
|
|
|
|
/* Search: full width below title */
|
|
.search-wrap { max-width: 100%; }
|
|
.search-input { font-size: 0.82rem; }
|
|
|
|
/* Header buttons: icon-only to save space */
|
|
.btn-new-course { padding: 8px 14px; font-size: 0.8rem; gap: 6px; }
|
|
|
|
/* Container: tighter padding */
|
|
.container { padding: 18px 12px 80px; }
|
|
|
|
/* Course grid: 1 column on small screens */
|
|
.courses-grid { grid-template-columns: 1fr; gap: 12px; }
|
|
|
|
/* Continue banner: allow body to shrink */
|
|
.cb-body { min-width: 0; overflow: hidden; }
|
|
.cb-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.cb-icon { width: 36px; height: 36px; font-size: 1rem; }
|
|
|
|
/* Modal: less padding, full width */
|
|
.modal { padding: 22px 18px; border-radius: 18px; }
|
|
.modal-title { font-size: 0.9rem; margin-bottom: 16px; }
|
|
.modal-footer { flex-wrap: wrap; }
|
|
.modal-footer .btn-cancel,
|
|
.modal-footer .btn-primary { flex: 1; text-align: center; justify-content: center; }
|
|
|
|
/* Subject filter: smaller pills */
|
|
.sf-btn { padding: 6px 13px; font-size: 0.78rem; }
|
|
|
|
/* Empty state: reduce padding */
|
|
.empty-state { padding: 36px 20px; }
|
|
}
|
|
|
|
/* ── empty ── */
|
|
.empty-state {
|
|
text-align: center; padding: 60px 28px;
|
|
background: #fff; border: 2px dashed rgba(155,93,229,0.15);
|
|
border-radius: 24px;
|
|
}
|
|
.empty-state-icon { font-size: 3.5rem; margin-bottom: 12px; }
|
|
.empty-state-title { font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800; color: #0F172A; margin-bottom: 8px; }
|
|
.empty-state-desc { font-size: 0.86rem; color: #6B7A8E; max-width: 300px; margin: 0 auto; line-height: 1.7; }
|
|
</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="btn-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="btn-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>
|
|
<span class="sb-link active"><i data-lucide="brain" class="sb-icon"></i><span class="sb-lbl">Теория</span></span>
|
|
<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>
|
|
<a href="/collection" class="sb-link"><i data-lucide="layers" class="sb-icon"></i><span class="sb-lbl">Коллекция</span></a>
|
|
<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>
|
|
<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="btn-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">
|
|
|
|
<!-- Page header -->
|
|
<div class="page-header">
|
|
<div class="ph-inner">
|
|
<div>
|
|
<div class="ph-title">Теория</div>
|
|
<div class="ph-sub">Изучай курсы и прокачивай знания</div>
|
|
</div>
|
|
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
|
|
<div class="search-wrap">
|
|
<i data-lucide="search" class="search-icon" style="width:15px;height:15px"></i>
|
|
<input class="search-input" id="search-input" placeholder="Поиск курсов и уроков…"
|
|
oninput="onSearchInput(this.value)" />
|
|
</div>
|
|
<button class="btn-new-course" id="btn-from-tpl" style="display:none;background:transparent;border:1.5px solid rgba(255,255,255,0.2);box-shadow:none;color:rgba(255,255,255,0.7)" onclick="openTplBrowser()">
|
|
<i data-lucide="clipboard" style="width:16px;height:16px"></i> Из шаблона
|
|
</button>
|
|
<button class="btn-new-course" id="btn-new-course" style="display:none" onclick="openNewCourseModal()">
|
|
<i data-lucide="plus" style="width:16px;height:16px"></i> Новый курс
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<!-- Continue reading banner -->
|
|
<div id="continue-wrap" style="display:none"></div>
|
|
|
|
<!-- Subject filter -->
|
|
<div class="subj-filter" id="subj-filter">
|
|
<button class="sf-btn active" data-subj="" onclick="setFilter(this,'')">Все</button>
|
|
<button class="sf-btn" data-subj="bio" onclick="setFilter(this,'bio')">Биология</button>
|
|
<button class="sf-btn" data-subj="chem" onclick="setFilter(this,'chem')">Химия</button>
|
|
<button class="sf-btn" data-subj="math" onclick="setFilter(this,'math')">Математика</button>
|
|
<button class="sf-btn" data-subj="phys" onclick="setFilter(this,'phys')"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg> Физика</button>
|
|
</div>
|
|
|
|
<!-- Search results count -->
|
|
<div id="search-results-info" style="display:none;margin-bottom:12px" class="search-results-count"></div>
|
|
|
|
<!-- Courses grid -->
|
|
<div class="courses-grid" id="courses-grid">
|
|
<div style="grid-column:1/-1">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /container -->
|
|
</div><!-- /sb-content -->
|
|
</div><!-- /app-layout -->
|
|
|
|
<!-- New course modal -->
|
|
<div class="modal-overlay" id="new-course-modal" onclick="if(event.target===this)closeNewCourseModal()">
|
|
<div class="modal">
|
|
<div class="modal-title">Новый курс</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Предмет</label>
|
|
<select class="form-input" id="nc-subject">
|
|
<option value="bio">Биология</option>
|
|
<option value="chem">Химия</option>
|
|
<option value="math">Математика</option>
|
|
<option value="phys">Физика</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Название</label>
|
|
<input class="form-input" id="nc-title" placeholder="Например: Молекулярная биология" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Описание (необязательно)</label>
|
|
<textarea class="form-input" id="nc-desc" rows="2" placeholder="Кратко о курсе…" style="resize:vertical"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Эмодзи обложки</label>
|
|
<input class="form-input" id="nc-emoji" placeholder="" maxlength="4" style="max-width:90px" />
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeNewCourseModal()">Отмена</button>
|
|
<button class="btn-primary" id="btn-do-create" onclick="doCreateCourse()">Создать</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/notifications.js"></script>
|
|
<script>
|
|
const { user, isTeacher, isAdmin } = LS.initPage();
|
|
LS.showBoardIfAllowed();
|
|
if (isTeacher || isAdmin) {
|
|
document.getElementById('btn-classes').style.display = '';
|
|
document.getElementById('btn-new-course').style.display = '';
|
|
document.getElementById('btn-from-tpl').style.display = '';
|
|
}
|
|
if (isAdmin) {
|
|
document.getElementById('btn-admin').style.display = '';
|
|
}
|
|
|
|
lucide.createIcons();
|
|
LS.notif.init();
|
|
|
|
/* ── filter + search ── */
|
|
let currentFilter = '';
|
|
let allCourses = [];
|
|
let searchQuery = '';
|
|
let _searchTimer = null;
|
|
|
|
function setFilter(btn, subj) {
|
|
document.querySelectorAll('.sf-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentFilter = subj;
|
|
searchQuery = '';
|
|
document.getElementById('search-input').value = '';
|
|
document.getElementById('search-results-info').style.display = 'none';
|
|
renderCourses();
|
|
}
|
|
|
|
function onSearchInput(val) {
|
|
clearTimeout(_searchTimer);
|
|
searchQuery = val.trim();
|
|
if (!searchQuery) {
|
|
document.getElementById('search-results-info').style.display = 'none';
|
|
renderCourses();
|
|
return;
|
|
}
|
|
_searchTimer = setTimeout(doSearch, 350);
|
|
}
|
|
|
|
async function doSearch() {
|
|
if (!searchQuery) return;
|
|
const grid = document.getElementById('courses-grid');
|
|
const info = document.getElementById('search-results-info');
|
|
grid.innerHTML = '<div style="grid-column:1/-1"><div class="spinner"></div></div>';
|
|
try {
|
|
const res = await LS.api('/api/courses/search?q=' + encodeURIComponent(searchQuery));
|
|
const courses = res.courses || [];
|
|
const lessons = res.lessons || [];
|
|
info.textContent = `Найдено: ${courses.length} курс(ов), ${lessons.length} урок(ов)`;
|
|
info.style.display = '';
|
|
|
|
let html = courses.map((c, i) => renderCourseCard(c, i)).join('');
|
|
if (lessons.length) {
|
|
html += `<div style="grid-column:1/-1;margin-top:8px;margin-bottom:2px">
|
|
<div style="font-family:'Unbounded',sans-serif;font-size:0.72rem;font-weight:800;color:#8898AA;text-transform:uppercase;letter-spacing:0.07em">Уроки</div>
|
|
</div>`;
|
|
html += lessons.map((l, i) => `
|
|
<a class="course-card stagger-item" href="/lesson?id=${l.id}" style="--i:${i};flex-direction:row;padding:16px;gap:14px;border-radius:16px">
|
|
<div style="width:36px;height:36px;border-radius:10px;background:rgba(155,93,229,0.1);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1rem"><svg class="ic" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></div>
|
|
<div style="flex:1">
|
|
<div style="font-size:0.88rem;font-weight:700;color:#0F172A">${esc(l.title)}</div>
|
|
<div style="font-size:0.74rem;color:#8898AA;margin-top:3px">${esc(l.courseTitle || l.course_title || '')}</div>
|
|
</div>
|
|
<i data-lucide="chevron-right" style="width:15px;height:15px;color:#CBD5E1;align-self:center;flex-shrink:0"></i>
|
|
</a>`).join('');
|
|
}
|
|
|
|
if (!html) {
|
|
grid.innerHTML = `<div style="grid-column:1/-1">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon"><svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></div>
|
|
<div class="empty-state-title">Ничего не найдено</div>
|
|
<div class="empty-state-desc">Попробуйте другой запрос</div>
|
|
</div>
|
|
</div>`;
|
|
} else {
|
|
grid.innerHTML = html;
|
|
}
|
|
lucide.createIcons();
|
|
} catch {
|
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#8898AA;padding:40px">Ошибка поиска</div>';
|
|
}
|
|
}
|
|
|
|
/* ── render ── */
|
|
const SUBJ_LABEL = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
|
|
const BANNER_CLASS = { bio:'cc-banner-bio', chem:'cc-banner-chem', math:'cc-banner-math', phys:'cc-banner-phys' };
|
|
const SUBJ_CLASS = { bio:'cc-subj-bio', chem:'cc-subj-chem', math:'cc-subj-math', phys:'cc-subj-phys' };
|
|
|
|
function renderCourseCard(c, i) {
|
|
const bannerClass = BANNER_CLASS[c.subjectSlug] || 'cc-banner-other';
|
|
const subjClass = SUBJ_CLASS[c.subjectSlug] || 'cc-subj-other';
|
|
const subjLabel = SUBJ_LABEL[c.subjectSlug] || c.subjectSlug;
|
|
const pct = c.lessonCount > 0 ? Math.round(c.doneCount / c.lessonCount * 100) : 0;
|
|
return `<a class="course-card stagger-item" href="/course?id=${c.id}" style="--i:${i}">
|
|
<div class="cc-banner ${bannerClass}">
|
|
${c.coverEmoji || LS.icon('book-open',20)}
|
|
${!c.isPublished && isTeacher ? '<span class="cc-draft-badge">Черновик</span>' : ''}
|
|
${isTeacher ? `<button class="cc-del-btn" onclick="delCourse(event,${c.id},'${esc(c.title).replace(/'/g, "\\'")}')" title="Удалить курс">${LS.icon('trash-2',14)}</button>` : ''}
|
|
</div>
|
|
<div class="cc-body">
|
|
<div class="cc-subj ${subjClass}">${esc(subjLabel)}</div>
|
|
<div class="cc-title">${esc(c.title)}</div>
|
|
${c.description ? `<div class="cc-desc">${esc(c.description)}</div>` : '<div class="cc-desc" style="opacity:0.4">Нет описания</div>'}
|
|
<div class="cc-footer">
|
|
<div class="cc-meta"><i data-lucide="book-open" style="width:13px;height:13px"></i> ${c.lessonCount} уроков</div>
|
|
${c.lessonCount > 0 ? `
|
|
<div style="display:flex;align-items:center;gap:7px">
|
|
<div class="cc-progress-bar"><div class="cc-progress-fill" style="width:${pct}%"></div></div>
|
|
<span class="cc-pct">${pct}%</span>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</a>`;
|
|
}
|
|
|
|
function renderCourses() {
|
|
const grid = document.getElementById('courses-grid');
|
|
const filtered = currentFilter ? allCourses.filter(c => c.subjectSlug === currentFilter) : allCourses;
|
|
|
|
if (!filtered.length) {
|
|
grid.innerHTML = `<div style="grid-column:1/-1">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon"><svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2V3zm20 0h-6a4 4 0 00-4 4v14a3 3 0 013-3h7V3z"/></svg></div>
|
|
<div class="empty-state-title">Курсов пока нет</div>
|
|
<div class="empty-state-desc">${isTeacher ? 'Создайте первый курс нажав кнопку «Новый курс»' : 'Здесь появятся курсы, когда учитель их добавит'}</div>
|
|
</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = filtered.map((c, i) => renderCourseCard(c, i)).join('');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
/* ── continue reading ── */
|
|
async function loadContinue() {
|
|
try {
|
|
const res = await LS.api('/api/courses/continue');
|
|
if (!res || !res.lessonId) return;
|
|
const wrap = document.getElementById('continue-wrap');
|
|
wrap.innerHTML = `
|
|
<a class="continue-banner" href="/lesson?id=${res.lessonId}">
|
|
<div class="cb-icon"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg></div>
|
|
<div class="cb-body">
|
|
<div class="cb-label">Продолжить обучение</div>
|
|
<div class="cb-title">${esc(res.lessonTitle || 'Урок')}</div>
|
|
<div class="cb-course">${esc(res.courseTitle || '')}</div>
|
|
</div>
|
|
<i data-lucide="arrow-right" class="cb-arrow" style="width:18px;height:18px;flex-shrink:0"></i>
|
|
</a>`;
|
|
wrap.style.display = '';
|
|
lucide.createIcons();
|
|
} catch {}
|
|
}
|
|
|
|
/* ── delete course ── */
|
|
async function delCourse(e, id, title) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const ok = await LS.confirm('Удалить курс «' + title + '» и все его уроки?', { title:'Удаление курса', confirmText:'Удалить', danger:true });
|
|
if (!ok) return;
|
|
try {
|
|
await LS.api('/api/courses/' + id, { method: 'DELETE' });
|
|
loadCourses();
|
|
} catch (err) { LS.toast(err.message || 'Ошибка', 'error'); }
|
|
}
|
|
|
|
/* ── load courses ── */
|
|
async function loadCourses() {
|
|
try {
|
|
allCourses = await LS.api('/api/courses');
|
|
renderCourses();
|
|
} catch (e) {
|
|
document.getElementById('courses-grid').innerHTML =
|
|
'<div style="grid-column:1/-1;text-align:center;color:#8898AA;padding:40px">Ошибка загрузки</div>';
|
|
}
|
|
}
|
|
loadCourses();
|
|
loadContinue();
|
|
|
|
/* ── modal ── */
|
|
function openNewCourseModal() {
|
|
document.getElementById('nc-title').value = '';
|
|
document.getElementById('nc-desc').value = '';
|
|
document.getElementById('nc-emoji').value = '';
|
|
document.getElementById('new-course-modal').classList.add('open');
|
|
setTimeout(() => document.getElementById('nc-title').focus(), 50);
|
|
}
|
|
function closeNewCourseModal() {
|
|
document.getElementById('new-course-modal').classList.remove('open');
|
|
}
|
|
async function doCreateCourse() {
|
|
const title = document.getElementById('nc-title').value.trim();
|
|
if (!title) { document.getElementById('nc-title').focus(); return; }
|
|
const btn = document.getElementById('btn-do-create');
|
|
btn.disabled = true;
|
|
try {
|
|
const res = await LS.api('/api/courses', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
subjectSlug: document.getElementById('nc-subject').value,
|
|
title,
|
|
description: document.getElementById('nc-desc').value.trim() || null,
|
|
coverEmoji: document.getElementById('nc-emoji').value.trim() || '',
|
|
}),
|
|
});
|
|
closeNewCourseModal();
|
|
location.href = '/course?id=' + res.id;
|
|
} catch (e) {
|
|
LS.toast(e.message || 'Ошибка', 'error');
|
|
} finally { btn.disabled = false; }
|
|
}
|
|
document.getElementById('nc-title').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') doCreateCourse();
|
|
});
|
|
|
|
/* ══════════════════════════════════════════════════════════════════
|
|
TEMPLATE BROWSER
|
|
══════════════════════════════════════════════════════════════════ */
|
|
function openTplBrowser() {
|
|
document.getElementById('tpl-browser-modal').classList.add('open');
|
|
loadCourseTpls();
|
|
}
|
|
function closeTplBrowser() {
|
|
document.getElementById('tpl-browser-modal').classList.remove('open');
|
|
}
|
|
|
|
let _tplFilter = '';
|
|
function setTplFilter(btn, val) {
|
|
document.querySelectorAll('.tpl-filter-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
_tplFilter = val;
|
|
loadCourseTpls();
|
|
}
|
|
|
|
async function loadCourseTpls() {
|
|
const grid = document.getElementById('tpl-grid');
|
|
grid.innerHTML = '<div style="grid-column:1/-1"><div class="spinner"></div></div>';
|
|
try {
|
|
const params = {};
|
|
if (_tplFilter) params.subject = _tplFilter;
|
|
const list = await LS.getCourseTemplates(params);
|
|
if (!list.length) {
|
|
grid.innerHTML = '<div style="grid-column:1/-1" class="tpl-empty">' + LS.icon('clipboard', 32) + '<br>Нет доступных шаблонов курсов</div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = list.map(t => {
|
|
const structure = t.structure || {};
|
|
const secCount = (structure.sections || []).length;
|
|
const lesCount = (structure.sections || []).reduce((sum, s) => sum + (s.lessons || []).length, 0);
|
|
const subjLabel = SUBJ_LABEL[t.subjectSlug] || t.subjectSlug || '';
|
|
const canDelete = t.createdBy === user.id || user.role === 'admin';
|
|
return '<div class="tpl-card">' +
|
|
'<div class="tpl-card-banner ' + (t.subjectSlug ? 'tpl-banner-' + t.subjectSlug : '') + '">' +
|
|
LS.icon('clipboard', 24) +
|
|
'</div>' +
|
|
'<div class="tpl-card-body">' +
|
|
(subjLabel ? '<div class="tpl-card-subj">' + esc(subjLabel) + '</div>' : '') +
|
|
'<div class="tpl-card-title">' + esc(t.title) + '</div>' +
|
|
(t.description ? '<div class="tpl-card-desc">' + esc(t.description) + '</div>' : '') +
|
|
'<div class="tpl-card-meta">' +
|
|
'<span>' + secCount + ' разд.</span>' +
|
|
'<span>' + lesCount + ' уроков</span>' +
|
|
'<span>' + esc(t.creatorName) + '</span>' +
|
|
'</div>' +
|
|
'<div class="tpl-card-actions">' +
|
|
'<button class="tpl-use-btn" onclick="useCourseTpl(' + t.id + ')">Использовать</button>' +
|
|
(canDelete ? '<button class="tpl-del-btn" onclick="delCourseTpl(event,' + t.id + ')" title="Удалить">' + LS.icon('trash',13) + '</button>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
} catch (e) {
|
|
grid.innerHTML = '<div style="grid-column:1/-1" class="tpl-empty">Ошибка загрузки</div>';
|
|
}
|
|
}
|
|
|
|
async function useCourseTpl(tplId) {
|
|
try {
|
|
const res = await LS.createFromCourseTemplate(tplId, {});
|
|
closeTplBrowser();
|
|
LS.toast('Курс создан из шаблона', 'success');
|
|
location.href = '/course?id=' + res.id;
|
|
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
|
}
|
|
|
|
async function delCourseTpl(e, tplId) {
|
|
e.stopPropagation();
|
|
const ok = await LS.confirm('Удалить этот шаблон курса?', { title:'Удаление шаблона', confirmText:'Удалить', danger:true });
|
|
if (!ok) return;
|
|
try {
|
|
await LS.deleteCourseTemplate(tplId);
|
|
LS.toast('Шаблон удалён', 'success');
|
|
loadCourseTpls();
|
|
} catch (e2) { LS.toast(e2.message || 'Ошибка', 'error'); }
|
|
}
|
|
</script>
|
|
|
|
<!-- Template browser modal -->
|
|
<style>
|
|
.tpl-modal{position:fixed;inset:0;background:rgba(15,23,42,0.4);backdrop-filter:blur(6px);z-index:200;display:none;align-items:center;justify-content:center;padding:20px;}
|
|
.tpl-modal.open{display:flex;}
|
|
.tpl-browser{background:#fff;border-radius:24px;padding:28px 32px;width:100%;max-width:780px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 32px 80px rgba(15,23,42,0.22);}
|
|
.tpl-browser-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px;flex-wrap:wrap;gap:10px;}
|
|
.tpl-browser-title{font-family:'Unbounded',sans-serif;font-size:1rem;font-weight:800;color:#0F172A;}
|
|
.tpl-filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px;}
|
|
.tpl-filter-btn{padding:6px 14px;border-radius:999px;border:1.5px solid rgba(15,23,42,0.1);background:#fff;color:#6B7A8E;font-family:'Manrope',sans-serif;font-size:0.78rem;font-weight:700;cursor:pointer;transition:all .15s;}
|
|
.tpl-filter-btn:hover{border-color:var(--violet);color:var(--violet);}
|
|
.tpl-filter-btn.active{background:var(--violet);color:#fff;border-color:var(--violet);}
|
|
.tpl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px;overflow-y:auto;flex:1;padding-right:4px;}
|
|
.tpl-card{background:#fff;border:1.5px solid rgba(15,23,42,0.07);border-radius:18px;overflow:hidden;transition:transform .18s,box-shadow .18s,border-color .18s;display:flex;flex-direction:column;box-shadow:0 2px 8px rgba(15,23,42,0.05);}
|
|
.tpl-card:hover{transform:translateY(-3px);box-shadow:0 8px 28px rgba(15,23,42,0.1);border-color:rgba(155,93,229,0.2);}
|
|
.tpl-card-banner{height:56px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a1a 0%,#1a1a2e 100%);color:rgba(255,255,255,0.4);}
|
|
.tpl-banner-bio{background:linear-gradient(135deg,#1a0a3c 0%,#2d0856 100%);}
|
|
.tpl-banner-chem{background:linear-gradient(135deg,#062a20 0%,#064a38 100%);}
|
|
.tpl-banner-math{background:linear-gradient(135deg,#061828 0%,#062848 100%);}
|
|
.tpl-banner-phys{background:linear-gradient(135deg,#1c1400 0%,#3a2800 100%);}
|
|
.tpl-card-body{padding:14px 16px 12px;flex:1;display:flex;flex-direction:column;}
|
|
.tpl-card-subj{font-size:0.66rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--violet);margin-bottom:4px;}
|
|
.tpl-card-title{font-family:'Unbounded',sans-serif;font-size:0.82rem;font-weight:800;color:#0F172A;margin-bottom:6px;line-height:1.35;}
|
|
.tpl-card-desc{font-size:0.76rem;color:#6B7A8E;line-height:1.5;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
|
|
.tpl-card-meta{font-size:0.7rem;color:#8898AA;display:flex;gap:8px;margin-bottom:10px;}
|
|
.tpl-card-actions{display:flex;gap:6px;margin-top:auto;}
|
|
.tpl-use-btn{flex:1;padding:7px 14px;border:none;border-radius:999px;background:var(--violet);color:#fff;font-family:'Manrope',sans-serif;font-size:0.78rem;font-weight:700;cursor:pointer;transition:all .15s;}
|
|
.tpl-use-btn:hover{background:#8a47d8;}
|
|
.tpl-del-btn{width:30px;height:30px;border:1.5px solid rgba(241,91,181,0.2);border-radius:999px;background:rgba(241,91,181,0.06);color:#E0335E;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s;flex-shrink:0;}
|
|
.tpl-del-btn:hover{background:#E0335E;color:#fff;}
|
|
.tpl-empty{text-align:center;padding:40px;color:#8898AA;font-size:0.86rem;}
|
|
</style>
|
|
|
|
<div class="tpl-modal" id="tpl-browser-modal" onclick="if(event.target===this)closeTplBrowser()">
|
|
<div class="tpl-browser">
|
|
<div class="tpl-browser-header">
|
|
<div class="tpl-browser-title">Шаблоны курсов</div>
|
|
<button class="btn-cancel" onclick="closeTplBrowser()" style="padding:6px 16px">Закрыть</button>
|
|
</div>
|
|
<div class="tpl-filter-bar">
|
|
<button class="tpl-filter-btn active" onclick="setTplFilter(this,'')">Все</button>
|
|
<button class="tpl-filter-btn" onclick="setTplFilter(this,'bio')">Биология</button>
|
|
<button class="tpl-filter-btn" onclick="setTplFilter(this,'chem')">Химия</button>
|
|
<button class="tpl-filter-btn" onclick="setTplFilter(this,'math')">Математика</button>
|
|
<button class="tpl-filter-btn" onclick="setTplFilter(this,'phys')">Физика</button>
|
|
</div>
|
|
<div class="tpl-grid" id="tpl-grid">
|
|
<div style="grid-column:1/-1"><div class="spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/search.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
</body>
|
|
</html>
|