edb4c211a0
- 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>
1135 lines
56 KiB
HTML
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">—</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">—</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">—</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">—</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">—</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>
|