From de812a0fd16460a6b68851e6dfefdc8ba919ce73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Sun, 1 Mar 2026 12:58:11 +0100 Subject: [PATCH] archive legacy code, begin clean rewrite legacy branch preserves all prior code. Co-Authored-By: Claude Opus 4.6 --- .env.1password | 2 - .env.production.example | 16 - .gitignore | 8 +- ARCHITECTURE.md | 131 - GamePlaylist.io | 1 - GamePlaylistMaker | 1 - IMPLEMENTATION-SUMMARY.md | 285 -- QUICK-START.md | 318 -- README.md | 84 - app.js | 279 -- config.local.json.example | 23 - deploy.sh | 54 - docs/BLIZZARD-SETUP.md | 138 - docs/CLOUDFLARE-WORKERS-SETUP.md | 421 --- docs/FEATURES-OVERVIEW.md | 328 -- docs/GOG-SETUP.md | 144 - docs/IOS-WEB-STRATEGY.md | 172 - index.html | 20 - package-lock.json | 3655 -------------------- package.json | 34 - public/.htaccess | 19 - public/apple-touch-icon.png | Bin 3542 -> 0 bytes public/clear-storage.html | 20 - public/icon-192.png | Bin 3582 -> 0 bytes public/icon-512.png | Bin 9951 -> 0 bytes public/icon.svg | 23 - public/manifest.json | 30 - scripts/fetch-steam.mjs | 104 - scripts/steam-cli.mjs | 101 - scripts/test-api.mjs | 75 - scripts/test-backend.mjs | 54 - scripts/test-config-load.mjs | 28 - server/data/.gitkeep | 0 server/gog-api.mjs | 90 - server/gog-backend.mjs | 157 - server/igdb-cache.mjs | 225 -- server/index.js | 175 - server/package.json | 16 - server/steam-api.mjs | 59 - server/steam-backend.mjs | 55 - server/steam-backend.test.mjs | 104 - src/App.css | 5 - src/App.tsx | 84 - src/data/tutorials.ts | 210 -- src/main.tsx | 26 - src/pages/Discover/DiscoverPage.css | 241 -- src/pages/Discover/DiscoverPage.tsx | 296 -- src/pages/Home/HomePage.css | 23 - src/pages/Home/HomePage.tsx | 33 - src/pages/Library/LibraryPage.css | 81 - src/pages/Library/LibraryPage.tsx | 313 -- src/pages/Playlists/PlaylistDetailPage.css | 29 - src/pages/Playlists/PlaylistDetailPage.tsx | 281 -- src/pages/Playlists/PlaylistsPage.css | 11 - src/pages/Playlists/PlaylistsPage.tsx | 196 -- src/pages/Settings/SettingsDetailPage.css | 69 - src/pages/Settings/SettingsDetailPage.tsx | 607 ---- src/pages/Settings/SettingsPage.css | 3 - src/pages/Settings/SettingsPage.tsx | 68 - src/services/ConfigService.ts | 158 - src/services/Database.ts | 366 -- src/theme/variables.css | 13 - styles.css | 231 -- tsconfig.json | 22 - vite.config.ts | 41 - 65 files changed, 4 insertions(+), 10852 deletions(-) delete mode 100644 .env.1password delete mode 100644 .env.production.example delete mode 100644 ARCHITECTURE.md delete mode 160000 GamePlaylist.io delete mode 160000 GamePlaylistMaker delete mode 100644 IMPLEMENTATION-SUMMARY.md delete mode 100644 QUICK-START.md delete mode 100644 README.md delete mode 100644 app.js delete mode 100644 config.local.json.example delete mode 100755 deploy.sh delete mode 100644 docs/BLIZZARD-SETUP.md delete mode 100644 docs/CLOUDFLARE-WORKERS-SETUP.md delete mode 100644 docs/FEATURES-OVERVIEW.md delete mode 100644 docs/GOG-SETUP.md delete mode 100644 docs/IOS-WEB-STRATEGY.md delete mode 100644 index.html delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 public/.htaccess delete mode 100644 public/apple-touch-icon.png delete mode 100644 public/clear-storage.html delete mode 100644 public/icon-192.png delete mode 100644 public/icon-512.png delete mode 100644 public/icon.svg delete mode 100644 public/manifest.json delete mode 100644 scripts/fetch-steam.mjs delete mode 100644 scripts/steam-cli.mjs delete mode 100644 scripts/test-api.mjs delete mode 100644 scripts/test-backend.mjs delete mode 100644 scripts/test-config-load.mjs delete mode 100644 server/data/.gitkeep delete mode 100644 server/gog-api.mjs delete mode 100644 server/gog-backend.mjs delete mode 100644 server/igdb-cache.mjs delete mode 100644 server/index.js delete mode 100644 server/package.json delete mode 100644 server/steam-api.mjs delete mode 100644 server/steam-backend.mjs delete mode 100644 server/steam-backend.test.mjs delete mode 100644 src/App.css delete mode 100644 src/App.tsx delete mode 100644 src/data/tutorials.ts delete mode 100644 src/main.tsx delete mode 100644 src/pages/Discover/DiscoverPage.css delete mode 100644 src/pages/Discover/DiscoverPage.tsx delete mode 100644 src/pages/Home/HomePage.css delete mode 100644 src/pages/Home/HomePage.tsx delete mode 100644 src/pages/Library/LibraryPage.css delete mode 100644 src/pages/Library/LibraryPage.tsx delete mode 100644 src/pages/Playlists/PlaylistDetailPage.css delete mode 100644 src/pages/Playlists/PlaylistDetailPage.tsx delete mode 100644 src/pages/Playlists/PlaylistsPage.css delete mode 100644 src/pages/Playlists/PlaylistsPage.tsx delete mode 100644 src/pages/Settings/SettingsDetailPage.css delete mode 100644 src/pages/Settings/SettingsDetailPage.tsx delete mode 100644 src/pages/Settings/SettingsPage.css delete mode 100644 src/pages/Settings/SettingsPage.tsx delete mode 100644 src/services/ConfigService.ts delete mode 100644 src/services/Database.ts delete mode 100644 src/theme/variables.css delete mode 100644 styles.css delete mode 100644 tsconfig.json delete mode 100644 vite.config.ts diff --git a/.env.1password b/.env.1password deleted file mode 100644 index c0feca9..0000000 --- a/.env.1password +++ /dev/null @@ -1,2 +0,0 @@ -TWITCH_CLIENT_ID=op://Private/WhatToPlay/TWITCH_CLIENT_ID -TWITCH_CLIENT_SECRET=op://Private/WhatToPlay/TWITCH_CLIENT_SECRET diff --git a/.env.production.example b/.env.production.example deleted file mode 100644 index f77cf8c..0000000 --- a/.env.production.example +++ /dev/null @@ -1,16 +0,0 @@ -# Backend URL (wo läuft dein Express Server?) -# Uberspace / eigenes Backend -VITE_API_URL=https://your-username.uber.space - -# GitHub Pages (wenn du GitHub Pages nutzt, aber Uberspace Backend) -# VITE_API_URL=https://your-username.uber.space - -# Lokales Backend (für Development mit separatem Backend) -# VITE_API_URL=http://localhost:3000 - -# Base Path (für URLs und Routing) -# GitHub Pages deployment: -# VITE_BASE_PATH=/whattoplay/ - -# Uberspace deployment (root): -# VITE_BASE_PATH=/ diff --git a/.gitignore b/.gitignore index 6826dc7..8d5f239 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,7 @@ node_modules .env .env.* !.env.*.example -!.env.1password -*.secret.* -*.key -*.pem +!.env.example # IGDB cache (generated at runtime) server/data/igdb-cache.json @@ -23,3 +20,6 @@ coverage # Logs *.log npm-debug.log* + +# bun +bun.lock diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index a3e1c5e..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,131 +0,0 @@ -# WhatToPlay - Architektur Entscheidung - -## Problem: Gaming Platform APIs für iOS/Web - -### Services Status: - -- ✅ **Steam**: Öffentliche Web API (`GetOwnedGames`) - funktioniert im Browser/iOS -- ⚠️ **GOG**: Galaxy Library API - benötigt OAuth (Server-Side Token Exchange) -- ❌ **Epic Games**: Keine öffentliche API - nur über Legendary CLI (Python) -- ❌ **Amazon Games**: Keine öffentliche API - nur über Nile CLI (Python) - -### Warum CLI-Tools nicht funktionieren: - -``` -❌ Python/Node CLI Tools (Legendary, Nile, gogdl) - └─> Benötigen native Runtime - └─> Funktioniert NICHT auf iOS - └─> Funktioniert NICHT im Browser - └─> Funktioniert NICHT als reine Web-App -``` - -## Lösung: Hybrid-Architektur - -### Phase 1: MVP (Jetzt) - -``` -Frontend (React/Ionic) - ↓ -Steam Web API (direkt) - - GetOwnedGames Endpoint - - Keine Auth nötig (nur API Key) - - Funktioniert im Browser -``` - -### Phase 2: GOG Integration (wenn Backend da ist) - -``` -Frontend (React/Ionic) - ↓ -Backend (Vercel Function / Cloudflare Worker) - ↓ -GOG Galaxy API - - OAuth Token Exchange (Server-Side) - - Library API mit Bearer Token - - CORS-Safe -``` - -### Phase 3: Epic/Amazon (Zukunft) - -**Option A: Backend Proxy** - -``` -Frontend → Backend → Epic GraphQL (Reverse-Engineered) - → Amazon Nile API -``` - -**Option B: Manuelle Import-Funktion** - -``` -User exportiert Library aus Epic/Amazon - ↓ -User uploaded JSON in App - ↓ -App parsed und zeigt an -``` - -## Aktuelle Implementation - -### Steam (✅ Funktioniert jetzt) - -```javascript -// fetch-steam.mjs -const response = await fetch( - `http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/`, - { params: { key, steamid, format: "json" } }, -); -``` - -### GOG (⚠️ Vorbereitet, braucht Backend) - -```javascript -// Jetzt: Manueller Token aus Browser DevTools -// Später: OAuth Flow über Backend -const response = await fetch( - `https://galaxy-library.gog.com/users/${userId}/releases`, - { headers: { Authorization: `Bearer ${token}` } }, -); -``` - -### Epic/Amazon (❌ Placeholder) - -```javascript -// Aktuell: Leere JSON-Dateien als Platzhalter -// Später: Backend-Integration oder manuelle Import-Funktion -``` - -## Deployment Strategie - -### Development (macOS - Jetzt) - -``` -npm run fetch → Lokale Node.js Scripts holen Daten -npm run dev → Vite Dev Server mit Hot Reload -``` - -### Production (iOS/Web - Später) - -``` -Frontend: Vercel/Netlify (Static React App) -Backend: Vercel Functions (für GOG OAuth) -Data: Supabase/Firebase (für User Libraries) -``` - -## Nächste Schritte - -1. ✅ **Steam**: Fertig implementiert -2. 🔄 **GOG**: Manuelle Token-Eingabe (Development) -3. 📝 **Epic/Amazon**: Placeholder JSON -4. 🚀 **Backend**: OAuth-Service für GOG (Vercel Function) -5. 📱 **iOS**: PWA mit Service Worker für Offline-Support - -## Wichtige Limitierungen - -- **Keine nativen CLI-Tools** in Production -- **CORS** blockiert direkte Browser → Gaming APIs -- **OAuth Secrets** können nicht im Browser gespeichert werden -- **Backend ist Pflicht** für GOG/Epic/Amazon - ---- - -**Fazit**: Für iOS/Web müssen wir ein Backend bauen. Steam funktioniert ohne Backend, GOG/Epic/Amazon brauchen Server-Side OAuth. diff --git a/GamePlaylist.io b/GamePlaylist.io deleted file mode 160000 index b9e8b6d..0000000 --- a/GamePlaylist.io +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b9e8b6d19c2dd87baa5bba1e8eaa5130f1dc21d2 diff --git a/GamePlaylistMaker b/GamePlaylistMaker deleted file mode 160000 index f695642..0000000 --- a/GamePlaylistMaker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f695642da9f0052f6dc4e84b6aeb79d519b5dfe4 diff --git a/IMPLEMENTATION-SUMMARY.md b/IMPLEMENTATION-SUMMARY.md deleted file mode 100644 index 04d3626..0000000 --- a/IMPLEMENTATION-SUMMARY.md +++ /dev/null @@ -1,285 +0,0 @@ -# IMPLEMENTATION SUMMARY - Februar 2026 - -## ✅ Was wurde implementiert - -### 1. Settings-Tab mit vollständiger Konfiguration - -- **UI Component**: `src/pages/Settings/SettingsPage.tsx` -- **Styling**: `src/pages/Settings/SettingsPage.css` -- **Features**: - - ✅ Separate Karten für jeden Gaming-Service - - ✅ Input-Felder für API Keys, IDs, Tokens (sicher - mit `type="password"`) - - ✅ Dropdown-Selektoren (z.B. Blizzard Region) - - ✅ Config Export/Import (JSON Download/Upload) - - ✅ "Alle Einstellungen löschen" Button - - ✅ Responsive Design für iOS/Web - -### 2. Integriertes Tutorial-System - -- **Component**: `src/components/TutorialModal.tsx` -- **Coverage**: 5 Services (Steam, GOG, Epic, Amazon, Blizzard) -- **Pro Service**: 4-6 Schritte + Tipps -- **Features**: - - ✅ Step-by-Step Guides mit Code-Beispielen - - ✅ Hinweise und Warnung-Boxen - - ✅ Links zu offiziellen Dokumentationen - - ✅ Modal-Dialog (nicht inline) - -### 3. ConfigService - Sichere Speicherung - -- **Service**: `src/services/ConfigService.ts` -- **Storage-Backend**: - - ✅ localStorage (schnell, 5-10MB) - - ✅ IndexedDB (Backup, 50MB+) - - ✅ Export/Import Funktionen -- **Validierung**: Prüft auf erforderliche Felder -- **Sicherheit**: Keine Verschlüsselung (würde Usability schaden) - -### 4. Blizzard API Integration - -- **Importer**: `scripts/fetch-blizzard.mjs` -- **OAuth-Flow**: Client Credentials (Token Exchange) -- **Unterstützte Games**: - - World of Warcraft - - Diablo III (Heroes) - - Diablo IV - - Overwatch 2 - - StarCraft II - - Heroes of the Storm - - Hearthstone -- **Data**: Level, Class, Kills, Hardcore Flag, Last Updated - -### 5. Cloudflare Workers Dokumentation - -- **Datei**: `docs/CLOUDFLARE-WORKERS-SETUP.md` -- **Coverage**: - - ✅ GOG OAuth Worker (Complete) - - ✅ Blizzard OAuth Worker (Complete) - - ✅ Deployment Instructions - - ✅ Security Best Practices - - ✅ KV Store Setup - - ✅ Debugging Guide - -### 6. App Navigation Update - -- **File**: `src/App.tsx` -- **Änderung**: Settings-Tab hinzugefügt (#5 von 5) -- **Icon**: `settingsOutline` von ionicons - -### 7. Dokumentation & Guides - -- **QUICK-START.md**: 5-Minuten Einstieg -- **BLIZZARD-SETUP.md**: OAuth Konfiguration -- **FEATURES-OVERVIEW.md**: Gesamtübersicht -- **CLOUDFLARE-WORKERS-SETUP.md**: Backend Deployment -- **config.local.json.example**: Config Template - ---- - -## 📊 Code Statistics - -| Komponente | Zeilen | Komplexität | -| --------------------------- | ------ | -------------------- | -| SettingsPage.tsx | 380 | Mittel | -| TutorialModal.tsx | 420 | Mittel | -| ConfigService.ts | 140 | Einfach | -| fetch-blizzard.mjs | 180 | Mittel | -| CLOUDFLARE-WORKERS-SETUP.md | 450 | Hoch (Dokumentation) | - -**Gesamt neue Code**: ~1.570 Zeilen - ---- - -## 🎯 Architektur-Entscheidungen - -### localStorage + IndexedDB Hybrid - -``` -Warum? - • localStorage: Schnell, einfach, < 5MB - • IndexedDB: Großer Storage, Backup-ready - • Beide Client-Side = Offline-Ready -``` - -### Cloudflare Workers statt Vercel Functions - -``` -Warum? - • Zero Configuration (vs. Vercel config) - • KV Store integriert (vs. external DB) - • Better Edge Performance (distributed) - • Free tier ist großzügig - • Secrets natürlich geschützt -``` - -### Client Credentials Flow (nicht Authorization Code) - -``` -Warum? - • Blizzard erlaubt nur Client Credentials - • Keine User Consent nötig - • Einfacher OAuth Flow - • Secretmanagement einfacher -``` - ---- - -## 🔒 Sicherheit - -### ✅ Implementiert - -- Client Secrets in Backend nur (Cloudflare KV Store) -- Token Export/Import mit Warnung -- Password Input Fields (verborgen) -- CORS auf Cloudflare Worker konfigurierbar -- State Parameter für CSRF (in Worker) - -### ⚠️ Bewusst NICHT implementiert - -- Token Verschlüsselung in localStorage (UX Impact) -- 2FA für Settings (Overkill für MVP) -- Audit Logs (später, wenn selbst-gehostet) -- Rate Limiting (kommt auf Server-Side) - -**Reasoning**: MVP-Fokus auf Usability, nicht auf Enterprise-Security - ---- - -## 📈 Performance - -| Metrik | Wert | Note | -| ------------------- | ------ | --------------------- | -| Settings Load | <10ms | localStorage nur | -| Config Save | <1ms | IndexedDB async | -| Tutorial Modal Open | <50ms | React render | -| Export (1000 Games) | <200ms | JSON stringify | -| Import (1000 Games) | <500ms | JSON parse + validate | - ---- - -## 🚀 Deployment Readiness - -### Frontend (Vite) - -``` -Status: ✅ Production-Ready -npm run build → dist/ -Deployment: Vercel, Netlify, GitHub Pages -CORS: Handled via Cloudflare Worker -``` - -### Backend (Cloudflare Workers) - -``` -Status: ⚠️ Dokumentiert, nicht deployed -Bedarf: - 1. Cloudflare Account (kostenlos) - 2. GOG Client ID + Secret - 3. Blizzard Client ID + Secret - 4. npx wrangler deploy -``` - -### Data Storage - -``` -Frontend: localStorage + IndexedDB -Backend: Cloudflare KV Store (für Secrets) -Optional: Supabase für Cloud-Sync -``` - ---- - -## 📋 Noch zu tun für Production - -### Sofort (< 1 Woche) - -- [ ] Cloudflare Worker deployen -- [ ] GOG/Blizzard Credentials besorgen -- [ ] KV Store konfigurieren -- [ ] CORS testen - -### Bald (1-2 Wochen) - -- [ ] Epic Games JSON Import UI -- [ ] Amazon Games JSON Import UI -- [ ] Token Refresh Logic -- [ ] Error Boundary Components - -### Later (2-4 Wochen) - -- [ ] Home-Page Widgets -- [ ] Playlists Feature -- [ ] Discover/Tinder UI -- [ ] PWA Service Worker - -### Optional (4+ Wochen) - -- [ ] Cloud-Sync (Supabase) -- [ ] Native iOS App (React Native) -- [ ] Social Features (Friends) -- [ ] Recommendations Engine - ---- - -## 🎓 Lernpunkte - -### OAuth Flows - -- ✅ Client Credentials (Blizzard) -- ⚠️ Authorization Code (GOG, dokumentiert) -- ❌ PKCE (zukünftig für Web) - -### Storage Patterns - -- ✅ Single Source of Truth (ConfigService) -- ✅ Backup + Restore (IndexedDB) -- ✅ Export/Import (JSON) - -### Component Design - -- ✅ Data-Driven Tutorials (TUTORIALS Objekt) -- ✅ Observable Pattern (setState + Service) -- ✅ Modal System (TutorialModal) - -### Infrastructure - -- ✅ Serverless (Cloudflare) -- ✅ No Database (localStorage MVP) -- ✅ Secret Management (KV Store) - ---- - -## 📚 Referenzen - -### Services & APIs - -- [Steam Web API](https://developer.valvesoftware.com/wiki/Steam_Web_API) -- [GOG Galaxy API](https://galaxy-library.gog.com/) -- [Blizzard OAuth](https://develop.battle.net/documentation/guides/using-oauth) -- [Cloudflare Workers](https://developers.cloudflare.com/workers/) - -### Tech Stack - -- React 18.2 + TypeScript -- Ionic React (iOS Mode) -- Vite 5.0 -- Cloudflare Workers - ---- - -## 🎉 Ergebnis - -**Komplette, produktionsreife Konfigurationsseite mit:** - -- ✅ 5 Gaming-Services -- ✅ Integriertes Tutorial-System -- ✅ Sichere Speicherung -- ✅ Export/Import Funktionalität -- ✅ Zero Infrastructure Backend (Cloudflare) -- ✅ iOS/Web kompatibel -- ✅ Offline funktional -- ✅ Umfassende Dokumentation - -**Zeitaufwand**: ~2-3 Stunden -**Code-Qualität**: Production-Ready -**Dokumentation**: Exzellent diff --git a/QUICK-START.md b/QUICK-START.md deleted file mode 100644 index 7f31f0c..0000000 --- a/QUICK-START.md +++ /dev/null @@ -1,318 +0,0 @@ -# WhatToPlay - Quick Start Guide - -## 🚀 Schnelleinstieg (5 Minuten) - -### 1. App öffnen - -```bash -cd /Users/felixfoertsch/Developer/whattoplay -npm run dev -# Opens: http://localhost:5173 -``` - -### 2. Settings-Tab öffnen - -``` -Navbar unten rechts → "Einstellungen" Tab -``` - -### 3. Steam integrieren (optional, funktioniert sofort) - -``` -Settings Tab - ↓ -Karte "🎮 Steam" - ↓ -"?" Button → Tutorial Modal - ↓ -Folge den 6 Schritten: - 1. https://steamcommunity.com/dev/apikey - 2. Login & Accept ToS - 3. API Key kopieren - 4. https://www.steamcommunity.com/ - 5. Auf Namen klicken - 6. Steam ID aus URL kopieren (z.B. 76561197960434622) - ↓ -Eintragen → Speichern - ↓ -Library Tab → 1103 Games erscheinen! -``` - ---- - -## 🎮 Für jeden Service - -### Steam ✅ (Funktioniert JETZT) - -``` -Difficulty: ⭐ Einfach -Time: 5 Minuten -Status: Voll funktionsfähig -``` - -### GOG ⚠️ (Funktioniert JETZT mit manuelem Token) - -``` -Difficulty: ⭐⭐ Mittel -Time: 10 Minuten -Status: Development-ready -Step: Tutorial → Browser DevTools → Token kopieren -``` - -### Blizzard ⚠️ (Funktioniert JETZT mit Credentials) - -``` -Difficulty: ⭐⭐ Mittel -Time: 10 Minuten -Status: Development-ready -Step: Docs → OAuth → Client ID + Secret -``` - -### Epic Games ⚠️ (Später, mit Backend) - -``` -Difficulty: ⭐⭐⭐ Schwer -Time: 30+ Minuten -Status: Needs Cloudflare Worker -Step: Warte auf Backend OAuth Proxy -``` - -### Amazon Games ⚠️ (Später, mit Backend) - -``` -Difficulty: ⭐⭐⭐ Schwer -Time: 30+ Minuten -Status: Needs Cloudflare Worker -Step: Warte auf Backend OAuth Proxy -``` - ---- - -## 💾 Config Management - -### Export (Backup machen) - -``` -Settings Tab - ↓ -"📦 Daten-Management" - ↓ -"Config exportieren" - ↓ -whattoplay-config.json herunterladen - ↓ -(WARNUNG: Enthält sensitive Daten! Sicher lagern!) -``` - -### Import (Von anderem Device) - -``` -Settings Tab - ↓ -"📦 Daten-Management" - ↓ -"Config importieren" - ↓ -whattoplay-config.json auswählen - ↓ -✓ Alles wiederhergestellt! -``` - ---- - -## 🐛 Häufige Probleme - -### "Keine Games angezeigt" - -``` -1. Settings-Tab überprüfen -2. Alle Felder gefüllt? ✓ -3. Library-Tab laden lassen (30 Sekunden) -4. Browser-Konsole öffnen (F12) → Fehler checken -``` - -### "Steam ID nicht gültig" - -``` -❌ Richtig: 76561197960434622 (lange Nummer) -❌ Falsch: felixfoertsch (Name/Community ID) - -→ Gehe zu https://www.steamcommunity.com/ -→ Öffne dein Profil -→ URL ist: /profiles/76561197960434622/ -→ Diese Nummer kopieren! -``` - -### "GOG Token abgelaufen" - -``` -Tokens laufen nach ~24h ab - -→ Settings Tab -→ GOG Karte -→ Neuer Token aus Browser (Follow Tutorial) -→ Speichern -``` - -### "Blizzard sagt 'invalid client'" - -``` -1. Client ID/Secret überprüfen -2. Battle.net Developer Portal: - https://develop.battle.net -3. "My Applications" öffnen -4. Correct Credentials kopieren -``` - ---- - -## 📱 Auf dem iPhone nutzen - -### Option 1: Web App (Empfohlen) - -``` -1. iPhone Safari -2. Gehe zu https://whattoplay.vercel.app (später) -3. Teilen → Home Screen hinzufügen -4. App sieht aus wie native App! -``` - -### Option 2: Localhost (Development) - -``` -1. iPhone und Computer im gleichen WiFi -2. Computer IP: 192.168.x.x -3. iPhone Safari: 192.168.x.x:5173 -4. Funktioniert auch ohne Internet (offline!) -``` - ---- - -## 🔄 Workflow zum Hinzufügen neuer Games - -``` -1. Spiel auf Steam/GOG/Epic spielen -2. Settings speichern (automatisch täglich?) -3. Library Tab öffnen -4. Neue Spiele erscheinen -5. Click auf Spiel → Details -6. Zu Playlist hinzufügen (später) -``` - ---- - -## 🎯 MVP vs. Production - -### MVP (Jetzt, February 2026) - -- ✅ Steam funktioniert perfekt -- ✅ Settings-Tab mit Tutorials -- ✅ GOG/Blizzard Development-ready -- ⚠️ Epic/Amazon nur placeholder -- ✅ Config Export/Import -- ✅ Offline funktional (localStorage) - -### Production (März+ 2026) - -- Cloudflare Worker deployen -- GOG/Blizzard OAuth automatisch -- Epic/Amazon manueller Import -- Home-Page Widgets -- Playlists Feature -- PWA + iOS App - ---- - -## 📚 Dokumentation - -| Datei | Inhalt | -| ------------------------------------------------------------ | -------------------- | -| [FEATURES-OVERVIEW.md](./FEATURES-OVERVIEW.md) | Was gibt es neues? | -| [CLOUDFLARE-WORKERS-SETUP.md](./CLOUDFLARE-WORKERS-SETUP.md) | Backend deployen | -| [BLIZZARD-SETUP.md](./BLIZZARD-SETUP.md) | Blizzard OAuth | -| [GOG-SETUP.md](./GOG-SETUP.md) | GOG Token extraction | -| [IOS-WEB-STRATEGY.md](./IOS-WEB-STRATEGY.md) | Gesamtstrategie | -| [ARCHITECTURE.md](./ARCHITECTURE.md) | Technische Details | - ---- - -## 💡 Pro Tipps - -### Mehrere Accounts gleichzeitig - -``` -Browser-Profile nutzen: - ↓ -Chrome/Firefox: Neue Person/Profil - ↓ -Unterschiedliche config.local.json je Profil - ↓ -Vergleiche deine Bibliothek mit Freunden! -``` - -### Spiele schneller finden - -``` -Library Tab - ↓ -Suchleiste (zukünftig): - - Nach Titel suchen - - Nach Plattform filtern - - Nach Länge sortieren -``` - -### Offline Modus - -``` -1. Settings speichern (einmalig online) -2. Dann brauchst du kein Internet mehr -3. Daten in localStorage gespeichert -4. Auf dem Flugzeug spielen? ✓ Funktioniert! -``` - ---- - -## 🚀 Nächste Schritte für dich - -### Sofort testen - -```bash -npm run dev -# → Settings Tab → Steam Tutorial folgen -``` - -### In 1 Woche - -``` -- GOG oder Blizzard einrichten -- Config exportieren -- Alle Games konsolidiert sehen -``` - -### In 2 Wochen - -``` -- Cloudflare Worker aufsetzen -- OAuth automatisieren -- Epic/Amazon hinzufügen (einfacher) -``` - ---- - -## ❓ Fragen? - -Siehe `docs/` Ordner für detaillierte Guides: - -``` -docs/ - ├── FEATURES-OVERVIEW.md (Was gibt es neues?) - ├── CLOUDFLARE-WORKERS-SETUP.md (Zero-Infra Backend) - ├── BLIZZARD-SETUP.md (Blizzard OAuth) - ├── GOG-SETUP.md (GOG Token) - ├── IOS-WEB-STRATEGY.md (Gesamtvision) - └── ARCHITECTURE.md (Tech Details) -``` - ---- - -**Viel Spaß mit WhatToPlay! 🎮** diff --git a/README.md b/README.md deleted file mode 100644 index 24ebf9e..0000000 --- a/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# WhatToPlay - Game Library Manager - -Eine PWA zum Verwalten deiner Spielebibliotheken von Steam, GOG, Epic, und mehr. - -## Features - -- 📚 Alle Spiele an einem Ort -- 🎮 Steam, GOG, Epic Games, Battle.net Integration -- 📱 PWA - funktioniert auf iPhone, Android, Desktop -- 🔒 Daten bleiben lokal (IndexedDB) -- ⚡ Schnelle Tinder-Style Entdeckung - -## Deployment - -Die App läuft komplett auf Uberspace (~5€/Monat): -- **Frontend**: PWA (statische Files) -- **Backend**: Node.js Express Server (CORS-Proxy für Steam API) -- **URL**: https://wtp.uber.space - -Details zum Deployment siehe [UBERSPACE.md](UBERSPACE.md). - -## Steam API Integration - -### 1. Steam API Key bekommen - -1. Gehe zu https://steamcommunity.com/dev/apikey -2. Akzeptiere die Terms -3. Domain: `localhost` (wird ignoriert) -4. Kopiere deinen API Key - -### 2. Steam ID finden - -Option A: Steam Profil URL nutzen -- `https://steamcommunity.com/id/DEINNAME/` → ID ist `DEINNAME` - -Option B: SteamID Finder -- https://steamid.io/ - -### 3. In der App konfigurieren - -1. Öffne https://wtp.uber.space -2. Gehe zu **Settings → Steam** -3. Füge **Steam API Key** und **Steam ID** hinzu -4. Klicke auf **Refresh** → Deine Spiele werden geladen! 🎉 - -## Architektur - -``` -PWA (wtp.uber.space) - ↓ POST /api/steam/refresh -Express Backend (wtp.uber.space:3000) - ↓ Forward mit API Key -Steam Web API - ↓ Games List -Backend → PWA → IndexedDB -``` - -## Local Development - -```bash -npm install -npm run dev -``` - -Der Dev-Server nutzt Vite-Middleware für API-Calls, kein separates Backend nötig. - -## Weitere Plattformen - -- **GOG**: OAuth Flow (geplant) -- **Epic Games**: Manueller Import (kein Public API) -- **Battle.net**: OAuth Flow (geplant) - -## Tech Stack - -- React + TypeScript -- Ionic Framework (Mobile UI) -- IndexedDB (lokale Persistenz) -- Vite (Build Tool) -- Node.js Express (Backend) -- Uberspace (Hosting) - -## License - -MIT diff --git a/app.js b/app.js deleted file mode 100644 index 63765f3..0000000 --- a/app.js +++ /dev/null @@ -1,279 +0,0 @@ -const sourcesConfigUrl = "./data/sources.json"; - -const state = { - allGames: [], - mergedGames: [], - search: "", - sourceFilter: "all", - sortBy: "title", - sources: [], -}; - -const ui = { - grid: document.getElementById("gamesGrid"), - summary: document.getElementById("summary"), - searchInput: document.getElementById("searchInput"), - sourceFilter: document.getElementById("sourceFilter"), - sortSelect: document.getElementById("sortSelect"), - refreshButton: document.getElementById("refreshButton"), - template: document.getElementById("gameCardTemplate"), -}; - -const normalizeTitle = (title) => - title.toLowerCase().replace(/[:\-]/g, " ").replace(/\s+/g, " ").trim(); - -const toDateValue = (value) => (value ? new Date(value).getTime() : 0); - -const mergeGames = (games) => { - const map = new Map(); - - games.forEach((game) => { - const key = game.canonicalId || normalizeTitle(game.title); - const entry = map.get(key) || { - title: game.title, - canonicalId: key, - platforms: new Set(), - sources: [], - tags: new Set(), - lastPlayed: null, - playtimeHours: 0, - }; - - entry.platforms.add(game.platform); - game.tags?.forEach((tag) => entry.tags.add(tag)); - entry.sources.push({ - name: game.source, - id: game.id, - url: game.url, - platform: game.platform, - }); - - if ( - game.lastPlayed && - (!entry.lastPlayed || game.lastPlayed > entry.lastPlayed) - ) { - entry.lastPlayed = game.lastPlayed; - } - - if (Number.isFinite(game.playtimeHours)) { - entry.playtimeHours += game.playtimeHours; - } - - map.set(key, entry); - }); - - return Array.from(map.values()).map((entry) => ({ - ...entry, - platforms: Array.from(entry.platforms), - tags: Array.from(entry.tags), - })); -}; - -const sortGames = (games, sortBy) => { - const sorted = [...games]; - sorted.sort((a, b) => { - if (sortBy === "lastPlayed") { - return toDateValue(b.lastPlayed) - toDateValue(a.lastPlayed); - } - if (sortBy === "platforms") { - return b.platforms.length - a.platforms.length; - } - return a.title.localeCompare(b.title, "de"); - }); - return sorted; -}; - -const filterGames = () => { - const query = state.search.trim().toLowerCase(); - let filtered = [...state.mergedGames]; - - if (state.sourceFilter !== "all") { - filtered = filtered.filter((game) => - game.sources.some((source) => source.name === state.sourceFilter), - ); - } - - if (query) { - filtered = filtered.filter((game) => { - const haystack = [ - game.title, - ...game.platforms, - ...game.tags, - ...game.sources.map((source) => source.name), - ] - .join(" ") - .toLowerCase(); - return haystack.includes(query); - }); - } - - return sortGames(filtered, state.sortBy); -}; - -const renderSummary = (games) => { - const totalGames = state.mergedGames.length; - const totalSources = state.sources.length; - const duplicates = state.allGames.length - state.mergedGames.length; - const totalPlaytime = state.allGames.reduce( - (sum, game) => sum + (game.playtimeHours || 0), - 0, - ); - - ui.summary.innerHTML = [ - { - label: "Konsolidierte Spiele", - value: totalGames, - }, - { - label: "Quellen", - value: totalSources, - }, - { - label: "Zusammengeführte Duplikate", - value: Math.max(duplicates, 0), - }, - { - label: "Gesamte Spielzeit (h)", - value: totalPlaytime.toFixed(1), - }, - ] - .map( - (item) => ` -
-

${item.label}

-

${item.value}

-
- `, - ) - .join(""); -}; - -const renderGames = (games) => { - ui.grid.innerHTML = ""; - - games.forEach((game) => { - const card = ui.template.content.cloneNode(true); - card.querySelector(".title").textContent = game.title; - card.querySelector(".badge").textContent = - `${game.platforms.length} Plattformen`; - card.querySelector(".meta").textContent = game.lastPlayed - ? `Zuletzt gespielt: ${new Date(game.lastPlayed).toLocaleDateString("de")}` - : "Noch nicht gespielt"; - - const tagList = card.querySelector(".tag-list"); - game.tags.slice(0, 4).forEach((tag) => { - const span = document.createElement("span"); - span.className = "tag"; - span.textContent = tag; - tagList.appendChild(span); - }); - - if (!game.tags.length) { - const span = document.createElement("span"); - span.className = "tag"; - span.textContent = "Ohne Tags"; - tagList.appendChild(span); - } - - const sources = card.querySelector(".sources"); - game.sources.forEach((source) => { - const item = document.createElement("div"); - item.className = "source-item"; - const name = document.createElement("span"); - name.textContent = source.name; - const details = document.createElement("p"); - details.textContent = `${source.platform} · ${source.id}`; - item.append(name, details); - sources.appendChild(item); - }); - - ui.grid.appendChild(card); - }); -}; - -const populateSourceFilter = () => { - ui.sourceFilter.innerHTML = ''; - state.sources.forEach((source) => { - const option = document.createElement("option"); - option.value = source.name; - option.textContent = source.label; - ui.sourceFilter.appendChild(option); - }); -}; - -const updateUI = () => { - const filtered = filterGames(); - renderSummary(filtered); - renderGames(filtered); -}; - -const loadSources = async () => { - const response = await fetch(sourcesConfigUrl); - if (!response.ok) { - throw new Error("Konnte sources.json nicht laden."); - } - - const config = await response.json(); - state.sources = config.sources; - - const data = await Promise.all( - config.sources.map(async (source) => { - const sourceResponse = await fetch(source.file); - if (!sourceResponse.ok) { - throw new Error(`Konnte ${source.file} nicht laden.`); - } - const list = await sourceResponse.json(); - return list.map((game) => ({ - ...game, - source: source.name, - platform: game.platform || source.platform, - })); - }), - ); - - state.allGames = data.flat(); - state.mergedGames = mergeGames(state.allGames); -}; - -const attachEvents = () => { - ui.searchInput.addEventListener("input", (event) => { - state.search = event.target.value; - updateUI(); - }); - - ui.sourceFilter.addEventListener("change", (event) => { - state.sourceFilter = event.target.value; - updateUI(); - }); - - ui.sortSelect.addEventListener("change", (event) => { - state.sortBy = event.target.value; - updateUI(); - }); - - ui.refreshButton.addEventListener("click", async () => { - ui.refreshButton.disabled = true; - ui.refreshButton.textContent = "Lade ..."; - try { - await loadSources(); - populateSourceFilter(); - updateUI(); - } finally { - ui.refreshButton.disabled = false; - ui.refreshButton.textContent = "Daten neu laden"; - } - }); -}; - -const init = async () => { - try { - await loadSources(); - populateSourceFilter(); - attachEvents(); - updateUI(); - } catch (error) { - ui.grid.innerHTML = `
${error.message}
`; - } -}; - -init(); diff --git a/config.local.json.example b/config.local.json.example deleted file mode 100644 index d9fb353..0000000 --- a/config.local.json.example +++ /dev/null @@ -1,23 +0,0 @@ -{ - "steam": { - "apiKey": "YOUR_STEAM_API_KEY", - "steamId": "YOUR_STEAM_ID" - }, - "gog": { - "userId": "", - "accessToken": "" - }, - "epic": { - "email": "", - "method": "manual" - }, - "amazon": { - "email": "", - "method": "manual" - }, - "blizzard": { - "clientId": "", - "clientSecret": "", - "region": "eu" - } -} diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index a5d7ab4..0000000 --- a/deploy.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SERVER="wtp" -REMOTE_HTML="~/html/" -REMOTE_SERVER="~/whattoplay/server/" -ENV_FILE=".env.1password" - -echo "=== WhatToPlay Deploy ===" - -# 1. Build frontend -echo "" -echo "[1/5] Building frontend..." -npm run build - -# 2. Deploy frontend -echo "" -echo "[2/5] Deploying frontend..." -rsync -avz --delete dist/ "$SERVER:$REMOTE_HTML" - -# 3. Deploy backend -echo "" -echo "[3/5] Deploying backend..." -rsync -avz --delete \ - --exclude node_modules \ - --exclude data/igdb-cache.json \ - server/ "$SERVER:$REMOTE_SERVER" - -# 4. Install backend dependencies on server -echo "" -echo "[4/5] Installing backend dependencies..." -ssh "$SERVER" "cd $REMOTE_SERVER && npm install --production" - -# 5. Inject secrets from 1Password and restart -echo "" -echo "[5/5] Updating secrets and restarting service..." - -TWITCH_CLIENT_ID=$(op read "op://Private/WhatToPlay/TWITCH_CLIENT_ID") -TWITCH_CLIENT_SECRET=$(op read "op://Private/WhatToPlay/TWITCH_CLIENT_SECRET") - -ssh "$SERVER" "cat > ~/whattoplay.env << 'ENVEOF' -PORT=3000 -ALLOWED_ORIGIN=https://wtp.uber.space -TWITCH_CLIENT_ID=$TWITCH_CLIENT_ID -TWITCH_CLIENT_SECRET=$TWITCH_CLIENT_SECRET -ENVEOF -chmod 600 ~/whattoplay.env" - -ssh "$SERVER" "systemctl --user restart whattoplay" - -echo "" -echo "=== Deploy complete ===" -echo "Frontend: https://wtp.uber.space" -echo "Backend: https://wtp.uber.space/api/health" diff --git a/docs/BLIZZARD-SETUP.md b/docs/BLIZZARD-SETUP.md deleted file mode 100644 index 8145e39..0000000 --- a/docs/BLIZZARD-SETUP.md +++ /dev/null @@ -1,138 +0,0 @@ -# Blizzard Setup für WhatToPlay - -## API OAuth Konfiguration - -### 1. Battle.net Developer Portal öffnen - -- Gehe zu https://develop.battle.net -- Melde dich mit deinem Battle.net Account an - -### 2. Application registrieren - -- Klicke auf "Create Application" -- Name: "WhatToPlay" (oder dein Projektname) -- Website: https://whattoplay.local (für Development) -- Beschreibung: "Game Library Manager" -- Akzeptiere die ToS - -### 3. OAuth Credentials kopieren - -Nach der Registrierung siehst du: - -- **Client ID** - die öffentliche ID -- **Client Secret** - HALTEN Sie geheim! (Nur auf Server, nie im Browser!) - -### 4. Redirect URI setzen - -In deiner Application Settings: - -``` -Redirect URIs: -https://whattoplay-oauth.workers.dev/blizzard/callback (Production) -http://localhost:3000/auth/callback (Development) -``` - ---- - -## config.local.json Setup - -```json -{ - "blizzard": { - "clientId": "your_client_id_here", - "clientSecret": "your_client_secret_here", - "region": "eu" - } -} -``` - -### Region Codes: - -- `us` - North America -- `eu` - Europe -- `kr` - Korea -- `tw` - Taiwan - ---- - -## Blizzard Games, die unterstützt werden - -1. **World of Warcraft** - Character-basiert -2. **Diablo III** - Hero-basiert -3. **Diablo IV** - Charakter-basiert -4. **Overwatch 2** - Account-basiert -5. **Starcraft II** - Campaign Progress -6. **Heroes of the Storm** - Character-basiert -7. **Hearthstone** - Deck-basiert - ---- - -## Development vs Production - -### Development (Lokal) - -```bash -# Teste mit lokalem Token -npm run import - -# Script verwendet config.local.json -``` - -### Production (Mit Cloudflare Worker) - -``` -Frontend → Cloudflare Worker → Blizzard OAuth - ↓ - Token Exchange - (Client Secret sicher!) -``` - -Siehe: [CLOUDFLARE-WORKERS-SETUP.md](./CLOUDFLARE-WORKERS-SETUP.md) - ---- - -## Troubleshooting - -### "Client ID invalid" - -- Überprüfe dass die Client ID korrekt kopiert wurde -- Stelle sicher dass du im Development Portal angemeldet bist - -### "Redirect URI mismatch" - -- Die Redirect URI muss exakt übereinstimmen -- Beachte Protocol (https vs http) -- Beachte Port-Nummern - -### "No games found" - -- Dein Account muss mindestens 1 Blizzard Game haben -- Bei Diablo III: Character muss erstellt sein -- Charaktere können bis zu 24h brauchen zum Erscheinen - -### Token-Fehler in Production - -- Client Secret ist abgelaufen → Neu generieren -- Überprüfe Cloudflare Worker Logs: - ```bash - npx wrangler tail whattoplay-blizzard - ``` - ---- - -## Sicherheit - -🔒 **Wichtig:** - -- **Client Secret** NIEMALS ins Frontend committen -- Nutze Cloudflare KV Store oder Environment Variables -- Token mit Ablaufdatum (expires_in) prüfen -- Token nicht in Browser LocalStorage speichern (nur Session) - ---- - -## Links - -- [Battle.net Developer Portal](https://develop.battle.net) -- [Blizzard OAuth Documentation](https://develop.battle.net/documentation/guides/using-oauth) -- [Game Data APIs](https://develop.battle.net/documentation/guides/game-data-apis) diff --git a/docs/CLOUDFLARE-WORKERS-SETUP.md b/docs/CLOUDFLARE-WORKERS-SETUP.md deleted file mode 100644 index 9b6ea39..0000000 --- a/docs/CLOUDFLARE-WORKERS-SETUP.md +++ /dev/null @@ -1,421 +0,0 @@ -# Cloudflare Workers - Serverless OAuth Proxy - -**Zero Infrastruktur, alles gekapselt** - So funktioniert der Proxy für GOG und Blizzard OAuth Flows. - ---- - -## 🎯 Überblick - -Statt auf einem eigenen Server zu hosten, nutzen wir **Cloudflare Workers** als serverless FaaS (Function as a Service): - -``` -WhatToPlay Frontend Cloudflare Worker GOG/Blizzard API - ↓ ↓ ↓ -[Settings speichern] → [OAuth Token Exchange] ← [Bearer Token zurück] -[API aufrufen] → [Token validieren] -``` - -**Vorteile:** - -- ✅ Keine Server zu verwalten -- ✅ Kein Backend-Hosting nötig -- ✅ Client Secrets geschützt (Server-Side) -- ✅ Kostenlos bis 100.000 Anfragen/Tag -- ✅ Überall deployed (weltweit verteilt) -- ✅ Automatische CORS-Konfiguration - ---- - -## 📋 Setup Anleitung - -### 1. Cloudflare Account erstellen - -```bash -# Gehe zu https://dash.cloudflare.com -# Registriere dich kostenfrei -# Du brauchst keine Domain für Workers! -``` - -### 2. Wrangler installieren (CLI Tool) - -```bash -npm install -D wrangler -npx wrangler login -``` - -### 3. Projekt initialisieren - -```bash -cd whattoplay -npx wrangler init workers -# oder für bestehendes Projekt: -# npx wrangler init whattoplay-oauth --type javascript -``` - ---- - -## 🔐 GOG OAuth Worker - -### Create `workers/gog-auth.js`: - -```javascript -/** - * GOG OAuth Proxy for WhatToPlay - * Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/gog/callback - */ - -const GOG_CLIENT_ID = "your_client_id"; -const GOG_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store (nicht in Code!) -const GOG_REDIRECT_URI = - "https://whattoplay-oauth.your-domain.workers.dev/gog/callback"; - -export default { - async fetch(request) { - const url = new URL(request.url); - - // CORS Headers - const headers = { - "Access-Control-Allow-Origin": "https://whattoplay.local", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }; - - // Preflight - if (request.method === "OPTIONS") { - return new Response(null, { headers }); - } - - // 1. Initiiere OAuth Flow - if (url.pathname === "/gog/authorize") { - const authUrl = new URL("https://auth.gog.com/auth"); - authUrl.searchParams.append("client_id", GOG_CLIENT_ID); - authUrl.searchParams.append("redirect_uri", GOG_REDIRECT_URI); - authUrl.searchParams.append("response_type", "code"); - authUrl.searchParams.append("layout", "client2"); - - return new Response(null, { - status: 302, - headers: { Location: authUrl.toString() }, - }); - } - - // 2. Callback Handler - if (url.pathname === "/gog/callback") { - const code = url.searchParams.get("code"); - if (!code) { - return new Response("Missing authorization code", { - status: 400, - }); - } - - try { - // Token Exchange (Server-Side!) - const tokenResponse = await fetch("https://auth.gog.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: GOG_CLIENT_ID, - client_secret: GOG_CLIENT_SECRET, // 🔒 Sicher! - grant_type: "authorization_code", - code: code, - redirect_uri: GOG_REDIRECT_URI, - }), - }); - - const tokenData = await tokenResponse.json(); - - // Redirect zurück zur App mit Token - const appRedirect = `https://whattoplay.local/#/settings?gog_token=${tokenData.access_token}&gog_user=${tokenData.user_id}`; - - return new Response(null, { - status: 302, - headers: { Location: appRedirect }, - }); - } catch (error) { - return new Response(`Token Error: ${error.message}`, { - status: 500, - }); - } - } - - // 3. Token Validation - if (url.pathname === "/gog/validate") { - const authHeader = request.headers.get("Authorization"); - if (!authHeader) { - return new Response("Missing Authorization", { - status: 401, - }); - } - - const token = authHeader.replace("Bearer ", ""); - - try { - const response = await fetch( - "https://galaxy-library.gog.com/users/me", - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - - if (response.ok) { - const data = await response.json(); - return new Response(JSON.stringify({ valid: true, user: data }), { - headers, - }); - } - return new Response(JSON.stringify({ valid: false }), { - headers, - }); - } catch (error) { - return new Response( - JSON.stringify({ valid: false, error: error.message }), - { - headers, - }, - ); - } - } - - return new Response("Not Found", { status: 404 }); - }, -}; -``` - -### `wrangler.toml` Config: - -```toml -name = "whattoplay-oauth" -main = "src/index.js" -compatibility_date = "2024-01-01" - -# KV Store für Secrets -[[kv_namespaces]] -binding = "SECRETS" -id = "your_kv_namespace_id" -preview_id = "your_preview_kv_id" - -# Environment Variables (Secrets!) -[env.production] -vars = { ENVIRONMENT = "production" } - -[env.production.secrets] -GOG_CLIENT_SECRET = "your_client_secret" -BLIZZARD_CLIENT_SECRET = "your_client_secret" -``` - ---- - -## 🎮 Blizzard OAuth Worker - -### Create `workers/blizzard-auth.js`: - -```javascript -/** - * Blizzard OAuth Proxy for WhatToPlay - * Läuft auf: https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback - */ - -const BLIZZARD_CLIENT_ID = "your_client_id"; -const BLIZZARD_CLIENT_SECRET = "your_client_secret"; // 🔒 KV Store! -const BLIZZARD_REDIRECT_URI = - "https://whattoplay-oauth.your-domain.workers.dev/blizzard/callback"; - -export default { - async fetch(request) { - const url = new URL(request.url); - - const headers = { - "Access-Control-Allow-Origin": "https://whattoplay.local", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }; - - if (request.method === "OPTIONS") { - return new Response(null, { headers }); - } - - // 1. Authorize - if (url.pathname === "/blizzard/authorize") { - const state = crypto.randomUUID(); - const authUrl = new URL("https://oauth.battle.net/authorize"); - authUrl.searchParams.append("client_id", BLIZZARD_CLIENT_ID); - authUrl.searchParams.append("redirect_uri", BLIZZARD_REDIRECT_URI); - authUrl.searchParams.append("response_type", "code"); - authUrl.searchParams.append("state", state); - - return new Response(null, { - status: 302, - headers: { Location: authUrl.toString() }, - }); - } - - // 2. Callback - if (url.pathname === "/blizzard/callback") { - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - - if (!code) { - return new Response("Missing authorization code", { - status: 400, - }); - } - - try { - const tokenResponse = await fetch("https://oauth.battle.net/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: BLIZZARD_CLIENT_ID, - client_secret: BLIZZARD_CLIENT_SECRET, // 🔒 Sicher! - grant_type: "authorization_code", - code: code, - redirect_uri: BLIZZARD_REDIRECT_URI, - }), - }); - - if (!tokenResponse.ok) { - throw new Error(`Token request failed: ${tokenResponse.status}`); - } - - const tokenData = await tokenResponse.json(); - - // Redirect zurück - const appRedirect = `https://whattoplay.local/#/settings?blizzard_token=${tokenData.access_token}`; - - return new Response(null, { - status: 302, - headers: { Location: appRedirect }, - }); - } catch (error) { - return new Response(`Error: ${error.message}`, { - status: 500, - }); - } - } - - return new Response("Not Found", { status: 404 }); - }, -}; -``` - ---- - -## 🚀 Deployment - -### 1. Deploy zu Cloudflare - -```bash -npx wrangler deploy workers/gog-auth.js --name whattoplay-gog -npx wrangler deploy workers/blizzard-auth.js --name whattoplay-blizzard -``` - -### 2. Custom Domain (optional) - -```bash -# Wenn du einen Domain hast, verbinde Cloudflare: -# https://dash.cloudflare.com → Workers Routes - -# Beispiel: -# Domain: api.whattoplay.com -# Worker: whattoplay-oauth -# Route: api.whattoplay.com/gog/* -``` - -### 3. Secrets hinzufügen - -```bash -# GOG Secret -echo "your_gog_secret" | npx wrangler secret put GOG_CLIENT_SECRET --name whattoplay-gog - -# Blizzard Secret -echo "your_blizzard_secret" | npx wrangler secret put BLIZZARD_CLIENT_SECRET --name whattoplay-blizzard -``` - ---- - -## 🔗 Frontend Integration - -In `SettingsPage.tsx`: - -```typescript -// Button für GOG OAuth Login -const handleGogOAuth = () => { - const workerUrl = "https://whattoplay-oauth.workers.dev/gog/authorize"; - window.location.href = workerUrl; -}; - -// Callback mit URL-Parametern -const handleOAuthCallback = () => { - const params = new URLSearchParams(window.location.hash.split("?")[1]); - const token = params.get("gog_token"); - const userId = params.get("gog_user"); - - if (token) { - handleSaveConfig("gog", { - accessToken: token, - userId: userId, - }); - // Token ist jetzt gespeichert in localStorage - } -}; -``` - ---- - -## 📊 Kosten (Februar 2026) - -| Service | Free Tier | Kosten | -| ------------------ | ------------ | ---------------------- | -| Cloudflare Workers | 100k req/Tag | $0.50 pro 10M Anfragen | -| KV Store | 3GB Storage | $0.50 pro GB | -| Bandwidth | Unlimited | Keine Zusatzkosten | - -**Beispiel:** 1.000 Users, je 10 Tokens/Monat = 10.000 Anfragen = **Kostenlos** 🎉 - ---- - -## 🔒 Security Best Practices - -### ✅ Was wir tun: - -- Client Secrets in KV Store (nicht im Code) -- Token Exchange Server-Side -- CORS nur für unsere Domain -- State Parameter für CSRF Protection -- Keine Tokens in URLs speichern (Session nur) - -### ❌ Was wir NICHT tun: - -- Client Secrets hardcoden -- Tokens in localStorage ohne Verschlüsselung -- CORS für alle Origins -- Tokens in Browser Console anzeigen - ---- - -## 🐛 Debugging - -```bash -# Logs anschauen -npx wrangler tail whattoplay-gog - -# Local testen -npx wrangler dev workers/gog-auth.js -# Öffne dann: http://localhost:8787/gog/authorize -``` - ---- - -## 📚 Links - -- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) -- [Wrangler CLI Guide](https://developers.cloudflare.com/workers/wrangler/) -- [KV Store Reference](https://developers.cloudflare.com/workers/runtime-apis/kv/) -- [GOG OAuth Docs](https://gogapidocs.readthedocs.io/) -- [Blizzard OAuth Docs](https://develop.battle.net/documentation/guides/using-oauth) diff --git a/docs/FEATURES-OVERVIEW.md b/docs/FEATURES-OVERVIEW.md deleted file mode 100644 index 826d1d6..0000000 --- a/docs/FEATURES-OVERVIEW.md +++ /dev/null @@ -1,328 +0,0 @@ -# WhatToPlay - Feature-Übersicht (Februar 2026) - -## 🆕 Neue Features - -### 1️⃣ Settings-Tab mit Konfiguration - -**Pfad**: `src/pages/Settings/SettingsPage.tsx` - -``` -Settings-Tab - ├── 🎮 Steam Integration - │ ├── API Key Input (verborgen) - │ ├── Steam ID Input - │ └── Tutorial-Button (✨ Step-by-Step Anleitung) - │ - ├── 🌐 GOG Integration - │ ├── User ID Input - │ ├── Access Token Input (verborgen) - │ └── Tutorial für Token-Extraction - │ - ├── ⚙️ Epic Games - │ ├── E-Mail Input - │ ├── Import-Methode (Manual oder OAuth) - │ └── ℹ️ Info: Keine öffentliche API - │ - ├── 🔶 Amazon Games - │ ├── E-Mail Input - │ ├── Import-Methode (Manual oder OAuth) - │ └── Ähnlich wie Epic - │ - ├── ⚔️ Blizzard Entertainment - │ ├── Client ID Input (verborgen) - │ ├── Client Secret Input (verborgen) - │ ├── Region Selector (US/EU/KR/TW) - │ └── Tutorial-Button - │ - └── 📦 Daten-Management - ├── Config Exportieren (JSON Download) - ├── Config Importieren (JSON Upload) - └── Alle Einstellungen löschen -``` - -### 2️⃣ Integriertes Tutorial-System - -**Pfad**: `src/components/TutorialModal.tsx` - -Jeder Service hat sein eigenes Step-by-Step Tutorial: - -``` -Tutorial Modal - ├── Steam - │ ├── API Key generieren - │ ├── Steam ID finden - │ └── 6 Schritte mit Screenshots-Links - │ - ├── GOG - │ ├── Browser DevTools öffnen - │ ├── Bearer Token kopieren - │ └── 5 Schritte mit Code-Beispiele - │ - ├── Epic Games - │ ├── Account-Setup - │ ├── JSON Export erklären - │ └── 4 Schritte, einfach - │ - ├── Amazon Games - │ ├── Prime Gaming aktivieren - │ ├── Luna erklärt - │ └── 4 Schritte - │ - └── Blizzard - ├── Developer Portal - ├── OAuth Credentials - └── 6 Schritte detailliert -``` - -### 3️⃣ ConfigService - Sichere Speicherung - -**Pfad**: `src/services/ConfigService.ts` - -```typescript -ConfigService - ├── loadConfig() - Lade aus localStorage - ├── saveConfig() - Speichere in localStorage - ├── exportConfig() - Download als JSON - ├── importConfig() - Upload aus JSON - ├── backupToIndexedDB() - Redundante Speicherung - ├── restoreFromIndexedDB() - Aus Backup zurück - ├── validateConfig() - Prüfe auf Fehler - └── clearConfig() - Alles löschen -``` - -**Speicher-Strategie:** - -- ✅ localStorage für schnellen Zugriff -- ✅ IndexedDB für Backup & Encryption-Ready -- ✅ Keine Tokens in localStorage ohne Verschlüsselung -- ✅ Export/Import für Cloud-Sync - -### 4️⃣ Blizzard API Integration - -**Pfad**: `scripts/fetch-blizzard.mjs` - -``` -Supported Games: - • World of Warcraft - • Diablo III (Heroes) - • Diablo IV - • Overwatch 2 - • StarCraft II - • Heroes of the Storm - • Hearthstone - -Data: - • Character Name - • Level - • Class - • Hardcore Flag - • Elite Kills - • Experience - • Last Updated -``` - -### 5️⃣ Cloudflare Workers Setup (Serverless) - -**Pfad**: `docs/CLOUDFLARE-WORKERS-SETUP.md` - -``` -Zero Infrastructure Deployment: - - Frontend (Vercel/Netlify) - ↓ - Cloudflare Workers (Serverless) - ↓ - OAuth Callbacks + Token Exchange - ↓ - GOG Galaxy Library API - Blizzard Battle.net API - Epic Games (später) - Amazon Games (später) - -✨ Benefits: - • Keine Server zu verwalten - • Kostenlos bis 100k req/Tag - • Client Secrets geschützt (Server-Side) - • CORS automatisch konfiguriert - • Weltweit verteilt -``` - ---- - -## 📁 Neue Dateien - -| Datei | Beschreibung | Status | -| ------------------------------------- | --------------------------- | ------ | -| `src/pages/Settings/SettingsPage.tsx` | Settings-Tab mit Formularen | ✅ | -| `src/pages/Settings/SettingsPage.css` | Styling | ✅ | -| `src/components/TutorialModal.tsx` | Tutorial-System | ✅ | -| `src/services/ConfigService.ts` | Konfiguration speichern | ✅ | -| `scripts/fetch-blizzard.mjs` | Blizzard API Importer | ✅ | -| `docs/CLOUDFLARE-WORKERS-SETUP.md` | Worker Dokumentation | ✅ | -| `docs/BLIZZARD-SETUP.md` | Blizzard OAuth Guide | ✅ | -| `config.local.json.example` | Config Template | ✅ | - ---- - -## 🔄 Workflow für Nutzer - -### Erste Nutzung: - -``` -1. App öffnen → Settings-Tab -2. Auf "?" Button klicken → Tutorial Modal -3. Step-by-Step folgen -4. Credentials eingeben -5. "Speichern" klicken → localStorage -6. Daten werden automatisch synced -``` - -### Daten importieren: - -``` -1. Settings-Tab → "Config importieren" -2. Datei auswählen (whattoplay-config.json) -3. Credentials werden wiederhergestellt -4. Alle APIs neu abfragen -``` - -### Daten exportieren: - -``` -1. Settings-Tab → "Config exportieren" -2. JSON-Datei downloaded -3. Kann auf anderem Device importiert werden -4. Oder als Backup gespeichert -``` - ---- - -## 🚀 Nächste Schritte - -### Phase 1: Production Ready (Jetzt) - -- [x] Steam Integration -- [x] Settings-Tab -- [x] Blizzard OAuth -- [x] Cloudflare Worker Setup (dokumentiert) - -### Phase 2: Backend Deployment (1-2 Wochen) - -- [ ] Cloudflare Worker deployen -- [ ] GOG OAuth Callback -- [ ] Blizzard OAuth Callback -- [ ] Token Encryption in KV Store - -### Phase 3: Import Features (2-4 Wochen) - -- [ ] Epic Games JSON Import UI -- [ ] Amazon Games JSON Import UI -- [ ] Drag & Drop Upload -- [ ] Validierung - -### Phase 4: Polish (4+ Wochen) - -- [ ] Home-Page Widgets -- [ ] Playlists Feature -- [ ] Discover/Tinder UI -- [ ] PWA Setup -- [ ] iOS Testing - ---- - -## 📊 Statistiken - -| Metric | Wert | -| --------------------------- | -------------------------------------- | -| Unterstützte Plattformen | 5 (Steam, GOG, Epic, Amazon, Blizzard) | -| Settings-Formulare | 5 | -| Tutorial-Schritte | 30+ | -| Cloudflare Worker Functions | 3 (GOG, Blizzard, Validation) | -| API Endpoints | 15+ | -| LocalStorage Capacity | 5-10MB | -| IndexedDB Capacity | 50MB+ | - ---- - -## 💡 Design Patterns - -### Konfiguration speichern (Observable Pattern) - -```typescript -// SettingsPage.tsx -const [config, setConfig] = useState({}); - -const handleSaveConfig = (service: keyof ServiceConfig, data: any) => { - const updated = { ...config, [service]: { ...config[service], ...data } }; - setConfig(updated); - ConfigService.saveConfig(updated); // → localStorage - // Optional: ConfigService.backupToIndexedDB(updated); // → Backup -}; -``` - -### Tutorial System (Data-Driven) - -```typescript -// TutorialModal.tsx - Alle Tutorials in TUTORIALS Objekt -const TUTORIALS: Record = { - steam: { ... }, - gog: { ... }, - // Einfach zu erweitern! -}; -``` - -### OAuth Flow mit Cloudflare Worker - -``` -Frontend initiiert: - ↓ -Worker erhält Callback: - ↓ -Token Exchange Server-Side: - ↓ -Frontend erhält Token in URL: - ↓ -ConfigService speichert Token: - ↓ -Nächster API Call mit Token -``` - ---- - -## 🔐 Sicherheit - -### ✅ Best Practices implementiert: - -- Client Secrets in Backend nur (Cloudflare KV) -- Tokens mit Session-Speicher (nicht persistent) -- Export/Import mit Warnung -- Validation der Credentials -- CORS nur für eigene Domain -- State Parameter für CSRF - -### ❌ Nicht implementiert (wäre Overkill): - -- Token-Verschlüsselung in localStorage (würde Komplexität erhöhen) -- 2FA für Settings -- Audit Logs -- Rate Limiting (kommt auf Server-Side) - ---- - -## 🎯 Gesamtziel - -**Zero Infrastructure, Full-Featured:** - -- Frontend: Statisch deployed (Vercel/Netlify) -- Backend: Serverless (Cloudflare Workers) -- Datenbank: Optional (Supabase/Firebase) -- Secrets: KV Store oder Environment Variables -- **Kosten**: ~$0/Monat für < 1000 User - -Nutzer kann: - -- ✅ Alle Credentials selbst eingeben -- ✅ Daten jederzeit exportieren/importieren -- ✅ Offline mit LocalStorage arbeiten -- ✅ Auf iOS/Web/Desktop gleiches UI -- ✅ Keine zusätzlichen Apps nötig diff --git a/docs/GOG-SETUP.md b/docs/GOG-SETUP.md deleted file mode 100644 index 1bba248..0000000 --- a/docs/GOG-SETUP.md +++ /dev/null @@ -1,144 +0,0 @@ -# GOG Integration - Development Setup - -## ⚠️ Wichtig: Temporäre Lösung für Development - -Da wir eine **Web/iOS App** bauen, können wir keine CLI-Tools (wie `gogdl`) nutzen. -Für Production brauchen wir ein **Backend mit OAuth Flow**. - -## Wie bekomme ich GOG Credentials? - -### Option 1: Manuell aus Browser (Development) - -1. **Öffne GOG.com (eingeloggt)** - - ``` - https://www.gog.com - ``` - -2. **Öffne Browser DevTools** - - Chrome/Edge: `F12` oder `Cmd+Option+I` (Mac) - - Firefox: `F12` - -3. **Gehe zu Network Tab** - - Klicke auf "Network" / "Netzwerk" - - Aktiviere "Preserve log" / "Log beibehalten" - -4. **Lade eine GOG Seite neu** - - Z.B. deine Library: `https://www.gog.com/account` - -5. **Finde Request mit Bearer Token** - - Suche nach Requests zu `gog.com` oder `galaxy-library.gog.com` - - Klicke auf einen Request - - Gehe zu "Headers" Tab - - Kopiere den `Authorization: Bearer ...` Token - -6. **Kopiere User ID** - - Suche nach Request zu `embed.gog.com/userData.json` - - Im Response findest du `"galaxyUserId": "123456789..."` - - Kopiere diese ID - -7. **Trage in config.local.json ein** - ```json - { - "steam": { ... }, - "epic": {}, - "gog": { - "userId": "DEINE_GALAXY_USER_ID", - "accessToken": "DEIN_BEARER_TOKEN" - } - } - ``` - -### Option 2: Backend OAuth Flow (Production - TODO) - -Für Production implementieren wir einen OAuth Flow: - -```javascript -// Backend Endpoint (z.B. Vercel Function) -export async function POST(request) { - // 1. User zu GOG Auth redirecten - const authUrl = `https://auth.gog.com/auth?client_id=...&redirect_uri=...`; - - // 2. Callback mit Code - // 3. Code gegen Access Token tauschen - const token = await fetch("https://auth.gog.com/token", { - method: "POST", - body: { code, client_secret: process.env.GOG_SECRET }, - }); - - // 4. Token sicher speichern (z.B. encrypted in DB) - return { success: true }; -} -``` - -## API Endpoints - -### GOG Galaxy Library - -``` -GET https://galaxy-library.gog.com/users/{userId}/releases -Headers: - Authorization: Bearer {accessToken} - User-Agent: WhatToPlay/1.0 - -Response: -{ - "items": [ - { - "external_id": "1207658930", - "platform_id": "gog", - "date_created": 1234567890, - ... - } - ], - "total_count": 123, - "next_page_token": "..." -} -``` - -### GOG User Data - -``` -GET https://embed.gog.com/userData.json -Headers: - Authorization: Bearer {accessToken} - -Response: -{ - "userId": "...", - "galaxyUserId": "...", - "username": "...", - ... -} -``` - -## Token Lebensdauer - -- GOG Tokens laufen nach **ca. 1 Stunde** ab -- Für Development: Token regelmäßig neu kopieren -- Für Production: Refresh Token Flow implementieren - -## Nächste Schritte - -1. ✅ Development: Manueller Token aus Browser -2. 📝 Backend: Vercel Function für OAuth -3. 🔐 Backend: Token Refresh implementieren -4. 📱 iOS: Secure Storage für Tokens (Keychain) - -## Troubleshooting - -### `401 Unauthorized` - -- Token abgelaufen → Neu aus Browser kopieren -- Falscher Token → Prüfe `Authorization: Bearer ...` - -### `CORS Error` - -- Normal im Browser (darum brauchen wir Backend) -- Development: Scripts laufen in Node.js (kein CORS) -- Production: Backend macht die Requests - -### Leere Library - -- Prüfe `userId` - muss `galaxyUserId` sein, nicht `userId` -- Prüfe API Endpoint - `/users/{userId}/releases`, nicht `/games` diff --git a/docs/IOS-WEB-STRATEGY.md b/docs/IOS-WEB-STRATEGY.md deleted file mode 100644 index 0f76109..0000000 --- a/docs/IOS-WEB-STRATEGY.md +++ /dev/null @@ -1,172 +0,0 @@ -# WhatToPlay - iOS/Web Strategie - -## ✅ Was funktioniert JETZT - -### Steam Integration (Voll funktionsfähig) - -```javascript -// ✅ Öffentliche Web API - funktioniert im Browser/iOS -const response = await fetch( - "http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/", - { - params: { - key: "YOUR_STEAM_API_KEY", - steamid: "YOUR_STEAM_ID", - format: "json", - }, - }, -); -``` - -**Status**: 1103 Games erfolgreich importiert ✅ - ---- - -## ⚠️ Was BACKEND braucht - -### GOG Integration - -**Problem**: OAuth Token Exchange geht nicht im Browser (CORS + Secrets) - -**Development-Lösung** (jetzt): - -1. Öffne https://www.gog.com (eingeloggt) -2. Browser DevTools → Network → Kopiere Bearer Token -3. Trage in `config.local.json` ein - -**Production-Lösung** (später): - -``` -Frontend → Backend (Vercel Function) → GOG OAuth - → GOG Galaxy Library API -``` - -**Siehe**: [docs/GOG-SETUP.md](./GOG-SETUP.md) - ---- - -### Epic Games Integration - -**Problem**: Keine öffentliche API, nur CLI-Tool (Legendary) - -**Optionen**: - -1. ❌ Legendary CLI → Funktioniert nicht auf iOS -2. ⚠️ Backend mit Epic GraphQL → Reverse-Engineered, gegen ToS -3. ✅ Manuelle Import-Funktion → User uploaded JSON - -**Empfehlung**: Manuelle Import-Funktion für MVP - ---- - -### Amazon Games Integration - -**Problem**: Keine öffentliche API, nur CLI-Tool (Nile) - -**Status**: Gleiche Situation wie Epic -**Empfehlung**: Später, wenn Epic funktioniert - ---- - -## 🎯 MVP Strategie (iOS/Web Ready) - -### Phase 1: Steam Only (✅ Fertig) - -``` -React/Ionic App - ↓ -Steam Web API (direkt vom Browser) - ↓ -1103 Games imported -``` - -### Phase 2: GOG mit Backend (🔜 Next) - -``` -React/Ionic App - ↓ -Vercel Function (OAuth Proxy) - ↓ -GOG Galaxy Library API -``` - -### Phase 3: Epic/Amazon Import (📝 TODO) - -``` -React/Ionic App - ↓ -User uploaded JSON - ↓ -Parse & Display -``` - ---- - -## 🚀 Deployment Plan - -### Frontend (iOS/Web) - -- **Hosting**: Vercel / Netlify (Static React App) -- **PWA**: Service Worker für Offline-Support -- **iOS**: Add to Home Screen (keine App Store App) - -### Backend (nur für GOG/Epic OAuth) - -- **Option 1**: Vercel Serverless Functions -- **Option 2**: Cloudflare Workers -- **Option 3**: Supabase Edge Functions - -### Datenbank (optional) - -- **Option 1**: localStorage (nur Client-Side) -- **Option 2**: Supabase (für Cloud-Sync) -- **Option 3**: Firebase Firestore - ---- - -## ❓ FAQ - -### Warum kein Python/CLI auf iOS? - -iOS erlaubt keine nativen Binaries in Web-Apps. Nur JavaScript im Browser oder Swift in nativer App. - -### Warum brauchen wir ein Backend? - -OAuth Secrets können nicht sicher im Browser gespeichert werden (jeder kann den Source-Code sehen). CORS blockiert direkte API-Calls. - -### Kann ich die App ohne Backend nutzen? - -Ja! Steam funktioniert ohne Backend. GOG/Epic brauchen aber Backend oder manuelle Imports. - -### Wie sicher sind die Tokens? - -- **Development**: Tokens in `config.local.json` (nicht in Git!) -- **Production**: Tokens im Backend, verschlüsselt in DB -- **iOS**: Tokens im Keychain (nativer secure storage) - ---- - -## 📋 Checklist - -- [x] Steam API Integration -- [x] React/Ionic UI Setup -- [x] Tab Navigation (Home, Library, Playlists, Discover, **Settings**) -- [x] Game Consolidation (Duplicates merging) -- [x] Blizzard API Integration -- [x] Settings-Tab mit Tutorials -- [x] ConfigService (localStorage + IndexedDB) -- [ ] GOG OAuth Backend (Cloudflare Worker) -- [ ] Epic Import-Funktion (JSON Upload) -- [ ] PWA Setup (Service Worker) -- [ ] iOS Testing (Add to Home Screen) -- [ ] Cloud-Sync (optional) - ---- - -## 🔗 Nützliche Links - -- [Steam Web API Docs](https://developer.valvesoftware.com/wiki/Steam_Web_API) -- [GOG Galaxy API](https://galaxy-library.gog.com/) -- [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (Referenz-Implementation) -- [Ionic React Docs](https://ionicframework.com/docs/react) -- [PWA Guide](https://web.dev/progressive-web-apps/) diff --git a/index.html b/index.html deleted file mode 100644 index 6c534cc..0000000 --- a/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - WhatToPlay - - -
- - - diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f2bb99c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3655 +0,0 @@ -{ - "name": "whattoplay", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "whattoplay", - "version": "0.0.0", - "dependencies": { - "@ionic/react": "^8.0.0", - "@ionic/react-router": "^8.0.0", - "@react-spring/web": "^9.7.5", - "ionicons": "^7.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "^5.3.4", - "react-router-dom": "^5.3.4", - "react-tinder-card": "^1.6.4" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/react-router": "^5.1.20", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^4.2.1", - "typescript": "^5.3.3", - "vite": "^5.0.0", - "wrangler": "^4.63.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", - "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz", - "integrity": "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": "^1.20260115.0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260205.0.tgz", - "integrity": "sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260205.0.tgz", - "integrity": "sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260205.0.tgz", - "integrity": "sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260205.0.tgz", - "integrity": "sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260205.0.tgz", - "integrity": "sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@ionic/core": { - "version": "8.7.17", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.17.tgz", - "integrity": "sha512-gp7PIEJX27NX/FkjiUlpjQUtJiFFE5W1lofRC5CfORQ8p4PrLh9wJO9EJH0YryCr2qZS0k47sYgRQP5FwiXlpg==", - "license": "MIT", - "dependencies": { - "@stencil/core": "4.38.0", - "ionicons": "^8.0.13", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/@ionic/core/node_modules/ionicons": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz", - "integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==", - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.35.3" - } - }, - "node_modules/@ionic/react": { - "version": "8.7.17", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.17.tgz", - "integrity": "sha512-t/ApHBEigSTvovM/hKtNAMrddoOQ5l2GlyjOzASUq7sJLvDS4ewDMk5pRahjGqmFSYSN8TIBlF9QAHswp6XTRg==", - "license": "MIT", - "dependencies": { - "@ionic/core": "8.7.17", - "ionicons": "^8.0.13", - "tslib": "*" - }, - "peerDependencies": { - "react": ">=16.8.6", - "react-dom": ">=16.8.6" - } - }, - "node_modules/@ionic/react-router": { - "version": "8.7.17", - "resolved": "https://registry.npmjs.org/@ionic/react-router/-/react-router-8.7.17.tgz", - "integrity": "sha512-kSkFNNA5m0vgnzpvWU9PDwJNHdEYqD9THpEGFh5aSM/pENvs59qlo5ziQQ5MMWy21EgKCGa045VmaO2D/5tF6g==", - "license": "MIT", - "dependencies": { - "@ionic/react": "8.7.17", - "tslib": "*" - }, - "peerDependencies": { - "react": ">=16.8.6", - "react-dom": ">=16.8.6", - "react-router": "^5.0.1", - "react-router-dom": "^5.0.1" - } - }, - "node_modules/@ionic/react/node_modules/ionicons": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz", - "integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==", - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.35.3" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", - "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", - "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/@stencil/core": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz", - "integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.10.0" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "4.34.9", - "@rollup/rollup-darwin-x64": "4.34.9", - "@rollup/rollup-linux-arm64-gnu": "4.34.9", - "@rollup/rollup-linux-arm64-musl": "4.34.9", - "@rollup/rollup-linux-x64-gnu": "4.34.9", - "@rollup/rollup-linux-x64-musl": "4.34.9", - "@rollup/rollup-win32-arm64-msvc": "4.34.9", - "@rollup/rollup-win32-x64-msvc": "4.34.9" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true, - "license": "MIT" - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "dev": true, - "license": "ISC" - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/ionicons": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", - "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", - "license": "MIT", - "dependencies": { - "@stencil/core": "^4.0.3" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/miniflare": { - "version": "4.20260205.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260205.0.tgz", - "integrity": "sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.18.2", - "workerd": "1.20260205.0", - "ws": "8.18.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "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/p-sleep": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-sleep/-/p-sleep-1.1.0.tgz", - "integrity": "sha512-bwP3GKZirBUYMtiUuBrheLUQdRXVeE/pmHOaLpNJzNfAD4b5AjDn6l823brXcQFade4G/g7GMNQ3KV86E8EaEw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "license": "MIT", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-tinder-card": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/react-tinder-card/-/react-tinder-card-1.6.4.tgz", - "integrity": "sha512-IC6YXoBZ+51jm7XsT8i+8G/ov8rvAob+kBRdp9unQyjsLc7jmuYb1cNfu95Q3mdFDgwE0AzTIyl1o2Klm61+aQ==", - "license": "MIT", - "dependencies": { - "p-sleep": "^1.1.0" - }, - "peerDependencies": { - "@react-spring/native": "^9.5.5", - "@react-spring/web": "^9.5.5", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@react-spring/native": { - "optional": true - }, - "@react-spring/web": { - "optional": true - } - } - }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/workerd": { - "version": "1.20260205.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260205.0.tgz", - "integrity": "sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260205.0", - "@cloudflare/workerd-darwin-arm64": "1.20260205.0", - "@cloudflare/workerd-linux-64": "1.20260205.0", - "@cloudflare/workerd-linux-arm64": "1.20260205.0", - "@cloudflare/workerd-windows-64": "1.20260205.0" - } - }, - "node_modules/wrangler": { - "version": "4.63.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.63.0.tgz", - "integrity": "sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.12.0", - "blake3-wasm": "2.1.5", - "esbuild": "0.27.0", - "miniflare": "4.20260205.0", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260205.0" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=20.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20260205.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" - } - }, - "node_modules/wrangler/node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@poppinss/dumper": "^0.6.4", - "@speed-highlight/core": "^1.2.7", - "cookie": "^1.0.2", - "youch-core": "^0.3.3" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index a33a236..0000000 --- a/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "whattoplay", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "op run --env-file=.env.1password -- vite", - "dev:no-op": "vite", - "build": "vite build", - "preview": "vite preview", - "test": "node --test server/**/*.test.mjs", - "deploy": "./deploy.sh" - }, - "dependencies": { - "@ionic/react": "^8.0.0", - "@ionic/react-router": "^8.0.0", - "@react-spring/web": "^9.7.5", - "ionicons": "^7.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "^5.3.4", - "react-router-dom": "^5.3.4", - "react-tinder-card": "^1.6.4" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/react-router": "^5.1.20", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^4.2.1", - "typescript": "^5.3.3", - "vite": "^5.0.0" - } -} diff --git a/public/.htaccess b/public/.htaccess deleted file mode 100644 index 3f0c8da..0000000 --- a/public/.htaccess +++ /dev/null @@ -1,19 +0,0 @@ - - RewriteEngine On - RewriteBase / - -# Don't rewrite files or directories -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d - -# Don't rewrite API calls -RewriteCond %{REQUEST_URI} !^/api/ - -# Rewrite everything else to index.html -RewriteRule . /index.html [L] - - -# No cache for manifest and index (PWA updates) - - Header set Cache-Control "no-cache, must-revalidate" - diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png deleted file mode 100644 index 15b7e6aa3c1273d862fe395720851b5bcf835ce1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3542 zcma)9X*d*I8@BH=_K1)fWY3nZXc{DY*6d3uBhlEm>}z;Q_ANrz44Fa3PWEktsK$(W zC6qPHB+PvCe&3((-}hbTI_Ejpd7k^nS?=e#&-1(Hh5$ApHX0flfYBX&E2>5P(^;6P zqs}yw3e_;(H!;+wq5RV-dTMj1k?BVIw`{^ncUHoKau+X-9_&S_vW|OllK_pcg>;UO zQg6ZQg%#f{tS^fWIChA(?Vg74y)gIfcWhe}os+E_ZCZr2Dc0HZJ42kgFpl(!EPko+ z;xCNuNClpbmZ-57Ng%sbLwCzI0?lVUa7E!8<5O80q@F;0B!B1K=JY|pLS>z8nh+q8 zW)U`FX+x(+o0sFL-wvzbo{r#~?hjxDI3hWQ2VsEp!V2wH7yVCA8~!<`4{4gT!t-ZW6XH?LMeUwv(a#owXUt(s~@~MH=#>EEB96W=x2*HmTAQq`oxP+BA7`ZizPvqZYbx#wKNc{%SCCdmkmq@;%@ zM!2XgKVSv~g{Stx1jS6fq18r#&R#j85PBwbx4GLJLgi`$4+y-$5mLGQR2aRj-P8Ol z3?L2cmKXmhFfW=yTzXTWgeh|?mZuAucua+R8A#vut-R{SqpqdhN1LmZ{;Vxf{sZ9o z?q*=3V&gosY}mEADCoEB_e9N>hm{dk8zVTw%?Ow3)ewm(5G7o8CJ2PSmmIkweVTeO zR;T5W)iVh_tK3N5rCv(z_leR134JC$)7w8O9nxg6O}NNftRvW#HM^I4Jvd_rJr|Wo zyjsFDsGBz5MKRi7Y!dIKOBR$fPCaVN|FGPvTq&dzYtMbVq_5munsT)q;Mm?zWkR+4=YWY*f;IA$1M3#8F zg)m2C*A|pIjbG7m7no0C;ePR$(;#n4HjyQZs$Sb5 z=vAO}A}t=a`?X+?@lKv6yb>4((15xgUD-pPZR|_PMk>Q~4GD~v)WD1)D@vSad;0}M zSRKXJu8`7te?IU1txWc0wT_{vbVAL^{xsA4;(IFe0nfMXe?zsXPzR}7GYYIh`FCDS zzko2m?F9J6IK@9nV_Wj}MY9xyvHkrkQwV&Hq0Lb$S$~!TcIJ>YsA(MI^cCUq)o?Kg z(c<5H!JJ|{#69HE=W!P)X#-Stv5j`<mH9X@)!1+f@(tq7@&Vbcs|SP)@y$}K{S`1w>SZ9NQgf4*g`^1!!3 zE~VdPwEtaeOJD_gk;dyLK=_O4y_ceLky`DFDW9*H6e0=lb=-+Ed}%p~b_#1)PH+3PfW%XSoA<*|*ZX_UUS7LtG@ zD!Ujwe#?3k!mqT>HE+xJYV37LaFQeMrU@w}jpv_*266a!bWP)(SNw4Z={S6bl8XUB zyXwM(Lg)9nG>BMN*QGME2I;seg)LAU2onH;;g-dU5tp^>7$PVhHF8|cXZ=BpLP+vZ3JGT?6z>C6v(7RKkFgTWO~EDjxW;FS-qY*| zK8nJY?e(9RINu9>q;M8(8uC+kYE5jgiQRpXNz?sGVMti0Mlqde$!bTHo7+}s@T=== z#N~$56QRO)T)PeoW=;F^E(EkPc@*}`Hr+EX2a%pZ`+3dJKR zG#8rVPrRTG8&~YD7zFk#h#R$Qk2lzPXu~>i7Y)F>L#SY;c`;)slBO;Bf$Mb~gtLLtn=g9;41T0y! z^Kx8!Vy!z~gcsitAX0kDff_+~=#aRv`{ZnOqr@8A>F0Q{N zNNt67m^fT7HHWZ|ocT5MT{Tn>d>d7o5$_j}QJ;L`6%^{7cY#Fl(kT~F$KJfRnIYiR^f_NML9*W=UCmpbmA^Kslbu)L&F;3z)&reW-lDG4Fa9srkgi ztb*3hVRbfEB4BNipPXT${!SL-52)3lHR)4zd48(v%3iDMfydA{(gDcpR-@#hayAoB z!=cdq!<5ypkLL5LXV8!Y{43ov-LRh~)QsyjAOQbx9)j?s$!8Ke10TPcEkO+G6wD^? zSr@8a#oG(RTAHK0=)dBv8|i%yvnAX<>k8K|-iwMs4p3SpoiAZ^H{?khsmOt&sxt@M zgpXKYqrwgD?eQOwj?weztty}RuN!M(#IhDiH=Q}IClzjIA2J)QnC=JX(61tr26lYR z_(V^i!cQmL|8V{(AG;xeBkLmx}R4yI-Yn% zRScA)x_v-PbvhT?T5YAsNTHc%slo`;KoG`En6Q6J*iswWZV&Fy)q;-) z4lL!#3FxqAcRnwu7oN%ikOgGgIXFc!sI_ARouB;NoI{&_h0oH*a!SQ1hil8SxK;?y2Q1tKRrLX9=1l9$VfeERus3Y zB{bxZKaP$eHIjEXt#obimDdEAo$|T7?_#;js%L~Z#S>;G=dd#mjjhF;ZK2iX1K_z3 zMZsAjT0!x9i)|<`K3CI zs+-RXVlDx4 znb0wWG&@F%s`p*tFN;OKWhMZEIQ760FemOc%M5xTZTr|KA6B`y2Fe0qi0ZOYG=i4H z@e|#OiGC#E{%tO=YP$vLW%@cM*;rY3RnKd*Ux(v@A-pZLzio!B-Z&(W4pN_1&W_2< b2MW!3Y3ky7E5)ArQKK<3FxN*w-Q)iS4M@){ diff --git a/public/clear-storage.html b/public/clear-storage.html deleted file mode 100644 index 9250908..0000000 --- a/public/clear-storage.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - Clear Storage - - -

Clearing Storage...

- - - diff --git a/public/icon-192.png b/public/icon-192.png deleted file mode 100644 index 914b685f935f260000d2a947557582fcd37c3ddd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3582 zcmb7Hc{J2-7yk|!*~Y%FDSIT0Wk!}FhDdhV%aZIgj4X{}!ZIFme4I;}V#?H@V zX<@7xYcd!VV~KbA^ZoCA&vTyVp6A@pz31Hf+~;%8O|`K$<6;+M2LONzVQyl}ki@_H zB$#p9P6e?t1o(!fnF(f6OE%p$oe+ zbr{{Z@1gnbZjZcQC99-)vxl+Ra6i^Nh(x`CBBlm`U}9>Wp=V>wr1Op!xd_{FTuWS- zQ|__fXZcw`E$5sUVwMIUaf&#sjAfg^S%PYFioKn`w~|^M`h{^<0I3G#Zr^-3Bw^F6e04V!P~tiL#f-1KGjT5wu>E;BSw{I=L2XRFBZ zfq7uAVbRY{Z!KF2Ef3ewd|b_3tb&uw{OrP+;V0gla;{*|R!s5re0m(wLukR}!D5DG zOsjcU0v6}(z06nFNLCP%MDGjk_ywZCR{j=9Jwa!KZ-gm*CsNiw1x0UM1D0Jncdsqy z{?vqp(!-}MKGftX2uh$ye|Drf->_a5axXB~$B3ge>N<|%UtCqZbu!zPYKuAv;_<==NQZ<2WxA1FhV)j7YOoW&!+#-|}r@*v4=jHKU_G zJ7EG%ec@KJ;jnyc2VJJ_QQ);@;bXV#@op#xN{Vn#eaO+Hu2~cFs!7eHE4jtn!ix!i z$Nk|$4ltgx#%+nf!)vC-jUK_{c*1?|09pk6#jeaA;8vJ>^25_5J$yA;qZk+J7yA)aDszra$$u6S}?5pXauept+8lPr)#R|MwXd*eBt zZ2uVwr5vf!4im>r-C%~<6X}`mwRf|JtFjL%eR)3MBbr8_{%a|*+s@-gc`_XY9 zQn1|82bj@;JzvZGSwp?sbSHaN*&?eCI6GqR@9?@hIkq~|-86tt7>!7y7^}TjD=x|> zxxDCNnQg!!lN!aisuNk9n(?Km)K!b(=RXNdqNJ$+yo#bnDbuj7tbVOk)fXyaSbYj9 z*p09U{M_Eu!Eoa>0N#RZ+;74%`{$;+lgy<(qVAZWfWkG9NFl9r-9RL_rtMm(+QZbF z>&FKlDDUT&L#~Rl-qS1Js&);4b9at4bl_jABjZqKeebehRDQEfvYOii%JGB!BWI@? zr0y0=ONo&0IiZw%ZP)ZU$L_Jh2%VGF5{c3u!JqDl$De!}&BUHPjPh|cz!`mKWkF(0^Ve$LZs zxswKte(R1xtcHq@$*;r@ism(1z(+SC76Z?AJ=LQ)f83>&XNwk9P9VfWnbjL02AXvn z4a^l_2=P9ygJTnSjo~i2AxnD(EMrw#KoPCNYHdYH6BSwqX#y8PKYVNWMfs!d&&R}J zDN0F9!vkI5JZSAkL!dmyvi3}VHN$w=!023O;A}FZet9o=`lHxiTm`s)k!Yl1p`(MG z70O!u5W`h5P(%yIRPgMd_%BM~6GYd?LH!Hl)Dx+t;r?{ft9mD(GcEl}cCCgMpS0O)UVkbe4L7H`YNvFfIln=u#P&|&-dsxwV^}Ck2DIhaG zKU{|RvH^pF&yr(-(e>L8tfqy0~CDRi}pcok~NulOOc29`dx>9UnH1G#-#} zBaJJzRZI-FB_Cd{55`YP1jmN&FN0dCFC{*OUOqlRsycuG`XM*PS-YS?>Od|?kH^YY zO{(3vyWzU&=<@<}~r09(=SnrCxLavpjqb9iYk!Mbj^4jdT9A?UxN-$g`KKL8a z$8kbSa!hxKE4Te{g;vqVo%{Gch7!Lqv34rtu5!K zCeZDDEB^OIY|;CTlAH>HpS71Z&iw}QG(WM2B$}|7|9=fm6{s&rr&nnLwly6u%@Nm` zWPB|pQw8400oKib+o>H0qvPguYOe2yxHP^-VCNM+ONDT~A*~iaIg= z`cO=Fg&>ldhb3A2#S)pamgBR~4?zWkTv{oHM})S%V5LgEg&iZ8WyaOY7>%W=5@&r&m759TSfNb6@Ov42$Hzrz4IF zxB8N6ujdvXgF{Re1`rVos&TK@4C=xwS%vB~+@kXLq;iHpO7!(Rh-L{4d@?s5nXFId z{PQGILxt+bc@=cpQrW8P9qcV-XXk)4ak1FO9stA~$%R`O|HP4Q1E;e4*5I5N{}d{V z%H8m((Z!`^eG*fG2Q|`Exr26Na4V%dDnT(E`6JzT$tX%d#|w2r^1M*=oAYs+liew> z+`{;BEt_9Fm2HG)J22)9kmf+JW!Xu15OH}jw;*^?%XVj%x&EA{Yuz-dA?L%$>#w#k zW^WhqK{|B@iHEx3jm9v{EUSrVU&up!V&1 zlNxJIHi=fg?prmVSVX7w&`ex~hd%qr8lHZk0s0{q*@pE|-A>ZD$gDO diff --git a/public/icon-512.png b/public/icon-512.png deleted file mode 100644 index cd91d41797c28d753e4a862ef3006d183a4faa93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9951 zcmd^lX*|{6_wVN%^IYa+%9s$oC62iWhYTUJkkmJYgUsS6oG3%bn7K@y$dJhFWX@PQ zBJ;^q=6QD9ef<9Se;?h~eR3b&`|<$$v-a9+t-aP>@4fblG&a;>qUWK9AczU6t7QT~ zaPTJ_qNN7EHvI_u;1~5BeH|_6nEaDnUl0#Lf)G;cs+nK<(wKjWt#jzg+KSDXRlvEs zF!ocl^l!svVt!YZ(s^=dX*qkGd2HLZz}M!eJ^Rlc-70yJtM^h~T#8bP5mvg`9K-OU zit%I4@2BXS&r>>BPkoHbtyRi+yc2l*j`EsrO>NCQ!N$9HXG6tbw`QSkF4@>_rDWV| zJ{`?7)il(vj9+|(A3wcK?ZXn9LD$VF+#MBarNJfpq@OcBH1n>%C{BKp;_Q_p(gO!U zE%P>3$;hIYuQ>2du%sj*{ev3IHmf)4cB6;1IEqghFYZ|Qh9k6?nMIj-BYD{;A%{7_ zcM+Um=1WwF1j?S5s0MF*|9InJFCzu+Sp^#do;#p(Exat=qP61mk~P>kr1G}Xb17<8rZZmP+*Lj$V#z{nsVrC0Xq*ROknjSXQ>DJ411=;TV5|}pxN3r*=3>tk; zciXsk*-Jy_rLeizYSgvVeyXb<%6i8|{#y(GW)oA(=)2wr8&6YuA%cVzGm4VlySw>a z@J_P+bPM*!n%jtO72!}ouo6KIH@U#?Tg;w|#}mkNbE;mQ zQzI~Eah}g(mQ5-SUt59&hsOhjw)phL8<(xwClZx9EUkS{tZGVpN_#&yMhI#0K`6-8 z;d#FVrh#BpnI{Tt`gC`lh?j@mP9k&lwj4d}-(Isv3?*Ex{OO5-YPecH$51YNt|yDk z;7OXHwY8ir@uoDttQU$P+tbQ5{0=v14^=-T=woyDuM2URZ9AzIL=Xv%*|)@o8JvuE z1e4+7Cksco0lVQO)%btrF=OkX$1+j1@Ih98XJ94~`7b5EPr08tkI^xW(6xpFxLPT+ zwW~7mnG#;QSi;?q^ND_56QiYyx6pT~vI8AeD63AVBwlzXf}~UBBUXj_6Wb+xGN(-; zQ?52)^l{r)d#sslKgD6T?^PA0*50(+PylC(lm37&sXw*e9f`HRv7$RUoO!+MuSKov z{5fw`oQTq{#Vht0tpW<}v4#y}FXn`_r>rk$V&NUbnJVuw5Mx{*^q z`!W}tE-`x>lj|Jv? zZxY#DPI~T;50tn$n%H57ziqXwgUwC1;*9Wf4MC{98esl-z^;MtVcX+0Bw=%IY~ZU#CX_ z(U6lud5}$md`Lx^L>?W?MihvBTnn>+w3T)l$=1sI*x#|*qRt`v25_QUiOma^Q}J@)r&4t%`X1Dst6!ABO!x|KAR zad`d1*?~{+(kbz&m(k5qUt*5{y`H%@bJ@zUr=|QjlZ!Qso5xX&ymUqC>$A#=wMk5E z_3&@?=a==S&t(_RT4F^c9#Sxt8RhXN#m;@1vWGqIZU)YoYd*jswzf0%WVOm1s=4?) zFyb6PW&RDtBoSoiutV4rn%Qujwtz|Su}Rj;14JHfz<@3^Hg+!52^^!~_!7>LYMudd z4qwf4dS3rCu{r8H|K(YFysmf%`3OKRy$$qZ4MF^Trfm;9-;Ns_@I99K*Z4dVn@}$< zIo3VfEn8Yo^Rfp1O*L|dI4y%Tsr{7~h*m$l1j`YCKM%@%;~|1H@p>tTasLbg^(6)> z`PWYnXwQgwoW*r~H*kX2m1?s2OxPQS3vU3uy&hiB zT+~LGRg&7SSU>?+R(;YBMUgJP%0AwnO_dF0J@8Y)E8SOOLC{3tE9W-n_~0qGWBL^y zuT$RMx4`DASgPm6a}|U1@@xG?pABqi=*tM%6cRG|Zrs5nNAw&Anp+Wv@CTA!YU%iD z6S7o|f!-&R?C2gVNG1SF+J-pulq(7|x{;c^%%K1=4-xWC%@`Q1@Gy_9GuQ~g6^kPs z!Sr~><}w@79)ik2L~SPx$S!crZ#nJoelP6L#rCm3(`BB)l^6Ll92c!7o3*M70Vvg}~b`&5;U!D{-neZ5-)UeLuTyD^TR zP`pVvVD);$_{<%hPRA#7*@aD^iXh4oeB*2NyF(2y(6!0zPbWHsF*AqtPzjnJ1H*e> zbh`;@qo_2nxkAs=W)J7-p~b8#F~D|M1QdF5%s#>O>tb{BpC_IR(62u`r0mTKq`Ca& z^Rs$A;qP!kq{*w72`P3_uYTd40Xng$MLW2UQHCanj+@ih^G*#RyqUoStpfT;J)rms##63qT(}4<`T~7o z>Aq50-Bw3f+@=aAOyU^qMP-+e;7l@=#*Y*!xCzM;5{XW`uF=WDfX+>|qRia}dv@88 z6NY;Nwq~G5NU%Zxe2FNd)m>^n*)i{YWP~V44D~XO@FBmF8E+HeK+s06w>91XKPF#lZ?!?$eakU0bti2SZ|Y2JQ$}4L<5v$ z7oh??IDmu~1jU1qoDfbUeOL5ELwuzDiOMm6_b@-Oj;7%uw(SnEP7CbvM9(0Fs z89d8Gep5Yj??(7CQnp&y+eAY7Y{Z+7r_D<+oz#COXk&}RCq6u-F~?(XmEILC;=7Sy zcHIGHEEXW%TGjgYo5~E%877SdpmjeKrO)D2)1bgz9m3h<4Usx&t$|(}Rug zvikL1MIAQC4#br+)1FV8L)gm5rV;ysGpsY7*#XlvK?_B4lzC6t#{)o>w%=5Wa&Tvtg>S9n1 z*Wi!sYIyeCYgN96eILIWH(b2#`@z1X=7Zu;^&(`3)`vC@NVOeEWTQ4YEi==4uJ#L` zz*7tNsi^N@;uPyvOT!Js&0=}inNZT(ty%;|Jj|beLuLG zoS?26KN)2@NkHJ@-`(;fntROi?*51jZZYbt{mM6xwZ%>BY{m7Tm$!iEm$%A^6Naeu z!Y(Gq zq`;USa6!1)D;^n{7Q_+sNc#f=2f^?mT+B|X!*@%F{x0$R z_>j(z%AUoi8!GI^=fQNpr5BT*o{^__j{BC*(QrgkWkX8(t2igS{p8CvCg{Gp>iyI= zhZ`YYu(}-w#Z@Ytsjdr+mrv*pZ;DcQ>KAyN7axqYEl(a6zt1;+LF4ppqvD)lCdEPb zjf`t1%KmGNe(hbR1;*eoLU5vwH)_a7T28FMtU=RyFFel4eVAr@0^v=MT>9gz?B9;B zD~G0sviK5|LUVn#`Rz45d*E@V{rP4x2a-rO?w9*=<23v8@egk&D23;$_$~35HYX4y5~vt7nE4tk zw%{6Avyxs`-Wr{G_RgnNf|!^gN%CCJ=|uwF2~~Zt&pah|eOhVUc|PrvhN;fp;dSjX$J{5(TJiCCYV3Q;oMrJhUPqRtL9_BNjFfQKrX30i=nTYy2M zcj$+x^OwP-!?lPq#1uW6pr{}nUdYwV7mHu77${j%V`3g-GYmlRR`?DF^$-@ALQh!4 zE1QVxx;M+F`!d)D2&jBl*mv?BdlOLGzo)bBUT|O9ib|lK-z{W!Zz2%oyfv5{LWfzQ z;}T#wV4v()Fg@Js;A4TE_vIXgXVpAh{3_x68$0LZYhc+Edo3LL(X^K>DfNshlqCP=cQbw!RIx!qutuFA#Z$jNpL zEth+*4ANyq-KQ{|KHC@h%%l*c57q&rFv3HBinh4aR*;C8Sg*}f{@6WXcKywKh0Xu` zfPWK6P;YY#{HY#60QnLLHym#y$r%RtA35pvfK(y!USS*Ix!v+vJ*aQLGC8#eFiQ(s z_DdPCJMg(wGgdjx%NN!rof_zR=#VK28iY z7yt9*p5Y^P1(`sfr@@c9=mKKrk8%^!l0x^bHxuTQG*UpZXZdU|{J zXd>6}Chuo;+Y#x&0;6zAL)Y&1L!LJo8#d5-s~1GT4m+>YxQmY7&^I5r;(u+cY^9Es z-#Wvtc+F+!u#*e8bEQWcQ-wih+6R7gf62q3OzVok;F%Xx8YVVX4!#3_B*tv|pM=%R?zM+*W|xygg-|7-38(yM`oi=Wr^@WskzmwJ_jys>ux z@`|(%6nH;4P;νAE8@c*pCyQHE@Ow))%E$37q2fAynUk{zA%Dwpm({vs`)8`3rI z)3;I&?8gBVbNVmuQ2dW~{4mP=M%qi1$-q(Qnn*K`QMd)G2W?VUxxUn2;4ib-@~L6V zP;s~3UtB^byZb9osus3S#SYhQ%cZ@x=ZVHZo^8&LSYYg5d%7|+vIRywpN{7Q{20@U zeJw+d;NV5c6~2c1-cQHx&$evG-y#+H(F)qz!r};(x7BO+3tzc~_#J*;tY30eT<=1) z;@A#S>^b-H*4BB+e*E?Qnh_~YwZ5UIN3*b;?89WIPxsJtjX%CuNHe*)z>}rZi3Je- zUFNFq0=M`~^z1gKuzfSn2Ovk0$@i^Wu^!(49bp- z41gS-_G)h~S|0wK_MIy0iT>1vt`)$KKg7xln)Adh#5dPczf&b={gw_K2nO)-Q@G|1 zI&Fdlvm(Fk6g$M=wx{7_D43Ai;{}O4kOUC#tWn|Zk>mMRRJ-u%(D3|@PnLPcs?qp_ z6UoJ$d?3UO_7#(`>m8c^mv1y0y^pvJGc-*2nf8aV${>UMornY3QDy$@jSP+GdmxQ? zgXPbxR)~gx90GDfuGHqsAia+5V+eHMyL3Z7IdNj#4k#D{h0wXw+&rPm0?3SO zR;AGNx8bW*T>_R!Ce)npfY%Fv|Iz6+ha~dEFL^t8hS^1ykmvmSR#{nc{`RR1rfoRV z0I52PO0eK3a}$%k2|zBr#ZONCEHVNd%K&mvST+ErQa+IR4{~stIOdZIzHkCBK1)tw z8p(7Yp=nD>(q8Wg5@B#FybZV9bB1+P7X~vmZiw+?T~@!jRLv>?nDGz@1W@&NyAynK z{J55jO%l$a0Sh{^K&=RnaTD{he^VccD;(ga2;svmCjP$o%QdfP?~nhfJ$_pXv9|zV zI(VcTn|t$cPIWcD9pJWdG8P**D0LA8Vtx46nX32b<~+NL7=uP=Q4t=~XnN$AE#k@< z+F!E6bd3z$X4@vdw5kJ}nMm$!#u9620GuiZ*z#YJ0smaQ*;e_CaRWe5c@6+PMhmZo zx;I7X0c>j$@Ls^Mmgfp%f#F+rfC{%T@~hij+3uR;Z+}K0_gY4Nt2CuZlQM{XNQNI{ zS`v13q|xmB*$ASlg@w$1&hL*9`>1r+r0~R=&{dP0FI(mdLP;aaM;w_{8n)HkhPBi` zS@H~pB=o_IXA@5gIl)5a4zN?u1WCGX%F|LI1>FCnHeJ!C#qT(S>7=0HCcv%A-K>u> zDM7$!ySdCBBT%zgB5et6@-_zXGS^ZDAO1^5<;1xz;=2;Fkjgx1D`-;%+?^DjF!eS{ zgHp!U?&uh*+rsfzKS1RiR?sH{P2ti%KfzuOOckvJz7lZF`rj23E?+Pb#16;r?0y3v zndercFGIqxNtVSHfgAigFu#Rk(@lL|APw<=J5i2xjS`3!wzY5X33P;cB~u#Xhk*{F zb;MSj>E-rT_3+qZuzk*dn-rSOGxqt0uQFtg-tPOF$?XOu zy$2d4ha7pRO~`M#qC@sPfP-Dh>H~-P)TbamTa?W_d-Uf&=DSYz|NTLJx7%pa!YJ49 zyW)-tEUQr&c+~F=tLyh+->O`BLw9adEO?@#*BY2sN$$6f4KIjg^<7Y6K)bAebJ?{Y zY(ky2H*i*LqG@E(uQPKY?gg2@vVzRYS5%;ogE4}Hm>B&2C^V=JT*5lM?>Or2!rwmiP5rV_5)#5VBXsb^Yd(AT`N8 z0BT#hFDiO7*DXMbcYSHktjL(uQ_VZ?G;=gY4BTe0W18Nu0tcY+tj}_&@Xk|#Z z$ty5b5txVW&V%Q*SB{CV;ot0;jkI=MQ^~b+r=6|D=1UeNY1h3B*3qFu6)^VDqY%;X zX~4H}gm4*5lu&9qPn8{_;|AMLOVtIH=nswVn7QELm1;@oe%RGXot_&aNbEnHyI=AV zq%(PQs$wYT4Nwv$%fkU`<^EI-o!SjlMecehifq|w+BR-+Wm!PxiOtsV33o#=pkLX7 zMPl>thsNDHICN@qeR1PNpQY}VPQv!7K`B-riQ z26F9xE7}!U;^nIQuNh!wD~3=R*OkB#Y3OJ!!XDPxAp!rsV>Vr;4!i)Xr z+Yq`pI*8i+lnAmvXiXk82RtYD0+`gkd;D^$=OBg3!4**?v^vmsm%K^zK@Y}8OFfG) z(N++2hlUG!=A;Iy^?9Gv+@`bIHt-%C=+N@!W<1wI6xoU#@GJrFHrc*01U}5aOJ{ks z;c^WqUL8Cs(0(xK!G-Qb5ZAc(4#R*kb8obEv>wfcsmJWBffpZ0%=aU$QTQPW?s`YvPd zX5o08qoP)Ck-su<(0|L=_qf3$tF=N=Jp>(XG_yEOH;R4y00JQ8cri`DEwdwn?E(a4 z@TJ345Z{+XktTNL33`V|S_+vcn5BY35#wrzSWUeMGPR0(ccG!0h>LU+m}dfyg~42k zmE#`Z39rh|;x(7yyZdOm66g^pe1)d%2@!_Gnk5%Bof0VajjLh6=NB?R$vq)X5n6pi z^7MN<9y)&!f)f3SBlt#zJ5osOC%dc<-3)_Q?qo->1I^fbDLEBfRFK$B-W~HA@!+4w zKY7d{MafMTopEZyV~J}>?B)CdjIv_nrDlyB>PNOPhOzk{%9M|*B zs`|W-w?!45pTWI@I1!1)VDr~a3kb>0`*MC_k`R=BhFVW_oL(j2mm2WNg1mF9Nkw8I z9Umd+iC42}njZeW)Q&LHB&O)k`L$UCuHmj5`BYH0yPG91+#Sm7CBN@hR9E0V7G@(R z?$zgXGe8>02x2B}JH?Zpp)BC3@BV>{o6Ux2$)7wMLlhfL8RtA%WrvEM=wS(BKQSGV z3v=N%X%)@+zz0Zs#Dlz!vMN?Vby2$5ZJ;Jxohfusekvz82WbsK|Sj8JBk>S8K_w z2Rs<4@*hP770bV~w_s|~7)oHn^G$;nO#SR64N2eLN*N& zZ#wHxuJeL95597srMI%!8n_~M*(M{%?0K5K&!HR&jY!|9$y3RV7AUz=eRB7KEAi_o z#lqwC)d9lQw!#?;sPeL`j4cB9EE#qjGe@n9wbnpODo3RB8^)`D`bCdV3gBwV(Sc{d zne~8DX1Y5GLIMjQ>*s`O00{UW=Sy(oF&!MQlC4z1j z2`9?8Too+5!V+4J9y6VL1qvzRkwFbP&-#ke&VuwBpJ>a4zMn8fSVpF6PYdJE;iiDT z#*=Q^yVsm`8hXz-%6g|4apXXc-s~J!9k0r+FoZTj_wr8qcbHHJ;X@nHztctT-$7AL zo|zdMlaKry(SgHz;0XkBYL(NNtUY~E;LmSsa+4B8p7AKWH2n39qAQBkJsQRO$_YJA zcrAit8lX88q%5AFpC0BAU|BGs(d&ViR)Aic`DuBraaKKE&1sI^74WkCX9rd+?CE+O zGin|-wO@g{b5_$GvHZ`EuZ59ySo|BjqhFN<*1G3ZsC{?`pD-%0{nY2p#O8RMX2m}Y z8Xr16Tzn%D=TN6YDei3Dru0JFnd-#nUZa%~|EP$sr(Ax4a(5PfA%WS#<$!i7jT~F{ z+JM3GoetUZ(0aN>?JW{zIlMb+z=-!TtRKznz?%6`5cveJ?Z9oxU - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index d4c7408..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "WhatToPlay", - "short_name": "WhatToPlay", - "description": "Verwalte deine Spielebibliothek und entdecke neue Spiele", - "start_url": "/", - "scope": "/", - "display": "standalone", - "orientation": "portrait", - "background_color": "#f2f2f7", - "theme_color": "#0a84ff", - "categories": ["games", "entertainment"], - "icons": [ - { - "src": "icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/scripts/fetch-steam.mjs b/scripts/fetch-steam.mjs deleted file mode 100644 index 94a1853..0000000 --- a/scripts/fetch-steam.mjs +++ /dev/null @@ -1,104 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; - -const loadConfig = async () => { - const configUrl = new URL("../config.local.json", import.meta.url); - try { - const raw = await readFile(configUrl, "utf-8"); - return JSON.parse(raw); - } catch { - return {}; - } -}; - -const toIsoDate = (unixSeconds) => - unixSeconds ? new Date(unixSeconds * 1000).toISOString().slice(0, 10) : null; - -const sanitizeFileName = (value) => { - const normalized = value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); - return normalized || "spiel"; -}; - -const fetchOwnedGames = async ({ apiKey, steamId }) => { - const url = new URL( - "https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/", - ); - url.searchParams.set("key", apiKey); - url.searchParams.set("steamid", steamId); - url.searchParams.set("include_appinfo", "true"); - url.searchParams.set("include_played_free_games", "true"); - - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Steam API Fehler: ${response.status}`); - } - - const payload = await response.json(); - return payload.response?.games ?? []; -}; - -const buildSteamEntry = (game) => ({ - id: String(game.appid), - title: game.name, - platform: "PC", - lastPlayed: toIsoDate(game.rtime_last_played), - playtimeHours: Math.round((game.playtime_forever / 60) * 10) / 10, - tags: [], - url: `https://store.steampowered.com/app/${game.appid}`, -}); - -const buildTextFile = (entry) => { - const lines = [ - `Titel: ${entry.title}`, - `Steam AppID: ${entry.id}`, - `Zuletzt gespielt: ${entry.lastPlayed ?? "-"}`, - `Spielzeit (h): ${entry.playtimeHours ?? 0}`, - `Store: ${entry.url}`, - "Quelle: steam", - ]; - return lines.join("\n") + "\n"; -}; - -const writeOutputs = async (entries) => { - const dataDir = new URL("../public/data/", import.meta.url); - const textDir = new URL("../public/data/steam-text/", import.meta.url); - - await mkdir(dataDir, { recursive: true }); - await mkdir(textDir, { recursive: true }); - - const jsonPath = new URL("steam.json", dataDir); - await writeFile(jsonPath, JSON.stringify(entries, null, 2) + "\n", "utf-8"); - - await Promise.all( - entries.map(async (entry) => { - const fileName = `${sanitizeFileName(entry.title)}__${entry.id}.txt`; - const filePath = new URL(fileName, textDir); - await writeFile(filePath, buildTextFile(entry), "utf-8"); - }), - ); -}; - -const run = async () => { - const config = await loadConfig(); - const apiKey = config.steam?.apiKey || process.env.STEAM_API_KEY; - const steamId = config.steam?.steamId || process.env.STEAM_ID; - - if (!apiKey || !steamId) { - console.error( - "Bitte Steam-Zugangsdaten in config.local.json oder per Umgebungsvariablen setzen.", - ); - process.exit(1); - } - - const games = await fetchOwnedGames({ apiKey, steamId }); - const entries = games.map(buildSteamEntry); - await writeOutputs(entries); - console.log(`Steam-Export fertig: ${entries.length} Spiele.`); -}; - -run().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/scripts/steam-cli.mjs b/scripts/steam-cli.mjs deleted file mode 100644 index 1254617..0000000 --- a/scripts/steam-cli.mjs +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env node - -/** - * Steam CLI - Direktes Testen der Steam API - * Usage: node scripts/steam-cli.mjs [apiKey] [steamId] - */ - -import { fetchSteamGames } from "../server/steam-backend.mjs"; -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -async function loadConfig() { - try { - const configPath = join(__dirname, "..", "config.local.json"); - const configData = await readFile(configPath, "utf-8"); - return JSON.parse(configData); - } catch { - return null; - } -} - -async function main() { - console.log("=".repeat(70)); - console.log("Steam API CLI Test"); - console.log("=".repeat(70)); - - // API Key und Steam ID holen (CLI-Args oder config.local.json) - let apiKey = process.argv[2]; - let steamId = process.argv[3]; - - if (!apiKey || !steamId) { - console.log("\nKeine CLI-Args, versuche config.local.json zu laden..."); - const config = await loadConfig(); - if (config?.steam) { - apiKey = config.steam.apiKey; - steamId = config.steam.steamId; - console.log("✓ Credentials aus config.local.json geladen"); - } - } - - if (!apiKey || !steamId) { - console.error("\n❌ Fehler: API Key und Steam ID erforderlich!"); - console.error("\nUsage:"); - console.error(" node scripts/steam-cli.mjs "); - console.error( - " oder config.local.json mit steam.apiKey und steam.steamId", - ); - process.exit(1); - } - - console.log("\nParameter:"); - console.log(" API Key:", apiKey.substring(0, 8) + "..."); - console.log(" Steam ID:", steamId); - console.log("\nRufe Steam API auf...\n"); - - try { - const result = await fetchSteamGames(apiKey, steamId); - - console.log("=".repeat(70)); - console.log("✓ Erfolgreich!"); - console.log("=".repeat(70)); - console.log(`\nAnzahl Spiele: ${result.count}`); - - if (result.count > 0) { - console.log("\nErste 5 Spiele:"); - console.log("-".repeat(70)); - result.games.slice(0, 5).forEach((game, idx) => { - console.log(`\n${idx + 1}. ${game.title}`); - console.log(` ID: ${game.id}`); - console.log(` Spielzeit: ${game.playtimeHours}h`); - console.log(` Zuletzt gespielt: ${game.lastPlayed || "nie"}`); - console.log(` URL: ${game.url}`); - }); - - console.log("\n" + "-".repeat(70)); - console.log("\nKomplettes JSON (erste 3 Spiele):"); - console.log(JSON.stringify(result.games.slice(0, 3), null, 2)); - } - - console.log("\n" + "=".repeat(70)); - console.log("✓ Test erfolgreich abgeschlossen"); - console.log("=".repeat(70) + "\n"); - } catch (error) { - console.error("\n" + "=".repeat(70)); - console.error("❌ Fehler:"); - console.error("=".repeat(70)); - console.error("\nMessage:", error.message); - if (error.stack) { - console.error("\nStack:"); - console.error(error.stack); - } - console.error("\n" + "=".repeat(70) + "\n"); - process.exit(1); - } -} - -main(); diff --git a/scripts/test-api.mjs b/scripts/test-api.mjs deleted file mode 100644 index 9df28ec..0000000 --- a/scripts/test-api.mjs +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Test-Script für Backend-APIs - * Ruft die Endpoints direkt auf ohne Browser/GUI - */ - -import { handleConfigLoad, handleSteamRefresh } from "../server/steam-api.mjs"; - -// Mock Request/Response Objekte -class MockRequest { - constructor(method, url, body = null) { - this.method = method; - this.url = url; - this._body = body; - this._listeners = {}; - } - - on(event, callback) { - this._listeners[event] = callback; - - if (event === "data" && this._body) { - setTimeout(() => callback(this._body), 0); - } - if (event === "end") { - setTimeout(() => callback(), 0); - } - } -} - -class MockResponse { - constructor() { - this.statusCode = 200; - this.headers = {}; - this._chunks = []; - } - - setHeader(name, value) { - this.headers[name] = value; - } - - end(data) { - if (data) this._chunks.push(data); - const output = this._chunks.join(""); - console.log("\n=== RESPONSE ==="); - console.log("Status:", this.statusCode); - console.log("Headers:", this.headers); - console.log("Body:", output); - - // Parse JSON wenn Content-Type gesetzt ist - if (this.headers["Content-Type"] === "application/json") { - try { - const parsed = JSON.parse(output); - console.log("\nParsed JSON:"); - console.log(JSON.stringify(parsed, null, 2)); - } catch (e) { - console.error("JSON Parse Error:", e.message); - } - } - } -} - -// Test 1: Config Load -console.log("\n### TEST 1: Config Load ###"); -const configReq = new MockRequest("GET", "/api/config/load"); -const configRes = new MockResponse(); -await handleConfigLoad(configReq, configRes); - -// Test 2: Steam Refresh (braucht config.local.json) -console.log("\n\n### TEST 2: Steam Refresh ###"); -const steamBody = JSON.stringify({ - apiKey: "78CDB987B47DDBB9C385522E5F6D0A52", - steamId: "76561197960313963", -}); -const steamReq = new MockRequest("POST", "/api/steam/refresh", steamBody); -const steamRes = new MockResponse(); -await handleSteamRefresh(steamReq, steamRes); diff --git a/scripts/test-backend.mjs b/scripts/test-backend.mjs deleted file mode 100644 index 3d6929a..0000000 --- a/scripts/test-backend.mjs +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env node - -/** - * Standalone Backend-Test - * Testet die API-Funktionen direkt ohne Vite-Server - */ - -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const rootDir = join(__dirname, ".."); - -console.log("=".repeat(60)); -console.log("Backend API Test"); -console.log("=".repeat(60)); - -// Test 1: Config File lesen -console.log("\n[TEST 1] Config File direkt lesen"); -console.log("-".repeat(60)); - -const configPath = join(rootDir, "config.local.json"); -console.log("Config Pfad:", configPath); - -try { - const configRaw = await readFile(configPath, "utf-8"); - console.log("\n✓ Datei gelesen, Größe:", configRaw.length, "bytes"); - console.log("\nInhalt:"); - console.log(configRaw); - - const config = JSON.parse(configRaw); - console.log("\n✓ JSON parsing erfolgreich"); - console.log("\nGeparste Config:"); - console.log(JSON.stringify(config, null, 2)); - - if (config.steam?.apiKey && config.steam?.steamId) { - console.log("\n✓ Steam-Daten vorhanden:"); - console.log(" - API Key:", config.steam.apiKey.substring(0, 8) + "..."); - console.log(" - Steam ID:", config.steam.steamId); - } else { - console.log("\n⚠️ Steam-Daten nicht vollständig"); - } -} catch (error) { - console.error("\n❌ Fehler beim Lesen der Config:"); - console.error(" Error:", error.message); - console.error(" Stack:", error.stack); - process.exit(1); -} - -console.log("\n" + "=".repeat(60)); -console.log("✓ Alle Tests bestanden!"); -console.log("=".repeat(60)); diff --git a/scripts/test-config-load.mjs b/scripts/test-config-load.mjs deleted file mode 100644 index 82e2a54..0000000 --- a/scripts/test-config-load.mjs +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Einfacher Test: Lädt config.local.json - */ - -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const configPath = join(__dirname, "..", "config.local.json"); - -console.log("Config Pfad:", configPath); - -try { - const configData = await readFile(configPath, "utf-8"); - console.log("\nRaw File Content:"); - console.log(configData); - - const config = JSON.parse(configData); - console.log("\nParsed Config:"); - console.log(JSON.stringify(config, null, 2)); - - console.log("\n✓ Config erfolgreich geladen!"); -} catch (error) { - console.error("\n❌ Fehler:", error.message); - console.error(error); -} diff --git a/server/data/.gitkeep b/server/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/server/gog-api.mjs b/server/gog-api.mjs deleted file mode 100644 index 1bd1c5d..0000000 --- a/server/gog-api.mjs +++ /dev/null @@ -1,90 +0,0 @@ -/** - * GOG API Handler für Vite Dev Server - * Fungiert als Proxy um CORS-Probleme zu vermeiden - */ - -import { exchangeGogCode, fetchGogGames } from "./gog-backend.mjs"; -import { enrichGamesWithIgdb } from "./igdb-cache.mjs"; - -export async function handleGogAuth(req, res) { - if (req.method !== "POST") { - res.statusCode = 405; - res.end("Method Not Allowed"); - return; - } - - let body = ""; - req.on("data", (chunk) => { - body += chunk.toString(); - }); - - req.on("end", async () => { - try { - const payload = JSON.parse(body || "{}"); - const { code } = payload; - - if (!code) { - res.statusCode = 400; - res.end(JSON.stringify({ error: "code ist erforderlich" })); - return; - } - - const tokens = await exchangeGogCode(code); - - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify(tokens)); - } catch (error) { - res.statusCode = 500; - res.end( - JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - }), - ); - } - }); -} - -export async function handleGogRefresh(req, res) { - if (req.method !== "POST") { - res.statusCode = 405; - res.end("Method Not Allowed"); - return; - } - - let body = ""; - req.on("data", (chunk) => { - body += chunk.toString(); - }); - - req.on("end", async () => { - try { - const payload = JSON.parse(body || "{}"); - const { accessToken, refreshToken } = payload; - - if (!accessToken || !refreshToken) { - res.statusCode = 400; - res.end( - JSON.stringify({ - error: "accessToken und refreshToken sind erforderlich", - }), - ); - return; - } - - const result = await fetchGogGames(accessToken, refreshToken); - result.games = await enrichGamesWithIgdb(result.games); - - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify(result)); - } catch (error) { - res.statusCode = 500; - res.end( - JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - }), - ); - } - }); -} diff --git a/server/gog-backend.mjs b/server/gog-backend.mjs deleted file mode 100644 index deda957..0000000 --- a/server/gog-backend.mjs +++ /dev/null @@ -1,157 +0,0 @@ -/** - * GOG Backend - Unofficial GOG API Integration - * Uses Galaxy client credentials (well-known, used by lgogdownloader etc.) - */ - -const CLIENT_ID = "46899977096215655"; -const CLIENT_SECRET = - "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"; -const REDIRECT_URI = - "https://embed.gog.com/on_login_success?origin=client"; - -/** - * Exchange authorization code for access + refresh tokens - * @param {string} code - Auth code from GOG login redirect - * @returns {Promise<{access_token: string, refresh_token: string, user_id: string, expires_in: number}>} - */ -export async function exchangeGogCode(code) { - if (!code) { - throw new Error("Authorization code ist erforderlich"); - } - - const url = new URL("https://auth.gog.com/token"); - url.searchParams.set("client_id", CLIENT_ID); - url.searchParams.set("client_secret", CLIENT_SECRET); - url.searchParams.set("grant_type", "authorization_code"); - url.searchParams.set("code", code); - url.searchParams.set("redirect_uri", REDIRECT_URI); - - const response = await fetch(url); - - if (!response.ok) { - const text = await response.text(); - throw new Error( - `GOG Token Exchange Error: ${response.status} ${text}`, - ); - } - - const data = await response.json(); - return { - access_token: data.access_token, - refresh_token: data.refresh_token, - user_id: data.user_id, - expires_in: data.expires_in, - }; -} - -/** - * Refresh an expired access token - * @param {string} refreshToken - * @returns {Promise<{access_token: string, refresh_token: string, expires_in: number}>} - */ -async function refreshAccessToken(refreshToken) { - const url = new URL("https://auth.gog.com/token"); - url.searchParams.set("client_id", CLIENT_ID); - url.searchParams.set("client_secret", CLIENT_SECRET); - url.searchParams.set("grant_type", "refresh_token"); - url.searchParams.set("refresh_token", refreshToken); - - const response = await fetch(url); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`GOG Token Refresh Error: ${response.status} ${text}`); - } - - const data = await response.json(); - return { - access_token: data.access_token, - refresh_token: data.refresh_token, - expires_in: data.expires_in, - }; -} - -/** - * Fetch all owned games from GOG - * @param {string} accessToken - * @param {string} refreshToken - * @returns {Promise<{games: Array, count: number, newAccessToken?: string, newRefreshToken?: string}>} - */ -export async function fetchGogGames(accessToken, refreshToken) { - if (!accessToken || !refreshToken) { - throw new Error("accessToken und refreshToken sind erforderlich"); - } - - let token = accessToken; - let newTokens = null; - - // Fetch first page to get totalPages - let page = 1; - let totalPages = 1; - const allProducts = []; - - while (page <= totalPages) { - const url = `https://embed.gog.com/account/getFilteredProducts?mediaType=1&page=${page}`; - - let response = await fetch(url, { - headers: { Authorization: `Bearer ${token}` }, - }); - - // Token expired — try refresh - if (response.status === 401 && !newTokens) { - console.log("[GOG] Token expired, refreshing..."); - newTokens = await refreshAccessToken(refreshToken); - token = newTokens.access_token; - - response = await fetch(url, { - headers: { Authorization: `Bearer ${token}` }, - }); - } - - if (!response.ok) { - throw new Error( - `GOG API Error: ${response.status} ${response.statusText}`, - ); - } - - const data = await response.json(); - totalPages = data.totalPages || 1; - allProducts.push(...(data.products || [])); - page++; - } - - // Transform to our Game format, skip products without title - const games = allProducts - .filter((product) => product.title) - .map((product) => ({ - id: `gog-${product.id}`, - title: product.title, - source: "gog", - sourceId: String(product.id), - platform: "PC", - url: product.url - ? `https://www.gog.com${product.url}` - : undefined, - })); - - return { - games, - count: games.length, - ...(newTokens && { - newAccessToken: newTokens.access_token, - newRefreshToken: newTokens.refresh_token, - }), - }; -} - -/** - * Returns the GOG auth URL for the user to open in their browser - */ -export function getGogAuthUrl() { - const url = new URL("https://auth.gog.com/auth"); - url.searchParams.set("client_id", CLIENT_ID); - url.searchParams.set("redirect_uri", REDIRECT_URI); - url.searchParams.set("response_type", "code"); - url.searchParams.set("layout", "client2"); - return url.toString(); -} diff --git a/server/igdb-cache.mjs b/server/igdb-cache.mjs deleted file mode 100644 index 7f16d19..0000000 --- a/server/igdb-cache.mjs +++ /dev/null @@ -1,225 +0,0 @@ -/** - * IGDB Cache - Shared canonical game ID resolution - * Uses Twitch OAuth + IGDB external_games endpoint - * Cache is shared across all users (mappings are universal) - */ - -import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const CACHE_FILE = join(__dirname, "data", "igdb-cache.json"); - -// IGDB external game categories -const CATEGORY_STEAM = 1; -const CATEGORY_GOG = 2; - -const SOURCE_TO_CATEGORY = { - steam: CATEGORY_STEAM, - gog: CATEGORY_GOG, -}; - -// In-memory cache: "steam:12345" → { igdbId: 67890 } -const cache = new Map(); - -// Twitch OAuth token state -let twitchToken = null; -let tokenExpiry = 0; - -/** - * Load cache from JSON file on disk - */ -export function loadCache() { - try { - const data = readFileSync(CACHE_FILE, "utf-8"); - const entries = JSON.parse(data); - for (const [key, value] of Object.entries(entries)) { - cache.set(key, value); - } - console.log(`[IGDB] Cache loaded: ${cache.size} entries`); - } catch { - console.log("[IGDB] No cache file found, starting fresh"); - } -} - -/** - * Save cache to JSON file on disk - */ -function saveCache() { - try { - mkdirSync(join(__dirname, "data"), { recursive: true }); - const obj = Object.fromEntries(cache); - writeFileSync(CACHE_FILE, JSON.stringify(obj, null, 2)); - } catch (err) { - console.error("[IGDB] Failed to save cache:", err.message); - } -} - -/** - * Get a valid Twitch access token (refreshes if expired) - */ -async function getIgdbToken() { - if (twitchToken && Date.now() < tokenExpiry) { - return twitchToken; - } - - const clientId = process.env.TWITCH_CLIENT_ID; - const clientSecret = process.env.TWITCH_CLIENT_SECRET; - - if (!clientId || !clientSecret) { - return null; - } - - const url = new URL("https://id.twitch.tv/oauth2/token"); - url.searchParams.set("client_id", clientId); - url.searchParams.set("client_secret", clientSecret); - url.searchParams.set("grant_type", "client_credentials"); - - const response = await fetch(url, { method: "POST" }); - - if (!response.ok) { - console.error( - `[IGDB] Twitch auth failed: ${response.status} ${response.statusText}`, - ); - return null; - } - - const data = await response.json(); - twitchToken = data.access_token; - // Refresh 5 minutes before actual expiry - tokenExpiry = Date.now() + (data.expires_in - 300) * 1000; - console.log("[IGDB] Twitch token acquired"); - return twitchToken; -} - -/** - * Make an IGDB API request with Apicalypse query - */ -async function igdbRequest(endpoint, query) { - const token = await getIgdbToken(); - if (!token) return []; - - const response = await fetch(`https://api.igdb.com/v4${endpoint}`, { - method: "POST", - headers: { - "Client-ID": process.env.TWITCH_CLIENT_ID, - Authorization: `Bearer ${token}`, - "Content-Type": "text/plain", - }, - body: query, - }); - - if (!response.ok) { - const text = await response.text(); - console.error(`[IGDB] API error: ${response.status} ${text}`); - return []; - } - - return response.json(); -} - -/** - * Sleep helper for rate limiting (4 req/sec max) - */ -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -/** - * Batch-resolve IGDB IDs for a list of source IDs - * @param {number} category - IGDB category (1=Steam, 2=GOG) - * @param {string[]} sourceIds - List of source-specific IDs - * @returns {Map} sourceId → igdbGameId - */ -async function batchResolve(category, sourceIds) { - const results = new Map(); - const BATCH_SIZE = 500; - - for (let i = 0; i < sourceIds.length; i += BATCH_SIZE) { - const batch = sourceIds.slice(i, i + BATCH_SIZE); - const uids = batch.map((id) => `"${id}"`).join(","); - const query = `fields game,uid; where category = ${category} & uid = (${uids}); limit ${BATCH_SIZE};`; - - const data = await igdbRequest("/external_games", query); - - for (const entry of data) { - if (entry.game && entry.uid) { - results.set(entry.uid, entry.game); - } - } - - // Rate limit: wait between batches - if (i + BATCH_SIZE < sourceIds.length) { - await sleep(260); - } - } - - return results; -} - -/** - * Enrich games with IGDB canonical IDs - * Graceful: if IGDB is unavailable or no credentials, games pass through unchanged - * @param {Array<{source: string, sourceId: string}>} games - * @returns {Promise} Games with canonicalId added where available - */ -export async function enrichGamesWithIgdb(games) { - // Check if IGDB credentials are configured - if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) { - return games; - } - - // Find uncached games, grouped by source - const uncachedBySource = {}; - for (const game of games) { - const cacheKey = `${game.source}:${game.sourceId}`; - if (!cache.has(cacheKey) && SOURCE_TO_CATEGORY[game.source]) { - if (!uncachedBySource[game.source]) { - uncachedBySource[game.source] = []; - } - uncachedBySource[game.source].push(game.sourceId); - } - } - - // Batch-resolve uncached games from IGDB - let newEntries = 0; - try { - for (const [source, sourceIds] of Object.entries(uncachedBySource)) { - const category = SOURCE_TO_CATEGORY[source]; - console.log( - `[IGDB] Resolving ${sourceIds.length} ${source} games (category ${category})...`, - ); - - const resolved = await batchResolve(category, sourceIds); - - for (const [uid, igdbId] of resolved) { - cache.set(`${source}:${uid}`, { igdbId }); - newEntries++; - } - - // Mark unresolved games as null so we don't re-query them - for (const uid of sourceIds) { - if (!resolved.has(uid)) { - cache.set(`${source}:${uid}`, { igdbId: null }); - } - } - } - - if (newEntries > 0) { - console.log( - `[IGDB] Resolved ${newEntries} new games, cache now has ${cache.size} entries`, - ); - saveCache(); - } - } catch (err) { - console.error("[IGDB] Enrichment failed (non-fatal):", err.message); - } - - // Enrich games with canonicalId from cache - return games.map((game) => { - const cached = cache.get(`${game.source}:${game.sourceId}`); - if (cached?.igdbId) { - return { ...game, canonicalId: String(cached.igdbId) }; - } - return game; - }); -} diff --git a/server/index.js b/server/index.js deleted file mode 100644 index d58de23..0000000 --- a/server/index.js +++ /dev/null @@ -1,175 +0,0 @@ -import express from "express"; -import cors from "cors"; -import fetch from "node-fetch"; -import { exchangeGogCode, fetchGogGames } from "./gog-backend.mjs"; -import { enrichGamesWithIgdb, loadCache } from "./igdb-cache.mjs"; - -const app = express(); -const PORT = process.env.PORT || 3000; - -// Enable CORS for your PWA -app.use( - cors({ - origin: process.env.ALLOWED_ORIGIN || "*", - }), -); - -app.use(express.json()); - -// Load IGDB cache on startup -loadCache(); - -// Health check -app.get("/health", (req, res) => { - res.json({ status: "ok" }); -}); - -// Steam API refresh endpoint -app.post("/steam/refresh", async (req, res) => { - const { apiKey, steamId } = req.body; - - console.log(`[Steam] Starting refresh for user: ${steamId}`); - - if (!apiKey || !steamId) { - console.log("[Steam] Missing credentials"); - return res.status(400).json({ - error: "Missing required fields: apiKey and steamId", - }); - } - - try { - // Call Steam Web API - const steamUrl = `https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${apiKey}&steamid=${steamId}&include_appinfo=1&include_played_free_games=1&format=json`; - - console.log("[Steam] Calling Steam API..."); - const response = await fetch(steamUrl); - console.log(`[Steam] Got response: ${response.status}`); - - if (!response.ok) { - console.log(`[Steam] Steam API error: ${response.statusText}`); - return res.status(response.status).json({ - error: "Steam API error", - message: response.statusText, - }); - } - - const data = await response.json(); - console.log(`[Steam] Success! Games count: ${data.response?.game_count || 0}`); - - const rawGames = data.response?.games || []; - - // Enrich with IGDB canonical IDs - const gamesForIgdb = rawGames.map((g) => ({ - ...g, - source: "steam", - sourceId: String(g.appid), - })); - const enriched = await enrichGamesWithIgdb(gamesForIgdb); - - // Return enriched games (source/sourceId/canonicalId included) - const transformed = { - games: enriched, - count: enriched.length, - }; - - const responseSize = JSON.stringify(transformed).length; - console.log(`[Steam] Sending response: ${responseSize} bytes, ${transformed.games.length} games`); - res.json(transformed); - console.log(`[Steam] Response sent successfully`); - } catch (error) { - console.error("[Steam] Exception:", error); - res.status(500).json({ - error: "Failed to fetch games", - message: error.message, - }); - } -}); - -// GOG API: Exchange auth code for tokens -app.post("/gog/auth", async (req, res) => { - const { code } = req.body; - - console.log("[GOG] Starting code exchange"); - - if (!code) { - return res.status(400).json({ error: "Missing required field: code" }); - } - - try { - const tokens = await exchangeGogCode(code); - console.log(`[GOG] Token exchange successful, user: ${tokens.user_id}`); - res.json(tokens); - } catch (error) { - console.error("[GOG] Token exchange error:", error); - res.status(500).json({ - error: "GOG token exchange failed", - message: error.message, - }); - } -}); - -// GOG API: Refresh games -app.post("/gog/refresh", async (req, res) => { - const { accessToken, refreshToken } = req.body; - - console.log("[GOG] Starting game refresh"); - - if (!accessToken || !refreshToken) { - return res.status(400).json({ - error: "Missing required fields: accessToken and refreshToken", - }); - } - - try { - const result = await fetchGogGames(accessToken, refreshToken); - result.games = await enrichGamesWithIgdb(result.games); - console.log(`[GOG] Success! ${result.count} games fetched`); - res.json(result); - } catch (error) { - console.error("[GOG] Refresh error:", error); - res.status(500).json({ - error: "GOG refresh failed", - message: error.message, - }); - } -}); - -// Fallback proxy for other Steam API calls -app.all("/*", async (req, res) => { - const path = req.url; - const steamUrl = `https://store.steampowered.com${path}`; - - console.log(`Proxying: ${req.method} ${steamUrl}`); - - try { - const response = await fetch(steamUrl, { - method: req.method, - headers: { - "User-Agent": "WhatToPlay/1.0", - Accept: "application/json", - ...(req.body && { "Content-Type": "application/json" }), - }, - ...(req.body && { body: JSON.stringify(req.body) }), - }); - - const contentType = response.headers.get("content-type"); - - if (contentType && contentType.includes("application/json")) { - const data = await response.json(); - res.json(data); - } else { - const text = await response.text(); - res.send(text); - } - } catch (error) { - console.error("Proxy error:", error); - res.status(500).json({ - error: "Proxy error", - message: error.message, - }); - } -}); - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 055a3ec..0000000 --- a/server/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "whattoplay-server", - "version": "1.0.0", - "type": "module", - "description": "Simple proxy server for WhatToPlay Steam API calls", - "main": "index.js", - "scripts": { - "start": "node index.js", - "dev": "node --watch index.js" - }, - "dependencies": { - "express": "^4.18.2", - "cors": "^2.8.5", - "node-fetch": "^3.3.2" - } -} diff --git a/server/steam-api.mjs b/server/steam-api.mjs deleted file mode 100644 index 06feba7..0000000 --- a/server/steam-api.mjs +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Steam API Handler für Vite Dev Server - * Fungiert als Proxy um CORS-Probleme zu vermeiden - */ - -import { fetchSteamGames } from "./steam-backend.mjs"; -import { enrichGamesWithIgdb } from "./igdb-cache.mjs"; - -export async function handleSteamRefresh(req, res) { - if (req.method !== "POST") { - res.statusCode = 405; - res.end("Method Not Allowed"); - return; - } - - let body = ""; - req.on("data", (chunk) => { - body += chunk.toString(); - }); - - req.on("end", async () => { - try { - let payload; - try { - payload = JSON.parse(body || "{}"); - } catch (error) { - res.statusCode = 400; - res.end( - JSON.stringify({ - error: "Ungültiges JSON im Request-Body", - }), - ); - return; - } - - const { apiKey, steamId } = payload; - - if (!apiKey || !steamId) { - res.statusCode = 400; - res.end(JSON.stringify({ error: "apiKey und steamId erforderlich" })); - return; - } - - const { games, count } = await fetchSteamGames(apiKey, steamId); - const enriched = await enrichGamesWithIgdb(games); - - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ games: enriched, count: enriched.length })); - } catch (error) { - res.statusCode = 500; - res.end( - JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - }), - ); - } - }); -} diff --git a/server/steam-backend.mjs b/server/steam-backend.mjs deleted file mode 100644 index b8169e5..0000000 --- a/server/steam-backend.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Steam Backend - Isoliertes Modul für Steam API Calls - * Keine Dependencies zu Vite oder Express - */ - -/** - * Ruft Steam API auf und gibt formatierte Spiele zurück - * @param {string} apiKey - Steam Web API Key - * @param {string} steamId - Steam User ID - * @returns {Promise<{games: Array, count: number}>} - */ -export async function fetchSteamGames(apiKey, steamId) { - if (!apiKey || !steamId) { - throw new Error("apiKey und steamId sind erforderlich"); - } - - // Steam API aufrufen - const url = new URL( - "https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/", - ); - url.searchParams.set("key", apiKey); - url.searchParams.set("steamid", steamId); - url.searchParams.set("include_appinfo", "true"); - url.searchParams.set("include_played_free_games", "true"); - - const response = await fetch(url); - - if (!response.ok) { - throw new Error( - `Steam API Error: ${response.status} ${response.statusText}`, - ); - } - - const data = await response.json(); - const rawGames = data.response?.games ?? []; - - // Spiele formatieren - const games = rawGames.map((game) => ({ - id: `steam-${game.appid}`, - title: game.name, - source: "steam", - sourceId: String(game.appid), - platform: "PC", - lastPlayed: game.rtime_last_played - ? new Date(game.rtime_last_played * 1000).toISOString().slice(0, 10) - : null, - playtimeHours: Math.round((game.playtime_forever / 60) * 10) / 10, - url: `https://store.steampowered.com/app/${game.appid}`, - })); - - return { - games, - count: games.length, - }; -} diff --git a/server/steam-backend.test.mjs b/server/steam-backend.test.mjs deleted file mode 100644 index 8785b0b..0000000 --- a/server/steam-backend.test.mjs +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Tests für Steam Backend - * Verwendung: node --test server/steam-backend.test.mjs - */ - -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { fetchSteamGames } from "./steam-backend.mjs"; -import { readFile } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Lade Test-Credentials aus config.local.json -async function loadTestConfig() { - try { - const configPath = join(__dirname, "..", "config.local.json"); - const configData = await readFile(configPath, "utf-8"); - const config = JSON.parse(configData); - return config.steam; - } catch { - return null; - } -} - -describe("Steam Backend", () => { - describe("fetchSteamGames()", () => { - it("sollte Fehler werfen wenn apiKey fehlt", async () => { - await assert.rejects( - async () => await fetchSteamGames(null, "12345"), - /apiKey und steamId sind erforderlich/, - ); - }); - - it("sollte Fehler werfen wenn steamId fehlt", async () => { - await assert.rejects( - async () => await fetchSteamGames("test-key", null), - /apiKey und steamId sind erforderlich/, - ); - }); - - it("sollte Spiele von echter Steam API laden", async () => { - const testConfig = await loadTestConfig(); - - if (!testConfig?.apiKey || !testConfig?.steamId) { - console.log("⚠️ Überspringe Test - config.local.json nicht vorhanden"); - return; - } - - const result = await fetchSteamGames( - testConfig.apiKey, - testConfig.steamId, - ); - - // Validiere Struktur - assert.ok(result, "Result sollte existieren"); - assert.ok( - typeof result.count === "number", - "count sollte eine Zahl sein", - ); - assert.ok(Array.isArray(result.games), "games sollte ein Array sein"); - assert.strictEqual( - result.count, - result.games.length, - "count sollte games.length entsprechen", - ); - - // Validiere erstes Spiel (wenn vorhanden) - if (result.games.length > 0) { - const firstGame = result.games[0]; - assert.ok(firstGame.id, "Spiel sollte ID haben"); - assert.ok(firstGame.title, "Spiel sollte Titel haben"); - assert.strictEqual(firstGame.platform, "PC", "Platform sollte PC sein"); - assert.strictEqual( - firstGame.source, - "steam", - "Source sollte steam sein", - ); - assert.ok( - typeof firstGame.playtimeHours === "number", - "playtimeHours sollte eine Zahl sein", - ); - assert.ok( - firstGame.url?.includes("steampowered.com"), - "URL sollte steampowered.com enthalten", - ); - - console.log(`\n✓ ${result.count} Spiele erfolgreich geladen`); - console.log( - ` Beispiel: "${firstGame.title}" (${firstGame.playtimeHours}h)`, - ); - } - }); - - it("sollte Fehler bei ungültigen Credentials werfen", async () => { - await assert.rejects( - async () => await fetchSteamGames("invalid-key", "invalid-id"), - /Steam API Error/, - ); - }); - }); -}); diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b370dfb..0000000 --- a/src/App.css +++ /dev/null @@ -1,5 +0,0 @@ -.content { - --padding-top: 16px; - --padding-start: 16px; - --padding-end: 16px; -} diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index b526df0..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - IonIcon, - IonLabel, - IonRouterOutlet, - IonTabBar, - IonTabButton, - IonTabs, - IonApp, -} from "@ionic/react"; -import { IonReactRouter } from "@ionic/react-router"; -import { - albumsOutline, - heartCircleOutline, - homeOutline, - libraryOutline, - settingsOutline, -} from "ionicons/icons"; -import { Redirect, Route, Switch } from "react-router-dom"; - -import DiscoverPage from "./pages/Discover/DiscoverPage"; -import HomePage from "./pages/Home/HomePage"; -import LibraryPage from "./pages/Library/LibraryPage"; -import PlaylistsPage from "./pages/Playlists/PlaylistsPage"; -import PlaylistDetailPage from "./pages/Playlists/PlaylistDetailPage"; -import SettingsPage from "./pages/Settings/SettingsPage"; -import SettingsDetailPage from "./pages/Settings/SettingsDetailPage"; - -import "./App.css"; - -export default function App() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/data/tutorials.ts b/src/data/tutorials.ts deleted file mode 100644 index 024209c..0000000 --- a/src/data/tutorials.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { - cloudOutline, - gameControllerOutline, - globeOutline, - shieldOutline, - storefrontOutline, -} from "ionicons/icons"; - -export interface TutorialStep { - title: string; - description: string; - code?: string; - hint?: string; -} - -export interface Tutorial { - title: string; - icon: string; - steps: TutorialStep[]; - tips: string[]; -} - -export const TUTORIALS: Record = { - steam: { - title: "Steam API Key & ID einrichten", - icon: gameControllerOutline, - steps: [ - { - title: "1. Gehe zu Steam Web API", - description: - "Öffne https://steamcommunity.com/dev/apikey in deinem Browser", - code: "https://steamcommunity.com/dev/apikey", - }, - { - title: "2. Login & Registrierung", - description: - "Falls nötig, akzeptiere die Vereinbarungen und registriere dich", - hint: "Du brauchst einen Steam Account mit mindestens 5€ Spielezeit", - }, - { - title: "3. API Key kopieren", - description: "Kopiere deinen generierten API Key aus dem Textfeld", - hint: "Halte diesen Key privat! Teile ihn nicht öffentlich!", - }, - { - title: "4. Steam ID finden", - description: "Gehe zu https://www.steamcommunity.com/", - code: "https://www.steamcommunity.com/", - }, - { - title: "5. Profil öffnen", - description: "Klicke auf deinen Namen oben rechts", - hint: "Die URL sollte /profiles/[STEAM_ID]/ enthalten", - }, - { - title: "6. Steam ID kopieren", - description: "Kopiere die Nummern aus der URL (z.B. 76561197960434622)", - hint: "Das ist eine lange Nummer, keine Kurzform", - }, - ], - tips: [ - "Der API Key wird automatisch alle 24 Stunden zurückgesetzt", - "Dein Game-Profil muss auf 'Öffentlich' gestellt sein", - "Private Games werden nicht angezeigt", - ], - }, - - gog: { - title: "GOG Galaxy Login", - icon: globeOutline, - steps: [ - { - title: "1. OAuth Proxy starten", - description: "Im Terminal: npm run oauth", - code: "npm run oauth", - hint: "Startet lokalen OAuth Proxy auf Port 3001", - }, - { - title: "2. Mit GOG einloggen", - description: "Klicke auf 'Mit GOG einloggen' in der App", - hint: "Du wirst zu GOG weitergeleitet", - }, - { - title: "3. Bei GOG anmelden", - description: "Melde dich mit deinen GOG Zugangsdaten an", - hint: "Akzeptiere die Berechtigungen", - }, - { - title: "4. Automatisch verbunden", - description: "Nach der Anmeldung wirst du zurück zur App geleitet", - hint: "Dein Token wird automatisch gespeichert", - }, - ], - tips: [ - "Der OAuth Proxy muss laufen (npm run oauth)", - "Tokens werden automatisch erneuert", - "Für Production: Deploy den Worker zu Cloudflare", - ], - }, - - epic: { - title: "Epic Games (Manueller Import)", - icon: shieldOutline, - steps: [ - { - title: "1. Keine API verfügbar", - description: "Epic Games hat KEINE öffentliche API für Bibliotheken", - hint: "Auch OAuth ist nicht möglich", - }, - { - title: "2. JSON-Datei erstellen", - description: "Erstelle eine JSON-Datei mit deinen Spielen", - code: `[ - {"name": "Fortnite", "appId": "fortnite"}, - {"name": "Rocket League", "appId": "rocket-league"} -]`, - }, - { - title: "3. Datei hochladen", - description: "Klicke auf 'Games JSON importieren' und wähle deine Datei", - hint: "Unterstützt auch {games: [...]} Format", - }, - ], - tips: [ - "Epic erlaubt keinen API-Zugriff auf Libraries", - "Manuelle Import ist die einzige Option", - "Spiele-Namen aus Epic Launcher abschreiben", - ], - }, - - amazon: { - title: "Amazon Games (Manueller Import)", - icon: storefrontOutline, - steps: [ - { - title: "1. Keine API verfügbar", - description: "Amazon hat KEINE öffentliche API für Prime Gaming", - hint: "Auch OAuth ist nicht möglich", - }, - { - title: "2. Spiele-Liste erstellen", - description: "Gehe zu gaming.amazon.com und notiere deine Spiele", - code: "https://gaming.amazon.com/home", - }, - { - title: "3. JSON-Datei erstellen", - description: "Erstelle eine JSON-Datei mit deinen Spielen", - code: `[ - {"name": "Fallout 76", "source": "prime"}, - {"name": "Control", "source": "prime"} -]`, - }, - { - title: "4. Datei hochladen", - description: "Klicke auf 'Games JSON importieren' und wähle deine Datei", - hint: "source: 'prime' oder 'luna'", - }, - ], - tips: [ - "Amazon erlaubt keinen API-Zugriff", - "Manuelle Import ist die einzige Option", - "Prime Gaming Spiele wechseln monatlich", - ], - }, - - blizzard: { - title: "Blizzard OAuth Setup", - icon: cloudOutline, - steps: [ - { - title: "1. Battle.net Developers", - description: "Gehe zu https://develop.battle.net und melde dich an", - code: "https://develop.battle.net", - }, - { - title: "2. API-Zugang anfordern", - description: "Klicke auf 'Create Application' oder gehe zu API Access", - hint: "Du brauchst einen Account mit mindestens einem Blizzard-Spiel", - }, - { - title: "3. App registrieren", - description: - "Gebe einen Namen ein (z.B. 'WhatToPlay') und akzeptiere Terms", - hint: "Das ist für deine persönliche Nutzung", - }, - { - title: "4. Client ID kopieren", - description: "Kopiere die 'Client ID' aus deiner API-Anwendung", - code: "Client ID: xxx-xxx-xxx", - }, - { - title: "5. Client Secret kopieren", - description: - "Generiere und kopiere das 'Client Secret' (einmalig sichtbar!)", - hint: "Speichere es sicher! Du kannst es später nicht mehr sehen!", - }, - { - title: "6. OAuth Callback URL", - description: - "Setze die Redirect URI auf https://whattoplay.local/auth/callback", - hint: "Dies ist für lokale Entwicklung", - }, - ], - tips: [ - "Blizzard supports: WoW, Diablo, Overwatch, StarCraft, Heroes", - "Für Production brauchst du ein Backend für OAuth", - "Der API Access kann bis zu 24 Stunden dauern", - ], - }, -}; diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index f80da77..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import { setupIonicReact } from "@ionic/react"; - -import App from "./App"; - -import "@ionic/react/css/core.css"; -import "@ionic/react/css/normalize.css"; -import "@ionic/react/css/structure.css"; -import "@ionic/react/css/typography.css"; -import "@ionic/react/css/padding.css"; -import "@ionic/react/css/float-elements.css"; -import "@ionic/react/css/text-alignment.css"; -import "@ionic/react/css/text-transformation.css"; -import "@ionic/react/css/flex-utils.css"; -import "@ionic/react/css/display.css"; - -import "./theme/variables.css"; - -setupIonicReact({ mode: "ios" }); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); diff --git a/src/pages/Discover/DiscoverPage.css b/src/pages/Discover/DiscoverPage.css deleted file mode 100644 index 1be95ef..0000000 --- a/src/pages/Discover/DiscoverPage.css +++ /dev/null @@ -1,241 +0,0 @@ -.discover-content { - --padding-top: 16px; - --padding-start: 16px; - --padding-end: 16px; -} - -/* States: loading, empty */ -.discover-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 2rem; - text-align: center; - gap: 0.5rem; -} - -.discover-state p { - margin: 0; - color: #8e8e93; -} - -.discover-state-hint { - font-size: 0.85rem; -} - -/* Done state */ -.discover-done { - background: #ffffff; - border-radius: 20px; - padding: 2rem; - text-align: center; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); - margin-top: 2rem; -} - -.discover-done h2 { - margin: 0 0 1.5rem; - font-size: 1.5rem; -} - -.discover-done-stats { - display: flex; - justify-content: center; - gap: 2rem; - margin-bottom: 1.5rem; -} - -.discover-done-stat { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; -} - -.discover-done-stat strong { - font-size: 2rem; - font-weight: 700; -} - -.discover-done-stat span { - font-size: 0.85rem; - color: #8e8e93; -} - -/* Progress bar */ -.discover-progress { - display: flex; - align-items: center; - gap: 0.75rem; - margin-bottom: 1.5rem; -} - -.discover-progress span { - font-size: 0.85rem; - color: #8e8e93; - white-space: nowrap; -} - -.discover-progress-bar { - flex: 1; - height: 4px; - background: #e5e5ea; - border-radius: 2px; - overflow: hidden; -} - -.discover-progress-fill { - height: 100%; - background: var(--ion-color-primary, #0a84ff); - border-radius: 2px; - transition: width 0.3s ease; -} - -/* Card stack */ -.discover-stack { - position: relative; - width: 100%; - aspect-ratio: 3 / 4; - max-height: 55vh; - margin: 0 auto; -} - -.discover-stack > div { - position: absolute; - width: 100%; - height: 100%; -} - -/* Game card */ -.discover-card { - position: absolute; - width: 100%; - height: 100%; - background: #ffffff; - border-radius: 20px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - user-select: none; - cursor: grab; - overflow: hidden; -} - -.discover-card:active { - cursor: grabbing; -} - -.discover-card-behind { - pointer-events: none; -} - -/* Card image */ -.discover-card-image { - width: 100%; - height: 45%; - min-height: 120px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - overflow: hidden; - flex-shrink: 0; -} - -.discover-card-image img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; -} - -/* Card body */ -.discover-card-body { - flex: 1; - padding: 1.25rem; - display: flex; - flex-direction: column; - min-height: 0; -} - -.discover-card-source { - margin-bottom: 0.5rem; -} - -.discover-card-title { - font-size: 1.25rem; - font-weight: 700; - margin: 0 0 auto; - line-height: 1.2; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.discover-card-details { - display: flex; - gap: 1.5rem; - padding-top: 0.75rem; -} - -.discover-card-detail { - display: flex; - flex-direction: column; - gap: 0.15rem; -} - -.discover-card-detail-label { - font-size: 0.7rem; - color: #8e8e93; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.discover-card-detail-value { - font-size: 0.9rem; - font-weight: 600; -} - -/* Action buttons */ -.discover-actions { - display: flex; - justify-content: center; - gap: 2rem; - padding: 1.5rem 0; -} - -.discover-action-btn { - width: 64px; - height: 64px; - border-radius: 50%; - border: 2px solid; - background: #ffffff; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.75rem; - cursor: pointer; - transition: transform 0.15s ease, box-shadow 0.15s ease; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); -} - -.discover-action-btn:active { - transform: scale(0.92); -} - -.discover-action-skip { - border-color: #ff3b30; - color: #ff3b30; -} - -.discover-action-skip:hover { - background: #fff5f5; -} - -.discover-action-like { - border-color: #34c759; - color: #34c759; -} - -.discover-action-like:hover { - background: #f0faf3; -} diff --git a/src/pages/Discover/DiscoverPage.tsx b/src/pages/Discover/DiscoverPage.tsx deleted file mode 100644 index ce96a5b..0000000 --- a/src/pages/Discover/DiscoverPage.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { - IonAlert, - IonBadge, - IonButton, - IonContent, - IonHeader, - IonIcon, - IonPage, - IonSpinner, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { closeOutline, checkmarkOutline, refreshOutline } from "ionicons/icons"; -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - type SyntheticEvent, -} from "react"; -import TinderCard from "react-tinder-card"; -import { db, type Game, type Playlist } from "../../services/Database"; - -import "./DiscoverPage.css"; - -const formatDate = (value?: string | null) => { - if (!value) return "-"; - return new Date(value).toLocaleDateString("de"); -}; - -const formatPlaytime = (hours?: number) => { - if (!hours) return "0 h"; - if (hours < 1) return `${Math.round(hours * 60)} min`; - return `${hours.toFixed(1)} h`; -}; - -export default function DiscoverPage() { - const [games, setGames] = useState([]); - const [playlists, setPlaylists] = useState([]); - const [loading, setLoading] = useState(true); - const [showResetAlert, setShowResetAlert] = useState(false); - - const cardRefs = useRef>(new Map()); - - useEffect(() => { - let active = true; - - const load = async () => { - try { - setLoading(true); - const [dbGames, dbPlaylists] = await Promise.all([ - db.getGames(), - db.getPlaylists(), - ]); - - if (active) { - setGames(dbGames); - setPlaylists(dbPlaylists); - } - } finally { - if (active) setLoading(false); - } - }; - - load(); - return () => { - active = false; - }; - }, []); - - const unseenGames = useMemo(() => { - const allSwipedGameIds = new Set(playlists.flatMap((p) => p.gameIds)); - return games.filter((g) => !allSwipedGameIds.has(g.id)); - }, [games, playlists]); - - const handleSwipe = useCallback(async (direction: string, gameId: string) => { - const playlistId = - direction === "right" ? "want-to-play" : "not-interesting"; - await db.addGameToPlaylist(playlistId, gameId); - - // Reload playlists to update UI - const updatedPlaylists = await db.getPlaylists(); - setPlaylists(updatedPlaylists); - }, []); - - const swipeButton = useCallback( - (direction: "left" | "right") => { - if (unseenGames.length === 0) return; - const topGame = unseenGames[unseenGames.length - 1]; - const topIndex = games.indexOf(topGame); - const ref = cardRefs.current.get(topIndex); - if (ref) { - ref.swipe(direction); - } - }, - [unseenGames, games], - ); - - const handleReset = useCallback(async () => { - // Clear both playlists - const wantToPlay = playlists.find((p) => p.id === "want-to-play"); - const notInteresting = playlists.find((p) => p.id === "not-interesting"); - - if (wantToPlay) { - await db.createPlaylist({ ...wantToPlay, gameIds: [] }); - } - if (notInteresting) { - await db.createPlaylist({ ...notInteresting, gameIds: [] }); - } - - // Reload playlists - const updatedPlaylists = await db.getPlaylists(); - setPlaylists(updatedPlaylists); - }, [playlists]); - - const wantToPlay = playlists.find((p) => p.id === "want-to-play"); - const notInteresting = playlists.find((p) => p.id === "not-interesting"); - const totalSwiped = - (wantToPlay?.gameIds.length || 0) + (notInteresting?.gameIds.length || 0); - const interestedCount = wantToPlay?.gameIds.length || 0; - const skippedCount = notInteresting?.gameIds.length || 0; - - return ( - - - - Entdecken - {totalSwiped > 0 && ( - setShowResetAlert(true)} - color="medium" - > - - - )} - - - - setShowResetAlert(false)} - header="Zurucksetzen?" - message={`Alle ${totalSwiped} Swipe-Entscheidungen werden geloscht.`} - buttons={[ - { text: "Abbrechen", role: "cancel" }, - { - text: "Zurucksetzen", - role: "destructive", - handler: handleReset, - }, - ]} - /> - - - - - Entdecken - - - - {loading ? ( -
- -

Lade Spiele ...

-
- ) : games.length === 0 ? ( -
-

Keine Spiele vorhanden.

-

- Importiere zuerst Spiele in den Einstellungen. -

-
- ) : unseenGames.length === 0 ? ( -
-

Alle Spiele gesehen!

-
-
- {interestedCount} - Interessiert -
-
- {skippedCount} - Ubersprungen -
-
- - - Nochmal starten - -
- ) : ( - <> -
- - {totalSwiped} / {games.length} Spiele - -
-
-
-
- -
- {unseenGames.slice(-3).map((game, i, arr) => { - const globalIndex = games.indexOf(game); - const stackPosition = arr.length - 1 - i; - return ( - { - if (ref) { - cardRefs.current.set(globalIndex, ref); - } else { - cardRefs.current.delete(globalIndex); - } - }} - key={game.id} - onSwipe={(dir: string) => handleSwipe(dir, game.id)} - preventSwipe={["up", "down"]} - swipeRequirementType="position" - swipeThreshold={80} - > -
0 ? "discover-card-behind" : ""}`} - style={{ - zIndex: arr.length - stackPosition, - transform: `scale(${1 - stackPosition * 0.04}) translateY(${stackPosition * 12}px)`, - }} - > -
- {game.title}) => { - e.currentTarget.style.display = "none"; - }} - /> -
-
-
- - {game.source ?? "Unbekannt"} - -
-

{game.title}

-
-
- - Spielzeit - - - {formatPlaytime(game.playtimeHours)} - -
-
- - Zuletzt gespielt - - - {formatDate(game.lastPlayed)} - -
-
-
-
-
- ); - })} -
- -
- - -
- - )} - - - ); -} diff --git a/src/pages/Home/HomePage.css b/src/pages/Home/HomePage.css deleted file mode 100644 index 9b96117..0000000 --- a/src/pages/Home/HomePage.css +++ /dev/null @@ -1,23 +0,0 @@ -.home-content { - --padding-top: 16px; - --padding-start: 16px; - --padding-end: 16px; -} - -.home-placeholder { - background: #ffffff; - border-radius: 20px; - padding: 2rem; - text-align: center; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); -} - -.home-placeholder h2 { - margin: 0 0 0.5rem; - font-size: 1.5rem; -} - -.home-placeholder p { - margin: 0; - color: #8e8e93; -} diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx deleted file mode 100644 index a1e3437..0000000 --- a/src/pages/Home/HomePage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - IonContent, - IonHeader, - IonPage, - IonTitle, - IonToolbar, -} from "@ionic/react"; - -import "./HomePage.css"; - -export default function HomePage() { - return ( - - - - Home - - - - - - Home - - - -
-

Willkommen bei WhatToPlay

-

Helper-Widgets und Statistiken kommen hier später rein.

-
-
-
- ); -} diff --git a/src/pages/Library/LibraryPage.css b/src/pages/Library/LibraryPage.css deleted file mode 100644 index 63de344..0000000 --- a/src/pages/Library/LibraryPage.css +++ /dev/null @@ -1,81 +0,0 @@ -.library-content { - --padding-top: 16px; - --padding-start: 16px; - --padding-end: 16px; -} - -.library-searchbar { - --background: transparent; - --border-radius: 0; - --padding-top: 8px; - --padding-start: 8px; - --padding-end: 8px; - --padding-bottom: 8px; -} - -.action-sheet-button.sort-action-active { - color: #007aff; - font-weight: 600; -} - - flex-wrap: wrap; - justify-content: space-between; - gap: 1.5rem; - margin-bottom: 1.5rem; -} - -.hero h1 { - margin: 0 0 0.4rem; - font-size: 1.8rem; -} - -.hero p { - margin: 0; - color: #6b6f78; - max-width: 420px; -} - -.hero-stats { - display: grid; - grid-template-columns: repeat(2, minmax(120px, 1fr)); - gap: 1rem; -} - -.hero-stats div { - background: #f2f2f7; - border-radius: 16px; - padding: 0.8rem 0.9rem; - text-align: center; -} - -.hero-stats span { - color: #8e8e93; - font-size: 0.8rem; -} - -.hero-stats strong { - display: block; - font-size: 1.4rem; - margin-top: 0.2rem; -} - -.state { - padding: 2rem; - text-align: center; - color: #8e8e93; -} - -.state.error { - color: #ff453a; -} - -.game-list { - margin-bottom: 2rem; -} - -.game-list ion-item { - --padding-start: 16px; - --padding-end: 16px; - --inner-padding-end: 12px; - -} diff --git a/src/pages/Library/LibraryPage.tsx b/src/pages/Library/LibraryPage.tsx deleted file mode 100644 index 678d1af..0000000 --- a/src/pages/Library/LibraryPage.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import { - IonBadge, - IonActionSheet, - IonButton, - IonContent, - IonHeader, - IonIcon, - IonItem, - IonLabel, - IonList, - IonNote, - IonPage, - IonSearchbar, - IonSpinner, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { heart, heartOutline, swapVerticalOutline } from "ionicons/icons"; -import { useEffect, useMemo, useState } from "react"; -import { db, type Game } from "../../services/Database"; - -import "./LibraryPage.css"; - -const formatDate = (value?: string | null) => { - if (!value) return "-"; - return new Date(value).toLocaleDateString("de"); -}; - -const normalizeTitle = (title: string) => - title.toLowerCase().replace(/[:\-]/g, " ").replace(/\s+/g, " ").trim(); - -const mergeGames = (allGames: Game[]) => { - const map = new Map(); - - allGames.forEach((game) => { - if (!game.title) return; - - // Primary: canonicalId (IGDB), fallback: normalized title - const key = game.canonicalId || `title:${normalizeTitle(game.title)}`; - const existing = map.get(key); - - if (!existing) { - map.set(key, { ...game }); - } else { - // Merge: bevorzuge neuestes lastPlayed und summiere playtime - if ( - game.lastPlayed && - (!existing.lastPlayed || game.lastPlayed > existing.lastPlayed) - ) { - existing.lastPlayed = game.lastPlayed; - } - existing.playtimeHours = - (existing.playtimeHours || 0) + (game.playtimeHours || 0); - } - }); - - return Array.from(map.values()); -}; - -export default function LibraryPage() { - const [games, setGames] = useState([]); - const [favoriteIds, setFavoriteIds] = useState>(new Set()); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [searchText, setSearchText] = useState(""); - const [sortBy, setSortBy] = useState<"title" | "playtime" | "lastPlayed">( - "playtime", - ); - const [showSortSheet, setShowSortSheet] = useState(false); - const [displayCount, setDisplayCount] = useState(20); - const INITIAL_RENDER_COUNT = 20; - const BACKGROUND_CHUNK_SIZE = 50; - const BACKGROUND_CHUNK_DELAY = 50; - - useEffect(() => { - let active = true; - - const load = async () => { - try { - setLoading(true); - - const [dbGames, favPlaylist] = await Promise.all([ - db.getGames(), - db.getPlaylist("favorites"), - ]); - - if (active) { - setGames(mergeGames(dbGames)); - setFavoriteIds(new Set(favPlaylist?.gameIds ?? [])); - setError(null); - } - } catch (err) { - if (active) { - setError(err instanceof Error ? err.message : "Unbekannter Fehler"); - } - } finally { - if (active) { - setLoading(false); - } - } - }; - - load(); - return () => { - active = false; - }; - }, []); - - const totalPlaytime = useMemo(() => { - return games.reduce( - (sum: number, game: Game) => sum + (game.playtimeHours ?? 0), - 0, - ); - }, [games]); - - const filteredAndSortedGames = useMemo(() => { - let filtered = games.filter( - (game) => - game.title && - game.title.toLowerCase().includes(searchText.toLowerCase()), - ); - - filtered.sort((a, b) => { - if (sortBy === "title") { - return a.title.localeCompare(b.title, "de"); - } else if (sortBy === "playtime") { - return (b.playtimeHours ?? 0) - (a.playtimeHours ?? 0); - } - const aDate = a.lastPlayed ? new Date(a.lastPlayed).getTime() : 0; - const bDate = b.lastPlayed ? new Date(b.lastPlayed).getTime() : 0; - return bDate - aDate; - }); - - return filtered; - }, [games, searchText, sortBy]); - - useEffect(() => { - let timeoutId: number | undefined; - - const targetCount = filteredAndSortedGames.length; - setDisplayCount(Math.min(INITIAL_RENDER_COUNT, targetCount)); - - const scheduleNextChunk = () => { - timeoutId = window.setTimeout(() => { - setDisplayCount((prev) => { - const next = Math.min(prev + BACKGROUND_CHUNK_SIZE, targetCount); - if (next < targetCount) { - scheduleNextChunk(); - } - return next; - }); - }, BACKGROUND_CHUNK_DELAY); - }; - - if (targetCount > INITIAL_RENDER_COUNT) { - scheduleNextChunk(); - } - - return () => { - if (timeoutId !== undefined) { - window.clearTimeout(timeoutId); - } - }; - }, [ - filteredAndSortedGames, - INITIAL_RENDER_COUNT, - BACKGROUND_CHUNK_SIZE, - BACKGROUND_CHUNK_DELAY, - ]); - - const displayedGames = useMemo(() => { - return filteredAndSortedGames.slice(0, displayCount); - }, [filteredAndSortedGames, displayCount]); - - const handleToggleFavorite = async (gameId: string) => { - if (favoriteIds.has(gameId)) { - await db.removeGameFromPlaylist("favorites", gameId); - setFavoriteIds((prev) => { - const next = new Set(prev); - next.delete(gameId); - return next; - }); - } else { - await db.addGameToPlaylist("favorites", gameId); - setFavoriteIds((prev) => new Set(prev).add(gameId)); - } - }; - - return ( - - - - Bibliothek - setShowSortSheet(true)} - color="primary" - > - - - - - setSearchText(e.detail.value || "")} - className="library-searchbar" - /> - - - setShowSortSheet(false)} - header="Sortieren nach" - buttons={[ - { - text: "Titel", - cssClass: sortBy === "title" ? "sort-action-active" : "", - handler: () => setSortBy("title"), - }, - { - text: "Spielzeit", - cssClass: sortBy === "playtime" ? "sort-action-active" : "", - handler: () => setSortBy("playtime"), - }, - { - text: "Zuletzt gespielt", - cssClass: sortBy === "lastPlayed" ? "sort-action-active" : "", - handler: () => setSortBy("lastPlayed"), - }, - { text: "Abbrechen", role: "cancel" }, - ]} - /> - - - - Bibliothek - - - -
-
-

Spielebibliothek

-

- Deine Spiele aus allen Quellen. -

-
-
-
- Spiele - {games.length} -
-
- Spielzeit (h) - {totalPlaytime.toFixed(1)} -
-
-
- - {loading ? ( -
- -

Lade Spiele …

-
- ) : error ? ( -
-

{error}

-
- ) : filteredAndSortedGames.length === 0 ? ( -
-

- {searchText ? "Keine Spiele gefunden" : "Keine Spiele vorhanden"} -

-
- ) : ( - - {displayedGames.map((game) => ( - - { - e.preventDefault(); - e.stopPropagation(); - handleToggleFavorite(game.id); - }} - style={{ cursor: "pointer", fontSize: "22px" }} - /> - -

{game.title}

-

Zuletzt gespielt: {formatDate(game.lastPlayed)}

-
- - - {game.playtimeHours ?? 0} h - - -
- ))} -
- )} -
-
- ); -} diff --git a/src/pages/Playlists/PlaylistDetailPage.css b/src/pages/Playlists/PlaylistDetailPage.css deleted file mode 100644 index d92877a..0000000 --- a/src/pages/Playlists/PlaylistDetailPage.css +++ /dev/null @@ -1,29 +0,0 @@ -.detail-loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.detail-name-edit { - padding: 0 16px; -} - -.detail-name-edit .name-input { - font-size: 18px; - font-weight: 600; -} - -.detail-search { - padding: 0 4px; -} - -.action-icon { - cursor: pointer; - font-size: 22px; -} - -.source-badge { - font-size: 11px; - font-weight: 500; -} diff --git a/src/pages/Playlists/PlaylistDetailPage.tsx b/src/pages/Playlists/PlaylistDetailPage.tsx deleted file mode 100644 index 71e236d..0000000 --- a/src/pages/Playlists/PlaylistDetailPage.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { - IonBackButton, - IonBadge, - IonButtons, - IonContent, - IonHeader, - IonIcon, - IonInput, - IonItem, - IonItemOption, - IonItemOptions, - IonItemSliding, - IonLabel, - IonList, - IonListHeader, - IonPage, - IonSearchbar, - IonSpinner, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { useState, useEffect, useMemo, useCallback } from "react"; -import { useParams } from "react-router-dom"; -import { - addCircleOutline, - heart, - heartOutline, - removeCircleOutline, -} from "ionicons/icons"; -import { db, type Playlist, type Game } from "../../services/Database"; - -import "./PlaylistDetailPage.css"; - -export default function PlaylistDetailPage() { - const { playlistId } = useParams<{ playlistId: string }>(); - const [playlist, setPlaylist] = useState(null); - const [games, setGames] = useState([]); - const [favoriteIds, setFavoriteIds] = useState>(new Set()); - const [loading, setLoading] = useState(true); - const [searchText, setSearchText] = useState(""); - const [editName, setEditName] = useState(""); - - const load = useCallback(async () => { - const [pl, allGames, favPlaylist] = await Promise.all([ - db.getPlaylist(playlistId), - db.getGames(), - db.getPlaylist("favorites"), - ]); - setPlaylist(pl); - setGames(allGames); - setFavoriteIds(new Set(favPlaylist?.gameIds ?? [])); - if (pl) setEditName(pl.name); - setLoading(false); - }, [playlistId]); - - useEffect(() => { - load(); - }, [load]); - - const playlistGames = useMemo(() => { - if (!playlist) return []; - return playlist.gameIds - .map((id) => games.find((g) => g.id === id)) - .filter(Boolean) as Game[]; - }, [playlist, games]); - - const searchResults = useMemo(() => { - if (!searchText.trim() || !playlist) return []; - const term = searchText.toLowerCase(); - const inPlaylist = new Set(playlist.gameIds); - return games - .filter( - (g) => g.title.toLowerCase().includes(term) && !inPlaylist.has(g.id), - ) - .slice(0, 20); - }, [searchText, games, playlist]); - - const handleAddGame = async (gameId: string) => { - await db.addGameToPlaylist(playlistId, gameId); - const updated = await db.getPlaylist(playlistId); - setPlaylist(updated); - }; - - const handleRemoveGame = async (gameId: string) => { - await db.removeGameFromPlaylist(playlistId, gameId); - const updated = await db.getPlaylist(playlistId); - setPlaylist(updated); - }; - - const handleToggleFavorite = async (gameId: string) => { - if (favoriteIds.has(gameId)) { - await db.removeGameFromPlaylist("favorites", gameId); - setFavoriteIds((prev) => { - const next = new Set(prev); - next.delete(gameId); - return next; - }); - } else { - await db.addGameToPlaylist("favorites", gameId); - setFavoriteIds((prev) => new Set(prev).add(gameId)); - } - // If we're viewing the favorites playlist, reload it - if (playlistId === "favorites") { - const updated = await db.getPlaylist("favorites"); - setPlaylist(updated); - } - }; - - const handleNameBlur = async () => { - if (!playlist || playlist.isStatic) return; - const trimmed = editName.trim() || "Neue Liste"; - if (trimmed !== playlist.name) { - await db.updatePlaylist(playlistId, { name: trimmed }); - setPlaylist((prev) => (prev ? { ...prev, name: trimmed } : prev)); - } - }; - - if (loading) { - return ( - - - - - - - Playlist - - - -
- -
-
-
- ); - } - - if (!playlist) { - return ( - - - - - - - Nicht gefunden - - - -

- Playlist nicht gefunden. -

-
-
- ); - } - - return ( - - - - - - - {playlist.name} - - - - - - {playlist.name} - - - - {/* Editable name for custom playlists */} - {!playlist.isStatic && ( -
- setEditName(e.detail.value ?? "")} - onIonBlur={handleNameBlur} - className="name-input" - /> -
- )} - - {/* Search to add games */} -
- setSearchText(e.detail.value ?? "")} - debounce={200} - /> -
- - {searchResults.length > 0 && ( - - - Suchergebnisse - - {searchResults.map((game) => ( - - -

{game.title}

- {game.source && ( -

- - {game.source} - -

- )} -
- handleAddGame(game.id)} - className="action-icon" - /> -
- ))} -
- )} - - {/* Playlist games */} - - - - {playlist.gameIds.length}{" "} - {playlist.gameIds.length === 1 ? "Spiel" : "Spiele"} - - - {playlistGames.length === 0 ? ( - - - Keine Spiele in dieser Playlist - - - ) : ( - playlistGames.map((game) => ( - - - handleToggleFavorite(game.id)} - className="action-icon" - /> - -

{game.title}

- {game.source && ( -

- - {game.source} - -

- )} -
-
- - handleRemoveGame(game.id)} - > - - - -
- )) - )} -
-
-
- ); -} diff --git a/src/pages/Playlists/PlaylistsPage.css b/src/pages/Playlists/PlaylistsPage.css deleted file mode 100644 index 6247d59..0000000 --- a/src/pages/Playlists/PlaylistsPage.css +++ /dev/null @@ -1,11 +0,0 @@ -.playlists-loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.chevron-icon { - font-size: 16px; - margin-left: 4px; -} diff --git a/src/pages/Playlists/PlaylistsPage.tsx b/src/pages/Playlists/PlaylistsPage.tsx deleted file mode 100644 index 5801eb7..0000000 --- a/src/pages/Playlists/PlaylistsPage.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { - IonBadge, - IonButton, - IonButtons, - IonContent, - IonHeader, - IonIcon, - IonItem, - IonItemOption, - IonItemOptions, - IonItemSliding, - IonLabel, - IonList, - IonListHeader, - IonPage, - IonSpinner, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { useState, useEffect, useCallback } from "react"; -import { useHistory } from "react-router-dom"; -import { - addOutline, - chevronForward, - heartOutline, - gameControllerOutline, - thumbsDownOutline, - listOutline, - trashOutline, -} from "ionicons/icons"; -import { db, type Playlist } from "../../services/Database"; - -import "./PlaylistsPage.css"; - -const STATIC_ORDER = ["favorites", "want-to-play", "not-interesting"]; -const STATIC_ICONS: Record = { - favorites: heartOutline, - "want-to-play": gameControllerOutline, - "not-interesting": thumbsDownOutline, -}; - -export default function PlaylistsPage() { - const [playlists, setPlaylists] = useState([]); - const [loading, setLoading] = useState(true); - const history = useHistory(); - - const load = useCallback(async () => { - const all = await db.getPlaylists(); - setPlaylists(all); - setLoading(false); - }, []); - - useEffect(() => { - load(); - }, [load]); - - // Reload when returning to this page (ionViewWillEnter equivalent) - useEffect(() => { - const unlisten = history.listen((location) => { - if (location.pathname === "/playlists") { - load(); - } - }); - return unlisten; - }, [history, load]); - - const staticPlaylists = STATIC_ORDER.map((id) => - playlists.find((p) => p.id === id), - ).filter(Boolean) as Playlist[]; - - const customPlaylists = playlists - .filter((p) => !p.isStatic) - .sort( - (a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), - ); - - const handleAdd = async () => { - const id = `custom-${Date.now()}`; - await db.createPlaylist({ - id, - name: "Neue Liste", - gameIds: [], - isStatic: false, - createdAt: new Date().toISOString(), - }); - history.push(`/playlists/${id}`); - }; - - const handleDelete = async (id: string) => { - await db.deletePlaylist(id); - await load(); - }; - - return ( - - - - Playlists - - - - - - - - - - - Playlists - - - - {loading ? ( -
- -
- ) : ( - <> - - {staticPlaylists.map((playlist) => ( - - - {playlist.name} - - {playlist.gameIds.length} - - - - ))} - - - - - Eigene Playlists - - {customPlaylists.length === 0 ? ( - - - Keine eigenen Playlists - - - ) : ( - customPlaylists.map((playlist) => ( - - - - {playlist.name} - - {playlist.gameIds.length} - - - - - handleDelete(playlist.id)} - > - - - - - )) - )} - - - )} -
-
- ); -} diff --git a/src/pages/Settings/SettingsDetailPage.css b/src/pages/Settings/SettingsDetailPage.css deleted file mode 100644 index 1c6e70d..0000000 --- a/src/pages/Settings/SettingsDetailPage.css +++ /dev/null @@ -1,69 +0,0 @@ -.settings-detail-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 18px 8px; - color: var(--ion-color-medium); -} - -.settings-detail-header h2 { - margin: 0; - font-size: 1.1rem; - color: var(--ion-text-color, #111); -} - -.settings-detail-header p { - margin: 2px 0 0; - font-size: 0.9rem; -} - -.settings-detail-note { - margin: 4px 16px 12px; - gap: 10px; - --inner-padding-end: 0; -} - -.settings-detail-file-item { - position: relative; -} - -.settings-detail-file-input { - position: absolute; - inset: 0; - opacity: 0; - cursor: pointer; -} - -.settings-detail-actions { - padding: 0 16px 16px; -} - -.settings-detail-empty { - padding: 24px; - text-align: center; -} - -.settings-detail-last-refresh { - padding: 0 16px 8px; - font-size: 0.85rem; -} - -.settings-detail-api-output { - margin: 12px 16px; - padding: 12px; - background: #f9fafb; - border: 1px solid #e5e7eb; - border-radius: 8px; - font-size: 0.85rem; -} - -.settings-detail-api-output pre { - margin: 8px 0 0 0; - padding: 0; - white-space: pre-wrap; - word-break: break-word; - font-family: "Monaco", "Courier New", monospace; - font-size: 0.8rem; - color: #374151; - overflow-x: auto; -} diff --git a/src/pages/Settings/SettingsDetailPage.tsx b/src/pages/Settings/SettingsDetailPage.tsx deleted file mode 100644 index cfcab1e..0000000 --- a/src/pages/Settings/SettingsDetailPage.tsx +++ /dev/null @@ -1,607 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react"; -import { - IonAlert, - IonBackButton, - IonButton, - IonButtons, - IonContent, - IonHeader, - IonIcon, - IonInput, - IonItem, - IonLabel, - IonList, - IonPage, - IonText, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { - linkOutline, - logOutOutline, - refreshOutline, - saveOutline, - settingsOutline, - shareOutline, - timeOutline, - trashOutline, -} from "ionicons/icons"; -import { useParams } from "react-router-dom"; - -import { - ConfigService, - type ServiceConfig, -} from "../../services/ConfigService"; -import { db } from "../../services/Database"; - -import "./SettingsDetailPage.css"; - -interface SettingsRouteParams { - serviceId: string; -} - -const GOG_AUTH_URL = - "https://auth.gog.com/auth?client_id=46899977096215655&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=code&layout=client2"; - -const SERVICE_META = { - steam: { - title: "Steam", - description: "Deine Steam-Bibliothek", - }, - gog: { - title: "GOG", - description: "Deine GOG-Bibliothek", - }, - data: { - title: "Datenverwaltung", - description: "Export, Import und Reset", - }, -} as const; - -type ServiceId = keyof typeof SERVICE_META; -const PROVIDER_IDS = ["steam", "gog"] as const; - -export default function SettingsDetailPage() { - const { serviceId } = useParams(); - const [config, setConfig] = useState({}); - const [showAlert, setShowAlert] = useState(false); - const [alertMessage, setAlertMessage] = useState(""); - const [apiOutput, setApiOutput] = useState(""); - const [gogCode, setGogCode] = useState(""); - - const meta = useMemo(() => SERVICE_META[serviceId as ServiceId], [serviceId]); - - useEffect(() => { - const loadConfig = async () => { - const loadedConfig = await ConfigService.loadConfig(); - setConfig(loadedConfig); - }; - - loadConfig(); - }, [serviceId]); - - const handleDraftChange = (service: keyof ServiceConfig, data: any) => { - setConfig((prev) => ({ - ...prev, - [service]: { ...prev[service], ...data }, - })); - }; - - const handleSaveService = async (service: keyof ServiceConfig) => { - await ConfigService.saveConfig(config); - setAlertMessage(`✓ ${service.toUpperCase()} Einstellungen gespeichert`); - setShowAlert(true); - - // Automatisch Daten abrufen nach dem Speichern - if (service === "steam") { - await handleManualRefresh(service); - } - }; - - const handleGogConnect = async () => { - if (!gogCode.trim()) { - setAlertMessage("Bitte den Code aus der URL eingeben"); - setShowAlert(true); - return; - } - - setApiOutput("Tausche Code gegen Token..."); - - try { - const apiUrl = ConfigService.getApiUrl("/api/gog/auth"); - const response = await fetch(apiUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code: gogCode.trim() }), - }); - - if (!response.ok) { - const errorText = await response.text(); - setApiOutput(`❌ Fehler: ${response.status}\n${errorText}`); - setAlertMessage("GOG Verbindung fehlgeschlagen"); - setShowAlert(true); - return; - } - - const tokens = await response.json(); - - const updatedConfig = { - ...config, - gog: { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - userId: tokens.user_id, - }, - }; - setConfig(updatedConfig); - await ConfigService.saveConfig(updatedConfig); - setGogCode(""); - setApiOutput("✓ Verbunden! Spiele werden abgerufen..."); - - // Automatically fetch games after connecting - await handleManualRefresh("gog", updatedConfig); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - setApiOutput(`❌ Fehler: ${errorMsg}`); - setAlertMessage("GOG Verbindung fehlgeschlagen"); - setShowAlert(true); - } - }; - - const handleGogDisconnect = async () => { - const updatedConfig = { ...config }; - delete updatedConfig.gog; - setConfig(updatedConfig); - await ConfigService.saveConfig(updatedConfig); - setApiOutput(""); - setAlertMessage("✓ GOG Verbindung getrennt"); - setShowAlert(true); - }; - - const handleManualRefresh = async ( - service: keyof ServiceConfig, - configOverride?: ServiceConfig, - ) => { - const currentConfig = configOverride || config; - setApiOutput("Rufe API auf..."); - - try { - if (service === "steam") { - const steamConfig = currentConfig.steam; - if (!steamConfig?.apiKey || !steamConfig?.steamId) { - setApiOutput("❌ Fehler: Steam API Key und Steam ID erforderlich"); - setAlertMessage("Bitte zuerst Steam-Zugangsdaten eingeben"); - setShowAlert(true); - return; - } - - const apiUrl = ConfigService.getApiUrl("/api/steam/refresh"); - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 60000); - - try { - const response = await fetch(apiUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - apiKey: steamConfig.apiKey, - steamId: steamConfig.steamId, - }), - signal: controller.signal, - }); - clearTimeout(timeoutId); - - if (!response.ok) { - const errorText = await response.text(); - setApiOutput(`❌ API Fehler: ${response.status}\n${errorText}`); - setAlertMessage("Steam Refresh fehlgeschlagen"); - setShowAlert(true); - return; - } - - const result = await response.json(); - - const transformedGames = result.games.map((steamGame: any) => ({ - id: `steam-${steamGame.appid}`, - title: steamGame.name, - source: "steam", - sourceId: String(steamGame.appid), - platform: "PC", - playtimeHours: steamGame.playtime_forever - ? Math.round(steamGame.playtime_forever / 60) - : 0, - url: `https://store.steampowered.com/app/${steamGame.appid}`, - ...(steamGame.canonicalId && { - canonicalId: steamGame.canonicalId, - }), - })); - - await db.saveGamesBySource("steam", transformedGames); - - const updatedConfig = { - ...currentConfig, - steam: { - ...currentConfig.steam, - lastRefresh: new Date().toISOString(), - }, - }; - setConfig(updatedConfig); - await ConfigService.saveConfig(updatedConfig); - - setApiOutput( - `✓ ${result.count} Spiele abgerufen\n\nBeispiel:\n${JSON.stringify(transformedGames.slice(0, 2), null, 2)}`, - ); - setAlertMessage(`✓ ${result.count} Spiele aktualisiert`); - setShowAlert(true); - } catch (fetchError: any) { - clearTimeout(timeoutId); - if (fetchError.name === "AbortError") { - throw new Error( - "Request timeout - Steam API took too long to respond", - ); - } - throw fetchError; - } - } else if (service === "gog") { - const gogConfig = currentConfig.gog; - if (!gogConfig?.accessToken || !gogConfig?.refreshToken) { - setApiOutput("❌ Fehler: Bitte zuerst mit GOG verbinden"); - setAlertMessage("Bitte zuerst mit GOG verbinden"); - setShowAlert(true); - return; - } - - const apiUrl = ConfigService.getApiUrl("/api/gog/refresh"); - const response = await fetch(apiUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - accessToken: gogConfig.accessToken, - refreshToken: gogConfig.refreshToken, - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - setApiOutput(`❌ API Fehler: ${response.status}\n${errorText}`); - setAlertMessage("GOG Refresh fehlgeschlagen"); - setShowAlert(true); - return; - } - - const result = await response.json(); - - await db.saveGamesBySource("gog", result.games); - - // Update tokens if refreshed - const updatedConfig = { - ...currentConfig, - gog: { - ...currentConfig.gog, - lastRefresh: new Date().toISOString(), - ...(result.newAccessToken && { - accessToken: result.newAccessToken, - }), - ...(result.newRefreshToken && { - refreshToken: result.newRefreshToken, - }), - }, - }; - setConfig(updatedConfig); - await ConfigService.saveConfig(updatedConfig); - - setApiOutput( - `✓ ${result.count} Spiele abgerufen\n\nBeispiel:\n${JSON.stringify(result.games.slice(0, 2), null, 2)}`, - ); - setAlertMessage(`✓ ${result.count} GOG-Spiele aktualisiert`); - setShowAlert(true); - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error("[Frontend] Error during refresh:", error); - setApiOutput(`❌ Fehler: ${errorMsg}`); - setAlertMessage("Aktualisierung fehlgeschlagen"); - setShowAlert(true); - } - }; - - const formatLastRefresh = (value?: string) => { - if (!value) return "Nie"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "Unbekannt"; - return new Intl.DateTimeFormat("de-DE", { - dateStyle: "medium", - timeStyle: "short", - }).format(date); - }; - - const handleExportConfig = () => { - const validation = ConfigService.validateConfig(config); - if (!validation.valid) { - setAlertMessage( - `⚠️ Config unvollständig:\n${validation.errors.join("\n")}`, - ); - setShowAlert(true); - return; - } - ConfigService.exportConfig(config); - setAlertMessage("✓ Config exportiert"); - setShowAlert(true); - }; - - const handleImportConfig = async ( - event: React.ChangeEvent, - ) => { - const file = event.target.files?.[0]; - if (!file) return; - - const imported = await ConfigService.importConfig(file); - if (imported) { - setConfig(imported); - setAlertMessage("✓ Config importiert"); - } else { - setAlertMessage("❌ Import fehlgeschlagen"); - } - setShowAlert(true); - }; - - const handleClearConfig = async () => { - await ConfigService.clearConfig(); - await db.clear(); - setConfig({}); - setApiOutput(""); - setAlertMessage("✓ Alle Einstellungen und Spiele gelöscht"); - setShowAlert(true); - }; - - if (!meta) { - return ( - - - - - - - Einstellungen - - - -
- Unbekannter Bereich. -
-
-
- ); - } - - const isProvider = (PROVIDER_IDS as readonly string[]).includes(serviceId); - const providerKey = isProvider ? (serviceId as keyof ServiceConfig) : null; - const lastRefresh = providerKey - ? config[providerKey]?.lastRefresh - : undefined; - - return ( - - - - - - - {meta.title} - {isProvider && providerKey && ( - - handleManualRefresh(providerKey)} - > - - - - )} - - - - -
- -
-

{meta.title}

-

{meta.description}

-
-
- - {serviceId === "steam" && ( - <> - - - Steam API Key - - handleDraftChange("steam", { - apiKey: e.detail.value || "", - }) - } - /> - - - Steam Profil URL oder ID - { - const input = e.detail.value || ""; - // Extract Steam ID from URL if provided - const idMatch = input.match(/\/(id|profiles)\/(\w+)/); - const extractedId = idMatch ? idMatch[2] : input; - handleDraftChange("steam", { - steamId: extractedId, - }); - }} - /> - - -
- handleSaveService("steam")} - > - - Speichern - -
- - )} - - {serviceId === "gog" && ( - <> - {config.gog?.refreshToken ? ( - <> - - - -

Verbunden

-

User ID: {config.gog.userId || "Unbekannt"}

-
-
-
-
- handleManualRefresh("gog")} - > - - Spiele aktualisieren - - - - Verbindung trennen - -
- - ) : ( - <> - - - -

- 1. Klicke auf "Bei GOG anmelden" und logge dich ein. -

-

- 2. Nach dem Login landest du auf einer Seite mit einer - URL die code=... enthält. -

-

- 3. Kopiere den Wert nach code= und - füge ihn unten ein. -

-
-
-
-
- window.open(GOG_AUTH_URL, "_blank")} - > - - Bei GOG anmelden - -
- - - Authorization Code - - setGogCode(e.detail.value || "") - } - /> - - -
- - - Verbinden - -
- - )} - - )} - - {serviceId === "data" && ( - <> - - - Config exportieren - - - - Config importieren - - - -
- handleClearConfig()} - > - - Alle Einstellungen löschen - -
- - )} - - {isProvider && ( - <> -
- - Letzter Abruf:{" "} - {formatLastRefresh(lastRefresh)} - -
- {apiOutput && ( -
- - API Response: - -
{apiOutput}
-
- )} - - )} - -
- - - setShowAlert(false)} - message={alertMessage} - buttons={["OK"]} - /> - - ); -} diff --git a/src/pages/Settings/SettingsPage.css b/src/pages/Settings/SettingsPage.css deleted file mode 100644 index d650126..0000000 --- a/src/pages/Settings/SettingsPage.css +++ /dev/null @@ -1,3 +0,0 @@ -.settings-page-note { - font-size: 0.85rem; -} diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx deleted file mode 100644 index cc7470c..0000000 --- a/src/pages/Settings/SettingsPage.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import { - IonContent, - IonHeader, - IonIcon, - IonItem, - IonLabel, - IonList, - IonListHeader, - IonNote, - IonPage, - IonTitle, - IonToolbar, -} from "@ionic/react"; -import { - cloudOutline, - cogOutline, - gameControllerOutline, -} from "ionicons/icons"; - -import "./SettingsPage.css"; - -export default function SettingsPage() { - return ( - - - - - Einstellungen - - - - - - - Provider - - - Steam - API Key · Steam ID - - - - GOG - OAuth Login - - - - - Verwaltung - - - Datenverwaltung - Export · Import - - - - - ); -} diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts deleted file mode 100644 index 9dd3715..0000000 --- a/src/services/ConfigService.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * ConfigService - Sichere Konfigurationsverwaltung - * Nutzt IndexedDB (Primary) mit localStorage Fallback (wie Voyager) - */ -import { db } from "./Database"; - -export interface ServiceConfig { - steam?: { - apiKey?: string; - steamId?: string; - lastRefresh?: string; - }; - gog?: { - accessToken?: string; - refreshToken?: string; - userId?: string; - lastRefresh?: string; - }; -} - -const STORAGE_KEY = "whattoplay_config"; - -export class ConfigService { - /** - * Lade Konfiguration aus IndexedDB (Primary) oder localStorage (Fallback) - */ - static async loadConfig(): Promise { - try { - // Versuche IndexedDB - const dbConfig = await db.getConfig(); - if (dbConfig) { - return dbConfig; - } - - // Fallback: localStorage - const stored = localStorage.getItem(STORAGE_KEY); - const config = stored ? JSON.parse(stored) : {}; - - // Migriere zu IndexedDB - if (stored) { - await db.saveConfig(config); - } - - return config; - } catch (error) { - console.warn("Config konnte nicht geladen werden", error); - // Letzter Fallback: localStorage - try { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : {}; - } catch { - return {}; - } - } - } - - /** - * Speichere Konfiguration in IndexedDB (Primary Storage) - */ - static async saveConfig(config: ServiceConfig) { - try { - await db.saveConfig(config); - return true; - } catch (error) { - console.error( - "Config konnte nicht in IndexedDB gespeichert werden", - error, - ); - return false; - } - } - - /** - * Exportiere Config als JSON-Datei für Download - * ⚠️ WARNUNG: Enthält sensitive Daten! - */ - static exportConfig(config: ServiceConfig) { - const element = document.createElement("a"); - const file = new Blob([JSON.stringify(config, null, 2)], { - type: "application/json", - }); - element.href = URL.createObjectURL(file); - element.download = "whattoplay-config.json"; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - } - - /** - * Importiere Config aus JSON-Datei - */ - static async importConfig(file: File): Promise { - try { - const text = await file.text(); - const config = JSON.parse(text); - await this.saveConfig(config); - return config; - } catch (error) { - console.error("Config-Import fehlgeschlagen", error); - return null; - } - } - - /** - * Lösche sensitive Daten aus IndexedDB + localStorage - */ - static async clearConfig() { - try { - await db.clear(); - console.log("✓ Config und Daten gelöscht"); - } catch (error) { - console.error("Fehler beim Löschen der Config", error); - } - } - - /** - * Validiere Config-Struktur - */ - static validateConfig(config: ServiceConfig): { - valid: boolean; - errors: string[]; - } { - const errors: string[] = []; - - if (config.steam) { - if (!config.steam.apiKey) errors.push("Steam: API Key fehlt"); - if (!config.steam.steamId) errors.push("Steam: Steam ID fehlt"); - } - - return { - valid: errors.length === 0, - errors, - }; - } - - /** - * Get API URL for Steam refresh - * - Development: Vite dev server proxy - * - Production: Uberspace backend via VITE_API_URL - */ - static getApiUrl(endpoint: string): string { - // Development mode: Use Vite dev server middleware - if (import.meta.env.DEV) { - return endpoint; - } - - // Production: Use backend URL from environment - const backendUrl = import.meta.env.VITE_API_URL; - if (!backendUrl) { - throw new Error( - "Backend not configured. Set VITE_API_URL in .env.production", - ); - } - - const baseUrl = backendUrl.replace(/\/$/, ""); - return `${baseUrl}${endpoint}`; - } -} diff --git a/src/services/Database.ts b/src/services/Database.ts deleted file mode 100644 index 44131db..0000000 --- a/src/services/Database.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Database Service - IndexedDB für PWA-Persistenz - * Strategie wie Voyager: IndexedDB als Primary Storage - */ - -export interface DbConfig { - steam?: { - apiKey?: string; - steamId?: string; - lastRefresh?: string; - }; - gog?: { - userId?: string; - accessToken?: string; - refreshToken?: string; - lastRefresh?: string; - }; - epic?: { - email?: string; - accessToken?: string; - method?: "oauth" | "manual"; - lastRefresh?: string; - }; - amazon?: { - email?: string; - manualGames?: Array<{ name: string; gameId?: string; source?: string }>; - method?: "oauth" | "manual"; - lastRefresh?: string; - }; - blizzard?: { - clientId?: string; - clientSecret?: string; - accessToken?: string; - region?: "us" | "eu" | "kr" | "tw"; - lastRefresh?: string; - }; -} - -export interface Game { - id: string; - title: string; - source?: string; - sourceId?: string; - platform?: string; - lastPlayed?: string | null; - playtimeHours?: number; - url?: string; - canonicalId?: string; -} - -export interface Playlist { - id: string; - name: string; - gameIds: string[]; - isStatic: boolean; - createdAt: string; -} - -const DB_NAME = "whattoplay"; -const DB_VERSION = 2; - -class Database { - private db: IDBDatabase | null = null; - - async init(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - - request.onerror = () => reject(request.error); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - - // Config Store - if (!db.objectStoreNames.contains("config")) { - db.createObjectStore("config", { keyPath: "id" }); - } - - // Games Store - if (!db.objectStoreNames.contains("games")) { - const gameStore = db.createObjectStore("games", { keyPath: "id" }); - gameStore.createIndex("source", "source", { unique: false }); - gameStore.createIndex("title", "title", { unique: false }); - } - - // Settings Store - if (!db.objectStoreNames.contains("settings")) { - db.createObjectStore("settings", { keyPath: "key" }); - } - - // Playlists Store - if (!db.objectStoreNames.contains("playlists")) { - db.createObjectStore("playlists", { keyPath: "id" }); - } - - // Sync Log (für zukünftige Cloud-Sync) - if (!db.objectStoreNames.contains("syncLog")) { - db.createObjectStore("syncLog", { - keyPath: "id", - autoIncrement: true, - }); - } - }; - - request.onsuccess = () => { - this.db = request.result; - this.initStaticPlaylists(); - resolve(); - }; - }); - } - - async getConfig(): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("config", "readonly"); - const store = tx.objectStore("config"); - const request = store.get("main"); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || null); - }); - } - - async saveConfig(config: DbConfig): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("config", "readwrite"); - const store = tx.objectStore("config"); - const request = store.put({ id: "main", ...config }); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async saveGames(games: Game[]): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("games", "readwrite"); - const store = tx.objectStore("games"); - - // Lösche alte Spiele - const clearRequest = store.clear(); - clearRequest.onsuccess = () => { - // Füge neue Spiele ein - games.forEach((game) => store.add(game)); - resolve(); - }; - clearRequest.onerror = () => reject(clearRequest.error); - }); - } - - async saveGamesBySource(source: string, games: Game[]): Promise { - if (!this.db) await this.init(); - - // Step 1: Find IDs of existing games for this source - const existingGames = await this.getGamesBySource(source); - const existingIds = existingGames.map((g) => g.id); - - // Step 2: Delete old and add new in one transaction - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("games", "readwrite"); - const store = tx.objectStore("games"); - - for (const id of existingIds) { - store.delete(id); - } - for (const game of games) { - store.put(game); - } - - tx.onerror = () => reject(tx.error); - tx.oncomplete = () => resolve(); - }); - } - - async getGames(): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("games", "readonly"); - const store = tx.objectStore("games"); - const request = store.getAll(); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || []); - }); - } - - async getGamesBySource(source: string): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("games", "readonly"); - const store = tx.objectStore("games"); - const index = store.index("source"); - const request = index.getAll(source); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || []); - }); - } - - async getSetting(key: string): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("settings", "readonly"); - const store = tx.objectStore("settings"); - const request = store.get(key); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result?.value || null); - }); - } - - async setSetting(key: string, value: any): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("settings", "readwrite"); - const store = tx.objectStore("settings"); - const request = store.put({ key, value }); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async clear(): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction( - ["config", "games", "settings", "playlists", "syncLog"], - "readwrite", - ); - - ["config", "games", "settings", "playlists", "syncLog"].forEach( - (storeName) => { - tx.objectStore(storeName).clear(); - }, - ); - - tx.onerror = () => reject(tx.error); - tx.oncomplete = () => resolve(); - }); - } - - private async initStaticPlaylists(): Promise { - const playlists = await this.getPlaylists(); - - const statics: Array<{ id: string; name: string }> = [ - { id: "favorites", name: "Favoriten" }, - { id: "want-to-play", name: "Want to Play" }, - { id: "not-interesting", name: "Not Interesting" }, - ]; - - for (const s of statics) { - if (!playlists.some((p) => p.id === s.id)) { - await this.createPlaylist({ - id: s.id, - name: s.name, - gameIds: [], - isStatic: true, - createdAt: new Date().toISOString(), - }); - } - } - } - - async getPlaylists(): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("playlists", "readonly"); - const store = tx.objectStore("playlists"); - const request = store.getAll(); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || []); - }); - } - - async getPlaylist(id: string): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("playlists", "readonly"); - const store = tx.objectStore("playlists"); - const request = store.get(id); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || null); - }); - } - - async createPlaylist(playlist: Playlist): Promise { - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("playlists", "readwrite"); - const store = tx.objectStore("playlists"); - const request = store.put(playlist); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async addGameToPlaylist(playlistId: string, gameId: string): Promise { - const playlist = await this.getPlaylist(playlistId); - if (!playlist) throw new Error(`Playlist ${playlistId} not found`); - - if (!playlist.gameIds.includes(gameId)) { - playlist.gameIds.push(gameId); - await this.createPlaylist(playlist); - } - } - - async removeGameFromPlaylist( - playlistId: string, - gameId: string, - ): Promise { - const playlist = await this.getPlaylist(playlistId); - if (!playlist) throw new Error(`Playlist ${playlistId} not found`); - - playlist.gameIds = playlist.gameIds.filter((id) => id !== gameId); - await this.createPlaylist(playlist); - } - - async updatePlaylist( - id: string, - updates: Partial>, - ): Promise { - const playlist = await this.getPlaylist(id); - if (!playlist) throw new Error(`Playlist ${id} not found`); - - Object.assign(playlist, updates); - await this.createPlaylist(playlist); - } - - async deletePlaylist(id: string): Promise { - const playlist = await this.getPlaylist(id); - if (!playlist) return; - if (playlist.isStatic) throw new Error("Cannot delete static playlist"); - - if (!this.db) await this.init(); - - return new Promise((resolve, reject) => { - const tx = this.db!.transaction("playlists", "readwrite"); - const store = tx.objectStore("playlists"); - const request = store.delete(id); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } -} - -// Singleton -export const db = new Database(); diff --git a/src/theme/variables.css b/src/theme/variables.css deleted file mode 100644 index 6a1730e..0000000 --- a/src/theme/variables.css +++ /dev/null @@ -1,13 +0,0 @@ -:root { - --ion-font-family: - "-apple-system", "SF Pro Text", "SF Pro Display", system-ui, sans-serif; - --ion-background-color: #f2f2f7; - --ion-text-color: #1c1c1e; - --ion-toolbar-background: #f2f2f7; - --ion-item-background: #ffffff; - --ion-item-border-color: rgba(60, 60, 67, 0.2); - --ion-color-primary: #0a84ff; - --ion-color-primary-contrast: #ffffff; - --ion-safe-area-top: env(safe-area-inset-top); - --ion-safe-area-bottom: env(safe-area-inset-bottom); -} diff --git a/styles.css b/styles.css deleted file mode 100644 index ac468ff..0000000 --- a/styles.css +++ /dev/null @@ -1,231 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"); - -:root { - color-scheme: light; - font-family: "Inter", system-ui, sans-serif; - line-height: 1.5; - --bg: #f6f7fb; - --panel: #ffffff; - --text: #1c1d2a; - --muted: #5c607b; - --accent: #4b4bff; - --accent-weak: #e6e8ff; - --border: #e0e3f2; - --shadow: 0 15px 40px rgba(28, 29, 42, 0.08); -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - background: var(--bg); - color: var(--text); - min-height: 100vh; -} - -.app-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 2rem; - padding: 3.5rem 6vw 2rem; -} - -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.2em; - font-size: 0.75rem; - font-weight: 600; - color: var(--muted); -} - -h1 { - font-size: clamp(2rem, 3vw, 3.2rem); - margin: 0.4rem 0 0.8rem; -} - -.subtitle { - max-width: 520px; - color: var(--muted); -} - -.header-actions { - display: flex; - gap: 1rem; -} - -button, -input, -select { - font-family: inherit; -} - -.primary { - background: var(--accent); - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 999px; - font-weight: 600; - box-shadow: var(--shadow); - cursor: pointer; -} - -.primary:hover { - filter: brightness(0.95); -} - -.app-main { - padding: 0 6vw 3rem; -} - -.controls { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1rem; - background: var(--panel); - padding: 1.4rem; - border-radius: 20px; - border: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.control-group { - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -label { - font-size: 0.85rem; - color: var(--muted); -} - -input, -select { - border-radius: 12px; - border: 1px solid var(--border); - padding: 0.6rem 0.8rem; - background: #fdfdff; -} - -.summary { - margin: 2rem 0 1.5rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1rem; -} - -.summary-card { - background: var(--panel); - border-radius: 18px; - padding: 1.2rem; - border: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.summary-card h3 { - font-size: 0.95rem; - color: var(--muted); - margin-bottom: 0.4rem; -} - -.summary-card p { - font-size: 1.7rem; - font-weight: 700; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 1.5rem; -} - -.card { - background: var(--panel); - border-radius: 20px; - border: 1px solid var(--border); - padding: 1.4rem; - display: flex; - flex-direction: column; - gap: 0.8rem; - box-shadow: var(--shadow); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; -} - -.title { - font-size: 1.1rem; - font-weight: 600; -} - -.badge { - background: var(--accent-weak); - color: var(--accent); - font-size: 0.75rem; - padding: 0.2rem 0.6rem; - border-radius: 999px; - font-weight: 600; -} - -.meta { - font-size: 0.85rem; - color: var(--muted); -} - -.tag-list { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; -} - -.tag { - background: #f1f2f8; - color: #2e3046; - padding: 0.2rem 0.6rem; - border-radius: 999px; - font-size: 0.75rem; -} - -.sources { - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -.source-item { - display: flex; - justify-content: space-between; - align-items: center; - background: #f8f9fe; - border-radius: 12px; - padding: 0.4rem 0.6rem; - font-size: 0.78rem; - color: var(--muted); -} - -.source-item span { - font-weight: 600; - color: var(--text); -} - -.app-footer { - padding: 2rem 6vw 3rem; - color: var(--muted); - font-size: 0.85rem; -} - -@media (max-width: 720px) { - .app-header { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 1fdcb78..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "react-jsx", - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true - }, - "include": [ - "src" - ] -} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index cfad577..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import react from "@vitejs/plugin-react"; -import { defineConfig, loadEnv } from "vite"; -import { handleSteamRefresh } from "./server/steam-api.mjs"; -import { handleGogAuth, handleGogRefresh } from "./server/gog-api.mjs"; - -const apiMiddlewarePlugin = { - name: "api-middleware", - configureServer(server) { - server.middlewares.use((req, res, next) => { - const url = req.url ?? ""; - if (url.startsWith("/api/steam/refresh")) { - return handleSteamRefresh(req, res); - } - if (url.startsWith("/api/gog/auth")) { - return handleGogAuth(req, res); - } - if (url.startsWith("/api/gog/refresh")) { - return handleGogRefresh(req, res); - } - next(); - }); - }, -}; - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - - return { - base: env.VITE_BASE_PATH || "/", - plugins: [react(), apiMiddlewarePlugin], - server: { - port: 5173, - hmr: { - overlay: true, - }, - watch: { - usePolling: true, - }, - }, - }; -});