feat: role selection, badges, calendar, coach/parent dashboard, champion comparison, video analysis, PDF report

This commit is contained in:
Dauren777 2026-06-01 11:34:30 +00:00
parent b67252633a
commit b120586a01

View File

@ -130,6 +130,43 @@ th{color:var(--gray-500);font-size:11px;text-transform:uppercase}
label.file-btn{display:inline-block;background:#1a2332;padding:12px 20px;border-radius:12px;font-weight:600;cursor:pointer;font-size:14px;margin:8px 0}
label.file-btn:active{background:#2a3342}
input[type=file]{display:none}
/* ROLE BUTTONS */
.role-btn{width:100%;padding:16px;background:#1a2332;border:2px solid #2a3342;border-radius:14px;color:var(--white);font-size:18px;font-weight:600;cursor:pointer;text-align:center;transition:all .2s}
.role-btn.selected{border-color:var(--cyan);background:rgba(0,229,255,0.08)}
.role-btn:active{background:#2a3342}
/* BADGES */
.badge-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.badge-item{display:flex;align-items:center;gap:4px;background:#1a2332;border:1px solid #2a3342;border-radius:12px;padding:8px 12px;font-size:13px;transition:all .2s}
.badge-item .bi{font-size:20px}
.badge-item.earned{border-color:var(--cyan);background:rgba(0,229,255,0.06)}
.badge-item.locked{opacity:.4}
/* CALENDAR */
.event-card{background:#1a2332;border-radius:12px;padding:14px;margin-bottom:8px;border-left:3px solid var(--cyan)}
.event-card .ev-date{font-size:12px;color:var(--cyan);font-weight:700}
.event-card .ev-title{font-size:15px;font-weight:600;margin:2px 0}
.event-card .ev-loc{font-size:12px;color:var(--gray-500)}
/* VIDEO FRAME CONTROLS */
.video-wrap{position:relative;margin:8px 0}
.video-wrap video{width:100%;border-radius:12px;background:#000}
.frame-controls{display:flex;align-items:center;gap:8px;justify-content:center;margin-top:8px}
.frame-controls button{width:48px;height:48px;border-radius:50%;background:#1a2332;border:2px solid #2a3342;color:var(--white);font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.frame-controls button:active{background:#2a3342}
.frame-counter{font-size:17px;font-weight:700;color:var(--cyan);min-width:80px;text-align:center}
@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}
.card .muted{color:#555!important}
.badge-item{background:#eee!important;border:1px solid #ccc!important;color:#000!important}
.profile-header .avatar{background:#00E5FF!important}
}
</style>
</head>
<body>
@ -154,7 +191,7 @@ input[type=file]{display:none}
<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>/7</span>
<span style="font-weight:700;color:var(--cyan)">Шаг <span id="stepNum">1</span>/8</span>
</div>
<div class="step-indicator" id="stepDots"></div>
@ -186,8 +223,25 @@ input[type=file]{display:none}
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 4: Birth & Avatar -->
<div class="reg-step" data-step="4">
<!-- Step 4: Role -->
<div class="reg-step" data-step="5">
<h2>&#x1F464; Кто ты?</h2>
<p class="hint">Выбери свою роль в спорте</p>
<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>
<input type="date" id="rBirth" onchange="autoAge()">
@ -210,8 +264,8 @@ input[type=file]{display:none}
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 5: Location -->
<div class="reg-step" data-step="5">
<!-- Step 6: Location -->
<div class="reg-step" data-step="8">
<h2>&#x1F30D; Где ты?</h2>
<p class="hint">Страна и город</p>
<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>
@ -222,8 +276,8 @@ input[type=file]{display:none}
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 6: Club & Coach -->
<div class="reg-step" data-step="6">
<!-- Step 7: Club & Coach -->
<div class="reg-step" data-step="8">
<h2>&#x1F3EB; Клуб и тренер</h2>
<p class="hint">Где и с кем ты тренируешься</p>
<input type="text" id="rClub" placeholder="Клуб / спортивная школа">
@ -235,8 +289,8 @@ input[type=file]{display:none}
<button class="btn" onclick="nextStep()">Дальше &#x2192;</button>
</div>
</div>
<!-- Step 7: Contacts -->
<div class="reg-step" data-step="7">
<!-- Step 8: Contacts -->
<div class="reg-step" data-step="8">
<h2>&#x1F4F1; Контакты</h2>
<p class="hint">Телефон и email — чтобы тренер и родители могли связаться</p>
<input type="tel" id="rPhone" placeholder="Телефон (WhatsApp)">
@ -251,12 +305,7 @@ input[type=file]{display:none}
<!-- MAIN APP SCREEN -->
<div class="screen" id="appScreen">
<div class="content" id="mainContent"></div>
<div class="bottom-nav">
<button class="nav-item active" data-page="profile" onclick="showPage('profile')"><span class="icon">&#x1F464;</span>Профиль</button>
<button class="nav-item" data-page="diary" onclick="showPage('diary')"><span class="icon">&#x1F4D6;</span>Дневник</button>
<button class="nav-item" data-page="chat" onclick="showPage('chat')"><span class="icon">&#x1F4AC;</span>Чаты</button>
<button class="nav-item" data-page="tools" onclick="showPage('tools')"><span class="icon">&#x2699;</span>Инструменты</button>
</div>
<div class="bottom-nav" id="bottomNav"></div>
</div>
<div class="toast" id="toast"></div>
@ -278,7 +327,9 @@ function doLogin(){
const users=LS('users')||[];
const user=users.find(x=>x.login===u&&x.pass===p);
if(!user){showErr('Неверный логин или пароль');return}
if(!user.role) user.role='athlete';
currentUser=user;
currentPage='profile';
document.getElementById('loginScreen').classList.remove('active');
document.getElementById('appScreen').classList.add('active');
hideErr();
@ -288,12 +339,12 @@ function showErr(m){const e=document.getElementById('loginErr');e.textContent=m;
function hideErr(){document.getElementById('loginErr').style.display='none'}
// === REGISTRATION ===
let regStep=1, regPhoto=null, regAvatar='&#x1F3CA;';
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; showRegStep(1);
regStep=1; regRole=null; showRegStep(1);
buildStepDots();
}
function backToLogin(){
@ -309,7 +360,7 @@ function showRegStep(n){
updateStepDots();
}
function buildStepDots(){
let h=''; for(let i=1;i<=7;i++)h+=`<div class="step-dot${i<=regStep?' done':''}${i===regStep?' current':''}"></div>`;
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(){
@ -318,11 +369,21 @@ function updateStepDots(){
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<7){buildStepDots();showRegStep(regStep+1)}else finishReg()
if(regStep===4){if(!regRole){toast('Выбери свою роль!');return}if(regRole==='parent'&&!document.getElementById('rChildName').value.trim()){toast('Введи имя ребёнка!');return}}
if(regStep<8){buildStepDots();showRegStep(regStep+1)}else finishReg()
}
function prevStep(){if(regStep>1)showRegStep(regStep-1);buildStepDots()}
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}
@ -346,10 +407,13 @@ function finishReg(){
const phone=document.getElementById('rPhone').value.trim();
const email=document.getElementById('rEmail').value.trim();
if(!name||!login||!pass||!sport){toast('Заполни обязательные поля!');return}
const childName=document.getElementById('rChildName').value.trim();
if(!name||!login||!pass||!sport||!regRole){toast('Заполни обязательные поля!');return}
const user={
id:Date.now(),name,login,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:[]
@ -362,9 +426,11 @@ function finishReg(){
document.getElementById('regScreen').classList.remove('active');
document.getElementById('appScreen').classList.add('active');
// reset form
regStep=1;regPhoto=null;regAvatar='&#x1F3CA;';
['rName','rLogin','rPass','rSport','rBirth','rAge','rCountry','rCity','rClub','rCoach','rRank','rGoal','rPhone','rEmail'].forEach(id=>{const el=document.getElementById(id);if(el){if(el.tagName==='SELECT')el.selectedIndex=0;else el.value=''}});
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; Профиль создан! Добро пожаловать в Галикон!');
}
@ -376,6 +442,41 @@ 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;
@ -387,23 +488,51 @@ function renderPage(){
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>${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">
@ -426,6 +555,7 @@ function renderProfile(){
<button class="btn small" onclick="saveAchievement()">Сохранить</button>
</div>
</div>
<button class="btn small" onclick="openPdfReport()" style="margin-top:8px">&#x1F4C4; Отчёт для тренера</button>
<button class="btn danger" onclick="doLogout()" style="margin-top:8px">&#x1F6AA; Выйти</button>
`;return h;
}
@ -446,6 +576,37 @@ function saveAchievement(){
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');
@ -479,6 +640,131 @@ function addDiary(){
}
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();
@ -503,7 +789,7 @@ function renderChatList(){
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">${u.name}${u.role?` <span class="badge blue" style="font-size:9px">${u.role}</span>`:''}</div><div class="last">${last?last.text:'Начни общение'}</div></div>
<div class="info"><div class="name">${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>`;
});
@ -631,7 +917,7 @@ function doGuess(){
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; SS('users', users); currentUser = users[idx]; }
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; Меньше!';
@ -707,7 +993,7 @@ function renderQuiz(){
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 = g.score; SS('users', users); currentUser = users[idx]; }
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;
@ -752,6 +1038,69 @@ function createGroupChat(){
}
// === 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">
@ -766,6 +1115,43 @@ function renderToolsPage(){
<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>
@ -856,6 +1242,7 @@ function updateCities(){
// === RENDER ALL ===
function renderAll(){
renderBottomNav();
renderPage();
setTimeout(()=>{renderVitamins();renderRanking()},200);
}