diff --git a/.github/copilot-instructions.md b/AGENTS.md similarity index 100% rename from .github/copilot-instructions.md rename to AGENTS.md diff --git a/README.md b/README.md index b39d783..a62305d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ yarn install cp .env.example .env.local # 4. Get API keys (optional, but recommended) -# See: docs/API_KEYS.md +# See: [docs/02-tecnico/apis.md](docs/02-tecnico/apis.md) # 5. Run migrations cd backend @@ -70,21 +70,33 @@ quasar/ │ │ └── controllers/ # Request handlers │ └── tests/ # Vitest unit tests (63+ tests) │ -├── frontend/ # React + Vite + React Query +├── frontend/ # React 18 + Vite + TypeScript + TanStack │ ├── src/ -│ │ ├── routes/ # Pages (/games, /roms, etc.) -│ │ ├── components/ # React components (Forms, Dialogs, Cards) -│ │ └── hooks/ # TanStack Query hooks -│ └── tests/ # Vitest + React Testing Library (59+ tests) +│ │ ├── components/ # shadcn/ui components + custom components +│ │ ├── pages/ # Application pages (Dashboard, Games, etc.) +│ │ ├── api/ # API services and types +│ │ ├── query/ # TanStack Query configuration +│ │ ├── form/ # TanStack Form + Zod configuration +│ │ ├── router/ # TanStack Router configuration +│ │ ├── types/ # TypeScript type definitions +│ │ ├── hooks/ # Custom React hooks +│ │ ├── lib/ # Utility functions +│ │ ├── styles/ # Global styles and Tailwind config +│ │ └── layout/ # Layout components (Header, Sidebar, etc.) +│ ├── tests/ # Vitest + React Testing Library (59+ tests) +│ ├── public/ # Static assets +│ └── index.html # HTML entry point │ ├── tests/ │ ├── e2e/ # Playwright E2E tests (15 tests) │ └── *.spec.ts # Config validation tests │ ├── docs/ # Documentation -│ ├── API_KEYS.md # How to get API credentials -│ ├── SECURITY.md # Security guidelines -│ └── ... +│ ├── README.md # Documentation index +│ ├── 01-conceptos/ # Fundamental concepts and requirements +│ ├── 02-tecnico/ # Technical documentation +│ ├── 03-analisis/ # Comparative analysis +│ └── 04-operaciones/ # Operations and deployment │ ├── .gitea/ │ └── workflows/ @@ -103,7 +115,7 @@ Copy `.env.example` to `.env.local` (or `.env.development`) and fill in: # Database (local SQLite) DATABASE_URL="file:./dev.db" -# API Keys (get from docs/API_KEYS.md) +# API Keys (get from [docs/02-tecnico/apis.md](docs/02-tecnico/apis.md)) IGDB_CLIENT_ID=your_client_id IGDB_CLIENT_SECRET=your_client_secret RAWG_API_KEY=your_api_key @@ -115,10 +127,12 @@ PORT=3000 LOG_LEVEL=debug ``` -For production, use Gitea Secrets. See **SECURITY.md** and **docs/API_KEYS.md**. +For production, use Gitea Secrets. See **SECURITY.md** and **[docs/02-tecnico/apis.md](docs/02-tecnico/apis.md)**. ## Testing +### General Tests + ```bash # Run all tests (unit + config) yarn test @@ -139,6 +153,97 @@ yarn lint yarn format ``` +### Frontend Development and Testing + +```bash +# Navigate to frontend directory +cd frontend + +# Install dependencies +yarn install + +# Start development server +yarn dev +# Frontend will be available at: http://localhost:5173 + +# Build for production +yarn build + +# Preview production build +yarn preview + +# Run frontend-specific tests +yarn test + +# Run frontend tests with coverage +yarn test:coverage + +# Lint frontend code +yarn lint + +# Format frontend code +yarn format + +# Type check frontend +yarn type-check +``` + +### Backend Development and Testing + +```bash +# Navigate to backend directory +cd backend + +# Install dependencies +yarn install + +# Start development server +yarn dev +# Backend API will be available at: http://localhost:3000 + +# Run backend tests +yarn test + +# Run backend tests with coverage +yarn test:coverage + +# Run specific test file +yarn test -- gamesController.spec.ts + +# Run tests in watch mode +yarn test:watch + +# Lint backend code +yarn lint + +# Format backend code +yarn format + +# Type check backend +yarn type-check + +# Database operations +yarn prisma:migrate +yarn prisma:generate +yarn prisma:studio +``` + +### API Testing + +```bash +# Test backend API endpoints +curl http://localhost:3000/health + +# Test games endpoint +curl http://localhost:3000/api/games + +# Test metadata search +curl "http://localhost:3000/api/metadata/search?q=Mario" + +# Test ROM scanning +curl -X POST http://localhost:3000/api/roms/scan -d '{"path":"/path/to/roms"}' +``` + ## Troubleshooting ### Backend won't start @@ -158,7 +263,7 @@ Error: EADDRINUSE: address already in use :::3000 ### Metadata search returns no results ``` -→ Check that API keys are correct (docs/API_KEYS.md) +→ Check that API keys are correct ([docs/02-tecnico/apis.md](docs/02-tecnico/apis.md)) → Check logs: tail -f backend/logs/*.log → Test with: curl http://localhost:3000/api/metadata/search\?q\=Mario ``` @@ -172,12 +277,12 @@ Error: EADDRINUSE: address already in use :::3000 ## Architecture -For detailed architecture and decisions, see [docs/architecture.md](docs/architecture.md). +For detailed architecture and decisions, see [docs/01-conceptos/architecture.md](docs/01-conceptos/architecture.md). ### Tech Stack - **Backend:** Node.js, Fastify, Prisma ORM, SQLite, TypeScript -- **Frontend:** React 18, Vite, TanStack Query, Tailwind CSS, shadcn/ui +- **Frontend:** React 18, Vite, TypeScript, TanStack Query, TanStack Router, TanStack Form, Zod, Tailwind CSS, shadcn/ui - **Testing:** Vitest (unit), Playwright (E2E) - **APIs:** IGDB (OAuth), RAWG, TheGamesDB @@ -193,10 +298,13 @@ For security guidelines, see [SECURITY.md](SECURITY.md). ## Documentation - **[SECURITY.md](SECURITY.md)** — Security policies and best practices -- **[docs/API_KEYS.md](docs/API_KEYS.md)** — How to obtain and configure API credentials -- **[docs/architecture.md](docs/architecture.md)** — System architecture and design decisions -- **[docs/data-model.md](docs/data-model.md)** — Database schema and entities -- **[docs/requirements.md](docs/requirements.md)** — Project requirements and use cases +- **[docs/README.md](docs/README.md)** — Documentation index and navigation guide +- **[docs/01-conceptos/requirements.md](docs/01-conceptos/requirements.md)** — Project requirements and use cases +- **[docs/01-conceptos/architecture.md](docs/01-conceptos/architecture.md)** — System architecture and design decisions +- **[docs/01-conceptos/data-model.md](docs/01-conceptos/data-model.md)** — Database schema and entities +- **[docs/02-tecnico/apis.md](docs/02-tecnico/apis.md)** — APIs configuration and integration guide +- **[docs/02-tecnico/frontend.md](docs/02-tecnico/frontend.md)** — Complete frontend architecture and implementation +- **[docs/03-analisis/competitive-analysis.md](docs/03-analisis/competitive-analysis.md)** — Market analysis and competitive research ## Development @@ -251,6 +359,7 @@ MIT (or choose your license) --- -**Status:** MVP (v1.0.0) — Ready for self-hosted deployment. -**Last updated:** 2026-02-12 +**Status:** MVP (v1.0.0) — Ready for self-hosted deployment. +**Last updated:** 2026-02-22 **Test coverage:** 122+ unit tests + 15 E2E tests ✅ +**Documentation:** Reorganized and consolidated ✅ diff --git a/docs/architecture.md b/docs/01-conceptos/architecture.md similarity index 100% rename from docs/architecture.md rename to docs/01-conceptos/architecture.md diff --git a/docs/data-model.md b/docs/01-conceptos/data-model.md similarity index 100% rename from docs/data-model.md rename to docs/01-conceptos/data-model.md diff --git a/docs/requirements.md b/docs/01-conceptos/requirements.md similarity index 100% rename from docs/requirements.md rename to docs/01-conceptos/requirements.md diff --git a/docs/apis-comparison.md b/docs/02-tecnico/apis.md similarity index 52% rename from docs/apis-comparison.md rename to docs/02-tecnico/apis.md index 674f418..ac11a5d 100644 --- a/docs/apis-comparison.md +++ b/docs/02-tecnico/apis.md @@ -1,15 +1,222 @@ -# Comparativa de APIs — cobertura, límites, coste y calidad +# APIs del Sistema — Guía completa -**Introducción** -Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots, géneros, desarrolladores), y datos de precio/ofertas. Las decisiones de integración deben priorizar cobertura, coste (preferencia: gratuito), calidad y facilidad de uso. - -**Nota:** límites y condiciones pueden cambiar — verificar TOS antes de integración. +Este documento integra toda la información sobre APIs del sistema: obtención de claves, prioridades, estrategias, comparación y configuración. --- -## Resumen por API +## Tabla de Contenidos -### IGDB (Internet Games Database) +1. [APIs priorizadas (MVP)](#apis-priorizadas-mvp) +2. [Obtención de claves](#obtención-de-claves) +3. [Guía de integración](#guía-de-integración) +4. [Comparación detallada](#comparación-detallada) +5. [Estrategias técnicas](#estrategias-técnicas) +6. [Configuración y despliegue](#configuración-y-despliegue) + +--- + +## APIs priorizadas (MVP) + +### Prioridad Alta + +1. **IGDB (Internet Game Database)** - Calidad superior, amplia cobertura +2. **RAWG (Rawg.io)** - Buena cobertura, datos de tiendas + +### Prioridad Media + +3. **TheGamesDB** - Artwork comunitario +4. **ScreenScraper** - Media específica para ROMs + +### Prioridad Baja (para futuras versiones) + +5. **PriceCharting** - Precios físicos +6. **IsThereAnyDeal** - Ofertas digitales +7. **MobyGames** - Datos históricos detallados +8. **eBay** - Datos de mercado + +--- + +## Obtención de claves + +### IGDB (Internet Game Database) + +IGDB usa **OAuth 2.0 via Twitch**. Steps: + +1. Go to [Twitch Developer Console](https://dev.twitch.tv/console/apps) +2. Sign in with your Twitch account (create one if needed) +3. Click "Create Application" + - Name: "Quasar" (or your app name) + - Category: Select relevant category + - Accept terms, click Create +4. You'll see: + - **Client ID** — Copy this + - Click "New Secret" to generate **Client Secret** — Copy this +5. Go to Settings → OAuth Redirect URLs + - Add: `http://localhost:3000/oauth/callback` (development) + - For production: `https://yourdomain.com/oauth/callback` +6. In your `.env` file: + ``` + IGDB_CLIENT_ID=your_client_id + IGDB_CLIENT_SECRET=your_client_secret + ``` +7. Start Quasar, it will use IGDB automatically + +**Rate Limit:** 4 requests/second + +### RAWG (Rawg.io) + +RAWG has a simpler **API Key** approach: + +1. Go to [RAWG Settings](https://rawg.io/settings/account) +2. Sign up if needed, then login +3. Find "API Key" section +4. Click "Create new key" (if needed) or copy existing key +5. In your `.env` file: + ``` + RAWG_API_KEY=your_api_key_here + ``` +6. Start Quasar + +**Rate Limit:** 20 requests/second (free tier) + +**Note:** RAWG requires attribution in UI (include "Powered by RAWG" somewhere visible) + +### TheGamesDB (thegamesdb.net) + +TheGamesDB uses a simple **API Key**: + +1. Go to [TheGamesDB API](https://thegamesdb.net/api) +2. Find "API Key" section (free registration required) +3. Register or login +4. Copy your API key +5. In your `.env` file: + ``` + THEGAMESDB_API_KEY=your_api_key_here + ``` +6. Start Quasar + +**Rate Limit:** 1 request/second (free tier) + +### ScreenScraper + +ScreenScraper requiere cuenta y modelo de donación: + +1. Go to [ScreenScraper](https://www.screenscraper.fr/) +2. Create account +3. Niveles de donación ofrecen límites distintos (ej.: 50.000 scrapes/día en nivel Bronze) +4. En tu `.env` file: + ``` + SCREENSCRAPER_USERNAME=your_username + SCREENSCRAPER_PASSWORD=your_password + ``` + +--- + +## Guía de integración + +### IGDB + +- **Obtener credenciales**: registrar una app en Twitch Developer Console para obtener `CLIENT_ID` y `CLIENT_SECRET`. Obtener token con grant type `client_credentials` (POST a `https://id.twitch.tv/oauth2/token`). + +- **Endpoints principales**: `POST https://api.igdb.com/v4/games` (consulta flexible via body con sintaxis IGDB), `POST https://api.igdb.com/v4/covers`, `POST https://api.igdb.com/v4/platforms`. + +- **Ejemplo (buscar)**: + +```bash +# Obtener token +curl -X POST 'https://id.twitch.tv/oauth2/token?client_id=$IGDB_CLIENT_ID&client_secret=$IGDB_CLIENT_SECRET&grant_type=client_credentials' + +# Buscar juegos +curl -X POST 'https://api.igdb.com/v4/games' \ + -H "Client-ID: $IGDB_CLIENT_ID" \ + -H "Authorization: Bearer $IGDB_TOKEN" \ + -H 'Accept: application/json' \ + --data 'fields id,name,first_release_date,platforms.name,genres.name,cover.url; search "zelda"; limit 5;' +``` + +- **Respuesta (esquemática)**: + +```json +[ + { + "id": 12345, + "name": "Ejemplo", + "first_release_date": 1459468800, + "platforms": [{ "name": "Nintendo Switch" }], + "cover": { "url": "//images.igdb.com/...jpg" } + } +] +``` + +- **Límites y manejo**: la API puede devolver `429` o cabeceras de límite; implementar retries exponenciales (ej. 3 intentos) y respetar `Retry-After`. Implementar circuit breaker si la API falla repetidamente. +- **Atribución**: mostrar origen de datos (ej. "Datos: IGDB") según términos del servicio. + +### RAWG + +- **Obtener credenciales**: registrarse en RAWG para obtener `RAWG_API_KEY` (https://rawg.io/apidocs). +- **Endpoints principales**: `GET https://api.rawg.io/api/games?key=API_KEY&search=...`, `GET https://api.rawg.io/api/games/{id}`. +- **Ejemplo**: + +```bash +curl 'https://api.rawg.io/api/games?key=$RAWG_API_KEY&search=zelda&page_size=5' +``` + +- **Respuesta (esquemática)**: + +```json +{ + "count": 100, + "results": [ + { "id": 3498, "name": "GTA V", "released": "2013-09-17", "background_image": "https://..." } + ] +} +``` + +- **Límites y manejo**: RAWG suele tener límites por clave/plan; cachear y fallback a otros proveedores si falla. +- **Atribución**: revisar condiciones y mostrar HTTP o texto de fuente si es requerido por el proveedor. + +### TheGamesDB + +- **Obtener credenciales**: crear cuenta y generar API Key en https://thegamesdb.net. +- **Endpoints**: búsqueda por nombre y detalles (`/v1/Games/ByGameName?name=...`, `/v1/Games/ByGameID?id=...`). +- **Ejemplo**: + +```bash +curl -H 'Authorization: Bearer $THEGAMESDB_KEY' 'https://api.thegamesdb.net/v1/Games/ByGameName?name=zelda' +``` + +### Estrategia de fallback y normalización + +- **Orden de prioridad**: IGDB → RAWG → TheGamesDB (configurable). +- **Normalización (mapping)**: + - `title` ← `name` + - `platform` ← `platforms[].name` + - `release_date` ← `first_release_date` / `released` → convertir a ISO 8601 + - `genres` ← `genres[].name` + - `cover_url` ← `cover.url` / `background_image` + - `external_ids` ← `{ igdb: id, rawg: id, thegamesdb: id }` + +- **Fallback**: si IGDB no tiene portada, intentar RAWG; si falla, usar TheGamesDB. Registrar la fuente usada. + +### Caché y almacenamiento de artwork + +- **Caché metadata**: LRU en memoria o Redis con TTL (por ejemplo 24h) para evitar sobrecargar APIs. +- **Almacenamiento de imágenes**: descargar y optimizar con `sharp` (crear versiones: thumb, medium), almacenar en `storage/artwork/{gameId}/cover.jpg` o S3. +- **Servicio proxy**: servir imágenes desde backend para no exponer keys ni URLs externas. + +### Manejo de errores y resiliencia + +- Implementar **retries** exponenciales con jitter (3 intentos). +- Implementar **circuit breaker** para desconectar llamadas a un proveedor fuera de servicio por N minutos. +- Limitar concurrencia por proveedor (p. ej. 5 llamadas simultáneas) y usar colas para trabajos masivos (enriquecimiento masivo). + +--- + +## Comparación detallada + +### Resumen por API + +#### IGDB (Internet Games Database) - **Resumen:** Base de datos muy completa (propiedad de Twitch/Amazon) con endpoints para juegos, covers, screenshots, plataformas, ratings, compañías y más. - **Autenticación / Requisitos:** OAuth vía Twitch (Client ID + Client Secret → token) — requiere cuenta Twitch y 2FA para registrar apps. @@ -21,9 +228,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Gratuito para uso no comercial; acuerdos comerciales para partners (atribución en caso de partnership). - **Enlace:** https://api-docs.igdb.com/ ---- - -### RAWG +#### RAWG - **Resumen:** Gran base de datos (medio millón de juegos), buena para metadata general y enlaces a tiendas. - **Autenticación / Requisitos:** API key en query string (`key=YOUR_API_KEY`). @@ -35,9 +240,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Free tier para proyectos personales; planes comerciales (pago mensual) para uso en productos con gran tráfico. - **Enlace:** https://rawg.io/apidocs ---- - -### TheGamesDB +#### TheGamesDB - **Resumen:** Base de datos comunitaria para juegos y artwork, con API pública v2. - **Autenticación / Requisitos:** Registro y uso de API key (ver docs); repositorio público del proyecto (GPLv3 para el código del servidor). @@ -48,9 +251,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Cláusula clave:** No documentado públicamente — verificar con el equipo de TheGamesDB antes de uso comercial/redistribución. - **Enlace:** https://api.thegamesdb.net/ ---- - -### ScreenScraper +#### ScreenScraper - **Resumen:** Servicio francés orientado a frontends, con enorme cantidad de media y opciones de scraping. - **Autenticación / Requisitos:** Cuenta en ScreenScraper; modelo de soporte/donación que habilita límites mayores. @@ -62,9 +263,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Donación / suscripción para aumentar cuotas y velocidad. - **Enlace:** https://www.screenscraper.fr/ ---- - -### MobyGames +#### MobyGames - **Resumen:** Base histórica con screenshots, covers, reviews y credits; muy usada por investigación y metadata profunda. - **Autenticación / Requisitos:** API y/o MobyPlus; la API requiere registro y suscripción. @@ -76,9 +275,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Acceso vía suscripción / MobyPro; contactar para condiciones comerciales. - **Enlace:** https://www.mobygames.com/api/subscribe/ ---- - -### PriceCharting +#### PriceCharting - **Resumen:** Fuente especializada en historial de precios para juegos físicos y coleccionables. - **Autenticación / Requisitos:** API documentada en el sitio; el acceso completo requiere suscripción / token pagado. @@ -90,9 +287,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Servicio comercial (licencias / API keys pagadas). - **Enlace:** https://www.pricecharting.com/api-documentation ---- - -### IsThereAnyDeal (Itad) +#### IsThereAnyDeal (Itad) - **Resumen:** Agregador de ofertas con histórico y mapeo de keys/tiendas; útil para tracking de ofertas digitales. - **Autenticación / Requisitos:** API Key (docs en https://docs.isthereanydeal.com/). @@ -104,9 +299,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Free tier; acuerdos comerciales para uso intensivo. - **Enlace:** https://docs.isthereanydeal.com/ ---- - -### eBay +#### eBay - **Resumen:** Fuente de datos de mercado (listings, precios vendidos) para estimar valor real de mercado. - **Autenticación / Requisitos:** Registro en eBay Developers Program; claves y OAuth para endpoints de venta/completed items. @@ -118,9 +311,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots - **Costes / modelo:** Free para desarrolladores con límites; uso intensivo o comerciales pueden requerir acuerdos o certificaciones. - **Enlace:** https://developer.ebay.com/ ---- - -## Tabla resumida +### Tabla resumida | API | Data types | Auth | Free / Paid | Fecha verificación | Licencia / Nota legal | Notes | | -------------- | ------------------------------------------------------- | -------------------------------- | ------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | @@ -133,9 +324,7 @@ Comparar APIs públicas y comerciales que aportan metadatos (covers, screenshots | MobyGames | screenshots, credits, covers | Subscribe / API key | Paid / subscription | 2026-02-07 | Paid/Subscribe: https://www.mobygames.com/api/subscribe/ | Access via subscription; non-commercial rate limits documented | | eBay | listings, sold data | eBay Dev keys / OAuth | Free (with limits) | 2026-02-07 | TOS: https://developer.ebay.com/ | Terms restrict distribution; API License Agreement | ---- - -## Conclusión y recomendación para MVP +### Conclusión y recomendación para MVP Recomiendo un **set inicial de APIs (priorizado)**: **IGDB, RAWG, TheGamesDB, ScreenScraper, PriceCharting, IsThereAnyDeal.** @@ -144,13 +333,171 @@ Recomiendo un **set inicial de APIs (priorizado)**: **IGDB, RAWG, TheGamesDB, Sc --- -## Vacíos y verificación pendiente +## Estrategias técnicas -- **APIs que requieren suscripción / acuerdos comerciales:** PriceCharting (API premium, requiere suscripción), MobyGames (MobyPro/API requiere suscripción), EmuMovies (servicio comercial con TOS y cuentas), y en casos especiales eBay (certificaciones / acuerdos adicionales para ciertos permisos). -- **PriceCharting:** la documentación de la API existe pero el acceso completo está sujeto a registro/pago; no se publicó límite público durante la verificación. -- **MobyGames:** API y límites requieren suscripción/registro; hay que contactar para condiciones comerciales. -- **eBay:** múltiples APIs y límites por endpoint; requiere revisar caso de uso específico y cumplimiento del API License Agreement. -- **Notas:** Algunas APIs (ScreenScraper) usan modelos por donación/premium para aumentar cuotas; en APIs sin límites públicos, contactar al proveedor para confirmar condiciones. +### Variables de entorno (ejemplos) + +``` +IGDB_CLIENT_ID=... +IGDB_CLIENT_SECRET=... +RAWG_API_KEY=... +THEGAMESDB_API_KEY=... +SCREENSCRAPER_USERNAME=... +SCREENSCRAPER_PASSWORD=... +EXTERNAL_API_CONCURRENCY=5 +``` + +> Nota: **Nunca** exponer estas claves en el cliente; siempre pasar por el backend. + +### Normalización de datos + +```typescript +interface NormalizedGame { + title: string; + platform: string; + release_date: string; // ISO 8601 + genres: string[]; + cover_url: string; + external_ids: { + igdb?: string; + rawg?: string; + thegamesdb?: string; + }; + source: 'igdb' | 'rawg' | 'thegamesdb' | 'screenscraper'; +} +``` + +### Ejemplo de implementación + +```typescript +class MetadataService { + private apis = [ + new IGDBService(), + new RAWGService(), + new TheGamesDBService(), + new ScreenScraperService(), + ]; + + async searchGame(title: string): Promise { + for (const api of this.apis) { + try { + const result = await api.search(title); + if (result) { + return this.normalize(result, api.getSource()); + } + } catch (error) { + console.warn(`${api.getSource()} failed:`, error); + continue; + } + } + throw new Error('All APIs failed'); + } + + private normalize(data: any, source: string): NormalizedGame { + return { + title: data.name || data.title, + platform: data.platforms?.[0]?.name || '', + release_date: this.normalizeDate(data.first_release_date || data.released), + genres: data.genres?.map((g: any) => g.name) || [], + cover_url: data.cover?.url || data.background_image || '', + external_ids: { + igdb: data.id, + rawg: data.id, + thegamesdb: data.id, + }, + source: source as any, + }; + } +} +``` + +--- + +## Configuración y despliegue + +### Testing Without Real Keys + +Para desarrollo/testing: + +- Dejar API keys como `your_*_here` en `.env.local` +- Quasar will gracefully degrade and show limited metadata +- Frontend will still work with manual game entry + +### Production Deployment + +Para producción: + +1. Generar nuevas claves en cada servicio (no reutilizar claves de desarrollo) +2. Almacenar claves en **Gitea Secrets** (para pipelines CI/CD automatizados) +3. O usar variables de entorno en tu proveedor de hosting +4. Rotar claves cada 3 meses +5. Monitorear límites de rate en los dashboards de los servicios + +### Gitea Actions CI/CD Setup + +Para habilitar pruebas automatizadas con API keys en Gitea Actions: + +#### 1. Store Secrets in Gitea + +Navigate to your repository settings: + +``` +https://your-gitea-instance/your-org/quasar/settings/secrets/actions +``` + +Add these secrets: + +- `IGDB_CLIENT_ID` (from Twitch Developer Console) +- `IGDB_CLIENT_SECRET` (from Twitch Developer Console) +- `RAWG_API_KEY` (from RAWG settings) +- `THEGAMESDB_API_KEY` (from TheGamesDB API) +- `SCREENSCRAPER_USERNAME` (from ScreenScraper) +- `SCREENSCRAPER_PASSWORD` (from ScreenScraper) + +#### 2. Workflow Configuration + +The `.gitea/workflows/ci.yml` workflow automatically: + +- ✅ Installs dependencies +- ✅ Runs linting checks +- ✅ Executes backend tests (Vitest) +- ✅ Executes frontend tests (Vitest) +- ✅ Starts backend + frontend servers +- ✅ Runs E2E tests (Playwright) with real metadata APIs +- ✅ Uploads test reports on failure + +#### 3. Testing Flow + +1. **Push** code to `main` or `develop` +2. **Gitea Actions** picks up the `.gitea/workflows/ci.yml` +3. **Secrets are injected** as environment variables +4. **E2E tests** fetch real metadata from APIs (using injected secrets) +5. **Build fails** if any test fails (prevents broken code) + +#### 4. Local Development + +For local testing, use `.env.local`: + +```bash +IGDB_CLIENT_ID=your_local_id +IGDB_CLIENT_SECRET=your_local_secret +RAWG_API_KEY=your_local_key +THEGAMESDB_API_KEY=your_local_key +SCREENSCRAPER_USERNAME=your_username +SCREENSCRAPER_PASSWORD=your_password +``` + +**Note:** CI/CD uses Gitea Secrets (not `.env` files), so never commit real credentials. + +### Troubleshooting + +**"IGDB_CLIENT_ID not found"** → Check `.env` file exists and has correct format + +**"429 Too Many Requests"** → Rate limit exceeded, wait and retry + +**"Invalid API Key"** → Copy key exactly (no spaces), verify it's active on service website + +**"ScreenScraper authentication failed"** → Check donation level and account status --- diff --git a/docs/02-tecnico/frontend.md b/docs/02-tecnico/frontend.md new file mode 100644 index 0000000..f9dce59 --- /dev/null +++ b/docs/02-tecnico/frontend.md @@ -0,0 +1,1195 @@ +# Frontend de Quasar 🎮 + +## Visión general + +El frontend de Quasar es una Single Page Application (SPA) moderna construida con React + Vite, utilizando shadcn/ui para componentes UI y Tailwind CSS para estilos. La aplicación está diseñada con una estética de modo oscuro sofisticada, priorizando la accesibilidad, la responsividad y un excelente rendimiento. + +### Stack tecnológico + +| Categoría | Tecnología | Versión | Propósito | +| -------------- | --------------- | -------- | ---------------------------------- | +| Framework | React | ^18.3.0 | Framework base | +| Build tool | Vite | ^5.0.0 | Build tool y dev server | +| Lenguaje | TypeScript | ^5.3.0 | Type safety | +| Estado | TanStack Query | ^5.0.0 | Gestión de estado y cache de datos | +| Routing | TanStack Router | ^1.0.0 | Routing con carga de datos | +| Formularios | TanStack Form | ^0.0.0 | Manejo de formularios | +| Validación | Zod | ^3.0.0 | Validación de esquemas | +| Componentes UI | shadcn/ui | latest | Componentes UI reutilizables | +| Estilos | Tailwind CSS | ^3.4.0 | Framework de estilos | +| Iconos | lucide-react | ^0.300.0 | Iconos | +| HTTP | axios | latest | Cliente HTTP | +| Fechas | date-fns | latest | Manipulación de fechas | +| Utils | clsx | ^2.0.0 | Utilidad de clases condicionales | +| Utils | tailwind-merge | ^2.0.0 | Fusión de clases Tailwind | + +## Arquitectura del sistema + +### Diagrama de arquitectura + +```mermaid +graph TB + subgraph Browser + UI[Componentes UI] + Router[TanStack Router] + Query[TanStack Query] + Forms[TanStack Form + Zod] + end + + subgraph Services + API[API Client] + Auth[Auth Service] + end + + UI --> Router + UI --> Query + UI --> Forms + Router --> Query + Query --> API + Forms --> Query + + API --> Backend[Backend API] + + style UI fill:#1e293b + style Router fill:#334155 + style Query fill:#334155 + style Forms fill:#334155 + style API fill:#475569 + style Backend fill:#64748b +``` + +### Estructura de carpetas + +``` +frontend/ +├── src/ +│ ├── components/ +│ │ ├── ui/ # Componentes shadcn/ui +│ │ ├── layout/ # Componentes de layout +│ │ ├── games/ # Componentes específicos de juegos +│ │ ├── roms/ # Componentes específicos de ROMs +│ │ ├── shared/ # Componentes compartidos +│ │ └── ... # Otros componentes específicos +│ ├── pages/ # Páginas de la aplicación +│ │ ├── DashboardPage.tsx +│ │ ├── GamesPage.tsx +│ │ ├── GameDetailPage.tsx +│ │ ├── GamesNewPage.tsx +│ │ ├── ImportPage.tsx +│ │ ├── PlatformsPage.tsx +│ │ ├── TagsPage.tsx +│ │ ├── SettingsPage.tsx +│ │ └── ExportPage.tsx +│ ├── api/ # Servicios de API y tipos +│ │ ├── client.ts # Cliente HTTP base +│ │ ├── games.ts # Servicios de juegos +│ │ ├── platforms.ts # Servicios de plataformas +│ │ ├── tags.ts # Servicios de etiquetas +│ │ ├── import.ts # Servicios de importación +│ │ ├── settings.ts # Servicios de configuración +│ │ └── types.ts # Tipos de API +│ ├── query/ # Configuración de TanStack Query +│ │ └── client.tsx # Cliente de Query +│ ├── form/ # Configuración de TanStack Form +│ │ └── config.tsx # Configuración de Form + Zod +│ ├── router/ # Configuración de TanStack Router +│ │ └── router.tsx # Configuración del router +│ ├── types/ # Definiciones TypeScript +│ │ └── index.ts # Tipos globales +│ ├── hooks/ # Custom hooks +│ ├── lib/ # Utilidades y configuraciones +│ │ ├── utils.ts # Utilidades generales +│ │ └── ... # Otras utilidades +│ ├── styles/ # Estilos globales +│ │ └── globals.css # CSS global con variables +│ └── layout/ # Componentes de layout +│ ├── Header.tsx # Header principal +│ ├── Sidebar.tsx # Sidebar de navegación +│ └── Layout.tsx # Layout principal +├── public/ # Assets estáticos +├── index.html # HTML entry point +├── package.json # Dependencias y scripts +├── tsconfig.json # Configuración TypeScript +├── vite.config.ts # Configuración Vite +├── tailwind.config.ts # Configuración Tailwind +└── postcss.config.js # Configuración PostCSS +``` + +## Sistema de diseño + +### Principios de diseño + +1. **Modo oscuro por defecto**: La aplicación está diseñada para funcionar principalmente en modo oscuro +2. **Alto contraste**: Asegurar legibilidad excelente en todos los contextos +3. **Consistencia**: Componentes y patrones consistentes en toda la aplicación +4. **Accesibilidad**: Cumplimiento con WCAG AA +5. **Performance**: Componentes optimizados para rendimiento +6. **Responsividad**: Mobile-first con breakpoints claros + +### Paleta de colores (Modo oscuro) + +#### Colores semánticos + +```css +/* Primary - Violet (Acción principal) */ +--primary-50: #f5f3ff; +--primary-100: #ede9fe; +--primary-200: #ddd6fe; +--primary-300: #c4b5fd; +--primary-400: #a78bfa; +--primary-500: #8b5cf6; +--primary-600: #7c3aed; /* Color principal */ +--primary-700: #6d28d9; +--primary-800: #5b21b6; +--primary-900: #4c1d95; +--primary-950: #2e1065; + +/* Secondary - Slate (Acciones secundarias) */ +--secondary-50: #f8fafc; +--secondary-100: #f1f5f9; +--secondary-200: #e2e8f0; +--secondary-300: #cbd5e1; +--secondary-400: #94a3b8; +--secondary-500: #64748b; +--secondary-600: #475569; +--secondary-700: #334155; +--secondary-800: #1e293b; +--secondary-900: #0f172a; +--secondary-950: #020617; + +/* Success - Emerald (Estados positivos) */ +--success-50: #ecfdf5; +--success-100: #d1fae5; +--success-200: #a7f3d0; +--success-300: #6ee7b7; +--success-400: #34d399; +--success-500: #10b981; +--success-600: #059669; +--success-700: #047857; +--success-800: #065f46; +--success-900: #064e3b; +--success-950: #022c22; + +/* Warning - Amber (Estados de advertencia) */ +--warning-50: #fffbeb; +--warning-100: #fef3c7; +--warning-200: #fde68a; +--warning-300: #fcd34d; +--warning-400: #fbbf24; +--warning-500: #f59e0b; +--warning-600: #d97706; +--warning-700: #b45309; +--warning-800: #92400e; +--warning-900: #78350f; +--warning-950: #451a03; + +/* Error - Red (Estados de error) */ +--error-50: #fef2f2; +--error-100: #fee2e2; +--error-200: #fecaca; +--error-300: #fca5a5; +--error-400: #f87171; +--error-500: #ef4444; +--error-600: #dc2626; +--error-700: #b91c1c; +--error-800: #991b1b; +--error-900: #7f1d1d; +--error-950: #450a0a; + +/* Info - Sky (Información) */ +--info-50: #f0f9ff; +--info-100: #e0f2fe; +--info-200: #bae6fd; +--info-300: #7dd3fc; +--info-400: #38bdf8; +--info-500: #0ea5e9; +--info-600: #0284c7; +--info-700: #0369a1; +--info-800: #075985; +--info-900: #0c4a6e; +--info-950: #082f49; +``` + +#### Uso de colores + +| Propósito | Color | Hex | +| -------------- | ---------- | ------- | +| Background | Slate-950 | #020617 | +| Card | Slate-950 | #020617 | +| Card hover | Slate-900 | #0f172a | +| Border | Slate-800 | #1e293b | +| Primary action | Violet-600 | #7c3aed | +| Primary hover | Violet-700 | #6d28d9 | +| Text primary | Slate-50 | #f8fafc | +| Text secondary | Slate-400 | #94a3b8 | +| Text muted | Slate-500 | #64748b | + +### Tipografía + +#### Font family + +```css +font-family: + 'Inter', + system-ui, + -apple-system, + sans-serif; +``` + +#### Escala de tamaños + +| Token | Tamaño | Line-height | Uso | +| --------- | ------ | ----------- | ------------------------- | +| text-xs | 12px | 16px | Labels pequeños, captions | +| text-sm | 14px | 20px | Texto secundario | +| text-base | 16px | 24px | Texto de cuerpo | +| text-lg | 18px | 28px | Texto de cuerpo grande | +| text-xl | 20px | 28px | Subtítulos | +| text-2xl | 24px | 32px | Títulos pequeños | +| text-3xl | 30px | 40px | Títulos medianos | +| text-4xl | 36px | 44px | Títulos grandes | +| text-5xl | 48px | 56px | Display | + +#### Font weights + +| Token | Weight | Uso | +| ------------- | ------ | ---------------- | +| font-normal | 400 | Texto de cuerpo | +| font-medium | 500 | Texto de énfasis | +| font-semibold | 600 | Subtítulos | +| font-bold | 700 | Títulos | + +### Espaciado + +#### Escala de espaciado (base 4px) + +| Token | Valor | Uso | +| ---------- | ----- | ---------------------------- | +| p-0, m-0 | 0px | Sin espaciado | +| p-1, m-1 | 4px | Micro espaciado | +| p-2, m-2 | 8px | Espaciado pequeño | +| p-3, m-3 | 12px | Espaciado medio-pequeño | +| p-4, m-4 | 16px | Espaciado medio | +| p-5, m-5 | 20px | Espaciado medio-grande | +| p-6, m-6 | 24px | Espaciado grande | +| p-8, m-8 | 32px | Espaciado extra grande | +| p-10, m-10 | 40px | Espaciado extra extra grande | +| p-12, m-12 | 48px | Espaciado masivo | + +## Rutas de la aplicación + +### Diagrama de rutas + +```mermaid +graph LR + A[/] --> B[Dashboard] + C[/games] --> D[Lista de juegos] + E[/games/:id] --> F[Detalle de juego] + G[/games/new] --> H[Crear juego] + I[/import] --> J[Importar ROMs] + K[/platforms] --> L[Gestión de plataformas] + M[/tags] --> N[Gestión de etiquetas] + O[/settings] --> P[Configuración] + Q[/export] --> R[Exportar/Importar] + + style A fill:#7c3aed + style C fill:#7c3aed + style E fill:#7c3aed + style G fill:#7c3aed + style I fill:#7c3aed + style K fill:#7c3aed + style M fill:#7c3aed + style O fill:#7c3aed + style Q fill:#7c3aed +``` + +### Descripción de rutas + +| Ruta | Componente | Descripción | +| ------------ | -------------- | ------------------------------------------------------- | +| `/` | Dashboard | Vista general con estadísticas y juegos recientes | +| `/games` | GamesPage | Lista paginada de juegos con filtros y búsqueda | +| `/games/:id` | GameDetailPage | Vista detallada de un juego con metadata, ROMs, compras | +| `/games/new` | GamesNewPage | Formulario para crear un nuevo juego | +| `/import` | ImportPage | Interfaz para escanear e importar ROMs locales | +| `/platforms` | PlatformsPage | Gestión de plataformas | +| `/tags` | TagsPage | Gestión de etiquetas | +| `/settings` | SettingsPage | Configuración de la aplicación | +| `/export` | ExportPage | Exportar/importar colección | + +## Componentes UI (shadcn/ui) + +### Componentes base + +| Componente | Uso principal | +| ------------ | ---------------------------------- | +| Button | Acciones principales y secundarias | +| Card | Contenedores de contenido | +| Input | Entradas de texto | +| Select | Selección de opciones | +| Dialog | Modales y diálogos | +| Table | Tablas de datos | +| Badge | Etiquetas y estados | +| Avatar | Avatares de usuario | +| DropdownMenu | Menús desplegables | +| Tabs | Navegación por pestañas | +| Form | Formularios | +| Separator | Separadores visuales | +| ScrollArea | Áreas con scroll | +| Tooltip | Información contextual | +| Skeleton | Estados de carga | +| AlertDialog | Confirmaciones | +| Toast | Notificaciones | + +### Variantes y tamaños + +#### Button + +**Variantes:** + +- `default`: Acción principal (violet) +- `destructive`: Acción destructiva (rojo) +- `outline`: Acción secundaria con borde +- `secondary`: Acción secundaria (slate) +- `ghost`: Acción sutil sin borde +- `link`: Estilo de enlace + +**Tamaños:** + +- `sm`: 32px de altura +- `default`: 40px de altura +- `lg`: 48px de altura +- `icon`: 32px x 32px (cuadrado) + +#### Card + +```tsx + + + Título + Descripción opcional + + Contenido principal + Acciones + +``` + +#### Badge + +**Variantes:** + +- `default`: Violet +- `secondary`: Slate +- `destructive`: Rojo +- `outline`: Borde solo +- `success`: Verde +- `warning`: Amarillo + +## Componentes específicos + +### Componentes de Juegos + +#### GameCard + +Componente de tarjeta para mostrar un juego en una lista o grid. + +```typescript +interface GameCardProps { + game: Game; + onClick?: () => void; + onEdit?: () => void; + onDelete?: () => void; +} +``` + +**Estructura:** + +```tsx + + +
+
+ +
+ {game.title} + + {game.platforms.map((p) => p.name).join(', ')} + +
+
+ + + + + + + + Editar + + + + + Eliminar + + + +
+
+ +
+ {game.tags.slice(0, 3).map((tag) => ( + + {tag.name} + + ))} + {game.tags.length > 3 && ( + + +{game.tags.length - 3} + + )} +
+
+ +
+
+ + {game.romFiles.length} ROMs +
+ {game.releaseDate && ( +
+ + {formatDate(game.releaseDate)} +
+ )} +
+
+
+``` + +#### GameList + +Componente de lista de juegos con filtros y paginación. + +```typescript +interface GameListProps { + filters?: GameFilters; + onFiltersChange?: (filters: GameFilters) => void; +} +``` + +**Estructura:** + +```tsx +
+ + + +
+``` + +#### GameDetail + +Página de detalle de un juego. + +```tsx +
+ + + + Resumen + ROMs + Compras + Metadata + + + + + + + + + + + + + + +
+``` + +### Componentes de ROMs + +#### RomFileCard + +Componente de tarjeta para mostrar un archivo ROM. + +```typescript +interface RomFileCardProps { + rom: RomFile; + game?: Game; + onLink?: () => void; + onUnlink?: () => void; +} +``` + +#### ImportRomWizard + +Wizard para importar ROMs desde el sistema de archivos. + +**Pasos del wizard:** + +1. **Configurar rutas**: Seleccionar directorios para escanear +2. **Escanear**: Ejecutar escaneo de archivos +3. **Revisar**: Revisar resultados y detectar duplicados +4. **Importar**: Importar ROMs seleccionadas + +### Componentes de Metadata + +#### MetadataSearch + +Componente para buscar metadata de APIs externas (IGDB, RAWG). + +```typescript +interface MetadataSearchProps { + onSelect: (metadata: ExternalMetadata) => void; + game?: Game; +} +``` + +**Estructura:** + +```tsx +
+
+ setSearchQuery(e.target.value)} + /> + +
+ {isLoading && } + {results && ( +
+ {results.map((result) => ( + onSelect(result)} /> + ))} +
+ )} +
+``` + +### Componentes de Dashboard + +#### DashboardStats + +Tarjetas de estadísticas del dashboard. + +```typescript +interface DashboardStatsProps { + stats: { + totalGames: number; + totalRoms: number; + totalPlatforms: number; + recentGames: number; + }; +} +``` + +**Estructura:** + +```tsx +
+ } + /> + } + /> + } + /> + } + /> +
+``` + +## Layout Components + +### AppLayout + +Layout principal de la aplicación. + +```typescript +interface AppLayoutProps { + children: React.ReactNode; +} +``` + +**Estructura:** + +```tsx +
+
+
+ +
{children}
+
+
+``` + +### Header + +Header de la aplicación. + +```typescript +interface HeaderProps { + onMenuToggle?: () => void; +} +``` + +**Estructura:** + +```tsx +
+
+ + +
+ + +
+
+
+``` + +### Sidebar + +Sidebar de navegación. + +```typescript +interface SidebarProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} +``` + +**Estructura:** + +```tsx + +``` + +## Gestión de estado + +### TanStack Query + +- **Cache de datos**: Cache automático de respuestas de API +- **Invalidación**: Invalidación inteligente de cache +- **Optimistic updates**: Actualizaciones optimistas +- **Background refetch**: Refetch en background + +```typescript +// Ejemplo de query +const gamesQuery = useQuery({ + queryKey: ['games', { page, limit, search }], + queryFn: () => fetchGames({ page, limit, search }), + staleTime: 5 * 60 * 1000, // 5 minutos +}); + +// Ejemplo de mutation +const createGameMutation = useMutation({ + mutationFn: createGame, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['games'] }); + }, +}); +``` + +### TanStack Router + +- **File-based routing**: Rutas basadas en archivos +- **Route loaders**: Carga de datos antes de renderizar +- **Type-safe routing**: Tipado de parámetros y búsqueda +- **Code splitting**: División de código automática + +```typescript +// Ejemplo de route loader +export const loader = ({ params }: LoaderArgs) => { + return queryClient.ensureQueryData({ + queryKey: ['game', params.id], + queryFn: () => fetchGame(params.id), + }); +}; +``` + +### TanStack Form + Zod + +```typescript +// Esquema de validación +const gameSchema = z.object({ + title: z.string().min(1, 'El título es requerido'), + description: z.string().optional(), + releaseDate: z.date().optional(), + platforms: z.array(z.string()).min(1, 'Selecciona al menos una plataforma'), + tags: z.array(z.string()).default([]), +}) + +type GameFormData = z.infer + +// Componente de formulario +function GameForm() { + const form = useForm({ + defaultValues: { + title: '', + description: '', + platforms: [], + tags: [], + }, + validators: { + onChange: gameSchema, + }, + }) + + const { mutate: createGame } = useCreateGame() + + const handleSubmit = (data: GameFormData) => { + createGame(data) + } + + return ( +
{ + e.preventDefault() + form.handleSubmit() + }} + > + + !value ? 'El título es requerido' : undefined, + }} + > + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors && ( + {field.state.meta.errors.join(', ')} + )} +
+ )} +
+ +
+ ) +} +``` + +## API Client + +```typescript +// API client base +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', + timeout: 10000, +}); + +// Interceptor para manejo de errores +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // Redirigir a login + } + return Promise.reject(error); + } +); + +// Servicios de API +export const gamesApi = { + list: (params: GamesListParams) => apiClient.get('/games', { params }), + get: (id: string) => apiClient.get(`/games/${id}`), + create: (data: CreateGameDto) => apiClient.post('/games', data), + update: (id: string, data: UpdateGameDto) => apiClient.patch(`/games/${id}`, data), + delete: (id: string) => apiClient.delete(`/games/${id}`), +}; +``` + +## Accesibilidad + +### WCAG AA Compliance + +- **Contraste**: Mínimo 4.5:1 para texto normal, 3:1 para texto grande +- **Keyboard navigation**: Navegación completa por teclado +- **Focus indicators**: Indicadores visuales de foco +- **Screen reader support**: ARIA labels y roles apropiados +- **Semantic HTML**: Uso correcto de elementos semánticos + +### Implementación + +```typescript +// Ejemplo de componente accesible + +``` + +## Responsividad + +### Breakpoints + +```css +/* Tailwind breakpoints */ +sm: 640px /* Móvil grande */ +md: 768px /* Tablet */ +lg: 1024px /* Desktop pequeño */ +xl: 1280px /* Desktop */ +2xl: 1536px /* Desktop grande */ +``` + +### Estrategia mobile-first + +```typescript +// Ejemplo de componente responsivo +
+ {games.map((game) => ( + + ))} +
+``` + +## Optimizaciones + +### Performance + +- **Code splitting**: División de código por rutas +- **Lazy loading**: Carga diferida de componentes +- **Image optimization**: Optimización de imágenes +- **Memoization**: Uso de React.memo y useMemo +- **Virtual scrolling**: Para listas largas + +### Bundle size + +- **Tree shaking**: Eliminación de código no usado +- **Compression**: Gzip/Brotli compression +- **CDN**: Uso de CDN para dependencias + +## Configuración + +### Scripts de desarrollo + +```json +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "type-check": "tsc --noEmit", + "test": "vitest", + "test:ui": "vitest --ui", + "test:e2e": "playwright test" + } +} +``` + +### Variables de entorno + +```env +# API +VITE_API_URL=http://localhost:3000 + +# Feature flags +VITE_ENABLE_ANALYTICS=false +VITE_ENABLE_TELEMETRY=false +``` + +### Configuración de Tailwind + +```typescript +import type { Config } from 'tailwindcss'; + +const config: Config = { + darkMode: ['class'], + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [], +}; + +export default config; +``` + +### Configuración de Vite + +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}); +``` + +## Testing + +### Estrategia de testing + +- **Unit tests**: Componentes y hooks individuales +- **Integration tests**: Flujos de usuario +- **E2E tests**: Flujos completos con Playwright + +### Herramientas + +- **Vitest**: Unit tests +- **Testing Library**: Testing de componentes React +- **Playwright**: E2E tests + +## Patrones de diseño + +### Page Layout + +```tsx +
+
+
+ +
+ + +
+
+
+``` + +### Card Grid + +```tsx +
+ {items.map((item) => ( + {/* Card content */} + ))} +
+``` + +### Form Layout + +```tsx +
+ + ( + + Título + + + + Descripción del campo + + + )} + /> +
+ + +
+ + +``` + +### Estados de carga + +#### Skeleton Loading + +```tsx +{isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + ))} +
+) : ( + // Actual content +)} +``` + +#### Empty State + +```tsx +
+
+ +
+

No hay elementos

+

Comienza añadiendo tu primer elemento.

+ +
+``` + +## Iconos + +Se utiliza `lucide-react` para todos los iconos. Iconos comunes: + +| Icono | Uso | +| -------------- | ------------- | +| `Home` | Dashboard | +| `Gamepad2` | Juegos | +| `Upload` | Importar | +| `Settings` | Configuración | +| `Monitor` | Plataformas | +| `Tag` | Etiquetas | +| `Download` | Exportar | +| `Search` | Búsqueda | +| `Plus` | Crear nuevo | +| `Edit` | Editar | +| `Trash2` | Eliminar | +| `Filter` | Filtros | +| `SortAsc` | Ordenar | +| `ChevronRight` | Navegación | +| `Menu` | Menú móvil | +| `X` | Cerrar | +| `Check` | Confirmar | +| `AlertCircle` | Advertencia | +| `Info` | Información | +| `Loader2` | Carga | + +## Implementación + +### Prerrequisitos + +- Node.js 18+ +- Yarn 4.x (package manager) + +### Instalación + +```bash +# Navegar al directorio frontend +cd frontend + +# Instalar dependencias +yarn install + +# Iniciar desarrollo +yarn dev +``` + +### Build para producción + +```bash +# Build +yarn build + +# Preview +yarn preview +``` + +## Metadatos + +**Autor**: Quasar Frontend Team +**Última actualización**: 2026-02-22 +**Versión**: 1.0.0 +**Estado**: Implementación completa en progreso diff --git a/docs/lessons-learned.md b/docs/02-tecnico/lessons-learned.md similarity index 100% rename from docs/lessons-learned.md rename to docs/02-tecnico/lessons-learned.md diff --git a/docs/competitive-analysis.md b/docs/03-analisis/competitive-analysis.md similarity index 100% rename from docs/competitive-analysis.md rename to docs/03-analisis/competitive-analysis.md diff --git a/docs/04-operaciones/deployment.md b/docs/04-operaciones/deployment.md new file mode 100644 index 0000000..2186c48 --- /dev/null +++ b/docs/04-operaciones/deployment.md @@ -0,0 +1,546 @@ +# Guía de Despliegue y Operaciones 🚀 + +Esta guía cubre el despliegue, configuración y operación de Quasar en producción. + +--- + +## Tabla de Contenidos + +1. [Requisitos del Sistema](#requisitos-del-sistema) +2. [Configuración de Producción](#configuración-de-producción) +3. [Despliegue](#despliegue) +4. [Monitoreo y Mantenimiento](#monitoreo-y-mantenimiento) +5. [Actualizaciones](#actualizaciones) +6. [Backup y Recuperación](#backup-y-recuperación) +7. [Solución de Problemas](#solución-de-problemas) + +--- + +## Requisitos del Sistema + +### Hardware Mínimo + +- **CPU:** 2 cores +- **RAM:** 4GB +- **Almacenamiento:** 20GB (para ROMs y metadata) +- **Red:** Estable (para descargas de artwork) + +### Software + +- **Node.js 18+** +- **Yarn 4.x** +- **SQLite** (o PostgreSQL para producción) +- **Nginx** (recomendado para reverse proxy) +- **Certificado SSL** (HTTPS obligatorio) + +### Dependencias Externas + +- Claves API de IGDB, RAWG, TheGamesDB +- Acceso a servicios de descarga de imágenes + +--- + +## Configuración de Producción + +### Variables de Entorno + +Crear `.env.production` con: + +```env +# Database +DATABASE_URL="file:./production.db" +# Para PostgreSQL: postgresql://user:password@localhost:5432/quasar + +# API Keys +IGDB_CLIENT_ID=your_production_client_id +IGDB_CLIENT_SECRET=your_production_client_secret +RAWG_API_KEY=your_production_api_key +THEGAMESDB_API_KEY=your_production_api_key +SCREENSCRAPER_USERNAME=your_screenscraper_username +SCREENSCRAPER_PASSWORD=your_screenscraper_password + +# App Config +NODE_ENV=production +PORT=3000 +HOST=0.0.0.0 +LOG_LEVEL=info + +# Security +CORS_ORIGIN=https://yourdomain.com +JWT_SECRET=your_secure_jwt_secret_here +API_RATE_LIMIT=100 + +# Performance +CACHE_TTL=86400 +MAX_CONCURRENT_API_REQUESTS=5 +``` + +### Configuración de Nginx + +```nginx +server { + listen 443 ssl http2; + server_name yourdomain.com; + + # SSL Configuration + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + # Security headers + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Frontend + location / { + root /var/www/quasar/frontend/dist; + try_files $uri $uri/ /index.html; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Backend API + location /api/ { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 864s; + } + + # Static files + location /static/ { + root /var/www/quasar; + expires 1y; + add_header Cache-Control "public, immutable"; + } +} +``` + +--- + +## Despliegue + +### Opción 1: Docker (Recomendado) + +```dockerfile +# Dockerfile +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* ./ +RUN yarn install --frozen-lockfile + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build the application +RUN yarn build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 + +# Copy built application +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "dist/server.js"] +``` + +```yaml +# docker-compose.yml +version: '3.8' + +services: + quasar-backend: + build: ./backend + ports: + - '3000:3000' + environment: + - NODE_ENV=production + - DATABASE_URL=file:./production.db + - IGDB_CLIENT_ID=${IGDB_CLIENT_ID} + - IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET} + - RAWG_API_KEY=${RAWG_API_KEY} + - THEGAMESDB_API_KEY=${THEGAMESDB_API_KEY} + volumes: + - ./data:/app/data + - ./backend/prisma:/app/prisma + restart: unless-stopped + + quasar-frontend: + build: ./frontend + ports: + - '5173:5173' + depends_on: + - quasar-backend + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: + - '443:443' + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - quasar-backend + - quasar-frontend + restart: unless-stopped +``` + +### Opción 2: VPS Manual + +```bash +# 1. Setup server +sudo apt update +sudo apt install -y nodejs yarn nginx sqlite3 + +# 2. Clone repository +git clone https://your-repo/quasar.git +cd quasar + +# 3. Install dependencies +yarn install --production + +# 4. Setup environment +cp .env.example .env.production +# Edit .env.production with real values + +# 5. Build frontend +cd frontend +yarn build +cd .. + +# 6. Setup database +cd backend +npx prisma migrate deploy +cd .. + +# 7. Configure nginx +sudo cp nginx.conf /etc/nginx/sites-available/quasar +sudo ln -s /etc/nginx/sites-available/quasar /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx + +# 8. Start services +cd backend +nohup yarn start > /var/log/quasar-backend.log 2>&1 & +cd ../frontend +nohup yarn start > /var/log/quasar-frontend.log 2>&1 & +``` + +--- + +## Monitoreo y Mantenimiento + +### Health Checks + +```bash +# Backend health +curl http://localhost:3000/health + +# Database connection +curl http://localhost:3000/api/health/database + +# API rate limits status +curl http://localhost:3000/api/health/rate-limits +``` + +### Logging + +Configurar logrotate: + +```bash +# /etc/logrotate.d/quasar +/var/log/quasar/*.log { + daily + missingok + rotate 7 + compress + delaycompress + notifempty + copytruncate +} +``` + +### Monitoreo de API Keys + +Crear script para verificar límites: + +```bash +#!/bin/bash +# check-api-limits.sh + +# Check IGDB rate limits +curl -s -I "https://api.igdb.com/v4/games" | grep -i "x-ratelimit" + +# Check RAWG usage +curl -s "https://api.rawg.io/api/games?key=$RAWG_API_KEY&search=test" | jq '.count' + +# Log warnings +echo "$(date): API rate limits checked" >> /var/log/quasar/api-monitor.log +``` + +--- + +## Actualizaciones + +### Proceso de Actualización + +```bash +# 1. Backup +./backup.sh + +# 2. Stop services +sudo systemctl stop quasar-backend +sudo systemctl stop quasar-frontend + +# 3. Pull latest code +git pull origin main + +# 4. Update dependencies +yarn install --frozen-lockfile + +# 5. Build frontend +cd frontend && yarn build && cd .. + +# 6. Run migrations +cd backend && npx prisma migrate deploy && cd .. + +# 7. Start services +sudo systemctl start quasar-backend +sudo systemctl start quasar-frontend +``` + +### Actualizaciones de API Keys + +1. Generar nuevas claves en cada servicio +2. Actualizar variables de entorno +3. Reiniciar servicios +4. Monitorear errores durante 24h + +--- + +## Backup y Recuperación + +### Script de Backup + +```bash +#!/bin/bash +# backup.sh + +BACKUP_DIR="/backups/quasar" +DATE=$(date +%Y%m%d_%H%M%S) +DB_FILE="quasar_$DATE.db" +ROMS_DIR="roms_$DATE" + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +# Backup database +cp backend/prisma/production.db "$BACKUP_DIR/$DB_FILE" + +# Backup ROM metadata (not actual ROMs) +cp -r data/roms_metadata "$BACKUP_DIR/$ROMS_DIR" + +# Backup configuration +cp .env.production "$BACKUP_DIR/env_$DATE" + +# Compress backup +tar -czf "$BACKUP_DIR/backup_$DATE.tar.gz" -C "$BACKUP_DIR" "$DB_FILE" "$ROMS_DIR" "env_$DATE" + +# Clean up old backups (keep last 7 days) +find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete + +echo "Backup completed: $BACKUP_DIR/backup_$DATE.tar.gz" +``` + +### Recuperación + +```bash +#!/bin/bash +# restore.sh + +BACKUP_FILE=$1 +BACKUP_DIR="/backups/quasar" + +if [ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]; then + echo "Backup file not found: $BACKUP_DIR/$BACKUP_FILE" + exit 1 +fi + +# Stop services +sudo systemctl stop quasar-backend +sudo systemctl stop quasar-frontend + +# Extract backup +cd "$BACKUP_DIR" +tar -xzf "$BACKUP_FILE" + +# Restore database +cp "$DB_FILE" backend/prisma/production.db + +# Restore ROM metadata +cp -r "$ROMS_DIR"/* data/ + +# Restore configuration (optional) +# cp "env_$DATE" .env.production + +# Start services +sudo systemctl start quasar-backend +sudo systemctl start quasar-frontend + +echo "Restore completed from: $BACKUP_FILE" +``` + +--- + +## Solución de Problemas + +### Problemas Comunes + +#### 1. "Database connection failed" + +```bash +# Check database file +ls -la backend/prisma/production.db + +# Check permissions +sudo chown -R nodejs:nodejs backend/prisma/ + +# Check database integrity +sqlite3 backend/prisma/production.db "PRAGMA integrity_check;" +``` + +#### 2. "API rate limit exceeded" + +```bash +# Check current rate limits +curl -I "https://api.igdb.com/v4/games" | grep -i "x-ratelimit" + +# Implement backoff strategy +# Check logs for specific API errors +tail -f /var/log/quasar/backend.log | grep "429" +``` + +#### 3. "Frontend cannot connect to backend" + +```bash +# Check backend is running +curl http://localhost:3000/health + +# Check CORS configuration +curl -H "Origin: https://yourdomain.com" -v http://localhost:3000/health + +# Check nginx configuration +sudo nginx -t +``` + +#### 4. "ROM scanning fails" + +```bash +# Check directory permissions +ls -la /path/to/roms/ + +# Check file formats +find /path/to/roms/ -name "*.zip" -o -name "*.7z" -o -name "*.rar" + +# Check disk space +df -h +``` + +### Diagnóstico Remoto + +```bash +# Create diagnostic script +#!/bin/bash +# diagnostic.sh + +echo "=== Quasar Diagnostic Report ===" +echo "Date: $(date)" +echo "Node.js version: $(node --version)" +echo "Yarn version: $(yarn --version)" +echo "" + +echo "=== System Resources ===" +free -h +df -h +echo "" + +echo "=== Services Status ===" +systemctl status quasar-backend +systemctl status quasar-frontend +echo "" + +echo "=== Database Status ===" +sqlite3 backend/prisma/production.db "SELECT COUNT(*) FROM games;" +sqlite3 backend/prisma/production.db "SELECT COUNT(*) FROM rom_files;" +echo "" + +echo "=== API Keys Status ===" +echo "IGDB: ${IGDB_CLIENT_ID:0:10}..." +echo "RAWG: ${RAWG_API_KEY:0:10}..." +echo "TheGamesDB: ${THEGAMESDB_API_KEY:0:10}..." +echo "" + +echo "=== Recent Errors ===" +tail -20 /var/log/quasar/backend.log | grep -i "error" +tail -20 /var/log/quasar/frontend.log | grep -i "error" +``` + +--- + +## Soporte + +### Logs de Depuración + +```bash +# Backend logs +tail -f /var/log/quasar/backend.log + +# Frontend logs +tail -f /var/log/quasar/frontend.log + +# Nginx logs +tail -f /var/log/nginx/access.log +tail -f /var/log/nginx/error.log +``` + +### Contacto + +- **Issues:** Reportar en el repositorio de Gitea +- **Emergencias:** Email: support@yourdomain.com +- **Documentación:** Ver [docs/README.md](../../README.md) + +--- + +_Última actualización: 2026-02-22_ diff --git a/docs/API_KEYS.md b/docs/API_KEYS.md deleted file mode 100644 index d464da0..0000000 --- a/docs/API_KEYS.md +++ /dev/null @@ -1,140 +0,0 @@ -# Obtaining API Keys - -This guide explains how to get credentials for each metadata service. - -## IGDB (Internet Game Database) - -IGDB uses **OAuth 2.0 via Twitch**. Steps: - -1. Go to [Twitch Developer Console](https://dev.twitch.tv/console/apps) -2. Sign in with your Twitch account (create one if needed) -3. Click "Create Application" - - Name: "Quasar" (or your app name) - - Category: Select relevant category - - Accept terms, click Create -4. You'll see: - - **Client ID** — Copy this - - Click "New Secret" to generate **Client Secret** — Copy this -5. Go to Settings → OAuth Redirect URLs - - Add: `http://localhost:3000/oauth/callback` (development) - - For production: `https://yourdomain.com/oauth/callback` -6. In your `.env` file: - ``` - IGDB_CLIENT_ID=your_client_id - IGDB_CLIENT_SECRET=your_client_secret - ``` -7. Start Quasar, it will use IGDB automatically - -**Rate Limit:** 4 requests/second - -## RAWG (Rawg.io) - -RAWG has a simpler **API Key** approach: - -1. Go to [RAWG Settings](https://rawg.io/settings/account) -2. Sign up if needed, then login -3. Find "API Key" section -4. Click "Create new key" (if needed) or copy existing key -5. In your `.env` file: - ``` - RAWG_API_KEY=your_api_key_here - ``` -6. Start Quasar - -**Rate Limit:** 20 requests/second (free tier) - -**Note:** RAWG requires attribution in UI (include "Powered by RAWG" somewhere visible) - -## TheGamesDB (thegamesdb.net) - -TheGamesDB uses a simple **API Key**: - -1. Go to [TheGamesDB API](https://thegamesdb.net/api) -2. Find "API Key" section (free registration required) -3. Register or login -4. Copy your API key -5. In your `.env` file: - ``` - THEGAMESDB_API_KEY=your_api_key_here - ``` -6. Start Quasar - -**Rate Limit:** 1 request/second (free tier) - -## Testing Without Real Keys - -For development/testing: - -- Leave API keys as `your_*_here` in `.env.local` -- Quasar will gracefully degrade and show limited metadata -- Frontend will still work with manual game entry - -## Production Deployment - -For production: - -1. Generate new keys on each service (don't reuse dev keys) -2. Store keys in **Gitea Secrets** (for automated CI/CD pipelines) -3. Or use environment variables on your hosting provider -4. Rotate keys every 3 months -5. Monitor rate limits in service dashboards - -## Gitea Actions CI/CD Setup - -To enable automated testing with API keys in Gitea Actions: - -### 1. Store Secrets in Gitea - -Navigate to your repository settings: - -``` -https://your-gitea-instance/your-org/quasar/settings/secrets/actions -``` - -Add these secrets: - -- `IGDB_CLIENT_ID` (from Twitch Developer Console) -- `IGDB_CLIENT_SECRET` (from Twitch Developer Console) -- `RAWG_API_KEY` (from RAWG settings) -- `THEGAMESDB_API_KEY` (from TheGamesDB API) - -### 2. Workflow Configuration - -The `.gitea/workflows/ci.yml` workflow automatically: - -- ✅ Installs dependencies -- ✅ Runs linting checks -- ✅ Executes backend tests (Vitest) -- ✅ Executes frontend tests (Vitest) -- ✅ Starts backend + frontend servers -- ✅ Runs E2E tests (Playwright) with real metadata APIs -- ✅ Uploads test reports on failure - -### 3. Testing Flow - -1. **Push** code to `main` or `develop` -2. **Gitea Actions** picks up the `.gitea/workflows/ci.yml` -3. **Secrets are injected** as `IGDB_CLIENT_ID`, `IGDB_CLIENT_SECRET`, `RAWG_API_KEY`, `THEGAMESDB_API_KEY` -4. **E2E tests** fetch real metadata from APIs (using injected secrets) -5. **Build fails** if any test fails (prevents broken code) - -### 4. Local Development - -For local testing, use `.env.local`: - -```bash -IGDB_CLIENT_ID=your_local_id -IGDB_CLIENT_SECRET=your_local_secret -RAWG_API_KEY=your_local_key -THEGAMESDB_API_KEY=your_local_key -``` - -**Note:** CI/CD uses Gitea Secrets (not `.env` files), so never commit real credentials. - -## Troubleshooting - -**"IGDB_CLIENT_ID not found"** → Check `.env` file exists and has correct format - -**"429 Too Many Requests"** → Rate limit exceeded, wait and retry - -**"Invalid API Key"** → Copy key exactly (no spaces), verify it's active on service website diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..07c1f34 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,92 @@ +# Documentación del Proyecto Quasar 📚 + +Esta documentación está organizada en secciones lógicas para facilitar la navegación y mantenimiento. + +## Estructura de la Documentación + +``` +docs/ +├── README.md # Este archivo (índice general) +├── 01-conceptos/ # Conceptos fundamentales y requisitos +│ ├── requirements.md # Requisitos funcionales y no funcionales +│ ├── architecture.md # Arquitectura técnica general +│ └── data-model.md # Modelo de datos y esquema +├── 02-tecnico/ # Documentación técnica detallada +│ ├── apis.md # APIs del sistema (consolidado) +│ ├── frontend.md # Documentación del frontend +│ └── lessons-learned.md # Lecciones aprendidas y recomendaciones +├── 03-analisis/ # Análisis comparativos y estudios +│ └── competitive-analysis.md # Análisis competitivo +└── 04-operaciones/ # Guías de operación y despliegue +``` + +## Guía de Navegación + +### 🎯 Para nuevos desarrolladores + +1. Comienza con [`01-conceptos/requirements.md`](01-conceptos/requirements.md) para entender el propósito +2. Lee [`01-conceptos/architecture.md`](01-conceptos/architecture.md) para la visión general +3. Revisa [`01-conceptos/data-model.md`](01-conceptos/data-model.md) para entender los datos + +### 🔧 Para trabajo técnico + +1. Consulta [`02-tecnico/apis.md`](02-tecnico/apis.md) para APIs y configuración +2. Revisa [`02-tecnico/frontend.md`](02-tecnico/frontend.md) para detalles del frontend +3. Lee [`02-tecnico/lessons-learned.md`](02-tecnico/lessons-learned.md) para buenas prácticas + +### 📊 Para análisis y decisiones + +1. Revisa [`03-analisis/competitive-analysis.md`](03-analisis/competitive-analysis.md) para contexto del mercado + +### 🚀 Para operaciones y despliegue + +1. Las guías de operación están en desarrollo (sección `04-operaciones/`) + +## Convenciones + +### Formato de enlaces + +Todos los enlaces internos usan formato markdown estándar: + +```markdown +[texto de enlace](ruta/al/archivo.md) +``` + +### Nomenclatura de archivos + +- Todos los usan `kebab-case.md` +- Los prefijos numéricos indican orden de lectura + +### Estructura de documentos + +- Cada documento tiene tabla de contenidos (TOC) +- Secciones numeradas para mejor navegación +- Ejemplos de código con formato sintáctico + +## Estado Actual + +| Sección | Estado | Comentarios | +| -------------- | ---------------- | ------------------------------------ | +| 01-conceptos | ✅ Completa | Documentación fundamental estable | +| 02-tecnico | ✅ Actualizada | APIs consolidados, frontend completo | +| 03-analisis | ✅ Completa | Análisis competitivo actualizado | +| 04-operaciones | 🚧 En desarrollo | Guías de operación pendientes | + +## Próximos Pasos + +- [ ] Añadir documentación de testing y CI/CD +- [ ] Crear índice temático para búsqueda rápida +- [ ] Documentar API REST detallada + +## Contribuir + +Al agregar nuevo contenido: + +1. Coloca el documento en la sección adecuada +2. Sigue las convenciones de nomenclatura +3. Actualiza este README si agregas nuevas secciones +4. Revisa y actualiza referencias cruzadas + +--- + +_Última actualización: 2026-02-22_ diff --git a/docs/api-integration.md b/docs/api-integration.md deleted file mode 100644 index 749d5fe..0000000 --- a/docs/api-integration.md +++ /dev/null @@ -1,148 +0,0 @@ -# Integración de APIs externas — Prioridad y guía práctica - -## Objetivo - -Definir APIs prioritarias para el MVP, cómo obtener credenciales, ejemplos de uso y estrategias de robustez (rate limit, retries, fallback y normalización de datos). - ---- - -## APIs priorizadas (MVP) - -1. **IGDB (prioridad alta)** -2. **RAWG (prioridad alta)** -3. **TheGamesDB (prioridad media)** - ---- - -## IGDB - -- **Obtener credenciales**: registrar una app en Twitch Developer Console para obtener `CLIENT_ID` y `CLIENT_SECRET`. Obtener token con grant type `client_credentials` (POST a `https://id.twitch.tv/oauth2/token`). - -- **Endpoints principales**: `POST https://api.igdb.com/v4/games` (consulta flexible via body con sintaxis IGDB), `POST https://api.igdb.com/v4/covers`, `POST https://api.igdb.com/v4/platforms`. - -- **Ejemplo (buscar)**: - -```bash -# Obtener token -curl -X POST 'https://id.twitch.tv/oauth2/token?client_id=$IGDB_CLIENT_ID&client_secret=$IGDB_CLIENT_SECRET&grant_type=client_credentials' - -# Buscar juegos -curl -X POST 'https://api.igdb.com/v4/games' \ - -H "Client-ID: $IGDB_CLIENT_ID" \ - -H "Authorization: Bearer $IGDB_TOKEN" \ - -H 'Accept: application/json' \ - --data 'fields id,name,first_release_date,platforms.name,genres.name,cover.url; search "zelda"; limit 5;' -``` - -- **Respuesta (esquemática)**: - -```json -[ - { - "id": 12345, - "name": "Ejemplo", - "first_release_date": 1459468800, - "platforms": [{ "name": "Nintendo Switch" }], - "cover": { "url": "//images.igdb.com/...jpg" } - } -] -``` - -- **Límites y manejo**: la API puede devolver `429` o cabeceras de límite; implementar retries exponenciales (ej. 3 intentos) y respetar `Retry-After`. Implementar circuit breaker si la API falla repetidamente. -- **Atribución**: mostrar origen de datos (ej. "Datos: IGDB") según términos del servicio. - ---- - -## RAWG - -- **Obtener credenciales**: registrarse en RAWG para obtener `RAWG_API_KEY` (https://rawg.io/apidocs). -- **Endpoints principales**: `GET https://api.rawg.io/api/games?key=API_KEY&search=...`, `GET https://api.rawg.io/api/games/{id}`. -- **Ejemplo**: - -```bash -curl 'https://api.rawg.io/api/games?key=$RAWG_API_KEY&search=zelda&page_size=5' -``` - -- **Respuesta (esquemática)**: - -```json -{ - "count": 100, - "results": [ - { "id": 3498, "name": "GTA V", "released": "2013-09-17", "background_image": "https://..." } - ] -} -``` - -- **Límites y manejo**: RAWG suele tener límites por clave/plan; cachear y fallback a otros proveedores si falla. -- **Atribución**: revisar condiciones y mostrar HTTP o texto de fuente si es requerido por el proveedor. - ---- - -## TheGamesDB - -- **Obtener credenciales**: crear cuenta y generar API Key en https://thegamesdb.net. -- **Endpoints**: búsqueda por nombre y detalles (`/v1/Games/ByGameName?name=...`, `/v1/Games/ByGameID?id=...`). -- **Ejemplo**: - -```bash -curl -H 'Authorization: Bearer $THEGAMESDB_KEY' 'https://api.thegamesdb.net/v1/Games/ByGameName?name=zelda' -``` - ---- - -## Estrategia de fallback y normalización - -- **Orden de prioridad**: IGDB → RAWG → TheGamesDB (configurable). -- **Normalización (mapping)**: - - `title` ← `name` - - `platform` ← `platforms[].name` - - `release_date` ← `first_release_date` / `released` → convertir a ISO 8601 - - `genres` ← `genres[].name` - - `cover_url` ← `cover.url` / `background_image` - - `external_ids` ← `{ igdb: id, rawg: id, thegamesdb: id }` - -- **Fallback**: si IGDB no tiene portada, intentar RAWG; si falla, usar TheGamesDB. Registrar la fuente usada. - ---- - -## Caché y almacenamiento de artwork - -- **Caché metadata**: LRU en memoria o Redis con TTL (por ejemplo 24h) para evitar sobrecargar APIs. -- **Almacenamiento de imágenes**: descargar y optimizar con `sharp` (crear versiones: thumb, medium), almacenar en `storage/artwork/{gameId}/cover.jpg` o S3. -- **Servicio proxy**: servir imágenes desde backend para no exponer keys ni URLs externas. - ---- - -## Manejo de errores y resiliencia - -- Implementar **retries** exponenciales con jitter (3 intentos). -- Implementar **circuit breaker** para desconectar llamadas a un proveedor fuera de servicio por N minutos. -- Limitar concurrencia por proveedor (p. ej. 5 llamadas simultáneas) y usar colas para trabajos masivos (enriquecimiento masivo). - ---- - -## Variables de entorno (ejemplos) - -``` -IGDB_CLIENT_ID=... -IGDB_CLIENT_SECRET=... -RAWG_API_KEY=... -THEGAMESDB_API_KEY=... -EXTERNAL_API_CONCURRENCY=5 -``` - -> Nota: **Nunca** exponer estas claves en el cliente; siempre pasar por el backend. - ---- - -## Fuentes - -- IGDB API docs, RAWG API docs, TheGamesDB API docs. -- Patrones: retries, circuit breakers (ej. libraries: `p-retry`, `cockatiel`). - ---- - -**Metadatos** -Autor: Quasar (investigación automatizada) -Última actualización: 2026-02-07 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md index 03265cd..d2e7761 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,49 +1,73 @@ -# Frontend - Quasar Game Library +# React + TypeScript + Vite -React + Vite + React Query UI for Quasar. +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -## Setup +Currently, two official plugins are available: -```bash -cd frontend -yarn install -yarn dev # Start Vite dev server on http://localhost:5173 +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) ``` -## Testing +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: -```bash -yarn test # Run Vitest in watch mode -yarn test:run # Run once +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) ``` - -## Building - -```bash -yarn build # Build for production (dist/) -yarn preview # Preview production build locally -``` - -## Structure - -``` -src/ -├── routes/ # Page components -├── components/ # Reusable components -├── hooks/ # TanStack Query hooks -├── lib/ # API client, utilities -├── types/ # TypeScript types -└── styles.css # Tailwind CSS -``` - -## API Integration - -Backend API runs on `http://localhost:3000`. - -Frontend proxies requests via `.env`: - -``` -VITE_API_URL=http://localhost:3000 -``` - -All API calls go through `src/lib/api.ts`. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html index 24f4faf..072a57e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,33 +1,10 @@ - - - - - Quasar - - -
- - - - - - - - - Quasar - - -
- - - - - + + - Quasar + frontend
diff --git a/frontend/package.json b/frontend/package.json index d8fbb6d..a62a7dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,38 +1,54 @@ { - "name": "quasar-frontend", - "version": "0.0.0", + "name": "frontend", "private": true, - "packageManager": "yarn@4.12.0", + "version": "0.0.0", + "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "test": "vitest", - "test:run": "vitest run", - "lint": "echo \"No lint configured\"", - "format": "prettier --write ." + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" }, "dependencies": { - "@hookform/resolvers": "^3.3.0", - "@tanstack/react-query": "^4.34.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.48.0", - "zod": "^3.22.0" + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@tanstack/react-form": "^1.28.3", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-router": "^1.162.2", + "axios": "^1.13.5", + "date-fns": "^4.1.0", + "lucide-react": "^0.575.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "zod": "^4.3.6" }, "devDependencies": { - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.5.0", - "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.0", - "autoprefixer": "^10.4.14", - "jsdom": "^22.1.0", - "postcss": "^8.4.24", - "tailwindcss": "^3.4.7", - "typescript": "^5.2.2", - "vite": "^5.1.0", - "vitest": "^0.34.1" + "@eslint/js": "^9.39.1", + "@radix-ui/react-slot": "^1.2.4", + "@tailwindcss/postcss": "^4.2.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.0", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" } } diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs deleted file mode 100644 index c2d4cca..0000000 --- a/frontend/postcss.config.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..51a6e4e --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3ce81c5..846fe95 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,13 +1,38 @@ -import React from 'react'; -import Navbar from './components/layout/Navbar'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; -export default function App(): JSX.Element { +function App() { return ( -
- -
-

Quasar

-
+
+
+ + + Quasar - Biblioteca de Videojuegos + + Aplicación para gestionar tu colección personal de videojuegos + + + +
+ + + +
+
+ +
+
+ Etiqueta + Secundaria + Outline +
+
+
+
); } + +export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..1cca928 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,165 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +// Función para unir clases de Tailwind +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// Configuración base de la API +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; + +// Interceptor para manejar errores comunes +async function handleApiError(response: Response) { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + + // Manejar errores de autenticación + if (response.status === 401) { + throw new Error('No autorizado. Por favor inicia sesión.'); + } + + // Manejar errores de validación + if (response.status === 422) { + const fieldErrors = errorData.errors || {}; + const errorMessage = Object.entries(fieldErrors) + .map(([field, errors]) => `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}`) + .join('; '); + throw new Error(errorMessage || 'Error de validación'); + } + + // Manejar errores de servidor + if (response.status >= 500) { + throw new Error('Error del servidor. Por favor intenta de nuevo más tarde.'); + } + + // Manejar otros errores + throw new Error(errorData.message || 'Error en la solicitud'); + } + + return response; +} + +// Función genérica para peticiones GET +export async function apiGet(endpoint: string, params?: Record): Promise { + const url = new URL(`${API_BASE_URL}${endpoint}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)); + } + }); + } + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + await handleApiError(response); + return response.json(); +} + +// Función genérica para peticiones POST +export async function apiPost(endpoint: string, data: any): Promise { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + await handleApiError(response); + return response.json(); +} + +// Función genérica para peticiones PUT +export async function apiPut(endpoint: string, data: any): Promise { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + await handleApiError(response); + return response.json(); +} + +// Función genérica para peticiones DELETE +export async function apiDelete(endpoint: string): Promise { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + + await handleApiError(response); + return response.json(); +} + +// Función para subir archivos +export async function apiUpload( + endpoint: string, + file: File, + additionalData?: Record +): Promise { + const formData = new FormData(); + formData.append('file', file); + + if (additionalData) { + Object.entries(additionalData).forEach(([key, value]) => { + formData.append(key, String(value)); + }); + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'POST', + body: formData, + }); + + await handleApiError(response); + return response.json(); +} + +// Función para peticiones con paginación +export async function apiGetPaginated( + endpoint: string, + page: number = 1, + limit: number = 10, + filters?: Record +): Promise<{ + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +}> { + const params = { + page, + limit, + ...filters, + }; + + return apiGet(endpoint, params); +} + +// Función para buscar +export async function apiSearch( + endpoint: string, + query: string, + filters?: Record +): Promise { + return apiGet(endpoint, { + search: query, + ...filters, + }); +} diff --git a/frontend/src/api/games.ts b/frontend/src/api/games.ts new file mode 100644 index 0000000..a1ead29 --- /dev/null +++ b/frontend/src/api/games.ts @@ -0,0 +1,80 @@ +import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; +import type { Game, GameFilters, GameFormData, PaginatedResponse } from '@/types'; + +// Obtener todos los juegos con paginación +export const getGames = async (page: number = 1, limit: number = 10, filters?: GameFilters) => { + return apiGetPaginated('/api/games', page, limit, filters); +}; + +// Obtener un juego por ID +export const getGameById = async (id: number) => { + return apiGet(`/api/games/${id}`); +}; + +// Crear un nuevo juego +export const createGame = async (data: GameFormData) => { + return apiPost('/api/games', data); +}; + +// Actualizar un juego existente +export const updateGame = async (id: number, data: GameFormData) => { + return apiPut(`/api/games/${id}`, data); +}; + +// Eliminar un juego +export const deleteGame = async (id: number) => { + return apiDelete(`/api/games/${id}`); +}; + +// Buscar juegos +export const searchGames = async (query: string, filters?: GameFilters) => { + return apiSearch('/api/games', query, filters); +}; + +// Obtener juegos por plataforma +export const getGamesByPlatform = async ( + platformId: number, + page: number = 1, + limit: number = 10 +) => { + return apiGetPaginated(`/api/games/platform/${platformId}`, page, limit); +}; + +// Obtener juegos por etiqueta +export const getGamesByTag = async (tagId: number, page: number = 1, limit: number = 10) => { + return apiGetPaginated(`/api/games/tag/${tagId}`, page, limit); +}; + +// Obtener juegos sin ROM +export const getGamesWithoutRom = async (page: number = 1, limit: number = 10) => { + return apiGetPaginated('/api/games/without-rom', page, limit); +}; + +// Obtener juegos metadata pendiente +export const getGamesWithPendingMetadata = async (page: number = 1, limit: number = 10) => { + return apiGetPaginated('/api/games/pending-metadata', page, limit); +}; + +// Actualizar metadata de un juego +export const updateGameMetadata = async (id: number, metadata: any) => { + return apiPut(`/api/games/${id}/metadata`, metadata); +}; + +// Enriquecer metadata de un juego +export const enrichGameMetadata = async (id: number, source: 'igdb' | 'rawg' | 'thegamesdb') => { + return apiPost(`/api/games/${id}/enrich`, { source }); +}; + +// Obtener estadísticas de juegos +export const getGameStats = async () => { + return apiGet<{ + totalGames: number; + gamesWithRom: number; + gamesWithoutRom: number; + gamesWithMetadata: number; + gamesWithoutMetadata: number; + totalPlatforms: number; + totalTags: number; + averageRating: number; + }>('/api/games/stats'); +}; diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts new file mode 100644 index 0000000..b62c0b5 --- /dev/null +++ b/frontend/src/api/import.ts @@ -0,0 +1,76 @@ +import { apiGet, apiPost, apiPut, apiDelete, apiUpload } from './client'; +import type { ImportRomFormData } from '@/types'; + +// Iniciar escaneo de directorio +export const startDirectoryScan = async (directoryPath: string) => { + return apiPost('/api/import/scan', { directoryPath }); +}; + +// Obtener estado del escaneo +export const getScanStatus = async (scanId: string) => { + return apiGet(`/api/import/scan/${scanId}/status`); +}; + +// Obtener lista de ROMs detectadas +export const getDetectedRoms = async (scanId: string) => { + return apiGet(`/api/import/scan/${scanId}/roms`); +}; + +// Procesar ROMs detectadas +export const processDetectedRoms = async (scanId: string, romIds: number[]) => { + return apiPost(`/api/import/scan/${scanId}/process`, { romIds }); +}; + +// Importar ROM manualmente +export const importRom = async (data: ImportRomFormData & { file: File }) => { + const { file, ...rest } = data; + return apiUpload('/api/import/rom', file, rest); +}; + +// Obtener historial de importaciones +export const getImportHistory = async (page: number = 1, limit: number = 10) => { + return apiGet(`/api/import/history?page=${page}&limit=${limit}`); +}; + +// Obtener estadísticas de importación +export const getImportStats = async () => { + return apiGet<{ + totalImports: number; + successfulImports: number; + failedImports: number; + totalRomsScanned: number; + totalRomsImported: number; + averageProcessingTime: number; + lastImportDate?: string; + }>('/api/import/stats'); +}; + +// Cancelar escaneo en progreso +export const cancelScan = async (scanId: string) => { + return apiDelete(`/api/import/scan/${scanId}`); +}; + +// Reintentar importación fallida +export const retryImport = async (importId: number) => { + return apiPost(`/api/import/retry/${importId}`); +}; + +// Eliminar registro de importación +export const deleteImportRecord = async (importId: number) => { + return apiDelete(`/api/import/history/${importId}`); +}; + +// Obtener información de archivo ROM +export const getRomInfo = async (romPath: string) => { + return apiGet(`/api/import/rom-info?path=${encodeURIComponent(romPath)}`); +}; + +// Verificar integridad de ROM +export const verifyRomIntegrity = async (romPath: string, romType: string) => { + return apiPost('/api/import/verify-rom', { romPath, romType }); +}; + +// Obtener directorios disponibles para escaneo +export const getAvailableDirectories = async () => { + return apiGet('/api/import/directories'); +}; diff --git a/frontend/src/api/platforms.ts b/frontend/src/api/platforms.ts new file mode 100644 index 0000000..044b790 --- /dev/null +++ b/frontend/src/api/platforms.ts @@ -0,0 +1,52 @@ +import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; +import type { Platform, PlatformFilters, PlatformFormData } from '@/types'; + +// Obtener todas las plataformas con paginación +export const getPlatforms = async ( + page: number = 1, + limit: number = 10, + filters?: PlatformFilters +) => { + return apiGetPaginated('/api/platforms', page, limit, filters); +}; + +// Obtener todas las plataformas (sin paginación para selectores) +export const getAllPlatforms = async () => { + return apiGet('/api/platforms/all'); +}; + +// Obtener una plataforma por ID +export const getPlatformById = async (id: number) => { + return apiGet(`/api/platforms/${id}`); +}; + +// Crear una nueva plataforma +export const createPlatform = async (data: PlatformFormData) => { + return apiPost('/api/platforms', data); +}; + +// Actualizar una plataforma existente +export const updatePlatform = async (id: number, data: PlatformFormData) => { + return apiPut(`/api/platforms/${id}`, data); +}; + +// Eliminar una plataforma +export const deletePlatform = async (id: number) => { + return apiDelete(`/api/platforms/${id}`); +}; + +// Buscar plataformas +export const searchPlatforms = async (query: string, filters?: PlatformFilters) => { + return apiSearch('/api/platforms', query, filters); +}; + +// Obtener estadísticas de plataformas +export const getPlatformStats = async () => { + return apiGet<{ + totalPlatforms: number; + totalGames: number; + averageGamesPerPlatform: number; + mostPopularPlatform: string; + leastPopularPlatform: string; + }>('/api/platforms/stats'); +}; diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts new file mode 100644 index 0000000..6bdb25d --- /dev/null +++ b/frontend/src/api/settings.ts @@ -0,0 +1,107 @@ +import { apiGet, apiPost, apiPut } from './client'; +import type { SettingsFormData } from '@/types'; + +// Obtener configuración actual +export const getSettings = async () => { + return apiGet('/api/settings'); +}; + +// Actualizar configuración +export const updateSettings = async (data: SettingsFormData) => { + return apiPut('/api/settings', data); +}; + +// Probar conexión con IGDB +export const testIgdbConnection = async (apiKey: string) => { + return apiPost('/api/settings/test-igdb', { apiKey }); +}; + +// Probar conexión con RAWG +export const testRawgConnection = async (apiKey: string) => { + return apiPost('/api/settings/test-rawg', { apiKey }); +}; + +// Probar conexión con TheGamesDB +export const testThegamesdbConnection = async (apiKey: string) => { + return apiPost('/api/settings/test-thegamesdb', { apiKey }); +}; + +// Obtener estado de servicios externos +export const getExternalServicesStatus = async () => { + return apiGet<{ + igdb: { connected: boolean; lastChecked?: string }; + rawg: { connected: boolean; lastChecked?: string }; + thegamesdb: { connected: boolean; lastChecked?: string }; + }>('/api/settings/services-status'); +}; + +// Obtener configuración de importación automática +export const getAutoImportConfig = async () => { + return apiGet('/api/settings/auto-import'); +}; + +// Actualizar configuración de importación automática +export const updateAutoImportConfig = async (enabled: boolean, directory?: string) => { + return apiPut('/api/settings/auto-import', { enabled, directory }); +}; + +// Obtener configuración de exportación +export const getExportConfig = async () => { + return apiGet('/api/settings/export'); +}; + +// Actualizar configuración de exportación +export const updateExportConfig = async (format: 'csv' | 'json', fields: string[]) => { + return apiPut('/api/settings/export', { format, fields }); +}; + +// Exportar datos +export const exportData = async (format: 'csv' | 'json', filters?: Record) => { + const params = new URLSearchParams(); + params.append('format', format); + + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + } + + const response = await fetch(`/api/settings/export?${params.toString()}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error('Error al exportar datos'); + } + + return response.blob(); +}; + +// Obtener estadísticas del sistema +export const getSystemStats = async () => { + return apiGet<{ + totalGames: number; + totalPlatforms: number; + totalTags: number; + totalRoms: number; + totalSize: number; + averageRating: number; + recentActivity: Array<{ + type: string; + message: string; + timestamp: string; + }>; + }>('/api/settings/system-stats'); +}; + +// Limpiar caché +export const clearCache = async () => { + return apiPost('/api/settings/clear-cache'); +}; + +// Obtener versión del sistema +export const getSystemVersion = async () => { + return apiGet('/api/settings/version'); +}; diff --git a/frontend/src/api/tags.ts b/frontend/src/api/tags.ts new file mode 100644 index 0000000..2d2ecbd --- /dev/null +++ b/frontend/src/api/tags.ts @@ -0,0 +1,68 @@ +import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; +import type { Tag, TagFilters, TagFormData } from '@/types'; + +// Obtener todas las etiquetas con paginación +export const getTags = async (page: number = 1, limit: number = 10, filters?: TagFilters) => { + return apiGetPaginated('/api/tags', page, limit, filters); +}; + +// Obtener todas las etiquetas (sin paginación para selectores) +export const getAllTags = async () => { + return apiGet('/api/tags/all'); +}; + +// Obtener una etiqueta por ID +export const getTagById = async (id: number) => { + return apiGet(`/api/tags/${id}`); +}; + +// Crear una nueva etiqueta +export const createTag = async (data: TagFormData) => { + return apiPost('/api/tags', data); +}; + +// Actualizar una etiqueta existente +export const updateTag = async (id: number, data: TagFormData) => { + return apiPut(`/api/tags/${id}`, data); +}; + +// Eliminar una etiqueta +export const deleteTag = async (id: number) => { + return apiDelete(`/api/tags/${id}`); +}; + +// Buscar etiquetas +export const searchTags = async (query: string, filters?: TagFilters) => { + return apiSearch('/api/tags', query, filters); +}; + +// Obtener etiquetas más usadas +export const getPopularTags = async (limit: number = 10) => { + return apiGet(`/api/tags/popular?limit=${limit}`); +}; + +// Obtener etiquetas por juego +export const getTagsByGame = async (gameId: number) => { + return apiGet(`/api/tags/game/${gameId}`); +}; + +// Asignar etiquetas a un juego +export const assignTagsToGame = async (gameId: number, tagIds: number[]) => { + return apiPost(`/api/games/${gameId}/tags`, { tagIds }); +}; + +// Eliminar etiquetas de un juego +export const removeTagsFromGame = async (gameId: number, tagIds: number[]) => { + return apiPost(`/api/games/${gameId}/tags/remove`, { tagIds }); +}; + +// Obtener estadísticas de etiquetas +export const getTagStats = async () => { + return apiGet<{ + totalTags: number; + totalTaggedGames: number; + averageTagsPerGame: number; + mostUsedTag: string; + leastUsedTag: string; + }>('/api/tags/stats'); +}; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/games/GameCard.tsx b/frontend/src/components/games/GameCard.tsx deleted file mode 100644 index 0f8ad11..0000000 --- a/frontend/src/components/games/GameCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Game } from '../../types/game'; - -interface GameCardProps { - game: Game; - onEdit?: (game: Game) => void; - onDelete?: (id: string) => void; -} - -export default function GameCard({ game, onEdit, onDelete }: GameCardProps): JSX.Element { - return ( -
-

{game.title}

-

{game.slug}

- {game.description &&

{game.description}

} -

- Added: {new Date(game.createdAt).toLocaleDateString()} -

-
- {onEdit && ( - - )} - {onDelete && ( - - )} -
-
- ); -} diff --git a/frontend/src/components/games/GameForm.tsx b/frontend/src/components/games/GameForm.tsx deleted file mode 100644 index a89ee22..0000000 --- a/frontend/src/components/games/GameForm.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { Game, CreateGameInput } from '../../types/game'; - -const gameFormSchema = z.object({ - title: z.string().min(1, 'Title is required'), - platformId: z.string().min(1, 'Platform is required'), - description: z.string().optional().nullable(), - priceCents: z.number().optional(), - currency: z.string().optional().default('USD'), - store: z.string().optional(), - date: z.string().optional(), - condition: z.enum(['Loose', 'CIB', 'New']).optional(), - notes: z.string().optional().nullable(), -}); - -type GameFormData = z.infer; - -interface GameFormProps { - initialData?: Game; - onSubmit: (data: CreateGameInput | Game) => void | Promise; - isLoading?: boolean; -} - -export default function GameForm({ - initialData, - onSubmit, - isLoading = false, -}: GameFormProps): JSX.Element { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(gameFormSchema), - defaultValues: initialData - ? { - title: initialData.title, - description: initialData.description, - priceCents: undefined, - currency: 'USD', - store: undefined, - date: undefined, - condition: undefined, - notes: undefined, - } - : undefined, - }); - - const onFormSubmit = (data: GameFormData) => { - onSubmit(data as CreateGameInput); - }; - - return ( -
-
- - - {errors.title &&

{errors.title.message}

} -
- -
- - - {errors.platformId &&

{errors.platformId.message}

} -
- -
- - -
- -
- -