diff --git a/js/api.js b/js/api.js
index 1ea3303..0728cef 100644
--- a/js/api.js
+++ b/js/api.js
@@ -1616,60 +1616,102 @@ async function adminGamGetUser(id) { return req('GET', `/gamifi
});
})();
-/* ── Classroom started notification (students on any page) ─────────────── */
+/* ── Онлайн-урок: липкий верхний баннер + индикатор «В эфире» в сайдбаре ── */
+/* Пока идёт урок — заметный баннер сверху на ЛЮБОЙ странице + пульс пункта */
+/* «Онлайн-урок» в сайдбаре. Зайти можно одним кликом откуда угодно. */
(function initClassroomNotify() {
const token = getToken();
if (!token) return;
let payload; try { payload = JSON.parse(atob(token.split('.')[1])); } catch { return; }
- // Only show for students (teachers navigate manually)
+ // Только ученики (учителя сами начинают урок и обычно уже в нём)
if (!payload || payload.role === 'teacher' || payload.role === 'admin') return;
- // Don't show if already on classroom page
- if (window.location.pathname === '/classroom') return;
+ const onClassroomPage = () => window.location.pathname.replace(/\.html$/, '') === '/classroom';
const STYLE = `
- #ls-cr-notify{position:fixed;bottom:24px;right:24px;z-index:8000;
- background:#0F172A;border:1.5px solid rgba(155,93,229,.4);border-radius:18px;
- padding:18px 20px;max-width:320px;box-shadow:0 20px 60px rgba(0,0,0,.5);
- display:none;animation:ls-cr-in .3s cubic-bezier(.34,1.56,.64,1);}
- #ls-cr-notify.open{display:block;}
- @keyframes ls-cr-in{from{opacity:0;transform:translateY(16px) scale(.95)}to{opacity:1;transform:none}}
- .ls-cr-ntop{display:flex;align-items:center;gap:10px;margin-bottom:10px;}
- .ls-cr-ndot{width:8px;height:8px;border-radius:50%;background:#9B5DE5;flex-shrink:0;
- animation:ls-live-pulse 1s ease infinite;}
- .ls-cr-nbadge{font-size:.7rem;font-weight:800;color:#9B5DE5;text-transform:uppercase;letter-spacing:.06em;}
- .ls-cr-nteacher{font-size:.82rem;color:#94A3B8;margin-bottom:12px;}
- .ls-cr-nbtn{display:block;width:100%;padding:10px;border-radius:12px;
- background:linear-gradient(135deg,#9B5DE5,#7C3ACD);color:#fff;
- font-size:.88rem;font-weight:700;text-align:center;text-decoration:none;
- transition:opacity .15s;cursor:pointer;border:none;}
- .ls-cr-nbtn:hover{opacity:.9;}
- .ls-cr-nclose{position:absolute;top:10px;right:12px;background:none;border:none;
- color:#475569;cursor:pointer;font-size:1rem;line-height:1;padding:4px;}
+ #ls-cr-banner{position:fixed;top:14px;left:50%;z-index:8500;display:none;align-items:center;gap:14px;
+ transform:translateX(-50%) translateY(-130%);
+ padding:9px 10px 9px 18px;border-radius:999px;max-width:calc(100vw - 24px);
+ background:linear-gradient(135deg,#1c1233,#2c1656);border:1.5px solid rgba(155,93,229,.55);
+ box-shadow:0 16px 50px rgba(124,58,205,.45);
+ transition:transform .4s cubic-bezier(.34,1.4,.64,1),opacity .25s;opacity:0;}
+ #ls-cr-banner.open{display:flex;transform:translateX(-50%) translateY(0);opacity:1;}
+ .ls-crb-dot{width:9px;height:9px;border-radius:50%;background:#F15BB5;flex-shrink:0;
+ animation:ls-crb-pulse 1.3s ease infinite;}
+ @keyframes ls-crb-pulse{0%{box-shadow:0 0 0 0 rgba(241,91,181,.55)}70%{box-shadow:0 0 0 9px rgba(241,91,181,0)}100%{box-shadow:0 0 0 0 rgba(241,91,181,0)}}
+ .ls-crb-txt{color:#F3EEFF;font-size:.86rem;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
+ font-family:'Manrope',sans-serif;}
+ .ls-crb-txt b{font-weight:800;color:#fff;}
+ .ls-crb-join{flex-shrink:0;display:inline-flex;align-items:center;gap:6px;padding:8px 18px;border-radius:999px;
+ border:none;background:linear-gradient(135deg,#06D6E0,#9B5DE5);color:#fff;font-weight:800;font-size:.82rem;
+ font-family:'Manrope',sans-serif;text-decoration:none;cursor:pointer;transition:opacity .15s,transform .15s;}
+ .ls-crb-join:hover{opacity:.92;transform:translateY(-1px);}
+ .ls-crb-x{flex-shrink:0;background:none;border:none;color:#8b7da8;cursor:pointer;padding:4px 8px;
+ font-size:1.15rem;line-height:1;border-radius:8px;transition:color .15s;}
+ .ls-crb-x:hover{color:#fff;}
+ @media (max-width:768px){#ls-cr-banner{top:64px;left:12px;right:12px;transform:translateY(-130%);}
+ #ls-cr-banner.open{transform:translateY(0);}.ls-crb-txt{white-space:normal;}}
`;
- const el = document.createElement('div');
- el.id = 'ls-cr-notify';
- el.innerHTML = `
-
-
Онлайн-урок
-
- Присоединиться`;
+ let bannerEl = null, dismissed = false, current = null;
- const styleEl = document.createElement('style');
- styleEl.textContent = STYLE;
-
- document.addEventListener('DOMContentLoaded', () => {
+ function buildBanner() {
+ if (bannerEl) return bannerEl;
+ const styleEl = document.createElement('style');
+ styleEl.textContent = STYLE;
document.head.appendChild(styleEl);
- document.body.appendChild(el);
+ bannerEl = document.createElement('div');
+ bannerEl.id = 'ls-cr-banner';
+ bannerEl.setAttribute('role', 'status');
+ bannerEl.innerHTML =
+ '' +
+ 'Идёт онлайн-урок' +
+ '' + lsIcon('video', 15) + ' Войти' +
+ '';
+ document.body.appendChild(bannerEl);
+ bannerEl.querySelector('.ls-crb-x').onclick = () => { dismissed = true; bannerEl.classList.remove('open'); };
+ return bannerEl;
+ }
+
+ function setSidebarLive(on) {
+ const link = document.getElementById('btn-classroom');
+ if (link) link.classList.toggle('is-live', on);
+ }
+
+ function goLive(session) {
+ current = session || current;
+ setSidebarLive(true); // пульс в сайдбаре — всегда
+ if (onClassroomPage() || dismissed) return; // баннер не нужен на самой странице / если закрыли
+ const b = buildBanner();
+ const t = b.querySelector('#ls-crb-title');
+ const title = current && current.title;
+ t.textContent = '';
+ if (title) { t.appendChild(document.createTextNode(': ')); const bold = document.createElement('b'); bold.textContent = title; t.appendChild(bold); }
+ requestAnimationFrame(() => b.classList.add('open'));
+ }
+
+ function goEnded() {
+ current = null; dismissed = false;
+ setSidebarLive(false);
+ if (bannerEl) bannerEl.classList.remove('open');
+ }
+
+ function ready(fn) {
+ if (document.readyState !== 'loading') fn();
+ else document.addEventListener('DOMContentLoaded', fn);
+ }
+
+ ready(() => {
+ // Урок мог начаться ДО загрузки страницы — спросим сервер
+ if (typeof LS !== 'undefined' && LS.api) {
+ LS.api('/api/classroom/my/active')
+ .then(r => { const s = r && r.sessions && r.sessions[0]; if (s) goLive(s); })
+ .catch(() => {});
+ }
+ // Реалтайм: старт/конец урока
connectSSE(d => {
- if (d.type === 'classroom_started') {
- document.getElementById('ls-cr-nteacher').textContent =
- `${d.teacherName || 'Учитель'} начал урок${d.title ? ': ' + d.title : ''}`;
- el.classList.add('open');
- } else if (d.type === 'classroom_ended') {
- el.classList.remove('open');
- }
+ if (d.type === 'classroom_started') { dismissed = false; goLive({ title: d.title, sessionId: d.sessionId }); }
+ else if (d.type === 'classroom_ended') goEnded();
});
});
})();
diff --git a/js/sidebar.js b/js/sidebar.js
index 0bf5100..ad292b2 100644
--- a/js/sidebar.js
+++ b/js/sidebar.js
@@ -67,7 +67,7 @@
${G('learning', 'Учебный процесс', `
${L('/classes', 'graduation-cap', 'Классы', { id: 'btn-classes', hidden: !isTch })}
${L('/my-students', 'user-plus', 'Мои ученики', { cls: 'sb-teacher-only', hidden: !isTch })}
- ${L('/classroom', 'presentation', 'Онлайн-урок')}
+ ${L('/classroom', 'presentation', 'Онлайн-урок', { id: 'btn-classroom', cls: 'sb-link-cr' })}
${L('/lesson-history','archive', 'Архив уроков')}
${L('/live-quiz', 'radio', 'Live-квиз', { cls: 'sb-teacher-only', hidden: !isTch })}
${L('/board', 'layout-dashboard', 'Доска', { id: 'btn-board', hidden: true })}
@@ -150,10 +150,42 @@
/* 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', 'В эфире');
+
// Toggle handler (global so onclick works)
window.__lsSbToggle = function (slug) {
const g = el.querySelector(`[data-sb-group="${slug}"]`);