📱 Контакты
Телефон и 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=`
+
+
🏅 Значки (${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('| '+e.date+' | '+e.type+' | '+e.km+' | '+e.time+' | '+e.feel+'/5 |
');});
+ w.document.write('
');
+ }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=`
+
+
+
${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.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.00 | 1:29 | 7:10 |
| 2 юн. | 47.00 | 1:45 | 8:10 |
+
+
🏊 Сравнение с чемпионами
+
Сравни свой прогресс с Калебом Дресселом (50 м в/с)
+
| Возраст | Дрессел (сек) |
+ | 14 лет | 22.5 |
+ | 15 лет | 21.8 |
+ | 16 лет | 20.9 |
+ | 17 лет | 19.8 |
+ | 18 лет | 19.2 |
+
Введи своё лучшее время на 50 м в/с:
+
+
+
+
+
+
+
🎬 Анализ техники
+
Загрузи видео своего плавания и смотри покадрово
+
+
+
+
+
+
+ 0.00 с
+
+
+
+
+
+
+
+
+
+
+
+
💊 Здоровье
Отмечай принятые витамины:
@@ -856,6 +1242,7 @@ function updateCities(){
// === RENDER ALL ===
function renderAll(){
+ renderBottomNav();
renderPage();
setTimeout(()=>{renderVitamins();renderRanking()},200);
}