🚀 Patch auto

This commit is contained in:
karim hassan
2025-08-24 23:00:40 +00:00
parent 92e6afff00
commit f1103d67a0
1314 changed files with 2511 additions and 562 deletions

View File

@@ -1,14 +1,28 @@
<!doctype html>
<html lang="fr">
<head>
<title>Super Sunday — Admin</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/style.css?v=4" />
<title>Super Sunday — Admin</title>
<link rel="stylesheet" href="/assets/style.css?v=10" />
<style>
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.muted-sm{color:#9fb0c7;font-size:13px}
.toast{position:fixed;right:16px;bottom:16px;padding:12px 16px;
background:#0f1b33;border:1px solid rgba(255,255,255,.2);color:#eaf3ff;
border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.35);display:none;min-width:260px}
.toast.show{display:block;animation:toastIn .18s ease-out}
@keyframes toastIn{from{transform:translateY(6px);opacity:.0}to{transform:translateY(0);opacity:1}}
.spinner{display:inline-block;width:16px;height:16px;border-radius:50%;
border:2px solid rgba(255,255,255,.25);border-top-color:#8fedff;animation:spin .6s linear infinite;margin-left:8px}
@keyframes spin{to{transform:rotate(360deg)}}
.help{font-size:12px;color:#cfe1ff;margin-top:4px}
.sel{display:flex;gap:8px}
.sel select{min-width:160px}
.hide{display:none !important}
</style>
</head>
<body>
<header>
@@ -21,64 +35,85 @@
</header>
<main class="container">
<section class="card accent" style="padding:22px; margin:8px 0 18px;">
<div class="kicker">Organisation</div>
<h2 class="hero-title">Pilote tes tournois</h2>
<p class="muted">Création rapide, génération Americano, scoring en deux clics.</p>
</section>
<!-- Login -->
<section id="loginSection" class="card">
<h2 class="section-title">Connexion</h2>
<input id="email" placeholder="Email" />
<input id="password" type="password" placeholder="Mot de passe" />
<button id="loginBtn" class="btn">Se connecter</button>
<p id="loginStatus" class="muted"></p>
<div class="form-row">
<input id="email" placeholder="Email (ex: admin@supersunday.local)" />
<input id="password" type="password" placeholder="Mot de passe (ex: changeme)" />
</div>
<div class="actions">
<button id="loginBtn" class="btn">Se connecter</button>
<button id="logoutBtn" class="btn-outline">Se déconnecter</button>
<span id="loginStatus" class="muted"></span>
</div>
<p class="muted-sm">Identifiants dans <code>backend/.env</code> (ADMIN_EMAIL / ADMIN_PASSWORD).</p>
</section>
<section id="adminSection" style="display:none;">
<h2 class="section-title">Actions rapides</h2>
<div class="grid">
<div class="card">
<h3>Créer un tournoi</h3>
<input id="t_name" placeholder="Nom" />
<input id="t_loc" placeholder="Lieu" />
<input id="t_sd" type="date" />
<input id="t_ed" type="date" />
<button onclick="createTournament()" class="btn">Créer</button>
<section id="adminSection" class="grid-2 hide">
<!-- Create tournament -->
<div class="card">
<h3 class="section-title">Créer un tournoi</h3>
<input id="t_name" placeholder="Nom du tournoi" />
<input id="t_location" placeholder="Lieu" />
<div class="form-row">
<input id="t_start" type="date" placeholder="Date début" />
<input id="t_end" type="date" placeholder="Date fin" />
</div>
<div class="card">
<h3>Générer Americano</h3>
<input id="g_tid" placeholder="Tournament ID" />
<input id="g_courts" placeholder="Courts (ex: Court 1,Court 2)" />
<input id="g_start" type="datetime-local" />
<input id="g_int" type="number" value="20" />
<button onclick="generateAmericano()" class="btn">Générer</button>
</div>
<div class="card">
<h3>Score Match</h3>
<input id="m_id" placeholder="Match ID" />
<input id="m_a" type="number" placeholder="Score A" />
<input id="m_b" type="number" placeholder="Score B" />
<button onclick="scoreMatch()" class="btn">Valider</button>
<div class="actions">
<button id="createTournamentBtn" class="btn">Créer</button>
<div id="createTournamentSpin" class="spinner hide"></div>
</div>
<div id="createTournamentErr" class="help"></div>
</div>
<h2 class="section-title">Tournaments</h2>
<pre id="adminTournaments"></pre>
<h2 class="section-title">Matches</h2>
<pre id="adminMatches"></pre>
<!-- Add participant -->
<div class="card">
<h3 class="section-title">Ajouter un joueur</h3>
<div class="sel">
<select id="p_tid"></select>
<input id="p_fullname" placeholder="Nom complet" />
</div>
<div class="actions">
<button id="addParticipantBtn" class="btn">Ajouter</button>
<div id="addParticipantSpin" class="spinner hide"></div>
</div>
<div id="addParticipantErr" class="help"></div>
</div>
<!-- Score match -->
<div class="card">
<h3 class="section-title">Scorer un match</h3>
<div class="sel">
<select id="s_tid"></select>
<select id="s_mid"></select>
</div>
<div class="form-row">
<input id="m_a" type="number" placeholder="Score A" min="0" />
<input id="m_b" type="number" placeholder="Score B" min="0" />
</div>
<div class="actions">
<label><input id="m_done" type="checkbox" /> Terminé</label>
<button id="scoreBtn" class="btn">Valider</button>
<div id="scoreSpin" class="spinner hide"></div>
</div>
<div id="scoreErr" class="help"></div>
</div>
<!-- Quick view tournaments -->
<div class="card">
<h3 class="section-title">Tournois (aperçu)</h3>
<div class="actions">
<button id="refreshTournaments" class="btn-outline">Rafraîchir</button>
</div>
<div id="listTournaments" class="muted"></div>
</div>
</section>
</main>
<div class="bg-bubble b1"></div>
<div class="bg-bubble b2"></div>
<div class="bg-bubble b3"></div>
<div class="bg-bubble b4"></div>
<div class="bg-bubble b5"></div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="/assets/api.js"></script>
<script src="/assets/admin.js"></script>
<script type="module" src="/assets/api.js?v=10"></script>
<script type="module" src="/assets/admin.ux.js?v=10"></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,160 @@
import {
bindLoginForm, isAuthenticated, logout,
createTournament, addParticipant, scoreMatch,
listTournaments, listMatches
} from '/assets/api.js?v=10';
const toast = document.getElementById('toast');
const adminSection = document.getElementById('adminSection');
const loginSection = document.getElementById('loginSection');
function showToast(msg, ok=true){
toast.textContent = msg;
toast.style.borderColor = ok ? 'rgba(143,237,255,.45)' : 'rgba(255,120,120,.45)';
toast.classList.add('show');
setTimeout(()=>toast.classList.remove('show'), 2200);
}
function toggleAdminUI(){
const logged = isAuthenticated();
adminSection.classList.toggle('hide', !logged);
loginSection.classList.toggle('hide', logged);
}
function disable(btn, spin, yes){
btn.disabled = !!yes;
spin.classList.toggle('hide', !yes);
}
async function renderTournaments(){
const wrap = document.getElementById('listTournaments');
const p_tid = document.getElementById('p_tid');
const s_tid = document.getElementById('s_tid');
try{
const ts = await listTournaments();
wrap.innerHTML = ts.map(t => `<div class="card"><strong>#${t.id}</strong> — ${t.name} <span class="muted">(${[t.location||'—', t.start_date].filter(Boolean).join(' • ')})</span></div>`).join('') || '<div class="empty">Aucun tournoi.</div>';
const opts = ['<option value="">— choisir un tournoi —</option>'].concat(
ts.map(t => `<option value="${t.id}">#${t.id}${t.name}</option>`)
).join('');
p_tid.innerHTML = opts;
s_tid.innerHTML = opts;
}catch(e){
wrap.innerHTML = `<div class="empty">Erreur chargement: ${e.message}</div>`;
}
}
async function renderMatchesForSelectedTournament(){
const tid = Number(document.getElementById('s_tid').value || '0');
const s_mid = document.getElementById('s_mid');
if(!tid){ s_mid.innerHTML = '<option value="">— choisir un match —</option>'; return; }
try{
const ms = await listMatches(tid);
s_mid.innerHTML = ['<option value="">— choisir un match —</option>'].concat(
ms.map(m => `<option value="${m.id}">#${m.id}${m.team_a} vs ${m.team_b} (${m.court||'—'})</option>`)
).join('');
}catch(e){
s_mid.innerHTML = '<option value="">Erreur chargement matches</option>';
}
}
function bindAuth(){
bindLoginForm({});
const logoutBtn = document.getElementById('logoutBtn');
logoutBtn?.addEventListener('click', ()=>{
logout(); toggleAdminUI(); showToast('Déconnecté.');
});
}
function bindAdminActions(){
document.getElementById('createTournamentBtn')?.addEventListener('click', async (e)=>{
const btn = e.currentTarget;
const spin = document.getElementById('createTournamentSpin');
const err = document.getElementById('createTournamentErr');
err.textContent='';
const name = document.getElementById('t_name').value.trim();
if(!name){ err.textContent='Le nom est requis.'; return; }
const payload = {
name,
location: document.getElementById('t_location').value.trim(),
start_date: document.getElementById('t_start').value || null,
end_date: document.getElementById('t_end').value || null
};
try{
disable(btn, spin, true);
await createTournament(payload);
showToast('Tournoi créé ✔');
document.getElementById('t_name').value='';
document.getElementById('t_location').value='';
document.getElementById('t_start').value='';
document.getElementById('t_end').value='';
renderTournaments();
}catch(e){
err.textContent = e?.payload?.error || e.message;
showToast('Erreur création tournoi', false);
}finally{
disable(btn, spin, false);
}
});
document.getElementById('addParticipantBtn')?.addEventListener('click', async (e)=>{
const btn = e.currentTarget;
const spin = document.getElementById('addParticipantSpin');
const err = document.getElementById('addParticipantErr');
err.textContent='';
const tid = Number(document.getElementById('p_tid').value || '0');
const full_name = document.getElementById('p_fullname').value.trim();
if(!tid){ err.textContent='Choisis un tournoi.'; return; }
if(!full_name){ err.textContent='Nom du joueur requis.'; return; }
try{
disable(btn, spin, true);
await addParticipant(tid, { full_name });
showToast('Joueur ajouté ✔');
document.getElementById('p_fullname').value='';
}catch(e){
err.textContent = e?.payload?.error || e.message;
showToast('Erreur ajout joueur', false);
}finally{
disable(btn, spin, false);
}
});
document.getElementById('s_tid')?.addEventListener('change', renderMatchesForSelectedTournament);
document.getElementById('scoreBtn')?.addEventListener('click', async (e)=>{
const btn = e.currentTarget;
const spin = document.getElementById('scoreSpin');
const err = document.getElementById('scoreErr');
err.textContent='';
const mid = Number(document.getElementById('s_mid').value || '0');
if(!mid){ err.textContent='Choisis un match.'; return; }
const payload = {
score_a: Number(document.getElementById('m_a').value||'0'),
score_b: Number(document.getElementById('m_b').value||'0'),
done: document.getElementById('m_done').checked
};
try{
disable(btn, spin, true);
await scoreMatch(mid, payload);
showToast('Score enregistré ✔');
document.getElementById('m_a').value='';
document.getElementById('m_b').value='';
document.getElementById('m_done').checked=false;
}catch(e){
err.textContent = e?.payload?.error || e.message;
showToast('Erreur scoring', false);
}finally{
disable(btn, spin, false);
}
});
document.getElementById('refreshTournaments')?.addEventListener('click', renderTournaments);
}
function init(){
toggleAdminUI();
bindAuth();
bindAdminActions();
if(isAuthenticated()) renderTournaments();
}
document.addEventListener('DOMContentLoaded', init);

View File

@@ -1,12 +1,80 @@
const api = {
base: '',
token: null,
setToken(t){ this.token = t; localStorage.setItem('ss_token', t); },
getToken(){ return this.token || localStorage.getItem('ss_token'); },
headers(){ const h = { 'Content-Type':'application/json' }; const t=this.getToken(); if(t) h['Authorization']='Bearer '+t; return h; },
async get(path){ const r = await fetch('/api'+path, { headers: this.headers() }); if(!r.ok) throw new Error(await r.text()); return r.json(); },
async post(path, body){ const r = await fetch('/api'+path, { method:'POST', headers:this.headers(), body: JSON.stringify(body) }); if(!r.ok) throw new Error(await r.text()); return r.json(); },
async put(path, body){ const r = await fetch('/api'+path, { method:'PUT', headers:this.headers(), body: JSON.stringify(body) }); if(!r.ok) throw new Error(await r.text()); return r.json(); },
async del(path, body){ const r = await fetch('/api'+path, { method:'DELETE', headers:this.headers(), body: JSON.stringify(body) }); if(!r.ok) throw new Error(await r.text()); return r.json(); }
};
window.SSAPI = api;
/* ============================================================================
Super Sunday — Frontend API client (ESM)
============================================================================ */
const BASE = '/api';
const TOKEN_KEY = 'ss_token';
function getToken() { return localStorage.getItem(TOKEN_KEY) || ''; }
function setToken(t) { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); }
async function fetchJSON(url, opts = {}) {
const res = await fetch(url, opts);
const ct = res.headers.get('content-type') || '';
const isJSON = ct.includes('application/json');
const data = isJSON ? await res.json().catch(() => ({})) : await res.text();
if (!res.ok) {
const err = new Error((data && data.error) || res.statusText || 'HTTP Error');
err.status = res.status; err.payload = data;
throw err;
}
return data;
}
async function authFetch(path, options = {}) {
const token = getToken();
const headers = new Headers(options.headers || {});
headers.set('Content-Type','application/json');
if(token) headers.set('Authorization',`Bearer ${token}`);
return fetchJSON(`${BASE}${path}`,{...options,headers});
}
/* Public endpoints */
export async function listTournaments(){ return fetchJSON(`${BASE}/tournaments`); }
export async function getTournament(id){ return fetchJSON(`${BASE}/tournaments/${id}`); }
export async function listParticipants(id){ return fetchJSON(`${BASE}/tournaments/${id}/participants`); }
export async function listMatches(id){ return fetchJSON(`${BASE}/tournaments/${id}/matches`); }
/* Admin endpoints */
export async function login(email,password){
const data = await fetchJSON(`${BASE}/auth/login`,{
method:'POST',headers:{'Content-Type':'application/json'},
body: JSON.stringify({email,password})
});
if(data && data.token) setToken(data.token);
return data;
}
export function logout(){ setToken(''); }
export function isAuthenticated(){ return !!getToken(); }
export async function createTournament(payload){
return authFetch(`/tournaments`,{method:'POST',body:JSON.stringify(payload)});
}
export async function addParticipant(tid,payload){
return authFetch(`/tournaments/${tid}/participants`,{method:'POST',body:JSON.stringify(payload)});
}
export async function generateAmericano(tid,payload){
return authFetch(`/tournaments/${tid}/generate-americano`,{method:'POST',body:JSON.stringify(payload)});
}
export async function scoreMatch(mid,payload){
return authFetch(`/matches/${mid}/score`,{method:'POST',body:JSON.stringify(payload)});
}
/* Helpers */
export function bindLoginForm({formSel='#loginSection',statusSel='#loginStatus'}={}){
const form=document.querySelector(formSel);
const status=document.querySelector(statusSel);
if(!form) return;
const email=form.querySelector('#email');
const password=form.querySelector('#password');
const btn=form.querySelector('#loginBtn');
btn?.addEventListener('click',async ()=>{
status.textContent='Connexion…';
try{
await login(email.value.trim(),password.value);
status.textContent='Connecté ✔';
const admin=document.querySelector('#adminSection');
if(admin) admin.style.display='grid';
form.style.display='none';
}catch(e){ status.textContent=`Erreur: ${e.payload?.error||e.message}`; }
});
}

View File

@@ -1,371 +1,247 @@
/* === Super Sunday — Global sporty theme + hero background + subtle bubbles === */
/* ==========================================================================
Super Sunday — Frontend Theme (Clean, Consolidated)
- Dark, Sirion-like base
- Hero gradient headings (n8n vibe)
- Background image + dark overlays + colored bubbles
- Glass-dark cards, flat rounded buttons, dark inputs
- Simple, deterministic z-index stack
-------------------------------------------------------------------------- */
/* Palette */
/* ========== 0) Design tokens ==========
*/
:root {
--bg: #f4f7fb;
--surface: #ffffff;
--surface-2: #f2f5fa;
--ink: #101827;
--muted: #6b7280;
--border: #e5e9f2;
/* Colors */
--ink: #eaf1ff;
--ink-strong: #ffffff;
--muted: #9fb0c7;
--panel: rgba(12,16,28,.75);
--panel-border: rgba(255,255,255,.12);
--cta: #1890ff;
--cta-hover: #3aa0ff;
--accent-start: #1ea7ff;
--accent-end: #27d980;
--accent: #1890ff;
/* Accents for gradients */
--grad-rose: #ff7ad9;
--grad-violet: #7c4dff;
--grad-cyan: #00e5ff;
/* Layout */
--radius: 16px;
--shadow: 0 6px 18px rgba(0,0,0,.06);
--shadow-hover: 0 10px 28px rgba(0,0,0,.12);
--radius-xl: 20px;
--shadow: 0 10px 30px rgba(0,0,0,.35);
--shadow-hover: 0 16px 36px rgba(0,0,0,.45);
--trans: 180ms ease;
}
*{box-sizing:border-box}
html,body{height:100%}/* === Super Sunday — Dark "Sirion-like" theme overrides === */
* { box-sizing: border-box; }
html, body { height: 100%; }
/* 0) Couleurs globales + typo */
:root{
--ink: #e8eef6; /* texte global clair */
--ink-strong: #ffffff; /* titres */
--muted: #b7c3d6; /* textes secondaires */
--border-dark: rgba(255,255,255,.08);
--surface-dark: rgba(15,23,42,.60); /* slate-900 translucide */
--surface-dark-2: rgba(15,23,42,.72);
}
/* 1) Fond sombre bleuté + image adoucie */
body{
/* ========== 1) Base & background ==========
*/
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji", sans-serif;
color: var(--ink);
line-height: 1.5;
}
body::before{
content:"";
position:fixed; inset:0; z-index:-2;
/* Background image + dark overlays */
body::before {
content: "";
position: fixed; inset: 0;
z-index: -3; /* far background */
background:
radial-gradient(120% 85% at 20% -10%, rgba(3,7,18,.9) 0%, rgba(3,7,18,.6) 40%, rgba(3,7,18,.3) 60%, rgba(3,7,18,.2) 100%),
linear-gradient(180deg, rgba(9,14,30,.75), rgba(9,14,30,.55) 40%, rgba(9,14,30,.75)),
/* colored ambience blobs */
radial-gradient(40% 35% at 15% 10%, rgba(124,77,255,.23), rgba(124,77,255,0) 60%),
radial-gradient(45% 40% at 85% 20%, rgba(0,229,255,.20), rgba(0,229,255,0) 60%),
radial-gradient(55% 50% at 50% 85%, rgba(255,122,217,.16), rgba(255,122,217,0) 60%),
/* dark veil */
linear-gradient(180deg, rgba(0,0,0,.72), rgba(0,0,0,.58) 35%, rgba(0,0,0,.70)),
/* photo */
url("/assets/image/backgroundp24p.jpg") center top / cover no-repeat;
filter: saturate(.9) contrast(.95) brightness(.9) blur(0.5px);
filter: saturate(.96) contrast(.98) brightness(.9) blur(.4px);
}
body::after{
/* vignette douce pour focus centre */
content:"";
position:fixed; inset:0; z-index:-1; pointer-events:none;
body::after {
/* vignette for edge darkening */
content: "";
position: fixed; inset: 0;
z-index: -2; /* above background */
pointer-events: none;
background:
radial-gradient(140% 100% at 50% 0%, rgba(0,0,0,0) 55%, rgba(0,0,0,.26) 100%),
radial-gradient(120% 90% at 50% 100%, rgba(0,0,0,0) 60%, rgba(0,0,0,.22) 100%);
radial-gradient(145% 100% at 50% 0%, rgba(0,0,0,0) 52%, rgba(0,0,0,.30) 100%),
radial-gradient(125% 90% at 50% 100%, rgba(0,0,0,0) 58%, rgba(0,0,0,.24) 100%);
}
/* 2) Titres & textes */
h1,h2,h3{ color: var(--ink-strong); }
.hero-title{
color:#fff !important;
text-shadow: 0 8px 30px rgba(30,167,255,.25), 0 2px 12px rgba(0,0,0,.35);
/* ========== 2) Colored bubbles (between overlays and UI) ==========
*/
@keyframes float {
0% { transform: translateY(0) scale(1); opacity: .75; }
50% { transform: translateY(-36px) scale(1.06); opacity: .45; }
100% { transform: translateY(0) scale(1); opacity: .75; }
}
.section-title{
color:#f1f5ff;
}
.section-title::before{
background: linear-gradient(180deg, #8ee7ff, #9fffcf);
box-shadow: 0 6px 16px rgba(143, 237, 255, .35);
}
.muted{ color: var(--muted); }
/* 3) Header sombre translucide */
header{
background: rgba(10,14,26,.78);
color:#fff;
border-bottom: 1px solid var(--border-dark);
.bg-bubble {
position: fixed;
z-index: -1; /* between overlays and UI */
pointer-events: none;
border-radius: 50%;
opacity: .9;
/* blue ↔ violet glow */
background:
radial-gradient(60% 60% at 30% 30%, rgba(124,77,255,.55), rgba(124,77,255,0) 70%),
radial-gradient(55% 55% at 70% 70%, rgba(0,229,255,.45), rgba(0,229,255,0) 75%);
animation: float 10s ease-in-out infinite;
filter: blur(.3px);
}
.bg-bubble.b1 { width:120px; height:120px; left:12%; top:68%; animation-duration: 9s; }
.bg-bubble.b2 { width:90px; height:90px; left:70%; top:58%; animation-duration:12s; }
.bg-bubble.b3 { width:150px; height:150px; left:46%; top:76%; animation-duration:14s; }
.bg-bubble.b4 { width:80px; height:80px; left:84%; top:22%; animation-duration:11s; }
.bg-bubble.b5 { width:110px; height:110px; left:6%; top:18%; animation-duration:10s; }
@media (prefers-reduced-motion: reduce) { .bg-bubble { animation: none; } }
/* Ensure content sits above bubbles */
header, main, nav, .card { position: relative; z-index: 0; }
/* ========== 3) Header & navigation ==========
*/
header {
position: sticky; top: 0; z-index: 40;
display: flex; align-items: center; justify-content: space-between;
padding: 14px 24px;
background: linear-gradient(180deg, rgba(5,8,14,.96), rgba(5,8,14,.88));
color: #fff;
border-bottom: 1px solid rgba(255,255,255,.10);
backdrop-filter: blur(10px) saturate(130%);
-webkit-backdrop-filter: blur(10px) saturate(130%);
}
nav a{ color:#e9f2ff; }
nav a:hover{ background: rgba(255,255,255,.06); }
header h1 { margin: 0; font-size: 20px; font-weight: 700; color: #fff; }
/* 4) Cartes dark glass */
.card{
background: var(--surface-dark);
border: 1px solid var(--border-dark);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 10px 30px rgba(0,0,0,.35);
nav a {
margin-left: 16px; padding: 8px 14px;
color: #f2f6ff; text-decoration: none;
border-radius: 12px; font-weight: 600;
transition: background var(--trans), transform var(--trans), border-color var(--trans);
}
.card.accent{
background:
linear-gradient(135deg, rgba(30,167,255,.12), rgba(39,217,128,.12)),
var(--surface-dark-2);
border-color: rgba(255,255,255,.12);
nav a:hover { background: rgba(255,255,255,.08); }
nav a.active {
background: rgba(255,255,255,.10);
border: 1px solid rgba(255,255,255,.22);
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
}
/* 5) Inputs / selects en sombre lisible */
input, select{
color:#e8eef6;
background: rgba(8,12,24,.6);
border: 1px solid rgba(255,255,255,.10);
}
input::placeholder, select::placeholder{ color:#9fb0c7; }
input:focus, select:focus{
border-color: rgba(143, 237, 255, .55);
box-shadow: 0 0 0 3px rgba(143, 237, 255, .18);
}
/* ========== 4) Layout & containers ==========
*/
.container { max-width: 1160px; margin: 24px auto; padding: 0 16px; }
.grid { display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
/* 6) Boutons */
.btn{
box-shadow: 0 12px 28px rgba(30,167,255,.3);
}
.btn-outline{
color:#eaf3ff;
background: rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.25);
}
.btn-outline:hover{
background: rgba(255,255,255,.12);
border-color: rgba(255,255,255,.45);
}
/* 7) État vide & préformatés */
.empty{
background: rgba(255,255,255,.06);
border: 1px dashed rgba(255,255,255,.18);
color: #c8d5e8;
}
pre{
color:#e7efff;
background: rgba(8,12,24,.55);
border: 1px solid rgba(255,255,255,.08);
padding: 12px; border-radius: 12px; overflow:auto;
}
/* 8) Légère retouche des bulles pour le mode sombre */
.bg-bubble{
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.22), rgba(255,255,255,0));
mix-blend-mode: screen;
opacity:.7;
}
body{
margin:0;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
color:var(--ink);
line-height:1.5;
/* Hero background image */
background-image: url("/assets/image/backgroundp24p.jpg");
background-size: cover;
background-position: center top;
background-attachment: fixed;
}
/* dark overlay to ensure readability */
body::before{
content:"";
position:fixed; inset:0;
background: linear-gradient(180deg, rgba(255,255,255,.30), rgba(255,255,255,.60));
mix-blend-mode: normal;
z-index:-1;
}
/* ===== Header / Nav ===== */
header{
position: sticky; top:0; z-index:40;
display:flex; align-items:center; justify-content:space-between;
padding: 14px 24px;
background: rgba(255,255,255,0.86);
border-bottom:1px solid var(--border);
backdrop-filter: blur(8px) saturate(120%);
}
header h1{margin:0; font-size:20px; font-weight:700; color:var(--ink)}
nav a{
margin-left:16px; padding:8px 14px;
color:var(--ink); text-decoration:none; border-radius:12px;
transition: background var(--trans), transform var(--trans);
}
nav a:hover{ background: linear-gradient(90deg, rgba(30,167,255,.15), rgba(39,217,128,.15)); transform:translateY(-1px) }
/* ===== Layout ===== */
.container{ max-width:1100px; margin:24px auto; padding:0 16px }
.grid{ display:grid; gap:20px; grid-template-columns: repeat(auto-fit,minmax(280px,1fr)) }
/* ===== Cards ===== */
.card{
background: var(--surface);
border:1px solid var(--border);
border-radius: var(--radius);
/* ========== 5) Cards ==========
*/
.card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: var(--radius-xl);
padding: 18px;
box-shadow: var(--shadow);
transition: transform var(--trans), box-shadow var(--trans), border-color var(--trans);
}
.card:hover{ transform: translateY(-2px); box-shadow: var(--shadow-hover); border-color:#dbe5f2 }
a.card{ display:block; color:inherit; text-decoration:none }
.card.accent{
background: linear-gradient(135deg, rgba(30,167,255,.08), rgba(39,217,128,.08)), var(--surface);
border-color: rgba(30,167,255,.25);
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow-hover); border-color: rgba(143,237,255,.35); }
.card.accent {
background:
linear-gradient(180deg, rgba(124,77,255,.10), rgba(0,229,255,.10)),
var(--panel);
border-color: rgba(255,255,255,.14);
}
#tournaments .card, #matches .card { margin-bottom: 12px; }
/* ========== 6) Typography ==========
*/
h1, h2, h3 { color: var(--ink-strong); line-height: 1.2; }
.muted { color: var(--muted); }
/* Hero & section titles (n8n-like gradients) */
.hero-title {
font-size: clamp(42px, 6.8vw, 78px);
font-weight: 900;
letter-spacing: -.02em;
line-height: 1.04;
background: linear-gradient(90deg, var(--grad-rose) 0%, var(--grad-violet) 45%, var(--grad-cyan) 95%);
-webkit-background-clip: text; background-clip: text; color: transparent;
text-shadow: 0 10px 30px rgba(124,77,255,.22), 0 2px 12px rgba(0,0,0,.35);
margin-bottom: .25rem;
}
.section-title {
font-size: clamp(26px, 3.8vw, 42px);
font-weight: 800; letter-spacing: -.01em;
background: linear-gradient(90deg, var(--grad-violet), var(--grad-cyan));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
/* Titles / text */
h1,h2,h3{ line-height:1.2 }
h2{ margin: 20px 0 12px; font-size:22px; font-weight:600 }
.muted{ color: var(--muted) }
.mt{ margin-top:24px }
/* Buttons & inputs */
.btn{
display:inline-block; padding:10px 18px; border-radius:12px; border:none;
background: linear-gradient(90deg, var(--accent-start), var(--accent-end));
color:#fff; font-weight:600; text-decoration:none;
box-shadow: 0 8px 20px rgba(24,144,255,.25);
transition: transform var(--trans), box-shadow var(--trans);
}
.btn:hover{ transform: translateY(-1px); box-shadow: 0 12px 28px rgba(24,144,255,.35) }
input,select{
width:100%; padding:10px 12px; border-radius:12px; border:1px solid var(--border);
background: var(--surface-2); color: var(--ink); outline:none;
/* ========== 7) Forms & buttons ==========
*/
input, select, textarea {
width: 100%;
padding: 12px 14px;
min-height: 44px;
border-radius: 14px;
color: #eaf3ff;
background: #0f1b33;
border: 1px solid rgba(255,255,255,.15);
outline: none;
transition: border-color var(--trans), box-shadow var(--trans);
margin: 10px 0 16px;
}
input:focus,select:focus{ border-color:#b8d8ff; box-shadow: 0 0 0 3px rgba(24,144,255,.20) }
/* Empty state */
.empty{
padding: 20px; text-align:center; color:var(--muted);
background: var(--surface-2); border:1px dashed var(--border);
border-radius: var(--radius);
input::placeholder, textarea::placeholder { color: #93a4c0; }
input:focus, select:focus, textarea:focus {
border-color: rgba(143,237,255,.55);
box-shadow: 0 0 0 3px rgba(143, 237, 255, .18);
}
/* Specific existing sections */
#tournaments .card, #matches .card{ margin-bottom:12px }
/* ===== Animated bubbles (tennis/padel ball ghosts) ===== */
@keyframes float {
0% { transform: translateY(0) scale(1); opacity:.7; }
50% { transform: translateY(-36px) scale(1.05); opacity:.4; }
100% { transform: translateY(0) scale(1); opacity:.7; }
}
.bg-bubble{
position: fixed; z-index: -1; pointer-events: none; border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.75), rgba(255,255,255,0));
filter: blur(0.2px);
animation: float 9s ease-in-out infinite;
mix-blend-mode: screen;
}
.bg-bubble.b1{ width:90px; height:90px; left:15%; top:72%; animation-duration:8s }
.bg-bubble.b2{ width:70px; height:70px; left:68%; top:64%; animation-duration:10s }
.bg-bubble.b3{ width:115px; height:115px; left:48%; top:78%; animation-duration:12s }
.bg-bubble.b4{ width:60px; height:60px; left:82%; top:20%; animation-duration:11s }
.bg-bubble.b5{ width:80px; height:80px; left:6%; top:18%; animation-duration:9s }
@media (prefers-reduced-motion: reduce){
.bg-bubble{ animation: none; }
}
/* Footer (if needed) */
footer{ margin:40px 0 0; padding:24px 16px; text-align:center; color:var(--muted);
border-top:1px solid var(--border); background: rgba(255,255,255,.75); backdrop-filter: blur(6px) }
/* === Retouches: overlay noir + spacing formulaires === */
/* 1) Overlay plus noir, façon Sirion */
body::before{
/* on garde l'image + on fonce nettement avec un voile noir bleuté */
background:
linear-gradient(180deg, rgba(0,0,0,.70), rgba(0,0,0,.55) 35%, rgba(0,0,0,.68)),
radial-gradient(120% 85% at 20% -10%, rgba(0,10,20,.75) 0%, rgba(0,10,20,.35) 60%, rgba(0,10,20,.20) 100%),
url("/assets/image/backgroundp24p.jpg") center top / cover no-repeat !important;
filter: saturate(.95) contrast(.98) brightness(.88) blur(0.6px);
}
/* vignette un peu plus marquée sur les bords */
body::after{
background:
radial-gradient(145% 100% at 50% 0%, rgba(0,0,0,0) 52%, rgba(0,0,0,.30) 100%),
radial-gradient(125% 90% at 50% 100%, rgba(0,0,0,0) 58%, rgba(0,0,0,.24) 100%) !important;
}
/* 2) Spacing des formulaires (inputs & dropdowns) */
.card input,
.card select,
.card textarea {
margin-top: 8px !important;
margin-bottom: 14px !important; /* +respiration */
min-height: 42px; /* dropdowns plus grands */
padding: 10px 12px !important;
border-radius: 12px !important;
}
/* espacement entre groupes de champs (si <div> direct dans .card) */
.card > div + div { margin-top: 16px; }
/* si tu utilises des <label>, on les rapproche proprement du champ */
.card label {
display: block;
font-weight: 600;
color: #e9f1ff;
margin-top: 12px;
margin-bottom: 6px;
}
/* 3) Lisibilité du header encore meilleure sur overlay noir */
header{
background: linear-gradient(180deg, rgba(5,8,14,.96), rgba(5,8,14,.88)) !important;
border-bottom: 1px solid rgba(255,255,255,.10) !important;
}
nav a{ color:#f2f6ff !important; }
nav a:hover{ background: rgba(255,255,255,.08) !important; }
/* 4) Cartes & blocs "empty" légèrement plus sombres pour contraster */
.card{
background: rgba(12,18,32,.72) !important;
border: 1px solid rgba(255,255,255,.12) !important;
}
.empty{
background: rgba(255,255,255,.07) !important;
border-color: rgba(255,255,255,.20) !important;
}
/* 5) Sections et titres : contraste blanc franc */
.section-title{ color:#ffffff !important; }
.hero-title{ color:#ffffff !important; }
/* === Retouches inputs + boutons === */
/* 1) Inputs & selects : fond bleu foncé homogène */
.card input,
.card select,
.card textarea {
background: #0f1b33 !important; /* bleu nuit uniforme */
color: #eaf3ff !important;
border: 1px solid rgba(255,255,255,.15) !important;
}
.card input::placeholder,
.card select::placeholder,
.card textarea::placeholder {
color: #93a4c0 !important;
}
/* 2) Boutons flat arrondis */
/* Buttons (flat, rounded) */
.btn {
display:inline-block;
padding:10px 18px;
border-radius: 999px; /* bien arrondi */
border:none;
background: #1890ff; /* bleu flat */
color:#fff;
font-weight:600;
box-shadow: none; /* pas dombre lourde */
transition: background 0.2s ease, transform 0.2s ease;
display: inline-block;
padding: 12px 20px;
border-radius: 999px;
border: none;
background: var(--cta);
color: #fff;
font-weight: 700;
box-shadow: none;
transition: background .2s ease, transform .2s ease;
}
.btn:hover {
background: #3aa0ff; /* bleu un peu plus clair au hover */
transform: translateY(-1px);
}
/* Variante secondaire : outline */
.btn:hover { background: var(--cta-hover); transform: translateY(-1px); }
.btn-outline {
border-radius: 999px;
padding:10px 16px;
border:1px solid rgba(255,255,255,.25);
padding: 10px 16px;
border: 1px solid rgba(255,255,255,.28);
background: transparent;
color:#eaf3ff;
transition: background 0.2s ease, border-color 0.2s ease;
color: #eaf3ff;
transition: background .2s ease, border-color .2s ease;
}
.btn-outline:hover {
background: rgba(255,255,255,.08);
border-color: rgba(255,255,255,.45);
.btn-outline:hover { background: rgba(255,255,255,.08); border-color: rgba(255,255,255,.45); }
/* ========== 8) Utility blocks ==========
*/
.empty {
padding: 20px; text-align: center; color: #cfe1ff;
background: rgba(255,255,255,.05);
border: 1px dashed rgba(255,255,255,.18);
border-radius: var(--radius);
}
pre {
color: #e7efff;
background: rgba(10,14,26,.55);
border: 1px solid rgba(255,255,255,.10);
padding: 12px; border-radius: 16px; overflow: auto;
}
/* ========== 9) Footer (optional) ==========
*/
footer {
margin: 40px 0 0; padding: 24px 16px; text-align: center; color: var(--muted);
border-top: 1px solid rgba(255,255,255,.10);
background: rgba(5,8,14,.82);
backdrop-filter: blur(6px);
}

View File

@@ -1,14 +1,10 @@
<!doctype html>
<html lang="fr">
<head>
<title>Super Sunday — Accueil</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/style.css?v=4" />
<title>Super Sunday — Accueil</title>
<link rel="stylesheet" href="/assets/style.css?v=8" />
</head>
<body>
<header>
@@ -19,36 +15,22 @@
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<div class="card accent" style="padding:22px; margin:8px 0 18px;">
<div class="kicker">Tournoi du dimanche</div>
<h2 class="hero-title">Padel, ambiance & performance</h2>
<p class="muted" style="margin:6px 0 0">Inscriptions, poules Americano, scores en direct.</p>
<div class="cta-row" style="display:flex; gap:12px; flex-wrap:wrap; margin-top:12px">
<a class="btn" href="/events">Voir les événements</a>
<a class="btn-outline" href="/admin">Espace organisateur</a>
</div>
</div>
<section>
<h2 class="section-title">Tournois</h2>
<div id="tournaments"></div>
</section>
<section>
<h2 class="section-title">Matches</h2>
<div id="matches" class="empty">Aucun match</div>
</section>
<h2 class="section-title">Tournois</h2>
<div id="tournaments"></div>
</main>
<div class="bg-bubble b1"></div>
<div class="bg-bubble b2"></div>
<div class="bg-bubble b3"></div>
<div class="bg-bubble b4"></div>
<div class="bg-bubble b5"></div>
<script src="/assets/api.js"></script>
<script src="/assets/main.js"></script>
<script type="module">
import { listTournaments } from '/assets/api.js?v=8';
(async()=>{
const root=document.getElementById('tournaments');
const ts=await listTournaments().catch(()=>[]);
root.innerHTML=ts.map(t=>
`<a class="card event-card" href="/tournament?id=${t.id}">
<h3>${t.name}</h3>
<p class="muted">${[t.location||'—',t.start_date].filter(Boolean).join(' • ')}</p>
</a>`
).join('') || '<div class="empty">Aucun tournoi</div>';
})();
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,102 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Super Sunday — Classement live</title>
<link rel="stylesheet" href="/assets/style.css?v=13" />
<style>
.wrap{max-width:1080px;margin:0 auto}
table{width:100%;border-collapse:separate;border-spacing:0 10px}
th,td{text-align:left;padding:10px 12px}
thead th{font-weight:700;letter-spacing:.02em}
tbody tr{background:rgba(15,27,51,.6);border:1px solid rgba(255,255,255,.08)}
tbody tr td:first-child{border-top-left-radius:14px;border-bottom-left-radius:14px}
tbody tr td:last-child{border-top-right-radius:14px;border-bottom-right-radius:14px}
.muted{color:#a9bfd6}
.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}
.controls select,.controls input{min-width:180px}
</style>
</head>
<body>
<header>
<h1>Classement Live</h1>
<nav>
<a href="/">Accueil</a>
<a href="/events">Événements</a>
<a href="/scoreboard" class="active">Classement</a>
</nav>
</header>
<main class="container wrap">
<div class="controls">
<select id="tid"></select>
<button id="refresh" class="btn-outline">Rafraîchir</button>
<label class="muted"><input type="checkbox" id="auto" checked> Auto-refresh (10s)</label>
</div>
<table>
<thead>
<tr>
<th>#</th><th>Joueur</th><th class="muted">J</th><th>Pts (jeux)</th><th>Diff</th><th class="muted">V</th><th class="muted">N</th><th class="muted">D</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
<p id="status" class="muted"></p>
</main>
<script type="module">
import { listTournaments } from '/assets/api.js?v=13';
async function fetchJSON(url){ const r=await fetch(url); if(!r.ok) throw new Error(await r.text()); return r.json(); }
const tid = document.getElementById('tid');
const rows = document.getElementById('rows');
const status = document.getElementById('status');
const auto = document.getElementById('auto');
async function loadTournaments(){
const ts = await listTournaments();
tid.innerHTML = ['<option value="">— choisir un tournoi —</option>'].concat(
ts.map(t=>`<option value="${t.id}">#${t.id}${t.name}</option>`)
).join('');
}
async function render(){
const id = Number(tid.value||'0');
if(!id){ rows.innerHTML=''; status.textContent=''; return; }
try{
const data = await fetchJSON(`/api/tournaments/${id}/standings`);
const st = data.standings || [];
rows.innerHTML = st.map(s => `
<tr>
<td>${s.rank}</td>
<td><strong>${s.name}</strong></td>
<td class="muted">${s.played}</td>
<td>${s.points}</td>
<td>${(s.games_for - s.games_against)}</td>
<td class="muted">${s.wins}</td>
<td class="muted">${s.draws}</td>
<td class="muted">${s.losses}</td>
</tr>`).join('');
status.textContent = st.length ? '' : 'Aucun résultat pour le moment.';
}catch(e){
status.textContent = 'Erreur: ' + e.message;
}
}
document.getElementById('refresh').addEventListener('click', render);
tid.addEventListener('change', render);
let timer = null;
function tick(){
if (auto.checked) render();
timer = setTimeout(tick, 10000);
}
auto.addEventListener('change', ()=>{ if(!auto.checked && timer){ clearTimeout(timer); } else { tick(); } });
await loadTournaments();
tick();
</script>
</body>
</html>

View File

@@ -1,14 +1,10 @@
<!doctype html>
<html lang="fr">
<head>
<title>Détail tournoi — Super Sunday</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/style.css?v=4" />
<title>Détail tournoi — Super Sunday</title>
<link rel="stylesheet" href="/assets/style.css?v=8" />
</head>
<body>
<header>
@@ -19,49 +15,27 @@
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<section class="card accent" style="padding:22px; margin:8px 0 18px;">
<div class="kicker">Super Sunday</div>
<h2 class="hero-title" id="ttitle">Tournoi</h2>
<p class="muted" id="tsub">Détails & participants</p>
</section>
<section>
<h2 class="section-title">Infos</h2>
<div id="info" class="card"></div>
</section>
<section>
<h2 class="section-title">Participants</h2>
<div id="participants"></div>
</section>
<section>
<h2 class="section-title">Matches</h2>
<div id="matchlist"></div>
</section>
<h2 class="hero-title" id="ttitle">Tournoi</h2>
<p class="muted" id="tsub"></p>
<h3 class="section-title">Participants</h3>
<div id="participants"></div>
<h3 class="section-title">Matches</h3>
<div id="matchlist"></div>
</main>
<div class="bg-bubble b1"></div>
<div class="bg-bubble b2"></div>
<div class="bg-bubble b3"></div>
<div class="bg-bubble b4"></div>
<div class="bg-bubble b5"></div>
<script src="/assets/api.js"></script>
<script src="/assets/tournament.js"></script>
<script>
(function(){
const params = new URLSearchParams(location.search);
const id = Number(params.get('id')||'0');
<script type="module">
import { getTournament,listParticipants,listMatches } from '/assets/api.js?v=8';
(async()=>{
const id=Number(new URLSearchParams(location.search).get('id')||'0');
if(!id) return;
fetch('/api/tournaments/'+id).then(r=>r.json()).then(t=>{
if(t && t.name){
document.getElementById('ttitle').textContent = t.name;
const d = (t.start_date||'') + (t.end_date? ' → '+t.end_date : '');
document.getElementById('tsub').textContent = [t.location||'Lieu à venir', d].filter(Boolean).join('');
}
}).catch(()=>{});
const t=await getTournament(id);
document.getElementById('ttitle').textContent=t.name||'Tournoi';
document.getElementById('tsub').textContent=[t.location||'—',t.start_date].filter(Boolean).join(' • ');
const ps=await listParticipants(id);
document.getElementById('participants').innerHTML=ps.map(p=>`<div class="card">${p.full_name}</div>`).join('')||'<div class="empty">Aucun participant</div>';
const ms=await listMatches(id);
document.getElementById('matchlist').innerHTML=ms.map(m=>`<div class="card"><strong>${m.team_a}</strong> vs <strong>${m.team_b}</strong> — <span class="muted">${m.court||'—'}</span></div>`).join('')||'<div class="empty">Aucun match</div>';
})();
</script>
</body>
</html>
</html>