v3 — PWA установка на телефон + рейтинг тренеров и спортсменов

This commit is contained in:
Dauren777 2026-06-01 10:29:24 +00:00
parent 4cec2866c5
commit d37012215b
3 changed files with 127 additions and 0 deletions

View File

@ -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">&#x1F4F7; Фото</button>
<button class="tab-btn" data-tab="grades">&#x1F393; Оценки</button>
<button class="tab-btn" data-tab="lessons">&#x1F4DA; Уроки</button>
<button class="tab-btn" data-tab="ranking">&#x2B50; Рейтинг</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">&#x1F3C6;</span>Рейтинг спортсменов</h3>
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">&#x2B50; = голоса родителей. Нажми, чтобы поддержать!</p>
<div id="swimmer-ranking"><div class="empty"><div class="big">&#x1F3CA;</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">&#x1F468;&#x200D;&#x1F3EB;</span>Рейтинг тренеров</h3>
<p style="color:var(--gray-500);font-size:13px;margin-bottom:12px">&#x2B50; = голоса родителей. Оцени тренера!</p>
<div id="coach-ranking"><div class="empty"><div class="big">&#x1F3CB;</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">&#x1F3CA;</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})">&#x2B50; ${s.stars}</button></div></div>`).join('');
}
const coEl = document.getElementById('coach-ranking');
if (!coaches.length) {
coEl.innerHTML = '<div class="empty"><div class="big">&#x1F3CB;</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})">&#x2B50; ${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
View 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
View 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)))
)
})