diff --git a/index.html b/index.html index 21cf56f..5a028ca 100644 --- a/index.html +++ b/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} +} + @@ -154,7 +191,7 @@ input[type=file]{display:none}
- Шаг 1/7 + Шаг 1/8
@@ -186,8 +223,25 @@ input[type=file]{display:none}
- -
+ + +
+

👤 Кто ты?

+

Выбери свою роль в спорте

+
+ + + +
+ +
+ + +
+
+ + +

🎂 Дата рождения

Возраст посчитается автоматически

@@ -210,8 +264,8 @@ input[type=file]{display:none}
- -
+ +

🌍 Где ты?

Страна и город

@@ -222,8 +276,8 @@ input[type=file]{display:none}
- -
+ +

🏫 Клуб и тренер

Где и с кем ты тренируешься

@@ -235,8 +289,8 @@ input[type=file]{display:none}
- -
+ +

📱 Контакты

Телефон и email — чтобы тренер и родители могли связаться

@@ -251,12 +305,7 @@ input[type=file]{display:none}
-
- - - - -
+
@@ -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+=`
`; + let h=''; for(let i=1;i<=8;i++)h+=`
`; 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(){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=>``).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=`
${u.photo?``:av}

${u.name}

${u.sport}
+
${roleLabel}
${u.rank?`
${u.rank}
`:''} ${u.goal?`
🎯 ${u.goal}
`:''}
+
+

🏅 Значки (${earnedCount}/${badges.length})

+
${badges.map(b=>`
${b.icon}${b.name}
`).join('')}
+

📋 Информация

@@ -426,6 +555,7 @@ function renderProfile(){
+ `;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('Отчёт — '+u.name+''); + w.document.write('

🏊 Галикон — Отчёт спортсмена

'); + w.document.write('

Имя: '+u.name+'

'); + w.document.write('

Вид спорта: '+u.sport+'

'); + if(u.club) w.document.write('

Клуб: '+u.club+'

'); + if(u.coach) w.document.write('

Тренер: '+u.coach+'

'); + if(u.rank) w.document.write('

Разряд: '+u.rank+'

'); + if(u.goal) w.document.write('

Цель: '+u.goal+'

'); + if(u.birth) w.document.write('

Дата рождения: '+u.birth+' ('+(u.age||'?')+' лет)

'); + w.document.write('

🏅 Значки

'); + w.document.write('

'+(badges.length?badges.map(b=>''+b.icon+' '+b.name+'').join(' '):'Нет значков')+'

'); + w.document.write('

🏆 Достижения

'); + if(u.achievements&&u.achievements.length){ + u.achievements.forEach(a=>{w.document.write('

'+a.title+'
'+a.date+' · '+(a.desc||'')+'

');}); + }else{w.document.write('

Нет достижений

');} + w.document.write('

📖 Последние записи в дневнике

'); + if(diary.length){ + w.document.write(''); + diary.forEach(e=>{w.document.write('');}); + w.document.write('
ДатаТипКмВремяСамочувствие
'+e.date+''+e.type+''+e.km+''+e.time+''+e.feel+'/5
'); + }else{w.document.write('

Нет записей

');} + w.document.write('

Отчёт сгенерирован в Галиконе — '+new Date().toLocaleDateString('ru-RU')+'

'); + 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.dateb.date.localeCompare(a.date)); + let h=` +
+

➕ Добавить событие

+ + + + +
`; + if(upcoming.length){ + h+='

📅 Предстоящие

'; + upcoming.forEach(e=>{h+=`
${e.date}
${e.title}
${e.location||'—'}
`}); + }else{h+='
📅
Нет предстоящих событий
'} + if(past.length){ + h+='

✅ Прошедшие

'; + past.forEach(e=>{h+=`
${e.date}
${e.title}
${e.location||'—'}
`}); + } + 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 `
👥
Нет учеников, привязанных к вам.

Спортсмен должен указать ваше ФИО в поле «Тренер» при регистрации.

`; + } + let h='

👥 Мои ученики

'; + 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+=` +
+
+
${a.photo?``:av}
+
${a.name}
${a.sport} · ${a.rank||'без разряда'} · Всего: ${totalKm.toFixed(1)} км
+
+ ${a.goal?`
🎯 ${a.goal}
`:''} +

Последние тренировки:

+ ${latest.length?latest.map(e=>`
${e.date} — ${e.type} | ${e.km} км | ${e.time}
`).join(''):'
Нет записей
'} +
`; + }); + return h; +} + +// === PARENT DASHBOARD === +function renderChildPage(){ + const childId=getMy('childId'); + if(!childId){ + return `
+

👶 Мой ребёнок

+

Введи логин ребёнка, чтобы видеть его профиль

+ + +
`; + } + 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=` +
+
${child.photo?``:av}
+

${child.name}

+
${child.sport}
+ ${child.rank?`
${child.rank}
`:''} +
+
+
${diary.length}
тренировок
+
${totalKm.toFixed(1)}
км всего
+
${avgFeel}
средн. самочувствие
+
+
+

🏅 Значки (${earnedCount}/${badges.length})

+
${badges.map(b=>`
${b.icon}${b.name}
`).join('')}
+
+
+

📖 Дневник тренировок

+ ${diary.length?diary.slice(0,15).map(e=>`
${e.date} — ${e.type} | ${e.km} км | ${e.time} | 🌟${e.feel}/5
`).join(''):'

Нет записей

'} +
+ ${child.goal?`

🎯 Цель

${child.goal}

`:''} + ${child.coach?`

👨‍🏫 Тренер

${child.coach}

`:''} + + `; + 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+=`
${u.photo?``:u.avatar||u.name.charAt(0)}
-
${u.name}${u.role?` ${u.role}`:''}
${last?last.text:'Начни общение'}
+
${u.name}${u.role?` ${u.role==='coach'?'Тренер':u.role==='parent'?'Родитель':'Спортсмен'}`:''}
${last?last.text:'Начни общение'}
${unread?`${unread}`:''}
`; }); @@ -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 = `

🏆 Викторина завершена!

${g.score} / ${g.questions.length}

${earned>0?`

+${earned} ⭐ в рейтинг!

`:''}

${g.score>=6?'👑 Ты знаток спорта!':g.score>=4?'👍 Неплохо!':'📚 Учи матчасть!'}

`; 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='Введи время и возраст!';return} + const nearest=dresselData.find(d=>d.age===userAge); + let lines=[]; + lines.push('🏊 Ты ('+userAge+' лет)'+userTime+' сек'); + const kmsTime=25.0; + if(userTime<=kmsTime){ + lines.push('🏅 КМС (25.00) → '+kmsTime+' сек'); + lines.push('✅ Ты уже выполнил КМС!'); + }else{ + const diffKms=(userTime-kmsTime).toFixed(2); + lines.push('🏅 КМС (25.00) → '+kmsTime+' сек (нужно сбросить '+diffKms+' сек)'); + } + if(nearest){ + lines.push('👑 Калеб Дрессел в '+nearest.age+' лет → '+nearest.time+' сек'); + const diff=(userTime-nearest.time).toFixed(2); + if(userTime<=nearest.time){ + lines.push('🏆 Ты быстрее Дрессела на '+Math.abs(diff)+' сек!'); + }else{ + lines.push('Разница: '+diff+' сек. Продолжай тренироваться!'); + } + }else{ + lines.push('Нет данных Дрессела для возраста '+userAge+' лет'); + } + lines.push('* данные приблизительные, для мотивации'); + el.innerHTML=lines.map(l=>'
'+l+'
').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 `
@@ -766,6 +1115,43 @@ function renderToolsPage(){ 1 юн.40.001:297:10 2 юн.47.001:458:10
+
+

🏊 Сравнение с чемпионами

+

Сравни свой прогресс с Калебом Дресселом (50 м в/с)

+ + + + + +
ВозрастДрессел (сек)
14 лет22.5
15 лет21.8
16 лет20.9
17 лет19.8
18 лет19.2
+

Введи своё лучшее время на 50 м в/с:

+ + + +
+
+
+

🎬 Анализ техники

+

Загрузи видео своего плавания и смотри покадрово

+ + +

💊 Здоровье

Отмечай принятые витамины:

@@ -856,6 +1242,7 @@ function updateCities(){ // === RENDER ALL === function renderAll(){ + renderBottomNav(); renderPage(); setTimeout(()=>{renderVitamins();renderRanking()},200); }