Files
quasar/docs/02-tecnico/frontend.md
Benito Rodríguez 0c9c408564
Some checks failed
CI / lint (push) Failing after 1m4s
CI / test-backend (push) Has been skipped
CI / test-frontend (push) Has been skipped
CI / test-e2e (push) Has been skipped
Refactor code structure for improved readability and maintainability
2026-02-22 18:18:46 +01:00

1196 lines
31 KiB
Markdown

# 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
<Card>
<CardHeader>
<CardTitle>Título</CardTitle>
<CardDescription>Descripción opcional</CardDescription>
</CardHeader>
<CardContent>Contenido principal</CardContent>
<CardFooter>Acciones</CardFooter>
</Card>
```
#### 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
<Card className="group hover:border-primary/50 transition-colors cursor-pointer">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<GameCover gameId={game.id} className="h-16 w-12 rounded" />
<div className="min-w-0">
<CardTitle className="line-clamp-1">{game.title}</CardTitle>
<CardDescription className="line-clamp-1">
{game.platforms.map((p) => p.name).join(', ')}
</CardDescription>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="opacity-0 group-hover:opacity-100">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onEdit}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onDelete} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="pb-3">
<div className="flex flex-wrap gap-2">
{game.tags.slice(0, 3).map((tag) => (
<Badge key={tag.id} variant="secondary" className="text-xs">
{tag.name}
</Badge>
))}
{game.tags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{game.tags.length - 3}
</Badge>
)}
</div>
</CardContent>
<CardFooter className="pt-0">
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<HardDrive className="h-3 w-3" />
{game.romFiles.length} ROMs
</div>
{game.releaseDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(game.releaseDate)}
</div>
)}
</div>
</CardFooter>
</Card>
```
#### GameList
Componente de lista de juegos con filtros y paginación.
```typescript
interface GameListProps {
filters?: GameFilters;
onFiltersChange?: (filters: GameFilters) => void;
}
```
**Estructura:**
```tsx
<div className="space-y-6">
<GameFilters filters={filters} onChange={onFiltersChange} />
<GameGrid games={games} isLoading={isLoading} />
<GamePagination pagination={pagination} onPageChange={onPageChange} />
</div>
```
#### GameDetail
Página de detalle de un juego.
```tsx
<div className="space-y-6">
<GameDetailHeader game={game} />
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Resumen</TabsTrigger>
<TabsTrigger value="roms">ROMs</TabsTrigger>
<TabsTrigger value="purchases">Compras</TabsTrigger>
<TabsTrigger value="metadata">Metadata</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<GameOverview game={game} />
</TabsContent>
<TabsContent value="roms">
<GameRoms game={game} />
</TabsContent>
<TabsContent value="purchases">
<GamePurchases game={game} />
</TabsContent>
<TabsContent value="metadata">
<GameMetadata game={game} />
</TabsContent>
</Tabs>
</div>
```
### 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
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="Buscar en IGDB, RAWG..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Button onClick={handleSearch}>
<Search className="mr-2 h-4 w-4" />
Buscar
</Button>
</div>
{isLoading && <MetadataSearchResultsSkeleton />}
{results && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{results.map((result) => (
<MetadataResultCard key={result.id} result={result} onSelect={() => onSelect(result)} />
))}
</div>
)}
</div>
```
### Componentes de Dashboard
#### DashboardStats
Tarjetas de estadísticas del dashboard.
```typescript
interface DashboardStatsProps {
stats: {
totalGames: number;
totalRoms: number;
totalPlatforms: number;
recentGames: number;
};
}
```
**Estructura:**
```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Total de juegos"
value={stats.totalGames}
icon={<Gamepad2 className="h-4 w-4" />}
/>
<StatCard
title="Total de ROMs"
value={stats.totalRoms}
icon={<HardDrive className="h-4 w-4" />}
/>
<StatCard
title="Plataformas"
value={stats.totalPlatforms}
icon={<Monitor className="h-4 w-4" />}
/>
<StatCard
title="Juegos recientes"
value={stats.recentGames}
icon={<Clock className="h-4 w-4" />}
/>
</div>
```
## Layout Components
### AppLayout
Layout principal de la aplicación.
```typescript
interface AppLayoutProps {
children: React.ReactNode;
}
```
**Estructura:**
```tsx
<div className="min-h-screen bg-background">
<Header />
<div className="flex">
<Sidebar />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
```
### Header
Header de la aplicación.
```typescript
interface HeaderProps {
onMenuToggle?: () => void;
}
```
**Estructura:**
```tsx
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur">
<div className="container flex h-14 items-center gap-4">
<Logo />
<GlobalSearch />
<div className="ml-auto flex items-center gap-2">
<ThemeToggle />
<UserMenu />
</div>
</div>
</header>
```
### Sidebar
Sidebar de navegación.
```typescript
interface SidebarProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
```
**Estructura:**
```tsx
<aside className="fixed inset-y-0 left-0 z-40 w-64 border-r border-border bg-background">
<div className="flex h-full flex-col">
<SidebarHeader />
<SidebarNav />
<SidebarFooter />
</div>
</aside>
```
## 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<typeof gameSchema>
// 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 (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="title"
validators={{
onChange: ({ value }) =>
!value ? 'El título es requerido' : undefined,
}}
>
{(field) => (
<div>
<label htmlFor={field.name}>Título</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors && (
<span>{field.state.meta.errors.join(', ')}</span>
)}
</div>
)}
</form.Field>
<button type="submit">Guardar</button>
</form>
)
}
```
## 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
<Button
aria-label="Crear nuevo juego"
type="button"
onClick={handleCreate}
>
<Plus className="mr-2 h-4 w-4" />
Nuevo juego
</Button>
```
## 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
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{games.map((game) => (
<GameCard key={game.id} game={game} />
))}
</div>
```
## 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
<div className="min-h-screen bg-background">
<Header />
<div className="flex">
<Sidebar />
<main className="flex-1 p-6">
<PageHeader />
<PageContent />
</main>
</div>
</div>
```
### Card Grid
```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{items.map((item) => (
<Card key={item.id}>{/* Card content */}</Card>
))}
</div>
```
### Form Layout
```tsx
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Título</FormLabel>
<FormControl>
<Input placeholder="Escribe el título..." {...field} />
</FormControl>
<FormDescription>Descripción del campo</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
Cancelar
</Button>
<Button type="submit">Guardar</Button>
</div>
</form>
</Form>
```
### Estados de carga
#### Skeleton Loading
```tsx
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-4 w-3/4" />
</CardHeader>
<CardContent>
<Skeleton className="h-20 w-full" />
</CardContent>
</Card>
))}
</div>
) : (
// Actual content
)}
```
#### Empty State
```tsx
<div className="flex flex-col items-center justify-center py-12">
<div className="rounded-full bg-muted p-4">
<Inbox className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="mt-4 text-lg font-semibold">No hay elementos</h3>
<p className="mt-2 text-sm text-muted-foreground">Comienza añadiendo tu primer elemento.</p>
<Button className="mt-4">
<Plus className="mr-2 h-4 w-4" />
Añadir elemento
</Button>
</div>
```
## 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