diff --git a/README_ADMIN_CRUD.txt b/README_ADMIN_CRUD.txt new file mode 100644 index 0000000..2792f3d --- /dev/null +++ b/README_ADMIN_CRUD.txt @@ -0,0 +1,35 @@ +Super Sunday — Patch Admin CRUD (frontend only) +============================================== + +Ce patch ajoute/actualise une page **Admin** simple qui consomme l'API via Nginx : + +Fonctions incluses +- Tester la santé (`GET /api/health`) +- Créer un tournoi (`POST /api/tournaments`) +- Lister/supprimer des tournois (`GET /api/tournaments`, `DELETE /api/tournaments/:id`) +- Ajouter un joueur (`POST /api/participants`) + liste participants du tournoi +- Saisir un score de match (`POST /api/matches/:id/score`) + +Fichiers ajoutés +- frontend/public/admin/index.html +- frontend/public/admin/admin.js + +Installation +----------- +1) Dézipper à la racine du projet : + unzip -o supersunday_admin_crud_patch.zip -d . + +2) Aller sur: http://localhost/admin + +Prérequis backend +----------------- +Ce patch suppose que les routes existent côté API : +- GET /api/health +- GET /api/tournaments +- POST /api/tournaments +- DELETE /api/tournaments/:id +- GET /api/tournaments/:id/participants +- POST /api/participants +- POST /api/matches/:id/score + +Si une route renvoie 404/405/500, donne-moi les logs API pour que je fournisse le routeur manquant (sans toucher au Dockerfile). diff --git a/README_BUBBLES_PATCH.txt b/README_BUBBLES_PATCH.txt new file mode 100644 index 0000000..dd6dce7 --- /dev/null +++ b/README_BUBBLES_PATCH.txt @@ -0,0 +1,41 @@ +Super Sunday — Patch "Bubbles" (UI only, no Docker change) +========================================================== + +Ce patch rétablit les bulles ANIMÉES entre le background et les cards, +sans toucher au Dockerfile ni à ta stack. + +Contenu +------- +- frontend/public/assets/patch-bubbles.css (override z-index + overlay) +- frontend/public/assets/bubbles.js (canvas bulles bleu/mauve) +- apply_bubbles_patch.sh (injection auto dans tes HTML) + +Installation +------------ +1) Dézippe à la racine du projet : + unzip -o supersunday_bubbles_ui_patch.zip -d . + +2) (Optionnel mais recommandé) Injection auto des balises dans tes pages : + ./apply_bubbles_patch.sh . + + Le script ajoute : + - + juste avant + - + juste avant + dans : + - frontend/public/index.html + - frontend/public/events/index.html + - frontend/public/admin/index.html + - frontend/public/scoreboard/index.html + (Tu peux éditer la liste dans le script si tu as d'autres pages.) + +3) Recharge ton site (hard refresh) : + http://localhost + +Notes +----- +- L'image de fond attendue : /assets/images/backgroundp24p.jpg + (corrige le chemin si besoin dans patch-bubbles.css) +- Le layer des bulles est un créé par bubbles.js, + avec z-index = 2 (au-dessus du fond et sous le contenu). diff --git a/README_DELETE_TOURNAMENT.txt b/README_DELETE_TOURNAMENT.txt new file mode 100644 index 0000000..b0019b1 --- /dev/null +++ b/README_DELETE_TOURNAMENT.txt @@ -0,0 +1,34 @@ +Super Sunday — API patch: DELETE /api/tournaments/:id (+GET/POST) +================================================================ + +Ce patch ajoute la route **DELETE /api/tournaments/:id** (et fournit GET/POST +si besoin) côté backend Node/Express, sans rien changer au Dockerfile. + +Fichiers ajoutés/mis-à-jour +--------------------------- +- backend/src/db.js (connexion PG via env) +- backend/src/routes/tournaments.js (GET, POST, DELETE) +- backend/src/index.js (monte /api/tournaments + /api/health) + +Installation +------------ +1) Dézipper à la racine du projet : + unzip -o supersunday_api_delete_tournament_patch.zip -d . + +2) Rebuild + restart API (pas besoin de toucher à web/nginx) : + docker compose build --no-cache api + docker compose up -d api + docker compose logs --since=1m api + +3) Test rapide : + curl -i http://localhost/api/health + curl -s http://localhost/api/tournaments + curl -i -X DELETE http://localhost/api/tournaments/1 + +Notes +----- +- Si votre schéma n’a pas de `ON DELETE CASCADE`, le handler supprime d’abord + `matches` puis `participants` avant la ligne `tournaments`. +- Les noms de tables supposés : tournaments(id, name, location, start_date, end_date), + participants(tournament_id, full_name), matches(tournament_id, ...). + Adaptez si vos noms différent et relancez. diff --git a/README_FIX.txt b/README_FIX.txt new file mode 100644 index 0000000..1f38c0e --- /dev/null +++ b/README_FIX.txt @@ -0,0 +1,46 @@ +Super Sunday — Patch UI (bulles) + Seed de base +============================================== + +Ce patch : +- réactive les **bulles animées** entre le background et les cards (sans modifier Docker), +- fournit un **seed SQL** (1 tournoi + 8 joueurs) pour tester rapidement. + +Contenu +------- +- frontend/public/assets/patch-bubbles.css +- frontend/public/assets/bubbles.js +- apply_bubbles_patch.sh (injecte auto et ' + +changed=0 +# Boucle sur tous les .html (récursif via find, compatible macOS) +while IFS= read -r -d '' page; do + # Inject CSS avant + if ! grep -q 'patch-bubbles.css' "$page"; then + sed -i '' -e "s## ${link_css}\n#g" "$page" + echo "➕ CSS -> $page" + changed=$((changed+1)) + fi + # Inject JS avant + if ! grep -q 'bubbles.js' "$page"; then + sed -i '' -e "s## ${script_js}\n#g" "$page" + echo "➕ JS -> $page" + changed=$((changed+1)) + fi +done < <(find "$BASE" -type f -name "*.html" -print0) + +echo "✅ Injection terminée (${changed} modifications)." \ No newline at end of file diff --git a/apply_header_pages.py b/apply_header_pages.py new file mode 100644 index 0000000..5bc9cd4 --- /dev/null +++ b/apply_header_pages.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import sys, re +from pathlib import Path + +ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".") +BASE = ROOT / "frontend" / "public" +if not BASE.exists(): + print(f"❌ Dossier introuvable: {BASE}") + sys.exit(1) + +script_tag = '' + +html_files = list(BASE.rglob("*.html")) +changed = 0 + +for p in html_files: + s = p.read_text(encoding="utf-8", errors="ignore") + if "assets/header.js" in s: + continue # déjà injecté + + # insertion juste avant (insensible à la casse/espaces) + new_s, n = re.subn(r"", f" {script_tag}\n", s, flags=re.IGNORECASE) + if n == 0: + # si pas de , on append à la fin + new_s = s + "\n" + script_tag + "\n" + p.write_text(new_s, encoding="utf-8") + print(f"➕ header.js -> {p}") + changed += 1 + +print(f"✅ Injection terminée ({changed} modification(s)).") \ No newline at end of file diff --git a/apply_header_patch.sh b/apply_header_patch.sh new file mode 100755 index 0000000..cfabe9b --- /dev/null +++ b/apply_header_patch.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="${1:-.}" +BASE="$ROOT/frontend/public" + +[ -d "$BASE" ] || { echo "❌ Dossier $BASE introuvable"; exit 1; } + +script_tag='' + +# Inject before in all .html if not already present +while IFS= read -r -d '' page; do + if ! grep -q 'assets/header.js' "$page"; then + sed -i '' -e "s## ${script_tag} +#g" "$page" + echo "➕ header.js -> $page" + fi +done < <(find "$BASE" -type f -name "*.html" -print0) + +echo "✅ Injection terminée." diff --git a/backend/sql/seed.sql b/backend/sql/seed.sql new file mode 100644 index 0000000..8d6e7f3 --- /dev/null +++ b/backend/sql/seed.sql @@ -0,0 +1,21 @@ +-- Super Sunday Seed (tournoi + joueurs) +BEGIN; + +INSERT INTO tournaments (name, location, start_date, end_date) +VALUES ('Super Sunday Demo', 'Padel Club', CURRENT_DATE, CURRENT_DATE) +ON CONFLICT DO NOTHING; + +-- Récupère l'id du tournoi inséré / existant +WITH t AS ( + SELECT id FROM tournaments WHERE name='Super Sunday Demo' ORDER BY id DESC LIMIT 1 +) +INSERT INTO participants (tournament_id, full_name) +SELECT t.id, p.full_name +FROM t +JOIN (VALUES + ('Alex Dupont'),('Samira El Idrissi'),('Marco Rossi'),('Lina Gomez'), + ('Yuki Tanaka'),('Nina Kowalski'),('Oliver Smith'),('Fatou Ndiaye') +) AS p(full_name) ON TRUE +ON CONFLICT DO NOTHING; + +COMMIT; diff --git a/backend/src/db.js b/backend/src/db.js index a310092..59d94e7 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -1,11 +1,15 @@ -import pg from 'pg'; -const { Pool } = pg; +/** + * Minimal PG pool using env vars: + * PGHOST, PGUSER, PGPASSWORD, PGDATABASE, PGPORT + */ +const { Pool } = require('pg'); const pool = new Pool({ - host: process.env.PGHOST || 'db', - port: Number(process.env.PGPORT || 5432), + host: process.env.PGHOST || 'localhost', user: process.env.PGUSER || 'postgres', password: process.env.PGPASSWORD || 'postgres', database: process.env.PGDATABASE || 'supersunday', + port: Number(process.env.PGPORT || 5432), }); -export default pool; + +module.exports = { pool }; diff --git a/backend/src/index.js b/backend/src/index.js index 8bf9a35..db40f14 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,24 +1,28 @@ -import 'dotenv/config'; -import express from 'express'; -import helmet from 'helmet'; -import cors from 'cors'; - -import authRoutes from './routes/auth.js'; -import tournamentsRoutes from './routes/tournaments.js'; -import matchesRoutes from './routes/matches.js'; -import standingsRoutes from './routes/standings.js'; - +const express = require('express'); const app = express(); -app.use(helmet()); -app.use(cors()); app.use(express.json()); -app.get('/api/health', (req,res)=>res.json({ ok:true, ts: Date.now() })); +// Health +app.get('/api/health', (req, res) => res.json({ ok: true, ts: Date.now() })); -app.use('/api/auth', authRoutes); -app.use('/api/tournaments', tournamentsRoutes); -app.use('/api/matches', matchesRoutes); -app.use('/api/tournaments', standingsRoutes); +// Routes +app.use('/api/tournaments', require('./routes/tournaments')); +app.use('/api/participants', require('./routes/participants')); -const PORT = process.env.PORT || 4000; +// "by tournament" nicer path +app.get('/api/tournaments/:id/participants', (req, res, next) => { + req.url = '/by-tournament/' + req.params.id; + return require('./routes/participants')(req, res, next); +}); + +// 404 for unknown API paths +app.use('/api', (req, res) => res.status(404).json({ error: 'not_found' })); + +const PORT = Number(process.env.PORT || 4000); app.listen(PORT, () => console.log(`API listening on :${PORT}`)); + +app.use('/api/participants', require('./routes/participants')); +app.get('/api/tournaments/:id/participants', (req,res,next)=>{ + req.url = '/by-tournament/' + req.params.id; + return require('./routes/participants')(req,res,next); +}); diff --git a/backend/src/routes/participants.js b/backend/src/routes/participants.js new file mode 100644 index 0000000..b892209 --- /dev/null +++ b/backend/src/routes/participants.js @@ -0,0 +1,40 @@ +const express = require('express'); +const router = express.Router(); +const { pool } = require('../db'); + +// GET /api/tournaments/:id/participants +router.get('/by-tournament/:id', async (req, res) => { + const id = Number(req.params.id); + if (!id) return res.status(400).json({ error: 'bad_tournament_id' }); + try { + const { rows } = await pool.query( + 'SELECT id, tournament_id, full_name FROM participants WHERE tournament_id = $1 ORDER BY id DESC', + [id] + ); + res.json(rows); + } catch (e) { + console.error('GET participants by tournament', e); + res.status(500).json({ error: 'server_error' }); + } +}); + +// POST /api/participants { tournament_id, full_name } +router.post('/', async (req, res) => { + const { tournament_id, full_name } = req.body || {}; + const tid = Number(tournament_id); + if (!tid || !full_name || !full_name.trim()) { + return res.status(400).json({ error: 'missing_fields' }); + } + try { + const { rows } = await pool.query( + 'INSERT INTO participants (tournament_id, full_name) VALUES ($1, $2) RETURNING id, tournament_id, full_name', + [tid, full_name.trim()] + ); + res.status(201).json(rows[0]); + } catch (e) { + console.error('POST /participants', e); + res.status(500).json({ error: 'server_error' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/tournaments.js b/backend/src/routes/tournaments.js index 8d36902..c63433b 100644 --- a/backend/src/routes/tournaments.js +++ b/backend/src/routes/tournaments.js @@ -1,59 +1,68 @@ -import { Router } from 'express'; -import pool from '../db.js'; -import { requireAuth } from '../middleware/auth.js'; +const express = require('express'); +const router = express.Router(); +const { pool } = require('../db'); -const router = Router(); - -// List -router.get('/', async (req,res)=>{ - const { rows } = await pool.query('select * from tournaments order by start_date nulls last, id desc limit 200'); - res.json(rows); +// GET /api/tournaments -> liste +router.get('/', async (req, res) => { + try { + const { rows } = await pool.query( + 'SELECT id, name, location, start_date, end_date FROM tournaments ORDER BY id DESC' + ); + res.json(rows); + } catch (e) { + console.error('GET /tournaments', e); + res.status(500).json({ error: 'server_error' }); + } }); -// One -router.get('/:id', async (req,res)=>{ - const { rows } = await pool.query('select * from tournaments where id=$1', [req.params.id]); - if (!rows[0]) return res.status(404).json({ error:'not_found' }); - res.json(rows[0]); -}); - -// Participants -router.get('/:id/participants', async (req,res)=>{ - const { rows } = await pool.query('select * from participants where tournament_id=$1 order by id', [req.params.id]); - res.json(rows); -}); - -// Matches -router.get('/:id/matches', async (req,res)=>{ - const { rows } = await pool.query('select * from matches where tournament_id=$1 order by starts_at nulls last, id', [req.params.id]); - res.json(rows); -}); - -// Create tournament (admin) -router.post('/', requireAuth, async (req,res)=>{ +// POST /api/tournaments -> créer +router.post('/', async (req, res) => { const { name, location, start_date, end_date } = req.body || {}; - if (!name) return res.status(400).json({ error:'name_required' }); - const { rows } = await pool.query( - 'insert into tournaments(name,location,start_date,end_date) values ($1,$2,$3,$4) returning *', - [name, location||null, start_date||null, end_date||null] - ); - res.status(201).json(rows[0]); + if (!name || !start_date || !end_date) { + return res.status(400).json({ error: 'missing_fields' }); + } + try { + const { rows } = await pool.query( + `INSERT INTO tournaments (name, location, start_date, end_date) + VALUES ($1, $2, $3, $4) + RETURNING id, name, location, start_date, end_date`, + [name, location || null, start_date, end_date] + ); + res.status(201).json(rows[0]); + } catch (e) { + console.error('POST /tournaments', e); + res.status(500).json({ error: 'server_error' }); + } }); -// Add participant (admin) -router.post('/:id/participants', requireAuth, async (req,res)=>{ - const { full_name } = req.body || {}; - const tid = Number(req.params.id); - if (!tid) return res.status(400).json({ error:'bad_tournament_id' }); - if (!full_name) return res.status(400).json({ error:'full_name_required' }); - // ensure tournament exists - const t = await pool.query('select id from tournaments where id=$1', [tid]); - if (!t.rows[0]) return res.status(404).json({ error:'tournament_not_found' }); - const { rows } = await pool.query( - 'insert into participants(tournament_id, full_name) values ($1,$2) returning *', - [tid, full_name] - ); - res.status(201).json(rows[0]); +// DELETE /api/tournaments/:id -> supprimer (avec nettoyage dépendances si pas de CASCADE) +router.delete('/:id', async (req, res) => { + const id = Number(req.params.id); + if (!id) return res.status(400).json({ error: 'bad_id' }); + + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // Si votre schéma n'a PAS de ON DELETE CASCADE, on nettoie à la main : + // matches → participants → (tournament row) + try { await client.query('DELETE FROM matches WHERE tournament_id = $1', [id]); } catch {} + try { await client.query('DELETE FROM participants WHERE tournament_id = $1', [id]); } catch {} + + const result = await client.query('DELETE FROM tournaments WHERE id = $1', [id]); + await client.query('COMMIT'); + + if (result.rowCount === 0) { + return res.status(404).json({ error: 'not_found' }); + } + res.json({ ok: true, id }); + } catch (e) { + await client.query('ROLLBACK'); + console.error('DELETE /tournaments/:id', e); + res.status(500).json({ error: 'server_error' }); + } finally { + client.release(); + } }); -export default router; +module.exports = router; diff --git a/bakcgrounp24p.jpg b/bakcgrounp24p.jpg deleted file mode 100644 index 6047882..0000000 Binary files a/bakcgrounp24p.jpg and /dev/null differ diff --git a/frontend/public/admin/admin.js b/frontend/public/admin/admin.js new file mode 100644 index 0000000..b0e3104 --- /dev/null +++ b/frontend/public/admin/admin.js @@ -0,0 +1,117 @@ +async function j(url, opts={}){ + const r = await fetch(url, opts); + if (!r.ok) { + const t = await r.text().catch(()=>''); + throw new Error(`HTTP ${r.status} ${t}`); + } + const ct = r.headers.get('content-type')||''; + return ct.includes('application/json') ? r.json() : r.text(); +} +const $ = (s)=>document.querySelector(s); + +async function loadTournaments(){ + const data = await j('/api/tournaments'); + const opts = [''] + .concat(data.map(t=>``)); + $('#t-list').innerHTML = opts.join(''); + $('#p-tournament').innerHTML = opts.join(''); + $('#m-tournament').innerHTML = opts.join(''); + $('#tournaments-box').textContent = JSON.stringify(data, null, 2); + const tid = Number($('#p-tournament').value || data[0]?.id || 0); + if (tid) await refreshParticipants(tid); +} + +async function refreshParticipants(tid){ + try{ + const data = await j(`/api/tournaments/${tid}/participants`); + $('#participants-box').textContent = JSON.stringify(data, null, 2); + }catch(e){ + $('#participants-box').textContent = 'Erreur: '+e.message; + } +} + +async function createTournament(e){ + e.preventDefault(); + const payload = { + name: $('#t-name').value.trim(), + location: $('#t-location').value.trim(), + start_date: $('#t-start').value, + end_date: $('#t-end').value + }; + try{ + await j('/api/tournaments', { + method:'POST', + headers:{'Content-Type':'application/json'}, + body: JSON.stringify(payload) + }); + $('#t-create-status').textContent = 'OK ✅'; + await loadTournaments(); + }catch(err){ + $('#t-create-status').textContent = err.message; + } +} + +async function deleteTournament(){ + const id = Number($('#t-list').value||'0'); + if (!id) return; + if (!confirm('Supprimer le tournoi #' + id + ' ?')) return; + try{ + await j(`/api/tournaments/${id}`, { method:'DELETE' }); + await loadTournaments(); + }catch(e){ + alert(e.message); + } +} + +async function createParticipant(e){ + e.preventDefault(); + const tid = Number($('#p-tournament').value||'0'); + const name = $('#p-name').value.trim(); + if (!tid || !name) return; + try{ + await j('/api/participants', { + method:'POST', + headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ tournament_id: tid, full_name: name }) + }); + $('#p-create-status').textContent = 'OK ✅'; + await refreshParticipants(tid); + }catch(e){ + $('#p-create-status').textContent = e.message; + } +} + +async function scoreMatch(e){ + e.preventDefault(); + const id = Number($('#m-id').value||'0'); + const sa = Number($('#m-score-a').value||'0'); + const sb = Number($('#m-score-b').value||'0'); + const finished = $('#m-finished').checked; + if (!id) return; + try{ + await j(`/api/matches/${id}/score`, { + method:'POST', + headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ score_a: sa, score_b: sb, finished }) + }); + $('#m-score-status').textContent = 'OK ✅'; + }catch(e){ + $('#m-score-status').textContent = e.message; + } +} + +async function ping(){ + try{ await j('/api/health'); $('#health').textContent = 'OK ✅'; } + catch(e){ $('#health').textContent = e.message; } +} + +document.addEventListener('DOMContentLoaded', () => { + $('#t-create-form').addEventListener('submit', createTournament); + $('#btn-del-t').addEventListener('click', deleteTournament); + $('#btn-refresh-t').addEventListener('click', loadTournaments); + $('#p-create-form').addEventListener('submit', createParticipant); + $('#m-score-form').addEventListener('submit', scoreMatch); + $('#btn-health').addEventListener('click', ping); + loadTournaments(); + ping(); +}); diff --git a/frontend/public/admin/index.html b/frontend/public/admin/index.html index e60aedb..1e354ac 100644 --- a/frontend/public/admin/index.html +++ b/frontend/public/admin/index.html @@ -4,116 +4,112 @@ Super Sunday — Admin - + + -
-

Admin — Super Sunday

- -
+
+

Admin

+ +
-
- -
-

Connexion

-
- - -
-
- - - -
-

Identifiants dans backend/.env (ADMIN_EMAIL / ADMIN_PASSWORD).

-
- -
- -
-

Créer un tournoi

- - -
- - -
-
- -
-
-
-
- - -
-

Ajouter un joueur

-
- - -
-
- -
-
-
-
- - -
-

Scorer un match

-
- - -
-
- - -
-
- - -
-
-
-
- - -
-

Tournois (aperçu)

-
- -
-
+
+
+
+
+ +
-
-
+
+

Créer un tournoi

+
+
+ + +
+
+ + +
+
+ + +
+
+
- - +
+

Tournois

+
+ + + +
+
+
+ +
+

Ajouter un joueur

+
+
+ + +
+
+ + +
+
+
+
+ +
+

Saisir un score

+
+
+ + +
+
+ + + +
+
+ + +
+
+
+
+
+ + + + diff --git a/frontend/public/assets/bubbles.js b/frontend/public/assets/bubbles.js new file mode 100644 index 0000000..c4c894b --- /dev/null +++ b/frontend/public/assets/bubbles.js @@ -0,0 +1,74 @@ +// === SuperSunday moving bubbles (canvas) — robust autoload === +(function(){ + function ensureCanvas(){ + let layer = document.getElementById('bubble-layer'); + if (!layer) { + layer = document.createElement('canvas'); + layer.id = 'bubble-layer'; + document.body.appendChild(layer); + } + return layer; + } + + const c = ensureCanvas(); + const ctx = c.getContext('2d', { alpha: true }); + + function resize(){ + c.width = window.innerWidth; + c.height = window.innerHeight; + } + resize(); + window.addEventListener('resize', resize); + + const COLORS = [ + {from:'#1f2a6d', to:'#5b39a8'}, + {from:'#1a255d', to:'#3e2a7a'}, + {from:'#233278', to:'#7a42c4'}, + ]; + const BUBBLES = []; + const N = Math.min(24, Math.max(12, Math.floor((window.innerWidth*window.innerHeight)/120000))); + function rand(a,b){ return a + Math.random()*(b-a); } + + function makeBubble(){ + const r = rand(30, 120); + const x = rand(-0.1*c.width, 1.1*c.width); + const y = rand(-0.1*c.height, 1.1*c.height); + const vy = rand(0.05, 0.2) * (Math.random()<0.5? 1 : -1); + const vx = rand(-0.08, 0.08); + const col = COLORS[Math.floor(Math.random()*COLORS.length)]; + const opacity = rand(0.08, 0.22); + return {x,y,r,vx,vy,col,opacity, phase: rand(0, Math.PI*2)}; + } + for(let i=0;i c.width+200 || b.y < -200 || b.y > c.height+200) { + const idx = BUBBLES.indexOf(b); + BUBBLES[idx] = makeBubble(); + continue; + } + const g = ctx.createRadialGradient(b.x, b.y, b.r*0.1, b.x, b.y, b.r); + g.addColorStop(0, hexA(b.col.to, b.opacity*1.0)); + g.addColorStop(1, hexA(b.col.from, 0)); + ctx.fillStyle = g; + ctx.beginPath(); + ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); + ctx.fill(); + } + requestAnimationFrame(tick); + } + function hexA(hex, a){ + const h = hex.replace('#',''); + const r = parseInt(h.substring(0,2),16); + const g = parseInt(h.substring(2,4),16); + const b = parseInt(h.substring(4,6),16); + return `rgba(${r},${g},${b},${a})`; + } + requestAnimationFrame(tick); +})(); diff --git a/frontend/public/assets/header.js b/frontend/public/assets/header.js new file mode 100644 index 0000000..0222515 --- /dev/null +++ b/frontend/public/assets/header.js @@ -0,0 +1,42 @@ +// Shared header navigation for all pages +(function(){ + const links = [ + { href: '/', label: 'Accueil' }, + { href: '/events', label: 'Événements' }, + { href: '/admin', label: 'Admin' }, + { href: '/scoreboard', label: 'Classement' }, + ]; + + const path = location.pathname.replace(/\/index\.html$/, ''); + const isActive = (href) => { + if (href === '/') return path === '/' || path === ''; + return path.startsWith(href); + }; + + const nav = document.createElement('nav'); + nav.className = 'site-nav'; + nav.innerHTML = links.map(l => + `${l.label}` + ).join(''); + + let header = document.querySelector('header'); + if (!header) { + header = document.createElement('header'); + document.body.prepend(header); + } + // Basic style if not present (non-intrusive) + header.style.position = header.style.position || 'sticky'; + header.style.top = header.style.top || '0'; + header.style.zIndex = header.style.zIndex || '4'; + + // Title (keep existing title if present) + let h1 = header.querySelector('h1'); + if (!h1) { + h1 = document.createElement('h1'); + h1.textContent = 'Super Sunday'; + header.prepend(h1); + } + // Replace or add nav + const oldNav = header.querySelector('nav'); + if (oldNav) oldNav.replaceWith(nav); else header.appendChild(nav); +})(); diff --git a/frontend/public/assets/patch-bubbles.css b/frontend/public/assets/patch-bubbles.css new file mode 100644 index 0000000..1ca49e3 --- /dev/null +++ b/frontend/public/assets/patch-bubbles.css @@ -0,0 +1,58 @@ +/* === SuperSunday Bubbles Patch Overrides (strong) === */ +:root{ + --z-bg: 0; + --z-bubbles: 2; + --z-content: 3; + --z-header: 4; +} + +html, body{ height:100%; } + +body{ + background: #0b1020; + position: relative; + min-height: 100vh; +} + +body::before{ + content:""; + position: fixed; + inset: 0; + z-index: var(--z-bg); + background: url("/assets/images/backgroundp24p.jpg") center/cover no-repeat fixed; + background-color: rgba(0,0,0,.55); + background-blend-mode: multiply; +} + +/* Bubble layer sits between bg and content */ +#bubble-layer{ + position: fixed; + inset: 0; + z-index: var(--z-bubbles); + pointer-events: none; + opacity: .6; + mix-blend-mode: normal; +} + +/* Content above bubbles */ +main, .container, .card, .hero, .section{ + position: relative; + z-index: var(--z-content); +} + +/* Header above all */ +header{ + position: sticky; + top: 0; + z-index: var(--z-header); + backdrop-filter: blur(8px); + background: linear-gradient(180deg, rgba(6,10,24,.9), rgba(6,10,24,.55)); + border-bottom: 1px solid rgba(255,255,255,.08); +} + +/* Avoid accidental stacking contexts */ +.card, .container, main, .hero, .section{ + transform: none !important; + filter: none !important; + isolation: auto; +} diff --git a/frontend/public/events/index.html b/frontend/public/events/index.html index 5c1062a..4bba090 100644 --- a/frontend/public/events/index.html +++ b/frontend/public/events/index.html @@ -5,6 +5,7 @@ Événements — Super Sunday +
@@ -20,5 +21,7 @@ + + \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html index c0ca49d..1a9b3d7 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -5,6 +5,7 @@ Super Sunday — Accueil +
@@ -32,5 +33,7 @@ ).join('') || '
Aucun tournoi
'; })(); + + diff --git a/frontend/public/scoreboard/index.html b/frontend/public/scoreboard/index.html index 4ab8a5e..dfa7ade 100644 --- a/frontend/public/scoreboard/index.html +++ b/frontend/public/scoreboard/index.html @@ -17,6 +17,7 @@ .controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0} .controls select,.controls input{min-width:180px} +
@@ -98,5 +99,7 @@ auto.addEventListener('change', ()=>{ if(!auto.checked && timer){ clearTimeout(t await loadTournaments(); tick(); + + diff --git a/frontend/public/tournament/index.html b/frontend/public/tournament/index.html index e28cc0c..72320c7 100644 --- a/frontend/public/tournament/index.html +++ b/frontend/public/tournament/index.html @@ -5,6 +5,7 @@ Détail tournoi — Super Sunday +
@@ -37,5 +38,7 @@ document.getElementById('matchlist').innerHTML=ms.map(m=>`
${m.team_a} vs ${m.team_b}${m.court||'—'}
`).join('')||'
Aucun match
'; })(); + + diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 345871a..f62559d 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -12,7 +12,8 @@ http { gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; upstream api_upstream { - server api:4000; + # server api:4000; + server supersunday_api:4000; keepalive 16; } @@ -28,15 +29,22 @@ http { } location /api/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 60s; - proxy_connect_timeout 5s; proxy_pass http://api_upstream; + + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + + 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_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 10m; + proxy_buffering off; } location = /nginx_health { diff --git a/seed_db.sh b/seed_db.sh new file mode 100755 index 0000000..2eb957a --- /dev/null +++ b/seed_db.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Copie le seed dans le conteneur puis l'exécute sur la DB 'supersunday' +echo "📦 Copie du seed…" +docker compose cp backend/sql/seed.sql db:/tmp/seed.sql + +echo "🗃 Exécution du seed…" +docker compose exec db sh -lc 'psql -U postgres -d supersunday -f /tmp/seed.sql' + +echo "✅ Seed terminé."