1196 lines
31 KiB
Markdown
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
|