# 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