804 lines
29 KiB
Markdown
804 lines
29 KiB
Markdown
# Arquitectura: Nueva Pantalla de Búsqueda de Juegos
|
|
|
|
## Resumen Ejecutivo
|
|
|
|
Este documento describe la arquitectura para reemplazar el popup actual ([`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx)) por una página completa de búsqueda de juegos que permite buscar en múltiples proveedores externos (IGDB, RAWG, TheGamesDB) y seleccionar un resultado para guardar en la base de datos.
|
|
|
|
## 1. Arquitectura del Backend
|
|
|
|
### 1.1 Modificaciones al Endpoint Existente
|
|
|
|
**Endpoint actual:** [`GET /api/metadata/search?q=query&platform=optional`](backend/src/routes/metadata.ts:22)
|
|
|
|
**Problema actual:**
|
|
|
|
- El endpoint usa [`enrichGame()`](backend/src/services/metadataService.ts:34) que solo devuelve el primer resultado encontrado
|
|
- La respuesta es un array con un solo elemento o vacío
|
|
|
|
**Solución propuesta:**
|
|
Crear una nueva función en [`metadataService.ts`](backend/src/services/metadataService.ts) que busque en TODOS los proveedores y devuelva múltiples resultados.
|
|
|
|
### 1.2 Nueva Función: `searchGames()`
|
|
|
|
**Ubicación:** [`backend/src/services/metadataService.ts`](backend/src/services/metadataService.ts)
|
|
|
|
```typescript
|
|
export async function searchGames(opts: {
|
|
title: string;
|
|
platform?: string;
|
|
year?: number;
|
|
}): Promise<EnrichedGame[]> {
|
|
// Buscar en IGDB, RAWG y TheGamesDB simultáneamente
|
|
// Devolver todos los resultados encontrados
|
|
// Aplicar deduplicación basada en nombre + plataforma + año
|
|
}
|
|
```
|
|
|
|
**Lógica de búsqueda:**
|
|
|
|
1. Ejecutar búsquedas en paralelo en los tres proveedores
|
|
2. Normalizar todos los resultados usando la función [`normalize()`](backend/src/services/metadataService.ts:12) existente
|
|
3. Aplicar deduplicación para evitar duplicados entre proveedores
|
|
4. Ordenar resultados por relevancia (coincidencia exacta primero)
|
|
5. Limitar resultados (ej: 20 resultados máximos por proveedor, 50 total)
|
|
|
|
**Deduplicación:**
|
|
|
|
- Agrupar por nombre normalizado (lowercase, sin caracteres especiales)
|
|
- Si hay duplicados, priorizar: IGDB > RAWG > TheGamesDB
|
|
- Mantener `externalIds` de todas las fuentes para cada resultado
|
|
|
|
### 1.3 Modificación del Endpoint
|
|
|
|
**Ubicación:** [`backend/src/routes/metadata.ts`](backend/src/routes/metadata.ts)
|
|
|
|
**Cambios:**
|
|
|
|
1. Actualizar el esquema de validación para incluir `year` como parámetro opcional
|
|
2. Llamar a `searchGames()` en lugar de `enrichGame()`
|
|
3. Devolver el array de resultados directamente
|
|
|
|
```typescript
|
|
const searchMetadataSchema = z.object({
|
|
q: z.string().min(1, 'El parámetro de búsqueda es requerido'),
|
|
platform: z.string().optional(),
|
|
year: z.coerce.number().int().min(1900).max(2100).optional(),
|
|
});
|
|
|
|
app.get('/metadata/search', async (request, reply) => {
|
|
const validated = searchMetadataSchema.parse(request.query);
|
|
const results = await metadataService.searchGames({
|
|
title: validated.q,
|
|
platform: validated.platform,
|
|
year: validated.year,
|
|
});
|
|
return reply.code(200).send(results);
|
|
});
|
|
```
|
|
|
|
### 1.4 Estructura de Respuesta de la API
|
|
|
|
```typescript
|
|
// GET /api/metadata/search?q=mario&year=1990&platform=NES
|
|
|
|
[
|
|
{
|
|
source: 'igdb',
|
|
externalIds: {
|
|
igdb: 1234,
|
|
rawg: 5678,
|
|
thegamesdb: 9012,
|
|
},
|
|
name: 'Super Mario Bros.',
|
|
title: 'Super Mario Bros.',
|
|
slug: 'super-mario-bros',
|
|
releaseDate: '1985-09-13T00:00:00.000Z',
|
|
genres: ['Platform'],
|
|
coverUrl: 'https://example.com/cover.jpg',
|
|
platforms: [
|
|
{
|
|
id: 18,
|
|
name: 'Nintendo Entertainment System (NES)',
|
|
abbreviation: 'NES',
|
|
slug: 'nes',
|
|
},
|
|
],
|
|
},
|
|
// ... más resultados
|
|
];
|
|
```
|
|
|
|
### 1.5 Nuevo Endpoint para Guardar Resultado
|
|
|
|
**Endpoint:** `POST /api/games/from-metadata`
|
|
|
|
**Propósito:** Crear un juego a partir de un resultado de búsqueda de metadatos.
|
|
|
|
**Body:**
|
|
|
|
```typescript
|
|
{
|
|
"metadata": {
|
|
"source": "igdb",
|
|
"externalIds": { "igdb": 1234 },
|
|
"name": "Super Mario Bros.",
|
|
"slug": "super-mario-bros",
|
|
"releaseDate": "1985-09-13T00:00:00.000Z",
|
|
"genres": ["Platform"],
|
|
"coverUrl": "https://example.com/cover.jpg"
|
|
},
|
|
"overrides": {
|
|
"platform": "NES",
|
|
"year": 1985,
|
|
"description": "Descripción personalizada..."
|
|
}
|
|
}
|
|
```
|
|
|
|
**Lógica:**
|
|
|
|
1. Mapear metadatos a estructura de [`Game`](backend/prisma/schema.prisma:12)
|
|
2. Aplicar overrides si se proporcionan
|
|
3. Guardar en base de datos usando [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73)
|
|
4. Devolver el juego creado
|
|
|
|
### 1.6 Diagrama de Flujo del Backend
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Cliente envía búsqueda] --> B[Validar parámetros]
|
|
B --> C[searchGames]
|
|
C --> D[Paralelo: IGDB]
|
|
C --> E[Paralelo: RAWG]
|
|
C --> F[Paralelo: TheGamesDB]
|
|
D --> G[Normalizar resultados]
|
|
E --> G
|
|
F --> G
|
|
G --> H[Deduplicar]
|
|
H --> I[Ordenar por relevancia]
|
|
I --> J[Limitar resultados]
|
|
J --> K[Devolver array]
|
|
K --> L[Cliente muestra resultados]
|
|
L --> M[Usuario selecciona resultado]
|
|
M --> N[POST /api/games/from-metadata]
|
|
N --> O[GamesController.createGame]
|
|
O --> P[Guardar en BD]
|
|
P --> Q[Devolver juego creado]
|
|
```
|
|
|
|
## 2. Arquitectura del Frontend
|
|
|
|
### 2.1 Nueva Ruta
|
|
|
|
**Ruta:** `/games/add` o `/games/search`
|
|
|
|
**Archivo:** [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx)
|
|
|
|
**Razón:** `/games/add` es más semántico y sigue el patrón RESTful para crear recursos.
|
|
|
|
### 2.2 Componentes a Crear
|
|
|
|
#### 2.2.1 SearchForm Component
|
|
|
|
**Ubicación:** [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx)
|
|
|
|
**Responsabilidades:**
|
|
|
|
- Formulario de búsqueda con campos: título (obligatorio), año (opcional), plataforma (opcional)
|
|
- Validación de formulario
|
|
- Disparar evento de búsqueda
|
|
|
|
**Componentes shadcn/ui a usar:**
|
|
|
|
- [`Input`](frontend/src/components/ui/input.tsx) para título y año
|
|
- [`Select`](frontend/src/components/ui/select.tsx) para plataforma
|
|
- [`Button`](frontend/src/components/ui/button.tsx) para submit
|
|
|
|
**Props:**
|
|
|
|
```typescript
|
|
interface SearchFormProps {
|
|
onSearch: (params: SearchParams) => void;
|
|
isLoading: boolean;
|
|
}
|
|
```
|
|
|
|
#### 2.2.2 SearchResults Component
|
|
|
|
**Ubicación:** [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx)
|
|
|
|
**Responsabilidades:**
|
|
|
|
- Mostrar lista de resultados de búsqueda
|
|
- Permitir selección de un resultado
|
|
- Mostrar estado de carga
|
|
|
|
**Componentes shadcn/ui a usar:**
|
|
|
|
- [`Card`](frontend/src/components/ui/card.tsx) para cada resultado
|
|
- [`Button`](frontend/src/components/ui/button.tsx) para seleccionar
|
|
|
|
**Props:**
|
|
|
|
```typescript
|
|
interface SearchResultsProps {
|
|
results: MetadataGame[];
|
|
onSelect: (result: MetadataGame) => void;
|
|
isLoading: boolean;
|
|
}
|
|
```
|
|
|
|
#### 2.2.3 GamePreviewDialog Component
|
|
|
|
**Ubicación:** [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx)
|
|
|
|
**Responsabilidades:**
|
|
|
|
- Mostrar detalles completos del resultado seleccionado
|
|
- Permitir editar campos antes de guardar
|
|
- Confirmar guardado
|
|
|
|
**Componentes shadcn/ui a usar:**
|
|
|
|
- [`Dialog`](frontend/src/components/ui/dialog.tsx) para el modal
|
|
- [`Input`](frontend/src/components/ui/input.tsx) para campos editables
|
|
- [`Textarea`](frontend/src/components/ui/textarea.tsx) para descripción
|
|
- [`Button`](frontend/src/components/ui/button.tsx) para acciones
|
|
|
|
**Props:**
|
|
|
|
```typescript
|
|
interface GamePreviewDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
metadata: MetadataGame | null;
|
|
onSave: (metadata: MetadataGame, overrides: GameOverrides) => void;
|
|
isSaving: boolean;
|
|
}
|
|
```
|
|
|
|
### 2.3 Flujo de Usuario
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Usuario accede a /games/add] --> B[SearchForm visible]
|
|
B --> C[Usuario ingresa título]
|
|
C --> D[Usuario hace clic en Buscar]
|
|
D --> E[LLAMADA API: GET /api/metadata/search]
|
|
E --> F[SearchResults muestra resultados]
|
|
F --> G[Usuario ve lista de juegos]
|
|
G --> H[Usuario hace clic en un resultado]
|
|
H --> I[GamePreviewDialog se abre]
|
|
I --> J[Usuario puede editar campos]
|
|
J --> K[Usuario hace clic en Guardar]
|
|
K --> L[LLAMADA API: POST /api/games/from-metadata]
|
|
L --> M{Éxito?}
|
|
M -->|Sí| N[Redirigir a /games o /games/id]
|
|
M -->|No| O[Mostrar error en dialog]
|
|
O --> J
|
|
```
|
|
|
|
### 2.4 Estructura de la Página
|
|
|
|
```typescript
|
|
// frontend/src/app/games/add/page.tsx
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { SearchForm } from '@/components/games/SearchForm';
|
|
import { SearchResults } from '@/components/games/SearchResults';
|
|
import { GamePreviewDialog } from '@/components/games/GamePreviewDialog';
|
|
import { metadataApi, gamesApi } from '@/lib/api';
|
|
|
|
export default function AddGamePage() {
|
|
const router = useRouter();
|
|
const [searchParams, setSearchParams] = useState<SearchParams | null>(null);
|
|
const [results, setResults] = useState<MetadataGame[]>([]);
|
|
const [selectedResult, setSelectedResult] = useState<MetadataGame | null>(null);
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
const handleSearch = async (params: SearchParams) => {
|
|
setIsSearching(true);
|
|
try {
|
|
const data = await metadataApi.search(params);
|
|
setResults(data);
|
|
setSearchParams(params);
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
};
|
|
|
|
const handleSelect = (result: MetadataGame) => {
|
|
setSelectedResult(result);
|
|
};
|
|
|
|
const handleSave = async (metadata: MetadataGame, overrides: GameOverrides) => {
|
|
setIsSaving(true);
|
|
try {
|
|
const game = await gamesApi.createFromMetadata(metadata, overrides);
|
|
router.push(`/games/${game.id}`);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8">
|
|
<h1 className="text-3xl font-bold mb-8">Añadir Juego</h1>
|
|
|
|
<SearchForm onSearch={handleSearch} isLoading={isSearching} />
|
|
|
|
{results.length > 0 && (
|
|
<SearchResults
|
|
results={results}
|
|
onSelect={handleSelect}
|
|
isLoading={isSearching}
|
|
/>
|
|
)}
|
|
|
|
<GamePreviewDialog
|
|
open={!!selectedResult}
|
|
onOpenChange={(open) => !open && setSelectedResult(null)}
|
|
metadata={selectedResult}
|
|
onSave={handleSave}
|
|
isSaving={isSaving}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 2.5 Actualización del Cliente API
|
|
|
|
**Ubicación:** [`frontend/src/lib/api.ts`](frontend/src/lib/api.ts)
|
|
|
|
**Nuevas funciones:**
|
|
|
|
```typescript
|
|
// Búsqueda de metadatos
|
|
export async function searchMetadata(params: {
|
|
q: string;
|
|
platform?: string;
|
|
year?: number;
|
|
}): Promise<MetadataGame[]> {
|
|
const queryParams = new URLSearchParams({ q: params.q });
|
|
if (params.platform) queryParams.append('platform', params.platform);
|
|
if (params.year) queryParams.append('year', String(params.year));
|
|
|
|
const res = await fetch(`${API_BASE}/metadata/search?${queryParams}`);
|
|
if (!res.ok) throw new Error('Error searching metadata');
|
|
return res.json();
|
|
}
|
|
|
|
// Crear juego desde metadatos
|
|
export async function createGameFromMetadata(
|
|
metadata: MetadataGame,
|
|
overrides?: GameOverrides
|
|
): Promise<Game> {
|
|
const res = await fetch(`${API_BASE}/games/from-metadata`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ metadata, overrides }),
|
|
});
|
|
if (!res.ok) throw new Error('Error creating game from metadata');
|
|
return res.json();
|
|
}
|
|
```
|
|
|
|
### 2.6 Integración con Página de Juegos
|
|
|
|
**Ubicación:** [`frontend/src/app/games/page.tsx`](frontend/src/app/games/page.tsx)
|
|
|
|
**Cambio:**
|
|
|
|
- Reemplazar el botón "Nuevo Juego" que abre [`GameDialog`](frontend/src/components/games/GameDialog.tsx) por un enlace a `/games/add`
|
|
- Mantener [`GameDialog`](frontend/src/components/games/GameDialog.tsx) solo para edición de juegos existentes
|
|
|
|
```typescript
|
|
// Antes
|
|
<Button onClick={handleCreate}>
|
|
<PlusIcon className="w-4 h-4 mr-2" />
|
|
Nuevo Juego
|
|
</Button>
|
|
|
|
// Después
|
|
<Link href="/games/add">
|
|
<Button>
|
|
<PlusIcon className="w-4 h-4 mr-2" />
|
|
Añadir Juego
|
|
</Button>
|
|
</Link>
|
|
```
|
|
|
|
## 3. Estrategia TDD (Test Driven Development)
|
|
|
|
### 3.1 Orden de Implementación
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Tests Backend - searchGames] --> B[Implementar searchGames]
|
|
B --> C[Tests Backend - Endpoint /metadata/search]
|
|
C --> D[Implementar Endpoint]
|
|
D --> E[Tests Backend - POST /api/games/from-metadata]
|
|
E --> F[Implementar Endpoint from-metadata]
|
|
F --> G[Tests Frontend - Componentes Unitarios]
|
|
G --> H[Implementar Componentes]
|
|
H --> I[Tests E2E - Flujo Completo]
|
|
I --> J[Validar Integración]
|
|
```
|
|
|
|
### 3.2 Tests del Backend
|
|
|
|
#### 3.2.1 Tests de `searchGames()`
|
|
|
|
**Archivo:** [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe devolver resultados de IGDB cuando hay coincidencias
|
|
2. ✅ Debe devolver resultados de RAWG cuando IGDB falla
|
|
3. ✅ Debe devolver resultados de TheGamesDB cuando IGDB y RAWG fallan
|
|
4. ✅ Debe deduplicar resultados entre proveedores
|
|
5. ✅ Debe priorizar IGDB sobre RAWG y TheGamesDB
|
|
6. ✅ Debe filtrar por plataforma cuando se proporciona
|
|
7. ✅ Debe filtrar por año cuando se proporciona
|
|
8. ✅ Debe devolver array vacío cuando no hay resultados
|
|
9. ✅ Debe manejar errores de API externas gracefully
|
|
10. ✅ Debe limitar número de resultados
|
|
|
|
**Ejemplo de test:**
|
|
|
|
```typescript
|
|
it('debe deduplicar resultados entre proveedores', async () => {
|
|
const mockIgdbResult = {
|
|
name: 'Super Mario Bros.',
|
|
source: 'igdb',
|
|
id: 1234,
|
|
// ... otros campos
|
|
};
|
|
|
|
const mockRawgResult = {
|
|
name: 'Super Mario Bros.',
|
|
source: 'rawg',
|
|
id: 5678,
|
|
// ... otros campos
|
|
};
|
|
|
|
vi.spyOn(igdb, 'searchGames').mockResolvedValue([mockIgdbResult]);
|
|
vi.spyOn(rawg, 'searchGames').mockResolvedValue([mockRawgResult]);
|
|
|
|
const results = await metadataService.searchGames({ title: 'mario' });
|
|
|
|
// Solo debería haber un resultado con ambos IDs externos
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].externalIds.igdb).toBe(1234);
|
|
expect(results[0].externalIds.rawg).toBe(5678);
|
|
});
|
|
```
|
|
|
|
#### 3.2.2 Tests del Endpoint `/api/metadata/search`
|
|
|
|
**Archivo:** [`backend/tests/routes/metadata.search.spec.ts`](backend/tests/routes/metadata.search.spec.ts)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe devolver 200 con array de resultados
|
|
2. ✅ Debe devolver 400 si falta parámetro `q`
|
|
3. ✅ Debe devolver 400 si `year` no es válido
|
|
4. ✅ Debe pasar parámetros a `searchGames()`
|
|
5. ✅ Debe manejar errores del servicio
|
|
|
|
#### 3.2.3 Tests del Endpoint `POST /api/games/from-metadata`
|
|
|
|
**Archivo:** [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe crear juego con metadatos de IGDB
|
|
2. ✅ Debe crear juego con metadatos de RAWG
|
|
3. ✅ Debe crear juego con metadatos de TheGamesDB
|
|
4. ✅ Debe aplicar overrides cuando se proporcionan
|
|
5. ✅ Debe guardar `externalIds` correctamente
|
|
6. ✅ Debe devolver 400 si metadata es inválida
|
|
7. ✅ Debe devolver 201 con juego creado
|
|
|
|
### 3.3 Tests del Frontend
|
|
|
|
#### 3.3.1 Tests Unitarios de Componentes
|
|
|
|
**Archivo:** [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe mostrar campos de formulario
|
|
2. ✅ Debe validar que título es obligatorio
|
|
3. ✅ Debe llamar `onSearch` con parámetros correctos
|
|
4. ✅ Debe deshabilitar botón cuando `isLoading` es true
|
|
|
|
**Archivo:** [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe mostrar lista de resultados
|
|
2. ✅ Debe mostrar mensaje cuando no hay resultados
|
|
3. ✅ Debe llamar `onSelect` cuando se hace clic en resultado
|
|
4. ✅ Debe mostrar estado de carga
|
|
|
|
**Archivo:** [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Debe mostrar metadatos del juego
|
|
2. ✅ Debe permitir editar campos
|
|
3. ✅ Debe llamar `onSave` con metadatos y overrides
|
|
4. ✅ Debe cerrar cuando se cancela
|
|
|
|
#### 3.3.2 Tests E2E
|
|
|
|
**Archivo:** [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts)
|
|
|
|
**Casos de prueba:**
|
|
|
|
1. ✅ Flujo completo: búsqueda → selección → guardado
|
|
2. ✅ Búsqueda sin resultados
|
|
3. ✅ Cancelar selección
|
|
4. ✅ Editar campos antes de guardar
|
|
5. ✅ Validación de errores
|
|
|
|
**Ejemplo de test E2E:**
|
|
|
|
```typescript
|
|
test('flujo completo de búsqueda y guardado', async ({ page }) => {
|
|
await page.goto('/games/add');
|
|
|
|
// Ingresar búsqueda
|
|
await page.fill('input[name="title"]', 'Super Mario');
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Esperar resultados
|
|
await page.waitForSelector('[data-testid="search-results"]');
|
|
const results = await page.locator('[data-testid="game-result"]').count();
|
|
expect(results).toBeGreaterThan(0);
|
|
|
|
// Seleccionar primer resultado
|
|
await page.click('[data-testid="game-result"]:first-child');
|
|
|
|
// Esperar dialog de preview
|
|
await page.waitForSelector('[data-testid="preview-dialog"]');
|
|
|
|
// Guardar
|
|
await page.click('[data-testid="save-button"]');
|
|
|
|
// Verificar redirección
|
|
await page.waitForURL(/\/games\/[a-z0-9]+$/);
|
|
});
|
|
```
|
|
|
|
### 3.4 Orden de Implementación Recomendado
|
|
|
|
1. **Sprint 1: Backend Core**
|
|
- Tests de `searchGames()`
|
|
- Implementar `searchGames()`
|
|
- Tests del endpoint `/api/metadata/search`
|
|
- Actualizar endpoint `/api/metadata/search`
|
|
|
|
2. **Sprint 2: Backend Save**
|
|
- Tests del endpoint `POST /api/games/from-metadata`
|
|
- Implementar endpoint `POST /api/games/from-metadata`
|
|
|
|
3. **Sprint 3: Frontend Components**
|
|
- Tests unitarios de `SearchForm`
|
|
- Implementar `SearchForm`
|
|
- Tests unitarios de `SearchResults`
|
|
- Implementar `SearchResults`
|
|
- Tests unitarios de `GamePreviewDialog`
|
|
- Implementar `GamePreviewDialog`
|
|
|
|
4. **Sprint 4: Frontend Page & Integration**
|
|
- Implementar página `/games/add`
|
|
- Actualizar cliente API
|
|
- Integrar con página `/games`
|
|
|
|
5. **Sprint 5: E2E & Polish**
|
|
- Tests E2E del flujo completo
|
|
- Corregir bugs encontrados
|
|
- Mejorar UX (loading states, error messages)
|
|
|
|
## 4. Integración con Código Existente
|
|
|
|
### 4.1 Código a Reutilizar
|
|
|
|
#### Backend
|
|
|
|
| Componente | Archivo | Uso |
|
|
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------- |
|
|
| [`normalize()`](backend/src/services/metadataService.ts:12) | [`metadataService.ts`](backend/src/services/metadataService.ts) | Normalizar resultados de búsqueda |
|
|
| [`igdb.searchGames()`](backend/src/services/igdbClient.ts:73) | [`igdbClient.ts`](backend/src/services/igdbClient.ts) | Buscar en IGDB |
|
|
| [`rawg.searchGames()`](backend/src/services/rawgClient.ts:13) | [`rawgClient.ts`](backend/src/services/rawgClient.ts) | Buscar en RAWG |
|
|
| [`thegamesdb.searchGames()`](backend/src/services/thegamesdbClient.ts:18) | [`thegamesdbClient.ts`](backend/src/services/thegamesdbClient.ts) | Buscar en TheGamesDB |
|
|
| [`GamesController.createGame()`](backend/src/controllers/gamesController.ts:73) | [`gamesController.ts`](backend/src/controllers/gamesController.ts) | Crear juego en BD |
|
|
| [`Game`](backend/prisma/schema.prisma:12) schema | [`schema.prisma`](backend/prisma/schema.prisma) | Modelo de datos |
|
|
|
|
#### Frontend
|
|
|
|
| Componente | Archivo | Uso |
|
|
| ------------------------------------------------- | ----------------------------------------------------- | ---------------------- |
|
|
| [`Button`](frontend/src/components/ui/button.tsx) | [`button.tsx`](frontend/src/components/ui/button.tsx) | Botones de acción |
|
|
| [`Input`](frontend/src/components/ui/input.tsx) | [`input.tsx`](frontend/src/components/ui/input.tsx) | Campos de formulario |
|
|
| [`Select`](frontend/src/components/ui/select.tsx) | [`select.tsx`](frontend/src/components/ui/select.tsx) | Selector de plataforma |
|
|
| [`Card`](frontend/src/components/ui/card.tsx) | [`card.tsx`](frontend/src/components/ui/card.tsx) | Tarjetas de resultados |
|
|
| [`Dialog`](frontend/src/components/ui/dialog.tsx) | [`dialog.tsx`](frontend/src/components/ui/dialog.tsx) | Dialog de preview |
|
|
| [`MetadataGame`](frontend/src/lib/api.ts:39) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de metadatos |
|
|
| [`Game`](frontend/src/lib/api.ts:57) type | [`api.ts`](frontend/src/lib/api.ts) | Tipo de juego |
|
|
|
|
### 4.2 Código a Modificar
|
|
|
|
#### Backend
|
|
|
|
| Archivo | Cambio |
|
|
| --------------------------------------------------------------- | ------------------------------------------------ |
|
|
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar función `searchGames()` |
|
|
| [`metadata.ts`](backend/src/routes/metadata.ts) | Actualizar endpoint `/api/metadata/search` |
|
|
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
|
|
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos para búsqueda múltiple |
|
|
|
|
#### Frontend
|
|
|
|
| Archivo | Cambio |
|
|
| ---------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
| [`api.ts`](frontend/src/lib/api.ts) | Agregar funciones `searchMetadata()` y `createGameFromMetadata()` |
|
|
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón "Nuevo Juego" a enlace `/games/add` |
|
|
| [`GameDialog.tsx`](frontend/src/components/games/GameDialog.tsx) | Mantener solo para edición (opcional) |
|
|
|
|
### 4.3 Código Nuevo a Crear
|
|
|
|
#### Backend
|
|
|
|
| Archivo | Propósito |
|
|
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
| [`backend/tests/services/metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | Tests de `searchGames()` |
|
|
| [`backend/tests/routes/games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | Tests del endpoint from-metadata |
|
|
|
|
#### Frontend
|
|
|
|
| Archivo | Propósito |
|
|
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
|
|
| [`frontend/src/app/games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | Página de búsqueda de juegos |
|
|
| [`frontend/src/components/games/SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | Formulario de búsqueda |
|
|
| [`frontend/src/components/games/SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | Lista de resultados |
|
|
| [`frontend/src/components/games/GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | Dialog de preview y edición |
|
|
| [`frontend/src/components/games/__tests__/SearchForm.test.tsx`](frontend/src/components/games/__tests__/SearchForm.test.tsx) | Tests de SearchForm |
|
|
| [`frontend/src/components/games/__tests__/SearchResults.test.tsx`](frontend/src/components/games/__tests__/SearchResults.test.tsx) | Tests de SearchResults |
|
|
| [`frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx`](frontend/src/components/games/__tests__/GamePreviewDialog.test.tsx) | Tests de GamePreviewDialog |
|
|
| [`tests/e2e/game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | Tests E2E del flujo |
|
|
|
|
## 5. Diagrama de Arquitectura Completa
|
|
|
|
```mermaid
|
|
graph TB
|
|
subgraph Frontend
|
|
A[Página /games/add] --> B[SearchForm]
|
|
A --> C[SearchResults]
|
|
A --> D[GamePreviewDialog]
|
|
B --> E[api.ts - searchMetadata]
|
|
C --> F[api.ts - createGameFromMetadata]
|
|
D --> F
|
|
end
|
|
|
|
subgraph Backend
|
|
E --> G[GET /api/metadata/search]
|
|
F --> H[POST /api/games/from-metadata]
|
|
G --> I[metadataService.searchGames]
|
|
I --> J[igdbClient.searchGames]
|
|
I --> K[rawgClient.searchGames]
|
|
I --> L[thegamesdbClient.searchGames]
|
|
H --> M[GamesController.createGame]
|
|
M --> N[(Prisma - Game)]
|
|
end
|
|
|
|
subgraph External APIs
|
|
J --> O[IGDB API]
|
|
K --> P[RAWG API]
|
|
L --> Q[TheGamesDB API]
|
|
end
|
|
|
|
style Frontend fill:#e1f5fe
|
|
style Backend fill:#f3e5f5
|
|
style External APIs fill:#fff3e0
|
|
```
|
|
|
|
## 6. Consideraciones Adicionales
|
|
|
|
### 6.1 Manejo de Errores
|
|
|
|
**Backend:**
|
|
|
|
- Capturar errores de APIs externas sin fallar completamente
|
|
- Devolver resultados parciales si algún proveedor falla
|
|
- Logging de errores para debugging
|
|
|
|
**Frontend:**
|
|
|
|
- Mostrar mensajes de error claros al usuario
|
|
- Permitir reintentar búsqueda
|
|
- Mostrar estado de carga durante búsquedas
|
|
|
|
### 6.2 Performance
|
|
|
|
**Backend:**
|
|
|
|
- Usar `Promise.all()` para búsquedas paralelas
|
|
- Implementar caching de resultados (opcional)
|
|
- Limitar número de resultados por proveedor
|
|
|
|
**Frontend:**
|
|
|
|
- Implementar debounce en búsqueda
|
|
- Lazy loading de imágenes de portada
|
|
- Paginación de resultados si hay muchos
|
|
|
|
### 6.3 Seguridad
|
|
|
|
**Backend:**
|
|
|
|
- Validar todos los parámetros de entrada
|
|
- Sanitizar datos antes de guardar en BD
|
|
- Rate limiting en endpoints de búsqueda
|
|
|
|
**Frontend:**
|
|
|
|
- Sanitizar HTML antes de mostrar
|
|
- Validar en cliente y servidor
|
|
|
|
### 6.4 Accesibilidad
|
|
|
|
**Frontend:**
|
|
|
|
- Labels en todos los campos de formulario
|
|
- Focus management en dialogs
|
|
- Keyboard navigation
|
|
- ARIA labels donde sea necesario
|
|
|
|
### 6.5 UX Considerations
|
|
|
|
- Mostrar loading states claros
|
|
- Feedback inmediato al usuario
|
|
- Permitir cancelar búsqueda en progreso
|
|
- Mostrar fuente de cada resultado (IGDB, RAWG, etc.)
|
|
- Permitir ver más detalles antes de seleccionar
|
|
- Confirmación antes de guardar
|
|
|
|
## 7. Resumen de Cambios
|
|
|
|
### Backend
|
|
|
|
| Archivo | Acción |
|
|
| ----------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
| [`metadataService.ts`](backend/src/services/metadataService.ts) | Agregar `searchGames()` |
|
|
| [`metadata.ts`](backend/src/routes/metadata.ts) | Modificar endpoint `/api/metadata/search` |
|
|
| [`games.ts`](backend/src/routes/games.ts) | Agregar endpoint `POST /api/games/from-metadata` |
|
|
| [`types/index.ts`](backend/src/types/index.ts) | Agregar tipos nuevos |
|
|
| [`metadataService.search.spec.ts`](backend/tests/services/metadataService.search.spec.ts) | **NUEVO** |
|
|
| [`games.from-metadata.spec.ts`](backend/tests/routes/games.from-metadata.spec.ts) | **NUEVO** |
|
|
|
|
### Frontend
|
|
|
|
| Archivo | Acción |
|
|
| ------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
| [`api.ts`](frontend/src/lib/api.ts) | Agregar `searchMetadata()` y `createGameFromMetadata()` |
|
|
| [`games/page.tsx`](frontend/src/app/games/page.tsx) | Cambiar botón a enlace |
|
|
| [`games/add/page.tsx`](frontend/src/app/games/add/page.tsx) | **NUEVO** |
|
|
| [`SearchForm.tsx`](frontend/src/components/games/SearchForm.tsx) | **NUEVO** |
|
|
| [`SearchResults.tsx`](frontend/src/components/games/SearchResults.tsx) | **NUEVO** |
|
|
| [`GamePreviewDialog.tsx`](frontend/src/components/games/GamePreviewDialog.tsx) | **NUEVO** |
|
|
| Tests unitarios | **NUEVOS** |
|
|
| [`game-search-flow.spec.ts`](tests/e2e/game-search-flow.spec.ts) | **NUEVO** |
|
|
|
|
---
|
|
|
|
**Documento creado:** 2025-03-21
|
|
**Autor:** Architect Mode
|
|
**Estado:** Diseño completo listo para implementación
|