galikon/index.html

1122 lines
63 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="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Галикон">
<meta name="theme-color" content="#0F1218">
<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,#loginScreen.hidden{display:none}
.ai-helper{position:fixed;bottom:16px;right:16px;z-index:350;width:300px;max-width:90vw;background:var(--ink);border:1px solid #2a3342;border-radius:16px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.4);transition:transform .3s}
.ai-helper.minimized{transform:translateY(calc(100% - 48px))}
.ai-helper .ai-header{background:var(--cyan);color:var(--ink);padding:10px 14px;font-weight:700;font-size:14px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}
.ai-helper .ai-header button{background:none;border:none;color:var(--ink);font-size:18px;cursor:pointer;padding:0 4px}
.ai-helper .ai-body{padding:12px;max-height:240px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}
.ai-helper .ai-msg{background:#1a2332;color:#9aa3b2;padding:8px 12px;border-radius:12px;font-size:13px;line-height:1.4;max-width:90%;animation:fadeIn .4s}
.ai-helper .ai-msg.user{background:var(--cyan);color:var(--ink);align-self:flex-end;text-align:right}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
@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 -->
<!-- LOGIN SCREEN -->
<div class="profile-screen" id="loginScreen">
<div style="max-width:400px;margin:auto">
<h2>&#x1F3CA; <span>Галикон</span></h2>
<p class="sub">Войди в свой аккаунт</p>
<div class="card">
<input type="text" id="loginUser" placeholder="Логин">
<input type="password" id="loginPass" placeholder="Пароль">
<div id="loginError" style="color:var(--red);font-size:13px;margin-bottom:8px;display:none"></div>
<button class="btn" onclick="doLogin()" style="width:100%;margin-bottom:8px">&#x1F512; Войти</button>
<button class="btn outline" onclick="showRegister()" style="width:100%">&#x270F; Создать аккаунт</button>
</div>
</div>
</div>
<!-- REGISTER + PROFILE SCREEN -->
<!-- AI ASSISTANT -->
<div class="ai-helper" id="aiHelper">
<div class="ai-header" onclick="toggleAI()">
&#x1F916; ИИ-помощник
<button onclick="event.stopPropagation();toggleAI()" id="aiToggleBtn">&#x25BC;</button>
</div>
<div class="ai-body" id="aiBody">
<div class="ai-msg">&#x1F44B; Привет! Я помогу тебе зарегистрироваться. Давай по шагам!</div>
</div>
</div>
<div class="profile-screen hidden" 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="regLogin" placeholder="Придумай логин">
<input type="password" id="regPass" placeholder="Придумай пароль">
</div>
<div class="grid2">
<select id="regSport">
<option value="">Выбери вид спорта</option>
<option>Академическая гребля</option>
<option>Бадминтон</option>
<option>Баскетбол</option>
<option>Бокс</option>
<option>Борьба вольная</option>
<option>Борьба греко-римская</option>
<option>Велоспорт</option>
<option>Водное поло</option>
<option>Волейбол</option>
<option>Гандбол</option>
<option>Гимнастика спортивная</option>
<option>Гимнастика художественная</option>
<option>Гольф</option>
<option>Гребля на байдарках и каноэ</option>
<option>Дзюдо</option>
<option>Карате</option>
<option>Конный спорт</option>
<option>Лёгкая атлетика</option>
<option>Настольный теннис</option>
<option>Парусный спорт</option>
<option>Плавание</option>
<option>Прыжки в воду</option>
<option>Прыжки на батуте</option>
<option>Регби</option>
<option>Сёрфинг</option>
<option>Синхронное плавание</option>
<option>Скейтбординг</option>
<option>Современное пятиборье</option>
<option>Спортивное скалолазание</option>
<option>Стрельба из лука</option>
<option>Стрельба пулевая</option>
<option>Теннис</option>
<option>Триатлон</option>
<option>Тхэквондо</option>
<option>Тяжёлая атлетика</option>
<option>Фехтование</option>
<option>Футбол</option>
<option>Хоккей на траве</option>
</select>>
<input type="number" id="regAge" placeholder="Возраст (авто)" min="5" max="100" readonly style="background:var(--gray-100)">
<input type="date" id="regBirth" placeholder="Дата рождения" onchange="calcAge()" onblur="calcAge()">
</div>
<input type="text" id="regClub" placeholder="Клуб / спортивная школа">
<div class="grid2">
<select id="regCountry" onchange="updateCityList()">
<option value="">Страна</option>
<option>Австралия</option><option>Австрия</option><option>Азербайджан</option><option>Албания</option><option>Алжир</option><option>Ангола</option><option>Аргентина</option><option>Армения</option><option>Афганистан</option><option>Бангладеш</option><option>Бахрейн</option><option>Беларусь</option><option>Бельгия</option><option>Болгария</option><option>Боливия</option><option>Бразилия</option><option>Великобритания</option><option>Венгрия</option><option>Венесуэла</option><option>Вьетнам</option><option>Германия</option><option>Греция</option><option>Грузия</option><option>Дания</option><option>Египет</option><option>Израиль</option><option>Индия</option><option>Индонезия</option><option>Иран</option><option>Ирландия</option><option>Исландия</option><option>Испания</option><option>Италия</option><option>Казахстан</option><option>Канада</option><option>Катар</option><option>Кения</option><option>Кипр</option><option>Китай</option><option>Колумбия</option><option>Корея Южная</option><option>Куба</option><option>Кувейт</option><option>Кыргызстан</option><option>Латвия</option><option>Литва</option><option>Малайзия</option><option>Марокко</option><option>Мексика</option><option>Молдова</option><option>Монголия</option><option>Нидерланды</option><option>Новая Зеландия</option><option>Норвегия</option><option>ОАЭ</option><option>Пакистан</option><option>Перу</option><option>Польша</option><option>Португалия</option><option>Россия</option><option>Румыния</option><option>Саудовская Аравия</option><option>Сербия</option><option>Сингапур</option><option>Словакия</option><option>Словения</option><option>США</option><option>Таджикистан</option><option>Таиланд</option><option>Тайвань</option><option>Тунис</option><option>Туркменистан</option><option>Турция</option><option>Узбекистан</option><option>Украина</option><option>Филиппины</option><option>Финляндия</option><option>Франция</option><option>Хорватия</option><option>Чехия</option><option>Чили</option><option>Швейцария</option><option>Швеция</option><option>Эстония</option><option>ЮАР</option><option>Япония</option>
</select>
<input type="text" id="regCity" placeholder="Город" list="cityList" autocomplete="off">
<datalist id="cityList"></datalist>
</div>
<input type="text" id="regCoach" placeholder="Тренер (ФИО)">
<div class="grid2">
<input type="tel" id="regPhone" placeholder="Телефон (WhatsApp)">
<input type="email" id="regEmail" placeholder="Email">
</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">
<button class="btn outline small" style="padding:4px 10px;font-size:11px;margin-right:4px" onclick="doLogout()">&#x1F6AA;</button>
<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>
<button class="tab-btn" data-tab="ranking">&#x2B50; Рейтинг</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="ranking">
<div class="card">
<h3><span class="emoji">&#x1F3C6;</span>Рейтинг спортсменов</h3>
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">&#x2B50; = голоса родителей. Нажми, чтобы поддержать!</p>
<div id="swimmer-ranking"><div class="empty"><div class="big">&#x1F3CA;</div>Добавьте спортсменов</div></div>
<button class="btn outline" onclick="addSwimmerForm()" style="width:100%;margin-top:8px">+ Добавить спортсмена</button>
<div id="swimmer-form" style="display:none;margin-top:12px">
<input id="sw-name" placeholder="ФИО спортсмена">
<div class="grid2">
<input id="sw-club" placeholder="Клуб">
<input id="sw-time" placeholder="Лучшее время (50 м в/с)">
</div>
<button class="btn" onclick="addSwimmer()" style="width:100%">Добавить</button>
</div>
</div>
<div class="card">
<h3><span class="emoji">&#x1F468;&#x200D;&#x1F3EB;</span>Рейтинг тренеров</h3>
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">&#x2B50; = голоса родителей. Оцени тренера!</p>
<div id="coach-ranking"><div class="empty"><div class="big">&#x1F3CB;</div>Добавьте тренеров</div></div>
<button class="btn outline" onclick="addCoachForm()" style="width:100%;margin-top:8px">+ Добавить тренера</button>
<div id="coach-form" style="display:none;margin-top:12px">
<input id="co-name" placeholder="ФИО тренера">
<div class="grid2">
<input id="co-club" placeholder="Клуб">
<input id="co-exp" placeholder="Стаж (лет)">
</div>
<textarea id="co-achieve" placeholder="Достижения (воспитанники, разряды...)" rows="2"></textarea>
<button class="btn" onclick="addCoach()" style="width:100%">Добавить</button>
</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>
<!-- AI ASSISTANT для приложения -->
<div class="ai-helper" id="aiAppHelper">
<div class="ai-header" onclick="toggleAppAI()">
&#x1F916; ИИ-помощник
<button onclick="event.stopPropagation();toggleAppAI()" id="aiAppToggleBtn">&#x25BC;</button>
</div>
<div class="ai-body" id="aiAppBody">
<div class="ai-msg">&#x1F44B; Я здесь! Выбери раздел — я подскажу что делать.</div>
</div>
</div>
</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 doLogin() {
const login = document.getElementById('loginUser').value.trim().toLowerCase();
const pass = document.getElementById('loginPass').value;
if (!login || !pass) { showLoginError('Введи логин и пароль!'); return; }
const profiles = LS('profiles') || [];
const profile = profiles.find(p => p.login === login && p.pass === pass);
if (!profile) { showLoginError('Неверный логин или пароль'); return; }
document.getElementById('loginScreen').classList.add('hidden');
document.getElementById('profileScreen').classList.add('hidden');
document.getElementById('loginScreen').classList.add('hidden');
document.getElementById('appScreen').classList.add('active');
loginProfile(profile.id);
hideLoginError();
}
function doLogout() {
if (confirm('Выйти из аккаунта?')) {
currentProfileId = null;
document.getElementById('appScreen').classList.remove('active');
document.getElementById('loginScreen').classList.remove('hidden');
document.getElementById('loginUser').value = '';
document.getElementById('loginPass').value = '';
}
}
function showLoginError(msg) {
const el = document.getElementById('loginError');
el.textContent = msg; el.style.display = 'block';
}
function hideLoginError() {
document.getElementById('loginError').style.display = 'none';
}
function showRegister() {
document.getElementById('loginScreen').classList.add('hidden');
document.getElementById('profileScreen').classList.remove('hidden');
document.getElementById('loginScreen').classList.add('hidden');
renderProfileList();
}
function registerProfile() {
calcAge();
const name = document.getElementById('regName').value.trim();
const login = document.getElementById('regLogin').value.trim().toLowerCase();
const pass = document.getElementById('regPass').value;
if (!login) return toast('Придумай логин!');
if (!pass || pass.length < 3) return toast('Пароль — минимум 3 символа!');
const profiles = LS('profiles') || [];
if (profiles.find(p => p.login === login)) return toast('Такой логин уже занят!');
if (!name) return toast('Введи ФИО!');
const profile = {
id: Date.now(),
name,
sport: document.getElementById('regSport').value.trim(),
age: document.getElementById('regAge').value,
birth: document.getElementById('regBirth').value,
club: document.getElementById('regClub').value.trim(),
country: document.getElementById('regCountry').value,
city: document.getElementById('regCity').value.trim(),
coach: document.getElementById('regCoach').value.trim(),
phone: document.getElementById('regPhone').value.trim(),
email: document.getElementById('regEmail').value.trim(),
rank: document.getElementById('regRank').value.trim(),
goal: document.getElementById('regGoal').value.trim(),
photo: document.getElementById('regPhotoPreview').src || '',
login, pass, 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','regLogin','regPass','regSport','regAge','regBirth','regCountry','regClub','regCity','regCoach','regPhone','regEmail','regRank','regGoal'].forEach(id => document.getElementById(id).value = '');
document.getElementById('regPhotoPreview').style.display = 'none';
loginProfile(profile.id);
}
const cityData = {
'Казахстан': ['Астана','Алматы','Шымкент','Актобе','Караганда','Тараз','Павлодар','Усть-Каменогорск','Семей','Атырау','Костанай','Кызылорда','Уральск','Петропавловск','Актау','Туркестан','Кокшетау','Талдыкорган'],
'Россия': ['Москва','Санкт-Петербург','Новосибирск','Екатеринбург','Казань','Нижний Новгород','Челябинск','Самара','Омск','Ростов-на-Дону','Уфа','Красноярск','Воронеж','Пермь','Волгоград'],
'США': ['Нью-Йорк','Лос-Анджелес','Чикаго','Хьюстон','Финикс','Филадельфия','Сан-Антонио','Сан-Диего','Даллас','Сан-Хосе','Остин','Майами','Бостон','Сиэтл','Денвер'],
'Китай': ['Пекин','Шанхай','Гуанчжоу','Шэньчжэнь','Чэнду','Ухань','Нанкин','Ханчжоу','Тяньцзинь','Чунцин'],
'Турция': ['Стамбул','Анкара','Измир','Бурса','Анталья','Адана','Конья','Газиантеп','Мерсин','Диярбакыр'],
'Украина': ['Киев','Харьков','Одесса','Днепр','Львов','Запорожье','Винница','Полтава','Чернигов','Ивано-Франковск'],
'Узбекистан': ['Ташкент','Самарканд','Бухара','Наманган','Андижан','Фергана','Нукус','Карши','Коканд','Маргилан'],
'Германия': ['Берлин','Мюнхен','Гамбург','Кёльн','Франкфурт','Штутгарт','Дюссельдорф','Лейпциг','Дортмунд','Эссен'],
'Франция': ['Париж','Марсель','Лион','Тулуза','Ницца','Нант','Страсбург','Монпелье','Бордо','Лилль'],
'Великобритания': ['Лондон','Манчестер','Бирмингем','Лидс','Глазго','Ливерпуль','Бристоль','Эдинбург','Шеффилд','Ноттингем'],
'Япония': ['Токио','Осака','Киото','Иокогама','Нагоя','Саппоро','Фукуока','Кобе','Хиросима','Сендай'],
'Корея Южная': ['Сеул','Пусан','Инчхон','Тэгу','Тэджон','Кванджу','Сувон','Ульсан','Чханвон','Коян'],
'ОАЭ': ['Дубай','Абу-Даби','Шарджа','Аль-Айн','Аджман','Рас-эль-Хайма','Фуджейра','Умм-эль-Кувейн'],
'Кыргызстан': ['Бишкек','Ош','Джалал-Абад','Каракол','Токмок','Нарын','Балыкчы','Талас'],
'Азербайджан': ['Баку','Гянджа','Сумгаит','Мингечаур','Ширван','Нахичевань','Шеки','Ленкорань'],
'Беларусь': ['Минск','Гомель','Могилёв','Витебск','Гродно','Брест','Бобруйск','Барановичи'],
'Италия': ['Рим','Милан','Неаполь','Турин','Флоренция','Болонья','Венеция','Генуя','Верона','Палермо'],
'Испания': ['Мадрид','Барселона','Валенсия','Севилья','Малага','Бильбао','Сарагоса','Аликанте','Гранада','Пальма'],
'Канада': ['Торонто','Монреаль','Ванкувер','Калгари','Эдмонтон','Оттава','Квебек','Виннипег','Гамильтон','Китченер'],
'Австралия': ['Сидней','Мельбурн','Брисбен','Перт','Аделаида','Голд-Кост','Канберра','Ньюкасл','Хобарт','Дарвин'],
'Бразилия': ['Сан-Паулу','Рио-де-Жанейро','Бразилиа','Салвадор','Форталеза','Белу-Оризонти','Манаус','Куритиба','Ресифи','Порту-Алегри'],
'Индия': ['Дели','Мумбаи','Бангалор','Ченнаи','Калькутта','Хайдарабад','Пуна','Ахмадабад','Джайпур','Лакхнау']
};
function updateCityList() {
const country = document.getElementById('regCountry').value;
const datalist = document.getElementById('cityList');
datalist.innerHTML = '';
const cities = cityData[country] || [];
cities.forEach(c => { const opt = document.createElement('option'); opt.value = c; datalist.appendChild(opt); });
}
function toggleAppAI() {
const ai = document.getElementById('aiAppHelper');
const btn = document.getElementById('aiAppToggleBtn');
if (ai.classList.contains('minimized')) {
ai.classList.remove('minimized');
btn.textContent = '\u25BC';
} else {
ai.classList.add('minimized');
btn.textContent = '\u25B2';
}
}
function aiSay(msg) {
// Find active AI helper
const body = document.getElementById('aiAppBody') || document.getElementById('aiBody');
if (!body) return;
const div = document.createElement('div');
div.className = 'ai-msg';
div.innerHTML = msg;
body.appendChild(div);
body.scrollTop = body.scrollHeight;
}
function getTabHelp(tab) {
const helps = {
diary: [
'&#x1F4D6; Это твой дневник тренировок. Записывай сюда каждую тренировку!',
'&#x270F; Заполни дату, тип тренировки, километраж и лучшее время.',
'&#x1F4C8; Через месяц ты увидишь свой прогресс — это очень мотивирует!'
],
health: [
'&#x2764;&#xFE0F; Здесь ты следишь за здоровьем. Витамины, сон, анализы.',
'&#x1F48A; Отмечай галочками принятые витамины каждый день.',
'&#x1F4A4; Записывай часы сна и утренний пульс — это важно для восстановления!'
],
norms: [
'&#x1F4CA; Здесь таблица разрядов. Смотри, сколько осталось до следующего уровня!',
'&#x1F3AF; Твоя цель — МС (23 секунды). Ты уже перешагнул 3 взрослый!'
],
video: [
'&#x1F3AC; Загружай видео своих заплывов и смотри в повторе.',
'&#x1F4F9; Снимай сбоку от бортика — так лучше видно технику гребка и поворота.'
],
photos: [
'&#x1F4F7; Галерея твоих достижений. Загружай фото с соревнований!'
],
grades: [
'&#x1F393; Школьные оценки. Учёба важна не меньше тренировок — особенно для NCAA!',
'&#x1F4CA; Средний балл считается автоматически. Держи его выше 4.0!'
],
lessons: [
'&#x1F4DA; Здесь ссылки на обучающие видео. Найди их на YouTube и учись у лучших!'
],
ranking: [
'&#x2B50; Рейтинг спортсменов и тренеров. Родители могут голосовать за лучших!',
'&#x1F3C6; Топ-3 отмечены золотом, серебром и бронзой.'
]
};
return helps[tab] || ['&#x1F44B; Я здесь чтобы помочь! Выбери раздел и я подскажу что делать.'];
}
function aiWelcome() {
const tab = document.querySelector('.tab-btn.active');
const tabId = tab ? tab.dataset.tab : 'diary';
const msgs = getTabHelp(tabId);
msgs.forEach((m, i) => setTimeout(() => aiSay(m), i * 1200));
}
// Update welcome on tab switch
document.querySelectorAll('.tab-btn').forEach(btn => {
const orig = btn.onclick;
btn.addEventListener('click', () => {
setTimeout(() => {
const helps = getTabHelp(btn.dataset.tab);
aiSay('&#x1F4CC; ' + helps[0]);
}, 300);
});
});
function toggleAI() {
const ai = document.getElementById('aiHelper');
const btn = document.getElementById('aiToggleBtn');
if (ai.classList.contains('minimized')) {
ai.classList.remove('minimized');
btn.textContent = '\u25BC';
} else {
ai.classList.add('minimized');
btn.textContent = '\u25B2';
}
}
function aiHelp(field) {
const tips = {
regName: '&#x270F; Напиши свои Фамилию, Имя и Отчество. Например: Кайрат Гали Аскарович.',
regLogin: '&#x1F511; Придумай логин — короткое имя на латинице. Например: gali_swim. Его НЕ видно другим!',
regPass: '&#x1F512; Пароль — твой секрет. Минимум 3 символа. Не говори никому!',
regSport: '&#x1F3CA; Выбери свой вид спорта из списка. Ты пловец? Выбирай "Плавание"!',
regBirth: '&#x1F382; Твоя дата рождения. Возраст посчитается сам!',
regCountry: '&#x1F30D; Выбери страну — и появятся города этой страны.',
regCity: '&#x1F3D9; Напиши свой город. Если его нет в подсказках — просто впиши.',
regClub: '&#x1F3EB; В каком клубе или спортшколе ты занимаешься?',
regCoach: '&#x1F468;&#x200D;&#x1F3EB; Имя твоего тренера. Он будет рад, что ты здесь!',
regRank: '&#x1F3C5; Твой разряд или звание. Например: 1 юношеский, 3 взрослый.',
regGoal: '&#x1F3AF; Твоя главная цель! Например: 50 м в/с за 23 секунды.',
regPhone: '&#x1F4F1; Твой номер телефона. Можно WhatsApp. Нужен, чтобы тренер или родители могли связаться.',
regEmail: '&#x2709;&#xFE0F; Твоя почта. Пригодится для регистрации на соревнования и NCAA.',
regPhoto: '&#x1F4F7; Загрузи своё фото — так твой профиль будет красивее!'
};
if (tips[field]) aiSay(tips[field]);
}
// Attach focus listeners to all registration fields
setTimeout(() => {
const fields = ['regName','regLogin','regPass','regSport','regBirth','regCountry','regCity','regClub','regCoach','regPhone','regEmail','regRank','regGoal'];
fields.forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('focus', () => aiHelp(id));
});
// Welcome message sequence
const welcome = [
'&#x1F44B; Привет! Я помогу тебе зарегистрироваться.',
'&#x1F4CB; Сейчас мы заполним твой профиль шаг за шагом. Начнём!'
];
let i = 0;
const timer = setInterval(() => {
if (i < welcome.length) aiSay(welcome[i++]);
else clearInterval(timer);
}, 1500);
}, 500);
function calcAge() {
const b = document.getElementById('regBirth').value;
if (!b) return;
const birth = new Date(b);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const m = today.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
document.getElementById('regAge').value = age;
}
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('loginScreen').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();
renderRankings();
setTimeout(aiWelcome, 1000);
}
function showProfileScreen() {
document.getElementById('profileScreen').classList.remove('hidden');
document.getElementById('loginScreen').classList.add('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.birth ? '&#x1F382; '+p.birth : '', p.country ? '&#x1F30D; '+p.country+(p.city?'/'+p.city:'') : p.city, 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';
}
// === RANKING ===
function addSwimmerForm() { document.getElementById('swimmer-form').style.display='block'; }
function addCoachForm() { document.getElementById('coach-form').style.display='block'; }
function addSwimmer() {
const name = document.getElementById('sw-name').value.trim();
if (!name) return toast('Введи ФИО!');
const swimmers = LS('swimmers') || [];
swimmers.push({ id: Date.now(), name, club: document.getElementById('sw-club').value.trim(), time: document.getElementById('sw-time').value.trim(), stars: 0, voters: [] });
SS('swimmers', swimmers);
['sw-name','sw-club','sw-time'].forEach(id => document.getElementById(id).value='');
document.getElementById('swimmer-form').style.display='none';
renderRankings();
toast('Спортсмен добавлен!');
}
function addCoach() {
const name = document.getElementById('co-name').value.trim();
if (!name) return toast('Введи ФИО!');
const coaches = LS('coaches') || [];
coaches.push({ id: Date.now(), name, club: document.getElementById('co-club').value.trim(), exp: document.getElementById('co-exp').value.trim(), achieve: document.getElementById('co-achieve').value.trim(), stars: 0, voters: [] });
SS('coaches', coaches);
['co-name','co-club','co-exp','co-achieve'].forEach(id => document.getElementById(id).value='');
document.getElementById('coach-form').style.display='none';
renderRankings();
toast('Тренер добавлен!');
}
function voteSwimmer(id) {
const swimmers = LS('swimmers') || [];
const s = swimmers.find(x => x.id === id);
if (!s) return;
const vid = pid() + '_s_' + id;
if (s.voters.includes(vid)) return toast('Ты уже голосовал!');
s.stars++; s.voters.push(vid);
SS('swimmers', swimmers);
renderRankings();
toast('Голос учтён!');
}
function voteCoach(id) {
const coaches = LS('coaches') || [];
const c = coaches.find(x => x.id === id);
if (!c) return;
const vid = pid() + '_c_' + id;
if (c.voters.includes(vid)) return toast('Ты уже голосовал!');
c.stars++; c.voters.push(vid);
SS('coaches', coaches);
renderRankings();
toast('Голос учтён!');
}
function renderRankings() {
const swimmers = LS('swimmers') || [];
const coaches = LS('coaches') || [];
const swEl = document.getElementById('swimmer-ranking');
if (!swimmers.length) {
swEl.innerHTML = '<div class="empty"><div class="big">&#x1F3CA;</div>Добавьте спортсменов</div>';
} else {
swEl.innerHTML = [...swimmers].sort((a,b)=>b.stars-a.stars).map((s,i)=>`<div class="card" style="padding:14px;margin-bottom:6px"><div style="display:flex;align-items:center;gap:10px"><div style="width:28px;height:28px;border-radius:50%;background:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--gray-100)'};color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex-shrink:0">${i+1}</div><div style="flex:1"><strong>${s.name}</strong><div style="font-size:11px;color:var(--gray-500)">${[s.club,s.time].filter(Boolean).join(' · ')}</div></div><button class="btn small" onclick="voteSwimmer(${s.id})">&#x2B50; ${s.stars}</button></div></div>`).join('');
}
const coEl = document.getElementById('coach-ranking');
if (!coaches.length) {
coEl.innerHTML = '<div class="empty"><div class="big">&#x1F3CB;</div>Добавьте тренеров</div>';
} else {
coEl.innerHTML = [...coaches].sort((a,b)=>b.stars-a.stars).map((c,i)=>`<div class="card" style="padding:14px;margin-bottom:6px"><div style="display:flex;align-items:center;gap:10px"><div style="width:28px;height:28px;border-radius:50%;background:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--gray-100)'};color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex-shrink:0">${i+1}</div><div style="flex:1"><strong>${c.name}</strong><div style="font-size:11px;color:var(--gray-500)">${[c.club,c.exp?c.exp+' лет':null,c.achieve].filter(Boolean).join(' · ')}</div></div><button class="btn small" onclick="voteCoach(${c.id})">&#x2B50; ${c.stars}</button></div></div>`).join('');
}
}
// PWA
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(function(){}); }
// === 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();
renderRankings();
}
// === INIT ===
(function init() {
const profiles = LS('profiles') || [];
renderProfileList();
// Show login screen - user must enter password
document.getElementById('loginScreen').classList.remove('hidden');
document.getElementById('appScreen').classList.remove('active');
document.getElementById('diary-date').value = new Date().toISOString().slice(0,10);
})();
</script>
</body>
</html>