Initial commit
This commit is contained in:
729
templates/index.html
Normal file
729
templates/index.html
Normal file
@@ -0,0 +1,729 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Расписание преподавателей КС ПГУТИ</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@300;400;600;700&family=Onest:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d0f14;
|
||||
--surface: #13161e;
|
||||
--surface2: #1a1e28;
|
||||
--border: #252a38;
|
||||
--accent: #4f8ef7;
|
||||
--accent-glow: rgba(79,142,247,0.15);
|
||||
--text: #e8eaf0;
|
||||
--text-muted: #6b7280;
|
||||
--text-dim: #9ca3af;
|
||||
--success: #34d399;
|
||||
--r: 14px;
|
||||
--font-d: 'Unbounded', sans-serif;
|
||||
--font-b: 'Onest', sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-b);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed; top: -20%; left: -10%;
|
||||
width: 70vw; height: 70vw;
|
||||
background: radial-gradient(circle, rgba(79,142,247,0.07) 0%, transparent 70%);
|
||||
pointer-events: none; z-index: 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 1400px; margin: 0 auto;
|
||||
padding: 0 16px 80px;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
padding: 24px 0 20px;
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header-left h1 {
|
||||
font-family: var(--font-d);
|
||||
font-size: clamp(1rem, 4vw, 1.4rem);
|
||||
font-weight: 600; letter-spacing: -0.02em; line-height: 1.2;
|
||||
}
|
||||
.header-left h1 span { color: var(--accent); }
|
||||
.header-left .subtitle {
|
||||
font-size: 0.78rem; color: var(--text-muted);
|
||||
margin-top: 5px; font-weight: 300;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
max-width: 55vw;
|
||||
}
|
||||
.live-badge {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 100px; padding: 6px 12px;
|
||||
font-size: 0.7rem; color: var(--text-dim);
|
||||
white-space: nowrap; flex-shrink: 0; margin-top: 2px;
|
||||
}
|
||||
.live-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--success); box-shadow: 0 0 6px var(--success);
|
||||
animation: pulse 2s infinite; flex-shrink: 0;
|
||||
}
|
||||
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
|
||||
|
||||
/* ── Teacher search ── */
|
||||
.teachers-section { margin-bottom: 20px; }
|
||||
.teachers-label {
|
||||
font-size: 0.65rem; font-weight: 600; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--text-muted);
|
||||
margin-bottom: 8px; font-family: var(--font-d);
|
||||
}
|
||||
|
||||
/* Поле поиска */
|
||||
.search-wrap { position: relative; margin-bottom: 8px; }
|
||||
.search-input {
|
||||
width: 100%; background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 10px 14px 10px 38px;
|
||||
font-family: var(--font-b); font-size: 0.85rem; color: var(--text);
|
||||
outline: none; transition: border-color .2s;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.search-input::placeholder { color: var(--text-muted); }
|
||||
.search-input:focus { border-color: var(--accent); }
|
||||
.search-icon {
|
||||
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
|
||||
color: var(--text-muted); pointer-events: none;
|
||||
width: 16px; height: 16px;
|
||||
}
|
||||
.search-clear {
|
||||
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 6px; color: var(--text-muted); cursor: pointer;
|
||||
padding: 2px 6px; font-size: 0.7rem; display: none; line-height: 1.4;
|
||||
transition: all .15s;
|
||||
}
|
||||
.search-clear:hover { color: var(--text); border-color: var(--accent); }
|
||||
|
||||
/* Выпадающий список */
|
||||
.search-dropdown {
|
||||
position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 100;
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.4);
|
||||
max-height: 280px; overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
.search-dropdown.open { display: block; }
|
||||
.search-dropdown::-webkit-scrollbar { width: 4px; }
|
||||
.search-dropdown::-webkit-scrollbar-track { background: var(--surface); }
|
||||
.search-dropdown::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
.dd-item {
|
||||
padding: 10px 14px; font-size: 0.82rem; cursor: pointer;
|
||||
transition: background .12s; border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
}
|
||||
.dd-item:last-child { border-bottom: none; }
|
||||
.dd-item:hover, .dd-item.selected { background: var(--surface2); }
|
||||
.dd-item.selected { color: var(--accent); }
|
||||
.dd-item-name { flex: 1; min-width: 0; }
|
||||
.dd-item-name mark {
|
||||
background: rgba(79,142,247,.25); color: var(--accent);
|
||||
border-radius: 2px; padding: 0 1px;
|
||||
}
|
||||
.dd-item-id {
|
||||
font-family: var(--font-d); font-size: 0.6rem; color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dd-empty {
|
||||
padding: 20px 14px; text-align: center;
|
||||
color: var(--text-muted); font-size: 0.82rem;
|
||||
}
|
||||
.dd-loading {
|
||||
padding: 16px 14px; text-align: center;
|
||||
color: var(--text-muted); font-size: 0.8rem;
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
}
|
||||
|
||||
/* Таб избранных из конфига */
|
||||
.fav-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.teacher-tab {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 7px 14px;
|
||||
font-family: var(--font-b); font-size: 0.78rem; font-weight: 500;
|
||||
color: var(--text-dim); cursor: pointer;
|
||||
transition: all 0.18s; white-space: nowrap;
|
||||
min-height: 34px; display: flex; align-items: center;
|
||||
}
|
||||
.teacher-tab:hover { background: var(--surface2); border-color: rgba(79,142,247,0.4); color: var(--text); }
|
||||
.teacher-tab.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); font-weight: 600; }
|
||||
@keyframes shimmer { 0%,100%{opacity:.4} 50%{opacity:.8} }
|
||||
.teacher-tab-skeleton {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 10px; height: 34px; width: 120px;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
/* ── Week nav — ключевое изменение ── */
|
||||
.week-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.week-btn {
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 10px 14px;
|
||||
font-family: var(--font-b); font-size: 0.8rem; font-weight: 500;
|
||||
color: var(--text-dim); cursor: pointer; transition: all 0.2s;
|
||||
min-height: 42px; width: 100%;
|
||||
}
|
||||
.week-btn:hover:not(:disabled) { background: var(--surface2); border-color: var(--accent); color: var(--accent); }
|
||||
.week-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.week-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
|
||||
/* Скрыть текст кнопок на узких экранах, оставить только стрелку */
|
||||
.week-btn-text { display: inline; }
|
||||
.week-label {
|
||||
font-family: var(--font-d); font-size: clamp(0.6rem, 2.5vw, 0.8rem);
|
||||
font-weight: 400; color: var(--text);
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 10px 10px;
|
||||
text-align: center; line-height: 1.3;
|
||||
min-height: 42px; display: flex; align-items: center; justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.week-btn-text { display: none; }
|
||||
.week-btn { padding: 10px; }
|
||||
}
|
||||
|
||||
/* ── Stats — горизонтальная полоса ── */
|
||||
.stats-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.stats-bar { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
.stat {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 12px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.stat-icon {
|
||||
width: 30px; height: 30px; border-radius: 8px;
|
||||
background: var(--accent-glow);
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.stat-icon svg { width: 14px; height: 14px; color: var(--accent); }
|
||||
.stat-num { font-family: var(--font-d); font-size: 1.1rem; font-weight: 700; line-height: 1; }
|
||||
.stat-label { font-size: 0.65rem; color: var(--text-muted); margin-top: 2px; }
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.stat { flex-direction: column; align-items: flex-start; gap: 6px; padding: 10px; }
|
||||
.stat-icon { width: 26px; height: 26px; }
|
||||
.stat-num { font-size: 1rem; }
|
||||
}
|
||||
|
||||
/* ── Loading ── */
|
||||
.loading {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; padding: 80px 20px; gap: 16px;
|
||||
}
|
||||
.spinner {
|
||||
width: 36px; height: 36px;
|
||||
border: 2px solid var(--border); border-top-color: var(--accent);
|
||||
border-radius: 50%; animation: spin .8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading p { color: var(--text-muted); font-size: 0.85rem; }
|
||||
|
||||
.error-box {
|
||||
background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3);
|
||||
border-radius: var(--r); padding: 20px; text-align: center; color: #f87171;
|
||||
}
|
||||
|
||||
/* ── Days grid ── */
|
||||
.days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 700px) { .days-grid { grid-template-columns: 1fr; gap: 10px; } }
|
||||
|
||||
/* ── Day card ── */
|
||||
.day-card {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: var(--r); overflow: hidden;
|
||||
transition: border-color .2s;
|
||||
animation: fadeUp .35s ease both;
|
||||
}
|
||||
.day-card.has-classes { border-color: rgba(79,142,247,.2); }
|
||||
.day-card.today {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent), 0 4px 20px rgba(79,142,247,.12);
|
||||
}
|
||||
/* hover только на не-тач устройствах */
|
||||
@media (hover: hover) {
|
||||
.day-card:hover { border-color: rgba(79,142,247,.35); transform: translateY(-1px); }
|
||||
}
|
||||
@keyframes fadeUp { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
|
||||
|
||||
.day-header {
|
||||
padding: 12px 14px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border); background: var(--surface2);
|
||||
gap: 8px;
|
||||
}
|
||||
.day-name {
|
||||
font-family: var(--font-d); font-size: 0.72rem; font-weight: 600;
|
||||
letter-spacing: .04em; text-transform: uppercase; line-height: 1;
|
||||
}
|
||||
.today .day-name { color: var(--accent); }
|
||||
.day-date { font-size: 0.68rem; color: var(--text-muted); margin-top: 3px; }
|
||||
.today-tag {
|
||||
background: var(--accent); color: #fff;
|
||||
font-size: 0.58rem; font-weight: 600; font-family: var(--font-d);
|
||||
letter-spacing: .04em; padding: 3px 7px; border-radius: 5px;
|
||||
text-transform: uppercase; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Lesson row ── */
|
||||
.lesson {
|
||||
display: grid;
|
||||
/* номер | время | инфо | аудитория */
|
||||
grid-template-columns: 28px 1fr auto;
|
||||
align-items: start;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background .15s;
|
||||
padding: 10px 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
.lesson:last-child { border-bottom: none; }
|
||||
@media (hover: hover) { .lesson:hover { background: var(--surface2); } }
|
||||
.lesson.active-lesson { background: rgba(79,142,247,.07); }
|
||||
|
||||
.lesson-num {
|
||||
font-family: var(--font-d); font-size: 0.65rem; font-weight: 600;
|
||||
color: var(--text-muted); padding-top: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
.active-lesson .lesson-num { color: var(--accent); }
|
||||
|
||||
.lesson-info { min-width: 0; }
|
||||
|
||||
.lesson-time {
|
||||
font-size: 0.68rem; color: var(--text-muted);
|
||||
margin-bottom: 4px; white-space: nowrap;
|
||||
}
|
||||
.lesson-subject {
|
||||
font-size: 0.78rem; font-weight: 600; line-height: 1.35;
|
||||
margin-bottom: 5px; word-break: break-word;
|
||||
}
|
||||
.lesson-group { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
|
||||
.group-badge {
|
||||
background: rgba(79,142,247,.12); border: 1px solid rgba(79,142,247,.25);
|
||||
color: var(--accent); border-radius: 5px; padding: 2px 7px;
|
||||
font-size: 0.65rem; font-weight: 600; font-family: var(--font-d);
|
||||
}
|
||||
.type-badge {
|
||||
background: rgba(124,58,237,.1); border: 1px solid rgba(124,58,237,.2);
|
||||
color: #a78bfa; border-radius: 5px; padding: 2px 7px;
|
||||
font-size: 0.63rem; font-weight: 500;
|
||||
}
|
||||
.lesson-location {
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
font-size: 0.66rem; color: var(--accent); margin-top: 5px;
|
||||
}
|
||||
.lesson-location svg { width: 9px; height: 9px; flex-shrink: 0; }
|
||||
|
||||
.lesson-room {
|
||||
display: flex; align-items: flex-start; justify-content: flex-end;
|
||||
padding-top: 2px; flex-shrink: 0;
|
||||
}
|
||||
.room-badge {
|
||||
background: var(--surface2); border: 1px solid var(--border);
|
||||
border-radius: 7px; padding: 3px 7px;
|
||||
font-family: var(--font-d); font-size: 0.63rem; font-weight: 600; color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-day {
|
||||
padding: 24px 16px; text-align: center;
|
||||
color: var(--text-muted); font-size: 0.78rem;
|
||||
}
|
||||
.empty-day-icon { font-size: 1.4rem; margin-bottom: 6px; opacity: .4; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<h1>Расписание <span>преподавателя</span></h1>
|
||||
<p class="subtitle" id="teacher-full-name">Загрузка...</p>
|
||||
</div>
|
||||
<div class="live-badge">
|
||||
<span class="live-dot"></span>
|
||||
<span id="last-updated">...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="teachers-section" id="teachers-section">
|
||||
<div class="teachers-label">Преподаватель</div>
|
||||
|
||||
<!-- Поиск по всем преподавателям -->
|
||||
<div class="search-wrap" id="search-wrap">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input class="search-input" id="search-input" type="text"
|
||||
placeholder="Поиск по ФИО..." autocomplete="off" spellcheck="false">
|
||||
<button class="search-clear" id="search-clear" onclick="clearSearch()">✕</button>
|
||||
<div class="search-dropdown" id="search-dropdown">
|
||||
<div class="dd-loading">
|
||||
<div class="spinner" style="width:16px;height:16px;border-width:2px"></div>
|
||||
Загружаем список...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Избранные из config.yaml (если больше одного) -->
|
||||
<div class="fav-tabs" id="teachers-tabs" style="margin-top:10px">
|
||||
<div class="teacher-tab-skeleton"></div>
|
||||
<div class="teacher-tab-skeleton" style="width:90px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="week-nav">
|
||||
<button class="week-btn" id="prev-btn" disabled onclick="navigate('prev')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15,18 9,12 15,6"/></svg>
|
||||
<span class="week-btn-text">Предыдущая</span>
|
||||
</button>
|
||||
<div class="week-label" id="week-label">—</div>
|
||||
<button class="week-btn" id="next-btn" disabled onclick="navigate('next')">
|
||||
<span class="week-btn-text">Следующая</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9,18 15,12 9,6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar" id="stats-bar" style="display:none">
|
||||
<div class="stat">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="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>
|
||||
</div>
|
||||
<div><div class="stat-num" id="stat-days">0</div><div class="stat-label">дней</div></div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div><div class="stat-num" id="stat-lessons">0</div><div class="stat-label">пар</div></div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div><div class="stat-num" id="stat-groups">—</div><div class="stat-label">групп</div></div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div><div class="stat-num" id="stat-hours">—</div><div class="stat-label">часов</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="schedule-container">
|
||||
<div class="loading"><div class="spinner"></div><p>Загружаем расписание...</p></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
let currentObj = null;
|
||||
let prevWk = null, nextWk = null;
|
||||
const todayStr = getTodayStr();
|
||||
|
||||
function getTodayStr() {
|
||||
const d = new Date();
|
||||
return [String(d.getDate()).padStart(2,'0'), String(d.getMonth()+1).padStart(2,'0'), d.getFullYear()].join('.');
|
||||
}
|
||||
|
||||
function nowMin() { const d = new Date(); return d.getHours()*60+d.getMinutes(); }
|
||||
|
||||
function isActive(t) {
|
||||
const m = t.match(/(\d{2}):(\d{2})\s*[–-]\s*(\d{2}):(\d{2})/);
|
||||
if (!m) return false;
|
||||
const n = nowMin();
|
||||
return n >= +m[1]*60 + +m[2] && n <= +m[3]*60 + +m[4];
|
||||
}
|
||||
|
||||
let allTeachers = []; // полный список с сайта
|
||||
|
||||
async function loadTeachers() {
|
||||
try {
|
||||
// Грузим избранных из конфига
|
||||
const r = await fetch('/api/teachers');
|
||||
const data = await r.json();
|
||||
renderFavTabs(data.teachers, data.default);
|
||||
currentObj = String(data.default);
|
||||
loadSchedule(currentObj, null);
|
||||
} catch(e) {
|
||||
document.getElementById('teachers-tabs').innerHTML =
|
||||
`<span style="color:var(--text-muted);font-size:.8rem">Ошибка: ${e.message}</span>`;
|
||||
}
|
||||
// Фоном грузим полный список для поиска
|
||||
loadAllTeachers();
|
||||
}
|
||||
|
||||
async function loadAllTeachers() {
|
||||
try {
|
||||
const r = await fetch('/api/all-teachers');
|
||||
const data = await r.json();
|
||||
allTeachers = data.teachers;
|
||||
} catch(e) {
|
||||
allTeachers = [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderFavTabs(teachers, defaultId) {
|
||||
const tabs = document.getElementById('teachers-tabs');
|
||||
if (teachers.length <= 1) {
|
||||
tabs.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
tabs.innerHTML = teachers.map(t => `
|
||||
<button class="teacher-tab${String(t.id)===String(defaultId)?' active':''}"
|
||||
data-id="${t.id}" title="${t.name}"
|
||||
onclick="switchTeacher('${t.id}',this)">${t.short}</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function switchTeacher(id, btn) {
|
||||
if (String(id) === currentObj) return;
|
||||
// Снять активный у табов
|
||||
document.querySelectorAll('.teacher-tab').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
currentObj = String(id);
|
||||
prevWk = null; nextWk = null;
|
||||
loadSchedule(id, null);
|
||||
}
|
||||
|
||||
// ── Поиск ────────────────────────────────────────────────────────────────
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchClear = document.getElementById('search-clear');
|
||||
const dropdown = document.getElementById('search-dropdown');
|
||||
let ddSelected = -1;
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
const q = searchInput.value.trim();
|
||||
searchClear.style.display = q ? 'block' : 'none';
|
||||
if (!q) { closeDropdown(); return; }
|
||||
renderDropdown(q);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('focus', () => {
|
||||
if (searchInput.value.trim()) renderDropdown(searchInput.value.trim());
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keydown', e => {
|
||||
const items = dropdown.querySelectorAll('.dd-item');
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
ddSelected = Math.min(ddSelected + 1, items.length - 1);
|
||||
highlightDD(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
ddSelected = Math.max(ddSelected - 1, 0);
|
||||
highlightDD(items);
|
||||
} else if (e.key === 'Enter' && ddSelected >= 0 && items[ddSelected]) {
|
||||
items[ddSelected].click();
|
||||
} else if (e.key === 'Escape') {
|
||||
closeDropdown();
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (!document.getElementById('search-wrap').contains(e.target)) closeDropdown();
|
||||
});
|
||||
|
||||
function highlightDD(items) {
|
||||
items.forEach((el, i) => el.classList.toggle('selected', i === ddSelected));
|
||||
if (items[ddSelected]) items[ddSelected].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function renderDropdown(q) {
|
||||
if (!allTeachers.length) {
|
||||
dropdown.innerHTML = '<div class="dd-loading"><div class="spinner" style="width:14px;height:14px;border-width:2px"></div>Загружаем список...</div>';
|
||||
dropdown.classList.add('open');
|
||||
// Попробуем ещё раз загрузить
|
||||
loadAllTeachers().then(() => { if (searchInput.value.trim()) renderDropdown(searchInput.value.trim()); });
|
||||
return;
|
||||
}
|
||||
|
||||
const norm = q.toLowerCase();
|
||||
const filtered = allTeachers.filter(t => t.name.toLowerCase().includes(norm)).slice(0, 30);
|
||||
ddSelected = -1;
|
||||
|
||||
if (!filtered.length) {
|
||||
dropdown.innerHTML = `<div class="dd-empty">Не найдено: «${q}»</div>`;
|
||||
} else {
|
||||
dropdown.innerHTML = filtered.map(t => {
|
||||
const hi = t.name.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>');
|
||||
return `<div class="dd-item${String(t.id)===currentObj?' selected':''}" onclick="selectTeacher('${t.id}','${t.name.replace(/'/g,"\'")}')">
|
||||
<span class="dd-item-name">${hi}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
dropdown.classList.add('open');
|
||||
}
|
||||
|
||||
function selectTeacher(id, name) {
|
||||
closeDropdown();
|
||||
searchInput.value = name;
|
||||
searchClear.style.display = 'block';
|
||||
// Снять активный у табов
|
||||
document.querySelectorAll('.teacher-tab').forEach(b => b.classList.remove('active'));
|
||||
switchTeacher(id, null);
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchInput.value = '';
|
||||
searchClear.style.display = 'none';
|
||||
closeDropdown();
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdown.classList.remove('open');
|
||||
ddSelected = -1;
|
||||
}
|
||||
|
||||
async function loadSchedule(obj, wk) {
|
||||
document.getElementById('schedule-container').innerHTML =
|
||||
`<div class="loading"><div class="spinner"></div><p>Загружаем расписание...</p></div>`;
|
||||
document.getElementById('stats-bar').style.display = 'none';
|
||||
const p = new URLSearchParams({ obj });
|
||||
if (wk) p.set('wk', wk);
|
||||
try {
|
||||
const r = await fetch('/api/schedule?' + p);
|
||||
render(await r.json());
|
||||
} catch(e) {
|
||||
document.getElementById('schedule-container').innerHTML =
|
||||
`<div class="error-box">⚠️ ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
if (data.error && !data.days?.length) {
|
||||
document.getElementById('schedule-container').innerHTML = `<div class="error-box">⚠️ ${data.error}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
prevWk = data.prev_wk; nextWk = data.next_wk;
|
||||
document.getElementById('prev-btn').disabled = !prevWk;
|
||||
document.getElementById('next-btn').disabled = !nextWk;
|
||||
document.getElementById('week-label').textContent = data.week_range || '—';
|
||||
if (data.teacher_name) document.getElementById('teacher-full-name').textContent = data.teacher_name;
|
||||
|
||||
const activeDays = data.days.filter(d => d.has_classes).length;
|
||||
const totalLessons = data.days.reduce((a,d) => a + d.lessons.filter(l=>l.has_class).length, 0);
|
||||
const groups = new Set(data.days.flatMap(d=>d.lessons).filter(l=>l.group_short).map(l=>l.group_short));
|
||||
document.getElementById('stat-days').textContent = activeDays;
|
||||
document.getElementById('stat-lessons').textContent = totalLessons;
|
||||
document.getElementById('stat-groups').textContent = groups.size || '—';
|
||||
// 1 пара = 1.5 акад. часа (90 мин), но по условию 1 пара = 2 часа
|
||||
document.getElementById('stat-hours').textContent = totalLessons * 2;
|
||||
document.getElementById('stats-bar').style.display = 'grid';
|
||||
|
||||
const now = new Date();
|
||||
document.getElementById('last-updated').textContent =
|
||||
`${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}`;
|
||||
|
||||
if (!data.days.length) {
|
||||
document.getElementById('schedule-container').innerHTML =
|
||||
`<div class="error-box" style="background:rgba(79,142,247,.07);border-color:var(--border);color:var(--text-muted)">Расписание не найдено</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const pin = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="9" height="9"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>`;
|
||||
|
||||
let html = '<div class="days-grid">';
|
||||
data.days.forEach((day, i) => {
|
||||
const isToday = day.date === todayStr;
|
||||
const cls = ['day-card', day.has_classes?'has-classes':'', isToday?'today':''].filter(Boolean).join(' ');
|
||||
html += `<div class="${cls}" style="animation-delay:${i*.04}s">
|
||||
<div class="day-header">
|
||||
<div>
|
||||
<div class="day-name">${day.name}</div>
|
||||
<div class="day-date">${day.date}</div>
|
||||
</div>
|
||||
${isToday ? '<span class="today-tag">Сегодня</span>' : ''}
|
||||
</div>`;
|
||||
|
||||
if (!day.has_classes) {
|
||||
html += `<div class="empty-day"><div class="empty-day-icon">☕</div>Выходной / нет пар</div>`;
|
||||
} else {
|
||||
day.lessons.forEach(l => {
|
||||
if (!l.has_class) return;
|
||||
const active = isToday && isActive(l.time);
|
||||
html += `<div class="lesson${active?' active-lesson':''}">
|
||||
<div class="lesson-num">${l.num}</div>
|
||||
<div class="lesson-info">
|
||||
<div class="lesson-time">${l.time}</div>
|
||||
<div class="lesson-subject">${l.subject}</div>
|
||||
<div class="lesson-group">
|
||||
${l.group_short ? `<span class="group-badge">${l.group_short}</span>` : ''}
|
||||
${l.lesson_type ? `<span class="type-badge">${l.lesson_type}</span>` : ''}
|
||||
</div>
|
||||
${l.location ? `<div class="lesson-location">${pin}${l.location}</div>` : ''}
|
||||
</div>
|
||||
<div class="lesson-room">${l.room ? `<div class="room-badge">${l.room}</div>` : ''}</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
document.getElementById('schedule-container').innerHTML = html;
|
||||
}
|
||||
|
||||
function navigate(dir) {
|
||||
const wk = dir === 'prev' ? prevWk : nextWk;
|
||||
if (wk) loadSchedule(currentObj, wk);
|
||||
}
|
||||
|
||||
loadTeachers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user