edb4c211a0
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar, handles role-based visibility, active link (with prefix matching), toggle wiring, collapsed state, board/features/notif init - Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar"> across all 35 standard-layout pages via scripts/apply-sidebar.js - Add notifications.js to 5 pages that were missing it - Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set, fix active link selector .sb-item → .sb-link - Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls that crashed after sidebar replacement (lab, classes, collection, crossword, hangman, knowledge-map, library, pet, profile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
609 lines
28 KiB
HTML
609 lines
28 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>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<style>
|
|
.sb-content { background: #f4f5f8; overflow: hidden; display: flex; flex-direction: column; }
|
|
|
|
/* ── page header ── */
|
|
.qb-header {
|
|
background: linear-gradient(140deg, #0a0220 0%, #1a0a3c 60%, #080818 100%);
|
|
padding: 24px 28px 20px; position: relative; overflow: hidden; flex-shrink: 0;
|
|
}
|
|
.qb-header-dots {
|
|
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;
|
|
}
|
|
.qb-header-inner { position: relative; z-index: 1; }
|
|
.qb-title-row { display: flex; align-items: center; gap: 12px; }
|
|
.qb-title {
|
|
font-family: 'Unbounded', sans-serif; font-size: 1.2rem; font-weight: 800;
|
|
color: #fff; display: flex; align-items: center; gap: 10px;
|
|
}
|
|
.qb-count-chip {
|
|
padding: 4px 12px; background: rgba(255,255,255,0.1); border-radius: 999px;
|
|
font-size: 0.74rem; font-weight: 700; color: rgba(255,255,255,0.7);
|
|
font-family: 'Unbounded', sans-serif;
|
|
}
|
|
|
|
/* ── layout ── */
|
|
.qb-body {
|
|
flex: 1; display: flex; overflow: hidden; min-height: 0;
|
|
}
|
|
|
|
/* ── filters sidebar ── */
|
|
.qb-filters {
|
|
width: 280px; flex-shrink: 0;
|
|
background: #fff; border-right: 1.5px solid rgba(15,23,42,0.08);
|
|
overflow-y: auto; padding: 20px 16px;
|
|
display: flex; flex-direction: column; gap: 20px;
|
|
}
|
|
.filter-section { display: flex; flex-direction: column; gap: 8px; }
|
|
.filter-label {
|
|
font-size: 0.7rem; font-weight: 700; color: #8898AA;
|
|
text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px;
|
|
}
|
|
.filter-search {
|
|
width: 100%; padding: 9px 12px; border: 1.5px solid rgba(15,23,42,0.12);
|
|
border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 0.84rem;
|
|
font-weight: 500; color: #0F172A; background: #f8f9fc; transition: border-color 0.15s;
|
|
outline: none;
|
|
}
|
|
.filter-search:focus { border-color: var(--violet); background: #fff; }
|
|
.filter-select {
|
|
width: 100%; padding: 9px 12px; border: 1.5px solid rgba(15,23,42,0.12);
|
|
border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 0.84rem;
|
|
font-weight: 500; color: #0F172A; background: #f8f9fc; cursor: pointer;
|
|
outline: none; appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238898AA' stroke-width='2.5'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
|
background-repeat: no-repeat; background-position: right 12px center;
|
|
padding-right: 32px;
|
|
}
|
|
.filter-select:focus { border-color: var(--violet); }
|
|
.filter-checkboxes { display: flex; flex-direction: column; gap: 6px; }
|
|
.filter-cb-row {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
transition: background 0.12s;
|
|
}
|
|
.filter-cb-row:hover { background: rgba(155,93,229,0.05); }
|
|
.filter-cb-row input[type="checkbox"] { accent-color: var(--violet); width: 14px; height: 14px; cursor: pointer; }
|
|
.filter-cb-label { font-size: 0.82rem; font-weight: 600; color: #3D4F6B; cursor: pointer; user-select: none; }
|
|
.filter-cb-badge {
|
|
margin-left: auto; padding: 1px 7px; border-radius: 999px;
|
|
font-size: 0.65rem; font-weight: 700;
|
|
}
|
|
.btn-reset {
|
|
width: 100%; padding: 9px; border: 1.5px solid rgba(155,93,229,0.2);
|
|
border-radius: 10px; background: rgba(155,93,229,0.05); color: var(--violet);
|
|
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
|
cursor: pointer; transition: all 0.15s; display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
}
|
|
.btn-reset:hover { background: rgba(155,93,229,0.12); border-color: var(--violet); }
|
|
|
|
/* ── main area ── */
|
|
.qb-main { flex: 1; overflow-y: auto; padding: 20px 24px 60px; min-width: 0; }
|
|
|
|
/* ── result header ── */
|
|
.qb-result-header {
|
|
display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
|
|
}
|
|
.qb-found {
|
|
font-family: 'Unbounded', sans-serif; font-size: 0.8rem; font-weight: 800; color: #0F172A; flex: 1;
|
|
}
|
|
.qb-found span { color: var(--violet); }
|
|
|
|
/* ── card grid ── */
|
|
.qb-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 14px; margin-bottom: 24px;
|
|
}
|
|
@media (max-width: 1100px) { .qb-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
@media (max-width: 700px) { .qb-grid { grid-template-columns: 1fr; } }
|
|
|
|
.qc {
|
|
background: #fff; border: 1.5px solid rgba(15,23,42,0.07);
|
|
border-radius: 16px; padding: 16px 18px;
|
|
box-shadow: 0 2px 8px rgba(15,23,42,0.04);
|
|
cursor: pointer; transition: all 0.18s; display: flex; flex-direction: column; gap: 10px;
|
|
}
|
|
.qc:hover { border-color: rgba(155,93,229,0.3); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(155,93,229,0.08); }
|
|
.qc.expanded { border-color: var(--violet); box-shadow: 0 6px 24px rgba(155,93,229,0.12); }
|
|
.qc-top { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
.qc-text {
|
|
font-size: 0.84rem; font-weight: 600; color: #0F172A; line-height: 1.5;
|
|
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
|
|
}
|
|
.qc-footer { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.qc-topic {
|
|
font-size: 0.7rem; font-weight: 700; color: #8898AA;
|
|
display: flex; align-items: center; gap: 4px;
|
|
}
|
|
.qc-opts-count {
|
|
margin-left: auto; font-size: 0.7rem; color: #8898AA; font-weight: 600;
|
|
display: flex; align-items: center; gap: 3px;
|
|
}
|
|
|
|
/* badges */
|
|
.badge {
|
|
display: inline-flex; align-items: center;
|
|
padding: 3px 9px; border-radius: 999px;
|
|
font-size: 0.68rem; font-weight: 700; white-space: nowrap;
|
|
}
|
|
.badge-diff-1 { background: rgba(6,214,160,0.12); color: #059652; }
|
|
.badge-diff-2 { background: rgba(255,179,71,0.15); color: #B8860B; }
|
|
.badge-diff-3 { background: rgba(239,71,111,0.12); color: #EF476F; }
|
|
.badge-type { background: rgba(6,214,224,0.12); color: #0891B2; }
|
|
|
|
/* ── expanded preview ── */
|
|
.qc-preview {
|
|
margin-top: 4px; border-top: 1.5px solid rgba(15,23,42,0.07);
|
|
padding-top: 14px; display: flex; flex-direction: column; gap: 8px;
|
|
}
|
|
.qc-preview-text { font-size: 0.84rem; font-weight: 600; color: #0F172A; line-height: 1.55; }
|
|
.qc-options { display: flex; flex-direction: column; gap: 6px; }
|
|
.qc-option {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 7px 10px; border-radius: 8px;
|
|
border: 1.5px solid rgba(15,23,42,0.07); font-size: 0.8rem; font-weight: 500;
|
|
}
|
|
.qc-option.correct {
|
|
border-color: rgba(6,214,160,0.4); background: rgba(6,214,160,0.06);
|
|
color: #059652; font-weight: 700;
|
|
}
|
|
.qc-option-marker {
|
|
width: 18px; height: 18px; border-radius: 50%; border: 2px solid rgba(15,23,42,0.15);
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
font-size: 0.6rem; font-weight: 800;
|
|
}
|
|
.qc-option.correct .qc-option-marker {
|
|
background: #06D6A0; border-color: #06D6A0; color: #fff;
|
|
}
|
|
.qc-explanation {
|
|
font-size: 0.78rem; color: #8898AA; font-style: italic; line-height: 1.5;
|
|
background: rgba(155,93,229,0.04); border-left: 3px solid rgba(155,93,229,0.3);
|
|
padding: 8px 12px; border-radius: 0 8px 8px 0;
|
|
}
|
|
|
|
/* ── pagination ── */
|
|
.qb-pagination {
|
|
display: flex; align-items: center; gap: 10px; justify-content: center;
|
|
}
|
|
.pag-btn {
|
|
padding: 8px 18px; border-radius: 999px;
|
|
border: 1.5px solid rgba(15,23,42,0.12); background: #fff;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
|
|
color: #3D4F6B; cursor: pointer; transition: all 0.15s;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.pag-btn:hover:not(:disabled) { border-color: var(--violet); color: var(--violet); }
|
|
.pag-btn:disabled { opacity: 0.4; cursor: default; }
|
|
.pag-info { font-size: 0.82rem; font-weight: 600; color: #8898AA; }
|
|
|
|
/* ── empty state ── */
|
|
.qb-empty {
|
|
text-align: center; padding: 80px 20px; color: #8898AA; font-size: 0.9rem;
|
|
}
|
|
.qb-empty-icon { margin-bottom: 14px; opacity: 0.25; }
|
|
|
|
/* ── nav ── */
|
|
.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-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; }
|
|
|
|
@media (max-width: 768px) {
|
|
.qb-filters { display: none; }
|
|
.qb-main { padding: 14px 12px 60px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="app-sidebar"></aside>
|
|
<div class="notif-drop" id="notif-drop"></div>
|
|
<div class="sb-content">
|
|
|
|
<!-- Header -->
|
|
<div class="qb-header">
|
|
<div class="qb-header-dots"></div>
|
|
<div class="qb-header-inner">
|
|
<div class="qb-title-row">
|
|
<div class="qb-title"><i data-lucide="database" style="width:22px;height:22px;opacity:0.5"></i> Банк вопросов</div>
|
|
<div class="qb-count-chip" id="hdr-count">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body: filters + main -->
|
|
<div class="qb-body">
|
|
|
|
<!-- Filters -->
|
|
<div class="qb-filters">
|
|
<div class="filter-section">
|
|
<div class="filter-label">Поиск</div>
|
|
<input class="filter-search" id="f-search" type="text" placeholder="Текст вопроса…" />
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-label">Предмет</div>
|
|
<select class="filter-select" id="f-subject">
|
|
<option value="">Все предметы</option>
|
|
<option value="bio">Биология</option>
|
|
<option value="chem">Химия</option>
|
|
<option value="math">Математика</option>
|
|
<option value="phys">Физика</option>
|
|
<option value="other">Другое</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-label">Тема</div>
|
|
<select class="filter-select" id="f-topic">
|
|
<option value="">Все темы</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-label">Сложность</div>
|
|
<div class="filter-checkboxes">
|
|
<label class="filter-cb-row">
|
|
<input type="checkbox" id="diff-1" value="1" onchange="onFilterChange()" />
|
|
<span class="filter-cb-label">Лёгкий</span>
|
|
<span class="filter-cb-badge badge badge-diff-1">1</span>
|
|
</label>
|
|
<label class="filter-cb-row">
|
|
<input type="checkbox" id="diff-2" value="2" onchange="onFilterChange()" />
|
|
<span class="filter-cb-label">Средний</span>
|
|
<span class="filter-cb-badge badge badge-diff-2">2</span>
|
|
</label>
|
|
<label class="filter-cb-row">
|
|
<input type="checkbox" id="diff-3" value="3" onchange="onFilterChange()" />
|
|
<span class="filter-cb-label">Сложный</span>
|
|
<span class="filter-cb-badge badge badge-diff-3">3</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-label">Тип вопроса</div>
|
|
<select class="filter-select" id="f-type" onchange="onFilterChange()">
|
|
<option value="">Все типы</option>
|
|
<option value="single">Один ответ</option>
|
|
<option value="multi">Несколько</option>
|
|
<option value="true_false">Да/Нет</option>
|
|
<option value="short_answer">Краткий ответ</option>
|
|
<option value="matching">Соответствие</option>
|
|
<option value="fill-blank">Заполни пробел</option>
|
|
<option value="ordering">Порядок</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-label">Сортировка</div>
|
|
<select class="filter-select" id="f-sort" onchange="onFilterChange()">
|
|
<option value="newest">Новые</option>
|
|
<option value="oldest">Старые</option>
|
|
<option value="easy">Лёгкие <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Сложные</option>
|
|
<option value="hard">Сложные <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Лёгкие</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button class="btn-reset" onclick="resetFilters()">
|
|
<i data-lucide="rotate-ccw" style="width:14px;height:14px"></i> Сбросить
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Main -->
|
|
<div class="qb-main" id="qb-main">
|
|
<div class="qb-result-header">
|
|
<div class="qb-found">Найдено: <span id="found-count">0</span> вопросов</div>
|
|
</div>
|
|
<div id="qb-grid" class="qb-grid"></div>
|
|
<div class="qb-pagination" id="qb-pagination" style="display:none">
|
|
<button class="pag-btn" id="pag-prev" onclick="prevPage()">
|
|
<i data-lucide="chevron-left" style="width:14px;height:14px"></i> Назад
|
|
</button>
|
|
<div class="pag-info" id="pag-info">1 / 1</div>
|
|
<button class="pag-btn" id="pag-next" onclick="nextPage()">
|
|
Далее <i data-lucide="chevron-right" style="width:14px;height:14px"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/sidebar.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
|
<script>
|
|
if (!LS.requireAuth()) throw new Error();
|
|
|
|
const user = LS.getUser();
|
|
document.getElementById('nav-user').textContent = user?.name || user?.email || '';
|
|
document.getElementById('nav-avatar').textContent =
|
|
(user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
|
|
|
const isTeacher = ['admin', 'teacher'].includes(user?.role);
|
|
if (!isTeacher) { location.href = '/dashboard'; throw new Error(); }
|
|
if (user?.role === 'admin') document.getElementById('btn-admin').style.display = '';
|
|
|
|
/* ── helpers ── */
|
|
function fmtTime(s) {
|
|
const d = new Date(s && s.includes('T') ? s : (s || '').replace(' ', 'T') + 'Z');
|
|
const diff = Date.now() - d.getTime();
|
|
if (diff < 60000) return 'только что';
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
|
|
return d.toLocaleDateString('ru', { day: 'numeric', month: 'short' });
|
|
}
|
|
const TYPE_LABELS = {
|
|
single: 'Один ответ', multi: 'Несколько', true_false: 'Да/Нет',
|
|
short_answer: 'Краткий ответ', matching: 'Соответствие',
|
|
'fill-blank': 'Заполни пробел', ordering: 'Порядок',
|
|
};
|
|
const SUBJECT_NAMES = { bio: 'Биология', chem: 'Химия', math: 'Математика', phys: 'Физика', other: 'Другое' };
|
|
|
|
/* ── math rendering ── */
|
|
const _MATH_DELIMS = [
|
|
{ left: '\\(', right: '\\)', display: false },
|
|
{ left: '\\[', right: '\\]', display: true },
|
|
{ left: '$', right: '$', display: false },
|
|
];
|
|
function mathHtml(text) {
|
|
if (!text) return '';
|
|
const tmp = document.createElement('span');
|
|
tmp.textContent = text;
|
|
if (window.renderMathInElement) {
|
|
try { renderMathInElement(tmp, { delimiters: _MATH_DELIMS, throwOnError: false }); } catch {}
|
|
}
|
|
return tmp.innerHTML;
|
|
}
|
|
|
|
/* ── sidebar ── */
|
|
function toggleSidebar() {
|
|
const layout = document.querySelector('.app-layout');
|
|
const collapsed = layout.classList.toggle('sb-collapsed');
|
|
localStorage.setItem('ls_sb_collapsed', collapsed ? '1' : '');
|
|
lucide.createIcons();
|
|
}
|
|
if (localStorage.getItem('ls_sb_collapsed'))
|
|
document.querySelector('.app-layout').classList.add('sb-collapsed');
|
|
|
|
/* ── notifications ── */
|
|
function toggleNotifDrop() {
|
|
const btn = document.getElementById('notif-btn');
|
|
const drop = document.getElementById('notif-drop');
|
|
const r = btn.getBoundingClientRect();
|
|
drop.style.left = (r.right + 8) + 'px';
|
|
drop.style.top = r.top + 'px';
|
|
if (drop.classList.toggle('open')) loadNotifs();
|
|
}
|
|
async function loadNotifs() {
|
|
const drop = document.getElementById('notif-drop');
|
|
drop.innerHTML = '<div class="notif-drop-header"><span class="notif-drop-title">Уведомления</span><button class="notif-read-all" onclick="readAllNotifs()">Прочитать все</button></div><div class="notif-empty">Загрузка…</div>';
|
|
try {
|
|
const data = await LS.api('/api/notifications?limit=20');
|
|
const items = data.items || [];
|
|
const badge = document.getElementById('notif-badge');
|
|
const unread = items.filter(n => !n.is_read).length;
|
|
badge.textContent = unread; badge.style.display = unread ? '' : 'none';
|
|
if (!items.length) { drop.querySelector('.notif-empty').textContent = 'Нет уведомлений'; return; }
|
|
drop.innerHTML = '<div class="notif-drop-header"><span class="notif-drop-title">Уведомления</span><button class="notif-read-all" onclick="readAllNotifs()">Прочитать все</button></div>' +
|
|
items.map(n => `<a class="notif-item${n.is_read ? '' : ' unread'}" href="${LS.safeHref(n.link)}" onclick="markRead(${n.id})">
|
|
<div class="notif-dot${n.is_read ? ' read' : ''}"></div>
|
|
<div><div class="notif-msg">${esc(n.message)}</div><div class="notif-time">${fmtTime(n.created_at)}</div></div>
|
|
</a>`).join('');
|
|
} catch {}
|
|
}
|
|
async function markRead(id) { try { await LS.api('/api/notifications/' + id + '/read', { method: 'POST' }); } catch {} }
|
|
async function readAllNotifs() { try { await LS.api('/api/notifications/read-all', { method: 'POST' }); loadNotifs(); } catch {} }
|
|
document.addEventListener('click', e => {
|
|
const drop = document.getElementById('notif-drop');
|
|
if (drop.classList.contains('open') && !drop.contains(e.target) && !document.getElementById('notif-btn').contains(e.target))
|
|
drop.classList.remove('open');
|
|
});
|
|
|
|
/* ── filter state ── */
|
|
let currentPage = 1;
|
|
const LIMIT = 24;
|
|
let expandedId = null;
|
|
let searchTimeout = null;
|
|
|
|
function getFilters() {
|
|
const diffs = [1, 2, 3].filter(d => document.getElementById('diff-' + d)?.checked);
|
|
return {
|
|
q: document.getElementById('f-search').value.trim(),
|
|
subject: document.getElementById('f-subject').value,
|
|
topic_id: document.getElementById('f-topic').value,
|
|
difficulty: diffs.length === 1 ? diffs[0] : (diffs.length === 0 ? '' : diffs.join(',')),
|
|
type: document.getElementById('f-type').value,
|
|
sort: document.getElementById('f-sort').value,
|
|
};
|
|
}
|
|
|
|
function onFilterChange() { currentPage = 1; expandedId = null; loadQuestions(); }
|
|
|
|
/* debounce search input */
|
|
document.getElementById('f-search').addEventListener('input', () => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => onFilterChange(), 380);
|
|
});
|
|
document.getElementById('f-subject').addEventListener('change', () => {
|
|
loadTopics();
|
|
onFilterChange();
|
|
});
|
|
|
|
function resetFilters() {
|
|
document.getElementById('f-search').value = '';
|
|
document.getElementById('f-subject').value = '';
|
|
document.getElementById('f-topic').value = '';
|
|
document.getElementById('f-type').value = '';
|
|
document.getElementById('f-sort').value = 'newest';
|
|
[1, 2, 3].forEach(d => { const cb = document.getElementById('diff-' + d); if (cb) cb.checked = false; });
|
|
document.getElementById('f-topic').innerHTML = '<option value="">Все темы</option>';
|
|
onFilterChange();
|
|
}
|
|
|
|
/* ── load topics after subject change ── */
|
|
async function loadTopics() {
|
|
const subject = document.getElementById('f-subject').value;
|
|
const topicSel = document.getElementById('f-topic');
|
|
topicSel.innerHTML = '<option value="">Все темы</option>';
|
|
if (!subject) return;
|
|
try {
|
|
const data = await LS.api('/api/questions?subject=' + subject + '&limit=200');
|
|
const rows = data.rows || [];
|
|
const topicMap = new Map();
|
|
rows.forEach(q => { if (q.topic_id && q.topic) topicMap.set(q.topic_id, q.topic); });
|
|
topicMap.forEach((name, id) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = id;
|
|
opt.textContent = name;
|
|
topicSel.appendChild(opt);
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
/* ── load questions ── */
|
|
async function loadQuestions() {
|
|
const f = getFilters();
|
|
const params = new URLSearchParams({ page: currentPage, limit: LIMIT });
|
|
if (f.q) params.set('q', f.q);
|
|
if (f.subject) params.set('subject', f.subject);
|
|
if (f.topic_id) params.set('topic_id', f.topic_id);
|
|
if (f.difficulty) params.set('difficulty', f.difficulty);
|
|
if (f.type) params.set('type', f.type);
|
|
if (f.sort === 'oldest') params.set('sort', 'oldest');
|
|
else if (f.sort === 'easy') params.set('sort', 'difficulty_asc');
|
|
else if (f.sort === 'hard') params.set('sort', 'difficulty_desc');
|
|
|
|
document.getElementById('qb-grid').innerHTML = '<div style="grid-column:1/-1"><div class="spinner" style="margin:60px auto"></div></div>';
|
|
document.getElementById('qb-pagination').style.display = 'none';
|
|
|
|
try {
|
|
const data = await LS.api('/api/questions?' + params.toString());
|
|
renderQuestions(data);
|
|
} catch (e) {
|
|
document.getElementById('qb-grid').innerHTML = `<div style="grid-column:1/-1" class="qb-empty"><div class="qb-empty-icon"><i data-lucide="alert-circle" style="width:48px;height:48px"></i></div>${esc(e.message || 'Ошибка загрузки')}</div>`;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
function renderQuestions(data) {
|
|
const { rows = [], total = 0, page = 1, limit = LIMIT } = data;
|
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
|
|
document.getElementById('found-count').textContent = total;
|
|
document.getElementById('hdr-count').textContent = total;
|
|
|
|
if (!rows.length) {
|
|
document.getElementById('qb-grid').innerHTML = `
|
|
<div style="grid-column:1/-1" class="qb-empty">
|
|
<div class="qb-empty-icon"><i data-lucide="search-x" style="width:52px;height:52px"></i></div>
|
|
<div style="font-family:'Unbounded',sans-serif;font-weight:800;font-size:0.95rem;color:#0F172A;margin-bottom:6px">Ничего не найдено</div>
|
|
Попробуйте изменить фильтры или сбросить их
|
|
</div>`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
rows.forEach(q => {
|
|
const diffCls = 'badge-diff-' + (q.difficulty || 1);
|
|
const diffLabel = q.difficulty === 1 ? 'Лёгкий' : q.difficulty === 2 ? 'Средний' : 'Сложный';
|
|
const typeLabel = TYPE_LABELS[q.type] || q.type || '—';
|
|
const isExpanded = expandedId === q.id;
|
|
const optsCount = (q.options || []).length;
|
|
|
|
html += `<div class="qc${isExpanded ? ' expanded' : ''}" id="qcard-${q.id}" onclick="toggleExpand(${q.id})">
|
|
<div class="qc-top">
|
|
<span class="badge ${diffCls}">${diffLabel}</span>
|
|
<span class="badge badge-type">${esc(typeLabel)}</span>
|
|
</div>
|
|
<div class="qc-text">${mathHtml(q.text)}</div>
|
|
<div class="qc-footer">
|
|
<span class="qc-topic"><i data-lucide="tag" style="width:11px;height:11px"></i> ${esc(q.topic || SUBJECT_NAMES[q.subject_slug] || '—')}</span>
|
|
${optsCount ? `<span class="qc-opts-count"><i data-lucide="list" style="width:11px;height:11px"></i> ${optsCount} вар.</span>` : ''}
|
|
</div>`;
|
|
|
|
if (isExpanded) {
|
|
html += `<div class="qc-preview">
|
|
<div class="qc-preview-text">${mathHtml(q.text)}</div>`;
|
|
if ((q.options || []).length) {
|
|
html += '<div class="qc-options">';
|
|
q.options.forEach((opt, idx) => {
|
|
const letter = String.fromCharCode(65 + idx);
|
|
html += `<div class="qc-option${opt.is_correct ? ' correct' : ''}">
|
|
<div class="qc-option-marker">${opt.is_correct ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : letter}</div>
|
|
<span>${mathHtml(opt.text)}</span>
|
|
</div>`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
if (q.explanation) {
|
|
html += `<div class="qc-explanation"><strong>Пояснение:</strong> ${mathHtml(q.explanation)}</div>`;
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
});
|
|
|
|
document.getElementById('qb-grid').innerHTML = html;
|
|
|
|
// pagination
|
|
const pagWrap = document.getElementById('qb-pagination');
|
|
if (totalPages > 1) {
|
|
pagWrap.style.display = '';
|
|
document.getElementById('pag-prev').disabled = page <= 1;
|
|
document.getElementById('pag-next').disabled = page >= totalPages;
|
|
document.getElementById('pag-info').textContent = page + ' / ' + totalPages;
|
|
} else {
|
|
pagWrap.style.display = 'none';
|
|
}
|
|
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function toggleExpand(id) {
|
|
expandedId = expandedId === id ? null : id;
|
|
loadQuestions();
|
|
}
|
|
|
|
function prevPage() { if (currentPage > 1) { currentPage--; loadQuestions(); } }
|
|
function nextPage() { currentPage++; loadQuestions(); }
|
|
|
|
lucide.createIcons();
|
|
loadQuestions();
|
|
</script>
|
|
<script src="/js/notifications.js"></script>
|
|
<script src="/js/search.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
</body>
|
|
</html>
|