Day:1
This commit is contained in:
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# SuperSunday — V1 (prod, port 8080)
|
||||
|
||||
## Démarrage
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
# http://localhost:8080
|
||||
```
|
||||
|
||||
## 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)
|
||||
190
data/db.json
Normal file
190
data/db.json
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "supersunday-v1-base",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": { "start": "node server.mjs" }
|
||||
}
|
||||
27
public/admin.html
Normal file
27
public/admin.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<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>
|
||||
27
public/assets/css/app.css
Normal file
27
public/assets/css/app.css
Normal file
@@ -0,0 +1,27 @@
|
||||
: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) }
|
||||
6
public/assets/img/logo.svg
Normal file
6
public/assets/img/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 449 B |
12
public/assets/js/app.js
Normal file
12
public/assets/js/app.js
Normal file
@@ -0,0 +1,12 @@
|
||||
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 {}
|
||||
});
|
||||
18
public/events.html
Normal file
18
public/events.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
28
public/index.html
Normal file
28
public/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
32
public/register.html
Normal file
32
public/register.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
10
public/reglement.html
Normal file
10
public/reglement.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
146
server.mjs
Normal file
146
server.mjs
Normal file
@@ -0,0 +1,146 @@
|
||||
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
Executable file
5
start.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
echo "🚀 SuperSunday V1 (prod) on :8080"
|
||||
export PORT=8080
|
||||
export ENABLE_EFFECTS=0
|
||||
node server.mjs
|
||||
25
sync.sh
Executable file
25
sync.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
{\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
Executable file
16
sync2.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/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