diff --git a/ACHIEVEMENTS.md b/ACHIEVEMENTS.md new file mode 100644 index 0000000..2c812c4 --- /dev/null +++ b/ACHIEVEMENTS.md @@ -0,0 +1,217 @@ +# 🏆 Ninja Cross Parkour Achievement System + +Ein umfassendes Achievement-System für das Ninja Cross Parkour im Schwimmbad. + +## 📊 System-Übersicht + +Das Achievement-System besteht aus: +- **32 verschiedene Achievements** in 4 Kategorien +- **Automatische tägliche Vergabe** am Ende des Tages +- **REST API Endpoints** für Frontend-Integration +- **PostgreSQL Funktionen** für effiziente Verarbeitung + +## 🎯 Achievement-Kategorien + +### 1. Konsistenz-basierte Achievements +- **Erste Schritte** 👶 - Erste Zeit aufgezeichnet (5 Punkte) +- **Durchhalter** 💪 - 3 Versuche an einem Tag (10 Punkte) +- **Fleißig** 🔥 - 5 Versuche an einem Tag (15 Punkte) +- **Besessen** 😤 - 10 Versuche an einem Tag (25 Punkte) +- **Regelmäßig** 📅 - 5 verschiedene Tage gespielt (20 Punkte) +- **Stammgast** ⭐ - 10 verschiedene Tage gespielt (30 Punkte) +- **Treue** 💎 - 20 verschiedene Tage gespielt (50 Punkte) +- **Veteran** 🏆 - 50 verschiedene Tage gespielt (100 Punkte) + +### 2. Verbesserungs-basierte Achievements +- **Fortschritt** 📈 - Persönliche Bestzeit um 5 Sekunden verbessert (15 Punkte) +- **Durchbruch** ⚡ - Persönliche Bestzeit um 10 Sekunden verbessert (25 Punkte) +- **Transformation** 🔄 - Persönliche Bestzeit um 15 Sekunden verbessert (40 Punkte) +- **Perfektionist** ✨ - Persönliche Bestzeit um 20 Sekunden verbessert (60 Punkte) + +### 3. Saisonale Achievements +- **Wochenend-Krieger** 🏁 - Am Wochenende gespielt (10 Punkte) +- **Nachmittags-Sportler** ☀️ - Zwischen 14-18 Uhr gespielt (10 Punkte) +- **Frühaufsteher** 🌅 - Vor 10 Uhr gespielt (15 Punkte) +- **Abend-Sportler** 🌙 - Nach 18 Uhr gespielt (10 Punkte) + +### 4. Monatliche Achievements +- **Januar-Krieger** ❄️ bis **Dezember-Dynamo** 🎄 (je 20 Punkte) + +### 5. Jahreszeiten-Achievements +- **Frühjahrs-Fighter** 🌱 - Im Frühling gespielt (30 Punkte) +- **Sommer-Sportler** ☀️ - Im Sommer gespielt (30 Punkte) +- **Herbst-Held** 🍂 - Im Herbst gespielt (30 Punkte) +- **Winter-Warrior** ❄️ - Im Winter gespielt (30 Punkte) + +## 🗄️ Datenbank-Schema + +### Tabelle: `achievements` +```sql +- id (uuid, PK) +- name (varchar) - Achievement-Name +- description (text) - Beschreibung +- category (varchar) - Kategorie +- condition_type (varchar) - Bedingungstyp +- condition_value (integer) - Bedingungswert +- icon (varchar) - Emoji-Icon +- points (integer) - Punkte +- is_active (boolean) - Aktiv +- created_at (timestamp) +``` + +### Tabelle: `player_achievements` +```sql +- id (uuid, PK) +- player_id (uuid, FK) - Verweis auf players.id +- achievement_id (uuid, FK) - Verweis auf achievements.id +- earned_at (timestamp) - Wann erreicht +- progress (integer) - Fortschritt +- is_completed (boolean) - Abgeschlossen +- created_at (timestamp) +``` + +## 🔧 PostgreSQL Funktionen + +### `check_consistency_achievements(player_uuid)` +Überprüft alle Konsistenz-basierten Achievements für einen Spieler. + +### `check_improvement_achievements(player_uuid)` +Überprüft alle Verbesserungs-basierten Achievements für einen Spieler. + +### `check_seasonal_achievements(player_uuid)` +Überprüft alle saisonalen und monatlichen Achievements für einen Spieler. + +### `check_all_achievements(player_uuid)` +Führt alle Achievement-Überprüfungen für einen Spieler aus. + +## 🚀 API Endpoints + +### GET `/api/achievements` +Alle verfügbaren Achievements abrufen. + +### GET `/api/achievements/player/:playerId` +Achievements eines bestimmten Spielers abrufen. + +### GET `/api/achievements/player/:playerId/stats` +Achievement-Statistiken eines Spielers abrufen. + +### POST `/api/achievements/check/:playerId` +Achievements für einen Spieler manuell überprüfen. + +### POST `/api/achievements/daily-check` +Tägliche Achievement-Überprüfung für alle Spieler ausführen. + +### GET `/api/achievements/leaderboard?limit=10` +Bestenliste der Spieler nach Achievement-Punkten. + +## 📅 Automatisierung + +### Tägliches Script +```bash +# Manuell ausführen +node scripts/daily_achievements.js + +# Cron-Job einrichten +node scripts/setup_cron.js setup + +# Cron-Job Status prüfen +node scripts/setup_cron.js status + +# Cron-Job entfernen +node scripts/setup_cron.js remove +``` + +### Cron-Schedule +- **Zeit**: Täglich um 23:59 Uhr +- **Log**: `/var/log/ninjaserver_achievements.log` + +## 🎮 Frontend-Integration + +### Beispiel: Achievement-Liste laden +```javascript +fetch('/api/achievements/player/PLAYER_ID') + .then(response => response.json()) + .then(data => { + data.data.forEach(achievement => { + console.log(`${achievement.icon} ${achievement.name}: ${achievement.is_completed ? '✅' : '❌'}`); + }); + }); +``` + +### Beispiel: Statistiken anzeigen +```javascript +fetch('/api/achievements/player/PLAYER_ID/stats') + .then(response => response.json()) + .then(data => { + console.log(`Punkte: ${data.data.total_points}`); + console.log(`Abgeschlossen: ${data.data.completed_achievements}/${data.data.total_achievements}`); + }); +``` + +## 🔍 Monitoring + +### Logs überwachen +```bash +# Live-Logs anzeigen +tail -f /var/log/ninjaserver_achievements.log + +# Letzte Ausführung prüfen +grep "Daily achievement check completed" /var/log/ninjaserver_achievements.log | tail -1 +``` + +### Datenbank-Status prüfen +```sql +-- Achievement-Statistiken +SELECT + COUNT(*) as total_achievements, + COUNT(CASE WHEN is_active = true THEN 1 END) as active_achievements +FROM achievements; + +-- Spieler-Statistiken +SELECT + COUNT(DISTINCT player_id) as players_with_achievements, + COUNT(*) as total_earned_achievements +FROM player_achievements +WHERE is_completed = true; +``` + +## 🛠️ Wartung + +### Neue Achievements hinzufügen +1. Achievement in `achievements` Tabelle einfügen +2. Logik in entsprechenden PostgreSQL Funktionen erweitern +3. API Endpoints testen + +### Achievement deaktivieren +```sql +UPDATE achievements SET is_active = false WHERE name = 'Achievement-Name'; +``` + +### Daten zurücksetzen +```sql +-- Alle Spieler-Achievements löschen +DELETE FROM player_achievements; + +-- Achievement-Statistiken zurücksetzen +UPDATE achievements SET created_at = NOW(); +``` + +## 📈 Performance + +- **Indizierung**: Automatische Indizes auf `player_id` und `achievement_id` +- **Batch-Processing**: Effiziente Verarbeitung aller Spieler +- **Caching**: Achievements werden nur bei Änderungen neu berechnet +- **Timezone**: Korrekte Zeitzone-Behandlung (Europe/Berlin) + +## 🔒 Sicherheit + +- **API-Schutz**: Alle Endpoints über bestehende Authentifizierung +- **SQL-Injection**: Parametrisierte Queries +- **Datenvalidierung**: Eingabe-Validierung in allen Funktionen +- **Fehlerbehandlung**: Umfassende Error-Handling + +--- + +**Erstellt am**: $(date) +**Version**: 1.0.0 +**Autor**: Ninja Cross Parkour System diff --git a/package-lock.json b/package-lock.json index c2fd6f2..4ca5d08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,58 @@ "express": "^4.18.2", "express-session": "^1.17.3", "pg": "^8.11.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -370,6 +416,12 @@ "node": ">= 0.6" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -472,6 +524,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -493,6 +552,12 @@ "@types/node": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", @@ -876,6 +941,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -972,6 +1043,15 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1137,6 +1217,18 @@ "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", "license": "BSD-3-Clause" }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -2153,6 +2245,26 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -2496,6 +2608,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -3603,6 +3722,83 @@ "node": ">=4" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", + "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -3768,6 +3964,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3883,6 +4088,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -3920,6 +4134,36 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -3940,6 +4184,40 @@ } }, "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "requires": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==" + }, + "@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + } + }, "@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -4182,6 +4460,11 @@ } } }, + "@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -4258,6 +4541,11 @@ } } }, + "@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==" + }, "@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -4276,6 +4564,11 @@ "@types/node": "*" } }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, "@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", @@ -4537,6 +4830,11 @@ "get-intrinsic": "^1.3.0" } }, + "call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4600,6 +4898,11 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, + "commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4711,6 +5014,14 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==" }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "requires": { + "esutils": "^2.0.2" + } + }, "dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -5399,6 +5710,21 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -5618,6 +5944,12 @@ "wrappy": "1" } }, + "openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "peer": true + }, "pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -6363,6 +6695,58 @@ "has-flag": "^3.0.0" } }, + "swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "requires": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "requires": { + "@apidevtools/swagger-parser": "10.0.3" + } + }, + "swagger-ui-dist": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", + "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "requires": { + "@scarf/scarf": "=1.4.0" + } + }, + "swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "requires": { + "swagger-ui-dist": ">=5.0.0" + } + }, "tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -6491,6 +6875,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6562,6 +6951,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==" + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6590,6 +6984,25 @@ "fd-slicer": "~1.1.0" } }, + "z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "dependencies": { + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true + } + } + }, "zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index f160caa..e99d48c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "express": "^4.18.2", "express-session": "^1.17.3", "pg": "^8.11.3", - "socket.io": "^4.8.1" + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 2f016e0..c1f1aad 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -1026,3 +1026,351 @@ body { gap: 1.5rem; } } + +/* ==================== ACHIEVEMENT STYLES ==================== */ + +.achievements-section { + margin-bottom: 3rem; +} + +.achievements-header { + text-align: center; + margin-bottom: 2rem; +} + +.achievements-header h2 { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(135deg, #ffd700, #ff6b35); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.5rem; +} + +.achievements-header p { + color: #8892b0; + font-size: 1.1rem; +} + +/* Achievement Stats */ +.achievement-stats { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; + justify-content: center; +} + +.achievement-stat { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 1rem; + padding: 1.5rem; + text-align: center; + min-width: 150px; + flex: 1; + max-width: 200px; +} + +.achievement-stat .stat-number { + font-size: 2rem; + font-weight: 700; + color: #00d4ff; + margin-bottom: 0.5rem; +} + +.achievement-stat .stat-label { + color: #8892b0; + font-size: 0.9rem; + font-weight: 500; +} + +/* Achievement Categories */ +.achievement-categories { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 1rem; + padding: 2rem; +} + +.category-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + flex-wrap: wrap; + justify-content: center; +} + +.category-tab { + background: transparent; + border: 1px solid #2a2a3e; + color: #8892b0; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.3s ease; + font-family: inherit; + font-size: 0.9rem; + font-weight: 500; +} + +.category-tab:hover { + background: #2a2a3e; + color: #ffffff; +} + +.category-tab.active { + background: linear-gradient(135deg, #00d4ff, #ff6b35); + border-color: transparent; + color: #ffffff; +} + +/* Achievements Grid */ +.achievements-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.achievement-card { + background: linear-gradient(135deg, #0f1419 0%, #1a1a2e 100%); + border: 1px solid #2a2a3e; + border-radius: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.achievement-card:hover { + transform: translateY(-2px); + border-color: #00d4ff; + box-shadow: 0 8px 25px rgba(0, 212, 255, 0.1); +} + +.achievement-card.completed { + border-color: #10b981; + background: linear-gradient(135deg, #064e3b 0%, #1a1a2e 100%); +} + +.achievement-card.completed:hover { + border-color: #10b981; + box-shadow: 0 8px 25px rgba(16, 185, 129, 0.1); +} + +.achievement-card.incomplete { + opacity: 0.7; +} + +.achievement-card.incomplete:hover { + opacity: 1; +} + +.achievement-icon { + font-size: 2.5rem; + margin-bottom: 1rem; + text-align: center; +} + +.achievement-content { + flex: 1; +} + +.achievement-name { + font-size: 1.2rem; + font-weight: 600; + color: #ffffff; + margin-bottom: 0.5rem; +} + +.achievement-description { + color: #8892b0; + font-size: 0.9rem; + line-height: 1.4; + margin-bottom: 1rem; +} + +.achievement-meta { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.achievement-points { + color: #ffd700; + font-weight: 600; + font-size: 0.9rem; +} + +.achievement-progress { + color: #00d4ff; + font-size: 0.8rem; + font-weight: 500; +} + +.achievement-status { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.2rem; +} + +/* Achievement Loading States */ +.achievements-loading { + text-align: center; + padding: 3rem; + color: #8892b0; +} + +.achievements-not-available { + text-align: center; + padding: 3rem; +} + +.not-available-content { + max-width: 400px; + margin: 0 auto; +} + +.not-available-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.not-available-content h3 { + color: #ffffff; + margin-bottom: 1rem; + font-size: 1.5rem; +} + +.not-available-content p { + color: #8892b0; + margin-bottom: 2rem; + line-height: 1.5; +} + +/* No Achievements State */ +.no-achievements { + text-align: center; + padding: 3rem; + color: #8892b0; + grid-column: 1 / -1; +} + +.no-achievements-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.no-achievements h3 { + color: #ffffff; + margin-bottom: 0.5rem; + font-size: 1.3rem; +} + +/* Achievement Notifications */ +.achievement-notification { + position: fixed; + top: 2rem; + right: 2rem; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #10b981; + border-radius: 1rem; + padding: 1.5rem; + box-shadow: 0 8px 25px rgba(16, 185, 129, 0.2); + z-index: 1000; + max-width: 350px; + animation: slideInRight 0.3s ease; +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.notification-content { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification-icon { + font-size: 2rem; +} + +.notification-text h4 { + color: #ffffff; + margin-bottom: 0.25rem; + font-size: 1.1rem; +} + +.notification-text p { + color: #8892b0; + font-size: 0.9rem; + margin: 0; +} + +.notification-close { + background: none; + border: none; + color: #8892b0; + font-size: 1.5rem; + cursor: pointer; + padding: 0; + margin-left: auto; + transition: color 0.3s ease; +} + +.notification-close:hover { + color: #ffffff; +} + +/* Mobile Responsiveness for Achievements */ +@media (max-width: 768px) { + .achievements-header h2 { + font-size: 2rem; + } + + .achievement-stats { + flex-direction: column; + align-items: center; + } + + .achievement-stat { + width: 100%; + max-width: 250px; + } + + .category-tabs { + flex-direction: column; + align-items: center; + } + + .category-tab { + width: 100%; + max-width: 200px; + text-align: center; + } + + .achievements-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .achievement-notification { + top: 1rem; + right: 1rem; + left: 1rem; + max-width: none; + } +} diff --git a/public/dashboard.html b/public/dashboard.html index 1bd8e2e..518ed75 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -120,6 +120,67 @@ + + +
Sammele Punkte und erreiche neue Meilensteine!
+Starte deine ersten Läufe, um Achievements zu sammeln!
+${achievement.description}
+ +Du hast ${newAchievements.length} neue Achievement${newAchievements.length > 1 ? 's' : ''} erhalten!
+