v3 — PWA установка на телефон + рейтинг тренеров и спортсменов
This commit is contained in:
parent
4cec2866c5
commit
d37012215b
115
index.html
115
index.html
@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
|
||||
<title>Галикон — приложение спортсмена</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Галикон">
|
||||
<meta name="theme-color" content="#0F1218">
|
||||
<style>
|
||||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--red:#FF6B6B;--green:#4CAF50}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
@ -160,6 +165,7 @@ input[type=file]{display:none}
|
||||
<button class="tab-btn" data-tab="photos">📷 Фото</button>
|
||||
<button class="tab-btn" data-tab="grades">🎓 Оценки</button>
|
||||
<button class="tab-btn" data-tab="lessons">📚 Уроки</button>
|
||||
<button class="tab-btn" data-tab="ranking">⭐ Рейтинг</button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
@ -259,6 +265,39 @@ input[type=file]{display:none}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- РЕЙТИНГ -->
|
||||
<div class="tab-content" id="ranking">
|
||||
<div class="card">
|
||||
<h3><span class="emoji">🏆</span>Рейтинг спортсменов</h3>
|
||||
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">⭐ = голоса родителей. Нажми, чтобы поддержать!</p>
|
||||
<div id="swimmer-ranking"><div class="empty"><div class="big">🏊</div>Добавьте спортсменов</div></div>
|
||||
<button class="btn outline" onclick="addSwimmerForm()" style="width:100%;margin-top:8px">+ Добавить спортсмена</button>
|
||||
<div id="swimmer-form" style="display:none;margin-top:12px">
|
||||
<input id="sw-name" placeholder="ФИО спортсмена">
|
||||
<div class="grid2">
|
||||
<input id="sw-club" placeholder="Клуб">
|
||||
<input id="sw-time" placeholder="Лучшее время (50 м в/с)">
|
||||
</div>
|
||||
<button class="btn" onclick="addSwimmer()" style="width:100%">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3><span class="emoji">👨‍🏫</span>Рейтинг тренеров</h3>
|
||||
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">⭐ = голоса родителей. Оцени тренера!</p>
|
||||
<div id="coach-ranking"><div class="empty"><div class="big">🏋</div>Добавьте тренеров</div></div>
|
||||
<button class="btn outline" onclick="addCoachForm()" style="width:100%;margin-top:8px">+ Добавить тренера</button>
|
||||
<div id="coach-form" style="display:none;margin-top:12px">
|
||||
<input id="co-name" placeholder="ФИО тренера">
|
||||
<div class="grid2">
|
||||
<input id="co-club" placeholder="Клуб">
|
||||
<input id="co-exp" placeholder="Стаж (лет)">
|
||||
</div>
|
||||
<textarea id="co-achieve" placeholder="Достижения (воспитанники, разряды...)" rows="2"></textarea>
|
||||
<button class="btn" onclick="addCoach()" style="width:100%">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ВИДЕОУРОКИ -->
|
||||
<div class="tab-content" id="lessons">
|
||||
<div class="card">
|
||||
@ -337,6 +376,7 @@ function loginProfile(id) {
|
||||
av.innerHTML = p.name.charAt(0).toUpperCase();
|
||||
}
|
||||
renderAll();
|
||||
renderRankings();
|
||||
}
|
||||
|
||||
function showProfileScreen() {
|
||||
@ -630,6 +670,80 @@ function showLightbox(type, id) {
|
||||
lb.style.display = 'flex';
|
||||
}
|
||||
|
||||
// === RANKING ===
|
||||
function addSwimmerForm() { document.getElementById('swimmer-form').style.display='block'; }
|
||||
function addCoachForm() { document.getElementById('coach-form').style.display='block'; }
|
||||
|
||||
function addSwimmer() {
|
||||
const name = document.getElementById('sw-name').value.trim();
|
||||
if (!name) return toast('Введи ФИО!');
|
||||
const swimmers = LS('swimmers') || [];
|
||||
swimmers.push({ id: Date.now(), name, club: document.getElementById('sw-club').value.trim(), time: document.getElementById('sw-time').value.trim(), stars: 0, voters: [] });
|
||||
SS('swimmers', swimmers);
|
||||
['sw-name','sw-club','sw-time'].forEach(id => document.getElementById(id).value='');
|
||||
document.getElementById('swimmer-form').style.display='none';
|
||||
renderRankings();
|
||||
toast('Спортсмен добавлен!');
|
||||
}
|
||||
|
||||
function addCoach() {
|
||||
const name = document.getElementById('co-name').value.trim();
|
||||
if (!name) return toast('Введи ФИО!');
|
||||
const coaches = LS('coaches') || [];
|
||||
coaches.push({ id: Date.now(), name, club: document.getElementById('co-club').value.trim(), exp: document.getElementById('co-exp').value.trim(), achieve: document.getElementById('co-achieve').value.trim(), stars: 0, voters: [] });
|
||||
SS('coaches', coaches);
|
||||
['co-name','co-club','co-exp','co-achieve'].forEach(id => document.getElementById(id).value='');
|
||||
document.getElementById('coach-form').style.display='none';
|
||||
renderRankings();
|
||||
toast('Тренер добавлен!');
|
||||
}
|
||||
|
||||
function voteSwimmer(id) {
|
||||
const swimmers = LS('swimmers') || [];
|
||||
const s = swimmers.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
const vid = pid() + '_s_' + id;
|
||||
if (s.voters.includes(vid)) return toast('Ты уже голосовал!');
|
||||
s.stars++; s.voters.push(vid);
|
||||
SS('swimmers', swimmers);
|
||||
renderRankings();
|
||||
toast('Голос учтён!');
|
||||
}
|
||||
|
||||
function voteCoach(id) {
|
||||
const coaches = LS('coaches') || [];
|
||||
const c = coaches.find(x => x.id === id);
|
||||
if (!c) return;
|
||||
const vid = pid() + '_c_' + id;
|
||||
if (c.voters.includes(vid)) return toast('Ты уже голосовал!');
|
||||
c.stars++; c.voters.push(vid);
|
||||
SS('coaches', coaches);
|
||||
renderRankings();
|
||||
toast('Голос учтён!');
|
||||
}
|
||||
|
||||
function renderRankings() {
|
||||
const swimmers = LS('swimmers') || [];
|
||||
const coaches = LS('coaches') || [];
|
||||
|
||||
const swEl = document.getElementById('swimmer-ranking');
|
||||
if (!swimmers.length) {
|
||||
swEl.innerHTML = '<div class="empty"><div class="big">🏊</div>Добавьте спортсменов</div>';
|
||||
} else {
|
||||
swEl.innerHTML = [...swimmers].sort((a,b)=>b.stars-a.stars).map((s,i)=>`<div class="card" style="padding:14px;margin-bottom:6px"><div style="display:flex;align-items:center;gap:10px"><div style="width:28px;height:28px;border-radius:50%;background:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--gray-100)'};color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex-shrink:0">${i+1}</div><div style="flex:1"><strong>${s.name}</strong><div style="font-size:11px;color:var(--gray-500)">${[s.club,s.time].filter(Boolean).join(' · ')}</div></div><button class="btn small" onclick="voteSwimmer(${s.id})">⭐ ${s.stars}</button></div></div>`).join('');
|
||||
}
|
||||
|
||||
const coEl = document.getElementById('coach-ranking');
|
||||
if (!coaches.length) {
|
||||
coEl.innerHTML = '<div class="empty"><div class="big">🏋</div>Добавьте тренеров</div>';
|
||||
} else {
|
||||
coEl.innerHTML = [...coaches].sort((a,b)=>b.stars-a.stars).map((c,i)=>`<div class="card" style="padding:14px;margin-bottom:6px"><div style="display:flex;align-items:center;gap:10px"><div style="width:28px;height:28px;border-radius:50%;background:${i===0?'#FFD700':i===1?'#C0C0C0':i===2?'#CD7F32':'var(--gray-100)'};color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex-shrink:0">${i+1}</div><div style="flex:1"><strong>${c.name}</strong><div style="font-size:11px;color:var(--gray-500)">${[c.club,c.exp?c.exp+' лет':null,c.achieve].filter(Boolean).join(' · ')}</div></div><button class="btn small" onclick="voteCoach(${c.id})">⭐ ${c.stars}</button></div></div>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// PWA
|
||||
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(function(){}); }
|
||||
|
||||
// === LESSONS ===
|
||||
const lessons = [
|
||||
{ title: 'Техника старта с тумбы', q: 'swimming start technique tutorial' },
|
||||
@ -658,6 +772,7 @@ function renderAll() {
|
||||
renderVideos();
|
||||
renderPhotos();
|
||||
renderGrades();
|
||||
renderRankings();
|
||||
}
|
||||
|
||||
// === INIT ===
|
||||
|
||||
1
manifest.json
Normal file
1
manifest.json
Normal file
@ -0,0 +1 @@
|
||||
{"name":"Галикон","short_name":"Галикон","description":"Приложение для спортсменов — дневник тренировок, здоровье, рейтинги","start_url":".","display":"standalone","background_color":"#0F1218","theme_color":"#00E5FF","icons":[{"src":"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%230F1218'/><text y='.9em' font-size='70' x='50' text-anchor='middle'>🏊</text></svg>","sizes":"any","type":"image/svg+xml"}]}
|
||||
11
sw.js
Normal file
11
sw.js
Normal file
@ -0,0 +1,11 @@
|
||||
self.addEventListener('install', e => { self.skipWaiting() })
|
||||
self.addEventListener('activate', e => { e.waitUntil(clients.claim()) })
|
||||
self.addEventListener('fetch', e => {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(r => r || fetch(e.request).then(res => {
|
||||
const clone = res.clone()
|
||||
caches.open('galikon-v1').then(c => c.put(e.request, clone))
|
||||
return res
|
||||
}).catch(() => caches.match(e.request)))
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user