feat: universal sidebar via sidebar.js + stale ID cleanup
- 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>
This commit is contained in:
@@ -491,19 +491,21 @@ function initPage({ requireLogin = true } = {}) {
|
||||
document.querySelector('.app-layout')?.classList.add('sb-collapsed');
|
||||
}
|
||||
|
||||
// Sidebar toggle wiring
|
||||
// Sidebar toggle wiring (skip if sidebar.js already wired it via data-sb-wired)
|
||||
const togBtn = document.querySelector('.sb-toggle');
|
||||
if (togBtn) togBtn.addEventListener('click', () => {
|
||||
const layout = document.querySelector('.app-layout');
|
||||
const collapsed = layout.classList.toggle('sb-collapsed');
|
||||
localStorage.setItem('ls_sb_collapsed', collapsed ? '1' : '0');
|
||||
});
|
||||
if (togBtn && !togBtn.dataset.sbWired) {
|
||||
togBtn.addEventListener('click', () => {
|
||||
const layout = document.querySelector('.app-layout');
|
||||
const collapsed = layout.classList.toggle('sb-collapsed');
|
||||
localStorage.setItem('ls_sb_collapsed', collapsed ? '1' : '0');
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar active link
|
||||
// Sidebar active link (fallback for pages without sidebar.js)
|
||||
const currentPath = location.pathname.replace(/\.html$/, '').replace(/^\//, '') || 'dashboard';
|
||||
document.querySelectorAll('.sidebar .sb-item').forEach(a => {
|
||||
document.querySelectorAll('.sidebar .sb-link').forEach(a => {
|
||||
const href = a.getAttribute('href')?.replace(/^\//, '').replace(/\.html$/, '') || '';
|
||||
if (href === currentPath) a.classList.add('active');
|
||||
if (href && href === currentPath) a.classList.add('active');
|
||||
});
|
||||
|
||||
// Cosmetics
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') : '';
|
||||
}
|
||||
|
||||
// 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>`;
|
||||
}
|
||||
|
||||
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('/board', 'layout-dashboard', 'Доска', { id: 'btn-board', hidden: true })}
|
||||
${L('/classes', 'graduation-cap', 'Классы', { id: 'btn-classes', hidden: !isTch })}
|
||||
${L('/library', 'book-open', 'Библиотека')}
|
||||
${L('/theory', 'brain', 'Теория')}
|
||||
${L('/lab', 'atom', 'Лаборатория')}
|
||||
${L('/biochem', 'flask-conical', 'Биохимия')}
|
||||
${L('/hangman', 'gamepad-2', 'Виселица')}
|
||||
${L('/crossword', 'grid-3x3', 'Кроссворд')}
|
||||
${L('/pet', 'heart', 'Питомец')}
|
||||
${L('/collection', 'layers', 'Коллекция')}
|
||||
${L('/knowledge-map', 'share-2', 'Карта знаний')}
|
||||
${L('/red-book', 'leaf', 'Красная книга')}
|
||||
${L('/classroom', 'presentation', 'Онлайн-урок')}
|
||||
${L('/lesson-history','archive', 'Архив уроков')}
|
||||
<div class="sb-divider"></div>
|
||||
${L('/analytics', 'bar-chart-2', 'Аналитика', { cls: 'sb-teacher-only', hidden: !isTch })}
|
||||
${L('/question-bank', 'database', 'Банк вопросов', { cls: 'sb-teacher-only', hidden: !isTch })}
|
||||
${L('/live-quiz', 'radio', 'Live-квиз', { 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">
|
||||
${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>`;
|
||||
|
||||
// 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?.();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user