feat(classroom): выделить вход в онлайн-урок — акцент в сайдбаре + липкий баннер
Пункт «Онлайн-урок» в сайдбаре теперь визуально выделен (акцентная иконка), а когда урок идёт — пульсирующий бейдж «В эфире» (и точка-пульс в свёрнутом режиме). Вместо легко пропускаемой всплывашки снизу — липкий баннер сверху на любой странице с кнопкой «Войти», пока урок активен. Состояние берётся из SSE classroom_started/ended + проверки /api/classroom/my/active при загрузке (чтобы баннер появлялся и при заходе в середине урока). Для учеников. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 = `
|
||||
<button class="ls-cr-nclose" onclick="document.getElementById('ls-cr-notify').classList.remove('open')">×</button>
|
||||
<div class="ls-cr-ntop"><span class="ls-cr-ndot"></span><span class="ls-cr-nbadge">Онлайн-урок</span></div>
|
||||
<div class="ls-cr-nteacher" id="ls-cr-nteacher"></div>
|
||||
<a href="/classroom" class="ls-cr-nbtn">Присоединиться</a>`;
|
||||
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 =
|
||||
'<span class="ls-crb-dot"></span>' +
|
||||
'<span class="ls-crb-txt">Идёт онлайн-урок<span id="ls-crb-title"></span></span>' +
|
||||
'<a href="/classroom" class="ls-crb-join">' + lsIcon('video', 15) + ' Войти</a>' +
|
||||
'<button class="ls-crb-x" aria-label="Скрыть">×</button>';
|
||||
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();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user