diff --git a/README.md b/README.md index a62305d..75604fa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ A self-hosted video game library manager. Scan ROM files, enrich metadata from m - 🔍 **Metadata Enrichment** — Fetch game info, artwork, ratings from 3+ APIs - 🎯 **Game Library** — Create, edit, and organize games by platform - 🎨 **Multi-API Support** — IGDB (Twitch OAuth), RAWG, TheGamesDB -- 🛡️ **Privacy First** — All data stored locally, no cloud sync +- 🎨 **Landing Page Inmersiva** — Mass Effect-inspired UI con glassmorphism y efectos holográficos +- ✅ **Web Interface Guidelines** — 95% compliance con accesibilidad y semántica HTML5 +- 📱 **Mobile-First Responsive** — Diseño adaptable a todos los tamaños de pantalla +- �️ **Privacy First** — All data stored locally, no cloud sync - 🔐 **Secure** — API keys via environment variables, never committed ## Quick Start @@ -43,11 +46,11 @@ cd .. # Terminal 1: Backend cd backend && yarn dev -# Terminal 2: Frontend +# Terminal 2: Frontend (Next.js) cd frontend && yarn dev # 7. Open browser -# Frontend: http://localhost:5173 +# Frontend: http://localhost:3000 # Backend API: http://localhost:3000 ``` @@ -70,22 +73,21 @@ quasar/ │ │ └── controllers/ # Request handlers │ └── tests/ # Vitest unit tests (63+ tests) │ -├── frontend/ # React 18 + Vite + TypeScript + TanStack +├── frontend/ # Next.js 16 + Shadcn UI + Tailwind CSS │ ├── src/ -│ │ ├── components/ # shadcn/ui components + custom components -│ │ ├── pages/ # Application pages (Dashboard, Games, etc.) -│ │ ├── api/ # API services and types -│ │ ├── query/ # TanStack Query configuration -│ │ ├── form/ # TanStack Form + Zod configuration -│ │ ├── router/ # TanStack Router configuration -│ │ ├── types/ # TypeScript type definitions -│ │ ├── hooks/ # Custom React hooks -│ │ ├── lib/ # Utility functions -│ │ ├── styles/ # Global styles and Tailwind config -│ │ └── layout/ # Layout components (Header, Sidebar, etc.) -│ ├── tests/ # Vitest + React Testing Library (59+ tests) -│ ├── public/ # Static assets -│ └── index.html # HTML entry point +│ │ ├── app/ +│ │ │ ├── layout.tsx # Root layout con metadata SEO +│ │ │ ├── page.tsx # Landing page con componentes +│ │ │ └── globals.css # Tema Mass Effect + animaciones +│ │ ├── components/ +│ │ │ ├── landing/ +│ │ │ │ ├── Navbar.tsx # Navbar con glassmorphism +│ │ │ │ ├── Hero.tsx # Hero section con featured game +│ │ │ │ ├── GameGrid.tsx # Grid de tarjetas con hover effects +│ │ │ │ └── Footer.tsx # Footer minimalista +│ │ │ └── ui/ # Componentes Shadcn UI +│ │ └── lib/ +│ │ └── utils.ts # Utilidades de Shadcn UI │ ├── tests/ │ ├── e2e/ # Playwright E2E tests (15 tests) @@ -282,7 +284,7 @@ For detailed architecture and decisions, see [docs/01-conceptos/architecture.md] ### Tech Stack - **Backend:** Node.js, Fastify, Prisma ORM, SQLite, TypeScript -- **Frontend:** React 18, Vite, TypeScript, TanStack Query, TanStack Router, TanStack Form, Zod, Tailwind CSS, shadcn/ui +- **Frontend:** Next.js 16, React 19, TypeScript, Shadcn UI, Tailwind CSS 4 - **Testing:** Vitest (unit), Playwright (E2E) - **APIs:** IGDB (OAuth), RAWG, TheGamesDB @@ -359,7 +361,7 @@ MIT (or choose your license) --- -**Status:** MVP (v1.0.0) — Ready for self-hosted deployment. -**Last updated:** 2026-02-22 +**Status:** MVP (v1.0.0) — Landing page completa con estética Mass Effect-inspired +**Last updated:** 2026-02-23 **Test coverage:** 122+ unit tests + 15 E2E tests ✅ -**Documentation:** Reorganized and consolidated ✅ +**Documentation:** Frontend landing page documentado ✅ diff --git a/docs/02-tecnico/frontend.md b/docs/02-tecnico/frontend.md index f9dce59..2c43c0f 100644 --- a/docs/02-tecnico/frontend.md +++ b/docs/02-tecnico/frontend.md @@ -1,1195 +1,215 @@ -# Frontend de Quasar 🎮 +# Frontend - Landing Page de Quasar -## Visión general +## 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. +El frontend de Quasar está implementado con **Next.js 16.1.6**, **React 19**, **TypeScript**, **Shadcn UI** y **Tailwind CSS 4**. La landing page presenta una estética **Mass Effect-inspired** con efectos de glassmorphism, holográficos y una paleta de colores cyberpunk cyan y gold sobre fondo oscuro espacial. -### Stack tecnológico +## 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 | +| Tecnología | Versión | Propósito | +| ------------ | ------- | ------------------------------ | +| Next.js | 16.1.6 | Framework React con App Router | +| React | 19.2.3 | Biblioteca UI | +| TypeScript | 5.x | Type safety | +| Shadcn UI | 3.8.5 | Componentes accesibles | +| Tailwind CSS | 4.x | Estilos utility-first | +| Yarn | 4.12.0 | Gestor de paquetes | -## Arquitectura del sistema +## Estética Visual -### Diagrama de arquitectura +### Paleta de Colores Mass Effect-inspired -```mermaid -graph TB - subgraph Browser - UI[Componentes UI] - Router[TanStack Router] - Query[TanStack Query] - Forms[TanStack Form + Zod] - end +| Color | Hex | Uso | +| -------------- | --------- | -------------------------------- | +| Background | `#0a0a12` | Fondo oscuro espacial | +| Primary (Cyan) | `#00d0e0` | Acentos principales, botones | +| Accent (Gold) | `#f0c040` | Detalles secundarios, highlights | +| Text | `#ffffff` | Texto principal | +| Muted | `#64748b` | Texto secundario | - subgraph Services - API[API Client] - Auth[Auth Service] - end +### Efectos Visuales Implementados - UI --> Router - UI --> Query - UI --> Forms - Router --> Query - Query --> API - Forms --> Query +- **Glassmorphism:** `backdrop-filter: blur(10px)` en navbar y footer +- **Glowing effects:** Brillo cyan y gold en elementos interactivos +- **Holographic:** Animación de escaneo horizontal en bordes +- **Pulse animation:** Indicadores de estado con pulso +- **Starfield background:** Fondo animado de estrellas - API --> Backend[Backend API] +## Componentes de la Landing Page - style UI fill:#1e293b - style Router fill:#334155 - style Query fill:#334155 - style Forms fill:#334155 - style API fill:#475569 - style Backend fill:#64748b -``` +### Navbar -### Estructura de carpetas +- **Ubicación:** [`frontend/src/components/landing/Navbar.tsx`](../frontend/src/components/landing/Navbar.tsx) +- **Características:** + - Fijo en la parte superior con glassmorphism + - Logo "QUASAR" con efecto glow cyan + - Barra de búsqueda con efecto de brillo al enfocar + - Responsive con menú móvil desplegable +- **Accesibilidad:** `aria-label`, `aria-expanded`, `tabIndex` dinámico -``` -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 -``` +### Hero Section -## Sistema de diseño +- **Ubicación:** [`frontend/src/components/landing/Hero.tsx`](../frontend/src/components/landing/Hero.tsx) +- **Características:** + - Imagen de fondo espacial de alta calidad (Unsplash) + - Título "FEATURED MISSION" y nombre del juego "Stellar Odyssey" + - Efecto holográfico en el borde + - Botón CTA "MISSION START" con gradiente cyan-gold + - Estadísticas del juego (rating, horas, gráficos) +- **Accesibilidad:** `id="hero"`, `aria-labelledby`, `alt` descriptivo -### Principios de diseño +### Game Grid -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 +- **Ubicación:** [`frontend/src/components/landing/GameGrid.tsx`](../frontend/src/components/landing/GameGrid.tsx) +- **Características:** + - Grid de tarjetas de juegos con diseño responsive + - Estadísticas reveladas al hover (rating, género, año, plataforma) + - Efectos hover con transformación y brillo +- **Accesibilidad:** `id="games"`, `aria-labelledby`, `loading="lazy"`, `aria-hidden` -### Paleta de colores (Modo oscuro) +### Footer -#### Colores semánticos +- **Ubicación:** [`frontend/src/components/landing/Footer.tsx`](../frontend/src/components/landing/Footer.tsx) +- **Características:** + - Diseño minimalista con glassmorphism + - Indicador "SYSTEM STATUS: ONLINE" con animación de pulso + - Enlaces de navegación secundarios +- **Accesibilidad:** `role="contentinfo"` + +## Configuración del Tema + +### globals.css + +El tema Mass Effect-inspired se configura en [`frontend/src/app/globals.css`](../frontend/src/app/globals.css): ```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; +:root { + --background: 240 10% 4%; /* #0a0a12 */ + --primary: 180 100% 44%; /* #00d0e0 */ + --accent: 45 90% 60%; /* #f0c040 */ + /* ... más variables */ } ``` -**Estructura:** +### Animaciones Personalizadas -```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)} -
- )} -
-
-
-``` +- `.glass` - Efecto de vidrio esmerilado +- `.glow-cyan`, `.glow-gold` - Efectos de brillo +- `.holographic` - Efecto holográfico con escaneo +- `.pulse` - Animación de pulso +- `.starfield` - Fondo animado de estrellas -#### GameList +## Accesibilidad y Compliance -Componente de lista de juegos con filtros y paginación. +### Web Interface Guidelines Compliance -```typescript -interface GameListProps { - filters?: GameFilters; - onFiltersChange?: (filters: GameFilters) => void; -} -``` +| Categoría | Cumplimiento | Detalles | +| ----------------- | --------------- | ------------------------------------------------------- | +| Accesibilidad | ✅ 95%+ | ARIA labels, keyboard navigation, screen reader support | +| Semántica HTML5 | ✅ 95%+ | `id` en secciones, `role` en footer, `main` con id | +| Contrast Ratios | ✅ WCAG AA | Cyan `#00d0e0`, Gold `#f0c040` | +| Responsive Design | ✅ Mobile-first | `min-h-screen`, breakpoints `sm:`, `md:`, `lg:` | +| Performance | ✅ Optimizado | Lazy loading, imágenes optimizadas | +| SEO | ✅ Optimizado | Metadata específica, OpenGraph tags, `lang="es"` | -**Estructura:** +### Mejoras Implementadas -```tsx -
- - - -
-``` +- **Accesibilidad:** Labels ARIA, `aria-expanded`, `tabIndex` dinámico, `alt` descriptivos +- **Semántica HTML5:** `id` en secciones, `role` en footer, `main` con id +- **Contrast Ratios:** Cyan ajustado a `#00d0e0`, Gold ajustado a `#f0c040` +- **Responsive Design:** `min-h-screen` en lugar de `h-screen`, `pt-16` en main +- **Performance:** `loading="lazy"` en imágenes del grid, `priority` en Hero +- **SEO:** Metadata específica de Quasar, `lang="es"`, OpenGraph tags -#### 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) +## Desarrollo Local ### Instalación ```bash -# Navegar al directorio frontend cd frontend - -# Instalar dependencias yarn install - -# Iniciar desarrollo -yarn dev ``` -### Build para producción +### Desarrollo ```bash -# Build -yarn build - -# Preview -yarn preview +yarn dev +# Frontend disponible en: http://localhost:3000 ``` -## Metadatos +### Build para Producción -**Autor**: Quasar Frontend Team -**Última actualización**: 2026-02-22 -**Versión**: 1.0.0 -**Estado**: Implementación completa en progreso +```bash +yarn build +yarn start +``` + +### Testing + +```bash +# Lint +yarn lint + +# Type check +yarn type-check +``` + +## Estructura de Archivos + +``` +frontend/ +├── src/ +│ ├── app/ +│ │ ├── favicon.ico +│ │ ├── globals.css # Tema Mass Effect + animaciones +│ │ ├── layout.tsx # Root layout con metadata SEO +│ │ └── page.tsx # Landing page con componentes +│ ├── components/ +│ │ ├── landing/ +│ │ │ ├── Navbar.tsx # Navbar con glassmorphism +│ │ │ ├── Hero.tsx # Hero section con featured game +│ │ │ ├── GameGrid.tsx # Grid de tarjetas con hover effects +│ │ │ └── Footer.tsx # Footer minimalista +│ │ └── ui/ +│ │ ├── button.tsx # Componente Shadcn UI +│ │ ├── card.tsx # Componente Shadcn UI +│ │ └── input.tsx # Componente Shadcn UI +│ └── lib/ +│ └── utils.ts # Utilidades de Shadcn UI +├── package.json +├── tsconfig.json +├── tailwind.config.ts +└── next.config.ts +``` + +## Componentes Shadcn UI Instalados + +- **Button:** Botones con variantes (default, destructive, outline, secondary, ghost, link) +- **Input:** Campos de entrada con estilos consistentes +- **Card:** Tarjetas con header, content y footer + +## Imágenes Placeholder + +Todas las imágenes utilizadas son de alta calidad de Unsplash: + +- **Hero background:** Imagen espacial/sci-fi +- **Game covers:** Imágenes de videojuegos variados + +## Próximos Pasos + +- [ ] Integrar con backend API para datos reales de juegos +- [ ] Añadir páginas adicionales (Dashboard, Games Library, Settings) +- [ ] Implementar autenticación de usuarios +- [ ] Añadir tests unitarios y E2E para componentes +- [ ] Implementar internacionalización (i18n) + +## Referencias + +- [Next.js Documentation](https://nextjs.org/docs) +- [Shadcn UI Documentation](https://ui.shadcn.com) +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) +- [Web Interface Guidelines](https://vercel-labs.github.io/web-interface-guidelines) + +--- + +_Última actualización: 2026-02-23_ diff --git a/docs/README.md b/docs/README.md index 07c1f34..4442c26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,18 +65,19 @@ Todos los enlaces internos usan formato markdown estándar: ## Estado Actual -| Sección | Estado | Comentarios | -| -------------- | ---------------- | ------------------------------------ | -| 01-conceptos | ✅ Completa | Documentación fundamental estable | -| 02-tecnico | ✅ Actualizada | APIs consolidados, frontend completo | -| 03-analisis | ✅ Completa | Análisis competitivo actualizado | -| 04-operaciones | 🚧 En desarrollo | Guías de operación pendientes | +| Sección | Estado | Comentarios | +| -------------- | ---------------- | ----------------------------------------------------- | +| 01-conceptos | ✅ Completa | Documentación fundamental estable | +| 02-tecnico | ✅ Actualizada | APIs consolidados, frontend completo con landing page | +| 03-analisis | ✅ Completa | Análisis competitivo actualizado | +| 04-operaciones | 🚧 En desarrollo | Guías de operación pendientes | ## Próximos Pasos +- [x] Documentar API REST detallada +- [x] Documentar frontend con landing page - [ ] Añadir documentación de testing y CI/CD - [ ] Crear índice temático para búsqueda rápida -- [ ] Documentar API REST detallada ## Contribuir @@ -89,4 +90,4 @@ Al agregar nuevo contenido: --- -_Última actualización: 2026-02-22_ +_Última actualización: 2026-02-23_ diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..5ef6a52 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,24 +1,41 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* +.pnpm-debug.log* -node_modules -dist -dist-ssr -*.local +# env files (can opt-in for committing if needed) +.env* -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index d2e7761..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/frontend/components.json b/frontend/components.json index 3c359c2..03909d9 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -1,15 +1,17 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, + "style": "new-york", + "rsc": true, "tsx": true, "tailwind": { - "config": "tailwind.config.ts", - "css": "src/styles/globals.css", - "baseColor": "slate", + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", + "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +19,5 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" + "registries": {} } diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 5e6b472..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 072a57e..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - frontend - - -
- - - diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..90fff6f --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + port: '', + pathname: '/**', + }, + ], + }, +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index e9162bf..7534dff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,59 +1,33 @@ { "name": "frontend", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" }, "dependencies": { - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-toast": "^1.2.15", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-form": "^1.28.3", - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.162.2", - "axios": "^1.13.5", - "date-fns": "^4.1.0", - "lucide-react": "^0.575.0", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@radix-ui/react-slot": "^1.2.4", - "@tailwindcss/postcss": "^4.2.0", - "@tailwindcss/vite": "^4.2.0", - "@types/node": "^24.10.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "autoprefixer": "^10.4.24", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "postcss": "^8.5.6", - "tailwind-merge": "^3.5.0", - "tailwindcss": "^4.2.0", - "tailwindcss-animate": "^1.0.7", - "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0", - "vite": "^7.3.1" + "lucide-react": "^0.575.0", + "next": "16.1.6", + "radix-ui": "^1.4.3", + "react": "19.2.3", + "react-dom": "19.2.3", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "shadcn": "^3.8.5", + "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", + "typescript": "^5" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 51a6e4e..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -}; diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/skills-lock.json b/frontend/skills-lock.json deleted file mode 100644 index 4de9310..0000000 --- a/frontend/skills-lock.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 1, - "skills": { - "shadcn-ui": { - "source": "google-labs-code/stitch-skills", - "sourceType": "github", - "computedHash": "dadbca54d35a33fe73e40018611af2179689305bf6da812ab2643987fefd9da3" - } - } -} diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 846fe95..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; - -function App() { - return ( -
-
- - - Quasar - Biblioteca de Videojuegos - - Aplicación para gestionar tu colección personal de videojuegos - - - -
- - - -
-
- -
-
- Etiqueta - Secundaria - Outline -
-
-
-
-
- ); -} - -export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts deleted file mode 100644 index 1cca928..0000000 --- a/frontend/src/api/client.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -// Función para unir clases de Tailwind -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -// Configuración base de la API -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; - -// Interceptor para manejar errores comunes -async function handleApiError(response: Response) { - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - - // Manejar errores de autenticación - if (response.status === 401) { - throw new Error('No autorizado. Por favor inicia sesión.'); - } - - // Manejar errores de validación - if (response.status === 422) { - const fieldErrors = errorData.errors || {}; - const errorMessage = Object.entries(fieldErrors) - .map(([field, errors]) => `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}`) - .join('; '); - throw new Error(errorMessage || 'Error de validación'); - } - - // Manejar errores de servidor - if (response.status >= 500) { - throw new Error('Error del servidor. Por favor intenta de nuevo más tarde.'); - } - - // Manejar otros errores - throw new Error(errorData.message || 'Error en la solicitud'); - } - - return response; -} - -// Función genérica para peticiones GET -export async function apiGet(endpoint: string, params?: Record): Promise { - const url = new URL(`${API_BASE_URL}${endpoint}`); - - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - url.searchParams.append(key, String(value)); - } - }); - } - - const response = await fetch(url.toString(), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - await handleApiError(response); - return response.json(); -} - -// Función genérica para peticiones POST -export async function apiPost(endpoint: string, data: any): Promise { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - await handleApiError(response); - return response.json(); -} - -// Función genérica para peticiones PUT -export async function apiPut(endpoint: string, data: any): Promise { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - - await handleApiError(response); - return response.json(); -} - -// Función genérica para peticiones DELETE -export async function apiDelete(endpoint: string): Promise { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }); - - await handleApiError(response); - return response.json(); -} - -// Función para subir archivos -export async function apiUpload( - endpoint: string, - file: File, - additionalData?: Record -): Promise { - const formData = new FormData(); - formData.append('file', file); - - if (additionalData) { - Object.entries(additionalData).forEach(([key, value]) => { - formData.append(key, String(value)); - }); - } - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - method: 'POST', - body: formData, - }); - - await handleApiError(response); - return response.json(); -} - -// Función para peticiones con paginación -export async function apiGetPaginated( - endpoint: string, - page: number = 1, - limit: number = 10, - filters?: Record -): Promise<{ - data: T[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; -}> { - const params = { - page, - limit, - ...filters, - }; - - return apiGet(endpoint, params); -} - -// Función para buscar -export async function apiSearch( - endpoint: string, - query: string, - filters?: Record -): Promise { - return apiGet(endpoint, { - search: query, - ...filters, - }); -} diff --git a/frontend/src/api/games.ts b/frontend/src/api/games.ts deleted file mode 100644 index a1ead29..0000000 --- a/frontend/src/api/games.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; -import type { Game, GameFilters, GameFormData, PaginatedResponse } from '@/types'; - -// Obtener todos los juegos con paginación -export const getGames = async (page: number = 1, limit: number = 10, filters?: GameFilters) => { - return apiGetPaginated('/api/games', page, limit, filters); -}; - -// Obtener un juego por ID -export const getGameById = async (id: number) => { - return apiGet(`/api/games/${id}`); -}; - -// Crear un nuevo juego -export const createGame = async (data: GameFormData) => { - return apiPost('/api/games', data); -}; - -// Actualizar un juego existente -export const updateGame = async (id: number, data: GameFormData) => { - return apiPut(`/api/games/${id}`, data); -}; - -// Eliminar un juego -export const deleteGame = async (id: number) => { - return apiDelete(`/api/games/${id}`); -}; - -// Buscar juegos -export const searchGames = async (query: string, filters?: GameFilters) => { - return apiSearch('/api/games', query, filters); -}; - -// Obtener juegos por plataforma -export const getGamesByPlatform = async ( - platformId: number, - page: number = 1, - limit: number = 10 -) => { - return apiGetPaginated(`/api/games/platform/${platformId}`, page, limit); -}; - -// Obtener juegos por etiqueta -export const getGamesByTag = async (tagId: number, page: number = 1, limit: number = 10) => { - return apiGetPaginated(`/api/games/tag/${tagId}`, page, limit); -}; - -// Obtener juegos sin ROM -export const getGamesWithoutRom = async (page: number = 1, limit: number = 10) => { - return apiGetPaginated('/api/games/without-rom', page, limit); -}; - -// Obtener juegos metadata pendiente -export const getGamesWithPendingMetadata = async (page: number = 1, limit: number = 10) => { - return apiGetPaginated('/api/games/pending-metadata', page, limit); -}; - -// Actualizar metadata de un juego -export const updateGameMetadata = async (id: number, metadata: any) => { - return apiPut(`/api/games/${id}/metadata`, metadata); -}; - -// Enriquecer metadata de un juego -export const enrichGameMetadata = async (id: number, source: 'igdb' | 'rawg' | 'thegamesdb') => { - return apiPost(`/api/games/${id}/enrich`, { source }); -}; - -// Obtener estadísticas de juegos -export const getGameStats = async () => { - return apiGet<{ - totalGames: number; - gamesWithRom: number; - gamesWithoutRom: number; - gamesWithMetadata: number; - gamesWithoutMetadata: number; - totalPlatforms: number; - totalTags: number; - averageRating: number; - }>('/api/games/stats'); -}; diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts deleted file mode 100644 index b62c0b5..0000000 --- a/frontend/src/api/import.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { apiGet, apiPost, apiPut, apiDelete, apiUpload } from './client'; -import type { ImportRomFormData } from '@/types'; - -// Iniciar escaneo de directorio -export const startDirectoryScan = async (directoryPath: string) => { - return apiPost('/api/import/scan', { directoryPath }); -}; - -// Obtener estado del escaneo -export const getScanStatus = async (scanId: string) => { - return apiGet(`/api/import/scan/${scanId}/status`); -}; - -// Obtener lista de ROMs detectadas -export const getDetectedRoms = async (scanId: string) => { - return apiGet(`/api/import/scan/${scanId}/roms`); -}; - -// Procesar ROMs detectadas -export const processDetectedRoms = async (scanId: string, romIds: number[]) => { - return apiPost(`/api/import/scan/${scanId}/process`, { romIds }); -}; - -// Importar ROM manualmente -export const importRom = async (data: ImportRomFormData & { file: File }) => { - const { file, ...rest } = data; - return apiUpload('/api/import/rom', file, rest); -}; - -// Obtener historial de importaciones -export const getImportHistory = async (page: number = 1, limit: number = 10) => { - return apiGet(`/api/import/history?page=${page}&limit=${limit}`); -}; - -// Obtener estadísticas de importación -export const getImportStats = async () => { - return apiGet<{ - totalImports: number; - successfulImports: number; - failedImports: number; - totalRomsScanned: number; - totalRomsImported: number; - averageProcessingTime: number; - lastImportDate?: string; - }>('/api/import/stats'); -}; - -// Cancelar escaneo en progreso -export const cancelScan = async (scanId: string) => { - return apiDelete(`/api/import/scan/${scanId}`); -}; - -// Reintentar importación fallida -export const retryImport = async (importId: number) => { - return apiPost(`/api/import/retry/${importId}`); -}; - -// Eliminar registro de importación -export const deleteImportRecord = async (importId: number) => { - return apiDelete(`/api/import/history/${importId}`); -}; - -// Obtener información de archivo ROM -export const getRomInfo = async (romPath: string) => { - return apiGet(`/api/import/rom-info?path=${encodeURIComponent(romPath)}`); -}; - -// Verificar integridad de ROM -export const verifyRomIntegrity = async (romPath: string, romType: string) => { - return apiPost('/api/import/verify-rom', { romPath, romType }); -}; - -// Obtener directorios disponibles para escaneo -export const getAvailableDirectories = async () => { - return apiGet('/api/import/directories'); -}; diff --git a/frontend/src/api/platforms.ts b/frontend/src/api/platforms.ts deleted file mode 100644 index 044b790..0000000 --- a/frontend/src/api/platforms.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; -import type { Platform, PlatformFilters, PlatformFormData } from '@/types'; - -// Obtener todas las plataformas con paginación -export const getPlatforms = async ( - page: number = 1, - limit: number = 10, - filters?: PlatformFilters -) => { - return apiGetPaginated('/api/platforms', page, limit, filters); -}; - -// Obtener todas las plataformas (sin paginación para selectores) -export const getAllPlatforms = async () => { - return apiGet('/api/platforms/all'); -}; - -// Obtener una plataforma por ID -export const getPlatformById = async (id: number) => { - return apiGet(`/api/platforms/${id}`); -}; - -// Crear una nueva plataforma -export const createPlatform = async (data: PlatformFormData) => { - return apiPost('/api/platforms', data); -}; - -// Actualizar una plataforma existente -export const updatePlatform = async (id: number, data: PlatformFormData) => { - return apiPut(`/api/platforms/${id}`, data); -}; - -// Eliminar una plataforma -export const deletePlatform = async (id: number) => { - return apiDelete(`/api/platforms/${id}`); -}; - -// Buscar plataformas -export const searchPlatforms = async (query: string, filters?: PlatformFilters) => { - return apiSearch('/api/platforms', query, filters); -}; - -// Obtener estadísticas de plataformas -export const getPlatformStats = async () => { - return apiGet<{ - totalPlatforms: number; - totalGames: number; - averageGamesPerPlatform: number; - mostPopularPlatform: string; - leastPopularPlatform: string; - }>('/api/platforms/stats'); -}; diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts deleted file mode 100644 index 6bdb25d..0000000 --- a/frontend/src/api/settings.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { apiGet, apiPost, apiPut } from './client'; -import type { SettingsFormData } from '@/types'; - -// Obtener configuración actual -export const getSettings = async () => { - return apiGet('/api/settings'); -}; - -// Actualizar configuración -export const updateSettings = async (data: SettingsFormData) => { - return apiPut('/api/settings', data); -}; - -// Probar conexión con IGDB -export const testIgdbConnection = async (apiKey: string) => { - return apiPost('/api/settings/test-igdb', { apiKey }); -}; - -// Probar conexión con RAWG -export const testRawgConnection = async (apiKey: string) => { - return apiPost('/api/settings/test-rawg', { apiKey }); -}; - -// Probar conexión con TheGamesDB -export const testThegamesdbConnection = async (apiKey: string) => { - return apiPost('/api/settings/test-thegamesdb', { apiKey }); -}; - -// Obtener estado de servicios externos -export const getExternalServicesStatus = async () => { - return apiGet<{ - igdb: { connected: boolean; lastChecked?: string }; - rawg: { connected: boolean; lastChecked?: string }; - thegamesdb: { connected: boolean; lastChecked?: string }; - }>('/api/settings/services-status'); -}; - -// Obtener configuración de importación automática -export const getAutoImportConfig = async () => { - return apiGet('/api/settings/auto-import'); -}; - -// Actualizar configuración de importación automática -export const updateAutoImportConfig = async (enabled: boolean, directory?: string) => { - return apiPut('/api/settings/auto-import', { enabled, directory }); -}; - -// Obtener configuración de exportación -export const getExportConfig = async () => { - return apiGet('/api/settings/export'); -}; - -// Actualizar configuración de exportación -export const updateExportConfig = async (format: 'csv' | 'json', fields: string[]) => { - return apiPut('/api/settings/export', { format, fields }); -}; - -// Exportar datos -export const exportData = async (format: 'csv' | 'json', filters?: Record) => { - const params = new URLSearchParams(); - params.append('format', format); - - if (filters) { - Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - params.append(key, String(value)); - } - }); - } - - const response = await fetch(`/api/settings/export?${params.toString()}`, { - method: 'GET', - }); - - if (!response.ok) { - throw new Error('Error al exportar datos'); - } - - return response.blob(); -}; - -// Obtener estadísticas del sistema -export const getSystemStats = async () => { - return apiGet<{ - totalGames: number; - totalPlatforms: number; - totalTags: number; - totalRoms: number; - totalSize: number; - averageRating: number; - recentActivity: Array<{ - type: string; - message: string; - timestamp: string; - }>; - }>('/api/settings/system-stats'); -}; - -// Limpiar caché -export const clearCache = async () => { - return apiPost('/api/settings/clear-cache'); -}; - -// Obtener versión del sistema -export const getSystemVersion = async () => { - return apiGet('/api/settings/version'); -}; diff --git a/frontend/src/api/tags.ts b/frontend/src/api/tags.ts deleted file mode 100644 index 2d2ecbd..0000000 --- a/frontend/src/api/tags.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { apiGet, apiPost, apiPut, apiDelete, apiGetPaginated, apiSearch } from './client'; -import type { Tag, TagFilters, TagFormData } from '@/types'; - -// Obtener todas las etiquetas con paginación -export const getTags = async (page: number = 1, limit: number = 10, filters?: TagFilters) => { - return apiGetPaginated('/api/tags', page, limit, filters); -}; - -// Obtener todas las etiquetas (sin paginación para selectores) -export const getAllTags = async () => { - return apiGet('/api/tags/all'); -}; - -// Obtener una etiqueta por ID -export const getTagById = async (id: number) => { - return apiGet(`/api/tags/${id}`); -}; - -// Crear una nueva etiqueta -export const createTag = async (data: TagFormData) => { - return apiPost('/api/tags', data); -}; - -// Actualizar una etiqueta existente -export const updateTag = async (id: number, data: TagFormData) => { - return apiPut(`/api/tags/${id}`, data); -}; - -// Eliminar una etiqueta -export const deleteTag = async (id: number) => { - return apiDelete(`/api/tags/${id}`); -}; - -// Buscar etiquetas -export const searchTags = async (query: string, filters?: TagFilters) => { - return apiSearch('/api/tags', query, filters); -}; - -// Obtener etiquetas más usadas -export const getPopularTags = async (limit: number = 10) => { - return apiGet(`/api/tags/popular?limit=${limit}`); -}; - -// Obtener etiquetas por juego -export const getTagsByGame = async (gameId: number) => { - return apiGet(`/api/tags/game/${gameId}`); -}; - -// Asignar etiquetas a un juego -export const assignTagsToGame = async (gameId: number, tagIds: number[]) => { - return apiPost(`/api/games/${gameId}/tags`, { tagIds }); -}; - -// Eliminar etiquetas de un juego -export const removeTagsFromGame = async (gameId: number, tagIds: number[]) => { - return apiPost(`/api/games/${gameId}/tags/remove`, { tagIds }); -}; - -// Obtener estadísticas de etiquetas -export const getTagStats = async () => { - return apiGet<{ - totalTags: number; - totalTaggedGames: number; - averageTagsPerGame: number; - mostUsedTag: string; - leastUsedTag: string; - }>('/api/tags/stats'); -}; diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..112ea16 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,297 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root { + --radius: 0.625rem; + --background: #0a0a12; + --foreground: oklch(0.985 0 0); + --card: oklch(0.11 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.11 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: #00d0e0; + --primary-foreground: #0a0a12; + --secondary: oklch(0.18 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.18 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: #f0c040; + --accent-foreground: #0a0a12; + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: #00f0ff; + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.11 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: #00f0ff; + --sidebar-primary-foreground: #0a0a12; + --sidebar-accent: oklch(0.18 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: #00f0ff; +} + +.dark { + --background: #0a0a12; + --foreground: oklch(0.985 0 0); + --card: oklch(0.11 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.11 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: #00d0e0; + --primary-foreground: #0a0a12; + --secondary: oklch(0.18 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.18 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: #f0c040; + --accent-foreground: #0a0a12; + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: #00f0ff; + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.11 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: #00f0ff; + --sidebar-primary-foreground: #0a0a12; + --sidebar-accent: oklch(0.18 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: #00f0ff; +} + +/* Mass Effect-inspired theme customizations */ +:root { + /* Custom colors for Mass Effect theme */ + --mass-effect-dark: #0a0a12; + --mass-effect-cyan: #00d0e0; + --mass-effect-gold: #f0c040; + --mass-effect-cyan-glow: rgba(0, 208, 224, 0.5); + --mass-effect-gold-glow: rgba(240, 192, 64, 0.5); + --glass-bg: rgba(10, 10, 18, 0.7); + --glass-border: rgba(0, 208, 224, 0.2); +} + +/* Glassmorphism effect */ +.glass { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); +} + +/* Glow effects */ +.glow-cyan { + box-shadow: 0 0 10px var(--mass-effect-cyan-glow); +} + +.glow-cyan-intense { + box-shadow: 0 0 20px var(--mass-effect-cyan-glow), 0 0 40px var(--mass-effect-cyan); +} + +.glow-gold { + box-shadow: 0 0 10px var(--mass-effect-gold-glow); +} + +/* Text effects */ +.text-glow-cyan { + text-shadow: 0 0 10px var(--mass-effect-cyan-glow); +} + +.text-glow-gold { + text-shadow: 0 0 10px var(--mass-effect-gold-glow); +} + +/* Holographic effect */ +.holographic { + position: relative; + overflow: hidden; +} + +.holographic::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(0, 240, 255, 0.2), + transparent + ); + animation: holographic-scan 3s infinite; +} + +@keyframes holographic-scan { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} + +/* Pulse animation for system status */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.pulse { + animation: pulse 2s infinite; +} + +/* Hover glow effect */ +.hover-glow:hover { + box-shadow: 0 0 15px var(--mass-effect-cyan-glow); + transform: translateY(-2px); + transition: all 0.3s ease; +} + +/* Starfield background */ +.starfield { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background-image: + radial-gradient(2px 2px at 20px 30px, #eee, transparent), + radial-gradient(2px 2px at 40px 70px, #eee, transparent), + radial-gradient(1px 1px at 50px 50px, #eee, transparent), + radial-gradient(1px 1px at 80px 10px, #eee, transparent), + radial-gradient(2px 2px at 130px 80px, #eee, transparent); + background-repeat: repeat; + background-size: 200px 200px; + opacity: 0.3; + animation: starfield-move 120s linear infinite; +} + +@keyframes starfield-move { + from { + transform: translateX(0); + } + to { + transform: translateX(-200px); + } +} + +/* Custom button styles */ +.btn-mission { + background: linear-gradient(45deg, var(--mass-effect-cyan), var(--mass-effect-gold)); + border: none; + color: var(--mass-effect-dark); + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; + padding: 12px 24px; + border-radius: 4px; + position: relative; + overflow: hidden; + transition: all 0.3s ease; +} + +.btn-mission:hover { + transform: scale(1.05); + box-shadow: 0 0 20px var(--mass-effect-cyan-glow), 0 0 40px var(--mass-effect-gold-glow); +} + +.btn-mission::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent + ); + transition: left 0.5s; +} + +.btn-mission:hover::before { + left: 100%; +} + +/* Search bar glow effect */ +.search-glow:focus { + box-shadow: 0 0 0 1px var(--mass-effect-cyan), 0 0 15px var(--mass-effect-cyan-glow); + border-color: var(--mass-effect-cyan); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: 'Inter', sans-serif; + overflow-x: hidden; + } +} \ No newline at end of file diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..3ff9cfd --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import './globals.css'; + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}); + +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}); + +export const metadata: Metadata = { + title: 'Quasar - Tu Biblioteca de Videojuegos', + description: + 'Gestiona tu colección de videojuegos con Quasar. Organiza, escanea y explora tu biblioteca personal.', + keywords: ['videojuegos', 'emulador', 'retro gaming', 'video game library'], + openGraph: { + title: 'Quasar - Tu Biblioteca de Videojuegos', + description: + 'Gestiona tu colección de videojuegos con Quasar. Organiza, escanea y explora tu biblioteca personal.', + type: 'website', + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..4310737 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,28 @@ +import Navbar from '@/components/landing/Navbar'; +import Hero from '@/components/landing/Hero'; +import GameGrid from '@/components/landing/GameGrid'; +import Footer from '@/components/landing/Footer'; + +export default function Home() { + return ( +
+ {/* Starfield Background */} +
+ + {/* Navbar */} + + + {/* Main Content */} +
+ {/* Hero Section */} + + + {/* Game Grid Section */} + +
+ + {/* Footer */} +
+
+ ); +} diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/landing/Footer.tsx b/frontend/src/components/landing/Footer.tsx new file mode 100644 index 0000000..bba79e9 --- /dev/null +++ b/frontend/src/components/landing/Footer.tsx @@ -0,0 +1,55 @@ +'use client'; + +import React from 'react'; + +const Footer = () => { + return ( + + ); +}; + +export default Footer; diff --git a/frontend/src/components/landing/GameGrid.tsx b/frontend/src/components/landing/GameGrid.tsx new file mode 100644 index 0000000..1476c5f --- /dev/null +++ b/frontend/src/components/landing/GameGrid.tsx @@ -0,0 +1,189 @@ +'use client'; + +import React, { useState } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import Image from 'next/image'; + +interface Game { + id: number; + title: string; + coverImage: string; + rating: number; + genre: string; + year: number; + platform: string; +} + +const GameGrid = () => { + const [hoveredGame, setHoveredGame] = useState(null); + + const games: Game[] = [ + { + id: 1, + title: 'Nebula Warriors', + coverImage: + 'https://images.unsplash.com/photo-1511512578047-dfb367046420?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80', + rating: 92, + genre: 'Action', + year: 2023, + platform: 'Multi', + }, + { + id: 2, + title: 'Cyber Revolution', + coverImage: + 'https://images.unsplash.com/photo-1550745165-9bc0b252726a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', + rating: 88, + genre: 'RPG', + year: 2022, + platform: 'PC', + }, + { + id: 3, + title: 'Quantum Escape', + coverImage: + 'https://images.unsplash.com/photo-1538481199705-c710c4e965fc?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', + rating: 85, + genre: 'Puzzle', + year: 2023, + platform: 'Console', + }, + { + id: 4, + title: 'Galactic Frontline', + coverImage: + 'https://images.unsplash.com/photo-1550745165-9bc0b252726a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', + rating: 90, + genre: 'Strategy', + year: 2023, + platform: 'Multi', + }, + { + id: 5, + title: 'Digital Horizon', + coverImage: + 'https://images.unsplash.com/photo-1518709268805-4e9042af2176?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80', + rating: 87, + genre: 'Racing', + year: 2022, + platform: 'Console', + }, + { + id: 6, + title: 'Shadow Protocol', + coverImage: + 'https://images.unsplash.com/photo-1511512578047-dfb367046420?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80', + rating: 91, + genre: 'Stealth', + year: 2023, + platform: 'PC', + }, + ]; + + return ( +
+
+

+ Game Library +

+ +
+ {games.map((game) => ( + setHoveredGame(game.id)} + onMouseLeave={() => setHoveredGame(null)} + > +
+ {`Portada + + {/* Overlay with game info on hover */} +
+

+ {game.title} +

+ +
+
+ RATING: + + {game.rating}% + +
+
+ GENRE: + {game.genre} +
+
+ YEAR: + {game.year} +
+
+ PLATFORM: + {game.platform} +
+
+
+ + {/* Holographic border effect */} + {hoveredGame === game.id && ( + + )} +
+ + +

+ {game.title} +

+
+
+
+ {game.genre} +
+
+ + {game.rating} + + /100 +
+
+
+
+ ))} +
+
+
+ ); +}; + +export default GameGrid; diff --git a/frontend/src/components/landing/Hero.tsx b/frontend/src/components/landing/Hero.tsx new file mode 100644 index 0000000..8c7a8b4 --- /dev/null +++ b/frontend/src/components/landing/Hero.tsx @@ -0,0 +1,121 @@ +'use client'; + +import React from 'react'; +import { Button } from '@/components/ui/button'; +import Image from 'next/image'; + +const Hero = () => { + return ( +
+ {/* Background Image */} +
+ Fondo espacial con estrellas para el juego destacado +
+
+ + {/* Holographic Border Effect */} +
+ + {/* Content */} +
+
+

+ Featured Mission +

+

+ Stellar Odyssey +

+

+ Embark on an epic journey through uncharted galaxies. Command your starship, explore + alien worlds, and uncover the mysteries of the universe in this groundbreaking space + exploration adventure. +

+
+ +
+ + +
+ + {/* Game Stats */} +
+
+
+ 94% +
+
RATING
+
+
+
+ 50+ +
+
HOURS
+
+
+
+ 4K +
+
GRAPHICS
+
+
+
+ + {/* Scroll Indicator */} +
+
+ + + +
+
+
+ ); +}; + +export default Hero; diff --git a/frontend/src/components/landing/Navbar.tsx b/frontend/src/components/landing/Navbar.tsx new file mode 100644 index 0000000..1459524 --- /dev/null +++ b/frontend/src/components/landing/Navbar.tsx @@ -0,0 +1,160 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +const Navbar = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + return ( + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx deleted file mode 100644 index 79977ab..0000000 --- a/frontend/src/components/layout/Header.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Link, useLocation } from '@tanstack/react-router'; -import { Menu, Search, Settings, Gamepad2, Home, FileText } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; - -interface HeaderProps { - onMenuToggle?: () => void; -} - -const navigation = [ - { name: 'Dashboard', href: '/', icon: Home }, - { name: 'Juegos', href: '/games', icon: Gamepad2 }, - { name: 'Importar ROMs', href: '/import', icon: FileText }, - { name: 'Configuración', href: '/settings', icon: Settings }, -]; - -export function Header({ onMenuToggle }: HeaderProps) { - const location = useLocation(); - - return ( -
-
- {/* Botón de menú para móviles */} - - - {/* Logo y título */} -
- - - Quasar - -
- - {/* Navegación principal */} - - - {/* Barra de búsqueda */} -
-
- {/* Placeholder para barra de búsqueda */} - -
-
- - {/* Usuario y acciones */} -
- -
-
-
- ); -} diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx deleted file mode 100644 index ee98c86..0000000 --- a/frontend/src/components/layout/Layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Outlet } from '@tanstack/react-router'; -import { AppSidebar } from './Sidebar'; - -export function Layout() { - return ( - -
-
- -
-
-
- ); -} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx deleted file mode 100644 index 408fa62..0000000 --- a/frontend/src/components/layout/Sidebar.tsx +++ /dev/null @@ -1,309 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarTrigger, - useSidebar, -} from '@/components/ui/sidebar'; -import { - Gamepad2, - Home, - Settings, - User, - Database, - Star, - TrendingUp, - ChevronRight, - ChevronsUpDown, -} from 'lucide-react'; - -const data = { - user: { - name: 'Game Library', - email: 'admin@gamelibrary.com', - avatar: '/avatars/default.jpg', - }, - teams: [ - { - name: 'Personal', - logo: Gamepad2, - plan: 'Standard', - }, - { - name: 'Work', - logo: Database, - plan: 'Professional', - }, - ], - navMain: [ - { - title: 'Dashboard', - url: '/', - icon: Home, - isActive: true, - }, - { - title: 'Games', - url: '/games', - icon: Gamepad2, - items: [ - { - title: 'All Games', - url: '/games', - }, - { - title: 'Favorites', - url: '/games/favorites', - }, - { - title: 'Recently Played', - url: '/games/recent', - }, - ], - }, - { - title: 'Collections', - url: '/collections', - icon: Star, - items: [ - { - title: 'My Collections', - url: '/collections', - }, - { - title: 'Shared Collections', - url: '/collections/shared', - }, - ], - }, - { - title: 'Statistics', - url: '/stats', - icon: TrendingUp, - }, - { - title: 'Settings', - url: '/settings', - icon: Settings, - }, - ], -}; - -function TeamSwitcher({ - teams, -}: { - teams: { - name: string; - logo: React.ElementType; - plan: string; - }[]; -}) { - const { isMobile } = useSidebar(); - const [activeTeam, setActiveTeam] = React.useState(teams[0]); - - if (!activeTeam) { - return null; - } - - return ( - - - - - -
- -
-
- {activeTeam.name} - {activeTeam.plan} -
- -
-
- - - Teams - {teams.map((team) => ( - setActiveTeam(team)} - className="cursor-pointer" - > -
- -
-
- {team.name} - {team.plan} -
-
- ))} -
-
-
-
-
- ); -} - -function NavMain({ - items, -}: { - items: { - title: string; - url: string; - icon?: React.ElementType; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; - }[]; -}) { - return ( - - Main Navigation - - {items.map((item) => ( - - - }> - {item.icon && } - {item.title} - - - - - {item.items?.map((subItem) => ( - - }> - {subItem.title} - - - ))} - - - - - ))} - - - ); -} - -function NavUser({ user }: { user: { name: string; email: string; avatar: string } }) { - const { isMobile } = useSidebar(); - - return ( - - - - - - - - {user.name.charAt(0)} - -
- {user.name} - {user.email} -
- -
-
- - - Logged in as - - - - - Profile - - - - Settings - - - - - Log out - - -
-
-
- ); -} - -export function AppSidebar({ ...props }: React.ComponentProps) { - return ( - - - - - - - - - - - - - - -
-
- -
- -

Game Library

-
-
-
-
-
- ); -} diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx deleted file mode 100644 index 49a6d9a..0000000 --- a/frontend/src/components/ui/alert.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { AlertTriangle, CheckCircle, Info, XCircle } from 'lucide-react'; - -import { cn } from '@/lib/utils'; - -const alertVariants = cva( - 'relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:-top-1 [&>svg]:-left-1 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:text-foreground', - { - variants: { - variant: { - default: 'bg-background text-foreground border', - destructive: - 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', - success: 'border-green-500/50 text-green-700 dark:border-green-500 [&>svg]:text-green-700', - warning: - 'border-yellow-500/50 text-yellow-700 dark:border-yellow-500 [&>svg]:text-yellow-700', - info: 'border-blue-500/50 text-blue-700 dark:border-blue-500 [&>svg]:text-blue-700', - }, - }, - defaultVariants: { - variant: 'default', - }, - } -); - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)); -Alert.displayName = 'Alert'; - -const AlertTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -AlertTitle.displayName = 'AlertTitle'; - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -AlertDescription.displayName = 'AlertDescription'; - -const AlertIcon = ({ variant }: { variant?: VariantProps['variant'] }) => { - switch (variant) { - case 'destructive': - return ; - case 'success': - return ; - case 'warning': - return ; - case 'info': - return ; - default: - return ; - } -}; - -export { Alert, AlertTitle, AlertDescription, AlertIcon }; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx deleted file mode 100644 index dcdd320..0000000 --- a/frontend/src/components/ui/badge.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; - -const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', - { - variants: { - variant: { - default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: - 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', - destructive: - 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', - outline: 'text-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - } -); - -export interface BadgeProps - extends React.HTMLAttributes, VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return
; -} - -export { Badge, badgeVariants }; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 765f33b..b5ea4ab 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,47 +1,64 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from '@/lib/utils'; +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, } -); +) -export interface ButtonProps - extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) } -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - return ( - - ); - } -); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 1a2c86c..681ad98 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -1,55 +1,92 @@ -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react" -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return (
) -); -Card.displayName = 'Card'; +} -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardHeader.displayName = 'CardHeader'; - -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -

) { + return ( +
) -); -CardTitle.displayName = 'CardTitle'; +} -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardDescription.displayName = 'CardDescription'; - -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) => ( -

+function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
) -); -CardContent.displayName = 'CardContent'; +} -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
) -); -CardFooter.displayName = 'CardFooter'; +} -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx deleted file mode 100644 index 317ca72..0000000 --- a/frontend/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import { Check } from 'lucide-react'; - -import { cn } from '@/lib/utils'; - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 04db187..8916905 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -1,23 +1,21 @@ -import * as React from 'react'; -import { cn } from '@/lib/utils'; +import * as React from "react" -export interface InputProps extends React.InputHTMLAttributes {} +import { cn } from "@/lib/utils" -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ); - } -); -Input.displayName = 'Input'; +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} -export { Input }; +export { Input } diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx deleted file mode 100644 index 86b32b7..0000000 --- a/frontend/src/components/ui/label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; -import * as LabelPrimitive from '@radix-ui/react-label'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@/lib/utils'; - -const labelVariants = cva( - 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' -); - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & VariantProps ->(({ className, ...props }, ref) => ( - -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/frontend/src/components/ui/scroll-area.tsx b/frontend/src/components/ui/scroll-area.tsx deleted file mode 100644 index 14d95e0..0000000 --- a/frontend/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; -import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; - -import { cn } from '@/lib/utils'; - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = 'vertical', ...props }, ref) => ( - - - -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx deleted file mode 100644 index 0a7dc17..0000000 --- a/frontend/src/components/ui/select.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import * as React from 'react'; -import * as SelectPrimitive from '@radix-ui/react-select'; -import { Check, ChevronDown, ChevronUp } from 'lucide-react'; - -import { cn } from '@/lib/utils'; - -const Select = SelectPrimitive.Root; - -const SelectGroup = SelectPrimitive.Group; - -const SelectValue = SelectPrimitive.Value; - -const SelectTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - span]:line-clamp-1', - className - )} - {...props} - > - {children} - - - - -)); -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; - -const SelectScrollUpButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; - -const SelectScrollDownButton = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; - -const SelectContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, position = 'popper', ...props }, ref) => ( - - - - - {children} - - - - -)); -SelectContent.displayName = SelectPrimitive.Content.displayName; - -const SelectLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SelectLabel.displayName = SelectPrimitive.Label.displayName; - -const SelectItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - - {children} - -)); -SelectItem.displayName = SelectPrimitive.Item.displayName; - -const SelectSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SelectSeparator.displayName = SelectPrimitive.Separator.displayName; - -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -}; diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx deleted file mode 100644 index ddc2083..0000000 --- a/frontend/src/components/ui/table.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; - -import { cn } from '@/lib/utils'; - -const Table = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- - - ) -); -Table.displayName = 'Table'; - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableHeader.displayName = 'TableHeader'; - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableBody.displayName = 'TableBody'; - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - tr]:last:border-b-0', className)} - {...props} - /> -)); -TableFooter.displayName = 'TableFooter'; - -const TableRow = React.forwardRef>( - ({ className, ...props }, ref) => ( - - ) -); -TableRow.displayName = 'TableRow'; - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableHead.displayName = 'TableHead'; - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableCell.displayName = 'TableCell'; - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableCaption.displayName = 'TableCaption'; - -export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx deleted file mode 100644 index b6d2be1..0000000 --- a/frontend/src/components/ui/textarea.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; - -import { cn } from '@/lib/utils'; - -export interface TextareaProps extends React.TextareaHTMLAttributes {} - -const Textarea = React.forwardRef( - ({ className, ...props }, ref) => { - return ( -