Header menu via header.js
This commit is contained in:
35
README_ADMIN_CRUD.txt
Normal file
35
README_ADMIN_CRUD.txt
Normal file
@@ -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).
|
||||||
41
README_BUBBLES_PATCH.txt
Normal file
41
README_BUBBLES_PATCH.txt
Normal file
@@ -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 :
|
||||||
|
- <link rel="stylesheet" href="/assets/patch-bubbles.css?v=1" />
|
||||||
|
juste avant </head>
|
||||||
|
- <script type="module" src="/assets/bubbles.js?v=1"></script>
|
||||||
|
juste avant </body>
|
||||||
|
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 <canvas id="bubble-layer"> créé par bubbles.js,
|
||||||
|
avec z-index = 2 (au-dessus du fond et sous le contenu).
|
||||||
34
README_DELETE_TOURNAMENT.txt
Normal file
34
README_DELETE_TOURNAMENT.txt
Normal file
@@ -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.
|
||||||
46
README_FIX.txt
Normal file
46
README_FIX.txt
Normal file
@@ -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 <link> et <script> dans tous les HTML sous frontend/public)
|
||||||
|
- backend/sql/seed.sql (tournoi + joueurs)
|
||||||
|
- seed_db.sh (copie/execute le seed dans le conteneur Postgres)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
1) Dézippe à la racine du projet :
|
||||||
|
unzip -o supersunday_fix_bubbles_and_seed_patch.zip -d .
|
||||||
|
|
||||||
|
2) Injecte les assets bulles dans toutes tes pages HTML :
|
||||||
|
./apply_bubbles_patch.sh .
|
||||||
|
|
||||||
|
3) Relance le stack si nécessaire (ou recharge la page).
|
||||||
|
(Si tu utilises Nginx front, juste rafraîchir http://localhost)
|
||||||
|
|
||||||
|
Seed (optionnel mais recommandé)
|
||||||
|
--------------------------------
|
||||||
|
1) Démarre DB si pas déjà up :
|
||||||
|
docker compose up -d db
|
||||||
|
|
||||||
|
2) Applique le seed :
|
||||||
|
./seed_db.sh
|
||||||
|
|
||||||
|
3) Vérifie côté API :
|
||||||
|
- GET /api/tournaments
|
||||||
|
- GET /api/tournaments/:id/participants
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
- L'image de fond attendue : /assets/images/backgroundp24p.jpg
|
||||||
|
(modifie le chemin dans patch-bubbles.css si besoin)
|
||||||
|
- Si les bulles ne s'affichent toujours pas :
|
||||||
|
- vérifie dans DevTools > Network que /assets/patch-bubbles.css et /assets/bubbles.js sont en **200 OK**,
|
||||||
|
- inspecte l'élément <canvas id="bubble-layer"> (doit exister dans <body>),
|
||||||
|
- vérifie que tes cards n'ont pas de z-index supérieur au header (par défaut, --z-content est 3).
|
||||||
53
README_PARTICIPANTS_HEADER.txt
Normal file
53
README_PARTICIPANTS_HEADER.txt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
Super Sunday — Participants API + Header partagé
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
Ce patch apporte :
|
||||||
|
1) **Routes backend Participants**
|
||||||
|
- `POST /api/participants` (body: { tournament_id, full_name })
|
||||||
|
- `GET /api/tournaments/:id/participants`
|
||||||
|
(via un routeur dédié + alias pratique)
|
||||||
|
|
||||||
|
2) **Header mutualisé côté frontend**
|
||||||
|
- `assets/header.js` remplace/injecte le menu dans un `<header>` unique
|
||||||
|
- Script d'injection qui ajoute la balise script à toutes les pages
|
||||||
|
|
||||||
|
Contenu
|
||||||
|
-------
|
||||||
|
- backend/src/routes/participants.js
|
||||||
|
- backend/src/index.js (baseline avec montages; optionnel si tu as déjà un index)
|
||||||
|
- frontend/public/assets/header.js
|
||||||
|
- apply_header_patch.sh
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
1) Dézippe à la racine du projet :
|
||||||
|
unzip -o supersunday_participants_and_header_patch.zip -d .
|
||||||
|
|
||||||
|
2) Backend (si tu utilises l'index fourni) :
|
||||||
|
docker compose build --no-cache api
|
||||||
|
docker compose up -d api
|
||||||
|
|
||||||
|
Si tu conserves ton propre `backend/src/index.js`, **assure-toi d'ajouter** :
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
3) Frontend — header partagé sur toutes les pages :
|
||||||
|
./apply_header_patch.sh .
|
||||||
|
(le script ajoute <script type="module" src="/assets/header.js"> avant </body>)
|
||||||
|
|
||||||
|
Tests rapides
|
||||||
|
-------------
|
||||||
|
# Ajouter un joueur dans le tournoi 1
|
||||||
|
curl -s -X POST http://localhost/api/participants -H 'Content-Type: application/json' -d '{"tournament_id":1,"full_name":"Nouveau Joueur"}'
|
||||||
|
|
||||||
|
# Lister les participants du tournoi 1
|
||||||
|
curl -s http://localhost/api/tournaments/1/participants
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
- Aucun changement Docker requis.
|
||||||
|
- Les noms de tables attendus: participants(id, tournament_id, full_name).
|
||||||
|
- Si ton schéma diffère, dis-moi les colonnes exactes et je t'adapte la requête.
|
||||||
29
apply_bubbles_patch.sh
Executable file
29
apply_bubbles_patch.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="${1:-.}"
|
||||||
|
BASE="$ROOT/frontend/public"
|
||||||
|
|
||||||
|
[ -d "$BASE" ] || { echo "❌ Dossier $BASE introuvable"; exit 1; }
|
||||||
|
|
||||||
|
link_css='<link rel="stylesheet" href="/assets/patch-bubbles.css?v=2" />'
|
||||||
|
script_js='<script type="module" src="/assets/bubbles.js?v=2"></script>'
|
||||||
|
|
||||||
|
changed=0
|
||||||
|
# Boucle sur tous les .html (récursif via find, compatible macOS)
|
||||||
|
while IFS= read -r -d '' page; do
|
||||||
|
# Inject CSS avant </head>
|
||||||
|
if ! grep -q 'patch-bubbles.css' "$page"; then
|
||||||
|
sed -i '' -e "s#</head># ${link_css}\n</head>#g" "$page"
|
||||||
|
echo "➕ CSS -> $page"
|
||||||
|
changed=$((changed+1))
|
||||||
|
fi
|
||||||
|
# Inject JS avant </body>
|
||||||
|
if ! grep -q 'bubbles.js' "$page"; then
|
||||||
|
sed -i '' -e "s#</body># ${script_js}\n</body>#g" "$page"
|
||||||
|
echo "➕ JS -> $page"
|
||||||
|
changed=$((changed+1))
|
||||||
|
fi
|
||||||
|
done < <(find "$BASE" -type f -name "*.html" -print0)
|
||||||
|
|
||||||
|
echo "✅ Injection terminée (${changed} modifications)."
|
||||||
30
apply_header_pages.py
Normal file
30
apply_header_pages.py
Normal file
@@ -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 = '<script type="module" src="/assets/header.js?v=1"></script>'
|
||||||
|
|
||||||
|
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 </body> (insensible à la casse/espaces)
|
||||||
|
new_s, n = re.subn(r"</body\s*>", f" {script_tag}\n</body>", s, flags=re.IGNORECASE)
|
||||||
|
if n == 0:
|
||||||
|
# si pas de </body>, 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)).")
|
||||||
20
apply_header_patch.sh
Executable file
20
apply_header_patch.sh
Executable file
@@ -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='<script type="module" src="/assets/header.js?v=1"></script>'
|
||||||
|
|
||||||
|
# Inject before </body> 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#</body># ${script_tag}
|
||||||
|
</body>#g" "$page"
|
||||||
|
echo "➕ header.js -> $page"
|
||||||
|
fi
|
||||||
|
done < <(find "$BASE" -type f -name "*.html" -print0)
|
||||||
|
|
||||||
|
echo "✅ Injection terminée."
|
||||||
21
backend/sql/seed.sql
Normal file
21
backend/sql/seed.sql
Normal file
@@ -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;
|
||||||
@@ -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({
|
const pool = new Pool({
|
||||||
host: process.env.PGHOST || 'db',
|
host: process.env.PGHOST || 'localhost',
|
||||||
port: Number(process.env.PGPORT || 5432),
|
|
||||||
user: process.env.PGUSER || 'postgres',
|
user: process.env.PGUSER || 'postgres',
|
||||||
password: process.env.PGPASSWORD || 'postgres',
|
password: process.env.PGPASSWORD || 'postgres',
|
||||||
database: process.env.PGDATABASE || 'supersunday',
|
database: process.env.PGDATABASE || 'supersunday',
|
||||||
|
port: Number(process.env.PGPORT || 5432),
|
||||||
});
|
});
|
||||||
export default pool;
|
|
||||||
|
module.exports = { pool };
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import 'dotenv/config';
|
const express = require('express');
|
||||||
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 app = express();
|
const app = express();
|
||||||
app.use(helmet());
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
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);
|
// Routes
|
||||||
app.use('/api/tournaments', tournamentsRoutes);
|
app.use('/api/tournaments', require('./routes/tournaments'));
|
||||||
app.use('/api/matches', matchesRoutes);
|
app.use('/api/participants', require('./routes/participants'));
|
||||||
app.use('/api/tournaments', standingsRoutes);
|
|
||||||
|
|
||||||
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.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);
|
||||||
|
});
|
||||||
|
|||||||
40
backend/src/routes/participants.js
Normal file
40
backend/src/routes/participants.js
Normal file
@@ -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;
|
||||||
@@ -1,59 +1,68 @@
|
|||||||
import { Router } from 'express';
|
const express = require('express');
|
||||||
import pool from '../db.js';
|
const router = express.Router();
|
||||||
import { requireAuth } from '../middleware/auth.js';
|
const { pool } = require('../db');
|
||||||
|
|
||||||
const router = Router();
|
// GET /api/tournaments -> liste
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
// List
|
try {
|
||||||
router.get('/', async (req,res)=>{
|
const { rows } = await pool.query(
|
||||||
const { rows } = await pool.query('select * from tournaments order by start_date nulls last, id desc limit 200');
|
'SELECT id, name, location, start_date, end_date FROM tournaments ORDER BY id DESC'
|
||||||
|
);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('GET /tournaments', e);
|
||||||
|
res.status(500).json({ error: 'server_error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// One
|
// POST /api/tournaments -> créer
|
||||||
router.get('/:id', async (req,res)=>{
|
router.post('/', 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)=>{
|
|
||||||
const { name, location, start_date, end_date } = req.body || {};
|
const { name, location, start_date, end_date } = req.body || {};
|
||||||
if (!name) return res.status(400).json({ error:'name_required' });
|
if (!name || !start_date || !end_date) {
|
||||||
|
return res.status(400).json({ error: 'missing_fields' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
'insert into tournaments(name,location,start_date,end_date) values ($1,$2,$3,$4) returning *',
|
`INSERT INTO tournaments (name, location, start_date, end_date)
|
||||||
[name, location||null, start_date||null, end_date||null]
|
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]);
|
res.status(201).json(rows[0]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('POST /tournaments', e);
|
||||||
|
res.status(500).json({ error: 'server_error' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add participant (admin)
|
// DELETE /api/tournaments/:id -> supprimer (avec nettoyage dépendances si pas de CASCADE)
|
||||||
router.post('/:id/participants', requireAuth, async (req,res)=>{
|
router.delete('/:id', async (req, res) => {
|
||||||
const { full_name } = req.body || {};
|
const id = Number(req.params.id);
|
||||||
const tid = Number(req.params.id);
|
if (!id) return res.status(400).json({ error: 'bad_id' });
|
||||||
if (!tid) return res.status(400).json({ error:'bad_tournament_id' });
|
|
||||||
if (!full_name) return res.status(400).json({ error:'full_name_required' });
|
const client = await pool.connect();
|
||||||
// ensure tournament exists
|
try {
|
||||||
const t = await pool.query('select id from tournaments where id=$1', [tid]);
|
await client.query('BEGIN');
|
||||||
if (!t.rows[0]) return res.status(404).json({ error:'tournament_not_found' });
|
|
||||||
const { rows } = await pool.query(
|
// Si votre schéma n'a PAS de ON DELETE CASCADE, on nettoie à la main :
|
||||||
'insert into participants(tournament_id, full_name) values ($1,$2) returning *',
|
// matches → participants → (tournament row)
|
||||||
[tid, full_name]
|
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 {}
|
||||||
res.status(201).json(rows[0]);
|
|
||||||
|
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;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 926 KiB |
117
frontend/public/admin/admin.js
Normal file
117
frontend/public/admin/admin.js
Normal file
@@ -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 = ['<option value="">— choisir —</option>']
|
||||||
|
.concat(data.map(t=>`<option value="${t.id}">#${t.id} — ${t.name}</option>`));
|
||||||
|
$('#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();
|
||||||
|
});
|
||||||
@@ -4,116 +4,112 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Super Sunday — Admin</title>
|
<title>Super Sunday — Admin</title>
|
||||||
<link rel="stylesheet" href="/assets/style.css?v=10" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/patch-bubbles.css" />
|
||||||
<style>
|
<style>
|
||||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
.wrap{max-width:1100px;margin:0 auto}
|
||||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}
|
||||||
.actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
.col-6{grid-column:span 6}
|
||||||
.muted-sm{color:#9fb0c7;font-size:13px}
|
.col-12{grid-column:span 12}
|
||||||
.toast{position:fixed;right:16px;bottom:16px;padding:12px 16px;
|
.card{background:rgba(15,27,51,.7);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:16px}
|
||||||
background:#0f1b33;border:1px solid rgba(255,255,255,.2);color:#eaf3ff;
|
.card h2{font-size:20px;margin:0 0 10px}
|
||||||
border-radius:14px;box-shadow:0 8px 24px rgba(0,0,0,.35);display:none;min-width:260px}
|
.row{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0}
|
||||||
.toast.show{display:block;animation:toastIn .18s ease-out}
|
.row > *{flex:1 1 180px}
|
||||||
@keyframes toastIn{from{transform:translateY(6px);opacity:.0}to{transform:translateY(0);opacity:1}}
|
.muted{color:#a9bfd6}
|
||||||
.spinner{display:inline-block;width:16px;height:16px;border-radius:50%;
|
.btn{padding:10px 14px;border-radius:12px;border:1px solid rgba(255,255,255,.1);background:#1a2442;color:#fff;cursor:pointer}
|
||||||
border:2px solid rgba(255,255,255,.25);border-top-color:#8fedff;animation:spin .6s linear infinite;margin-left:8px}
|
.btn:hover{filter:brightness(1.1)}
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
input,select{background:#0f1b33;border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:10px 12px;color:#fff}
|
||||||
.help{font-size:12px;color:#cfe1ff;margin-top:4px}
|
pre{white-space:pre-wrap;background:rgba(0,0,0,.3);padding:10px;border-radius:12px;border:1px solid rgba(255,255,255,.08);max-height:320px;overflow:auto}
|
||||||
.sel{display:flex;gap:8px}
|
.status{min-height:24px}
|
||||||
.sel select{min-width:160px}
|
.danger{background:#3a1520;border-color:#ce4f76}
|
||||||
.hide{display:none !important}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>Admin — Super Sunday</h1>
|
<h1>Admin</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Accueil</a>
|
<a href="/">Accueil</a>
|
||||||
<a href="/events">Événements</a>
|
<a href="/events">Événements</a>
|
||||||
<a href="/admin" class="active">Admin</a>
|
<a class="active" href="/admin">Admin</a>
|
||||||
|
<a href="/scoreboard">Classement</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container wrap">
|
||||||
<!-- Login -->
|
<div class="grid">
|
||||||
<section id="loginSection" class="card">
|
<section class="card col-12">
|
||||||
<h2 class="section-title">Connexion</h2>
|
<div class="row">
|
||||||
<div class="form-row">
|
<button class="btn" id="btn-health">Tester /api/health</button>
|
||||||
<input id="email" placeholder="Email (ex: admin@supersunday.local)" />
|
<span id="health" class="status muted"></span>
|
||||||
<input id="password" type="password" placeholder="Mot de passe (ex: changeme)" />
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section id="adminSection" class="grid-2 hide">
|
<section class="card col-6">
|
||||||
<!-- Create tournament -->
|
<h2>Créer un tournoi</h2>
|
||||||
<div class="card">
|
<form id="t-create-form">
|
||||||
<h3 class="section-title">Créer un tournoi</h3>
|
<div class="row">
|
||||||
<input id="t_name" placeholder="Nom du tournoi" />
|
<input id="t-name" placeholder="Nom" required />
|
||||||
<input id="t_location" placeholder="Lieu" />
|
<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>
|
||||||
<div class="actions">
|
<div class="row">
|
||||||
<button id="createTournamentBtn" class="btn">Créer</button>
|
<label class="muted">Début<input id="t-start" type="date" required /></label>
|
||||||
<div id="createTournamentSpin" class="spinner hide"></div>
|
<label class="muted">Fin<input id="t-end" type="date" required /></label>
|
||||||
</div>
|
</div>
|
||||||
<div id="createTournamentErr" class="help"></div>
|
<div class="row">
|
||||||
</div>
|
<button class="btn" type="submit">Créer</button>
|
||||||
|
<span id="t-create-status" class="status muted"></span>
|
||||||
<!-- 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>
|
</div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
|
||||||
|
|
||||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
<section class="card col-6">
|
||||||
|
<h2>Tournois</h2>
|
||||||
|
<div class="row">
|
||||||
|
<select id="t-list"></select>
|
||||||
|
<button class="btn" id="btn-refresh-t">Rafraîchir</button>
|
||||||
|
<button class="btn danger" id="btn-del-t">Supprimer</button>
|
||||||
|
</div>
|
||||||
|
<pre id="tournaments-box" class="muted">—</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script type="module" src="/assets/api.js?v=10"></script>
|
<section class="card col-6">
|
||||||
<script type="module" src="/assets/admin.ux.js?v=10"></script>
|
<h2>Ajouter un joueur</h2>
|
||||||
|
<form id="p-create-form">
|
||||||
|
<div class="row">
|
||||||
|
<select id="p-tournament"></select>
|
||||||
|
<input id="p-name" placeholder="Nom complet" required />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="btn" type="submit">Ajouter</button>
|
||||||
|
<span id="p-create-status" class="status muted"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<pre id="participants-box" class="muted">—</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card col-6">
|
||||||
|
<h2>Saisir un score</h2>
|
||||||
|
<form id="m-score-form">
|
||||||
|
<div class="row">
|
||||||
|
<select id="m-tournament"></select>
|
||||||
|
<input id="m-id" type="number" placeholder="Match ID" required />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="m-score-a" type="number" placeholder="Jeux A" min="0" />
|
||||||
|
<input id="m-score-b" type="number" placeholder="Jeux B" min="0" />
|
||||||
|
<label class="muted"><input id="m-finished" type="checkbox" /> Terminé</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="btn" type="submit">Enregistrer</button>
|
||||||
|
<span id="m-score-status" class="status muted"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/assets/bubbles.js"></script>
|
||||||
|
<script type="module" src="/admin/admin.js"></script>
|
||||||
|
<script type="module" src="/assets/header.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
74
frontend/public/assets/bubbles.js
Normal file
74
frontend/public/assets/bubbles.js
Normal file
@@ -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<N;i++) BUBBLES.push(makeBubble());
|
||||||
|
|
||||||
|
function tick(){
|
||||||
|
ctx.clearRect(0,0,c.width,c.height);
|
||||||
|
for(const b of BUBBLES){
|
||||||
|
b.phase += 0.01 + (b.r/1200);
|
||||||
|
const sway = Math.sin(b.phase) * 0.3;
|
||||||
|
b.x += b.vx + sway;
|
||||||
|
b.y += b.vy;
|
||||||
|
if (b.x < -200 || b.x > 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);
|
||||||
|
})();
|
||||||
42
frontend/public/assets/header.js
Normal file
42
frontend/public/assets/header.js
Normal file
@@ -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 =>
|
||||||
|
`<a href="${l.href}" class="${isActive(l.href) ? 'active' : ''}">${l.label}</a>`
|
||||||
|
).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);
|
||||||
|
})();
|
||||||
58
frontend/public/assets/patch-bubbles.css
Normal file
58
frontend/public/assets/patch-bubbles.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Événements — Super Sunday</title>
|
<title>Événements — Super Sunday</title>
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/patch-bubbles.css?v=2" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -20,5 +21,7 @@
|
|||||||
</main>
|
</main>
|
||||||
<script src="/assets/api.js"></script>
|
<script src="/assets/api.js"></script>
|
||||||
<script src="/assets/events.js"></script>
|
<script src="/assets/events.js"></script>
|
||||||
|
<script type="module" src="/assets/bubbles.js?v=2"></script>
|
||||||
|
<script type="module" src="/assets/header.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Super Sunday — Accueil</title>
|
<title>Super Sunday — Accueil</title>
|
||||||
<link rel="stylesheet" href="/assets/style.css?v=8" />
|
<link rel="stylesheet" href="/assets/style.css?v=8" />
|
||||||
|
<link rel="stylesheet" href="/assets/patch-bubbles.css?v=2" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -32,5 +33,7 @@
|
|||||||
).join('') || '<div class="empty">Aucun tournoi</div>';
|
).join('') || '<div class="empty">Aucun tournoi</div>';
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="/assets/bubbles.js?v=2"></script>
|
||||||
|
<script type="module" src="/assets/header.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}
|
.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}
|
||||||
.controls select,.controls input{min-width:180px}
|
.controls select,.controls input{min-width:180px}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/assets/patch-bubbles.css?v=2" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -98,5 +99,7 @@ auto.addEventListener('change', ()=>{ if(!auto.checked && timer){ clearTimeout(t
|
|||||||
await loadTournaments();
|
await loadTournaments();
|
||||||
tick();
|
tick();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="/assets/bubbles.js?v=2"></script>
|
||||||
|
<script type="module" src="/assets/header.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Détail tournoi — Super Sunday</title>
|
<title>Détail tournoi — Super Sunday</title>
|
||||||
<link rel="stylesheet" href="/assets/style.css?v=8" />
|
<link rel="stylesheet" href="/assets/style.css?v=8" />
|
||||||
|
<link rel="stylesheet" href="/assets/patch-bubbles.css?v=2" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -37,5 +38,7 @@
|
|||||||
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>';
|
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>
|
</script>
|
||||||
|
<script type="module" src="/assets/bubbles.js?v=2"></script>
|
||||||
|
<script type="module" src="/assets/header.js?v=1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ http {
|
|||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
upstream api_upstream {
|
upstream api_upstream {
|
||||||
server api:4000;
|
# server api:4000;
|
||||||
|
server supersunday_api:4000;
|
||||||
keepalive 16;
|
keepalive 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,15 +29,22 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
|
proxy_pass http://api_upstream;
|
||||||
|
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Connection "";
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
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-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_connect_timeout 5s;
|
client_max_body_size 10m;
|
||||||
proxy_pass http://api_upstream;
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /nginx_health {
|
location = /nginx_health {
|
||||||
|
|||||||
11
seed_db.sh
Executable file
11
seed_db.sh
Executable file
@@ -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é."
|
||||||
Reference in New Issue
Block a user