Files
Learn_System/frontend/my-lessons.html
T
Maxim Dolgolyov edb4c211a0 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>
2026-04-13 21:22:21 +03:00

1135 lines
56 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Мои уроки — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/ls.css"/>
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
.ml-wrap {
--r: 12px;
--ml-bg: #f5f4fb;
--ml-surface: #ffffff;
--ml-surface-2: #f0eefb;
--ml-border: rgba(155,93,229,0.18);
--ml-border-h: rgba(155,93,229,0.4);
--ml-text: #1a1433;
--ml-text-2: #4a3f6b;
--ml-text-3: #8f86a8;
}
.app-layout { height: 100vh; overflow: hidden; }
.sb-content { height: 100%; overflow: hidden; }
.ml-wrap {
display: flex; flex-direction: column; gap: 0;
height: 100%; overflow: hidden;
background: var(--ml-bg); color: var(--ml-text);
}
/* ── Header ── */
.ml-header {
padding: 16px 32px 14px;
border-bottom: 1px solid var(--ml-border);
background: var(--ml-surface);
flex-shrink: 0;
}
.ml-header-top {
display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
}
.ml-title {
font-family: 'Unbounded', sans-serif; font-size: 1.2rem;
font-weight: 800; margin: 0;
display: flex; align-items: center; gap: 12px;
background: linear-gradient(135deg, #9B5DE5, #06D6E0);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.ml-title svg { color: #9B5DE5; -webkit-text-fill-color: initial; filter: drop-shadow(0 0 8px rgba(155,93,229,0.4)); }
.ml-filters {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px;
}
.ml-search {
display: flex; align-items: center; gap: 8px;
border: 1.5px solid var(--ml-border); border-radius: 10px;
padding: 8px 14px; background: var(--ml-surface);
transition: border-color .18s, box-shadow .18s;
}
.ml-search:focus-within { border-color: var(--violet); box-shadow: 0 0 0 3px rgba(155,93,229,0.15); }
.ml-search svg { color: var(--ml-text-3); flex-shrink: 0; }
.ml-search input {
border: none; outline: none; background: none;
font-family: 'Manrope', sans-serif; font-size: 0.82rem; color: var(--ml-text);
width: 190px;
}
.ml-search input::placeholder { color: var(--ml-text-3); }
/* ── KPI ── */
.ml-kpi {
display: flex; gap: 8px; padding: 10px 32px 12px; flex-shrink: 0; flex-wrap: wrap;
}
.ml-kpi-card {
flex: 1; min-width: 140px;
background: var(--ml-surface); border: 1px solid var(--ml-border);
border-radius: 12px; padding: 10px 14px;
display: flex; align-items: center; gap: 12px;
transition: border-color .2s, box-shadow .2s; position: relative; overflow: hidden;
}
.ml-kpi-card::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(155,93,229,0.03), rgba(6,214,224,0.01));
pointer-events: none;
}
.ml-kpi-card:hover { border-color: rgba(155,93,229,0.25); box-shadow: 0 2px 12px rgba(155,93,229,0.1); }
.ml-kpi-icon {
width: 36px; height: 36px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.ml-kpi-icon svg { width: 16px; height: 16px; }
.ml-kpi-icon.violet { background: rgba(155,93,229,0.12); color: #9B5DE5; }
.ml-kpi-icon.cyan { background: rgba(6,214,224,0.1); color: #06D6E0; }
.ml-kpi-icon.pink { background: rgba(241,91,181,0.1); color: #F15BB5; }
.ml-kpi-icon.yellow { background: rgba(255,224,102,0.1); color: #FFE066; }
.ml-kpi-val { font-family: 'Unbounded', sans-serif; font-size: 1.05rem; font-weight: 800; color: var(--ml-text); }
.ml-kpi-lbl { font-size: 0.68rem; color: var(--ml-text-3); margin-top: 1px; font-weight: 500; }
/* ── Body ── */
.ml-body { display: flex; flex: 1; overflow: hidden; gap: 0; }
/* ── List panel ── */
.ml-list-panel {
width: 340px; flex-shrink: 0;
border-right: 1px solid var(--ml-border);
display: flex; flex-direction: column; overflow: hidden;
background: var(--ml-surface-2);
}
@media (max-width: 768px) {
.ml-list-panel { width: 100%; border-right: none; }
.ml-list-panel.hidden-mobile { display: none; }
.ml-detail-panel.hidden-mobile { display: none; }
}
.ml-list-scroll {
flex: 1; overflow-y: auto; padding: 12px;
scrollbar-width: thin; scrollbar-color: rgba(155,93,229,0.2) transparent;
}
.ml-list-header { padding: 10px 12px 0; flex-shrink: 0; }
.ml-sort-row {
display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 4px 8px;
}
.ml-count-badge { font-size: 0.75rem; color: var(--ml-text-3); font-weight: 600; }
.ml-sort-select {
padding: 5px 10px; border-radius: 8px; border: 1px solid var(--ml-border);
background: var(--ml-surface); color: var(--ml-text-2); font-family: 'Manrope', sans-serif;
font-size: 0.75rem; font-weight: 600; cursor: pointer; outline: none;
transition: border-color .15s;
}
.ml-sort-select:focus { border-color: var(--violet); }
.ml-sort-select option { background: #ffffff; color: #1a1433; }
/* ── Session card ── */
.ml-session-card {
position: relative;
background: var(--ml-surface); border: 1px solid var(--ml-border);
border-left: 3px solid rgba(6,214,224,0.45);
border-radius: var(--r); padding: 14px 16px; cursor: pointer;
transition: all .18s ease; margin-bottom: 8px;
}
.ml-session-card:hover {
border-color: var(--ml-border-h);
border-left-color: #06D6E0;
box-shadow: 0 8px 32px rgba(6,214,224,0.15);
transform: translateY(-2px);
}
.ml-session-card.active {
border-color: rgba(6,214,224,0.35); border-left-color: #06D6E0;
background: linear-gradient(135deg, rgba(6,214,224,0.07), rgba(155,93,229,0.04));
box-shadow: 0 0 24px rgba(6,214,224,0.1);
}
.ml-card-row1 {
display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 8px;
}
.ml-card-title {
font-weight: 700; font-size: 0.85rem; color: var(--ml-text);
line-height: 1.35; flex: 1;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.ml-card-date { font-size: 0.72rem; color: var(--ml-text-3); white-space: nowrap; flex-shrink: 0; margin-top: 2px; }
.ml-card-row2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ml-card-teacher {
font-size: 0.72rem; font-weight: 700; color: #06D6E0;
background: rgba(6,214,224,0.1); padding: 3px 10px; border-radius: 99px;
display: flex; align-items: center; gap: 5px;
}
.ml-card-class {
font-size: 0.72rem; font-weight: 700; color: #9B5DE5;
background: rgba(155,93,229,0.1); padding: 3px 10px; border-radius: 99px;
}
.ml-card-stat {
display: flex; align-items: center; gap: 4px;
font-size: 0.72rem; color: var(--ml-text-2); font-weight: 500;
}
.ml-card-stat svg { color: var(--ml-text-3); }
/* My time badge */
.ml-card-mytime {
display: inline-flex; align-items: center; gap: 5px;
background: linear-gradient(135deg, rgba(6,214,224,0.1), rgba(155,93,229,0.07));
border: 1px solid rgba(6,214,224,0.2);
border-radius: 99px; padding: 3px 10px;
font-size: 0.72rem; font-weight: 700; color: #06D6E0;
}
/* ── Date separator ── */
.ml-date-sep {
font-size: 0.68rem; font-weight: 700; color: var(--ml-text-3);
text-transform: uppercase; letter-spacing: 0.08em;
padding: 10px 8px 6px; margin-top: 6px;
display: flex; align-items: center; gap: 8px;
}
.ml-date-sep::before {
content: '';
width: 16px; height: 16px; border-radius: 6px;
background: rgba(6,214,224,0.1);
flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2306D6E0' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: center; background-size: 10px;
}
/* ── Empty ── */
.ml-empty {
text-align: center; padding: 56px 24px; color: var(--ml-text-3);
display: flex; flex-direction: column; align-items: center; gap: 12px;
}
.ml-empty svg { display: block; opacity: 0.2; filter: drop-shadow(0 0 20px rgba(6,214,224,0.3)); }
.ml-empty p { font-size: 0.85rem; margin: 0; }
/* ── Pagination ── */
.ml-pagination {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 12px; border-top: 1px solid var(--ml-border); flex-shrink: 0;
}
.ml-page-btn {
padding: 6px 14px; border-radius: 10px; border: 1px solid var(--ml-border);
background: var(--ml-surface); color: var(--ml-text-2); font-size: 0.78rem; font-weight: 600;
cursor: pointer; transition: all .15s;
}
.ml-page-btn:hover:not(:disabled) { border-color: var(--violet); color: #9B5DE5; box-shadow: 0 0 12px rgba(155,93,229,0.15); }
.ml-page-btn:disabled { opacity: 0.35; cursor: default; }
.ml-page-info { font-size: 0.78rem; color: var(--ml-text-3); }
/* ── Skeleton ── */
.ml-skeleton {
background: linear-gradient(90deg, rgba(6,214,224,0.06) 25%, rgba(6,214,224,0.12) 50%, rgba(6,214,224,0.06) 75%);
background-size: 200% 100%; border-radius: 8px;
animation: ml-shimmer 1.6s ease-in-out infinite;
}
@keyframes ml-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.ml-sk-card { height: 84px; border-radius: var(--r); margin-bottom: 8px; }
/* ── Detail panel ── */
.ml-detail-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--ml-bg); }
.ml-detail-empty {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 18px;
color: var(--ml-text-3); padding: 40px;
}
.ml-detail-empty svg { opacity: 0.15; filter: drop-shadow(0 0 30px rgba(6,214,224,0.3)); }
.ml-detail-empty p { font-size: 0.9rem; margin: 0; }
.ml-detail-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* ── Detail header ── */
.ml-detail-hdr {
padding: 10px 22px 8px; border-bottom: 1px solid var(--ml-border);
background: var(--ml-surface); flex-shrink: 0;
}
.ml-detail-back {
display: none; align-items: center; gap: 6px;
font-size: 0.78rem; font-weight: 600; color: var(--ml-text-3);
cursor: pointer; margin-bottom: 12px; background: none; border: none; padding: 0; transition: color .15s;
}
.ml-detail-back:hover { color: #06D6E0; }
@media (max-width: 768px) { .ml-detail-back { display: flex; } }
.ml-detail-title {
font-family: 'Unbounded', sans-serif; font-size: 0.95rem; font-weight: 800;
color: var(--ml-text); margin: 0 0 6px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ml-detail-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
.ml-meta-item {
display: inline-flex; align-items: center; gap: 5px;
font-size: 0.7rem; font-weight: 600; color: var(--ml-text-2);
background: rgba(6,214,224,0.06); padding: 3px 9px;
border-radius: 99px; border: 1px solid rgba(6,214,224,0.12);
}
.ml-meta-item svg { color: var(--ml-text-3); }
.ml-detail-stats { display: flex; gap: 6px; flex-wrap: wrap; }
.ml-stat-chip {
display: flex; align-items: center; gap: 5px;
background: var(--ml-surface); border: 1px solid var(--ml-border);
border-radius: 99px; padding: 3px 10px;
font-size: 0.7rem; font-weight: 700; color: var(--ml-text-2);
}
.ml-stat-chip svg { color: var(--ml-text-3); }
/* My time chip — highlighted */
.ml-stat-chip.mytime {
background: linear-gradient(135deg, rgba(6,214,224,0.08), rgba(155,93,229,0.06));
border-color: rgba(6,214,224,0.25); color: #06D6E0;
}
/* ── Tabs ── */
.ml-tabs {
display: flex; gap: 4px; padding: 8px 22px;
border-bottom: 1px solid var(--ml-border); background: var(--ml-surface); flex-shrink: 0;
}
.ml-tab {
padding: 6px 14px; border-radius: 8px; border: 1px solid transparent;
font-size: 0.79rem; font-weight: 600; color: var(--ml-text-3);
cursor: pointer; background: none; transition: all .18s;
display: flex; align-items: center; gap: 6px;
}
.ml-tab:hover { color: var(--ml-text-2); background: rgba(6,214,224,0.07); }
.ml-tab.active {
color: #06D6E0; background: rgba(6,214,224,0.1);
border-color: rgba(6,214,224,0.25); box-shadow: 0 0 16px rgba(6,214,224,0.08);
}
.ml-tab svg { width: 13px; height: 13px; }
.ml-tab-content { flex: 1; overflow: hidden; min-height: 0; }
/* ── Board tab (identical to lesson-history) ── */
.lh-board-wrap { height: 100%; display: flex; flex-direction: column; overflow: hidden; padding: 0 20px 40px 0; }
.lh-board-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; position: relative; }
.lh-board-topbar {
height: 48px; flex-shrink: 0; position: relative;
background: #0e0b1e; border-bottom: 1px solid rgba(155,93,229,0.18);
display: flex; align-items: center; justify-content: space-between; padding: 0 16px; gap: 8px;
}
.lh-board-nav-btn {
width: 32px; height: 32px; border-radius: 9px;
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.09);
color: rgba(255,255,255,0.55); cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: all .15s; flex-shrink: 0;
}
.lh-board-nav-btn:hover:not(:disabled) { background: rgba(6,214,224,0.2); border-color: rgba(6,214,224,0.4); color: #06D6E0; }
.lh-board-nav-btn:disabled { opacity: 0.18; cursor: default; }
.lh-page-indicator {
display: flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 10px;
background: rgba(6,214,224,0.06); border: 1.5px solid rgba(6,214,224,0.18);
color: rgba(255,255,255,0.85); cursor: pointer;
font-family: 'Manrope', sans-serif; font-size: 12px; font-weight: 700; transition: all .15s;
}
.lh-page-indicator:hover { background: rgba(6,214,224,0.12); border-color: rgba(6,214,224,0.35); }
.lh-page-indicator.open { background: rgba(6,214,224,0.15); border-color: #06D6E0; box-shadow: 0 0 16px rgba(6,214,224,0.2); }
.lh-page-ind-title { max-width: 160px; overflow: hidden; text-overflow: ellipsis; }
.lh-page-ind-counter { color: rgba(6,214,224,0.75); font-size: 11px; }
.lh-page-ind-chevron { color: rgba(255,255,255,0.35); transition: transform .18s; }
.lh-page-indicator.open .lh-page-ind-chevron { transform: rotate(180deg); }
.lh-pages-popup {
position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%);
z-index: 200; background: rgba(18,14,32,0.92); border: 1px solid rgba(6,214,224,0.25);
border-radius: 16px; padding: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,0.7); backdrop-filter: blur(20px);
display: grid; grid-template-columns: repeat(4, 120px); gap: 10px;
max-height: 70vh; overflow-y: auto; scrollbar-width: thin;
}
.lh-pages-popup.hidden { display: none; }
@media (max-width: 700px) { .lh-pages-popup { grid-template-columns: repeat(2, 140px); } }
.lh-thumb-item {
cursor: pointer; border-radius: 10px; border: 2px solid rgba(255,255,255,0.06);
background: rgba(28,22,48,0.8); overflow: hidden; transition: all .18s;
}
.lh-thumb-item:hover { border-color: rgba(6,214,224,0.45); transform: translateY(-2px); }
.lh-thumb-item.active { border-color: #06D6E0; box-shadow: 0 0 0 3px rgba(6,214,224,0.2); }
.lh-thumb-canvas-wrap { width: 100%; aspect-ratio: 16/9; background: #2d5a2d; overflow: hidden; }
.lh-thumb-canvas-wrap canvas { display: block; width: 100%; height: 100%; }
.lh-thumb-footer { padding: 6px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; }
.lh-thumb-lbl { font-size: 10px; color: rgba(255,255,255,0.55); font-family: 'Manrope', sans-serif; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
.lh-thumb-num { font-size: 10px; color: rgba(6,214,224,0.65); font-family: 'Manrope', sans-serif; font-weight: 800; }
.lh-board-canvas-wrap {
flex: 1; position: relative; overflow: hidden; background: #2d5a2d; min-height: 0;
}
.lh-board-canvas-wrap::after {
content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 5;
background: linear-gradient(to bottom, rgba(0,0,0,0.18) 0%, transparent 60px), linear-gradient(to top, rgba(0,0,0,0.12) 0%, transparent 80px);
}
.lh-board-canvas { position: absolute; top: 0; left: 0; display: block; }
.lh-board-zoom {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 1px; z-index: 20;
background: rgba(10,7,24,0.88); border: 1px solid rgba(6,214,224,0.22);
border-radius: 14px; padding: 5px 6px; backdrop-filter: blur(16px);
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
}
.lh-zoom-btn {
width: 34px; height: 34px; border-radius: 9px; background: transparent; border: none;
color: rgba(255,255,255,0.55); cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: background .15s, color .15s;
}
.lh-zoom-btn:hover { background: rgba(6,214,224,0.2); color: #06D6E0; }
.lh-zoom-lbl { font-size: 12px; color: rgba(255,255,255,0.7); font-family: 'Manrope', sans-serif; font-weight: 700; min-width: 44px; text-align: center; }
.lh-zoom-divider { width: 1px; height: 18px; background: rgba(255,255,255,0.08); margin: 0 4px; }
.lh-board-export { position: absolute; top: 14px; right: 16px; z-index: 20; }
.lh-export-btn {
display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 10px;
background: rgba(10,7,24,0.75); border: 1px solid rgba(6,214,224,0.22);
color: rgba(255,255,255,0.8); font-size: 11px; font-family: 'Manrope', sans-serif;
font-weight: 700; cursor: pointer; transition: all .18s; backdrop-filter: blur(12px);
}
.lh-export-btn:hover { background: rgba(6,214,224,0.25); color: #fff; border-color: rgba(6,214,224,0.45); }
.lh-board-loading {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px;
background: #2d5a2d; color: rgba(255,255,255,0.45);
font-family: 'Manrope', sans-serif; font-size: 0.82rem; z-index: 30;
}
.lh-board-spinner {
width: 36px; height: 36px; border-radius: 50%;
border: 3px solid rgba(6,214,224,0.12); border-top-color: #06D6E0;
animation: ml-spin 0.75s linear infinite;
}
@keyframes ml-spin { to { transform: rotate(360deg); } }
/* ── Chat tab ── */
.lh-chat-wrap {
height: 100%; overflow-y: auto; padding: 18px 22px;
display: flex; flex-direction: column; gap: 6px;
scrollbar-width: thin; scrollbar-color: rgba(6,214,224,0.2) transparent;
}
.lh-chat-toolbar { display: flex; justify-content: flex-end; padding: 12px 22px 0; flex-shrink: 0; }
.lh-msg {
display: flex; align-items: flex-start; gap: 12px;
padding: 10px 14px; border-radius: 12px; transition: background .15s;
}
.lh-msg:hover { background: rgba(6,214,224,0.04); }
.lh-msg-avatar {
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: #fff;
}
.lh-msg-body { flex: 1; min-width: 0; }
.lh-msg-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.lh-msg-name { font-size: 0.8rem; font-weight: 700; color: var(--ml-text); }
.lh-msg-time { font-size: 0.72rem; color: var(--ml-text-3); }
.lh-msg-text {
font-size: 0.83rem; color: var(--ml-text-2); line-height: 1.55;
word-break: break-word; white-space: pre-wrap;
background: rgba(6,214,224,0.04); padding: 8px 14px; border-radius: 0 12px 12px 12px;
border: 1px solid rgba(6,214,224,0.08);
}
.lh-msg-img { max-width: 220px; border-radius: 10px; margin-top: 6px; display: block; }
.lh-msg.pinned {
background: rgba(255,224,102,0.04); border: 1px solid rgba(255,224,102,0.12); border-radius: 12px; padding: 10px 14px;
}
.lh-msg.pinned .lh-msg-name { color: #FFE066; }
.lh-msg.mine .lh-msg-text {
background: rgba(6,214,224,0.1); border-color: rgba(6,214,224,0.2); color: var(--ml-text);
}
/* ── Notes tab ── */
.lh-notes-wrap {
height: 100%; overflow-y: auto; padding: 18px 22px;
scrollbar-width: thin; scrollbar-color: rgba(6,214,224,0.2) transparent;
}
.lh-own-note-box {
background: rgba(6,214,224,0.04); border: 1px solid rgba(6,214,224,0.15);
border-radius: var(--r); padding: 18px; margin-bottom: 12px;
}
.lh-own-note-lbl { font-size: 0.78rem; font-weight: 700; color: #06D6E0; margin-bottom: 10px; }
.lh-note-text { font-size: 0.83rem; color: var(--ml-text-2); line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<main class="sb-content">
<div class="ml-wrap">
<!-- Header -->
<div class="ml-header">
<div class="ml-header-top">
<h1 class="ml-title">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
Мои уроки
</h1>
</div>
<div class="ml-filters">
<div class="ml-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" id="ml-search" placeholder="Поиск по названию, учителю..." oninput="onSearch()"/>
</div>
</div>
</div>
<!-- KPI -->
<div class="ml-kpi" id="ml-kpi">
<div class="ml-kpi-card">
<div class="ml-kpi-icon cyan"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg></div>
<div><div class="ml-kpi-val" id="kpi-total">&mdash;</div><div class="ml-kpi-lbl">Уроков посещено</div></div>
</div>
<div class="ml-kpi-card">
<div class="ml-kpi-icon violet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
<div><div class="ml-kpi-val" id="kpi-time">&mdash;</div><div class="ml-kpi-lbl">Общее время</div></div>
</div>
<div class="ml-kpi-card">
<div class="ml-kpi-icon pink"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
<div><div class="ml-kpi-val" id="kpi-teachers">&mdash;</div><div class="ml-kpi-lbl">Учителей</div></div>
</div>
<div class="ml-kpi-card">
<div class="ml-kpi-icon yellow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
<div><div class="ml-kpi-val" id="kpi-msgs">&mdash;</div><div class="ml-kpi-lbl">Сообщений в чатах</div></div>
</div>
</div>
<!-- Body -->
<div class="ml-body">
<!-- Session list -->
<div class="ml-list-panel" id="ml-list-panel">
<div class="ml-list-header">
<div class="ml-sort-row">
<span class="ml-count-badge" id="ml-count-badge"></span>
<select class="ml-sort-select" id="ml-sort" onchange="onSort()">
<option value="newest">Сначала новые</option>
<option value="oldest">Сначала старые</option>
<option value="longest">По длительности</option>
</select>
</div>
</div>
<div class="ml-list-scroll" id="ml-list">
<div class="ml-sk-card ml-skeleton"></div>
<div class="ml-sk-card ml-skeleton"></div>
<div class="ml-sk-card ml-skeleton"></div>
</div>
<div class="ml-pagination" id="ml-pagination" style="display:none">
<button class="ml-page-btn" id="ml-prev" onclick="changePage(-1)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span class="ml-page-info" id="ml-page-info">1 / 1</span>
<button class="ml-page-btn" id="ml-next" onclick="changePage(1)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
</div>
<!-- Detail -->
<div class="ml-detail-panel" id="ml-detail-panel">
<div class="ml-detail-empty" id="ml-detail-empty">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
<p>Выберите урок из списка слева</p>
</div>
<div class="ml-detail-content" id="ml-detail-content" style="display:none">
<div class="ml-detail-hdr">
<button class="ml-detail-back" onclick="backToList()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
Назад к списку
</button>
<h2 class="ml-detail-title" id="det-title">&mdash;</h2>
<div class="ml-detail-meta">
<span class="ml-meta-item" id="det-date">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</span>
<span class="ml-meta-item" id="det-teacher">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span class="ml-meta-item" id="det-class" style="display:none">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
</span>
</div>
<div class="ml-detail-stats" id="det-stats"></div>
</div>
<!-- Tabs -->
<div class="ml-tabs">
<button class="ml-tab active" id="tab-board" onclick="switchTab('board')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
Доска
</button>
<button class="ml-tab" id="tab-chat" onclick="switchTab('chat')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Чат
<span id="tab-chat-count" style="display:none;background:rgba(6,214,224,.15);color:#06D6E0;font-size:10px;padding:2px 7px;border-radius:99px;font-weight:700"></span>
</button>
<button class="ml-tab" id="tab-notes" onclick="switchTab('notes')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Мои заметки
</button>
</div>
<!-- Tab contents -->
<div class="ml-tab-content">
<!-- Board -->
<div id="tc-board" class="lh-board-wrap">
<div class="lh-board-main">
<div class="lh-board-topbar">
<button class="lh-board-nav-btn" id="lh-page-prev" onclick="lhPageNav(-1)" title="Предыдущая">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button class="lh-page-indicator" id="lh-page-indicator" onclick="lhTogglePages()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
<span class="lh-page-ind-title" id="lh-page-title">Страница 1</span>
<span class="lh-page-ind-counter" id="lh-page-total"></span>
<svg class="lh-page-ind-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="lh-pages-popup hidden" id="lh-pages-popup"><div id="lh-thumbs"></div></div>
<button class="lh-board-nav-btn" id="lh-page-next" onclick="lhPageNav(1)" title="Следующая">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>
<div class="lh-board-canvas-wrap">
<canvas id="lh-canvas" class="lh-board-canvas"></canvas>
<div class="lh-board-loading" id="lh-board-loading">
<div class="lh-board-spinner"></div>
<span>Загрузка доски...</span>
</div>
<div class="lh-board-export">
<button class="lh-export-btn" onclick="exportBoardPage()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
PNG
</button>
</div>
<div class="lh-board-zoom">
<button class="lh-zoom-btn" onclick="lhZoom(-0.25)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<span class="lh-zoom-lbl" id="lh-zoom-lbl">100%</span>
<button class="lh-zoom-btn" onclick="lhZoom(0.25)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
<div class="lh-zoom-divider"></div>
<button class="lh-zoom-btn" onclick="lhZoomFit()"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg></button>
</div>
</div>
</div>
</div>
<!-- Chat -->
<div id="tc-chat" style="display:none;height:100%;flex-direction:column;overflow:hidden">
<div class="lh-chat-toolbar" id="lh-chat-toolbar"></div>
<div class="lh-chat-wrap" id="lh-chat-list"></div>
</div>
<!-- Notes -->
<div id="tc-notes" style="display:none">
<div class="lh-notes-wrap" id="lh-notes-list"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/mobile.js"></script>
<script src="/js/whiteboard.js"></script>
<script>
let _me = null;
let _sessions = [];
let _allSessions = []; // unfiltered for search
let _totalPages = 1;
let _curPage = 1;
let _searchTimer = null;
let _sortMode = 'newest';
let _activeSession = null;
let _wb = null;
let _wbPages = [];
let _wbCurrentPage = 1;
/* ─── Init ─── */
(function() {
const { user } = LS.initPage();
if (!user) return;
// Teachers and admins belong on lesson-history
if (user.role === 'teacher' || user.role === 'admin') {
location.replace('/lesson-history');
return;
}
_me = user;
if (window.lucide) lucide.createIcons();
loadSessions();
checkUrlSession();
})();
function checkUrlSession() {
const sp = new URLSearchParams(location.search);
const sid = sp.get('session');
if (sid) setTimeout(() => openSession(Number(sid)), 600);
}
/* ─── Sessions list ─── */
async function loadSessions() {
const list = document.getElementById('ml-list');
list.innerHTML = '<div class="ml-sk-card ml-skeleton"></div><div class="ml-sk-card ml-skeleton"></div><div class="ml-sk-card ml-skeleton"></div>';
try {
const data = await LS.crGetMyHistory(_curPage);
_allSessions = data.sessions || [];
_sessions = _allSessions;
_totalPages = data.pages || 1;
applySearch();
renderPagination(_curPage, _totalPages, data.total || 0);
updateKPI(_allSessions);
} catch (e) {
list.innerHTML = `<div class="ml-empty"><p>Ошибка загрузки: ${LS.escapeHtml(e.message || 'неизвестная ошибка')}</p></div>`;
}
}
function onSearch() {
clearTimeout(_searchTimer);
_searchTimer = setTimeout(applySearch, 250);
}
function applySearch() {
const q = (document.getElementById('ml-search')?.value || '').trim().toLowerCase();
_sessions = q
? _allSessions.filter(s =>
(s.title || '').toLowerCase().includes(q) ||
(s.teacher_name || '').toLowerCase().includes(q) ||
(s.class_name || '').toLowerCase().includes(q))
: _allSessions;
renderList(_sessions);
}
function onSort() {
_sortMode = document.getElementById('ml-sort').value;
renderList(_sessions);
}
function renderList(sessions) {
const list = document.getElementById('ml-list');
const badge = document.getElementById('ml-count-badge');
if (badge) badge.textContent = sessions.length ? plural(sessions.length, 'урок','урока','уроков') : '';
if (!sessions.length) {
list.innerHTML = `<div class="ml-empty">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
<p>Нет посещённых уроков</p></div>`;
return;
}
const sorted = [...sessions].sort((a, b) => {
if (_sortMode === 'oldest') return new Date(a.ended_at||0) - new Date(b.ended_at||0);
if (_sortMode === 'longest') {
const da = a.ended_at && a.created_at ? new Date(a.ended_at)-new Date(a.created_at) : 0;
const db_ = b.ended_at && b.created_at ? new Date(b.ended_at)-new Date(b.created_at) : 0;
return db_ - da;
}
return new Date(b.ended_at||0) - new Date(a.ended_at||0);
});
// Group by month
const groups = [];
let lastKey = null;
sorted.forEach(s => {
const d = s.ended_at ? new Date(s.ended_at) : null;
const key = d ? d.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' }) : 'Неизвестно';
if (key !== lastKey) { groups.push({ key, items: [] }); lastKey = key; }
groups[groups.length-1].items.push(s);
});
list.innerHTML = groups.map(g =>
`<div class="ml-date-sep">${g.key}</div>` + g.items.map(s => renderCard(s)).join('')
).join('');
}
function renderCard(s) {
const dateStr = s.ended_at
? new Date(s.ended_at).toLocaleDateString('ru-RU', { day:'numeric', month:'short' })
: '—';
const dur = s.ended_at && s.created_at
? fmtDuration(Math.round((new Date(s.ended_at) - new Date(s.created_at)) / 1000))
: null;
const active = _activeSession?.session?.id === s.id ? ' active' : '';
const teacher = s.teacher_name
? `<span class="ml-card-teacher">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
${LS.escapeHtml(s.teacher_name)}
</span>` : '';
const cls = s.class_name
? `<span class="ml-card-class">${LS.escapeHtml(s.class_name)}</span>` : '';
const durChip = dur
? `<span class="ml-card-mytime">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
${dur}
</span>` : '';
const msgs = (s.message_count||0) > 0
? `<span class="ml-card-stat">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
${s.message_count}
</span>` : '';
return `<div class="ml-session-card${active}" data-id="${s.id}" onclick="openSession(${s.id})">
<div class="ml-card-row1">
<div class="ml-card-title">${LS.escapeHtml(s.title || 'Без названия')}</div>
<div class="ml-card-date">${dateStr}</div>
</div>
<div class="ml-card-row2">${teacher}${cls}${durChip}${msgs}</div>
</div>`;
}
function renderPagination(page, total, count) {
const pg = document.getElementById('ml-pagination');
const prev = document.getElementById('ml-prev');
const next = document.getElementById('ml-next');
const info = document.getElementById('ml-page-info');
pg.style.display = total > 1 ? 'flex' : 'none';
prev.disabled = page <= 1;
next.disabled = page >= total;
info.textContent = `${page} / ${total}`;
}
function changePage(dir) {
_curPage = Math.max(1, Math.min(_totalPages, _curPage + dir));
loadSessions();
}
function updateKPI(sessions) {
const durSec = sessions.reduce((a, s) => {
if (!s.ended_at || !s.created_at) return a;
return a + Math.round((new Date(s.ended_at) - new Date(s.created_at)) / 1000);
}, 0);
const teachers = new Set(sessions.map(s => s.teacher_name).filter(Boolean)).size;
const msgs = sessions.reduce((a, s) => a + (s.message_count || 0), 0);
document.getElementById('kpi-total').textContent = sessions.length;
document.getElementById('kpi-time').textContent = fmtDuration(durSec);
document.getElementById('kpi-teachers').textContent = teachers;
document.getElementById('kpi-msgs').textContent = msgs;
}
/* ─── Session detail ─── */
async function openSession(id) {
document.querySelectorAll('.ml-session-card').forEach(el => {
el.classList.toggle('active', Number(el.dataset.id) === id);
});
document.getElementById('ml-detail-empty').style.display = 'none';
document.getElementById('ml-detail-content').style.display = 'flex';
document.getElementById('ml-detail-content').style.flexDirection = 'column';
if (window.innerWidth <= 768) {
document.getElementById('ml-list-panel').classList.add('hidden-mobile');
}
const url = new URL(location.href);
url.searchParams.set('session', id);
history.replaceState({}, '', url);
_chatLoaded = false; _notesLoaded = false;
try {
const data = await LS.crGetSessionSummary(id);
_activeSession = data;
renderDetailHeader(data);
_wbPages = data.pages || [];
_wbCurrentPage = 1;
switchTab('board');
renderThumbs();
loadBoardPage(1);
} catch (e) {
LS.toast('Ошибка загрузки урока: ' + (e.message || ''), 'error');
}
}
function renderDetailHeader(data) {
const s = data.session;
const st = data.stats;
const me = data.attendance?.find(a => a.user_id === _me?.id);
document.getElementById('det-title').textContent = s.title || 'Без названия';
const d = s.ended_at ? new Date(s.ended_at) : null;
const dateEl = document.getElementById('det-date');
dateEl.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
${d ? d.toLocaleDateString('ru-RU', { day:'numeric', month:'long', year:'numeric' }) : '—'}`;
const teachEl = document.getElementById('det-teacher');
teachEl.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
${LS.escapeHtml(s.teacher_name || '')}`;
const clsEl = document.getElementById('det-class');
if (s.class_name) {
clsEl.style.display = 'flex';
clsEl.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/><path d="M6 12v5c3 3 9 3 12 0v-5"/></svg>
${LS.escapeHtml(s.class_name)}`;
} else {
clsEl.style.display = 'none';
}
// Stats chips
const chips = [];
if (st.duration_sec) chips.push(`<span class="ml-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>Длительность: ${fmtDuration(st.duration_sec)}</span>`);
if (me?.duration_sec) chips.push(`<span class="ml-stat-chip mytime"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>Я провёл: ${fmtDuration(me.duration_sec)}</span>`);
if (st.page_count > 1) chips.push(`<span class="ml-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>${st.page_count} стр.</span>`);
if (st.message_count) chips.push(`<span class="ml-stat-chip"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>${st.message_count} сообщ.</span>`);
document.getElementById('det-stats').innerHTML = chips.join('');
// Chat count badge
const cc = document.getElementById('tab-chat-count');
if (cc && st.message_count) {
cc.style.display = 'inline';
cc.textContent = st.message_count;
} else if (cc) {
cc.style.display = 'none';
}
}
function backToList() {
document.getElementById('ml-list-panel').classList.remove('hidden-mobile');
document.getElementById('ml-detail-content').style.display = 'none';
document.getElementById('ml-detail-empty').style.display = 'flex';
const url = new URL(location.href);
url.searchParams.delete('session');
history.replaceState({}, '', url);
}
/* ─── Tabs ─── */
let _chatLoaded = false, _notesLoaded = false;
function switchTab(name) {
['board','chat','notes'].forEach(t => {
document.getElementById(`tab-${t}`)?.classList.toggle('active', t === name);
const tc = document.getElementById(`tc-${t}`);
if (tc) tc.style.display = t === name ? (t === 'chat' || t === 'board' ? 'flex' : 'block') : 'none';
});
if (name === 'chat' && _activeSession && !_chatLoaded) loadChat();
if (name === 'notes' && _activeSession && !_notesLoaded) loadNotes();
}
/* ─── Board ─── */
function renderThumbs() {
const container = document.getElementById('lh-thumbs');
if (!container) return;
container.innerHTML = '';
container.style.cssText = 'display:contents';
_wbPages.forEach(p => {
const item = document.createElement('div');
item.className = 'lh-thumb-item' + (p.page_num === _wbCurrentPage ? ' active' : '');
item.dataset.page = p.page_num;
item.onclick = () => { loadBoardPage(p.page_num); lhClosePages(); };
const wrap = document.createElement('div'); wrap.className = 'lh-thumb-canvas-wrap';
const cvs = document.createElement('canvas'); cvs.width = 192; cvs.height = 108;
wrap.appendChild(cvs);
const footer = document.createElement('div'); footer.className = 'lh-thumb-footer';
const lbl = document.createElement('span'); lbl.className = 'lh-thumb-lbl'; lbl.textContent = p.name || `Стр. ${p.page_num}`;
const num = document.createElement('span'); num.className = 'lh-thumb-num'; num.textContent = p.page_num;
footer.append(lbl, num);
item.append(wrap, footer);
container.appendChild(item);
});
updateBoardTopbar();
}
let _pagesPopupOpen = false;
function lhTogglePages() { _pagesPopupOpen ? lhClosePages() : lhOpenPages(); }
function lhOpenPages() {
_pagesPopupOpen = true;
document.getElementById('lh-pages-popup').classList.remove('hidden');
document.getElementById('lh-page-indicator').classList.add('open');
if (_activeSession) _renderAllThumbs(_activeSession.session.id);
}
function lhClosePages() {
_pagesPopupOpen = false;
document.getElementById('lh-pages-popup')?.classList.add('hidden');
document.getElementById('lh-page-indicator')?.classList.remove('open');
}
async function loadBoardPage(pageNum) {
if (!_activeSession) return;
const sessionId = _activeSession.session.id;
_wbCurrentPage = pageNum;
document.querySelectorAll('.lh-thumb-item').forEach(el => {
const active = Number(el.dataset.page) === pageNum;
el.classList.toggle('active', active);
if (active) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
});
updateBoardTopbar();
const loading = document.getElementById('lh-board-loading');
loading.style.display = 'flex';
try {
const res = await LS.get(`/api/classroom/${sessionId}/strokes?page_num=${pageNum}`);
if (_wb) { _wb.destroy(); _wb = null; }
await new Promise(r => requestAnimationFrame(r));
_wb = new Whiteboard(document.getElementById('lh-canvas'), {
readOnly: true,
onZoomChange: z => {
const el = document.getElementById('lh-zoom-lbl');
if (el) el.textContent = Math.round(z * 100) + '%';
},
});
_wb.setTemplate(res.template || 'blank');
_wb.loadStrokes(res.strokes || []);
_wb.zoomFitStrokes();
const thumbEl = document.querySelector(`.lh-thumb-item[data-page="${pageNum}"] canvas`);
if (thumbEl) _wb.renderThumbnail(thumbEl);
loading.style.display = 'none';
_renderAllThumbs(sessionId);
} catch {
loading.innerHTML = '<span style="color:rgba(255,100,100,.7)">Ошибка загрузки страницы</span>';
}
}
function updateBoardTopbar() {
const cur = _wbCurrentPage, total = _wbPages.length;
const page = _wbPages.find(p => p.page_num === cur);
const idx = _wbPages.findIndex(p => p.page_num === cur) + 1;
const titleEl = document.getElementById('lh-page-title');
const totalEl = document.getElementById('lh-page-total');
const prevBtn = document.getElementById('lh-page-prev');
const nextBtn = document.getElementById('lh-page-next');
if (titleEl) titleEl.textContent = page?.name || `Страница ${cur}`;
if (totalEl) totalEl.textContent = total > 1 ? `${idx} / ${total}` : '';
if (prevBtn) prevBtn.disabled = idx <= 1;
if (nextBtn) nextBtn.disabled = idx >= total;
}
function lhPageNav(dir) {
const idx = _wbPages.findIndex(p => p.page_num === _wbCurrentPage);
const next = _wbPages[idx + dir];
if (next) loadBoardPage(next.page_num);
}
let _thumbRenderJob = null;
async function _renderAllThumbs(sessionId) {
const jobId = Date.now();
_thumbRenderJob = jobId;
for (const p of _wbPages) {
if (_thumbRenderJob !== jobId) return;
if (p.page_num === _wbCurrentPage) continue;
const thumbEl = document.querySelector(`.lh-thumb-item[data-page="${p.page_num}"] canvas`);
if (!thumbEl || thumbEl._rendered) continue;
try {
const res = await LS.get(`/api/classroom/${sessionId}/strokes?page_num=${p.page_num}`);
if (_thumbRenderJob !== jobId) return;
const tmpCvs = document.createElement('canvas'); tmpCvs.width = 192; tmpCvs.height = 108;
const tmpWb = new Whiteboard(tmpCvs, { readOnly: true });
tmpWb.setTemplate(res.template || 'blank');
tmpWb.loadStrokes(res.strokes || []);
tmpWb.renderThumbnail(thumbEl);
tmpWb.destroy();
thumbEl._rendered = true;
} catch {}
await new Promise(r => setTimeout(r, 80));
}
}
function lhZoom(delta) {
if (!_wb) return;
_wb.zoomTo(Math.max(0.25, Math.min(4, (_wb._zoom || 1) + delta)));
document.getElementById('lh-zoom-lbl').textContent = Math.round(_wb._zoom * 100) + '%';
}
function lhZoomFit() { if (_wb) _wb.zoomFitStrokes(); }
function exportBoardPage() { if (_wb) _wb.exportPNG(); }
/* ─── Chat ─── */
async function loadChat() {
_chatLoaded = true;
const sessionId = _activeSession.session.id;
const container = document.getElementById('lh-chat-list');
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--ml-text-3)">Загрузка...</div>';
try {
const data = await LS.crGetChat(sessionId);
const messages = data.messages || [];
if (!messages.length) {
container.innerHTML = '<div class="ml-empty"><p>Чат был пуст</p></div>';
return;
}
container.innerHTML = messages.map(m => renderMsg(m)).join('');
container.scrollTop = container.scrollHeight;
} catch {
container.innerHTML = '<div class="ml-empty"><p>Ошибка загрузки чата</p></div>';
}
}
function renderMsg(m) {
const time = m.created_at ? new Date(m.created_at).toLocaleTimeString('ru-RU', { hour:'2-digit', minute:'2-digit' }) : '';
const color = strToColor(m.user_name || '');
const ini = (m.user_name || '?')[0].toUpperCase();
const isMine = m.user_id === _me?.id;
let body = '';
if (m.message) body += `<div class="lh-msg-text">${LS.escapeHtml(m.message)}</div>`;
if (m.attachment_url) body += `<img class="lh-msg-img" src="${LS.escapeHtml(m.attachment_url)}" alt="вложение" loading="lazy"/>`;
return `<div class="lh-msg${m.is_pinned ? ' pinned' : ''}${isMine ? ' mine' : ''}">
<div class="lh-msg-avatar" style="background:${color}">${ini}</div>
<div class="lh-msg-body">
<div class="lh-msg-head">
<span class="lh-msg-name">${LS.escapeHtml(m.user_name || '?')}${isMine ? ' <span style="font-size:10px;color:var(--ml-text-3);font-weight:500">(вы)</span>' : ''}</span>
<span class="lh-msg-time">${time}</span>
</div>
${body}
</div>
</div>`;
}
/* ─── Notes ─── */
async function loadNotes() {
_notesLoaded = true;
const sessionId = _activeSession.session.id;
const container = document.getElementById('lh-notes-list');
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--ml-text-3)">Загрузка...</div>';
try {
const data = await LS.get(`/api/classroom/${sessionId}/notes`);
const content = data.content || '';
if (!content.trim()) {
container.innerHTML = `<div class="ml-empty"><p>Вы не оставили заметок к этому уроку</p></div>`;
return;
}
container.innerHTML = `<div class="lh-own-note-box">
<div class="lh-own-note-lbl">Мои заметки</div>
<div class="lh-note-text">${LS.escapeHtml(content)}</div>
</div>`;
} catch {
container.innerHTML = `<div class="ml-empty"><p>Ошибка загрузки заметок</p></div>`;
}
}
/* ─── Helpers ─── */
function fmtDuration(sec) {
if (!sec || sec < 0) return '0 мин';
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
if (h > 0) return `${h}ч ${m}м`;
if (m > 0) return `${m} мин`;
return `${sec} сек`;
}
function plural(n, one, few, many) {
const mod10 = n % 10, mod100 = n % 100;
if (mod10 === 1 && mod100 !== 11) return `${n} ${one}`;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} ${few}`;
return `${n} ${many}`;
}
function strToColor(str) {
const colors = ['#9B5DE5','#06D6E0','#FF6B6B','#4361EE','#A8E063','#F15BB5','#FF9F43','#06D6A0'];
let h = 0;
for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) & 0xffff;
return colors[h % colors.length];
}
window.addEventListener('resize', () => {
if (window.innerWidth > 768) document.getElementById('ml-list-panel').classList.remove('hidden-mobile');
});
document.addEventListener('click', e => {
if (!_pagesPopupOpen) return;
const popup = document.getElementById('lh-pages-popup');
const btn = document.getElementById('lh-page-indicator');
if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) lhClosePages();
});
document.addEventListener('keydown', e => {
if (!_activeSession || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const boardVisible = document.getElementById('tc-board')?.style.display !== 'none';
if (!boardVisible) return;
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); lhPageNav(-1); }
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); lhPageNav(1); }
if (e.key === '0') lhZoomFit();
});
</script>
</body>
</html>