Files
Learn_System/frontend/question-bank.html
T
Maxim Dolgolyov d9e9e65328 fix: банк вопросов — сортировка и фильтр по темам
- Исправлены ключи сортировки: oldest→date_asc, easy→diff_asc, hard→diff_desc
- loadTopics() теперь использует /api/subjects/:slug/topics вместо
  запроса 200 вопросов (не хватало для 400+ вопросов)
- Добавлен onchange для select#f-topic (без него выбор темы не срабатывал)
2026-04-23 23:10:58 +03:00

606 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: var(--text-3);
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: var(--text-3);
display: flex; align-items: center; gap: 4px;
}
.qc-opts-count {
margin-left: auto; font-size: 0.7rem; color: var(--text-3); 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: var(--text-3); 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: var(--text-3); }
/* ── empty state ── */
.qb-empty {
text-align: center; padding: 80px 20px; color: var(--text-3); 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" onchange="onFilterChange()">
<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 topics = await LS.api('/api/subjects/' + subject + '/topics');
(Array.isArray(topics) ? topics : []).forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.name + (t.question_count ? ' (' + t.question_count + ')' : '');
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', 'date_asc');
else if (f.sort === 'easy') params.set('sort', 'diff_asc');
else if (f.sort === 'hard') params.set('sort', 'diff_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>