Mise à jour : dernières modifs

This commit is contained in:
karim hassan
2025-08-24 14:49:43 +00:00
parent e3620c7f42
commit c5f16fe27b
45 changed files with 2426 additions and 552 deletions

3
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM nginx:alpine
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
COPY public /usr/share/nginx/html

67
frontend/index.html Normal file
View File

@@ -0,0 +1,67 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Super Sunday — Admin</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<h1>Admin — Super Sunday</h1>
<nav>
<a href="/">Accueil</a>
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<section id="loginSection">
<h2>Connexion</h2>
<input id="email" placeholder="Email" />
<input id="password" type="password" placeholder="Mot de passe" />
<button id="loginBtn">Se connecter</button>
<p id="loginStatus"></p>
</section>
<section id="adminSection" style="display:none;">
<h2>Actions rapides</h2>
<div class="grid">
<div class="card">
<h3>Nouveau 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()">Créer</button>
</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()">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()">Valider</button>
</div>
</div>
<h2 class="mt">Tournaments</h2>
<pre id="adminTournaments"></pre>
<h2>Matches</h2>
<pre id="adminMatches"></pre>
</section>
</main>
<script src="/assets/api.js"></script>
<script src="/assets/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
server {
listen 80;
server_name _;
location = /health { return 200 'ok'; add_header Content-Type text/plain; }
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://api:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
}
location / {
try_files $uri $uri/ /index.html;
}
}

6
frontend/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "supersunday-v1-base",
"version": "1.0.0",
"type": "module",
"scripts": { "start": "node server.mjs" }
}

View File

@@ -0,0 +1,74 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Super Sunday — Admin</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<h1>Admin — Super Sunday</h1>
<nav>
<a href="/">Accueil</a>
<a href="/events">Événements</a>
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<section id="loginSection" class="card">
<h2>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>
</section>
<section id="adminSection" style="display:none;">
<div class="grid">
<div class="card">
<h3>Nouveau 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>
</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>
</div>
<h2 class="mt">Tournaments</h2>
<pre id="adminTournaments"></pre>
<h2>Matches</h2>
<pre id="adminMatches"></pre>
</section>
</main>
<!-- Bubbles -->
<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/admin.js"></script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
const loginBtn = document.getElementById('loginBtn');
const loginSection = document.getElementById('loginSection');
const adminSection = document.getElementById('adminSection');
const loginStatus = document.getElementById('loginStatus');
async function refreshLists(){
const ts = await SSAPI.get('/tournaments').catch(()=>[]);
const ms = await SSAPI.get('/matches').catch(()=>[]);
document.getElementById('adminTournaments').textContent = JSON.stringify(ts,null,2);
document.getElementById('adminMatches').textContent = JSON.stringify(ms,null,2);
}
loginBtn.onclick = async () => {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
const { token } = await SSAPI.post('/auth/login', { email, password });
SSAPI.setToken(token);
loginStatus.textContent = 'Connecté ✓';
loginSection.style.display = 'none';
adminSection.style.display = 'block';
await refreshLists();
} catch (e) {
loginStatus.textContent = 'Erreur: ' + e.message;
}
};
window.createTournament = async () => {
const name = document.getElementById('t_name').value;
const location = document.getElementById('t_loc').value;
const start_date = document.getElementById('t_sd').value;
const end_date = document.getElementById('t_ed').value;
await SSAPI.post('/tournaments', { name, location, start_date, end_date });
await refreshLists();
};
window.generateAmericano = async () => {
const tournament_id = Number(document.getElementById('g_tid').value);
const courts = document.getElementById('g_courts').value.split(',').map(s=>s.trim()).filter(Boolean);
const start_at = document.getElementById('g_start').value || null;
const interval_minutes = Number(document.getElementById('g_int').value || 20);
const res = await SSAPI.post('/matches/generate', { tournament_id, courts, start_at, interval_minutes });
alert('Matches créés: ' + res.created.length);
await refreshLists();
};
window.scoreMatch = async () => {
const id = Number(document.getElementById('m_id').value);
const score_a = Number(document.getElementById('m_a').value);
const score_b = Number(document.getElementById('m_b').value);
await SSAPI.post(`/matches/${id}/score`, { score_a, score_b });
await refreshLists();
};
// New: bulk enroll & auto teams
window.bulkEnroll = async () => {
const tournament_id = Number(document.getElementById('b_tid').value);
const ids = document.getElementById('b_ids').value.split(',').map(s=>Number(s.trim())).filter(Boolean);
await SSAPI.post('/enrollments/bulk', { tournament_id, player_ids: ids });
await refreshLists();
};
window.autoTeams = async () => {
const tournament_id = Number(document.getElementById('b_tid').value);
const res = await SSAPI.post('/teams/auto', { tournament_id });
alert('Équipes créées: ' + (res.created_count || 0));
await refreshLists();
};
// auto-show admin if token present
if (SSAPI.getToken()) {
loginSection.style.display = 'none';
adminSection.style.display = 'block';
refreshLists();
}

View File

@@ -0,0 +1,12 @@
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;

View File

@@ -0,0 +1,11 @@
(async function(){
const items = await SSAPI.get('/tournaments/upcoming').catch(()=>[]);
const el = document.getElementById('list');
el.innerHTML = items.map(t=>`
<a class="card" href="/tournament/?id=${t.id}" style="display:block;text-decoration:none;color:inherit">
<strong>${t.name}</strong><br>
${t.location || ''}<br>
${t.start_date}${t.end_date}
</a>
`).join('') || '<p class="muted">Aucun événement à venir</p>';
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

View File

@@ -0,0 +1,23 @@
(async function(){
const tournaments = await SSAPI.get('/tournaments').catch(()=>[]);
const matches = await SSAPI.get('/matches').catch(()=>[]);
const tWrap = document.getElementById('tournaments');
tWrap.innerHTML = tournaments.map(t=>`
<div class="card">
<strong>${t.name}</strong><br>
${t.location || ''}<br>
${t.start_date}${t.end_date}
</div>
`).join('') || '<p class="muted">Aucun tournoi</p>';
const mWrap = document.getElementById('matches');
mWrap.innerHTML = matches.map(m=>`
<div class="card">
<div><strong>${m.round || 'Match'}</strong> — ${m.court || 'Court ?'}</div>
<div>Teams: ${m.team_a_id || '?'} vs ${m.team_b_id || '?'}</div>
<div>Score: ${m.score_a} - ${m.score_b} (${m.status})</div>
<div>${m.scheduled_at ? new Date(m.scheduled_at).toLocaleString() : ''}</div>
</div>
`).join('') || '<p class="muted">Aucun match</p>';
})();

View File

@@ -0,0 +1,141 @@
/* === Super Sunday — Global sporty theme + hero background + subtle bubbles === */
/* Palette */
:root {
--bg: #f4f7fb;
--surface: #ffffff;
--surface-2: #f2f5fa;
--ink: #101827;
--muted: #6b7280;
--border: #e5e9f2;
--accent-start: #1ea7ff;
--accent-end: #27d980;
--accent: #1890ff;
--radius: 16px;
--shadow: 0 6px 18px rgba(0,0,0,.06);
--shadow-hover: 0 10px 28px rgba(0,0,0,.12);
--trans: 180ms ease;
}
*{box-sizing:border-box}
html,body{height:100%}
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/images/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);
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);
}
/* 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;
transition: border-color var(--trans), box-shadow var(--trans);
}
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);
}
/* 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) }

View File

@@ -0,0 +1,55 @@
/* ==== TITLES UPGRADE (Sporty & Bold) ==== */
:root{
--display-font: 'Inter', ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
--title-size-xl: clamp(28px, 4.5vw, 48px);
--title-size-lg: clamp(22px, 3.2vw, 32px);
--title-weight: 800;
--title-gradient: linear-gradient(90deg, var(--accent-start), var(--accent-end));
}
.hero-title{
font-family: var(--display-font);
font-size: var(--title-size-xl);
font-weight: var(--title-weight);
line-height: 1.05;
letter-spacing: -0.02em;
margin: 0 0 6px 0;
background: var(--title-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 6px 24px rgba(24,144,255,.18);
}
.section-title{
position: relative;
font-size: var(--title-size-lg);
font-weight: 750;
letter-spacing: -0.01em;
margin: 22px 0 14px;
padding-left: 14px;
}
.section-title::before{
content:"";
position:absolute; left:0; top: 8px; bottom: 8px;
width:6px; border-radius: 6px;
background: var(--title-gradient);
box-shadow: 0 8px 18px rgba(24,144,255,.25);
}
.kicker{
display:inline-block;
font-size:12px; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
color:#0a3a6b;
background: rgba(24,144,255,.10);
border: 1px solid rgba(24,144,255,.25);
padding:4px 8px; border-radius:999px; margin-bottom:10px;
}
.cta-row{ display:flex; gap:12px; flex-wrap:wrap; margin-top:12px }
.btn-outline{
display:inline-block; padding:10px 16px; border-radius:12px;
border:1px solid #cfe3ff; color:#0f437c; text-decoration:none;
background: rgba(255,255,255,.6);
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease;
}
.btn-outline:hover{ transform: translateY(-1px); background: rgba(255,255,255,.85); border-color: #a3cdfd }

View File

@@ -0,0 +1,39 @@
(function(){
const params = new URLSearchParams(location.search);
const id = Number(params.get('id') || '0');
if(!id){
document.getElementById('info').innerHTML = '<p>Tournoi introuvable (id manquant)</p>';
return;
}
async function load(){
const t = await SSAPI.get('/tournaments/'+id).catch(()=>null);
if(!t){ document.getElementById('info').innerHTML = '<p>Tournoi introuvable</p>'; return; }
document.getElementById('info').innerHTML = `
<h2>${t.name}</h2>
<div><strong>Lieu:</strong> ${t.location || '—'}</div>
<div><strong>Dates:</strong> ${t.start_date}${t.end_date}</div>
<div class="grid">
<div class="card"><strong>Inscrits</strong><br>${t.enrollment_count}</div>
<div class="card"><strong>Équipes</strong><br>${t.team_count}</div>
<div class="card"><strong>Matches</strong><br>${t.match_count}</div>
</div>
${t.next_match ? `<div class="card mt"><strong>Prochain match</strong><br>${t.next_match.round || ''}${t.next_match.court || ''}<br>${t.next_match.scheduled_at ? new Date(t.next_match.scheduled_at).toLocaleString() : ''}</div>` : ''}
`;
const parts = await SSAPI.get('/enrollments?tournament_id='+id).catch(()=>[]);
document.getElementById('participants').innerHTML = parts.map(p=>`
<div class="card">
${p.first_name} ${p.last_name} <span class="muted">(#${p.ranking || '—'})</span>
</div>
`).join('') || '<p class="muted">Aucun inscrit</p>';
const matches = await SSAPI.get('/matches?tournament_id='+id).catch(()=>[]);
document.getElementById('matchlist').innerHTML = matches.map(m=>`
<div class="card">
<div><strong>${m.round || 'Match'}</strong> — ${m.court || 'Court ?'}</div>
<div>Teams: ${m.team_a_id || '?'} vs ${m.team_b_id || '?'}</div>
<div>Score: ${m.score_a} - ${m.score_b} (${m.status})</div>
<div>${m.scheduled_at ? new Date(m.scheduled_at).toLocaleString() : ''}</div>
</div>
`).join('') || '<p class="muted">Aucun match</p>';
}
load();
})();

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Événements — Super Sunday</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<h1>Événements à venir</h1>
<nav>
<a href="/">Accueil</a>
<a href="/events">Événements</a>
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<div id="list"></div>
</main>
<script src="/assets/api.js"></script>
<script src="/assets/events.js"></script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Super Sunday — Accueil</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<h1>Super Sunday — Padel Championship</h1>
<nav>
<a href="/">Accueil</a>
<a href="/events">Événements</a>
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<section>
<h2>Tournois</h2>
<div id="tournaments"></div>
</section>
<section>
<h2>Matches</h2>
<div id="matches" class="empty">Aucun match</div>
</section>
</main>
<!-- Bubbles -->
<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>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Détail tournoi — Super Sunday</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<header>
<h1>Détail du tournoi</h1>
<nav>
<a href="/">Accueil</a>
<a href="/events">Événements</a>
<a href="/admin">Admin</a>
</nav>
</header>
<main class="container">
<section id="info" class="card"></section>
<section>
<h2>Participants</h2>
<div id="participants"></div>
</section>
<section>
<h2>Matches</h2>
<div id="matchlist"></div>
</section>
</main>
<!-- Bubbles -->
<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>
</body>
</html>