galikon/index.html

553 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Галикон — приложение пловца</title>
<link rel="manifest" href="data:application/json,{}">
<meta name="apple-mobile-web-app-capable" content="yes">
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--red:#FF6B6B;--green:#4CAF50}
*{box-sizing:border-box;margin:0;padding:0}
body{font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100);-webkit-tap-highlight-color:transparent}
header{background:var(--ink);color:var(--white);padding:16px 20px;position:sticky;top:0;z-index:100}
header h1{font-size:22px;font-weight:800}
header h1 span{color:var(--cyan)}
nav.tabs{display:flex;overflow-x:auto;gap:4px;padding:8px 20px;background:var(--ink);position:sticky;top:54px;z-index:99;-webkit-overflow-scrolling:touch}
nav.tabs::-webkit-scrollbar{display:none}
.tab-btn{flex-shrink:0;padding:8px 16px;border-radius:20px;border:none;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap;background:#1a2332;color:#9aa3b2;transition:all .2s}
.tab-btn.active{background:var(--cyan);color:var(--ink)}
.tab-btn:active{transform:scale(.95)}
main{padding:20px;max-width:900px;margin:0 auto}
.tab-content{display:none}
.tab-content.active{display:block}
.card{background:var(--white);border-radius:16px;padding:24px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,.06)}
.card h3{font-size:18px;font-weight:700;margin-bottom:12px}
.card h3 .emoji{margin-right:8px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.stat-box{text-align:center;padding:16px 8px;background:var(--gray-100);border-radius:12px}
.stat-box .num{font-size:28px;font-weight:800;color:var(--cyan);line-height:1}
.stat-box .lbl{font-size:11px;color:var(--gray-500);margin-top:4px}
input,select,textarea{width:100%;padding:12px;border:2px solid var(--gray-100);border-radius:10px;font:inherit;font-size:15px;margin-bottom:10px;background:var(--white);transition:border-color .2s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--cyan)}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:10px 22px;border-radius:10px;font-weight:700;border:none;font-size:15px;cursor:pointer;transition:transform .2s}
.btn:active{transform:scale(.96)}
.btn.danger{background:var(--red);color:#fff}
.btn.small{padding:6px 14px;font-size:13px}
.check-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--gray-100)}
.check-row:last-child{border-bottom:none}
.check-row label{flex:1;font-size:15px;cursor:pointer}
.check-row input[type=checkbox]{width:20px;height:20px;margin:0;accent-color:var(--cyan)}
table{width:100%;border-collapse:collapse;font-size:14px}
th,td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--gray-100)}
th{color:var(--gray-500);font-weight:600;font-size:12px;text-transform:uppercase}
td .badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600}
.badge.gold{background:#FFF3CD;color:#856404}
.badge.blue{background:var(--cyan-50);color:var(--ink)}
.badge.green{background:#D4EDDA;color:#155724}
.video-preview{width:100%;border-radius:12px;max-height:300px;object-fit:contain;background:#000}
.photo-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px}
.photo-grid img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:12px;cursor:pointer}
.photo-grid img:active{opacity:.8}
label.file-btn{display:inline-block;background:var(--gray-100);padding:12px 20px;border-radius:10px;font-weight:600;cursor:pointer;font-size:14px;margin-bottom:10px}
label.file-btn:active{background:var(--cyan-50)}
input[type=file]{display:none}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--cyan);padding:12px 24px;border-radius:30px;font-weight:700;font-size:14px;z-index:999;opacity:0;transition:opacity .3s;pointer-events:none}
.toast.show{opacity:1}
.progress-bar{height:8px;background:var(--gray-100);border-radius:4px;overflow:hidden;margin-top:8px}
.progress-bar .fill{height:100%;background:var(--cyan);border-radius:4px;transition:width .5s}
.empty{text-align:center;padding:40px 20px;color:var(--gray-500)}
.empty .big{font-size:48px;margin-bottom:12px}
.lightbox{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.92);z-index:200;display:flex;align-items:center;justify-content:center;cursor:pointer}
.lightbox img,.lightbox video{max-width:95%;max-height:90vh;border-radius:8px}
@media(max-width:480px){
.grid2,.grid3{grid-template-columns:1fr}
header h1{font-size:18px}
nav.tabs{gap:2px;padding:8px 12px}
.tab-btn{padding:7px 12px;font-size:13px}
main{padding:12px}
}
</style>
</head>
<body>
<header><h1>&#x1F3CA; <span>Галикон</span></h1></header>
<nav class="tabs">
<button class="tab-btn active" data-tab="diary">&#x1F4D6; Дневник</button>
<button class="tab-btn" data-tab="health">&#x2764; Здоровье</button>
<button class="tab-btn" data-tab="norms">&#x1F4CA; Нормативы</button>
<button class="tab-btn" data-tab="video">&#x1F3AC; Видео</button>
<button class="tab-btn" data-tab="photos">&#x1F4F7; Фото</button>
<button class="tab-btn" data-tab="grades">&#x1F393; Оценки</button>
<button class="tab-btn" data-tab="lessons">&#x1F4DA; Уроки</button>
</nav>
<main>
<!-- ДНЕВНИК -->
<div class="tab-content active" id="diary">
<div class="card">
<h3><span class="emoji">&#x270F;</span>Новая запись</h3>
<input type="date" id="diary-date">
<select id="diary-type"><option value="">Тип тренировки</option><option>Скорость</option><option>Техника</option><option>Выносливость</option><option>ОФП</option><option>Соревнование</option></select>
<input type="number" id="diary-km" placeholder="Километраж (км)" step="0.1" min="0">
<input type="text" id="diary-time" placeholder="Лучшее время (например: 33.8)">
<input type="number" id="diary-feel" placeholder="Самочувствие (1-5)" min="1" max="5">
<input type="text" id="diary-note" placeholder="Заметка (что получилось, над чем работать)">
<button class="btn" onclick="addDiary()" style="width:100%">&#x2705; Сохранить</button>
</div>
<div class="card">
<h3><span class="emoji">&#x1F4C8;</span>История</h3>
<div id="diary-list" class="empty"><div class="big">&#x1F4AD;</div>Пока нет записей. Добавь первую!</div>
</div>
</div>
<!-- ЗДОРОВЬЕ -->
<div class="tab-content" id="health">
<div class="card">
<h3><span class="emoji">&#x1F48A;</span>Витамины на сегодня</h3>
<div id="vitamins-check"></div>
</div>
<div class="card">
<h3><span class="emoji">&#x1F4A4;</span>Сон и самочувствие</h3>
<div class="grid2">
<input type="number" id="sleep-hours" placeholder="Часов сна" min="0" max="24" step="0.5">
<input type="number" id="morning-pulse" placeholder="Пульс утром" min="30" max="150">
</div>
<button class="btn" onclick="saveSleep()" style="width:100%">Сохранить</button>
<div style="margin-top:12px;font-size:13px;color:var(--gray-500)" id="sleep-stats"></div>
</div>
<div class="card">
<h3><span class="emoji">&#x1F4C5;</span>Анализы</h3>
<select id="test-select"><option value="">Выбери анализ</option><option value="hb">Гемоглобин (г/л)</option><option value="ferritin">Ферритин (мкг/л)</option><option value="vitd">Витамин D (нг/мл)</option><option value="b12">Витамин B12 (пг/мл)</option></select>
<input type="number" id="test-value" placeholder="Значение">
<input type="date" id="test-date">
<button class="btn" onclick="saveTest()" style="width:100%">Сохранить анализ</button>
<div id="test-history" style="margin-top:8px;font-size:13px;color:var(--gray-500)"></div>
</div>
</div>
<!-- НОРМАТИВЫ -->
<div class="tab-content" id="norms">
<div class="card">
<h3><span class="emoji">&#x1F3CA;</span>Разряды по плаванию — 50 м в/с (мужчины)</h3>
<table>
<thead><tr><th>Разряд</th><th>50 м в/с</th><th>100 м в/с</th><th>400 м в/с</th></tr></thead>
<tbody>
<tr><td><span class="badge gold">МСМК</span></td><td>21.50″</td><td>47.00″</td><td>3:48.00</td></tr>
<tr><td><span class="badge blue">МС</span></td><td>23.00″</td><td>50.50″</td><td>4:05.00</td></tr>
<tr><td><span class="badge green">КМС</span></td><td>25.00″</td><td>54.50″</td><td>4:25.00</td></tr>
<tr><td>1 взр.</td><td>28.00″</td><td>1:01.00</td><td>5:00.00</td></tr>
<tr><td>2 взр.</td><td>31.50″</td><td>1:09.00</td><td>5:40.00</td></tr>
<tr><td>3 взр.</td><td>35.00″</td><td>1:17.50</td><td>6:20.00</td></tr>
<tr><td>1 юн.</td><td>40.00″</td><td>1:29.00</td><td>7:10.00</td></tr>
<tr><td>2 юн.</td><td>47.00″</td><td>1:45.00</td><td>8:10.00</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3><span class="emoji">&#x1F3AF;</span>Твой прогресс: 34″ → 23″</h3>
<div style="margin-bottom:8px;display:flex;justify-content:space-between;font-size:13px"><span>3 взр. (35″)</span><span>ТЫ (34″)</span><span>КМС (25″)</span><span>МС (23″)</span></div>
<div class="progress-bar"><div class="fill" style="width:8%"></div></div>
<div style="margin-top:8px;font-size:13px;color:var(--gray-500)">Ты уже перешагнул 3 взрослый! До КМС — 9 секунд.</div>
</div>
</div>
<!-- ВИДЕОАНАЛИЗ -->
<div class="tab-content" id="video">
<div class="card">
<h3><span class="emoji">&#x1F4F9;</span>Загрузи видео заплыва</h3>
<p style="color:var(--gray-500);font-size:14px;margin-bottom:12px">Сними свой заплыв на телефон сбоку от бортика. Загрузи сюда — смотри в повторе и анализируй технику.</p>
<label class="file-btn">&#x1F4C1; Выбрать видео<input type="file" accept="video/*" onchange="uploadVideo(this)"></label>
<div id="video-container"></div>
</div>
<div class="card">
<h3><span class="emoji">&#x1F3AC;</span>Мои видео</h3>
<div id="video-list" class="empty"><div class="big">&#x1F3A5;</div>Загрузи первое видео заплыва!</div>
</div>
</div>
<!-- ФОТО -->
<div class="tab-content" id="photos">
<div class="card">
<h3><span class="emoji">&#x1F4F8;</span>Добавить фото</h3>
<label class="file-btn">&#x1F4C1; Выбрать фото<input type="file" accept="image/*" multiple onchange="uploadPhotos(this)"></label>
</div>
<div class="card">
<h3><span class="emoji">&#x1F5BC;</span>Галерея</h3>
<div class="photo-grid" id="photo-grid"><div class="empty" style="grid-column:1/-1"><div class="big">&#x1F4F7;</div>Добавь первые фото!</div></div>
</div>
</div>
<!-- ОЦЕНКИ -->
<div class="tab-content" id="grades">
<div class="card">
<h3><span class="emoji">&#x1F4DD;</span>Добавить оценку</h3>
<div class="grid2">
<input type="text" id="grade-subject" placeholder="Предмет">
<select id="grade-value"><option value="5">5</option><option value="4">4</option><option value="3">3</option><option value="2">2</option></select>
</div>
<button class="btn" onclick="addGrade()" style="width:100%">Добавить</button>
</div>
<div class="card">
<h3><span class="emoji">&#x1F4CA;</span>Успеваемость</h3>
<div class="grid2" id="grades-summary" style="margin-bottom:12px">
<div class="stat-box"><div class="num" id="gpa-display"></div><div class="lbl">Средний балл</div></div>
<div class="stat-box"><div class="num" id="grades-count">0</div><div class="lbl">Оценок</div></div>
</div>
<div id="grades-list" class="empty"><div class="big">&#x1F393;</div>Добавь оценки по предметам</div>
</div>
</div>
<!-- ВИДЕОУРОКИ -->
<div class="tab-content" id="lessons">
<div class="card">
<h3><span class="emoji">&#x1F4FA;</span>Обучающие видео</h3>
<p style="color:var(--gray-500);font-size:14px;margin-bottom:12px">Открой YouTube и вбей эти запросы — там лучшие уроки по технике плавания:</p>
<div id="lesson-list"></div>
</div>
</div>
</main>
<div class="toast" id="toast">Сохранено!</div>
<div class="lightbox" id="lightbox" style="display:none" onclick="this.style.display='none'"></div>
<script>
const LS = key => {
try { const v = localStorage.getItem('galikon_'+key); return v ? JSON.parse(v) : null }
catch { return null }
};
const SS = (key, val) => {
try { localStorage.setItem('galikon_'+key, JSON.stringify(val)) }
catch { toast('Память браузера заполнена!') }
};
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
// === TABS ===
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.tab).classList.add('active');
if (btn.dataset.tab === 'norms') renderNorms();
if (btn.dataset.tab === 'grades') renderGrades();
});
});
// === DIARY ===
function addDiary() {
const entry = {
date: document.getElementById('diary-date').value || new Date().toISOString().slice(0,10),
type: document.getElementById('diary-type').value || 'Тренировка',
km: document.getElementById('diary-km').value || '0',
time: document.getElementById('diary-time').value || '—',
feel: document.getElementById('diary-feel').value || '—',
note: document.getElementById('diary-note').value || '—',
id: Date.now()
};
if (!entry.date) return toast('Выбери дату!');
const diary = LS('diary') || [];
diary.unshift(entry);
SS('diary', diary.slice(0, 200));
document.getElementById('diary-date').value = '';
document.getElementById('diary-type').value = '';
document.getElementById('diary-km').value = '';
document.getElementById('diary-time').value = '';
document.getElementById('diary-feel').value = '';
document.getElementById('diary-note').value = '';
renderDiary();
toast('Запись добавлена!');
}
function renderDiary() {
const diary = LS('diary') || [];
const el = document.getElementById('diary-list');
if (!diary.length) { el.innerHTML = '<div class="empty"><div class="big">&#x1F4AD;</div>Пока нет записей. Добавь первую!</div>'; return; }
el.innerHTML = diary.slice(0, 30).map(e => `<div class="card" style="margin-bottom:8px;padding:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
<strong>${e.date}</strong> <span class="badge blue">${e.type}</span>
</div>
<div style="font-size:14px;color:var(--gray-500)">
&#x1F4CF; ${e.km} км | &#x23F1; ${e.time} | &#x1F31F; ${e.feel}/5
${e.note !== '—' ? '<br>'+e.note : ''}
</div>
<button class="btn danger small" style="margin-top:6px" onclick="deleteDiary(${e.id})">Удалить</button>
</div>`).join('');
}
function deleteDiary(id) {
const diary = (LS('diary') || []).filter(e => e.id !== id);
SS('diary', diary);
renderDiary();
toast('Удалено');
}
// === HEALTH: VITAMINS ===
const vitamins = [
{ id: 'd3', name: 'Витамин D3', dose: '2000-4000 МЕ', when: 'Утром' },
{ id: 'omega', name: 'Омега-3', dose: '1000 мг', when: 'Утром' },
{ id: 'mg', name: 'Магний', dose: '300 мг', when: 'Перед сном' },
{ id: 'zn', name: 'Цинк', dose: '15 мг', when: 'Утром' },
{ id: 'bcaa', name: 'BCAA', dose: '5 г', when: 'После тренировки' }
];
function renderVitamins() {
const today = new Date().toISOString().slice(0,10);
const taken = LS('vitamins_taken') || {};
const el = document.getElementById('vitamins-check');
el.innerHTML = vitamins.map(v => {
const isTaken = taken[today] && taken[today][v.id];
return `<div class="check-row">
<input type="checkbox" ${isTaken ? 'checked' : ''} onchange="toggleVitamin('${v.id}')" id="vit_${v.id}">
<label for="vit_${v.id}">${v.name} (${v.dose}) — ${v.when}</label>
</div>`;
}).join('') + `<div style="margin-top:8px;font-size:13px;color:var(--gray-500)">Принято: ${countTaken(today)} из ${vitamins.length}</div>`;
}
function countTaken(date) {
const taken = LS('vitamins_taken') || {};
return taken[date] ? Object.values(taken[date]).filter(Boolean).length : 0;
}
function toggleVitamin(id) {
const today = new Date().toISOString().slice(0,10);
const taken = LS('vitamins_taken') || {};
if (!taken[today]) taken[today] = {};
taken[today][id] = !taken[today][id];
SS('vitamins_taken', taken);
renderVitamins();
toast(taken[today][id] ? 'Принято!' : 'Отменено');
}
// === HEALTH: SLEEP ===
function saveSleep() {
const h = document.getElementById('sleep-hours').value;
const p = document.getElementById('morning-pulse').value;
if (!h && !p) return toast('Введи данные!');
const sleep = LS('sleep') || [];
sleep.unshift({ date: new Date().toISOString().slice(0,10), hours: +h || 0, pulse: +p || 0 });
SS('sleep', sleep.slice(0, 90));
document.getElementById('sleep-hours').value = '';
document.getElementById('morning-pulse').value = '';
renderSleep();
toast('Сохранено!');
}
function renderSleep() {
const sleep = LS('sleep') || [];
const el = document.getElementById('sleep-stats');
if (!sleep.length) { el.innerHTML = ''; return; }
const last7 = sleep.slice(0, 7);
const avgH = (last7.reduce((s,e) => s+e.hours, 0) / last7.length).toFixed(1);
const avgP = (last7.reduce((s,e) => s+e.pulse, 0) / last7.length).toFixed(0);
el.innerHTML = `Последние 7 дней: средний сон <strong>${avgH} ч</strong>, средний пульс <strong>${avgP} уд/мин</strong>`;
}
// === HEALTH: TESTS ===
function saveTest() {
const type = document.getElementById('test-select').value;
const val = document.getElementById('test-value').value;
const date = document.getElementById('test-date').value;
if (!type || !val || !date) return toast('Заполни все поля!');
const tests = LS('tests') || {};
if (!tests[type]) tests[type] = [];
tests[type].push({ date, value: +val });
SS('tests', tests);
document.getElementById('test-value').value = '';
document.getElementById('test-date').value = '';
renderTests();
toast('Анализ сохранён!');
}
function renderTests() {
const tests = LS('tests') || {};
const el = document.getElementById('test-history');
const labels = { hb: 'Гемоглобин (норма 120-150)', ferritin: 'Ферритин (норма 30-100)', vitd: 'Витамин D (норма 50-80)', b12: 'B12 (норма 200-900)' };
el.innerHTML = Object.entries(tests).map(([k,vals]) => {
const last = vals.slice(-3).reverse().map(v => `${v.date}: <strong>${v.value}</strong>`).join(' | ');
return `<div style="margin-bottom:6px;font-size:13px">${labels[k] || k}: ${last}</div>`;
}).join('') || 'Нет сохранённых анализов';
}
// === NORMS ===
function renderNorms() {}
// === VIDEO ===
function uploadVideo(input) {
const file = input.files[0];
if (!file) return;
if (file.size > 100 * 1024 * 1024) return toast('Видео слишком большое (макс 100 МБ)');
const reader = new FileReader();
reader.onload = function(e) {
const videoData = { name: file.name, data: e.target.result, date: new Date().toLocaleDateString('ru-RU'), id: Date.now() };
const videos = LS('videos') || [];
videos.unshift(videoData);
SS('videos', videos.slice(0, 20));
document.getElementById('video-container').innerHTML = `
<video class="video-preview" controls src="${e.target.result}" style="margin-top:8px"></video>
<div style="margin-top:8px;font-size:12px;color:var(--gray-500)">&#x1F4AC; Смотри в повторе: старт, гребок, поворот, финиш. Где теряешь время?</div>`;
renderVideos();
toast('Видео загружено!');
};
reader.readAsDataURL(file);
}
function renderVideos() {
const videos = LS('videos') || [];
const el = document.getElementById('video-list');
if (!videos.length) { el.innerHTML = '<div class="empty"><div class="big">&#x1F3A5;</div>Загрузи первое видео заплыва!</div>'; return; }
el.innerHTML = videos.map(v => `<div class="card" style="padding:12px;margin-bottom:8px;cursor:pointer" onclick="showLightbox('video','${v.id}')">
<strong>${v.name}</strong> <span style="color:var(--gray-500);font-size:13px">— ${v.date}</span>
</div>`).join('');
}
// === PHOTOS ===
function uploadPhotos(input) {
const files = Array.from(input.files);
if (!files.length) return;
const photos = LS('photos') || [];
let loaded = 0;
files.forEach(file => {
if (file.size > 10 * 1024 * 1024) return toast('Фото слишком большое (макс 10 МБ)');
const reader = new FileReader();
reader.onload = function(e) {
photos.unshift({ name: file.name, data: e.target.result, date: new Date().toLocaleDateString('ru-RU'), id: Date.now() + loaded });
loaded++;
if (loaded === files.length || loaded >= 10) {
SS('photos', photos.slice(0, 50));
renderPhotos();
toast(loaded + ' фото добавлено!');
}
};
reader.readAsDataURL(file);
});
}
function renderPhotos() {
const photos = LS('photos') || [];
const el = document.getElementById('photo-grid');
if (!photos.length) { el.innerHTML = '<div class="empty" style="grid-column:1/-1"><div class="big">&#x1F4F7;</div>Добавь первые фото!</div>'; return; }
el.innerHTML = photos.map(p => `<img src="${p.data}" alt="${p.name}" onclick="showLightbox('photo','${p.id}')">`).join('');
}
// === GRADES ===
function addGrade() {
const subject = document.getElementById('grade-subject').value.trim();
const grade = +document.getElementById('grade-value').value;
if (!subject) return toast('Введи предмет!');
const grades = LS('grades') || {};
if (!grades[subject]) grades[subject] = [];
grades[subject].push({ grade, date: new Date().toISOString().slice(0,10) });
SS('grades', grades);
document.getElementById('grade-subject').value = '';
renderGrades();
toast('Оценка добавлена!');
}
function renderGrades() {
const grades = LS('grades') || {};
const el = document.getElementById('grades-list');
const allGrades = [];
Object.entries(grades).forEach(([subject, vals]) => {
vals.forEach(v => allGrades.push({ subject, ...v }));
});
const avg = allGrades.length ? (allGrades.reduce((s,v) => s+v.grade, 0) / allGrades.length).toFixed(2) : '—';
document.getElementById('gpa-display').textContent = avg;
document.getElementById('grades-count').textContent = allGrades.length;
if (!Object.keys(grades).length) {
el.innerHTML = '<div class="empty"><div class="big">&#x1F393;</div>Добавь оценки по предметам</div>';
return;
}
el.innerHTML = Object.entries(grades).map(([subject, vals]) => {
const subjAvg = (vals.reduce((s,v) => s+v.grade, 0) / vals.length).toFixed(1);
const last5 = vals.slice(-5).map(v => v.grade).join(', ');
return `<div class="card" style="padding:16px;margin-bottom:8px">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>${subject}</strong>
<span class="badge ${+subjAvg >= 4.5 ? 'gold' : +subjAvg >= 3.5 ? 'blue' : 'badge'}" style="background:${+subjAvg >= 4.5 ? '#FFF3CD' : +subjAvg >=3.5 ? 'var(--cyan-50)' : '#FEE'};color:${+subjAvg >= 4.5 ? '#856404' : +subjAvg >=3.5 ? 'var(--ink)' : '#c00'}">${subjAvg}</span>
</div>
<div style="font-size:13px;color:var(--gray-500)">Оценки: ${last5}</div>
<button class="btn danger small" style="margin-top:6px" onclick="deleteGrades('${subject}')">Сбросить</button>
</div>`;
}).join('');
}
function deleteGrades(subject) {
const grades = LS('grades') || {};
delete grades[subject];
SS('grades', grades);
renderGrades();
toast('Удалено');
}
// === LIGHTBOX ===
function showLightbox(type, id) {
const lb = document.getElementById('lightbox');
const items = LS(type === 'photo' ? 'photos' : 'videos') || [];
const item = items.find(i => i.id == id);
if (!item) return;
if (type === 'photo') {
lb.innerHTML = `<img src="${item.data}" alt="${item.name}">`;
} else {
lb.innerHTML = `<video controls src="${item.data}" style="max-width:95%;max-height:90vh"></video>`;
}
lb.style.display = 'flex';
}
// === LESSONS ===
const lessons = [
{ title: 'Техника старта с тумбы', q: 'swimming start technique tutorial' },
{ title: 'Поворот кувырком — идеальная техника', q: 'flip turn swimming technique' },
{ title: 'Как плыть быстрее: гребок и пронос', q: 'freestyle stroke technique fast' },
{ title: 'Дыхание в кроле — не задыхайся', q: 'freestyle breathing technique for beginners' },
{ title: 'Дельфиньи удары под водой', q: 'underwater dolphin kick technique' },
{ title: 'Брасс: техника ног и рук', q: 'breaststroke technique tutorial' },
{ title: 'Бабочка: как не уставать', q: 'butterfly stroke technique for beginners' },
{ title: 'Разбор заплыва Калеба Дрессела', q: 'Caeleb Dressel 50m freestyle technique analysis' },
{ title: 'Растяжка для пловцов', q: 'swimming stretching routine flexibility' },
{ title: 'Силовые упражнения для пловцов на суше', q: 'dryland training swimming exercises' },
{ title: 'Психология победителя — настрой на старт', q: 'swimming race psychology mental preparation' },
{ title: 'Питание пловца — что есть до и после', q: 'swimmer nutrition meal plan' }
];
document.getElementById('lesson-list').innerHTML = lessons.map(l =>
`<div class="card" style="padding:14px;margin-bottom:6px">
<strong>${l.title}</strong>
<div style="font-size:12px;color:var(--gray-500);margin-top:4px">Найди на YouTube: <em>${l.q}</em></div>
</div>`
).join('');
// === INIT ===
renderDiary();
renderVitamins();
renderSleep();
renderTests();
renderVideos();
renderPhotos();
renderGrades();
document.getElementById('diary-date').value = new Date().toISOString().slice(0,10);
</script>
</body>
</html>