1462 lines
91 KiB
HTML
1462 lines
91 KiB
HTML
<!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:#080c14;--surface:#0f1624;--surface2:#151f30;--surface3:#1a2740;--cyan:#00E5FF;--cyan2:#00b8d4;--white:#fff;--gray:#6b7a90;--gray2:#3d4a5c;--red:#FF5252;--green:#4CAF50;--gold:#FFD700;--orange:#FF9800;--purple:#7C4DFF}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:#111827;background:var(--bg);overflow-x:hidden;-webkit-font-smoothing:antialiased}
|
||
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||
@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
||
|
||
input,select,textarea{width:100%;padding:14px 16px;border:1.5px solid var(--surface3);border-radius:14px;font:inherit;font-size:16px;background:#f9fafb;color:#111827;transition:all .25s ease;outline:none}
|
||
input:focus,select:focus,textarea:focus{border-color:var(--cyan);box-shadow:0 0 0 3px rgba(0,229,255,.1);background:var(--surface)}
|
||
input:hover,select:hover{border-color:var(--gray2)}
|
||
input::placeholder{color:var(--gray)}
|
||
|
||
.btn{position:relative;overflow:hidden;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:14px 28px;border-radius:14px;font-weight:700;border:none;font-size:16px;cursor:pointer;transition:all .25s ease;text-align:center;width:100%;color:var(--bg);background:linear-gradient(135deg,var(--cyan),var(--cyan2))}
|
||
.btn:hover:not(:active){transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,229,255,.3)}
|
||
.btn:active{transform:scale(.97)}
|
||
.btn::after{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.15),transparent);transition:left .5s}
|
||
.btn:hover::after{left:100%}
|
||
.btn.outline{background:transparent;border:2px solid var(--cyan);color:var(--cyan)}
|
||
.btn.outline:hover{background:rgba(0,229,255,.08)}
|
||
.btn.small{padding:8px 18px;font-size:13px;width:auto;border-radius:10px}
|
||
.btn.danger{background:linear-gradient(135deg,var(--red),#e53935);color:#fff}
|
||
|
||
.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#ffffff;border:1px solid var(--cyan);color:var(--cyan);padding:12px 28px;border-radius:30px;font-weight:600;font-size:14px;z-index:999;opacity:0;transition:all .3s ease;pointer-events:none;backdrop-filter:blur(20px)}
|
||
.toast.show{opacity:1;bottom:90px}
|
||
|
||
.screen{position:fixed;top:0;left:0;right:0;bottom:0;display:none;flex-direction:column;background:var(--bg);color:#111827;overflow-y:auto;z-index:10}
|
||
.screen.active{display:flex;animation:slideUp .4s ease}
|
||
|
||
.login-box{max-width:420px;margin:auto;padding:32px 24px;width:100%}
|
||
.login-box h1{text-align:center;font-size:36px;margin-bottom:4px;letter-spacing:-.5px;background:linear-gradient(135deg,var(--white),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
||
.login-box h1 span{-webkit-text-fill-color:var(--cyan)}
|
||
.login-box .sub{text-align:center;color:var(--gray);margin-bottom:32px;font-size:15px}
|
||
|
||
.reg-step{display:none;flex-direction:column;gap:10px;max-width:420px;margin:auto;padding:20px 24px;width:100%;animation:fadeIn .4s ease}
|
||
.reg-step.active{display:flex}
|
||
.reg-step h2{text-align:center;font-size:24px;margin-bottom:2px;font-weight:700}
|
||
.reg-step .hint{text-align:center;color:var(--gray);font-size:13px;margin-bottom:20px}
|
||
.step-indicator{display:flex;justify-content:center;gap:8px;margin-bottom:20px}
|
||
.step-dot{width:10px;height:10px;border-radius:50%;background:#e5e7eb;transition:all .4s ease;border:2px solid transparent}
|
||
.step-dot.done{background:var(--cyan);border-color:var(--cyan)}
|
||
.step-dot.current{background:transparent;border-color:var(--cyan);box-shadow:0 0 12px rgba(0,229,255,.4);transform:scale(1.3)}
|
||
.reg-nav{display:flex;gap:10px;margin-top:10px}
|
||
.reg-nav button{flex:1}
|
||
|
||
.avatar-picker{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin:16px 0}
|
||
.avatar-opt{width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:30px;cursor:pointer;border:2.5px solid var(--surface3);transition:all .3s ease;background:var(--surface2)}
|
||
.avatar-opt:hover{border-color:var(--gray2);transform:scale(1.05)}
|
||
.avatar-opt.selected{border-color:var(--cyan);box-shadow:0 0 20px rgba(0,229,255,.25);transform:scale(1.12);background:rgba(0,229,255,.1)}
|
||
|
||
.bottom-nav{display:flex;justify-content:space-around;padding:6px 8px 10px;background:linear-gradient(0deg,var(--surface) 0%,var(--surface) 80%,transparent 100%);border-top:1px solid var(--surface3);position:sticky;bottom:0;z-index:50}
|
||
.nav-item{display:flex;flex-direction:column;align-items:center;gap:3px;padding:6px 14px;border-radius:14px;cursor:pointer;color:var(--gray);font-size:10px;font-weight:600;transition:all .3s ease;border:none;background:none;position:relative}
|
||
.nav-item.active{color:var(--cyan)}
|
||
.nav-item.active::before{content:'';position:absolute;top:0;width:20px;height:3px;background:var(--cyan);border-radius:0 0 3px 3px}
|
||
.nav-item .icon{font-size:22px;transition:transform .3s ease}
|
||
.nav-item.active .icon{transform:translateY(-2px)}
|
||
|
||
.content{flex:1;overflow-y:auto;padding:16px;max-width:720px;margin:0 auto;width:100%}
|
||
.card{background:#ffffff;border-radius:18px;padding:22px;margin-bottom:14px;border:1px solid var(--surface2);transition:all .3s ease;animation:fadeIn .5s ease}
|
||
.card:hover{border-color:var(--surface3)}
|
||
.card h3{font-size:17px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
||
.card .muted{color:var(--gray);font-size:13px}
|
||
|
||
.profile-header{text-align:center;padding:32px 16px 20px}
|
||
.profile-header .avatar{width:88px;height:88px;border-radius:50%;background:linear-gradient(135deg,var(--cyan),var(--purple));color:#111827;display:flex;align-items:center;justify-content:center;font-size:38px;font-weight:800;margin:0 auto 16px;overflow:hidden;box-shadow:0 8px 32px rgba(0,229,255,.2);border:3px solid var(--surface)}
|
||
.profile-header h2{font-size:24px;margin-bottom:6px}
|
||
.profile-header .tag{display:inline-block;background:rgba(0,229,255,.12);color:var(--cyan);padding:4px 16px;border-radius:20px;font-size:13px;font-weight:700}
|
||
.achievement-row{display:flex;gap:10px;flex-wrap:wrap}
|
||
.achievement-badge{background:#f9fafb;border-radius:14px;padding:14px 18px;font-size:13px;text-align:center;min-width:90px}
|
||
.achievement-badge .val{font-size:22px;font-weight:800;color:var(--cyan)}
|
||
|
||
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}
|
||
.badge{display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:700}
|
||
.badge.gold{background:rgba(255,215,0,.15);color:var(--gold)}
|
||
.badge.blue{background:rgba(0,229,255,.12);color:var(--cyan)}
|
||
.badge.green{background:rgba(76,175,80,.15);color:var(--green)}
|
||
|
||
.chat-list-item{display:flex;align-items:center;gap:14px;padding:14px;background:#f9fafb;border-radius:14px;margin-bottom:8px;cursor:pointer;transition:all .25s 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:50%;background:linear-gradient(135deg,var(--cyan),var(--cyan2));color:var(--bg);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:18px;flex-shrink:0}
|
||
.chat-list-item .info{flex:1;min-width:0}
|
||
.chat-list-item .name{font-weight:600;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(--cyan);color:var(--bg);border-radius:12px;padding:2px 8px;font-size:11px;font-weight:700}
|
||
|
||
.chat-messages{display:flex;flex-direction:column;gap:10px;padding:12px 0;flex:1;overflow-y:auto}
|
||
.chat-msg{max-width:82%;padding:12px 16px;border-radius:18px;font-size:14px;line-height:1.45;word-break:break-word;animation:fadeIn .3s ease}
|
||
.chat-msg.mine{background:linear-gradient(135deg,var(--cyan),var(--cyan2));color:var(--bg);align-self:flex-end;border-bottom-right-radius:6px}
|
||
.chat-msg.theirs{background:#f9fafb;color:#111827;align-self:flex-start;border-bottom-left-radius:6px}
|
||
.chat-msg .sender{font-size:11px;opacity:.7;margin-bottom:3px}
|
||
|
||
.chat-tabs{display:flex;gap:6px;margin-bottom:14px}
|
||
.chat-tab{flex:1;padding:10px;border-radius:12px;border:none;font-size:13px;font-weight:600;cursor:pointer;background:#f9fafb;color:var(--gray);transition:all .25s ease}
|
||
.chat-tab:hover{color:#111827;background:var(--surface3)}
|
||
.chat-tab.active{background:var(--cyan);color:var(--bg)}
|
||
|
||
.game-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;width:200px;margin:16px auto}
|
||
.game-cell{aspect-ratio:1;background:#f9fafb;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:800;cursor:pointer;transition:all .2s ease;border:2px solid var(--surface3)}
|
||
.game-cell:hover:not(.disabled){background:#e5e7eb;border-color:var(--cyan)}
|
||
.game-cell:active{transform:scale(.95)}
|
||
.game-cell.x{color:var(--cyan)}
|
||
.game-cell.o{color:var(--orange)}
|
||
.game-cell.disabled{pointer-events:none;opacity:.7}
|
||
.game-status{text-align:center;font-size:14px;font-weight:600;margin:10px 0;color:var(--cyan)}
|
||
.quiz-option{display:block;width:100%;padding:13px 16px;margin:8px 0;background:#f9fafb;border:2px solid var(--surface3);border-radius:12px;color:#111827;font-size:14px;cursor:pointer;text-align:left;transition:all .25s ease}
|
||
.quiz-option:hover:not(:disabled){border-color:var(--cyan);background:var(--surface)}
|
||
.quiz-option:active{transform:scale(.98)}
|
||
.quiz-option.right{background:rgba(76,175,80,.15);border-color:var(--green);color:var(--green)}
|
||
.quiz-option.wrong{background:rgba(255,82,82,.15);border-color:var(--red);color:var(--red)}
|
||
|
||
.empty-state{text-align:center;padding:48px 24px;color:var(--gray);animation:fadeIn .6s ease}
|
||
.empty-state .big{font-size:56px;margin-bottom:12px;opacity:.6}
|
||
|
||
.progress-bar{height:8px;background:#f9fafb;border-radius:4px;overflow:hidden;margin-top:8px}
|
||
.progress-bar .fill{height:100%;background:linear-gradient(90deg,var(--cyan),var(--purple));border-radius:4px;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;backdrop-filter:blur(8px)}
|
||
.lightbox img,.lightbox video{max-width:95%;max-height:90vh;border-radius:12px}
|
||
|
||
label.file-btn{display:inline-flex;align-items:center;gap:8px;background:#f9fafb;padding:14px 24px;border-radius:14px;font-weight:600;cursor:pointer;font-size:14px;margin:10px 0;transition:all .25s ease;border:2px dashed var(--surface3)}
|
||
label.file-btn:hover{background:#e5e7eb;border-color:var(--cyan)}
|
||
|
||
input[type=file]{display:none}
|
||
|
||
.role-btn{width:100%;padding:18px;background:#f9fafb;border:2px solid var(--surface3);border-radius:16px;color:#111827;font-size:18px;font-weight:600;cursor:pointer;text-align:center;transition:all .3s ease}
|
||
.role-btn:hover{border-color:var(--gray2);background:var(--surface)}
|
||
.role-btn.selected{border-color:var(--cyan);background:rgba(0,229,255,.08);box-shadow:0 0 24px rgba(0,229,255,.1)}
|
||
.role-btn:active{transform:scale(.97)}
|
||
|
||
.badge-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
||
.badge-item{display:flex;align-items:center;gap:6px;background:#f9fafb;border:1.5px solid var(--surface3);border-radius:14px;padding:10px 14px;font-size:13px;transition:all .3s ease}
|
||
.badge-item .bi{font-size:22px}
|
||
.badge-item.earned{border-color:var(--cyan);background:rgba(0,229,255,.06);box-shadow:0 0 16px rgba(0,229,255,.08)}
|
||
.badge-item.locked{opacity:.35;filter:grayscale(1)}
|
||
|
||
.event-card{background:#f9fafb;border-radius:14px;padding:16px;margin-bottom:10px;border-left:4px solid var(--cyan);transition:all .25s ease}
|
||
.event-card:hover{border-left-color:var(--purple)}
|
||
.event-card .ev-date{font-size:12px;color:var(--cyan);font-weight:700;text-transform:uppercase;letter-spacing:.5px}
|
||
.event-card .ev-title{font-size:16px;font-weight:600;margin:4px 0}
|
||
|
||
.video-wrap{position:relative;margin:10px 0}
|
||
.video-wrap video{width:100%;border-radius:14px;background:#000}
|
||
.frame-controls{display:flex;align-items:center;gap:10px;justify-content:center;margin-top:10px}
|
||
.frame-controls button{width:44px;height:44px;border-radius:50%;background:#f9fafb;border:2px solid var(--surface3);color:#111827;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .25s ease}
|
||
.frame-controls button:hover{background:#e5e7eb;border-color:var(--cyan)}
|
||
.frame-counter{font-size:16px;font-weight:700;color:var(--cyan);min-width:80px;text-align:center}
|
||
|
||
.glass{background:rgba(15,22,36,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
|
||
|
||
.login-box .error{color:var(--red);font-size:13px;margin-bottom:10px;display:none;padding:10px 14px;background:rgba(255,82,82,.1);border-radius:10px;border:1px solid rgba(255,82,82,.2)}
|
||
|
||
.reg-hint{background:linear-gradient(135deg,rgba(0,229,255,.06),rgba(124,77,255,.06));border:1px solid rgba(0,229,255,.2);border-radius:12px;padding:12px 16px;margin-bottom:12px;font-size:13px;color:var(--cyan);animation:pulse 3s ease-in-out}
|
||
|
||
@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:#f5f5f5!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>🏊 <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()">🔒 Войти</button>
|
||
<div style="text-align:center;margin-top:12px">
|
||
<button class="btn outline" onclick="startReg()" style="width:auto;padding:12px 40px">✏ Регистрация</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()">← Назад</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>✏ Как тебя зовут?</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)">🤖 <b>Подсказка:</b> Напиши ФИО полностью. Например: Кайрат Гали Аскарович.</div>
|
||
<input type="text" id="rName" placeholder="Например: Кайрат Гали Аскарович" autofocus>
|
||
<div class="reg-nav"><button class="btn" onclick="nextStep()">Дальше →</button></div>
|
||
</div>
|
||
<!-- Step 2: Login -->
|
||
<div class="reg-step" data-step="2">
|
||
<h2>🔑 Придумай логин и пароль</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)">🤖 <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()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
<!-- Step 3: Sport -->
|
||
<div class="reg-step" data-step="3">
|
||
<h2>🏊 Твой вид спорта</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)">🤖 <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()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: Role -->
|
||
<div class="reg-step" data-step="4">
|
||
<h2>👤 Кто ты?</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)">🤖 <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')">🏊 Спортсмен</button>
|
||
<button class="role-btn" data-role="coach" onclick="selectRole('coach')">🏋️ Тренер</button>
|
||
<button class="role-btn" data-role="parent" onclick="selectRole('parent')">👨‍👦 Родитель</button>
|
||
</div>
|
||
<input type="text" id="rChildName" placeholder="Имя ребёнка" style="display:none">
|
||
<div class="reg-nav">
|
||
<button class="btn outline" onclick="prevStep()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 5: Birth & Avatar -->
|
||
<div class="reg-step" data-step="5">
|
||
<h2>🎂 Дата рождения</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)">🤖 <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">📷 Выбери аватарку:</p>
|
||
<div class="avatar-picker" id="avatarPicker">
|
||
<div class="avatar-opt selected" data-emoji="🏊">🏊</div>
|
||
<div class="avatar-opt" data-emoji="🏃">🏃</div>
|
||
<div class="avatar-opt" data-emoji="🏋">🏋</div>
|
||
<div class="avatar-opt" data-emoji="⚽">⚽</div>
|
||
<div class="avatar-opt" data-emoji="🏀">🏀</div>
|
||
<div class="avatar-opt" data-emoji="🥊">🥊</div>
|
||
<div class="avatar-opt" data-emoji="🏈">🏈</div>
|
||
<div class="avatar-opt" data-emoji="🎾">🎾</div>
|
||
</div>
|
||
<label class="file-btn" style="text-align:center;width:100%">📷 Или загрузи своё фото<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()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
<!-- Step 6: Location -->
|
||
<div class="reg-step" data-step="6">
|
||
<h2>🌍 Где ты?</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)">🤖 <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()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
<!-- Step 7: Club & Coach -->
|
||
<div class="reg-step" data-step="7">
|
||
<h2>🏫 Клуб и тренер</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)">🤖 <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()">← Назад</button>
|
||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||
</div>
|
||
</div>
|
||
<!-- Step 8: Contacts -->
|
||
<div class="reg-step" data-step="8">
|
||
<h2>📱 Контакты</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)">🤖 <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()">← Назад</button>
|
||
<button class="btn" onclick="finishReg()">✅ Завершить!</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('⏰ Сессия истекла. Войди заново.'); 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 => ({'<':'<','>':'>','"':'"',"'":''','&':'&'}[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: '🟡 Слабый пароль', color: '#FFD700' };
|
||
if(score <= 3) return { ok: true, msg: '🟢 Средний пароль', color: '#4CAF50' };
|
||
return { ok: true, msg: '🟢🟢🟢 Сильный пароль', 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>🛡 Админ-панель</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||'—'} | ⭐ ${u.stars||0}</div>
|
||
</div>
|
||
<div>
|
||
<button class="btn danger small" onclick="adminDeleteUser(${u.id})">🗑</button>
|
||
<button class="btn small outline" onclick="adminResetPass(${u.id})">🔒</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
h += '<button class="btn small outline" onclick="showPage(currentPage)" style="margin-top:8px">← Назад</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 = '🛡 Админ';
|
||
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('🛡 Режим администратора');
|
||
}
|
||
};
|
||
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='🏊', 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='🏊';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('🎉 Профиль создан! Добро пожаловать в Галикон!');
|
||
}
|
||
|
||
// === 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:'👤',label:'Профиль'},
|
||
{page:'students',icon:'👥',label:'Ученики'},
|
||
{page:'chat',icon:'💬',label:'Чаты'},
|
||
{page:'tools',icon:'⚙',label:'Инструменты'}
|
||
];
|
||
}else if(role==='parent'){
|
||
items=[
|
||
{page:'profile',icon:'👤',label:'Профиль'},
|
||
{page:'child',icon:'👶',label:'Ребёнок'},
|
||
{page:'chat',icon:'💬',label:'Чаты'},
|
||
{page:'tools',icon:'⚙',label:'Инструменты'}
|
||
];
|
||
}else{
|
||
items=[
|
||
{page:'profile',icon:'👤',label:'Профиль'},
|
||
{page:'diary',icon:'📖',label:'Дневник'},
|
||
{page:'calendar',icon:'📅',label:'Календарь'},
|
||
{page:'chat',icon:'💬',label:'Чаты'},
|
||
{page:'tools',icon:'⚙',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>🛡 Администратор</h3><p class="muted">Чаты доступны только пользователям.</p><button class="btn small outline" onclick="showAdminPanel()">← Панель</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:'🏊',name:'Первая тренировка',cond:diary.length>=1},
|
||
{icon:'📖',name:'10 тренировок',cond:diary.length>=10},
|
||
{icon:'🔥',name:'30 дней без пропуска',cond:diary.length>=30},
|
||
{icon:'🏆',name:'Рекордсмен',cond:!!(user.achievements&&user.achievements.length)},
|
||
{icon:'🎯',name:'Снайпер',cond:!!(user.guessWon&&user.guessWon>0)},
|
||
{icon:'⚡',name:'Молния',cond:reactBest<300},
|
||
{icon:'🧠',name:'Знаток',cond:!!(user.quizScore&&user.quizScore>=5)},
|
||
{icon:'💪',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'?'🏋️ Тренер':u.role==='parent'?'👨‍👦 Родитель'+(u.childName?': '+u.childName:''):'🏊 Спортсмен';
|
||
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)">🎯 ${u.goal}</div>`:''}
|
||
</div>
|
||
<div class="card">
|
||
<h3>🏅 Значки (${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>📋 Информация</h3>
|
||
<div style="font-size:14px;line-height:2">
|
||
${u.birth?`🎂 ${u.birth} (${u.age||'?'} лет)<br>`:''}
|
||
${u.country?`🌍 ${u.country}${u.city?', '+u.city:''}<br>`:''}
|
||
${u.club?`🏫 ${u.club}<br>`:''}
|
||
${u.coach?`👨‍🏫 Тренер: ${u.coach}<br>`:''}
|
||
${u.phone?`📱 ${u.phone}<br>`:''}
|
||
${u.email?`✉️ ${u.email}<br>`:''}
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>🏆 Достижения</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">📄 Отчёт для тренера</button>
|
||
<button class="btn danger" onclick="doLogout()" style="margin-top:8px">🚪 Выйти</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('Достижение добавлено!');
|
||
}
|
||
|
||
// === 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>✏ Новая запись</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">💭</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">📏 ${e.km} км | ⏱ ${e.time} | 🌟 ${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>➕ Добавить событие</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">📅 Предстоящие</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">📅</div>Нет предстоящих событий</div>'}
|
||
if(past.length){
|
||
h+='<h3 style="color:var(--gray-500);margin:16px 0 8px">✅ Прошедшие</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">👥</div>Нет учеников, привязанных к вам.<p class="muted">Спортсмен должен указать ваше ФИО в поле «Тренер» при регистрации.</p></div>`;
|
||
}
|
||
let h='<h3 style="color:var(--cyan);margin-bottom:12px">👥 Мои ученики</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">🎯 ${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>👶 Мой ребёнок</h3>
|
||
<p class="muted">Введи логин ребёнка, чтобы видеть его профиль</p>
|
||
<input type="text" id="childLoginInput" placeholder="Логин ребёнка">
|
||
<button class="btn" onclick="linkChild()" style="margin-top:8px">🔗 Привязать</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>🏅 Значки (${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>📖 Дневник тренировок</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} | 🌟${e.feel}/5</div>`).join(''):'<p class="muted">Нет записей</p>'}
|
||
</div>
|
||
${child.goal?`<div class="card"><h3>🎯 Цель</h3><p>${child.goal}</p></div>`:''}
|
||
${child.coach?`<div class="card"><h3>👨‍🏫 Тренер</h3><p>${child.coach}</p></div>`:''}
|
||
<button class="btn danger small" onclick="unlinkChild()">❌ Отвязать</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()">💬 Все</button><button class="chat-tab" onclick="chatFilter=\'athlete\';renderPage()">🏊 Спортсмены</button><button class="chat-tab" onclick="chatFilter=\'coach\';renderPage()">🏋 Тренеры</button><button class="chat-tab" onclick="chatFilter=\'parent\';renderPage()">👨‍👦 Родители</button></div>';
|
||
// Games button
|
||
h+='<button class="btn outline" onclick="renderGames()" style="width:100%;margin-bottom:8px">🎮 Игры</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">💬</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()">←</button>
|
||
<strong>${other?other.name:'Чат'}</strong>
|
||
</div>
|
||
<div class="chat-messages" id="chatMsgs">`;
|
||
if(!msgs.length)h+='<div class="empty-state"><div class="big">💬</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()">➤</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>🎮 Игры в чате</h3>
|
||
<p class="muted">Выбери игру и играй с друзьями!</p>
|
||
<button class="quiz-option" onclick="startTicTacToe()">❌⭕ Крестики-нолики</button>
|
||
<button class="quiz-option" onclick="startGuessNumber()">🔢 Угадай число (1-100)</button>
|
||
<button class="quiz-option" onclick="startReaction()">⚡ Реакция — кто быстрее</button>
|
||
<button class="quiz-option" onclick="startQuiz()">🏆 Спорт-викторина</button>
|
||
<button class="btn small outline" onclick="showPage('chat')" style="margin-top:8px">← Назад в чаты</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">❌⭕ Крестики-нолики</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' ? '❌ Крестики' : '⭕ Нолики');
|
||
}
|
||
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>🔢 Угадай число (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">🎉 Угадал! Это ${g.number}!</p><p class="muted">За ${g.attempts} попыток</p>${g.attempts<=5?'<p style=\'color:var(--cyan);font-weight:700\'>+2 ⭐ в рейтинг!</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%">← К играм</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 ? '📈 Больше!' : '📉 Меньше!';
|
||
const tempDiv = document.createElement('div'); tempDiv.innerHTML = `<div class="card" style="text-align:center;padding:12px;margin-bottom:8px"><p>${n} — ${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 = '🔴 Жди зелёный!'; }
|
||
else if(g.state === 'ready'){ bg = '#4CAF50'; txt = '🟢 ЖМИ СЕЙЧАС!'; }
|
||
else if(g.state === 'early'){ bg = '#FFD700'; txt = '⚠ Слишком рано! Жми "Начать"'; }
|
||
else if(g.state === 'done'){ txt = `⏱ Твоё время: <strong>${g.reaction} мс</strong>${g.reactStar?'<br><span style=\'color:var(--cyan);font-weight:700\'>+1 ⭐ быстрее 300 мс!</span>':''}<br>Рекорд: ${g.best !== 9999 ? g.best+' мс' : '—'}`; }
|
||
let h = `<div class="card" style="text-align:center">
|
||
<h3>⚡ Реакция</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||'🎯 Нажми "Начать"'}</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%">← К играм</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>🏆 Викторина завершена!</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} ⭐ в рейтинг!</p>`:''}<p class="muted">${g.score>=6?'👑 Ты знаток спорта!':g.score>=4?'👍 Неплохо!':'📚 Учи матчасть!'}</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>❓ Вопрос ${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">Дальше →</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>🏊 Ты ('+userAge+' лет)</strong> → <span style="color:var(--cyan)">'+userTime+' сек</span>');
|
||
const kmsTime=25.0;
|
||
if(userTime<=kmsTime){
|
||
lines.push('<strong>🏅 КМС (25.00)</strong> → '+kmsTime+' сек');
|
||
lines.push('<span style="color:var(--gray-500)">✅ Ты уже выполнил КМС!</span>');
|
||
}else{
|
||
const diffKms=(userTime-kmsTime).toFixed(2);
|
||
lines.push('<strong>🏅 КМС (25.00)</strong> → '+kmsTime+' сек <span style="color:var(--red)">(нужно сбросить '+diffKms+' сек)</span>');
|
||
}
|
||
if(nearest){
|
||
lines.push('<strong>👑 Калеб Дрессел в '+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">🏆 Ты быстрее Дрессела на '+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='⏸ Стоп';}
|
||
else{v.pause();btn.innerHTML='▶ Старт';}
|
||
}
|
||
function renderToolsPage(){
|
||
return `
|
||
<div class="card">
|
||
<h3>📊 Нормативы (плавание, 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>🏊 Сравнение с чемпионами</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>🎬 Анализ техники</h3>
|
||
<p class="muted">Загрузи видео своего плавания и смотри покадрово</p>
|
||
<label class="file-btn" style="width:100%;text-align:center">📁 Выбрать видео<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 сек">⏮</button>
|
||
<button onclick="videoStep(-0.033)" title="Назад 1 кадр">◀</button>
|
||
<span class="frame-counter" id="frameCounter">0.00 с</span>
|
||
<button onclick="videoStep(0.033)" title="Вперёд 1 кадр">▶</button>
|
||
<button onclick="videoStep(0.1)" title="Вперёд 0.1 сек">⏭</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">▶ Старт</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>💊 Здоровье</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>📚 Видеоуроки</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>⭐ Рейтинг</h3>
|
||
<p class="muted">Топ спортсменов (по голосам):</p>
|
||
<div id="rankList"></div>
|
||
<button class="btn small outline" onclick="voteRandom()" style="margin-top:8px">⭐ Проголосовать</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">⭐ ${u.stars||0}${u.quizScore?` | 🏆 ${u.quizScore}/8`:''}${u.gamesWon?` | 🎮 ${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>
|