feat: role selection, badges, calendar, coach/parent dashboard, champion comparison, video analysis, PDF report
This commit is contained in:
parent
b67252633a
commit
b120586a01
437
index.html
437
index.html
@ -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()">← Назад</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()">Дальше →</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>👤 Кто ты?</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')">🏊 Спортсмен</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>
|
||||
<input type="date" id="rBirth" onchange="autoAge()">
|
||||
@ -210,8 +264,8 @@ input[type=file]{display:none}
|
||||
<button class="btn" onclick="nextStep()">Дальше →</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step 5: Location -->
|
||||
<div class="reg-step" data-step="5">
|
||||
<!-- Step 6: Location -->
|
||||
<div class="reg-step" data-step="8">
|
||||
<h2>🌍 Где ты?</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()">Дальше →</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>🏫 Клуб и тренер</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()">Дальше →</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step 7: Contacts -->
|
||||
<div class="reg-step" data-step="7">
|
||||
<!-- Step 8: Contacts -->
|
||||
<div class="reg-step" data-step="8">
|
||||
<h2>📱 Контакты</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">👤</span>Профиль</button>
|
||||
<button class="nav-item" data-page="diary" onclick="showPage('diary')"><span class="icon">📖</span>Дневник</button>
|
||||
<button class="nav-item" data-page="chat" onclick="showPage('chat')"><span class="icon">💬</span>Чаты</button>
|
||||
<button class="nav-item" data-page="tools" onclick="showPage('tools')"><span class="icon">⚙</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='🏊';
|
||||
let regStep=1, regPhoto=null, regAvatar='🏊', 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='🏊';
|
||||
['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='🏊';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('🎉 Профиль создан! Добро пожаловать в Галикон!');
|
||||
}
|
||||
@ -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:'👤',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;
|
||||
@ -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:'🏊',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>${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">
|
||||
@ -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">📄 Отчёт для тренера</button>
|
||||
<button class="btn danger" onclick="doLogout()" style="margin-top:8px">🚪 Выйти</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>➕ Добавить событие</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();
|
||||
@ -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 ? '📈 Больше!' : '📉 Меньше!';
|
||||
@ -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>🏆 Викторина завершена!</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;
|
||||
@ -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>🏊 Ты ('+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">
|
||||
@ -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>🏊 Сравнение с чемпионами</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>
|
||||
@ -856,6 +1242,7 @@ function updateCities(){
|
||||
|
||||
// === RENDER ALL ===
|
||||
function renderAll(){
|
||||
renderBottomNav();
|
||||
renderPage();
|
||||
setTimeout(()=>{renderVitamins();renderRanking()},200);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user