galikon/index.html

675 lines
34 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>
<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:12px 20px;position:sticky;top:0;z-index:100;display:flex;align-items:center;justify-content:space-between;gap:12px}
header h1{font-size:20px;font-weight:800;white-space:nowrap}
header h1 span{color:var(--cyan)}
.header-right{display:flex;align-items:center;gap:8px}
.header-right .avatar{width:34px;height:34px;border-radius:50%;background:var(--cyan);color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:16px;cursor:pointer;overflow:hidden}
.header-right .avatar img{width:100%;height:100%;object-fit:cover}
.header-right .profile-name{font-size:13px;color:var(--cyan);max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
nav.tabs{display:flex;overflow-x:auto;gap:4px;padding:8px 20px;background:var(--ink);position:sticky;top:57px;z-index:99;-webkit-overflow-scrolling:touch}
nav.tabs::-webkit-scrollbar{display:none}
.tab-btn{flex-shrink:0;padding:8px 14px;border-radius:20px;border:none;font-size:13px;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}
.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;text-align:center}
.btn:active{transform:scale(.96)}
.btn.danger{background:var(--red);color:#fff}
.btn.small{padding:6px 14px;font-size:13px}
.btn.outline{background:transparent;border:2px solid var(--cyan);color:var(--cyan)}
.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}
/* PROFILE SCREEN */
.profile-screen{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--ink);z-index:300;display:flex;flex-direction:column;overflow-y:auto;padding:24px}
.profile-screen h2{color:var(--white);text-align:center;margin-bottom:8px;font-size:24px}
.profile-screen h2 span{color:var(--cyan)}
.profile-screen .sub{color:var(--gray-500);text-align:center;margin-bottom:24px;font-size:15px}
.profile-screen .card{background:#1a2332;border:1px solid #2a3342;margin-bottom:12px}
.profile-screen .card h3{color:var(--white)}
.profile-screen input,.profile-screen select,.profile-screen textarea{background:#0F1218;border-color:#2a3342;color:var(--white)}
.profile-screen input::placeholder{color:#5b6573}
.profile-screen label{color:var(--gray-500);font-size:13px;margin-bottom:4px;display:block}
.profile-list-item{display:flex;align-items:center;gap:14px;padding:14px;background:#1a2332;border-radius:12px;margin-bottom:8px;cursor:pointer;border:2px solid transparent;transition:border-color .2s}
.profile-list-item:hover,.profile-list-item.selected{border-color:var(--cyan)}
.profile-list-item .av{width:44px;height:44px;border-radius:50%;background:var(--cyan);color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:18px;flex-shrink:0;overflow:hidden}
.profile-list-item .av img{width:100%;height:100%;object-fit:cover}
.profile-list-item .info{flex:1;color:var(--white)}
.profile-list-item .info .name{font-weight:700;font-size:15px}
.profile-list-item .info .sport{font-size:12px;color:var(--gray-500)}
.app-screen{display:none}
.app-screen.active{display:block}
.profile-screen.hidden{display:none}
@media(max-width:480px){
.grid2{grid-template-columns:1fr}
header h1{font-size:16px}
nav.tabs{gap:2px;padding:6px 10px}
.tab-btn{padding:7px 10px;font-size:12px}
main{padding:12px}
}
</style>
</head>
<body>
<!-- PROFILE SCREEN -->
<div class="profile-screen" id="profileScreen">
<h2>&#x1F3CA; <span>Галикон</span></h2>
<p class="sub">Приложение для спортсменов. Создай свой профиль.</p>
<div class="card" id="regForm">
<h3>&#x270F; Регистрация</h3>
<input type="text" id="regName" placeholder="Фамилия Имя Отчество">
<div class="grid2">
<input type="text" id="regSport" placeholder="Вид спорта (плавание, бокс...)">
<input type="number" id="regAge" placeholder="Возраст" min="5" max="100">
</div>
<input type="text" id="regClub" placeholder="Клуб / спортивная школа">
<div class="grid2">
<input type="text" id="regCity" placeholder="Город">
<input type="text" id="regCoach" placeholder="Тренер (ФИО)">
</div>
<div class="grid2">
<input type="text" id="regRank" placeholder="Разряд / звание">
<input type="text" id="regGoal" placeholder="Главная цель">
</div>
<label class="file-btn" style="margin-bottom:10px">&#x1F4F7; Фото профиля<input type="file" accept="image/*" id="regPhoto" onchange="previewRegPhoto()"></label>
<img id="regPhotoPreview" style="width:80px;height:80px;border-radius:50%;object-fit:cover;display:none;margin:0 auto 10px">
<button class="btn" onclick="registerProfile()" style="width:100%">&#x1F44D; Создать профиль</button>
</div>
<div style="text-align:center;color:var(--gray-500);font-size:14px;margin-bottom:12px">— или —</div>
<div id="profileList"></div>
</div>
<!-- APP SCREEN -->
<div class="app-screen" id="appScreen">
<header>
<h1>&#x1F3CA; <span>Галикон</span></h1>
<div class="header-right">
<span class="profile-name" id="headerName"></span>
<div class="avatar" id="headerAvatar" onclick="showProfileScreen()"></div>
</div>
</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 id="diary-list" class="empty"><div class="big">&#x1F4AD;</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 м в/с (мужчины, бассейн 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>
<!-- ВИДЕО -->
<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 id="video-list" class="empty"><div class="big">&#x1F3A5;</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" 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>
<div class="toast" id="toast">Сохранено!</div>
<div class="lightbox" id="lightbox" style="display:none" onclick="this.style.display='none'"></div>
<script>
// === STORAGE HELPERS ===
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('Память браузера заполнена!') } };
let currentProfileId = null;
function pid() { return currentProfileId || 'default'; }
function toast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
// === PROFILE SYSTEM ===
function registerProfile() {
const name = document.getElementById('regName').value.trim();
if (!name) return toast('Введи ФИО!');
const profile = {
id: Date.now(),
name,
sport: document.getElementById('regSport').value.trim(),
age: document.getElementById('regAge').value,
club: document.getElementById('regClub').value.trim(),
city: document.getElementById('regCity').value.trim(),
coach: document.getElementById('regCoach').value.trim(),
rank: document.getElementById('regRank').value.trim(),
goal: document.getElementById('regGoal').value.trim(),
photo: document.getElementById('regPhotoPreview').src || '',
created: new Date().toISOString()
};
if (profile.photo === window.location.href) profile.photo = '';
const profiles = LS('profiles') || [];
profiles.push(profile);
SS('profiles', profiles);
// Clear form
['regName','regSport','regAge','regClub','regCity','regCoach','regRank','regGoal'].forEach(id => document.getElementById(id).value = '');
document.getElementById('regPhotoPreview').style.display = 'none';
loginProfile(profile.id);
}
function previewRegPhoto() {
const f = document.getElementById('regPhoto').files[0];
if (!f) return;
const r = new FileReader();
r.onload = e => { const img = document.getElementById('regPhotoPreview'); img.src = e.target.result; img.style.display = 'block'; };
r.readAsDataURL(f);
}
function loginProfile(id) {
currentProfileId = id;
const profiles = LS('profiles') || [];
const p = profiles.find(x => x.id === id);
if (!p) return;
document.getElementById('profileScreen').classList.add('hidden');
document.getElementById('appScreen').classList.add('active');
document.getElementById('headerName').textContent = p.name.split(' ')[0];
const av = document.getElementById('headerAvatar');
if (p.photo) {
av.innerHTML = `<img src="${p.photo}" alt="">`;
} else {
av.innerHTML = p.name.charAt(0).toUpperCase();
}
renderAll();
}
function showProfileScreen() {
document.getElementById('profileScreen').classList.remove('hidden');
document.getElementById('appScreen').classList.remove('active');
renderProfileList();
}
function deleteProfile(id) {
if (!confirm('Удалить профиль? Все данные будут потеряны.')) return;
const profiles = (LS('profiles') || []).filter(p => p.id !== id);
SS('profiles', profiles);
if (currentProfileId === id) { currentProfileId = null; document.getElementById('appScreen').classList.remove('active'); }
renderProfileList();
}
function renderProfileList() {
const profiles = LS('profiles') || [];
const el = document.getElementById('profileList');
if (!profiles.length) { el.innerHTML = '<p style="color:var(--gray-500);text-align:center">Нет профилей. Создай первый!</p>'; return; }
el.innerHTML = profiles.map(p => `
<div class="profile-list-item ${p.id === currentProfileId ? 'selected' : ''}" onclick="loginProfile(${p.id})">
<div class="av">${p.photo ? `<img src="${p.photo}">` : p.name.charAt(0).toUpperCase()}</div>
<div class="info">
<div class="name">${p.name}</div>
<div class="sport">${[p.sport, p.club, p.age ? p.age+' лет' : '', p.rank].filter(Boolean).join(' · ')}</div>
</div>
<button class="btn danger small" style="flex-shrink:0" onclick="event.stopPropagation();deleteProfile(${p.id})">&#x1F5D1;</button>
</div>
`).join('');
}
// === 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');
});
});
// === 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 all = LS('diary_all') || {};
if (!all[pid()]) all[pid()] = [];
all[pid()].unshift(entry);
SS('diary_all', all);
['diary-date','diary-type','diary-km','diary-time','diary-feel','diary-note'].forEach(id => document.getElementById(id).value = '');
renderDiary();
toast('Запись добавлена!');
}
function renderDiary() {
const all = LS('diary_all') || {};
const diary = all[pid()] || [];
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 all = LS('diary_all') || {};
if (!all[pid()]) return;
all[pid()] = all[pid()].filter(e => e.id !== id);
SS('diary_all', all);
renderDiary();
toast('Удалено');
}
// === HEALTH ===
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 all = LS('vitamins_all') || {};
const taken = all[pid()] || {};
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()} из ${vitamins.length}</div>`;
}
function countTaken() {
const all = LS('vitamins_all') || {};
const taken = all[pid()] || {};
const today = new Date().toISOString().slice(0,10);
return taken[today] ? Object.values(taken[today]).filter(Boolean).length : 0;
}
function toggleVitamin(id) {
const today = new Date().toISOString().slice(0,10);
const all = LS('vitamins_all') || {};
if (!all[pid()]) all[pid()] = {};
if (!all[pid()][today]) all[pid()][today] = {};
all[pid()][today][id] = !all[pid()][today][id];
SS('vitamins_all', all);
renderVitamins();
}
function saveSleep() {
const h = document.getElementById('sleep-hours').value;
const p = document.getElementById('morning-pulse').value;
if (!h && !p) return toast('Введи данные!');
const all = LS('sleep_all') || {};
if (!all[pid()]) all[pid()] = [];
all[pid()].unshift({ date: new Date().toISOString().slice(0,10), hours: +h || 0, pulse: +p || 0 });
SS('sleep_all', all);
document.getElementById('sleep-hours').value = '';
document.getElementById('morning-pulse').value = '';
renderSleep();
toast('Сохранено!');
}
function renderSleep() {
const all = LS('sleep_all') || {};
const sleep = all[pid()] || [];
const el = document.getElementById('sleep-stats');
if (!sleep.length) { el.innerHTML = ''; return; }
const last7 = sleep.slice(0,7).filter(e => e.hours > 0);
if (!last7.length) { el.innerHTML = ''; return; }
const avgH = (last7.reduce((s,e) => s+e.hours, 0) / last7.length).toFixed(1);
const avgP = last7.filter(e => e.pulse > 0).length ? (last7.filter(e=>e.pulse>0).reduce((s,e) => s+e.pulse, 0) / last7.filter(e=>e.pulse>0).length).toFixed(0) : '—';
el.innerHTML = `Последние 7 дней: сон <strong>${avgH} ч</strong>, пульс <strong>${avgP} уд/мин</strong>`;
}
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 all = LS('tests_all') || {};
if (!all[pid()]) all[pid()] = {};
if (!all[pid()][type]) all[pid()][type] = [];
all[pid()][type].push({ date, value: +val });
SS('tests_all', all);
document.getElementById('test-value').value = '';
document.getElementById('test-date').value = '';
renderTests();
toast('Анализ сохранён!');
}
function renderTests() {
const all = LS('tests_all') || {};
const tests = all[pid()] || {};
const labels = { hb: 'Гемоглобин (120-150)', ferritin: 'Ферритин (30-100)', vitd: 'Витамин D (50-80)', b12: 'B12 (200-900)' };
const el = document.getElementById('test-history');
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('') || 'Нет сохранённых анализов';
}
// === 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 all = LS('videos_all') || {};
if (!all[pid()]) all[pid()] = [];
all[pid()].unshift(videoData);
SS('videos_all', all);
document.getElementById('video-container').innerHTML = `<video class="video-preview" controls src="${e.target.result}" style="margin-top:8px"></video>`;
renderVideos();
toast('Видео загружено!');
};
reader.readAsDataURL(file);
}
function renderVideos() {
const all = LS('videos_all') || {};
const videos = all[pid()] || [];
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 all = LS('photos_all') || {};
if (!all[pid()]) all[pid()] = [];
let loaded = 0;
files.slice(0,10).forEach(file => {
if (file.size > 10*1024*1024) return;
const reader = new FileReader();
reader.onload = function(e) {
all[pid()].unshift({ name: file.name, data: e.target.result, date: new Date().toLocaleDateString('ru-RU'), id: Date.now()+loaded });
loaded++;
if (loaded === Math.min(files.length, 10)) { SS('photos_all', all); renderPhotos(); toast(loaded+' фото!'); }
};
reader.readAsDataURL(file);
});
}
function renderPhotos() {
const all = LS('photos_all') || {};
const photos = all[pid()] || [];
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 all = LS('grades_all') || {};
if (!all[pid()]) all[pid()] = {};
if (!all[pid()][subject]) all[pid()][subject] = [];
all[pid()][subject].push({ grade, date: new Date().toISOString().slice(0,10) });
SS('grades_all', all);
document.getElementById('grade-subject').value = '';
renderGrades();
toast('Оценка добавлена!');
}
function renderGrades() {
const all = LS('grades_all') || {};
const grades = all[pid()] || {};
const allGrades = [];
Object.entries(grades).forEach(([s,vals]) => vals.forEach(v => allGrades.push({ subject:s, ...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;
const el = document.getElementById('grades-list');
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);
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':''}" 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)">Последние: ${vals.slice(-5).map(v=>v.grade).join(', ')}</div>
<button class="btn danger small" style="margin-top:6px" onclick="deleteGrades('${subject}')">Сбросить</button>
</div>`;
}).join('');
}
function deleteGrades(subject) {
const all = LS('grades_all') || {};
if (all[pid()]) { delete all[pid()][subject]; SS('grades_all', all); }
renderGrades();
toast('Удалено');
}
// === LIGHTBOX ===
function showLightbox(type, id) {
const lb = document.getElementById('lightbox');
const key = type === 'photo' ? 'photos_all' : 'videos_all';
const all = LS(key) || {};
const items = all[pid()] || [];
const item = items.find(i => i.id == id);
if (!item) return;
lb.innerHTML = type === 'photo' ? `<img src="${item.data}">` : `<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' },
{ title: 'Дельфиньи удары под водой', q: 'underwater dolphin kick technique' },
{ title: 'Брасс: техника', q: 'breaststroke technique tutorial' },
{ title: 'Калеб Дрессел — анализ', q: 'Caeleb Dressel 50m freestyle analysis' },
{ title: 'Растяжка для пловцов', q: 'swimming stretching routine' },
{ title: 'Силовые на суше', q: 'dryland training swimming' },
{ title: 'Психология старта', q: 'swimming race psychology preparation' },
{ title: 'Питание пловца', q: 'swimmer nutrition meal plan' },
{ title: 'Восстановление после тренировки', q: 'swimming recovery routine' }
];
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('');
// === RENDER ALL ===
function renderAll() {
renderDiary();
renderVitamins();
renderSleep();
renderTests();
renderVideos();
renderPhotos();
renderGrades();
}
// === INIT ===
(function init() {
const profiles = LS('profiles') || [];
renderProfileList();
if (profiles.length > 0) {
// Auto-login last profile
loginProfile(profiles[profiles.length - 1].id);
}
document.getElementById('diary-date').value = new Date().toISOString().slice(0,10);
})();
</script>
</body>
</html>