diff --git a/.DS_Store b/.DS_Store index 3e99647..c6cc241 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.env. b/.env. new file mode 100644 index 0000000..93c8acf --- /dev/null +++ b/.env. @@ -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" \ No newline at end of file diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..63870b6 --- /dev/null +++ b/.env.prod @@ -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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9dabb86 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index d703d38..76a11ce 100644 --- a/README.md +++ b/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 +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 `. + +## Notes +- Change `JWT_SECRET` and admin credentials before going live. +- SQL seed creates minimal schema + demo tournament. \ No newline at end of file diff --git a/README_PATCH.txt b/README_PATCH.txt new file mode 100644 index 0000000..c39474f --- /dev/null +++ b/README_PATCH.txt @@ -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 , include: + + 3. Apply classes .hero-title, .section-title, .kicker, .btn-outline in your HTML. + +Rebuild web: + docker compose build web && docker compose up -d diff --git a/apply_patch.sh b/apply_patch.sh new file mode 100755 index 0000000..ab041b6 --- /dev/null +++ b/apply_patch.sh @@ -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 !" \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b509b8e --- /dev/null +++ b/backend/Dockerfile @@ -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"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..fee7bc9 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1200 @@ +{ + "name": "supersunday-api", + "version": "1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supersunday-api", + "version": "1.1.0", + "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" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..2702227 --- /dev/null +++ b/backend/package.json @@ -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" + } +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..95c0a6f --- /dev/null +++ b/backend/server.js @@ -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) 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) 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_ascore_a THEN 1 ELSE 0 END AS w, CASE WHEN score_b console.log(`API listening on :${PORT}`)); \ No newline at end of file diff --git a/bakcgrounp24p.jpg b/bakcgrounp24p.jpg new file mode 100644 index 0000000..6047882 Binary files /dev/null and b/bakcgrounp24p.jpg differ diff --git a/data/db.json b/data/db.json deleted file mode 100644 index fc3b274..0000000 --- a/data/db.json +++ /dev/null @@ -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" - } - ] -} \ No newline at end of file diff --git a/db/init/001_schema_and_seed.sql b/db/init/001_schema_and_seed.sql new file mode 100644 index 0000000..9c5d335 --- /dev/null +++ b/db/init/001_schema_and_seed.sql @@ -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 $$; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ef5c13 --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cefea48 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +COPY public /usr/share/nginx/html \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8f364fa --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,67 @@ + + + + + + Super Sunday — Admin + + + +
+

Admin — Super Sunday

+ +
+ +
+
+

Connexion

+ + + +

+
+ + +
+ + + + + \ No newline at end of file diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf new file mode 100644 index 0000000..6c54b3a --- /dev/null +++ b/frontend/nginx/default.conf @@ -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; + } +} \ No newline at end of file diff --git a/package.json b/frontend/package.json similarity index 100% rename from package.json rename to frontend/package.json diff --git a/frontend/public/admin/index.html b/frontend/public/admin/index.html new file mode 100644 index 0000000..c474c5f --- /dev/null +++ b/frontend/public/admin/index.html @@ -0,0 +1,74 @@ + + + + + + Super Sunday — Admin + + + +
+

Admin — Super Sunday

+ +
+ +
+
+

Connexion

+ + + +

+
+ + +
+ + +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/public/assets/admin.js b/frontend/public/assets/admin.js new file mode 100644 index 0000000..e062d2e --- /dev/null +++ b/frontend/public/assets/admin.js @@ -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(); +} \ No newline at end of file diff --git a/frontend/public/assets/api.js b/frontend/public/assets/api.js new file mode 100644 index 0000000..e7aa18a --- /dev/null +++ b/frontend/public/assets/api.js @@ -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; \ No newline at end of file diff --git a/frontend/public/assets/events.js b/frontend/public/assets/events.js new file mode 100644 index 0000000..a434563 --- /dev/null +++ b/frontend/public/assets/events.js @@ -0,0 +1,11 @@ +(async function(){ + const items = await SSAPI.get('/tournaments/upcoming').catch(()=>[]); + const el = document.getElementById('list'); + el.innerHTML = items.map(t=>` + + ${t.name}
+ ${t.location || ''}
+ ${t.start_date} → ${t.end_date} +
+ `).join('') || '

Aucun événement à venir

'; +})(); \ No newline at end of file diff --git a/frontend/public/assets/image/backgroundp24p.jpg b/frontend/public/assets/image/backgroundp24p.jpg new file mode 100644 index 0000000..6047882 Binary files /dev/null and b/frontend/public/assets/image/backgroundp24p.jpg differ diff --git a/frontend/public/assets/main.js b/frontend/public/assets/main.js new file mode 100644 index 0000000..37f70cf --- /dev/null +++ b/frontend/public/assets/main.js @@ -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=>` +
+ ${t.name}
+ ${t.location || ''}
+ ${t.start_date} → ${t.end_date} +
+ `).join('') || '

Aucun tournoi

'; + + const mWrap = document.getElementById('matches'); + mWrap.innerHTML = matches.map(m=>` +
+
${m.round || 'Match'} — ${m.court || 'Court ?'}
+
Teams: ${m.team_a_id || '?'} vs ${m.team_b_id || '?'}
+
Score: ${m.score_a} - ${m.score_b} (${m.status})
+
${m.scheduled_at ? new Date(m.scheduled_at).toLocaleString() : ''}
+
+ `).join('') || '

Aucun match

'; +})(); \ No newline at end of file diff --git a/frontend/public/assets/style.css b/frontend/public/assets/style.css new file mode 100644 index 0000000..bf37c58 --- /dev/null +++ b/frontend/public/assets/style.css @@ -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) } \ No newline at end of file diff --git a/frontend/public/assets/style.titles.patch.css b/frontend/public/assets/style.titles.patch.css new file mode 100644 index 0000000..86a86d9 --- /dev/null +++ b/frontend/public/assets/style.titles.patch.css @@ -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 } \ No newline at end of file diff --git a/frontend/public/assets/tournament.js b/frontend/public/assets/tournament.js new file mode 100644 index 0000000..79c3ddd --- /dev/null +++ b/frontend/public/assets/tournament.js @@ -0,0 +1,39 @@ +(function(){ + const params = new URLSearchParams(location.search); + const id = Number(params.get('id') || '0'); + if(!id){ + document.getElementById('info').innerHTML = '

Tournoi introuvable (id manquant)

'; + return; + } + async function load(){ + const t = await SSAPI.get('/tournaments/'+id).catch(()=>null); + if(!t){ document.getElementById('info').innerHTML = '

Tournoi introuvable

'; return; } + document.getElementById('info').innerHTML = ` +

${t.name}

+
Lieu: ${t.location || '—'}
+
Dates: ${t.start_date} → ${t.end_date}
+
+
Inscrits
${t.enrollment_count}
+
Équipes
${t.team_count}
+
Matches
${t.match_count}
+
+ ${t.next_match ? `
Prochain match
${t.next_match.round || ''} — ${t.next_match.court || ''}
${t.next_match.scheduled_at ? new Date(t.next_match.scheduled_at).toLocaleString() : ''}
` : ''} + `; + const parts = await SSAPI.get('/enrollments?tournament_id='+id).catch(()=>[]); + document.getElementById('participants').innerHTML = parts.map(p=>` +
+ ${p.first_name} ${p.last_name} (#${p.ranking || '—'}) +
+ `).join('') || '

Aucun inscrit

'; + const matches = await SSAPI.get('/matches?tournament_id='+id).catch(()=>[]); + document.getElementById('matchlist').innerHTML = matches.map(m=>` +
+
${m.round || 'Match'} — ${m.court || 'Court ?'}
+
Teams: ${m.team_a_id || '?'} vs ${m.team_b_id || '?'}
+
Score: ${m.score_a} - ${m.score_b} (${m.status})
+
${m.scheduled_at ? new Date(m.scheduled_at).toLocaleString() : ''}
+
+ `).join('') || '

Aucun match

'; + } + load(); +})(); \ No newline at end of file diff --git a/frontend/public/events/index.html b/frontend/public/events/index.html new file mode 100644 index 0000000..5c1062a --- /dev/null +++ b/frontend/public/events/index.html @@ -0,0 +1,24 @@ + + + + + + Événements — Super Sunday + + + +
+

Événements à venir

+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..20fa625 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,40 @@ + + + + + + Super Sunday — Accueil + + + +
+

Super Sunday — Padel Championship

+ +
+ +
+
+

Tournois

+
+
+
+

Matches

+
Aucun match
+
+
+ + +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/public/tournament/index.html b/frontend/public/tournament/index.html new file mode 100644 index 0000000..c64c43d --- /dev/null +++ b/frontend/public/tournament/index.html @@ -0,0 +1,41 @@ + + + + + + Détail tournoi — Super Sunday + + + +
+

Détail du tournoi

+ +
+ +
+
+
+

Participants

+
+
+
+

Matches

+
+
+
+ + +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dc3c19a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "supersunday_prod_1.5", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/public/admin.html b/public/admin.html deleted file mode 100644 index 30f4ec5..0000000 --- a/public/admin.html +++ /dev/null @@ -1,27 +0,0 @@ - - -
-

🛠️ Admin — Inscriptions

-
-
-
JoueurEmailPaiementStatut
-

Version prod de base — paiements temps réel & effets à venir.

-
-
- - diff --git a/public/assets/css/app.css b/public/assets/css/app.css deleted file mode 100644 index 48961f9..0000000 --- a/public/assets/css/app.css +++ /dev/null @@ -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) } diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg deleted file mode 100644 index 6b476b7..0000000 --- a/public/assets/img/logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/public/assets/js/app.js b/public/assets/js/app.js deleted file mode 100644 index e3a18cf..0000000 --- a/public/assets/js/app.js +++ /dev/null @@ -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 {} -}); diff --git a/public/events.html b/public/events.html deleted file mode 100644 index ba05acc..0000000 --- a/public/events.html +++ /dev/null @@ -1,18 +0,0 @@ - - -
-

📅 Événements

-
-
NomTypeDateLieu
-
-
- - diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 6e8981c..0000000 --- a/public/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - -
-
-
-

🎉 SuperSunday — V1 (prod)

-

Base propre prête à tester (port 8080) — événements, inscriptions, iCal.

- -

Version sans effets “waouw” pour rester simple. On les ajoutera en brique.

-
-
-

Ce qui est dedans

-
    -
  • 8 éditions Winter (tous les 14 jours dès dim. 5 oct. 2025, 08:00–11:00)
  • -
  • Un live démo (pour les inscriptions) et un futur ouvert
  • -
  • Formulaire d’inscription + iCal
  • -
-
-
-
- diff --git a/public/register.html b/public/register.html deleted file mode 100644 index cca8a61..0000000 --- a/public/register.html +++ /dev/null @@ -1,32 +0,0 @@ - - -
-

📝 Inscription

-
-
-
-
-
-
-
-
-
-

-
-
- - diff --git a/public/reglement.html b/public/reglement.html deleted file mode 100644 index 5c55c7f..0000000 --- a/public/reglement.html +++ /dev/null @@ -1,10 +0,0 @@ - - -
-

📜 Règlement (extrait)

-

Americano, fair‑play, BYE équitables, remboursement jusqu’à J‑2 (48h), iCal après inscription.

-
diff --git a/run_local.sh b/run_local.sh new file mode 100755 index 0000000..df0f4b5 --- /dev/null +++ b/run_local.sh @@ -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" diff --git a/server.mjs b/server.mjs deleted file mode 100644 index 673ac3f..0000000 --- a/server.mjs +++ /dev/null @@ -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}`)); diff --git a/start.sh b/start.sh deleted file mode 100755 index 1518835..0000000 --- a/start.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -echo "🚀 SuperSunday V1 (prod) on :8080" -export PORT=8080 -export ENABLE_EFFECTS=0 -node server.mjs diff --git a/sync.sh b/sync.sh deleted file mode 100755 index ac696f2..0000000 --- a/sync.sh +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/sync2.sh b/sync2.sh deleted file mode 100755 index 651fd28..0000000 --- a/sync2.sh +++ /dev/null @@ -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"