galikon/index.html

1516 lines
97 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">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self'">
<style>
:root{--bg:#0a0a0a;--surface:#141414;--surface2:#1a1a1a;--surface3:#262626;--accent:#FF4D00;--accent2:#FF7A33;--accent-glow:rgba(255,77,0,.3);--white:#fff;--text:#f5f5f5;--gray:#888;--gray2:#444;--red:#FF3B30;--green:#34C759;--gold:#FFD60A;--radius:8px}
*{box-sizing:border-box;margin:0;padding:0}
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--text);background:var(--bg);overflow-x:hidden;-webkit-font-smoothing:antialiased;letter-spacing:-.1px}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
@keyframes slideUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 var(--accent-glow)}50%{box-shadow:0 0 0 12px transparent}}
input,select,textarea{width:100%;padding:15px 16px;border:1.5px solid var(--surface3);border-radius:8px;font:inherit;font-size:16px;background:var(--surface2);color:var(--white);transition:all .2s ease;outline:none}
input:focus,select:focus,textarea:focus{border-color:var(--accent);background:var(--surface)}
input::placeholder{color:var(--gray2)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:15px 28px;border-radius:8px;font-weight:700;border:none;font-size:16px;cursor:pointer;transition:all .25s ease;text-align:center;width:100%;color:var(--white);background:var(--accent);text-transform:uppercase;letter-spacing:.5px;position:relative;overflow:hidden}
.btn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);transition:left .4s}
.btn:hover::before{left:100%}
.btn:hover:not(:active){background:var(--accent2);transform:translateY(-1px);box-shadow:0 8px 24px var(--accent-glow)}
.btn:active{transform:scale(.97)}
.btn.outline{background:transparent;border:2px solid var(--surface3);color:var(--text)}
.btn.outline:hover{background:var(--surface2);border-color:var(--accent);color:var(--accent)}
.btn.small{padding:8px 18px;font-size:12px;width:auto;border-radius:6px}
.btn.danger{background:var(--red)}
.btn.danger:hover{background:#e02d20}
.btn.accent-outline{background:transparent;border:2px solid var(--accent);color:var(--accent)}
.btn.accent-outline:hover{background:rgba(255,77,0,.1)}
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--accent);color:var(--white);padding:14px 32px;border-radius:8px;font-weight:700;font-size:14px;z-index:999;opacity:0;transition:all .3s ease;pointer-events:none;text-transform:uppercase;letter-spacing:.5px}
.toast.show{opacity:1;bottom:32px}
.screen{position:fixed;top:0;left:0;right:0;bottom:0;display:none;flex-direction:column;background:var(--bg);color:var(--text);overflow-y:auto;z-index:10}
.screen.active{display:flex;animation:slideUp .4s ease}
.login-box{max-width:420px;margin:auto;padding:48px 28px;width:100%;text-align:center}
.login-box .logo{font-size:64px;margin-bottom:8px}
.login-box h1{font-size:36px;font-weight:900;margin-bottom:4px;text-transform:uppercase;letter-spacing:1px;color:var(--white)}
.login-box h1 span{color:var(--accent)}
.login-box .sub{color:var(--gray);margin-bottom:32px;font-size:14px;text-transform:uppercase;letter-spacing:1px}
.login-box .error{color:var(--red);font-size:13px;margin-bottom:10px;display:none;padding:10px 14px;background:rgba(255,59,48,.1);border-radius:8px;border:1px solid rgba(255,59,48,.2)}
.reg-step{display:none;flex-direction:column;gap:10px;max-width:420px;margin:auto;padding:28px;width:100%;animation:fadeIn .35s ease}
.reg-step.active{display:flex}
.reg-step h2{font-size:26px;font-weight:900;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px;color:var(--white)}
.reg-step .hint{color:var(--gray);font-size:13px;margin-bottom:24px;text-transform:uppercase;letter-spacing:.5px}
.step-indicator{display:flex;justify-content:center;gap:6px;margin-bottom:24px}
.step-dot{width:32px;height:4px;border-radius:2px;background:var(--surface3);transition:all .4s ease}
.step-dot.done{background:var(--accent)}
.step-dot.current{background:var(--accent);width:48px}
.reg-nav{display:flex;gap:10px;margin-top:12px}
.reg-nav button{flex:1}
.avatar-picker{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin:20px 0}
.avatar-opt{width:60px;height:60px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:30px;cursor:pointer;border:2px solid var(--surface3);transition:all .25s ease;background:var(--surface2)}
.avatar-opt:hover{border-color:var(--gray2)}
.avatar-opt.selected{border-color:var(--accent);background:rgba(255,77,0,.1);box-shadow:0 0 0 4px var(--accent-glow)}
.bottom-nav{display:flex;justify-content:space-around;padding:8px 12px 14px;background:var(--surface);border-top:1px solid var(--surface3);position:sticky;bottom:0;z-index:50}
.nav-item{display:flex;flex-direction:column;align-items:center;gap:4px;padding:4px 12px;border-radius:6px;cursor:pointer;color:var(--gray);font-size:10px;font-weight:700;transition:all .2s ease;border:none;background:none;text-transform:uppercase;letter-spacing:.3px}
.nav-item.active{color:var(--accent)}
.nav-item .icon{font-size:22px;transition:all .2s ease}
.content{flex:1;overflow-y:auto;padding:16px;max-width:640px;margin:0 auto;width:100%}
.card{background:var(--surface);border-radius:var(--radius);padding:20px;margin-bottom:12px;border:1px solid var(--surface3);animation:fadeIn .4s ease;transition:border-color .2s ease}
.card:hover{border-color:var(--surface3)}
.card h3{font-size:16px;font-weight:700;margin-bottom:10px;text-transform:uppercase;letter-spacing:.3px;display:flex;align-items:center;gap:8px}
.card .muted{color:var(--gray);font-size:12px}
.profile-header{text-align:center;padding:32px 16px 20px}
.profile-header .avatar{width:88px;height:88px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#ff8533);color:var(--white);display:flex;align-items:center;justify-content:center;font-size:40px;font-weight:900;margin:0 auto 16px;overflow:hidden;border:3px solid var(--surface)}
.profile-header h2{font-size:24px;font-weight:900;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.profile-header .tag{display:inline-block;background:rgba(255,77,0,.12);color:var(--accent);padding:4px 16px;border-radius:6px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--surface3)}
th{color:var(--gray);font-size:11px;text-transform:uppercase;letter-spacing:.5px;font-weight:700}
.badge-tag{display:inline-block;padding:2px 10px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
.badge-tag.gold{background:rgba(255,214,10,.12);color:var(--gold)}
.badge-tag.blue{background:rgba(255,77,0,.12);color:var(--accent)}
.badge-tag.green{background:rgba(52,199,89,.12);color:var(--green)}
.chat-list-item{display:flex;align-items:center;gap:12px;padding:14px;background:var(--surface2);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:all .2s ease;border:1px solid transparent}
.chat-list-item:hover{border-color:var(--surface3);background:var(--surface)}
.chat-list-item:active{transform:scale(.98)}
.chat-list-item .av{width:44px;height:44px;border-radius:8px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:var(--white);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:18px;flex-shrink:0;overflow:hidden}
.chat-list-item .av img{width:100%;height:100%;object-fit:cover}
.chat-list-item .info{flex:1;min-width:0}
.chat-list-item .name{font-weight:700;font-size:14px}
.chat-list-item .last{font-size:12px;color:var(--gray);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
.chat-list-item .unread{background:var(--accent);color:var(--white);border-radius:6px;padding:2px 8px;font-size:10px;font-weight:800}
.chat-messages{display:flex;flex-direction:column;gap:8px;padding:12px 0;flex:1;overflow-y:auto}
.chat-msg{max-width:80%;padding:12px 16px;border-radius:8px;font-size:14px;line-height:1.45;word-break:break-word;animation:fadeIn .3s ease}
.chat-msg.mine{background:var(--accent);color:var(--white);align-self:flex-end}
.chat-msg.theirs{background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--surface3)}
.chat-msg .sender{font-size:11px;opacity:.6;margin-bottom:3px}
.chat-tabs{display:flex;gap:4px;margin-bottom:14px}
.chat-tab{flex:1;padding:10px;border-radius:6px;border:none;font-size:12px;font-weight:700;cursor:pointer;background:var(--surface2);color:var(--gray);transition:all .2s ease;text-transform:uppercase;letter-spacing:.3px}
.chat-tab.active{background:var(--accent);color:var(--white)}
.game-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;width:200px;margin:16px auto}
.game-cell{aspect-ratio:1;background:var(--surface2);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:900;cursor:pointer;transition:all .2s ease;border:2px solid var(--surface3)}
.game-cell:hover:not(.disabled){border-color:var(--accent);background:var(--surface)}
.game-cell:active{transform:scale(.95)}
.game-cell.x{color:var(--accent)}
.game-cell.o{color:var(--gold)}
.game-cell.disabled{pointer-events:none;opacity:.8}
.game-status{text-align:center;font-size:13px;font-weight:700;margin:10px 0;color:var(--accent);text-transform:uppercase;letter-spacing:.5px}
.quiz-option{display:block;width:100%;padding:14px 16px;margin:8px 0;background:var(--surface2);border:2px solid var(--surface3);border-radius:8px;color:var(--text);font-size:14px;cursor:pointer;text-align:left;transition:all .2s ease}
.quiz-option:hover:not(:disabled){border-color:var(--accent);background:var(--surface)}
.quiz-option.right{background:rgba(52,199,89,.1);border-color:var(--green);color:var(--green)}
.quiz-option.wrong{background:rgba(255,59,48,.1);border-color:var(--red);color:var(--red)}
.empty-state{text-align:center;padding:60px 24px;color:var(--gray)}
.empty-state .big{font-size:64px;margin-bottom:12px;opacity:.4}
.progress-bar{height:4px;background:var(--surface3);border-radius:2px;overflow:hidden;margin-top:8px}
.progress-bar .fill{height:100%;background:var(--accent);border-radius:2px;transition:width .6s ease}
.lightbox{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.95);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:4px}
label.file-btn{display:inline-flex;align-items:center;gap:8px;background:var(--surface2);color:var(--accent);padding:12px 22px;border-radius:6px;font-weight:700;cursor:pointer;font-size:13px;margin:8px 0;transition:all .2s ease;border:2px dashed var(--surface3);text-transform:uppercase;letter-spacing:.5px}
label.file-btn:hover{background:var(--surface);border-color:var(--accent)}
input[type=file]{display:none}
.role-btn{width:100%;padding:18px 20px;background:var(--surface2);border:2px solid var(--surface3);border-radius:8px;color:var(--text);font-size:16px;font-weight:700;cursor:pointer;text-align:left;transition:all .2s ease;display:flex;align-items:center;gap:12px;text-transform:uppercase;letter-spacing:.5px}
.role-btn:hover{border-color:var(--accent);background:var(--surface)}
.role-btn.selected{border-color:var(--accent);background:rgba(255,77,0,.06)}
.role-btn .ri{font-size:28px}
.badge-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
.badge-item{display:flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--surface3);border-radius:6px;padding:8px 14px;font-size:11px;font-weight:700;transition:all .3s ease;text-transform:uppercase;letter-spacing:.3px}
.badge-item .bi{font-size:18px}
.badge-item.earned{border-color:var(--accent);background:rgba(255,77,0,.06)}
.badge-item.locked{opacity:.3;filter:grayscale(1)}
.event-card{background:var(--surface2);border-radius:8px;padding:16px;margin-bottom:10px;border-left:4px solid var(--accent)}
.event-card .ev-date{font-size:11px;color:var(--accent);font-weight:700;text-transform:uppercase;letter-spacing:.5px}
.event-card .ev-title{font-size:15px;font-weight:700;margin:4px 0}
.video-wrap video{width:100%;border-radius:4px;background:#000}
.frame-controls{display:flex;align-items:center;gap:10px;justify-content:center;margin-top:10px}
.frame-controls button{width:42px;height:42px;border-radius:6px;background:var(--surface2);border:1.5px solid var(--surface3);color:var(--text);font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease}
.frame-controls button:hover{background:var(--surface);border-color:var(--accent)}
.frame-counter{font-size:14px;font-weight:700;color:var(--accent);min-width:70px;text-align:center}
.reg-hint{background:rgba(255,77,0,.06);border:1px solid rgba(255,77,0,.15);border-radius:6px;padding:12px 16px;margin-bottom:12px;font-size:12px;color:var(--accent);font-weight:600;text-transform:uppercase;letter-spacing:.3px}
@media print{
body{background:#fff!important;color:#000!important}
.screen{position:static!important;display:block!important;background:#fff!important;color:#000!important}
.bottom-nav,.btn.danger,.btn.outline{display:none!important}
.card{background:#fff!important;border:1px solid #ddd!important;color:#000!important}
}
</style>
</head>
<body>
<!-- LOGIN SCREEN -->
<div class="screen active" id="loginScreen">
<div class="login-box">
<h1>&#x1F3CA; <span>Галикон</span></h1>
<p class="sub">Спорт будущего</p>
<input type="text" id="lUser" placeholder="Логин">
<input type="password" id="lPass" placeholder="Пароль">
<div class="error" id="loginErr"></div>
<button class="btn" onclick="doLogin()">&#x1F512; Войти</button>
<div style="text-align:center;margin-top:12px">
<button class="btn outline" onclick="startReg()" style="width:auto;padding:12px 40px">&#x270F; Регистрация</button>
</div>
<p style="text-align:center;color:var(--gray-500);font-size:12px;margin-top:16px">Нет аккаунта? Создай за 1 минуту</p>
</div>
</div>
<!-- REGISTRATION SCREEN -->
<div class="screen" id="regScreen">
<div style="padding:16px;display:flex;align-items:center;justify-content:space-between">
<button class="btn small outline" onclick="backToLogin()">&#x2190; Назад</button>
<span style="font-weight:700;color:var(--cyan)">Шаг <span id="stepNum">1</span>/8</span>
</div>
<div class="step-indicator" id="stepDots"></div>
<!-- Step 1: Name -->
<div class="reg-step active" data-step="1">
<h2>&#x270F; Как тебя зовут?</h2>
<p class="hint">Введи свои Фамилию, Имя и Отчество</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Напиши ФИО полностью. Например: Кайрат Гали Аскарович.</div>
<input type="text" id="rName" placeholder="Например: Кайрат Гали Аскарович" autofocus>
<div class="reg-nav"><button class="btn" onclick="nextStep()">Дальше &#x2192;</button></div>
</div>
<!-- Step 2: Login -->
<div class="reg-step" data-step="2">
<h2>&#x1F511; Придумай логин и пароль</h2>
<p class="hint">Логин — твоё имя в приложении. Пароль — секрет.</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Логин — латиница, без пробелов. Пароль — минимум 3 символа.</div>
<input type="text" id="rLogin" placeholder="Логин (латиница, без пробелов)">
<input type="password" id="rPass" placeholder="Пароль (минимум 3 символа)" oninput="showPassStrength()"><div id="passStrength" style="font-size:12px;margin-bottom:4px"></div>
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 3: Sport -->
<div class="reg-step" data-step="3">
<h2>&#x1F3CA; Твой вид спорта</h2>
<p class="hint">Выбери из списка олимпийских видов</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Выбери свой вид спорта. В списке — все 39 олимпийских видов.</div>
<select id="rSport"><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>
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 4: Role -->
<div class="reg-step" data-step="4">
<h2>&#x1F464; Кто ты?</h2>
<p class="hint">Выбери свою роль в спорте</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Кто ты? Спортсмен, тренер или родитель? Если родитель — укажи имя ребёнка.</div>
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">
<button class="role-btn" data-role="athlete" onclick="selectRole('athlete')">&#x1F3CA; Спортсмен</button>
<button class="role-btn" data-role="coach" onclick="selectRole('coach')">&#x1F3CB;&#xFE0F; Тренер</button>
<button class="role-btn" data-role="parent" onclick="selectRole('parent')">&#x1F468;&#x200D;&#x1F466; Родитель</button>
</div>
<input type="text" id="rChildName" placeholder="Имя ребёнка" style="display:none">
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 5: Birth & Avatar -->
<div class="reg-step" data-step="5">
<h2>&#x1F382; Дата рождения</h2>
<p class="hint">Возраст посчитается автоматически</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Дата рождения — возраст посчитается сам. Выбери аватарку или загрузи фото.</div>
<input type="date" id="rBirth" onchange="autoAge()">
<input type="text" id="rAge" placeholder="Возраст (авто)" readonly style="background:#0F1218;border-color:#1a2332">
<p style="text-align:center;color:var(--gray-500);font-size:13px;margin-top:8px">&#x1F4F7; Выбери аватарку:</p>
<div class="avatar-picker" id="avatarPicker">
<div class="avatar-opt selected" data-emoji="&#x1F3CA;">&#x1F3CA;</div>
<div class="avatar-opt" data-emoji="&#x1F3C3;">&#x1F3C3;</div>
<div class="avatar-opt" data-emoji="&#x1F3CB;">&#x1F3CB;</div>
<div class="avatar-opt" data-emoji="&#x26BD;">&#x26BD;</div>
<div class="avatar-opt" data-emoji="&#x1F3C0;">&#x1F3C0;</div>
<div class="avatar-opt" data-emoji="&#x1F94A;">&#x1F94A;</div>
<div class="avatar-opt" data-emoji="&#x1F3C8;">&#x1F3C8;</div>
<div class="avatar-opt" data-emoji="&#x1F3BE;">&#x1F3BE;</div>
</div>
<label class="file-btn" style="text-align:center;width:100%">&#x1F4F7; Или загрузи своё фото<input type="file" accept="image/*" id="rPhotoFile" onchange="previewRegPhoto()"></label>
<img id="rPhotoPreview" style="width:64px;height:64px;border-radius:50%;object-fit:cover;display:none;margin:0 auto">
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 6: Location -->
<div class="reg-step" data-step="6">
<h2>&#x1F30D; Где ты?</h2>
<p class="hint">Страна и город</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Сначала выбери страну, потом город.</div>
<select id="rCountry" onchange="updateCities()"><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></select>
<input type="text" id="rCity" placeholder="Город" list="cityList" autocomplete="off">
<datalist id="cityList"></datalist>
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 7: Club & Coach -->
<div class="reg-step" data-step="7">
<h2>&#x1F3EB; Клуб и тренер</h2>
<p class="hint">Где и с кем ты тренируешься</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Укажи клуб, тренера и разряд. Цель — твоя главная мечта.</div>
<input type="text" id="rClub" placeholder="Клуб / спортивная школа">
<input type="text" id="rCoach" placeholder="Тренер (ФИО)">
<input type="text" id="rRank" placeholder="Разряд / звание (например: 1 юн, 3 взр)">
<input type="text" id="rGoal" placeholder="Твоя главная цель (например: 50 м в/с за 23″)">
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 8: Contacts -->
<div class="reg-step" data-step="8">
<h2>&#x1F4F1; Контакты</h2>
<p class="hint">Телефон и email — чтобы тренер и родители могли связаться</p>
<div style="background:#1a2332;border:1px solid var(--cyan);border-radius:10px;padding:10px 14px;margin-bottom:8px;font-size:13px;color:var(--cyan)">&#x1F916; <b>Подсказка:</b> Телефон ОБЯЗАТЕЛЕН (формат +77011234567). Email нужен для связи.</div>
<input type="tel" id="rPhone" placeholder="+7 (___) ___-__-__" required>
<input type="email" id="rEmail" placeholder="Email" required>
<div class="reg-nav">
<button class="btn outline" onclick="prevStep()">&#x2190; Назад</button>
<button class="btn" onclick="finishReg()">&#x2705; Завершить!</button>
</div>
</div>
</div>
<!-- MAIN APP SCREEN -->
<div class="screen" id="appScreen">
<div class="content" id="mainContent"></div>
<div class="bottom-nav" id="bottomNav"></div>
</div>
<div class="toast" id="toast"></div>
<div class="lightbox" id="lightbox" style="display:none" onclick="this.style.display='none'"></div>
<script>
// === SECURITY ===
async function hashPass(pass) {
const encoder = new TextEncoder();
const data = encoder.encode(pass + 'galikon_salt_2026');
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,'0')).join('');
}
// Session timeout (30 min inactivity)
let sessionTimer;
function resetSession() {
if(!currentUser) return;
clearTimeout(sessionTimer);
sessionTimer = setTimeout(() => {
if(currentUser) { toast('&#x23F0; Сессия истекла. Войди заново.'); doLogout(); }
}, 30 * 60 * 1000);
}
document.addEventListener('click', resetSession);
document.addEventListener('keydown', resetSession);
document.addEventListener('touchstart', resetSession);
// Login attempt rate limiting
let loginAttempts = 0, loginBlocked = false;
function checkLoginRate() {
if(loginBlocked) { showErr('Слишком много попыток. Подожди 1 минуту.'); return false; }
loginAttempts++;
if(loginAttempts >= 5) {
loginBlocked = true;
showErr('5 неудачных попыток. Подожди 1 минуту.');
setTimeout(() => { loginBlocked = false; loginAttempts = 0; hideErr(); }, 60000);
return false;
}
return true;
}
// Sanitize text to prevent XSS
function sanitize(str) {
if(!str) return '';
return String(str).replace(/[<>"'&]/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','&':'&amp;'}[c]));
}
// Encrypt sensitive data in localStorage
async function encryptData(data) {
const json = JSON.stringify(data);
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey('raw', encoder.encode('galikon_key_2026'), {name:'AES-GCM'}, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt({name:'AES-GCM', iv}, key, encoder.encode(json));
return { iv: Array.from(iv), data: Array.from(new Uint8Array(encrypted)) };
}
// Check password strength
function checkPassStrength(pass) {
if(!pass || pass.length < 3) return { ok: false, msg: 'Минимум 3 символа' };
let score = 0;
if(pass.length >= 6) score++;
if(pass.length >= 10) score++;
if(/[A-Z]/.test(pass)) score++;
if(/[0-9]/.test(pass)) score++;
if(/[!@#$%^&*]/.test(pass)) score++;
if(score <= 1) return { ok: true, msg: '&#x1F7E1; Слабый пароль', color: '#FFD700' };
if(score <= 3) return { ok: true, msg: '&#x1F7E2; Средний пароль', color: '#4CAF50' };
return { ok: true, msg: '&#x1F7E2;&#x1F7E2;&#x1F7E2; Сильный пароль', color: '#4CAF50' };
}
// === ADMIN ===
const ADMIN_HASH = "1d7623489c0b8ae06d079d7d64b5e414e7132d0041ff08e1272c5b8a2112685c";
function isAdmin() { return currentUser && currentUser.login === 'admin'; }
async function adminCheck() {
const pass = prompt('Введи пароль администратора:');
if(!pass) return false;
const encoder = new TextEncoder();
const data = encoder.encode(pass + 'galikon_salt_2026');
const hash = await crypto.subtle.digest('SHA-256', data);
const hashStr = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,'0')).join('');
return hashStr === ADMIN_HASH;
}
async function showAdminPanel() {
if(!await adminCheck()) { toast('Неверный пароль!'); return; }
const users = LS('users') || [];
const c = document.getElementById('mainContent');
let h = `<div class="card"><h3>&#x1F6E1; Админ-панель</h3>
<p class="muted">Пользователей: <strong>${users.length}</strong></p></div>`;
users.forEach(u => {
h += `<div class="card" style="padding:14px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<strong>${sanitize(u.name)}</strong> <span class="badge blue">${u.role||'спортсмен'}</span>
<div class="muted">Логин: ${sanitize(u.login)} | ${u.sport||'—'} | ${u.club||'—'} | &#x2B50; ${u.stars||0}</div>
</div>
<div>
<button class="btn danger small" onclick="adminDeleteUser(${u.id})">&#x1F5D1;</button>
<button class="btn small outline" onclick="adminResetPass(${u.id})">&#x1F512;</button>
</div>
</div>
</div>`;
});
h += '<button class="btn small outline" onclick="showPage(currentPage)" style="margin-top:8px">&#x2190; Назад</button>';
c.innerHTML = h;
}
async function adminDeleteUser(id) {
if(!await adminCheck()) return;
const users = LS('users') || [];
const updated = users.filter(u => u.id !== id);
SS('users', updated);
if(currentUser && currentUser.id === id) doLogout();
showAdminPanel();
toast('Пользователь удалён');
}
async function adminResetPass(id) {
if(!await adminCheck()) return;
const newPass = prompt('Новый пароль:');
if(!newPass||newPass.length<3) { toast('Минимум 3 символа!'); return; }
const users = LS('users') || [];
const idx = users.findIndex(u => u.id === id);
if(idx < 0) return;
const encoder = new TextEncoder();
const data = encoder.encode(newPass + 'galikon_salt_2026');
const hash = await crypto.subtle.digest('SHA-256', data);
users[idx].pass = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2,'0')).join('');
SS('users', users);
toast('Пароль сброшен! Новый: ' + newPass);
}
// Add admin button to login screen
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const loginBox = document.querySelector('#loginScreen .login-box');
if(!loginBox) return;
const adminBtn = document.createElement('button');
adminBtn.className = 'btn small outline';
adminBtn.textContent = '&#x1F6E1; Админ';
adminBtn.style.cssText = 'margin-top:8px;width:100%;font-size:11px;opacity:0.5';
adminBtn.onclick = async () => {
if(await adminCheck()) {
hideErr();
currentUser = { id: '_admin_', login: 'admin', name: 'Администратор', role: 'admin' };
document.getElementById('loginScreen').classList.remove('active');
document.getElementById('appScreen').classList.add('active');
renderAll();
toast('&#x1F6E1; Режим администратора');
}
};
loginBox.appendChild(adminBtn);
}, 100);
});
// === DATA ===
const LS=(k)=>{try{return JSON.parse(localStorage.getItem('g_'+k))}catch{return null}};
const SS=(k,v)=>{try{localStorage.setItem('g_'+k,JSON.stringify(v))}catch{toast('Память полна!')}};
let currentUser=null, currentPage='profile', currentChat=null, chatFilter='all';
// Show AI assistant on login
setTimeout(() => {
const ai = document.getElementById('aiAppHelper');
if (ai) ai.classList.add('show');
}, 1500);
function toast(m){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
// === LOGIN ===
async function doLogin(){
if(!checkLoginRate()) return;
const u=sanitize(document.getElementById('lUser').value.trim().toLowerCase());
const p=document.getElementById('lPass').value;
if(!u||!p){showErr('Введи логин и пароль');return}
const users=LS('users')||[];
const hash=await hashPass(p);
const user=users.find(x=>x.login===u&&(x.pass===hash||x.pass===p));
if(!user){showErr('Неверный логин или пароль');return}
if(!user.role) user.role='athlete';
currentUser=user;
resetSession();
currentPage='profile';
document.getElementById('loginScreen').classList.remove('active');
document.getElementById('appScreen').classList.add('active');
hideErr();
renderAll();
}
function showErr(m){const e=document.getElementById('loginErr');e.textContent=m;e.style.display='block'}
function hideErr(){document.getElementById('loginErr').style.display='none'}
// === REGISTRATION ===
let regStep=1, regPhoto=null, regAvatar='&#x1F3CA;', regRole=null;
function startReg(){
document.getElementById('loginScreen').classList.remove('active');
document.getElementById('regScreen').classList.add('active');
regStep=1; regRole=null; showRegStep(1);
buildStepDots();
}
function backToLogin(){
document.getElementById('regScreen').classList.remove('active');
document.getElementById('loginScreen').classList.add('active');
// Show AI helper on login
}
function showRegStep(n){
regStep=n;
document.querySelectorAll('.reg-step').forEach(s=>s.classList.remove('active'));
const step=document.querySelector(`.reg-step[data-step="${n}"]`);
if(step){step.classList.add('active');step.querySelector('input,select')?.focus()}
document.getElementById('stepNum').textContent=n;
updateStepDots();
}
function buildStepDots(){
let h=''; for(let i=1;i<=8;i++)h+=`<div class="step-dot${i<=regStep?' done':''}${i===regStep?' current':''}"></div>`;
document.getElementById('stepDots').innerHTML=h;
}
function updateStepDots(){
document.querySelectorAll('#stepDots .step-dot').forEach((d,i)=>{
d.classList.toggle('done',i+1<=regStep);
d.classList.toggle('current',i+1===regStep);
});
}
function selectRole(role){
regRole=role;
document.querySelectorAll('.role-btn').forEach(b=>b.classList.toggle('selected',b.dataset.role===role));
const childEl=document.getElementById('rChildName');
childEl.style.display=role==='parent'?'block':'none';
if(role==='parent') childEl.focus();
}
function nextStep(){
if(regStep===1&&!document.getElementById('rName').value.trim()){toast('Введи ФИО!');return}
if(regStep===2){const l=document.getElementById('rLogin').value.trim();const p=document.getElementById('rPass').value;if(!l){toast('Придумай логин!');return}if(!p||p.length<3){toast('Пароль — минимум 3 символа!');return}const users=LS('users')||[];if(users.find(x=>x.login===l.toLowerCase())){toast('Такой логин уже занят!');return}}
if(regStep===3&&!document.getElementById('rSport').value){toast('Выбери вид спорта!');return}
if(regStep===4){if(!regRole){toast('Выбери свою роль!');return}if(regRole==='parent'&&!document.getElementById('rChildName').value.trim()){toast('Введи имя ребёнка!');return}}
if(regStep===8){const ph=document.getElementById('rPhone').value.trim();if(!ph){toast('Введи номер телефона!');return}if(!/^[+\d][\d\s\-()]{6,18}$/.test(ph)){toast('Неверный формат телефона! Пример: +77011234567');return}const em=document.getElementById('rEmail').value.trim();if(!em||!em.includes('@')){toast('Введи правильный email!');return}}
if(regStep<8){buildStepDots();showRegStep(regStep+1)}else finishReg()
}
function prevStep(){if(regStep>1)showRegStep(regStep-1);buildStepDots()}
function showPassStrength(){
const s = checkPassStrength(document.getElementById('rPass').value);
const el = document.getElementById('passStrength');
if(s.msg) el.innerHTML = s.msg;
else el.innerHTML = '';
}
function autoAge(){const b=document.getElementById('rBirth').value;if(!b)return;const bd=new Date(b),td=new Date();let a=td.getFullYear()-bd.getFullYear();const m=td.getMonth()-bd.getMonth();if(m<0||(m===0&&td.getDate()<bd.getDate()))a--;document.getElementById('rAge').value=a}
function previewRegPhoto(){const f=document.getElementById('rPhotoFile').files[0];if(!f)return;const r=new FileReader();r.onload=e=>{regPhoto=e.target.result;const p=document.getElementById('rPhotoPreview');p.src=e.target.result;p.style.display='block'};r.readAsDataURL(f)}
document.querySelectorAll('.avatar-opt').forEach(a=>{a.addEventListener('click',()=>{document.querySelectorAll('.avatar-opt').forEach(x=>x.classList.remove('selected'));a.classList.add('selected');regAvatar=a.dataset.emoji;regPhoto=null;document.getElementById('rPhotoPreview').style.display='none'})});
async function finishReg(){
const name=document.getElementById('rName').value.trim();
const login=document.getElementById('rLogin').value.trim().toLowerCase();
const pass=document.getElementById('rPass').value;
const strength=checkPassStrength(pass);
if(!strength.ok){toast(strength.msg);return}
const sport=document.getElementById('rSport').value;
const birth=document.getElementById('rBirth').value;
autoAge();
const age=document.getElementById('rAge').value;
const country=document.getElementById('rCountry').value;
const city=document.getElementById('rCity').value.trim();
const club=document.getElementById('rClub').value.trim();
const coach=document.getElementById('rCoach').value.trim();
const rank=document.getElementById('rRank').value.trim();
const goal=document.getElementById('rGoal').value.trim();
const phone=document.getElementById('rPhone').value.trim();
const email=document.getElementById('rEmail').value.trim();
const childName=document.getElementById('rChildName').value.trim();
if(!name||!login||!pass||!sport||!regRole){toast('Заполни обязательные поля!');return}
const user={
id:Date.now(),name,login,pass: await hashPass(pass),sport,birth,age,country,city,club,coach,rank,goal,phone,email,
role:regRole,childName:childName||'',
avatar:regPhoto||regAvatar,photo:regPhoto||null,
created:new Date().toISOString(),
achievements:[]
};
const users=LS('users')||[];
if(users.find(x=>x.login===login)){toast('Логин занят!');return}
users.push(user);
SS('users',users);
currentUser=user;
resetSession();
document.getElementById('regScreen').classList.remove('active');
document.getElementById('appScreen').classList.add('active');
// reset form
regStep=1;regPhoto=null;regAvatar='&#x1F3CA;';regRole=null;
['rName','rLogin','rPass','rSport','rBirth','rAge','rCountry','rCity','rClub','rCoach','rRank','rGoal','rPhone','rEmail','rChildName'].forEach(id=>{const el=document.getElementById(id);if(el){if(el.tagName==='SELECT')el.selectedIndex=0;else el.value=''}});
document.getElementById('rPhotoPreview').style.display='none';
document.getElementById('rChildName').style.display='none';
document.querySelectorAll('.role-btn').forEach(b=>b.classList.remove('selected'));
renderAll();
toast('&#x1F389; Профиль создан! Добро пожаловать в Галикон!');
}
// === USER STORAGE ===
function uid(){return currentUser?currentUser.id:'_'}
function getMy(k){const all=LS(k)||{};return all[uid()]||null}
function setMy(k,v){const all=LS(k)||{};all[uid()]=v;SS(k,all)}
function getMyArr(k){const all=LS(k)||{};return all[uid()]||[]}
function setMyArr(k,v){const all=LS(k)||{};all[uid()]=v;SS(k,all)}
function getArrFor(k,id){const all=LS(k)||{};return all[id]||[]}
function getValFor(k,id){const all=LS(k)||{};return all[id]||null}
// === BOTTOM NAV ===
function renderBottomNav(){
const nav=document.getElementById('bottomNav');
const role=currentUser?currentUser.role||'athlete':'athlete';
let items=[];
if(role==='coach'){
items=[
{page:'profile',icon:'&#x1F464;',label:'Профиль'},
{page:'students',icon:'&#x1F465;',label:'Ученики'},
{page:'chat',icon:'&#x1F4AC;',label:'Чаты'},
{page:'tools',icon:'&#x2699;',label:'Инструменты'}
];
}else if(role==='parent'){
items=[
{page:'profile',icon:'&#x1F464;',label:'Профиль'},
{page:'child',icon:'&#x1F476;',label:'Ребёнок'},
{page:'chat',icon:'&#x1F4AC;',label:'Чаты'},
{page:'tools',icon:'&#x2699;',label:'Инструменты'}
];
}else{
items=[
{page:'profile',icon:'&#x1F464;',label:'Профиль'},
{page:'diary',icon:'&#x1F4D6;',label:'Дневник'},
{page:'calendar',icon:'&#x1F4C5;',label:'Календарь'},
{page:'chat',icon:'&#x1F4AC;',label:'Чаты'},
{page:'tools',icon:'&#x2699;',label:'Инструменты'}
];
}
nav.innerHTML=items.map(it=>`<button class="nav-item${currentPage===it.page?' active':''}" data-page="${it.page}" onclick="showPage('${it.page}')"><span class="icon">${it.icon}</span>${it.label}</button>`).join('');
}
// === PAGES ===
function showPage(page){
currentPage=page;
document.querySelectorAll('.nav-item').forEach(n=>n.classList.toggle('active',n.dataset.page===page));
renderPage();
}
function renderPage(){
if(isAdmin()){
const c=document.getElementById('mainContent');
switch(currentPage){
case 'profile': showAdminPanel(); return;
case 'diary': showAdminPanel(); return;
case 'chat': c.innerHTML='<div class="card"><h3>&#x1F6E1; Администратор</h3><p class="muted">Чаты доступны только пользователям.</p><button class="btn small outline" onclick="showAdminPanel()">&#x2190; Панель</button></div>'; return;
default: showAdminPanel(); return;
}
}
const c=document.getElementById('mainContent');
switch(currentPage){
case 'profile':c.innerHTML=renderProfile();break;
case 'diary':c.innerHTML=renderDiaryPage();break;
case 'calendar':c.innerHTML=renderCalendarPage();break;
case 'students':c.innerHTML=renderStudentsPage();break;
case 'child':c.innerHTML=renderChildPage();break;
case 'chat':c.innerHTML=renderChatPage();currentChat=null;break;
case 'tools':c.innerHTML=renderToolsPage();break;
}
}
// === PROFILE ===
function computeBadges(forId){
const diary=getArrFor('diary',forId);
const user=((LS('users')||[]).find(u=>u.id===forId)||{});
const reactBest=getValFor('reactionBest',forId)||9999;
const totalKm=diary.reduce((s,e)=>s+(+e.km||0),0);
return [
{icon:'&#x1F3CA;',name:'Первая тренировка',cond:diary.length>=1},
{icon:'&#x1F4D6;',name:'10 тренировок',cond:diary.length>=10},
{icon:'&#x1F525;',name:'30 дней без пропуска',cond:diary.length>=30},
{icon:'&#x1F3C6;',name:'Рекордсмен',cond:!!(user.achievements&&user.achievements.length)},
{icon:'&#x1F3AF;',name:'Снайпер',cond:!!(user.guessWon&&user.guessWon>0)},
{icon:'&#x26A1;',name:'Молния',cond:reactBest<300},
{icon:'&#x1F9E0;',name:'Знаток',cond:!!(user.quizScore&&user.quizScore>=5)},
{icon:'&#x1F4AA;',name:'100 км',cond:totalKm>100}
];
}
function renderProfile(){
const u=currentUser;
const av=u.photo?(u.photo.startsWith('data:')?u.photo:u.avatar):u.avatar;
const badges=computeBadges(u.id);
const earnedCount=badges.filter(b=>b.cond).length;
const roleLabel=u.role==='coach'?'&#x1F3CB;&#xFE0F; Тренер':u.role==='parent'?'&#x1F468;&#x200D;&#x1F466; Родитель'+(u.childName?': '+u.childName:''):'&#x1F3CA; Спортсмен';
const h=`
<div class="profile-header">
<div class="avatar">${u.photo?`<img src="${u.photo}">`:av}</div>
<h2>${sanitize(u.name)}</h2>
<div class="tag">${u.sport}</div>
<div style="margin-top:4px;font-size:13px;color:var(--cyan)">${roleLabel}</div>
${u.rank?`<div style="margin-top:4px;font-size:13px;color:var(--gray-500)">${u.rank}</div>`:''}
${u.goal?`<div style="margin-top:4px;font-size:14px;color:var(--cyan)">&#x1F3AF; ${u.goal}</div>`:''}
</div>
<div class="card">
<h3>&#x1F3C5; Значки (${earnedCount}/${badges.length})</h3>
<div class="badge-row">${badges.map(b=>`<div class="badge-item${b.cond?' earned':' locked'}" title="${b.cond?'Получен: '+b.name:'Не получен: '+b.name}"><span class="bi">${b.icon}</span>${b.name}</div>`).join('')}</div>
</div>
<div class="card">
<h3>&#x1F4CB; Информация</h3>
<div style="font-size:14px;line-height:2">
${u.birth?`&#x1F382; ${u.birth} (${u.age||'?'} лет)<br>`:''}
${u.country?`&#x1F30D; ${u.country}${u.city?', '+u.city:''}<br>`:''}
${u.club?`&#x1F3EB; ${u.club}<br>`:''}
${u.coach?`&#x1F468;&#x200D;&#x1F3EB; Тренер: ${u.coach}<br>`:''}
${u.phone?`&#x1F4F1; ${u.phone}<br>`:''}
${u.email?`&#x2709;&#xFE0F; ${u.email}<br>`:''}
</div>
</div>
<div class="card">
<h3>&#x1F3C6; Достижения</h3>
${u.achievements&&u.achievements.length?u.achievements.map(a=>`<div style="padding:8px 0;border-bottom:1px solid #2a3342;font-size:14px"><strong>${a.title}</strong><br><span style="color:var(--gray-500);font-size:13px">${a.date} · ${a.desc||''}</span></div>`).join(''):'<p class="muted">Пока нет достижений. Добавь первое!</p>'}
<button class="btn small outline" onclick="addAchievement()" style="margin-top:8px">+ Добавить</button>
<div id="achForm" style="display:none;margin-top:8px">
<input type="text" id="achTitle" placeholder="Название (например: 1 место на Весёлый Дельфин)">
<input type="date" id="achDate">
<input type="text" id="achDesc" placeholder="Описание (результат, дистанция...)">
<button class="btn small" onclick="saveAchievement()">Сохранить</button>
</div>
</div>
<button class="btn small" onclick="openPdfReport()" style="margin-top:8px">&#x1F4C4; Отчёт для тренера</button>
<button class="btn accent-outline small" onclick="openMotivationReport()" style="margin-top:4px">&#x1F4CA; Мотивационный отчёт</button>
<button class="btn danger" onclick="doLogout()" style="margin-top:8px">&#x1F6AA; Выйти</button>
`;return h;
}
function addAchievement(){document.getElementById('achForm').style.display='block'}
function saveAchievement(){
const t=document.getElementById('achTitle').value.trim();
const d=document.getElementById('achDate').value;
const desc=document.getElementById('achDesc').value.trim();
if(!t||!d){toast('Введи название и дату!');return}
const u=currentUser;
if(!u.achievements)u.achievements=[];
u.achievements.unshift({title:t,date:d,desc});
const users=LS('users')||[];
const idx=users.findIndex(x=>x.id===u.id);
if(idx>=0){users[idx]=u;SS('users',users);currentUser=u}
document.getElementById('achTitle').value='';document.getElementById('achDate').value='';document.getElementById('achDesc').value='';
document.getElementById('achForm').style.display='none';
renderPage();toast('Достижение добавлено!');
}
// === MOTIVATION REPORT ===
function openMotivationReport(){
const u = currentUser;
const diary = getMyArr('diary');
const badges = computeBadges(u.id).filter(b => b.cond);
const totalKm = diary.reduce((s,e) => s + (+e.km || 0), 0);
const totalWorkouts = diary.length;
const avgFeel = diary.length ? (diary.reduce((s,e)=>s+(+e.feel||0),0)/diary.length).toFixed(1) : '—';
// Generate personalized recommendations
let recs = [];
if (totalWorkouts < 5) recs.push('&#x1F525; Ты только начинаешь! Самое сложное позади — первый шаг сделан. Теперь главное — регулярность. 3 тренировки в неделю = привычка.');
else if (totalWorkouts < 20) recs.push('&#x1F4C8; Отличный старт! Ты уже в ритме. Добавь одну дополнительную тренировку в неделю — прогресс ускорится.');
else recs.push('&#x1F525; Ты настоящий боец! ' + totalWorkouts + ' тренировок — это серьёзная работа. Гордись собой.');
if (totalKm > 50) recs.push('&#x1F30A; ' + totalKm.toFixed(0) + ' км в воде! Ты пересёк Ла-Манш ' + (totalKm/34).toFixed(1) + ' раз. Впечатляет!');
if (avgFeel !== '—' && +avgFeel >= 4) recs.push('&#x1F4AA; Твоё самочувствие на высоте — ' + avgFeel + '/5. Организм справляется с нагрузкой отлично.');
else if (avgFeel !== '—' && +avgFeel < 3.5) recs.push('&#x1F4A4; Самочувствие ' + avgFeel + '/5. Попробуй спать на час больше — восстановление улучшится.');
if (badges.length >= 5) recs.push('&#x1F3C5; ' + badges.length + ' значков! Ты собираешь их как чемпион. Продолжай!');
else recs.push('&#x1F3AF; У тебя ' + badges.length + ' значков. Следующая цель — получить ещё один. Попробуй викторину!');
recs.push('&#x1F3AF; Каждая тренировка приближает тебя к цели. Помни: чемпионами не рождаются — ими становятся каждый день.');
recs.push('&#x1F4DA; Смотри обучающие видео в разделе Инструменты. Техника — это скорость.');
if (u.sport === 'Плавание' || !u.sport) {
recs.push('&#x1F3CA; Работай над стартом и поворотом — на 50 м они дают до 30% времени. 10 стартов каждую тренировку.');
recs.push('&#x23F1; Засекай время каждую неделю. Прогресс в 0.1 секунды — это шаг к мечте.');
}
const w = window.open('', '_blank', 'width=800,height=700');
w.document.write('<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Мотивационный отчёт — ' + u.name + '</title><style>body{font:15px/1.7 Arial,sans-serif;max-width:750px;margin:30px auto;padding:20px;color:#1a1a1a;background:linear-gradient(180deg,#fff 0%,#f9fafb 100%)}h1{font-size:28px;border-bottom:3px solid #FF4D00;padding-bottom:10px;margin-bottom:0}h1 span{color:#FF4D00}h2{font-size:18px;margin-top:24px;color:#FF4D00;border-left:4px solid #FF4D00;padding-left:12px}.stats{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}.stat{background:linear-gradient(135deg,#fff5f0,#fff);border:1px solid #fdd;border-radius:12px;padding:16px 20px;min-width:100px;text-align:center}.stat .num{font-size:28px;font-weight:800;color:#FF4D00}.stat .lbl{font-size:11px;color:#888;text-transform:uppercase;letter-spacing:.5px;margin-top:4px}.rec{background:#fff;border:1px solid #eee;border-radius:10px;padding:14px 18px;margin:8px 0;border-left:3px solid #FF4D00;font-size:14px}.moto{background:linear-gradient(135deg,#FF4D00,#ff6b33);color:#fff;padding:20px 24px;border-radius:14px;margin:20px 0;font-size:18px;font-weight:700;text-align:center}.badge-tag{display:inline-block;padding:3px 10px;background:#fff5f0;border:1px solid #fdd;border-radius:6px;margin:2px;font-size:12px}@media print{body{background:#fff!important;margin:0;padding:10px}}</style></head><body>');
w.document.write('<h1>&#x1F3CA; Мотивационный отчёт<br><span>' + u.name + '</span></h1>');
w.document.write('<p style="color:#666;font-size:14px">' + u.sport + (u.club ? ' · ' + u.club : '') + (u.rank ? ' · ' + u.rank : '') + '</p>');
w.document.write('<div class="stats">');
w.document.write('<div class="stat"><div class="num">' + totalWorkouts + '</div><div class="lbl">Тренировок</div></div>');
w.document.write('<div class="stat"><div class="num">' + totalKm.toFixed(1) + '</div><div class="lbl">Км в воде</div></div>');
w.document.write('<div class="stat"><div class="num">' + avgFeel + '</div><div class="lbl">Самочувствие /5</div></div>');
w.document.write('<div class="stat"><div class="num">' + badges.length + '</div><div class="lbl">Значков</div></div>');
w.document.write('</div>');
if (badges.length) {
w.document.write('<p>Значки: ' + badges.map(b => '<span class="badge-tag">' + b.icon + ' ' + b.name + '</span>').join(' ') + '</p>');
}
if (u.goal) {
w.document.write('<div class="moto">&#x1F3AF; Твоя цель: ' + u.goal + '</div>');
}
w.document.write('<h2>&#x1F4DD; Персональные рекомендации</h2>');
recs.forEach(r => w.document.write('<div class="rec">' + r + '</div>'));
w.document.write('<div class="moto">Помни: ты уже на ' + (totalWorkouts || 1) + ' тренировок впереди тех, кто остался на диване. Не останавливайся. Твой главный соперник — ты вчерашний.</div>');
w.document.write('<p style="text-align:center;color:#999;font-size:12px;margin-top:20px">Отчёт сгенерирован в Галиконе · ' + new Date().toLocaleDateString('ru-RU') + '</p>');
w.document.write('</body></html>');
w.document.close();
}
// === PDF REPORT ===
function openPdfReport(){
const u=currentUser;
const diary=getMyArr('diary').slice(0,10);
const badges=computeBadges(u.id).filter(b=>b.cond);
const w=window.open('','_blank','width=800,height=600');
w.document.write('<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Отчёт — '+u.name+'</title><style>body{font:14px/1.6 Arial,sans-serif;max-width:700px;margin:40px auto;padding:20px;color:#000}h1{font-size:24px;border-bottom:2px solid #00E5FF;padding-bottom:8px}h2{font-size:18px;margin-top:20px;color:#333}table{width:100%;border-collapse:collapse;margin:8px 0}th,td{padding:6px 8px;border:1px solid #ccc;font-size:13px;text-align:left}th{background:#f0f0f0}.badge-tag{display:inline-block;padding:3px 10px;background:#E8FCFF;border-radius:8px;margin:2px;font-size:12px}.muted{color:#666;font-size:12px}@media print{body{margin:0;padding:10px}}</style></head><body>');
w.document.write('<h1>🏊 Галикон — Отчёт спортсмена</h1>');
w.document.write('<p><strong>Имя:</strong> '+u.name+'</p>');
w.document.write('<p><strong>Вид спорта:</strong> '+u.sport+'</p>');
if(u.club) w.document.write('<p><strong>Клуб:</strong> '+u.club+'</p>');
if(u.coach) w.document.write('<p><strong>Тренер:</strong> '+u.coach+'</p>');
if(u.rank) w.document.write('<p><strong>Разряд:</strong> '+u.rank+'</p>');
if(u.goal) w.document.write('<p><strong>Цель:</strong> '+u.goal+'</p>');
if(u.birth) w.document.write('<p><strong>Дата рождения:</strong> '+u.birth+' ('+(u.age||'?')+' лет)</p>');
w.document.write('<h2>🏅 Значки</h2>');
w.document.write('<p>'+(badges.length?badges.map(b=>'<span class="badge-tag">'+b.icon+' '+b.name+'</span>').join(' '):'<span class="muted">Нет значков</span>')+'</p>');
w.document.write('<h2>🏆 Достижения</h2>');
if(u.achievements&&u.achievements.length){
u.achievements.forEach(a=>{w.document.write('<p><strong>'+a.title+'</strong><br><span class="muted">'+a.date+' · '+(a.desc||'')+'</span></p>');});
}else{w.document.write('<p class="muted">Нет достижений</p>');}
w.document.write('<h2>📖 Последние записи в дневнике</h2>');
if(diary.length){
w.document.write('<table><tr><th>Дата</th><th>Тип</th><th>Км</th><th>Время</th><th>Самочувствие</th></tr>');
diary.forEach(e=>{w.document.write('<tr><td>'+e.date+'</td><td>'+e.type+'</td><td>'+e.km+'</td><td>'+e.time+'</td><td>'+e.feel+'/5</td></tr>');});
w.document.write('</table>');
}else{w.document.write('<p class="muted">Нет записей</p>');}
w.document.write('<p class="muted" style="margin-top:30px">Отчёт сгенерирован в Галиконе — '+new Date().toLocaleDateString('ru-RU')+'</p></body></html>');
w.document.close();
}
// === DIARY ===
function renderDiaryPage(){
const diary=getMyArr('diary');
const today=new Date().toISOString().slice(0,10);
let h=`
<div class="card">
<h3>&#x270F; Новая запись</h3>
<input type="date" id="dDate" value="${today}">
<select id="dType"><option>Скорость</option><option>Техника</option><option>Выносливость</option><option>ОФП</option><option>Соревнование</option></select>
<input type="number" id="dKm" placeholder="Километраж" step="0.1">
<input type="text" id="dTime" placeholder="Лучшее время">
<input type="number" id="dFeel" placeholder="Самочувствие (1-5)" min="1" max="5">
<input type="text" id="dNote" placeholder="Заметка">
<button class="btn" onclick="addDiary()">Сохранить</button>
</div>`;
if(!diary.length){h+='<div class="empty-state"><div class="big">&#x1F4AD;</div>Пока нет записей</div>'}
else{
h+=diary.slice(0,20).map(e=>`
<div class="card" style="padding:14px">
<div style="display:flex;justify-content:space-between"><strong>${e.date}</strong><span class="badge blue">${e.type}</span></div>
<div class="muted">&#x1F4CF; ${e.km} км | &#x23F1; ${e.time} | &#x1F31F; ${e.feel}/5${e.note!=='—'?'<br>'+e.note:''}</div>
<button class="btn danger small" style="margin-top:4px" onclick="delDiary(${e.id})">Удалить</button>
</div>`).join('');
}
return h;
}
function addDiary(){
const e={id:Date.now(),date:document.getElementById('dDate').value,type:document.getElementById('dType').value,km:document.getElementById('dKm').value||'0',time:document.getElementById('dTime').value||'—',feel:document.getElementById('dFeel').value||'—',note:document.getElementById('dNote').value||'—'};
const d=getMyArr('diary');d.unshift(e);setMyArr('diary',d);
renderPage();toast('Записано!');
}
function delDiary(id){const d=getMyArr('diary').filter(x=>x.id!==id);setMyArr('diary',d);renderPage();toast('Удалено')}
// === CALENDAR ===
function renderCalendarPage(){
const events=getMyArr('events');
const now=new Date().toISOString().slice(0,10);
const upcoming=events.filter(e=>e.date>=now).sort((a,b)=>a.date.localeCompare(b.date));
const past=events.filter(e=>e.date<now).sort((a,b)=>b.date.localeCompare(a.date));
let h=`
<div class="card">
<h3>&#x2795; Добавить событие</h3>
<input type="text" id="evTitle" placeholder="Название соревнования">
<input type="date" id="evDate" value="${now}">
<input type="text" id="evLoc" placeholder="Место проведения">
<button class="btn" onclick="addEvent()">Добавить</button>
</div>`;
if(upcoming.length){
h+='<h3 style="color:var(--cyan);margin:16px 0 8px">&#x1F4C5; Предстоящие</h3>';
upcoming.forEach(e=>{h+=`<div class="event-card"><div class="ev-date">${e.date}</div><div class="ev-title">${e.title}</div><div class="ev-loc">${e.location||'—'}</div><button class="btn danger small" style="margin-top:4px" onclick="delEvent(${e.id})">Удалить</button></div>`});
}else{h+='<div class="empty-state"><div class="big">&#x1F4C5;</div>Нет предстоящих событий</div>'}
if(past.length){
h+='<h3 style="color:var(--gray-500);margin:16px 0 8px">&#x2705; Прошедшие</h3>';
past.forEach(e=>{h+=`<div class="event-card" style="border-left-color:var(--gray-500);opacity:.7"><div class="ev-date">${e.date}</div><div class="ev-title">${e.title}</div><div class="ev-loc">${e.location||'—'}</div></div>`});
}
return h;
}
function addEvent(){
const title=document.getElementById('evTitle').value.trim();
const date=document.getElementById('evDate').value;
const location=document.getElementById('evLoc').value.trim();
if(!title||!date){toast('Введи название и дату!');return}
const events=getMyArr('events');
events.push({id:Date.now(),title,date,location});
setMyArr('events',events);
renderPage();toast('Событие добавлено!');
}
function delEvent(id){const events=getMyArr('events').filter(e=>e.id!==id);setMyArr('events',events);renderPage();toast('Удалено')}
// === COACH DASHBOARD ===
function renderStudentsPage(){
const users=LS('users')||[];
const myName=currentUser.name.toLowerCase();
const athletes=users.filter(u=>u.role!=='coach'&&u.coach&&u.coach.toLowerCase()===myName);
if(!athletes.length){
return `<div class="empty-state"><div class="big">&#x1F465;</div>Нет учеников, привязанных к вам.<p class="muted">Спортсмен должен указать ваше ФИО в поле «Тренер» при регистрации.</p></div>`;
}
let h='<h3 style="color:var(--cyan);margin-bottom:12px">&#x1F465; Мои ученики</h3>';
athletes.forEach(a=>{
const diary=getArrFor('diary',a.id);
const totalKm=diary.reduce((s,e)=>s+(+e.km||0),0);
const latest=diary.slice(0,5);
const av=a.photo?(a.photo.startsWith('data:')?a.photo:a.avatar):a.avatar;
h+=`
<div class="card">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
<div style="width:48px;height:48px;border-radius:50%;background:var(--cyan);color:var(--ink);display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;flex-shrink:0">${a.photo?`<img src="${a.photo}" style="width:100%;height:100%;border-radius:50%;object-fit:cover">`:av}</div>
<div><strong>${a.name}</strong><br><span class="muted">${a.sport} · ${a.rank||'без разряда'} · Всего: <span style="color:var(--cyan)">${totalKm.toFixed(1)} км</span></span></div>
</div>
${a.goal?`<div class="muted" style="margin-bottom:4px">&#x1F3AF; ${a.goal}</div>`:''}
<h4 style="font-size:13px;color:var(--gray-500);margin:8px 0 4px">Последние тренировки:</h4>
${latest.length?latest.map(e=>`<div style="font-size:12px;padding:4px 0;border-bottom:1px solid #2a3342">${e.date}${e.type} | ${e.km} км | ${e.time}</div>`).join(''):'<div class="muted">Нет записей</div>'}
</div>`;
});
return h;
}
// === PARENT DASHBOARD ===
function renderChildPage(){
const childId=getMy('childId');
if(!childId){
return `<div class="card">
<h3>&#x1F476; Мой ребёнок</h3>
<p class="muted">Введи логин ребёнка, чтобы видеть его профиль</p>
<input type="text" id="childLoginInput" placeholder="Логин ребёнка">
<button class="btn" onclick="linkChild()" style="margin-top:8px">&#x1F517; Привязать</button>
</div>`;
}
const users=LS('users')||[];
const child=users.find(u=>u.id===childId);
if(!child){
setMy('childId',null);
return renderChildPage();
}
const diary=getArrFor('diary',child.id);
const totalKm=diary.reduce((s,e)=>s+(+e.km||0),0);
const avgFeel=diary.length?((diary.reduce((s,e)=>s+(+e.feel||0),0)/diary.length).toFixed(1)):'—';
const badges=computeBadges(child.id);
const earnedCount=badges.filter(b=>b.cond).length;
const av=child.photo?(child.photo.startsWith('data:')?child.photo:child.avatar):child.avatar;
let h=`
<div class="profile-header">
<div class="avatar">${child.photo?`<img src="${child.photo}">`:av}</div>
<h2>${child.name}</h2>
<div class="tag">${child.sport}</div>
${child.rank?`<div style="margin-top:4px;font-size:13px;color:var(--gray-500)">${child.rank}</div>`:''}
</div>
<div class="achievement-row" style="justify-content:center;margin-bottom:12px">
<div class="achievement-badge"><div class="val">${diary.length}</div>тренировок</div>
<div class="achievement-badge"><div class="val">${totalKm.toFixed(1)}</div>км всего</div>
<div class="achievement-badge"><div class="val">${avgFeel}</div>средн. самочувствие</div>
</div>
<div class="card">
<h3>&#x1F3C5; Значки (${earnedCount}/${badges.length})</h3>
<div class="badge-row">${badges.map(b=>`<div class="badge-item${b.cond?' earned':' locked'}" title="${b.cond?'Получен: '+b.name:'Не получен: '+b.name}"><span class="bi">${b.icon}</span>${b.name}</div>`).join('')}</div>
</div>
<div class="card">
<h3>&#x1F4D6; Дневник тренировок</h3>
${diary.length?diary.slice(0,15).map(e=>`<div style="padding:8px 0;border-bottom:1px solid #2a3342;font-size:13px"><strong>${e.date}</strong> — ${e.type} | ${e.km} км | ${e.time} | &#x1F31F;${e.feel}/5</div>`).join(''):'<p class="muted">Нет записей</p>'}
</div>
${child.goal?`<div class="card"><h3>&#x1F3AF; Цель</h3><p>${child.goal}</p></div>`:''}
${child.coach?`<div class="card"><h3>&#x1F468;&#x200D;&#x1F3EB; Тренер</h3><p>${child.coach}</p></div>`:''}
<button class="btn danger small" onclick="unlinkChild()">&#x274C; Отвязать</button>
`;
return h;
}
function linkChild(){
const login=document.getElementById('childLoginInput').value.trim().toLowerCase();
if(!login){toast('Введи логин!');return}
const users=LS('users')||[];
const child=users.find(u=>u.login===login);
if(!child){toast('Пользователь не найден!');return}
if(child.id===uid()){toast('Это твой логин!');return}
setMy('childId',child.id);
renderPage();toast('Ребёнок привязан!');
}
function unlinkChild(){setMy('childId',null);renderPage();toast('Отвязано')}
// === CHAT ===
function renderChatPage(){
if(currentChat)return renderChatView();
return renderChatList();
}
function renderChatList(){
const users=LS('users')||[];
const other=users.filter(u=>u.id!==uid());
const msgs=LS('messages')||{};
let h='<div class="card"><div class="chat-tabs"><button class="chat-tab active" onclick="chatFilter=\'all\';renderPage()">&#x1F4AC; Все</button><button class="chat-tab" onclick="chatFilter=\'athlete\';renderPage()">&#x1F3CA; Спортсмены</button><button class="chat-tab" onclick="chatFilter=\'coach\';renderPage()">&#x1F3CB; Тренеры</button><button class="chat-tab" onclick="chatFilter=\'parent\';renderPage()">&#x1F468;&#x200D;&#x1F466; Родители</button></div>';
// Games button
h+='<button class="btn outline" onclick="renderGames()" style="width:100%;margin-bottom:8px">&#x1F3AE; Игры</button>';
h+='<button class="btn small outline" onclick="createGroupChat()" style="width:100%;margin-bottom:8px">+ Создать групповой чат</button>';
if(!other.length){h+='<div class="empty-state"><div class="big">&#x1F4AC;</div>Нет других пользователей</div></div>';return h}
h+='<div style="font-size:13px;color:var(--gray-500);margin-bottom:6px">Выбери с кем общаться:</div>';
other.forEach(u=>{
const chatKey=[uid(),u.id].sort().join('_');
const chatMsgs=msgs[chatKey]||[];
const last=chatMsgs[chatMsgs.length-1];
const unread=chatMsgs.filter(m=>m.to===uid()&&!m.read).length;
h+=`<div class="chat-list-item" onclick="openChat(${u.id})">
<div class="av">${u.photo?`<img src="${u.photo}" style="width:100%;height:100%;border-radius:50%;object-fit:cover">`:u.avatar||u.name.charAt(0)}</div>
<div class="info"><div class="name">${sanitize(u.name)}${u.role?` <span class="badge blue" style="font-size:9px">${u.role==='coach'?'Тренер':u.role==='parent'?'Родитель':'Спортсмен'}</span>`:''}</div><div class="last">${last?last.text:'Начни общение'}</div></div>
${unread?`<span class="unread">${unread}</span>`:''}
</div>`;
});
h+='</div>';return h;
}
function openChat(id){
currentChat=id;
// Mark messages as read
const msgs=LS('messages')||{};
const chatKey=[uid(),id].sort().join('_');
if(msgs[chatKey])msgs[chatKey].forEach(m=>{if(m.to===uid())m.read=true});
SS('messages',msgs);
renderPage();
}
function renderChatView(){
const other=((LS('users')||[]).find(u=>u.id===currentChat));
const chatKey=[uid(),currentChat].sort().join('_');
const msgs=(LS('messages')||{})[chatKey]||[];
let h=`<div style="display:flex;align-items:center;gap:8px;padding:8px 0">
<button class="btn small outline" onclick="currentChat=null;renderPage()">&#x2190;</button>
<strong>${other?other.name:'Чат'}</strong>
</div>
<div class="chat-messages" id="chatMsgs">`;
if(!msgs.length)h+='<div class="empty-state"><div class="big">&#x1F4AC;</div>Начните общение!</div>';
else msgs.forEach(m=>{h+=`<div class="chat-msg ${m.from===uid()?'mine':'theirs'}">${m.from!==uid()?`<div class="sender">${other?other.name:'Пользователь'}</div>`:''}${sanitize(m.text)}<div style="font-size:10px;color:${m.from===uid()?'var(--ink)':'var(--gray-500)'};margin-top:2px">${m.time||''}</div></div>`});
h+='</div><div class="chat-input-row"><input type="text" id="msgInput" placeholder="Сообщение..." onkeydown="if(event.key===\'Enter\')sendMsg()"><button class="btn small" onclick="sendMsg()">&#x27A4;</button></div>';
return h;
}
function sendMsg(){
const text=document.getElementById('msgInput')?.value.trim();
if(!text||!currentChat)return;
const msgs=LS('messages')||{};
const chatKey=[uid(),currentChat].sort().join('_');
if(!msgs[chatKey])msgs[chatKey]=[];
msgs[chatKey].push({from:uid(),to:currentChat,text,time:new Date().toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}),read:false});
SS('messages',msgs);
document.getElementById('msgInput').value='';
renderPage();
// scroll to bottom
setTimeout(()=>{const el=document.getElementById('chatMsgs');if(el)el.scrollTop=el.scrollHeight},100);
}
let gameState = null;
function renderGames(){
const c = document.getElementById('mainContent');
c.innerHTML = `
<div class="card" style="text-align:center">
<h3>&#x1F3AE; Игры в чате</h3>
<p class="muted">Выбери игру и играй с друзьями!</p>
<button class="quiz-option" onclick="startTicTacToe()">&#x274C;&#x2B55; Крестики-нолики</button>
<button class="quiz-option" onclick="startGuessNumber()">&#x1F522; Угадай число (1-100)</button>
<button class="quiz-option" onclick="startReaction()">&#x26A1; Реакция — кто быстрее</button>
<button class="quiz-option" onclick="startQuiz()">&#x1F3C6; Спорт-викторина</button>
<button class="btn small outline" onclick="showPage('chat')" style="margin-top:8px">&#x2190; Назад в чаты</button>
</div>
<div id="gameArea"></div>
`;
}
function startTicTacToe(){
gameState = { type: 'tictactoe', board: Array(9).fill(''), turn: 'X', over: false };
renderTicTacToe();
}
function renderTicTacToe(){
const g = gameState;
let h = '<div class="card"><h3 style="text-align:center">&#x274C;&#x2B55; Крестики-нолики</h3><div class="game-status">';
if(g.over){
h += g.winner === 'draw' ? 'Ничья!' : g.winner + ' победил!';
h += '<br><button class="btn small outline" onclick="startTicTacToe()" style="margin-top:8px">Играть ещё</button>';
} else {
h += 'Ход: ' + (g.turn === 'X' ? '&#x274C; Крестики' : '&#x2B55; Нолики');
}
h += '</div><div class="game-grid">';
g.board.forEach((cell, i) => {
h += `<div class="game-cell${cell?' '+ (cell==='X'?'x':'o')+(g.over||cell?' disabled':'') :''}" onclick="ticMove(${i})">${cell}</div>`;
});
h += '</div><button class="btn small outline" onclick="startTicTacToe()" style="width:100%;margin-top:8px">Новая игра</button></div>';
document.getElementById('gameArea').innerHTML = h;
}
function ticMove(i){
if(!gameState||gameState.over||gameState.board[i])return;
gameState.board[i] = gameState.turn;
const lines = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];
for(const [a,b,c] of lines){
if(gameState.board[a]&&gameState.board[a]===gameState.board[b]&&gameState.board[a]===gameState.board[c]){
gameState.over = true; gameState.winner = gameState.board[a];
// Award star for winning
const users = LS('users') || [];
const uidx = users.findIndex(u => u.id === uid());
if (uidx >= 0 && !gameState.starGiven) { users[uidx].stars = (users[uidx].stars || 0) + 1; users[uidx].gamesWon = (users[uidx].gamesWon || 0) + 1; SS('users', users); currentUser = users[uidx]; gameState.starGiven = true; }
break;
}
}
if(!gameState.over && gameState.board.every(c=>c)){ gameState.over = true; gameState.winner = 'draw'; }
if(!gameState.over) gameState.turn = gameState.turn === 'X' ? 'O' : 'X';
renderTicTacToe();
}
function startGuessNumber(){
gameState = { type: 'guess', number: Math.floor(Math.random()*100)+1, attempts: 0, max: 7 };
renderGuessNumber();
}
function renderGuessNumber(){
const g = gameState;
let h = `<div class="card" style="text-align:center"><h3>&#x1F522; Угадай число (1-100)</h3>
<p class="muted">Осталось попыток: <strong>${g.max - g.attempts}</strong></p>`;
if(g.won){
h += `<p style="color:var(--green);font-size:20px;font-weight:800">&#x1F389; Угадал! Это ${g.number}!</p><p class="muted">За ${g.attempts} попыток</p>${g.attempts<=5?'<p style=\'color:var(--cyan);font-weight:700\'>+2 &#x2B50; в рейтинг!</p>':''}`;
h += '<button class="btn small outline" onclick="startGuessNumber()">Играть ещё</button>';
} else if(g.attempts >= g.max){
h += `<p style="color:var(--red);font-size:18px;font-weight:800">Не угадал! Это было ${g.number}</p>`;
h += '<button class="btn small outline" onclick="startGuessNumber()">Играть ещё</button>';
} else {
h += '<input type="number" id="guessInput" placeholder="Твоё число" min="1" max="100" onkeydown="if(event.key==='Enter')doGuess()"><button class="btn" onclick="doGuess()" style="margin-top:8px">Проверить</button>';
}
h += '</div><button class="btn small outline" onclick="renderGames()" style="width:100%">&#x2190; К играм</button>';
document.getElementById('gameArea').innerHTML = h;
}
function doGuess(){
if(!gameState||gameState.won||gameState.attempts>=gameState.max)return;
const n = +document.getElementById('guessInput').value;
if(!n||n<1||n>100)return;
gameState.attempts++;
if(n === gameState.number){ gameState.won = true;
if(gameState.attempts <= 5) {
const users = LS('users') || [];
const idx = users.findIndex(u => u.id === uid());
if(idx >= 0) { users[idx].stars = (users[idx].stars || 0) + 2; users[idx].guessWon = (users[idx].guessWon || 0) + 1; SS('users', users); currentUser = users[idx]; }
}
renderGuessNumber(); return }
const hint = n < gameState.number ? '&#x1F4C8; Больше!' : '&#x1F4C9; Меньше!';
const tempDiv = document.createElement('div'); tempDiv.innerHTML = `<div class="card" style="text-align:center;padding:12px;margin-bottom:8px"><p>${n} &mdash; ${hint}</p></div>`;
document.getElementById('gameArea').insertBefore(tempDiv, document.getElementById('gameArea').firstChild);
if(gameState.attempts >= gameState.max) renderGuessNumber();
else { document.getElementById('guessInput').value=''; document.getElementById('guessInput').focus(); }
}
function startReaction(){
gameState = { type: 'reaction', state: 'waiting', startTime: 0, best: getMy('reactionBest') || 9999 };
renderReaction();
}
function renderReaction(){
const g = gameState;
let bg = '#151c28', txt = '';
if(g.state === 'waiting'){ bg = '#FF6B6B'; txt = '&#x1F534; Жди зелёный!'; }
else if(g.state === 'ready'){ bg = '#4CAF50'; txt = '&#x1F7E2; ЖМИ СЕЙЧАС!'; }
else if(g.state === 'early'){ bg = '#FFD700'; txt = '&#x26A0; Слишком рано! Жми "Начать"'; }
else if(g.state === 'done'){ txt = `&#x23F1; Твоё время: <strong>${g.reaction} мс</strong>${g.reactStar?'<br><span style=\'color:var(--cyan);font-weight:700\'>+1 &#x2B50; быстрее 300 мс!</span>':''}<br>Рекорд: ${g.best !== 9999 ? g.best+' мс' : '—'}`; }
let h = `<div class="card" style="text-align:center">
<h3>&#x26A1; Реакция</h3>
<div style="width:200px;height:200px;border-radius:50%;background:${bg};display:flex;align-items:center;justify-content:center;margin:16px auto;cursor:${g.state==='ready'?'pointer':'default'};font-size:18px;font-weight:800;transition:background .1s" onclick="reactClick()">${txt||'&#x1F3AF; Нажми "Начать"'}</div>`;
if(g.state === 'done'){
h += '<button class="btn small outline" onclick="startReaction()">Ещё раз</button>';
} else if(g.state === 'waiting' || g.state === 'early'){
h += '<button class="btn" onclick="reactStart()">Начать</button>';
}
h += '</div><button class="btn small outline" onclick="renderGames()" style="width:100%">&#x2190; К играм</button>';
document.getElementById('gameArea').innerHTML = h;
}
function reactStart(){
if(!gameState||gameState.state==='ready')return;
gameState.state = 'waiting'; renderReaction();
const delay = 1500 + Math.random() * 3000;
setTimeout(() => {
if(gameState.state !== 'waiting') return;
gameState.state = 'ready'; gameState.startTime = Date.now(); renderReaction();
}, delay);
}
function reactClick(){
if(!gameState)return;
if(gameState.state === 'waiting'){ gameState.state = 'early'; renderReaction(); return; }
if(gameState.state === 'ready'){ gameState.reaction = Date.now() - gameState.startTime; gameState.state = 'done';
if(gameState.reaction < 300) {
const users = LS('users') || [];
const idx = users.findIndex(u => u.id === uid());
if(idx >= 0) { users[idx].stars = (users[idx].stars || 0) + 1; SS('users', users); currentUser = users[idx]; gameState.reactStar = true; }
}
if(gameState.reaction < gameState.best){ gameState.best = gameState.reaction; setMy('reactionBest', gameState.reaction); }
renderReaction(); }
}
function startQuiz(){
const questions = [
{q:'Сколько метров в олимпийском бассейне?',a:['25 м','50 м','100 м','33 м'],r:1},
{q:'Какой стиль плавания самый быстрый?',a:['Брасс','Баттерфляй','Кроль','На спине'],r:2},
{q:'Сколько золотых медалей у Майкла Фелпса?',a:['8','15','23','28'],r:2},
{q:'Как зовут лучшего спринтера мира (50 м в/с)?',a:['Майкл Фелпс','Калеб Дрессел','Райан Лохте','Адам Пити'],r:1},
{q:'Где пройдёт Олимпиада-2032?',a:['Лос-Анджелес','Париж','Брисбен','Токио'],r:2},
{q:'Сколько длится олимпийский цикл?',a:['2 года','3 года','4 года','5 лет'],r:2},
{q:'Какая страна выиграла больше всех медалей в плавании?',a:['Китай','Австралия','Россия','США'],r:3},
{q:'Как называется поворот в кроле?',a:['Сальто','Кувырок','Разворот','Маятник'],r:1}
];
gameState = { type: 'quiz', questions, current: 0, score: 0, answered: false };
renderQuiz();
}
function renderQuiz(){
const g = gameState;
if(g.current >= g.questions.length){
// Award stars for quiz
const earned = g.score >= 7 ? 5 : g.score >= 5 ? 3 : g.score >= 3 ? 1 : 0;
if (earned > 0) {
const users = LS('users') || [];
const idx = users.findIndex(u => u.id === uid());
if (idx >= 0) { users[idx].stars = (users[idx].stars || 0) + earned; users[idx].quizScore = Math.max(users[idx].quizScore||0, g.score); SS('users', users); currentUser = users[idx]; }
}
document.getElementById('gameArea').innerHTML = `<div class="card" style="text-align:center"><h3>&#x1F3C6; Викторина завершена!</h3><p style="font-size:24px;font-weight:800;color:var(--cyan)">${g.score} / ${g.questions.length}</p>${earned>0?`<p style='color:var(--cyan);font-weight:700'>+${earned} &#x2B50; в рейтинг!</p>`:''}<p class="muted">${g.score>=6?'&#x1F451; Ты знаток спорта!':g.score>=4?'&#x1F44D; Неплохо!':'&#x1F4DA; Учи матчасть!'}</p><button class="btn" onclick="startQuiz()">Ещё раз</button></div>`;
return;
}
if(g.current >= g.questions.length) return;
const q = g.questions[g.current];
let h = `<div class="card"><h3>&#x2753; Вопрос ${g.current+1}/${g.questions.length}</h3><p style="font-size:17px;font-weight:600;margin:12px 0">${q.q}</p>`;
q.a.forEach((ans,i)=>{
let cls = 'quiz-option';
if(g.answered){
if(i === q.r) cls += ' right';
else if(g.selected === i) cls += ' wrong';
}
h += `<button class="${cls}" onclick="answerQuiz(${i})" ${g.answered?'disabled':''}>${ans}</button>`;
});
h += `<div style="margin-top:8px;font-size:13px;color:var(--gray-500)">Счёт: ${g.score}</div>`;
if(g.answered){
h += '<button class="btn small" onclick="nextQuiz()" style="margin-top:8px">Дальше &#x2192;</button>';
}
h += '</div>';
document.getElementById('gameArea').innerHTML = h;
}
function answerQuiz(i){
if(!gameState||gameState.answered)return;
gameState.answered = true; gameState.selected = i;
if(i === gameState.questions[gameState.current].r) gameState.score++;
renderQuiz();
}
function nextQuiz(){
gameState.current++; gameState.answered = false; gameState.selected = null; renderQuiz();
}
function createGroupChat(){
const name=prompt('Название группового чата:');
if(!name)return;
const groups=LS('groups')||[];
groups.push({id:Date.now(),name,members:[uid()],messages:[]});
SS('groups',groups);
toast('Групповой чат создан!');
renderPage();
}
// === TOOLS PAGE ===
const dresselData=[{age:14,time:22.5},{age:15,time:21.8},{age:16,time:20.9},{age:17,time:19.8},{age:18,time:19.2}];
function showChampionCompare(){
const userTime=parseFloat(document.getElementById('cmpUserTime').value);
const userAge=parseInt(document.getElementById('cmpUserAge').value);
const el=document.getElementById('cmpResult');
if(!userTime||!userAge){el.innerHTML='<span style="color:var(--red)">Введи время и возраст!</span>';return}
const nearest=dresselData.find(d=>d.age===userAge);
let lines=[];
lines.push('<strong>&#x1F3CA; Ты ('+userAge+' лет)</strong> → <span style="color:var(--cyan)">'+userTime+' сек</span>');
const kmsTime=25.0;
if(userTime<=kmsTime){
lines.push('<strong>&#x1F3C5; КМС (25.00)</strong> → '+kmsTime+' сек');
lines.push('<span style="color:var(--gray-500)">&#x2705; Ты уже выполнил КМС!</span>');
}else{
const diffKms=(userTime-kmsTime).toFixed(2);
lines.push('<strong>&#x1F3C5; КМС (25.00)</strong> → '+kmsTime+' сек <span style="color:var(--red)">(нужно сбросить '+diffKms+' сек)</span>');
}
if(nearest){
lines.push('<strong>&#x1F451; Калеб Дрессел в '+nearest.age+' лет</strong> → '+nearest.time+' сек');
const diff=(userTime-nearest.time).toFixed(2);
if(userTime<=nearest.time){
lines.push('<span style="color:var(--green);font-weight:700">&#x1F3C6; Ты быстрее Дрессела на '+Math.abs(diff)+' сек!</span>');
}else{
lines.push('<span style="color:var(--cyan)">Разница: '+diff+' сек. Продолжай тренироваться!</span>');
}
}else{
lines.push('<span class="muted">Нет данных Дрессела для возраста '+userAge+' лет</span>');
}
lines.push('<span class="muted" style="font-size:11px">* данные приблизительные, для мотивации</span>');
el.innerHTML=lines.map(l=>'<div style="padding:4px 0">'+l+'</div>').join('');
}
let videoEl=null;
function handleVideoUpload(input){
const file=input.files[0];
if(!file)return;
const url=URL.createObjectURL(file);
const container=document.getElementById('videoContainer');
const video=document.getElementById('videoPlayer');
video.src=url;
video.onloadedmetadata=()=>{updateFrameCounter();};
video.ontimeupdate=()=>{updateFrameCounter();};
container.style.display='block';
videoEl=video;
}
function updateFrameCounter(){
const v=document.getElementById('videoPlayer');
const el=document.getElementById('frameCounter');
if(el&&v)el.textContent=v.currentTime.toFixed(2)+' с';
}
function videoStep(delta){
const v=document.getElementById('videoPlayer');
if(!v||!v.src)return;
v.pause();
v.currentTime=Math.max(0,v.currentTime+delta);
updateFrameCounter();
}
function toggleVideoPlay(){
const v=document.getElementById('videoPlayer');
const btn=document.getElementById('videoPlayBtn');
if(!v||!v.src)return;
if(v.paused){v.play();btn.innerHTML='&#x23F8; Стоп';}
else{v.pause();btn.innerHTML='&#x25B6; Старт';}
}
function renderToolsPage(){
return `
<div class="card">
<h3>&#x1F4CA; Нормативы (плавание, 50 м бассейн)</h3>
<table><tr><th>Разряд</th><th>50 м в/с</th><th>100 м в/с</th><th>400 м в/с</th></tr>
<tr><td><span class="badge gold">МСМК</span></td><td>21.50</td><td>47.00</td><td>3:48</td></tr>
<tr><td><span class="badge blue">МС</span></td><td>23.00</td><td>50.50</td><td>4:05</td></tr>
<tr><td><span class="badge green">КМС</span></td><td>25.00</td><td>54.50</td><td>4:25</td></tr>
<tr><td>1 взр.</td><td>28.00</td><td>1:01</td><td>5:00</td></tr>
<tr><td>2 взр.</td><td>31.50</td><td>1:09</td><td>5:40</td></tr>
<tr><td>3 взр.</td><td>35.00</td><td>1:17</td><td>6:20</td></tr>
<tr><td>1 юн.</td><td>40.00</td><td>1:29</td><td>7:10</td></tr>
<tr><td>2 юн.</td><td>47.00</td><td>1:45</td><td>8:10</td></tr></table>
</div>
<div class="card">
<h3>&#x1F3CA; Сравнение с чемпионами</h3>
<p class="muted">Сравни свой прогресс с Калебом Дресселом (50 м в/с)</p>
<table><tr><th>Возраст</th><th>Дрессел (сек)</th></tr>
<tr><td>14 лет</td><td>22.5</td></tr>
<tr><td>15 лет</td><td>21.8</td></tr>
<tr><td>16 лет</td><td>20.9</td></tr>
<tr><td>17 лет</td><td>19.8</td></tr>
<tr><td>18 лет</td><td>19.2</td></tr></table>
<p class="muted" style="margin-top:8px">Введи своё лучшее время на 50 м в/с:</p>
<input type="number" id="cmpUserTime" placeholder="Твоё время (сек)" step="0.1" min="0">
<input type="number" id="cmpUserAge" placeholder="Твой возраст" min="1" max="100" value="${currentUser.age||''}">
<button class="btn small" onclick="showChampionCompare()" style="margin-top:8px">Сравнить!</button>
<div id="cmpResult" style="margin-top:8px;font-size:14px"></div>
</div>
<div class="card">
<h3>&#x1F3AC; Анализ техники</h3>
<p class="muted">Загрузи видео своего плавания и смотри покадрово</p>
<label class="file-btn" style="width:100%;text-align:center">&#x1F4C1; Выбрать видео<input type="file" accept="video/*" id="videoFile" onchange="handleVideoUpload(this)"></label>
<div class="video-wrap" id="videoContainer" style="display:none">
<video id="videoPlayer" style="width:100%;border-radius:12px;background:#000"></video>
<div class="frame-controls">
<button onclick="videoStep(-0.1)" title="Назад 0.1 сек">&#x23EE;</button>
<button onclick="videoStep(-0.033)" title="Назад 1 кадр">&#x25C0;</button>
<span class="frame-counter" id="frameCounter">0.00 с</span>
<button onclick="videoStep(0.033)" title="Вперёд 1 кадр">&#x25B6;</button>
<button onclick="videoStep(0.1)" title="Вперёд 0.1 сек">&#x23ED;</button>
</div>
<div style="display:flex;gap:4px;justify-content:center;margin-top:4px;flex-wrap:wrap">
<button class="btn small outline" onclick="videoStep(-1)">-1 с</button>
<button class="btn small outline" onclick="videoStep(-0.5)">-0.5 с</button>
<button class="btn small" onclick="toggleVideoPlay()" id="videoPlayBtn">&#x25B6; Старт</button>
<button class="btn small outline" onclick="videoStep(0.5)">+0.5 с</button>
<button class="btn small outline" onclick="videoStep(1)">+1 с</button>
</div>
</div>
</div>
<div class="card">
<h3>&#x1F48A; Здоровье</h3>
<p class="muted" style="margin-bottom:8px">Отмечай принятые витамины:</p>
<div id="vitCheck"></div>
<button class="btn small outline" onclick="addSleepEntry()" style="margin-top:8px">+ Записать сон и пульс</button>
<div id="vitHistory" class="muted" style="margin-top:8px"></div>
</div>
<div class="card">
<h3>&#x1F4DA; Видеоуроки</h3>
<p class="muted">Найди на YouTube:</p>
${['swimming start technique','flip turn tutorial','freestyle stroke technique','underwater dolphin kick','breaststroke technique','Caeleb Dressel 50m analysis','swimming stretching routine','dryland training swimming','swimming race psychology','swimmer nutrition meal plan'].map(q=>`<div style="padding:6px 0;font-size:13px;border-bottom:1px solid #2a3342"><em>${q}</em></div>`).join('')}
</div>
<div class="card">
<h3>&#x2B50; Рейтинг</h3>
<p class="muted">Топ спортсменов (по голосам):</p>
<div id="rankList"></div>
<button class="btn small outline" onclick="voteRandom()" style="margin-top:8px">&#x2B50; Проголосовать</button>
</div>
`;
}
// === VITAMINS ===
function renderVitamins(){
const el=document.getElementById('vitCheck');
if(!el)return;
const today=new Date().toISOString().slice(0,10);
const taken=getMy('vtaken')||{};
const vits=[{id:'d3',n:'Витамин D3'},{id:'omega',n:'Омега-3'},{id:'mg',n:'Магний'},{id:'zn',n:'Цинк'},{id:'bcaa',n:'BCAA'}];
el.innerHTML=vits.map(v=>{
const ok=taken[today]&&taken[today][v.id];
return `<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;font-size:14px"><input type="checkbox" ${ok?'checked':''} onchange="toggleVit('${v.id}')">${v.n}</label>`;
}).join('');
}
function toggleVit(id){
const today=new Date().toISOString().slice(0,10);
let taken=getMy('vtaken')||{};
if(!taken[today])taken[today]={};
taken[today][id]=!taken[today][id];
setMy('vtaken',taken);
renderVitamins();
}
function addSleepEntry(){
const h=prompt('Часов сна:');const p=prompt('Пульс утром:');
if(!h&&!p)return;
const s=getMyArr('sleep');s.unshift({date:new Date().toISOString().slice(0,10),hours:+h||0,pulse:+p||0});
setMyArr('sleep',s);toast('Сохранено!');
}
// === RANKING ===
function renderRanking(){
const el=document.getElementById('rankList');if(!el)return;
const users=LS('users')||[];
const sorted=[...users].filter(u=>u.stars>0).sort((a,b)=>b.stars-a.stars).slice(0,5);
if(!sorted.length){el.innerHTML='<p class="muted">Пока никто не проголосовал</p>';return}
el.innerHTML=sorted.map((u,i)=>`<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid #2a3342"><span style="color:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--gray-500)'}">${i+1}.</span><span>${sanitize(u.name)}</span><span style="margin-left:auto;color:var(--cyan);font-size:13px">&#x2B50; ${u.stars||0}${u.quizScore?` | &#x1F3C6; ${u.quizScore}/8`:''}${u.gamesWon?` | &#x1F3AE; ${u.gamesWon}`:''}</span></div>`).join('');
}
function voteRandom(){
const users=LS('users')||[];
const others=users.filter(u=>u.id!==uid());
if(!others.length){toast('Нет других пользователей!');return}
const target=others[Math.floor(Math.random()*others.length)];
const voterKey='voted_'+target.id;
if(localStorage.getItem('g_'+voterKey)){toast('Ты уже голосовал за '+target.name+'!');return}
target.stars=(target.stars||0)+1;
const idx=users.findIndex(u=>u.id===target.id);
if(idx>=0){users[idx]=target;SS('users',users)}
localStorage.setItem('g_'+voterKey,'1');
renderRanking();
toast('Голос за '+target.name+' учтён!');
}
// === LOGOUT ===
function doLogout(){
currentUser=null;currentChat=null;
document.getElementById('appScreen').classList.remove('active');
document.getElementById('loginScreen').classList.add('active');
// Show AI helper on login
document.getElementById('lUser').value='';document.getElementById('lPass').value='';
}
// === CITIES ===
const cityData={'Казахстан':['Астана','Алматы','Шымкент','Актобе','Караганда','Тараз','Павлодар','Семей','Атырау','Костанай'],'Россия':['Москва','Санкт-Петербург','Новосибирск','Екатеринбург','Казань','Омск','Уфа'],'США':['Нью-Йорк','Лос-Анджелес','Чикаго','Хьюстон','Майами','Бостон'],'Турция':['Стамбул','Анкара','Измир','Анталья','Бурса'],'Узбекистан':['Ташкент','Самарканд','Бухара'],'Кыргызстан':['Бишкек','Ош'],'Украина':['Киев','Харьков','Одесса','Львов'],'Беларусь':['Минск','Гомель','Брест'],'Германия':['Берлин','Мюнхен','Гамбург'],'Франция':['Париж','Марсель','Лион'],'Великобритания':['Лондон','Манчестер'],'Италия':['Рим','Милан'],'Испания':['Мадрид','Барселона'],'Япония':['Токио','Осака'],'Китай':['Пекин','Шанхай'],
'ОАЭ':['Дубай','Абу-Даби'],'Азербайджан':['Баку','Гянджа'],'ЮАР':['Йоханнесбург','Кейптаун'],'Канада':['Торонто','Ванкувер','Монреаль'],'Австралия':['Сидней','Мельбурн'],'Бразилия':['Сан-Паулу','Рио']};
function updateCities(){
const c=document.getElementById('rCountry').value;
const dl=document.getElementById('cityList');dl.innerHTML='';
(cityData[c]||[]).forEach(city=>{const o=document.createElement('option');o.value=city;dl.appendChild(o)});
}
// === RENDER ALL ===
function renderAll(){
renderBottomNav();
renderPage();
setTimeout(()=>{renderVitamins();renderRanking()},200);
}
// === INIT ===
if('serviceWorker'in navigator)navigator.serviceWorker.register('sw.js').catch(()=>{});
document.getElementById('loginScreen').classList.add('active');
// Show AI helper on login
</script>
</body>
</html>