Mise à jour : dernières modifs
This commit is contained in:
21
.env.
Normal file
21
.env.
Normal file
@@ -0,0 +1,21 @@
|
||||
# === Super Sunday PROD Environment ===
|
||||
NODE_ENV=production
|
||||
PORT=8080
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=supersunday
|
||||
POSTGRES_USER=supersunday
|
||||
POSTGRES_PASSWORD=Sup3rSund@y2025!
|
||||
DATABASE_URL=postgres://supersunday:Sup3rSund@y2025!@db:5432/supersunday
|
||||
|
||||
# API / Auth
|
||||
JWT_SECRET=Sup3rSundayUltraSecretKey_2025
|
||||
CORS_ORIGIN=http://localhost
|
||||
|
||||
# Admin credentials
|
||||
ADMIN_EMAIL=admin@padel24play.com
|
||||
ADMIN_PASSWORD=4575SataMGF+-
|
||||
|
||||
# Branding
|
||||
APP_NAME="Super Sunday Padel Championship"
|
||||
CLUB_NAME="Les Églantiers, Woluwe-Saint-Pierre"
|
||||
21
.env.prod
Normal file
21
.env.prod
Normal file
@@ -0,0 +1,21 @@
|
||||
# === Super Sunday PROD Environment ===
|
||||
NODE_ENV=production
|
||||
PORT=8080
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=supersunday
|
||||
POSTGRES_USER=supersunday
|
||||
POSTGRES_PASSWORD=Sup3rSund@y2025!
|
||||
DATABASE_URL=postgres://supersunday:Sup3rSund@y2025!@db:5432/supersunday
|
||||
|
||||
# API / Auth
|
||||
JWT_SECRET=Sup3rSundayUltraSecretKey_2025
|
||||
CORS_ORIGIN=http://localhost
|
||||
|
||||
# Admin credentials
|
||||
ADMIN_EMAIL=admin@supersunday.app
|
||||
ADMIN_PASSWORD=ChangeMeNow!42
|
||||
|
||||
# Branding
|
||||
APP_NAME="Super Sunday Padel Championship"
|
||||
CLUB_NAME="Les Églantiers, Woluwe-Saint-Pierre"
|
||||
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
*.out
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# OS / IDE
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environnements
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Coverage / tests
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Build / dist
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# DB / data temporaire
|
||||
data/*.sqlite
|
||||
data/*.sqlite-journal
|
||||
|
||||
# Misc
|
||||
*.zip
|
||||
*.tar
|
||||
44
README.md
44
README.md
@@ -1,14 +1,38 @@
|
||||
# SuperSunday — V1 (prod, port 8080)
|
||||
# Super Sunday Padel — Full PROD bundle
|
||||
|
||||
Stack: **Postgres 16 + Node/Express API + Nginx static frontend**
|
||||
Features: Players, Teams, Tournaments, Enrollments, Matches, Results, **Americano generator**, JWT Auth, basic admin SPA.
|
||||
|
||||
## Quick start
|
||||
|
||||
## Démarrage
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
# http://localhost:8080
|
||||
cp .env.example .env
|
||||
docker compose up -d --build
|
||||
# Open http://localhost:8080
|
||||
# Admin at http://localhost:8080/admin (login with ADMIN_EMAIL / ADMIN_PASSWORD from .env)
|
||||
```
|
||||
|
||||
## Fonctionnalités
|
||||
- Liste d'événements
|
||||
- Inscription + téléchargement iCal
|
||||
- Admin: consultation des inscriptions
|
||||
- Seeds réalistes (8 éditions winter + live démo + futur)
|
||||
## Push to Gitea (example)
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Super Sunday full prod v1"
|
||||
git remote add origin <URL_DE_TON_GITEA>
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## API (highlights)
|
||||
- `POST /api/auth/login` -> { token }
|
||||
- `GET /api/health`
|
||||
- Players: `GET/POST/PUT/DELETE /api/players`
|
||||
- Teams: `GET/POST/PUT/DELETE /api/teams`
|
||||
- Tournaments: `GET/POST/PUT/DELETE /api/tournaments`
|
||||
- Enrollments: `GET/POST/DELETE /api/enrollments`
|
||||
- Matches: `GET/POST/PUT /api/matches`, `POST /api/matches/generate` (Americano)
|
||||
- Scores: `POST /api/matches/:id/score`
|
||||
|
||||
Protected routes require `Authorization: Bearer <token>`.
|
||||
|
||||
## Notes
|
||||
- Change `JWT_SECRET` and admin credentials before going live.
|
||||
- SQL seed creates minimal schema + demo tournament.
|
||||
12
README_PATCH.txt
Normal file
12
README_PATCH.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Patch: Typography + titles facelift (hero titles, section titles, kicker labels).
|
||||
Files:
|
||||
- frontend/public/assets/style.titles.patch.css
|
||||
|
||||
Usage:
|
||||
1. Copy style.titles.patch.css into frontend/public/assets/
|
||||
2. In your HTML <head>, include:
|
||||
<link rel="stylesheet" href="/assets/style.titles.patch.css" />
|
||||
3. Apply classes .hero-title, .section-title, .kicker, .btn-outline in your HTML.
|
||||
|
||||
Rebuild web:
|
||||
docker compose build web && docker compose up -d
|
||||
25
apply_patch.sh
Executable file
25
apply_patch.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Projet courant
|
||||
PROJECT_DIR="$(pwd)"
|
||||
|
||||
# On cherche le dernier patch .zip téléchargé
|
||||
PATCH=$(ls -t ~/Downloads/supersunday_patch_*.zip | head -n 1)
|
||||
|
||||
if [ ! -f "$PATCH" ]; then
|
||||
echo "❌ Aucun patch trouvé dans ~/Downloads"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Application du patch: $PATCH ==="
|
||||
unzip -o "$PATCH" -d "$PROJECT_DIR"
|
||||
|
||||
echo "=== Rebuild docker ==="
|
||||
docker compose up -d --build
|
||||
|
||||
# Suppression du patch après application
|
||||
rm -f "$PATCH"
|
||||
echo "🗑️ Patch $PATCH supprimé"
|
||||
|
||||
echo "✅ Patch appliqué avec succès !"
|
||||
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* .npmrc* ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
EXPOSE 8080
|
||||
CMD ["node", "server.js"]
|
||||
1200
backend/package-lock.json
generated
Normal file
1200
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
backend/package.json
Normal file
18
backend/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "supersunday-api",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.11.5"
|
||||
}
|
||||
}
|
||||
277
backend/server.js
Normal file
277
backend/server.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import pkg from 'pg';
|
||||
|
||||
dotenv.config();
|
||||
const { Pool } = pkg;
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const DATABASE_URL = process.env.DATABASE_URL || 'postgres://supersunday:supersunday@db:5432/supersunday';
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret';
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@supersunday.local';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
|
||||
const app = express();
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.CORS_ORIGIN?.split(',') || '*' }));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
const pool = new Pool({ connectionString: DATABASE_URL });
|
||||
async function q(text, params=[]) { const { rows } = await pool.query(text, params); return rows; }
|
||||
|
||||
function auth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
if (!token) return res.status(401).json({ error: 'missing token' });
|
||||
try { req.user = jwt.verify(token, JWT_SECRET); next(); }
|
||||
catch { res.status(401).json({ error: 'invalid token' }); }
|
||||
}
|
||||
|
||||
/* Health + Auth */
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try { const { rows } = await pool.query('SELECT 1 as ok'); res.json({ status: 'ok', db: rows[0].ok === 1 }); }
|
||||
catch (e) { res.status(500).json({ status: 'error', error: String(e) }); }
|
||||
});
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (email === ADMIN_EMAIL && password === ADMIN_PASSWORD) {
|
||||
const token = jwt.sign({ email, role: 'admin' }, JWT_SECRET, { expiresIn: '12h' });
|
||||
return res.json({ token });
|
||||
}
|
||||
res.status(401).json({ error: 'invalid credentials' });
|
||||
});
|
||||
|
||||
/* Players */
|
||||
app.get('/api/players', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM players ORDER BY id DESC'));
|
||||
});
|
||||
app.get('/api/players/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM players WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/players', auth, async (req, res) => {
|
||||
const { first_name, last_name, email=null, ranking=0 } = req.body||{};
|
||||
if (!first_name || !last_name) return res.status(400).json({ error: 'first_name & last_name required' });
|
||||
const rows = await q('INSERT INTO players(first_name,last_name,email,ranking) VALUES ($1,$2,$3,$4) RETURNING *',
|
||||
[first_name, last_name, email, ranking]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/players/:id', auth, async (req, res) => {
|
||||
const { first_name, last_name, email, ranking } = req.body||{};
|
||||
const rows = await q('UPDATE players SET first_name=$1,last_name=$2,email=$3,ranking=$4 WHERE id=$5 RETURNING *',
|
||||
[first_name, last_name, email, ranking, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/players/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM players WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Tournaments */
|
||||
app.get('/api/tournaments', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM tournaments ORDER BY start_date DESC, id DESC'));
|
||||
});
|
||||
app.get('/api/tournaments/upcoming', async (req, res) => {
|
||||
res.json(await q('SELECT * FROM tournaments WHERE start_date >= CURRENT_DATE ORDER BY start_date ASC LIMIT 10'));
|
||||
});
|
||||
app.get('/api/tournaments/:id', async (req, res) => {
|
||||
const id = +req.params.id;
|
||||
const [t] = await q('SELECT * FROM tournaments WHERE id=$1', [id]);
|
||||
if (!t) return res.status(404).json({ error: 'tournament not found' });
|
||||
const [{ enrollment_count }] = await q('SELECT COUNT(*)::int AS enrollment_count FROM enrollments WHERE tournament_id=$1', [id]);
|
||||
const [{ team_count }] = await q('SELECT COUNT(*)::int AS team_count FROM teams WHERE tournament_id=$1', [id]);
|
||||
const [{ match_count }] = await q('SELECT COUNT(*)::int AS match_count FROM matches WHERE tournament_id=$1', [id]);
|
||||
const nextRows = await q(`SELECT * FROM matches WHERE tournament_id=$1 AND status IN ('scheduled','playing') AND scheduled_at IS NOT NULL ORDER BY scheduled_at ASC LIMIT 1`, [id]);
|
||||
res.json({ ...t, enrollment_count, team_count, match_count, next_match: nextRows[0] || null });
|
||||
});
|
||||
app.get('/api/tournaments/:id/summary', async (req, res) => {
|
||||
const id = +req.params.id;
|
||||
const [t] = await q('SELECT * FROM tournaments WHERE id=$1', [id]);
|
||||
if (!t) return res.status(404).json({ error: 'tournament not found' });
|
||||
const enroll = await q(`
|
||||
SELECT p.id, p.first_name, p.last_name, p.ranking
|
||||
FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC NULLS LAST, p.last_name ASC
|
||||
`, [id]);
|
||||
const matches = await q('SELECT * FROM matches WHERE tournament_id=$1 ORDER BY scheduled_at NULLS LAST, id ASC', [id]);
|
||||
res.json({ tournament: t, enrollments: enroll, matches });
|
||||
});
|
||||
app.post('/api/tournaments', auth, async (req, res) => {
|
||||
const { name, location=null, start_date, end_date } = req.body||{};
|
||||
if (!name || !start_date || !end_date) return res.status(400).json({ error: 'name, start_date, end_date required' });
|
||||
const rows = await q('INSERT INTO tournaments(name,location,start_date,end_date) VALUES ($1,$2,$3,$4) RETURNING *',
|
||||
[name, location, start_date, end_date]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/tournaments/:id', auth, async (req, res) => {
|
||||
const { name, location, start_date, end_date } = req.body||{};
|
||||
const rows = await q('UPDATE tournaments SET name=$1,location=$2,start_date=$3,end_date=$4 WHERE id=$5 RETURNING *',
|
||||
[name, location, start_date, end_date, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/tournaments/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM tournaments WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Enrollments */
|
||||
app.get('/api/enrollments', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) {
|
||||
return res.json(await q(`
|
||||
SELECT e.player_id, p.first_name, p.last_name, p.ranking
|
||||
FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC, p.last_name ASC
|
||||
`, [tid]));
|
||||
}
|
||||
res.json(await q('SELECT * FROM enrollments ORDER BY tournament_id, player_id'));
|
||||
});
|
||||
app.post('/api/enrollments', auth, async (req, res) => {
|
||||
const { player_id, tournament_id } = req.body||{};
|
||||
if (!player_id || !tournament_id) return res.status(400).json({ error: 'player_id & tournament_id required' });
|
||||
await q('INSERT INTO enrollments(player_id,tournament_id) VALUES($1,$2) ON CONFLICT DO NOTHING', [player_id, tournament_id]);
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
app.post('/api/enrollments/bulk', auth, async (req, res) => {
|
||||
const { player_ids = [], tournament_id } = req.body||{};
|
||||
if (!tournament_id || !Array.isArray(player_ids)) return res.status(400).json({ error: 'player_ids[] & tournament_id required' });
|
||||
for (const pid of player_ids) { await q('INSERT INTO enrollments(player_id,tournament_id) VALUES($1,$2) ON CONFLICT DO NOTHING', [pid, tournament_id]); }
|
||||
res.json({ ok: true, count: player_ids.length });
|
||||
});
|
||||
app.delete('/api/enrollments', auth, async (req, res) => {
|
||||
const { player_id, tournament_id } = req.body||{};
|
||||
await q('DELETE FROM enrollments WHERE player_id=$1 AND tournament_id=$2', [player_id, tournament_id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Teams */
|
||||
app.get('/api/teams', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) {
|
||||
return res.json(await q(`
|
||||
SELECT t.*,
|
||||
p1.first_name AS p1_first, p1.last_name AS p1_last,
|
||||
p2.first_name AS p2_first, p2.last_name AS p2_last
|
||||
FROM teams t
|
||||
JOIN players p1 ON p1.id=t.player1_id
|
||||
JOIN players p2 ON p2.id=t.player2_id
|
||||
WHERE t.tournament_id=$1 ORDER BY t.id DESC
|
||||
`, [tid]));
|
||||
}
|
||||
res.json(await q('SELECT * FROM teams ORDER BY id DESC'));
|
||||
});
|
||||
app.get('/api/teams/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM teams WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/teams', auth, async (req, res) => {
|
||||
const { player1_id, player2_id, tournament_id=null } = req.body||{};
|
||||
if (!player1_id || !player2_id) return res.status(400).json({ error: 'player1_id & player2_id required' });
|
||||
const rows = await q('INSERT INTO teams(player1_id,player2_id,tournament_id) VALUES ($1,$2,$3) RETURNING *',
|
||||
[Math.min(player1_id, player2_id), Math.max(player1_id, player2_id), tournament_id]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.post('/api/teams/auto', auth, async (req, res) => {
|
||||
const { tournament_id } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const players = await q(`
|
||||
SELECT p.* FROM enrollments e JOIN players p ON p.id=e.player_id
|
||||
WHERE e.tournament_id=$1 ORDER BY p.ranking ASC NULLS LAST
|
||||
`, [tournament_id]);
|
||||
const created = [];
|
||||
for (let i=0;i<players.length;i+=2) {
|
||||
if (i+1 >= players.length) break;
|
||||
const a = players[i].id, b = players[i+1].id;
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO teams(player1_id,player2_id,tournament_id) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING RETURNING *',
|
||||
[Math.min(a,b), Math.max(a,b), tournament_id]
|
||||
);
|
||||
if (rows[0]) created.push(rows[0]);
|
||||
}
|
||||
res.json({ created_count: created.length, created });
|
||||
});
|
||||
app.put('/api/teams/:id', auth, async (req, res) => {
|
||||
const { player1_id, player2_id, tournament_id } = req.body||{};
|
||||
const rows = await q('UPDATE teams SET player1_id=$1,player2_id=$2,tournament_id=$3 WHERE id=$4 RETURNING *',
|
||||
[Math.min(player1_id, player2_id), Math.max(player1_id, player2_id), tournament_id, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.delete('/api/teams/:id', auth, async (req, res) => {
|
||||
await q('DELETE FROM teams WHERE id=$1', [+req.params.id]); res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Matches */
|
||||
app.get('/api/matches', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (tid) return res.json(await q('SELECT * FROM matches WHERE tournament_id=$1 ORDER BY scheduled_at NULLS LAST, id DESC', [tid]));
|
||||
res.json(await q('SELECT * FROM matches ORDER BY scheduled_at NULLS LAST, id DESC'));
|
||||
});
|
||||
app.get('/api/matches/:id', async (req, res) => {
|
||||
const rows = await q('SELECT * FROM matches WHERE id=$1', [+req.params.id]);
|
||||
if (!rows[0]) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches', auth, async (req, res) => {
|
||||
const { tournament_id, court=null, round=null, scheduled_at=null, team_a_id=null, team_b_id=null } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const rows = await q('INSERT INTO matches(tournament_id,court,round,scheduled_at,team_a_id,team_b_id) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *',
|
||||
[tournament_id, court, round, scheduled_at, team_a_id, team_b_id]);
|
||||
res.status(201).json(rows[0]);
|
||||
});
|
||||
app.put('/api/matches/:id', auth, async (req, res) => {
|
||||
const { court, round, scheduled_at, team_a_id, team_b_id, status } = req.body||{};
|
||||
const rows = await q('UPDATE matches SET court=$1, round=$2, scheduled_at=$3, team_a_id=$4, team_b_id=$5, status=$6 WHERE id=$7 RETURNING *',
|
||||
[court, round, scheduled_at, team_a_id, team_b_id, status, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches/:id/score', auth, async (req, res) => {
|
||||
const { score_a, score_b, status='finished' } = req.body||{};
|
||||
const rows = await q('UPDATE matches SET score_a=$1, score_b=$2, status=$3 WHERE id=$4 RETURNING *',
|
||||
[score_a, score_b, status, +req.params.id]);
|
||||
res.json(rows[0]);
|
||||
});
|
||||
app.post('/api/matches/generate', auth, async (req, res) => {
|
||||
const { tournament_id, courts=['Court 1'], start_at=null, interval_minutes=20 } = req.body||{};
|
||||
if (!tournament_id) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const teams = await q('SELECT id FROM teams WHERE tournament_id=$1 ORDER BY id ASC', [tournament_id]);
|
||||
if (teams.length < 2) return res.status(400).json({ error: 'not enough teams' });
|
||||
const now = start_at ? new Date(start_at) : new Date(Date.now() + 5*60*1000);
|
||||
let t = new Date(now); let courtIdx = 0; const created = [];
|
||||
for (let i=0;i<teams.length;i+=2) {
|
||||
if (i+1 >= teams.length) break;
|
||||
const a = teams[i].id, b = teams[i+1].id;
|
||||
const court = courts[courtIdx % courts.length]; courtIdx++;
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO matches(tournament_id, court, round, scheduled_at, team_a_id, team_b_id, status) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *',
|
||||
[tournament_id, court, 'Americano', t.toISOString(), a, b, 'scheduled']
|
||||
);
|
||||
created.push(rows[0]); t = new Date(t.getTime() + interval_minutes*60*1000);
|
||||
}
|
||||
res.json({ created });
|
||||
});
|
||||
|
||||
/* Leaderboard (simple): sums wins/losses by team in a tournament */
|
||||
app.get('/api/leaderboard', async (req, res) => {
|
||||
const tid = req.query.tournament_id ? +req.query.tournament_id : null;
|
||||
if (!tid) return res.status(400).json({ error: 'tournament_id required' });
|
||||
const rows = await q(`
|
||||
SELECT team_id, SUM(w) AS wins, SUM(l) AS losses
|
||||
FROM (
|
||||
SELECT team_a_id AS team_id, CASE WHEN score_a>score_b THEN 1 ELSE 0 END AS w, CASE WHEN score_a<score_b THEN 1 ELSE 0 END AS l
|
||||
FROM matches WHERE tournament_id=$1 AND status='finished' AND team_a_id IS NOT NULL AND team_b_id IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT team_b_id AS team_id, CASE WHEN score_b>score_a THEN 1 ELSE 0 END AS w, CASE WHEN score_b<score_a THEN 1 ELSE 0 END AS l
|
||||
FROM matches WHERE tournament_id=$1 AND status='finished' AND team_a_id IS NOT NULL AND team_b_id IS NOT NULL
|
||||
) x
|
||||
GROUP BY team_id
|
||||
ORDER BY wins DESC, losses ASC
|
||||
`, [tid]);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`API listening on :${PORT}`));
|
||||
BIN
bakcgrounp24p.jpg
Normal file
BIN
bakcgrounp24p.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 926 KiB |
190
data/db.json
190
data/db.json
@@ -1,190 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"siteName": "Padel24Play — V1 (8080)",
|
||||
"currency": "EUR",
|
||||
"priceEur": 48,
|
||||
"refundWindowHours": 48
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"email": "admin@supersunday.com",
|
||||
"password": "password123",
|
||||
"role": "admin",
|
||||
"name": "Admin"
|
||||
},
|
||||
{
|
||||
"email": "player@supersunday.com",
|
||||
"password": "password123",
|
||||
"role": "user",
|
||||
"name": "Player"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"id": "ss-winter-1",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #1",
|
||||
"date": "2025-10-05T08:00:00",
|
||||
"end": "2025-10-05T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-2",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #2",
|
||||
"date": "2025-10-19T08:00:00",
|
||||
"end": "2025-10-19T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-3",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #3",
|
||||
"date": "2025-11-02T08:00:00",
|
||||
"end": "2025-11-02T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-4",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #4",
|
||||
"date": "2025-11-16T08:00:00",
|
||||
"end": "2025-11-16T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-5",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #5",
|
||||
"date": "2025-11-30T08:00:00",
|
||||
"end": "2025-11-30T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-6",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #6",
|
||||
"date": "2025-12-14T08:00:00",
|
||||
"end": "2025-12-14T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-7",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #7",
|
||||
"date": "2025-12-28T08:00:00",
|
||||
"end": "2025-12-28T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-winter-8",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Winter Edition #8",
|
||||
"date": "2026-01-11T08:00:00",
|
||||
"end": "2026-01-11T11:00:00",
|
||||
"location": "Sport City Woluwe"
|
||||
},
|
||||
{
|
||||
"id": "ss-live-now",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — LIVE (démo)",
|
||||
"date": "2025-08-18T18:33:23",
|
||||
"end": "2025-08-18T21:33:23",
|
||||
"location": "TC Églantiers"
|
||||
},
|
||||
{
|
||||
"id": "ss-futur-1",
|
||||
"kind": "Americano",
|
||||
"name": "SuperSunday — Inscription ouverte",
|
||||
"date": "2025-08-28T19:03:23",
|
||||
"end": "2025-08-28T22:03:23",
|
||||
"location": "Sport City Woluwe"
|
||||
}
|
||||
],
|
||||
"registrations": [
|
||||
{
|
||||
"id": "reg_seed_1",
|
||||
"createdAt": "2025-08-18T19:03:23.597Z",
|
||||
"eventId": "ss-live-now",
|
||||
"player": {
|
||||
"name": "SmashQueen",
|
||||
"email": "smashqueen@demo.local",
|
||||
"phone": "+32471000000",
|
||||
"level": "Avancé"
|
||||
},
|
||||
"payment": "card",
|
||||
"status": "confirmé",
|
||||
"paymentStatus": "paid"
|
||||
},
|
||||
{
|
||||
"id": "reg_seed_2",
|
||||
"createdAt": "2025-08-18T19:03:23.600Z",
|
||||
"eventId": "ss-winter-1",
|
||||
"player": {
|
||||
"name": "PadelKing42",
|
||||
"email": "padelking42@demo.local",
|
||||
"phone": "+32471000001",
|
||||
"level": "Intermédiaire"
|
||||
},
|
||||
"payment": "paypal",
|
||||
"status": "en_attente",
|
||||
"paymentStatus": "pending"
|
||||
},
|
||||
{
|
||||
"id": "reg_seed_3",
|
||||
"createdAt": "2025-08-18T19:03:23.600Z",
|
||||
"eventId": "ss-live-now",
|
||||
"player": {
|
||||
"name": "MrVibora",
|
||||
"email": "mrvibora@demo.local",
|
||||
"phone": "+32471000002",
|
||||
"level": "Débutant"
|
||||
},
|
||||
"payment": "onspot",
|
||||
"status": "confirmé",
|
||||
"paymentStatus": "paid"
|
||||
},
|
||||
{
|
||||
"id": "reg_seed_4",
|
||||
"createdAt": "2025-08-18T19:03:23.600Z",
|
||||
"eventId": "ss-winter-1",
|
||||
"player": {
|
||||
"name": "LaBandeja",
|
||||
"email": "labandeja@demo.local",
|
||||
"phone": "+32471000003",
|
||||
"level": "Avancé"
|
||||
},
|
||||
"payment": "card",
|
||||
"status": "en_attente",
|
||||
"paymentStatus": "pending"
|
||||
},
|
||||
{
|
||||
"id": "reg_seed_5",
|
||||
"createdAt": "2025-08-18T19:03:23.600Z",
|
||||
"eventId": "ss-live-now",
|
||||
"player": {
|
||||
"name": "MissChiquita",
|
||||
"email": "misschiquita@demo.local",
|
||||
"phone": "+32471000004",
|
||||
"level": "Intermédiaire"
|
||||
},
|
||||
"payment": "paypal",
|
||||
"status": "confirmé",
|
||||
"paymentStatus": "paid"
|
||||
},
|
||||
{
|
||||
"id": "reg_seed_6",
|
||||
"createdAt": "2025-08-18T19:03:23.600Z",
|
||||
"eventId": "ss-winter-1",
|
||||
"player": {
|
||||
"name": "VoléeMagique",
|
||||
"email": "volemagique@demo.local",
|
||||
"phone": "+32471000005",
|
||||
"level": "Débutant"
|
||||
},
|
||||
"payment": "onspot",
|
||||
"status": "en_attente",
|
||||
"paymentStatus": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
68
db/init/001_schema_and_seed.sql
Normal file
68
db/init/001_schema_and_seed.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id SERIAL PRIMARY KEY,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
ranking INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tournaments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
location TEXT,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS enrollments (
|
||||
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
|
||||
tournament_id INTEGER NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (player_id, tournament_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player1_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
|
||||
player2_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
|
||||
tournament_id INTEGER REFERENCES tournaments(id) ON DELETE CASCADE,
|
||||
UNIQUE(player1_id, player2_id, tournament_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS matches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tournament_id INTEGER NOT NULL REFERENCES tournaments(id) ON DELETE CASCADE,
|
||||
court TEXT,
|
||||
round TEXT,
|
||||
scheduled_at TIMESTAMP WITH TIME ZONE,
|
||||
team_a_id INTEGER REFERENCES teams(id) ON DELETE SET NULL,
|
||||
team_b_id INTEGER REFERENCES teams(id) ON DELETE SET NULL,
|
||||
score_a INTEGER DEFAULT 0,
|
||||
score_b INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'scheduled' -- scheduled|playing|finished
|
||||
);
|
||||
|
||||
-- Demo data
|
||||
INSERT INTO players (first_name, last_name, email, ranking)
|
||||
VALUES
|
||||
('Flore', 'Van den Broeck', 'flore@example.com', 200),
|
||||
('Karim', 'Hassan', 'karim@example.com', 210),
|
||||
('Félicie', 'Hassan', 'felicie@example.com', 350),
|
||||
('Balthazar', 'Hassan', 'balthazar@example.com', 400)
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
|
||||
INSERT INTO tournaments (name, location, start_date, end_date)
|
||||
SELECT 'Super Sunday - Demo', 'Les Églantiers, WSP', CURRENT_DATE, CURRENT_DATE
|
||||
WHERE NOT EXISTS (SELECT 1 FROM tournaments WHERE name='Super Sunday - Demo');
|
||||
|
||||
-- auto-enroll demo players
|
||||
DO $$
|
||||
DECLARE t_id INT; p INT;
|
||||
BEGIN
|
||||
SELECT id INTO t_id FROM tournaments WHERE name='Super Sunday - Demo';
|
||||
FOR p IN SELECT id FROM players LOOP
|
||||
INSERT INTO enrollments(player_id, tournament_id) VALUES (p, t_id)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-supersunday}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-supersunday}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-supersunday}
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
- ./db/init:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-supersunday} -d ${POSTGRES_DB:-supersunday}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
api:
|
||||
build: ./backend
|
||||
env_file: .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/api/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
web:
|
||||
build: ./frontend
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
3
frontend/Dockerfile
Normal file
3
frontend/Dockerfile
Normal 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
67
frontend/index.html
Normal 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>
|
||||
21
frontend/nginx/default.conf
Normal file
21
frontend/nginx/default.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
74
frontend/public/admin/index.html
Normal file
74
frontend/public/admin/index.html
Normal 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>
|
||||
74
frontend/public/assets/admin.js
Normal file
74
frontend/public/assets/admin.js
Normal 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();
|
||||
}
|
||||
12
frontend/public/assets/api.js
Normal file
12
frontend/public/assets/api.js
Normal 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;
|
||||
11
frontend/public/assets/events.js
Normal file
11
frontend/public/assets/events.js
Normal 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>';
|
||||
})();
|
||||
BIN
frontend/public/assets/image/backgroundp24p.jpg
Normal file
BIN
frontend/public/assets/image/backgroundp24p.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 926 KiB |
23
frontend/public/assets/main.js
Normal file
23
frontend/public/assets/main.js
Normal 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>';
|
||||
})();
|
||||
141
frontend/public/assets/style.css
Normal file
141
frontend/public/assets/style.css
Normal 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) }
|
||||
55
frontend/public/assets/style.titles.patch.css
Normal file
55
frontend/public/assets/style.titles.patch.css
Normal 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 }
|
||||
39
frontend/public/assets/tournament.js
Normal file
39
frontend/public/assets/tournament.js
Normal 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();
|
||||
})();
|
||||
24
frontend/public/events/index.html
Normal file
24
frontend/public/events/index.html
Normal 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>
|
||||
40
frontend/public/index.html
Normal file
40
frontend/public/index.html
Normal 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>
|
||||
41
frontend/public/tournament/index.html
Normal file
41
frontend/public/tournament/index.html
Normal 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>
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "supersunday_prod_1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<nav class="nav"><div class="inner">
|
||||
<img src="/assets/img/logo.svg" width="22" height="22" alt="logo"/>
|
||||
<div class="brand"><span class="badge">P24P</span><span class="title">Padel24Play</span></div>
|
||||
<a href="/">Accueil</a><a href="/events.html">Événements</a><a href="/admin.html">Admin</a>
|
||||
<div class="spacer"></div><a href="/reglement.html">Règlement</a></div></nav>
|
||||
|
||||
<main class="container">
|
||||
<h1>🛠️ Admin — Inscriptions</h1>
|
||||
<div class="card">
|
||||
<div class="row"><input id="ev" placeholder="eventId (ex: ss-futur-1)" value="ss-futur-1"/><button class="btn" id="loadRegs">Charger</button></div>
|
||||
<table id="regs"><thead><tr><th>Joueur</th><th>Email</th><th>Paiement</th><th>Statut</th></tr></thead><tbody></tbody></table>
|
||||
<p class="notice">Version prod de base — paiements temps réel & effets à venir.</p>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<script>
|
||||
document.getElementById('loadRegs').onclick = async ()=>{
|
||||
const evId = document.getElementById('ev').value.trim();
|
||||
const list = await api('/api/registrations?eventId='+encodeURIComponent(evId));
|
||||
const tbody = document.querySelector('#regs tbody'); tbody.innerHTML='';
|
||||
list.forEach(r=>{
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${r.player?.name||'-'}</td><td>${r.player?.email||'-'}</td><td>${r.payment} (${r.paymentStatus||'-'})</td><td>${r.status}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,27 +0,0 @@
|
||||
:root{ color-scheme:dark; --bg:#0a0f1f; --card:rgba(255,255,255,.06); --line:rgba(255,255,255,.12); --txt:#eaf2ff; --acc:#27b0ff; --good:#22c55e; --bad:#ef4444 }
|
||||
*{ box-sizing:border-box }
|
||||
html,body{ margin:0; padding:0; background:radial-gradient(1000px 600px at 10% -10%, #122040 0%, transparent 60%), var(--bg); color:var(--txt); font-family: ui-sans-serif,system-ui,Segoe UI,Roboto,Helvetica,Arial }
|
||||
a{ color:#9ad1ff; text-decoration:none }
|
||||
.container{ max-width:1100px; margin:0 auto; padding:24px }
|
||||
.nav{ position:sticky; top:0; z-index:10; backdrop-filter: blur(10px); background: rgba(6,12,24,.6); border-bottom:1px solid var(--line) }
|
||||
.nav .inner{ display:flex; gap:16px; align-items:center; padding:12px 20px }
|
||||
.brand{ display:flex; gap:10px; align-items:center; font-weight:900; letter-spacing:.3px }
|
||||
.badge{ background:linear-gradient(135deg,#0ea5e9,#2563eb); padding:6px 10px; border-radius:10px; font-size:12px; font-weight:800; color:white }
|
||||
.spacer{ flex:1 }
|
||||
.btn{ background:linear-gradient(135deg,#0ea5e9,#2563eb); color:white; padding:10px 14px; border:0; border-radius:12px; cursor:pointer; font-weight:700; box-shadow:0 10px 24px rgba(37,99,235,.25) }
|
||||
.row{ display:flex; gap:14px; flex-wrap:wrap }
|
||||
.card{ background:var(--card); border:1px solid var(--line); border-radius:16px; padding:16px }
|
||||
h1{ font-size:28px; margin:18px 0 }
|
||||
table{ width:100%; border-collapse: collapse }
|
||||
th,td{ border-top:1px solid var(--line); padding:10px 8px; text-align:left }
|
||||
small{ opacity:.8 }
|
||||
.hero{ display:grid; grid-template-columns: 1.2fr .8fr; gap:22px; align-items:center }
|
||||
@media (max-width:900px){ .hero{ grid-template-columns:1fr } }
|
||||
.pill{ border:1px solid var(--line); border-radius:999px; padding:6px 10px; display:inline-flex; gap:6px; align-items:center }
|
||||
input,select{ background:#0f1b36; color:var(--txt); border:1px solid var(--line); border-radius:10px; padding:10px 12px; width:100% }
|
||||
label{ font-size:13px; opacity:.9 }
|
||||
.grid{ display:grid; gap:12px }
|
||||
.grid-2{ grid-template-columns: 1fr 1fr }
|
||||
.grid-3{ grid-template-columns: repeat(3, 1fr) }
|
||||
footer{ margin-top:40px; opacity:.7; font-size:13px; border-top:1px solid var(--line); padding-top:14px }
|
||||
.notice{ padding:10px 12px; border:1px dashed var(--line); border-radius:12px; background: rgba(255,255,255,.04) }
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs><linearGradient id="g" x1="0" x2="1"><stop offset="0" stop-color="#0ea5e9"/><stop offset="1" stop-color="#2563eb"/></linearGradient></defs>
|
||||
<circle cx="32" cy="32" r="30" fill="url(#g)"/>
|
||||
<circle cx="32" cy="32" r="18" fill="none" stroke="white" stroke-width="4" stroke-dasharray="4 6"/>
|
||||
<rect x="20" y="26" width="24" height="12" rx="6" fill="white" opacity="0.9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 449 B |
@@ -1,12 +0,0 @@
|
||||
async function api(path, opts={}){
|
||||
const res = await fetch(path, { headers:{ 'content-type':'application/json' }, ...opts });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const ct = res.headers.get('content-type')||'';
|
||||
return ct.includes('application/json')? res.json() : res.text();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', async ()=>{
|
||||
try {
|
||||
const cfg = await api('/api/config');
|
||||
const el = document.querySelector('.title'); if (el) el.textContent = cfg.siteName || 'Padel24Play';
|
||||
} catch {}
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
<nav class="nav"><div class="inner">
|
||||
<img src="/assets/img/logo.svg" width="22" height="22" alt="logo"/>
|
||||
<div class="brand"><span class="badge">P24P</span><span class="title">Padel24Play</span></div>
|
||||
<a href="/">Accueil</a><a href="/events.html">Événements</a><a href="/admin.html">Admin</a>
|
||||
<div class="spacer"></div><a href="/reglement.html">Règlement</a></div></nav>
|
||||
|
||||
<main class="container">
|
||||
<h1>📅 Événements</h1>
|
||||
<div class="card">
|
||||
<table id="eventsTable"><thead><tr><th>Nom</th><th>Type</th><th>Date</th><th>Lieu</th><th></th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<script>
|
||||
(async()=>{ const rows=document.querySelector('#eventsTable tbody'); const list = await api('/api/events');
|
||||
list.forEach(ev=>{ const tr=document.createElement('tr'); tr.innerHTML = `<td>${ev.name}</td><td>${ev.kind||'-'}</td><td>${new Date(ev.date).toLocaleString()}</td><td>${ev.location}</td>
|
||||
<td><a class="btn" href="/register.html?eventId=${encodeURIComponent(ev.id)}">S'inscrire</a></td>`; rows.appendChild(tr); });
|
||||
})();</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<nav class="nav"><div class="inner">
|
||||
<img src="/assets/img/logo.svg" width="22" height="22" alt="logo"/>
|
||||
<div class="brand"><span class="badge">P24P</span><span class="title">Padel24Play</span></div>
|
||||
<a href="/">Accueil</a><a href="/events.html">Événements</a><a href="/admin.html">Admin</a>
|
||||
<div class="spacer"></div><a href="/reglement.html">Règlement</a></div></nav>
|
||||
|
||||
<main class="container">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<h1>🎉 SuperSunday — V1 (prod)</h1>
|
||||
<p>Base propre prête à tester (port 8080) — événements, inscriptions, iCal.</p>
|
||||
<div class="row" style="margin-top:16px">
|
||||
<a class="btn" href="/events.html">Voir les événements</a>
|
||||
<a class="btn" href="/admin.html">Admin</a>
|
||||
</div>
|
||||
<p class="notice" style="margin-top:12px">Version sans effets “waouw” pour rester simple. On les ajoutera en brique.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Ce qui est dedans</h3>
|
||||
<ul>
|
||||
<li>8 éditions Winter (tous les 14 jours dès dim. 5 oct. 2025, 08:00–11:00)</li>
|
||||
<li>Un live démo (pour les inscriptions) et un futur ouvert</li>
|
||||
<li>Formulaire d’inscription + iCal</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
@@ -1,32 +0,0 @@
|
||||
<nav class="nav"><div class="inner">
|
||||
<img src="/assets/img/logo.svg" width="22" height="22" alt="logo"/>
|
||||
<div class="brand"><span class="badge">P24P</span><span class="title">Padel24Play</span></div>
|
||||
<a href="/">Accueil</a><a href="/events.html">Événements</a><a href="/admin.html">Admin</a>
|
||||
<div class="spacer"></div><a href="/reglement.html">Règlement</a></div></nav>
|
||||
|
||||
<main class="container">
|
||||
<h1>📝 Inscription</h1>
|
||||
<div class="card">
|
||||
<form id="form" class="grid grid-2">
|
||||
<div><label>Nom complet</label><input name="name" required/></div>
|
||||
<div><label>Email</label><input name="email" type="email" required/></div>
|
||||
<div><label>Téléphone</label><input name="phone" required/></div>
|
||||
<div><label>Niveau</label><select name="level"><option>Débutant</option><option>Intermédiaire</option><option>Avancé</option></select></div>
|
||||
<div><label>Paiement</label><select name="payment"><option value="card">Stripe</option><option value="paypal">PayPal</option><option value="onspot">Sur place</option></select></div>
|
||||
<div><button class="btn">Valider</button></div>
|
||||
</form>
|
||||
<p id="msg" class="notice"></p>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search); const eventId = params.get('eventId')||'ss-futur-1';
|
||||
document.getElementById('form').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const f = new FormData(e.target);
|
||||
const reg = { eventId, player:{ name:f.get('name'), email:f.get('email'), phone:f.get('phone'), level:f.get('level') }, payment:f.get('payment') };
|
||||
const created = await fetch('/api/registrations', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(reg) }).then(r=>r.json());
|
||||
document.getElementById('msg').innerHTML = "Inscription créée ✔️ — "+
|
||||
"<a class='btn' href='/api/ical/"+created.id+"'>Télécharger iCal</a>";
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
<nav class="nav"><div class="inner">
|
||||
<img src="/assets/img/logo.svg" width="22" height="22" alt="logo"/>
|
||||
<div class="brand"><span class="badge">P24P</span><span class="title">Padel24Play</span></div>
|
||||
<a href="/">Accueil</a><a href="/events.html">Événements</a><a href="/admin.html">Admin</a>
|
||||
<div class="spacer"></div><a href="/reglement.html">Règlement</a></div></nav>
|
||||
|
||||
<main class="container">
|
||||
<h1>📜 Règlement (extrait)</h1>
|
||||
<div class="card"><p>Americano, fair‑play, BYE équitables, remboursement jusqu’à J‑2 (48h), iCal après inscription.</p></div>
|
||||
</main>
|
||||
19
run_local.sh
Executable file
19
run_local.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "=== Super Sunday — lancement local (env=.env.prod) ==="
|
||||
|
||||
if [ ! -f ".env.prod" ]; then
|
||||
echo "[erreur] .env.prod introuvable dans $(pwd)"
|
||||
echo "Crée-le ou copie-le depuis ton zip."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose --env-file .env.prod up -d --build
|
||||
sleep 5
|
||||
docker compose ps
|
||||
|
||||
echo
|
||||
echo "Frontend : http://localhost:8080"
|
||||
echo "Admin : http://localhost:8080/admin"
|
||||
echo "Logs API : docker compose logs -f api"
|
||||
echo "Stop : docker compose down"
|
||||
146
server.mjs
146
server.mjs
@@ -1,146 +0,0 @@
|
||||
import http from 'http';
|
||||
import { readFile, writeFile, stat } from 'fs/promises';
|
||||
import { createReadStream } from 'fs';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const DATA_DIR = path.join(__dirname, 'data');
|
||||
const DB_PATH = path.join(DATA_DIR, 'db.json');
|
||||
|
||||
const mime = { '.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'application/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.svg':'image/svg+xml','.png':'image/png','.ico':'image/x-icon','.webp':'image/webp' };
|
||||
|
||||
function send(res, code, body, headers={}){ res.writeHead(code, { 'content-type':'text/plain; charset=utf-8', ...headers }); res.end(body); }
|
||||
|
||||
async function ensureDb(){
|
||||
try { await stat(DB_PATH); }
|
||||
catch {
|
||||
const base = new Date(Date.UTC(2025, 9, 5, 8, 0, 0)); // 5 Oct 2025 08:00 UTC
|
||||
const events = [];
|
||||
for (let i=0;i<8;i++){
|
||||
const start = new Date(base.getTime() + i*14*24*3600*1000);
|
||||
const end = new Date(start.getTime() + 3*3600*1000);
|
||||
events.push({ id:`ss-winter-${i+1}`, kind:"Americano", name:`SuperSunday — Winter Edition #${i+1}`, date:start.toISOString().slice(0,19), end:end.toISOString().slice(0,19), location:"Sport City Woluwe" });
|
||||
}
|
||||
const now = new Date();
|
||||
events.push(
|
||||
{ id:"ss-live-now", kind:"Americano", name:"SuperSunday — LIVE (démo)", date: new Date(now.getTime()-30*60*1000).toISOString().slice(0,19), end: new Date(now.getTime()+2.5*3600*1000).toISOString().slice(0,19), location:"TC Églantiers" },
|
||||
{ id:"ss-futur-1", kind:"Americano", name:"SuperSunday — Inscription ouverte", date: new Date(now.getTime()+10*24*3600*1000).toISOString().slice(0,19), end: new Date(now.getTime()+10*24*3600*1000+3*3600*1000).toISOString().slice(0,19), location:"Sport City Woluwe" }
|
||||
);
|
||||
const names = ["SmashQueen","PadelKing42","MrVibora","LaBandeja","MissChiquita","VoléeMagique"];
|
||||
const regs = names.map((n,i)=>({
|
||||
id:`reg_seed_${i+1}`, createdAt:new Date().toISOString(), eventId: i%2===0? "ss-live-now" : "ss-winter-1",
|
||||
player:{ name:n, email:`${n.replace(/[^a-z0-9]/ig,'').toLowerCase()}@demo.local`, phone:`+3247${(1000000+i).toString().padStart(7,'0')}`, level: i%3===0? "Avancé": (i%3===1? "Intermédiaire":"Débutant") },
|
||||
payment:i%3===0? "card": (i%3===1? "paypal":"onspot"), status: i%2===0? "confirmé":"en_attente", paymentStatus: i%2===0? "paid":"pending"
|
||||
}));
|
||||
await writeFile(DB_PATH, JSON.stringify({
|
||||
config:{ siteName:"Padel24Play — V1 (8080)", currency:"EUR", priceEur:48, refundWindowHours:48 },
|
||||
users:[
|
||||
{ email:"admin@supersunday.com", password:"password123", role:"admin", name:"Admin"},
|
||||
{ email:"player@supersunday.com", password:"password123", role:"user", name:"Player"}
|
||||
],
|
||||
events, registrations: regs
|
||||
}, null, 2));
|
||||
}
|
||||
}
|
||||
async function loadDb(){ await ensureDb(); return JSON.parse(await readFile(DB_PATH,'utf-8')); }
|
||||
async function saveDb(d){ await writeFile(DB_PATH, JSON.stringify(d,null,2)); }
|
||||
|
||||
function sendJson(res, obj){ send(res, 200, JSON.stringify(obj), {'content-type':'application/json'}); }
|
||||
|
||||
async function parseBody(req){
|
||||
return new Promise((resolve,reject)=>{
|
||||
let data=''; req.on('data',c=> data+=c); req.on('end', ()=>{
|
||||
try{
|
||||
const ct = req.headers['content-type']||'';
|
||||
if (!data) return resolve({});
|
||||
if (ct.includes('application/json')) return resolve(JSON.parse(data));
|
||||
if (ct.includes('application/x-www-form-urlencoded')){
|
||||
const out={}; data.split('&').forEach(kv=>{ const [k,v] = kv.split('='); out[decodeURIComponent(k)] = decodeURIComponent((v||'').replace(/\\+/g,' ')); });
|
||||
return resolve(out);
|
||||
}
|
||||
resolve({ raw:data });
|
||||
}catch(e){ reject(e); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function serveStatic(req,res){
|
||||
const publicDir = path.join(__dirname, 'public');
|
||||
let pathname = url.parse(req.url).pathname || '/';
|
||||
if (pathname === '/') pathname = '/index.html';
|
||||
const filePath = path.join(publicDir, pathname);
|
||||
try {
|
||||
const st = await stat(filePath);
|
||||
if (!st.isDirectory()){
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
res.writeHead(200, {'content-type': mime[ext] || 'application/octet-stream'});
|
||||
createReadStream(filePath).pipe(res);
|
||||
return true;
|
||||
}
|
||||
}catch{}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isoToV(s){ return s.replace(/[-:]/g,'').replace('T','')+'Z'; }
|
||||
|
||||
async function handleApi(req,res){
|
||||
const { pathname, query } = url.parse(req.url, true);
|
||||
|
||||
if (pathname === '/api/config' && req.method==='GET'){
|
||||
const d = await loadDb(); return sendJson(res, d.config);
|
||||
}
|
||||
|
||||
if (pathname === '/api/events' && req.method==='GET'){
|
||||
const d = await loadDb(); return sendJson(res, d.events);
|
||||
}
|
||||
if (pathname === '/api/registrations' && req.method==='GET'){
|
||||
const d = await loadDb(); const list = query.eventId ? d.registrations.filter(r=> r.eventId===query.eventId) : d.registrations;
|
||||
return sendJson(res, list);
|
||||
}
|
||||
if (pathname === '/api/registrations' && req.method==='POST'){
|
||||
const d = await loadDb(); const body = await parseBody(req);
|
||||
const id = 'reg_'+Math.random().toString(36).slice(2,10);
|
||||
const rec = { id, createdAt:new Date().toISOString(), status:'en_attente', paymentStatus:'pending', ...body };
|
||||
d.registrations.push(rec); await saveDb(d);
|
||||
return sendJson(res, rec);
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/api/ical/') && req.method==='GET'){
|
||||
const regId = pathname.split('/').pop();
|
||||
const d = await loadDb(); const reg = d.registrations.find(r=> r.id===regId);
|
||||
if (!reg) return send(res,404,'Not found');
|
||||
const ev = d.events.find(e=> e.id===reg.eventId);
|
||||
if (!ev) return send(res,404,'Event not found');
|
||||
const ics = [
|
||||
'BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//P24P//SuperSunday//FR',
|
||||
'BEGIN:VEVENT',
|
||||
`UID:${reg.id}@p24p`,
|
||||
`DTSTAMP:${isoToV(ev.date)}`,
|
||||
`DTSTART:${isoToV(ev.date)}`,
|
||||
`DTEND:${isoToV(ev.end)}`,
|
||||
`SUMMARY:${ev.name}`,
|
||||
`LOCATION:${ev.location}`,
|
||||
`DESCRIPTION:Inscription ${reg.player?.name||''}`,
|
||||
'END:VEVENT','END:VCALENDAR'
|
||||
].join('\\r\\n');
|
||||
return send(res,200,ics,{'content-type':'text/calendar; charset=utf-8','content-disposition':`attachment; filename="${reg.id}.ics"`});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req,res)=>{
|
||||
try{
|
||||
if (req.url.startsWith('/api/')){
|
||||
const ok = await handleApi(req,res);
|
||||
if (ok===false) return send(res,404,'API not found');
|
||||
return;
|
||||
}
|
||||
const ok = await serveStatic(req,res);
|
||||
if (!ok) send(res,404,'Not found');
|
||||
}catch(e){ console.error(e); send(res,500,'Server error'); }
|
||||
});
|
||||
|
||||
server.listen(PORT, ()=> console.log(`✅ SuperSunday V1 (prod) http://localhost:${PORT}`));
|
||||
5
start.sh
5
start.sh
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "🚀 SuperSunday V1 (prod) on :8080"
|
||||
export PORT=8080
|
||||
export ENABLE_EFFECTS=0
|
||||
node server.mjs
|
||||
25
sync.sh
25
sync.sh
@@ -1,25 +0,0 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2822
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .AppleSystemUIFontMonospaced-Regular;}
|
||||
{\colortbl;\red255\green255\blue255;\red135\green5\blue129;\red0\green0\blue0;\red181\green0\blue19;
|
||||
\red50\green91\blue97;\red13\green100\blue1;\red151\green0\blue126;}
|
||||
{\*\expandedcolortbl;;\cssrgb\c60784\c13725\c57647;\csgray\c0;\cssrgb\c76863\c10196\c8627;
|
||||
\cssrgb\c24706\c43137\c45490;\cssrgb\c0\c45490\c0;\cssrgb\c66667\c5098\c56863;}
|
||||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
|
||||
|
||||
\f0\fs26 \cf2 #!/bin/bash\cf3 \
|
||||
set -e\
|
||||
MSG=\cf4 "\cf5 $\{1:-chore: quick sync\}\cf4 "\cf3 \
|
||||
\
|
||||
\cf6 # S\'92assure qu\'92on est dans un repo\cf3 \
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || \{ echo \cf4 "Pas un d\'e9p\'f4t Git"\cf3 ; exit 1; \}\
|
||||
\
|
||||
\cf6 # Ajoute, commit et push\cf3 \
|
||||
git add -A\
|
||||
\cf7 if\cf3 ! git diff --cached --quiet; \cf7 then\cf3 \
|
||||
git commit -m \cf4 "\cf5 $MSG\cf4 "\cf3 \
|
||||
\cf7 else\cf3 \
|
||||
echo \cf4 "Rien \'e0 committer."\cf3 \
|
||||
\cf7 fi\cf3 \
|
||||
git push\
|
||||
echo \cf4 "\uc0\u9989 Sync OK"}
|
||||
16
sync2.sh
16
sync2.sh
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
MSG="${1:-chore: quick sync}"
|
||||
|
||||
# S’assure qu’on est dans un repo
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "Pas un dépôt Git"; exit 1; }
|
||||
|
||||
# Ajoute, commit et push
|
||||
git add -A
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "$MSG"
|
||||
else
|
||||
echo "Rien à committer."
|
||||
fi
|
||||
git push
|
||||
echo "✅ Sync OK"
|
||||
Reference in New Issue
Block a user