Files
Learn_System/js/sidebar.js
T

249 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Universal sidebar — LearnSpace
// Generates full sidebar HTML into <aside id="app-sidebar">
// Load after api.js, before page-specific scripts.
(function () {
'use strict';
const el = document.getElementById('app-sidebar');
if (!el) return;
const user = (typeof LS !== 'undefined') ? LS.getUser?.() : null;
const role = user?.role || 'student';
const isTch = ['teacher', 'admin'].includes(role);
const isAdm = role === 'admin';
const isStu = role === 'student';
// Clean current path (strip .html extension)
const path = location.pathname.replace(/\.html$/, '');
// Active link: exact match OR prefix match (e.g. /biochem active for /biochem-library)
function isActive(href) {
const h = href.replace(/\.html$/, '');
if (path === h) return true;
if (h !== '/' && path.startsWith(h + '-')) return true;
if (h !== '/' && path.startsWith(h + '/')) return true;
return false;
}
function esc(s) {
return s ? String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') : '';
}
// Build an <a> sidebar link
function L(href, icon, label, { id = '', cls = '', hidden = false } = {}) {
const active = isActive(href) ? ' active' : '';
const classes = ['sb-link', active, cls].filter(Boolean).join(' ');
const idA = id ? ` id="${id}"` : '';
const hidA = hidden ? ' style="display:none"' : '';
return `<a href="${esc(href)}" class="${classes}"${idA}${hidA}><i data-lucide="${icon}" class="sb-icon"></i><span class="sb-lbl">${label}</span></a>`;
}
// Collapsible group: section header + body. State persisted in localStorage.
function G(slug, label, body) {
const stored = localStorage.getItem('ls_sb_g_' + slug);
const collapsed = stored === '1';
return `<div class="sb-group${collapsed ? ' collapsed' : ''}" data-sb-group="${slug}">
<button class="sb-group-hdr" onclick="window.__lsSbToggle('${slug}')">
<span class="sb-lbl sb-group-lbl">${label}</span>
<i data-lucide="chevron-down" class="sb-group-chev"></i>
</button>
<div class="sb-group-body">${body}</div>
</div>`;
}
const initials = (user?.name || 'LS').split(' ').slice(0, 2).map(w => (w[0] || '').toUpperCase()).join('') || 'LS';
const displayName = esc(user?.name || user?.email || '—');
el.innerHTML = `
<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="Свернуть меню" data-sb-wired="1"><i data-lucide="chevron-left" class="sb-icon"></i></button>
</div>
<nav class="sb-nav">
<button class="sb-link" onclick="typeof lsSearchOpen!=='undefined'&&lsSearchOpen()" title="Ctrl+K"><i data-lucide="search" class="sb-icon"></i><span class="sb-lbl">Поиск</span></button>
${L('/dashboard', 'home', 'Дашборд')}
${L('/sitemap', 'map', 'Путеводитель')}
${L('/teacher-guide', 'book-marked', 'Руководство', { cls: 'sb-teacher-only', hidden: !isTch })}
${G('learning', 'Учебный процесс', `
${L('/classes', 'graduation-cap', 'Классы', { id: 'btn-classes', hidden: !isTch })}
${L('/homework', 'clipboard-list', 'Домашние задания')}
${L('/my-students', 'user-plus', 'Мои ученики', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/classroom', 'presentation', 'Онлайн-урок', { id: 'btn-classroom', cls: 'sb-link-cr' })}
${L('/lesson-history','archive', 'Архив уроков')}
${L('/my-materials', 'bookmark', 'Мои материалы')}
${L('/live-quiz', 'radio', 'Live-квиз', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/board', 'layout-dashboard', 'Доска', { id: 'btn-board', hidden: true })}
`)}
${G('content', 'Контент', `
${L('/textbooks', 'book-open-text', 'Учебники')}
${L('/library', 'book-open', 'Библиотека')}
${L('/theory', 'brain', 'Теория')}
${L('/knowledge-map', 'share-2', 'Карта знаний')}
${L('/flashcards', 'copy', 'Флэшкарты')}
${L('/question-bank', 'database', 'Банк вопросов', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/exam-prep/math9', 'clipboard-check', 'Подготовка к экзамену 9')}
`)}
${G('practice', 'Практика и игры', `
${L('/lab', 'atom', 'Лаборатория')}
${L('/sim-builder', 'pencil-ruler', 'Конструктор симуляций', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/biochem', 'flask-conical', 'Биохимия')}
${L('/red-book', 'leaf', 'Красная книга')}
${L('/crossword', 'grid-3x3', 'Кроссворд')}
${L('/hangman', 'gamepad-2', 'Виселица')}
${L('/pet', 'heart', 'Питомец')}
${L('/collection', 'layers', 'Коллекция')}
`)}
${isTch ? G('management', 'Отчёты и управление', `
${L('/analytics', 'bar-chart-2', 'Аналитика', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/gradebook', 'table', 'Журнал', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/admin', 'settings', 'Управление', { id: 'btn-admin', hidden: !isTch })}
`) : ''}
</nav>
<div style="padding:4px 2px;flex-shrink:0">
${isStu ? '<button class="sb-link" id="btn-join" style="display:none" onclick="typeof openJoinModal!==\'undefined\'&&openJoinModal()"><i data-lucide="user-plus" class="sb-icon"></i><span class="sb-lbl">Вступить в класс</span></button>' : ''}
<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">${esc(initials)}</div>
<div class="sb-user-info">
<div class="sb-user-name" id="nav-user">${displayName}</div>
<span class="sb-logout" style="pointer-events:none">Мой профиль</span>
</div>
</a>
</div>`;
// Inject group styles once
if (!document.getElementById('sb-group-styles')) {
const style = document.createElement('style');
style.id = 'sb-group-styles';
style.textContent = `
.sb-group { margin: 10px 0 2px; }
.sb-group-hdr {
width: 100%; padding: 6px 14px 4px; border: none; background: none;
display: flex; align-items: center; justify-content: space-between;
font-family: 'Manrope', system-ui, sans-serif;
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em;
color: var(--text-3, #56687A); text-transform: uppercase;
cursor: pointer; transition: color .12s, opacity .12s;
opacity: 0.72;
}
.sb-group-hdr:hover { color: var(--violet, #9B5DE5); opacity: 1; }
.sb-group-chev {
width: 12px; height: 12px; transition: transform .18s;
}
.sb-group.collapsed .sb-group-chev { transform: rotate(-90deg); }
.sb-group-body {
display: flex; flex-direction: column;
max-height: 800px; overflow: hidden;
transition: max-height .25s ease, opacity .18s;
opacity: 1;
}
.sb-group.collapsed .sb-group-body {
max-height: 0; opacity: 0; pointer-events: none;
}
/* When sidebar collapsed (icon-only mode) — hide group headers */
.app-layout.sb-collapsed .sb-group-hdr { display: none; }
.app-layout.sb-collapsed .sb-group-body { max-height: none !important; opacity: 1 !important; pointer-events: auto !important; }
/* ── Онлайн-урок: всегда заметный пункт + live-индикатор ── */
.sb-link-cr .sb-icon { color: var(--violet, #9B5DE5); }
.sb-link-cr:not(.active) { font-weight: 700; }
.sb-cr-live {
display: none; align-items: center; gap: 5px; margin-left: auto; flex-shrink: 0;
padding: 3px 8px; border-radius: 999px;
font-size: 0.56rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase;
color: #fff; background: linear-gradient(135deg, #F15BB5, #9B5DE5);
}
.sb-cr-live::before {
content: ''; width: 6px; height: 6px; border-radius: 50%; background: #fff;
animation: sb-cr-pulse 1.15s ease-in-out infinite;
}
@keyframes sb-cr-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .3; transform: scale(.6); } }
.sb-link-cr.is-live { background: linear-gradient(90deg, rgba(241,91,181,.16), rgba(155,93,229,.05)); color: var(--text, #0F172A); }
.sb-link-cr.is-live .sb-icon { color: #F15BB5; }
.sb-link-cr.is-live .sb-cr-live { display: inline-flex; }
.sb-link-cr.is-live::after {
content: ''; position: absolute; left: 0; top: 5px; bottom: 5px; width: 3px;
border-radius: 0 3px 3px 0; background: linear-gradient(180deg, #F15BB5, #9B5DE5);
}
/* Свёрнутый сайдбар (только иконки): бейдж прячем, показываем точку-пульс */
.app-layout.sb-collapsed .sb-cr-live { display: none !important; }
.app-layout.sb-collapsed .sb-link-cr.is-live::after {
left: auto; right: 9px; top: 8px; bottom: auto; width: 8px; height: 8px;
border-radius: 50%; background: #F15BB5; animation: sb-cr-pulse 1.15s ease-in-out infinite;
}
`;
document.head.appendChild(style);
}
// Онлайн-урок: бейдж «В эфире» (скрыт; включается классом .is-live из api.js по SSE)
const crLink = el.querySelector('#btn-classroom');
if (crLink) crLink.insertAdjacentHTML('beforeend', '<span class="sb-cr-live">В эфире</span>');
// Toggle handler (global so onclick works)
window.__lsSbToggle = function (slug) {
const g = el.querySelector(`[data-sb-group="${slug}"]`);
if (!g) return;
const collapsed = g.classList.toggle('collapsed');
try { localStorage.setItem('ls_sb_g_' + slug, collapsed ? '1' : '0'); } catch {}
};
// Insert notif-drop sibling after <aside> if not already present
if (!document.getElementById('notif-drop')) {
const nd = document.createElement('div');
nd.className = 'notif-drop';
nd.id = 'notif-drop';
el.insertAdjacentElement('afterend', nd);
}
// Apply collapsed state
if (localStorage.getItem('ls_sb_collapsed') === '1') {
document.querySelector('.app-layout')?.classList.add('sb-collapsed');
}
// Wire sidebar toggle
el.querySelector('.sb-toggle')?.addEventListener('click', () => {
const layout = document.querySelector('.app-layout');
if (!layout) return;
const collapsed = layout.classList.toggle('sb-collapsed');
localStorage.setItem('ls_sb_collapsed', collapsed ? '1' : '0');
});
// Re-render Lucide icons
if (window.lucide) lucide.createIcons();
// Async: board visibility, feature flags, notifications
if (typeof LS !== 'undefined') {
LS.showBoardIfAllowed?.();
LS.hideDisabledFeatures?.();
LS.notif?.init?.();
}
// Глобальная плавающая кнопка «создать карточку» (на всех страницах с шапкой)
if (typeof LS !== 'undefined' && LS.isLoggedIn?.() && !document.getElementById('fc-fab-loader')) {
const s = document.createElement('script');
s.id = 'fc-fab-loader';
s.src = '/js/flashcard-fab.js';
s.defer = true;
document.body.appendChild(s);
}
// Квантик-ассистент — плавающий помощник на всех страницах с шапкой
if (typeof LS !== 'undefined' && LS.isLoggedIn?.() && !document.getElementById('asst-loader')) {
const s = document.createElement('script');
s.id = 'asst-loader';
s.src = '/js/assistant.js';
s.defer = true;
document.body.appendChild(s);
}
})();