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}"]`);