# 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 { // 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(null); const [results, setResults] = useState([]); const [selectedResult, setSelectedResult] = useState(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 (

Añadir Juego

{results.length > 0 && ( )} !open && setSelectedResult(null)} metadata={selectedResult} onSave={handleSave} isSaving={isSaving} />
); } ``` ### 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 { 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 { 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 // Después ``` ## 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