From 9f5569a8383a541289405b590cbe65a473a80807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benito=20Rodr=C3=ADguez?= Date: Thu, 19 Mar 2026 19:54:08 +0100 Subject: [PATCH] feat: update server port to 3003 and enhance logging; refactor frontend styles and components for improved UI/UX - Changed server port from 3000 to 3003 in backend. - Updated logging message for server startup. - Refactored global CSS styles for a neon theme with new color variables. - Introduced responsive typography and layout adjustments in frontend. - Added new components: EmptyState and GameCover for better game display. - Implemented loading states and error handling in the Home page. - Updated API base URL to match new server port. --- backend/src/index.ts | 9 +- frontend/src/app/globals.css | 521 +++++++++++++----- frontend/src/app/layout.tsx | 16 +- frontend/src/app/page.tsx | 265 ++++----- .../src/components/landing/EmptyState.tsx | 66 +++ frontend/src/components/landing/GameCover.tsx | 121 ++++ frontend/src/lib/api.ts | 2 +- 7 files changed, 726 insertions(+), 274 deletions(-) create mode 100644 frontend/src/components/landing/EmptyState.tsx create mode 100644 frontend/src/components/landing/GameCover.tsx diff --git a/backend/src/index.ts b/backend/src/index.ts index 3e90bc7..98a789b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,14 +3,15 @@ import { buildApp } from './app'; dotenv.config(); -const port = Number(process.env.PORT ?? 3000); +const port = Number(process.env.PORT ?? 3003); const app = buildApp(); const start = async () => { + const host = '0.0.0.0'; + try { - await app.listen({ port, host: '0.0.0.0' }); - // eslint-disable-next-line no-console - console.log(`Server listening on http://0.0.0.0:${port}`); + await app.listen({ port, host }); + console.log(`馃殌 Server ready and listening on http://${host}:${port}`); } catch (err) { app.log.error(err); process.exit(1); diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 112ea16..3db86c9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -7,8 +7,8 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-display: var(--font-display); + --font-mono: var(--font-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -48,123 +48,242 @@ } :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%); + /* Rich dark backgrounds */ + --background: #0a0a0a; + --foreground: #e5e5e5; + --card: #0f0f0f; + --card-foreground: #e5e5e5; + --popover: #0f0f0f; + --popover-foreground: #e5e5e5; + + /* Neon accents */ + --primary: #00f0ff; + --primary-foreground: #0a0a0a; + --secondary: #151515; + --secondary-foreground: #e5e5e5; + --muted: #151515; + --muted-foreground: #737373; + --accent: #b026ff; + --accent-foreground: #e5e5e5; + --destructive: #ff6b6b; + --border: #262626; + --input: #262626; --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); + + /* Chart colors */ + --chart-1: #00f0ff; + --chart-2: #b026ff; + --chart-3: #39ff14; + --chart-4: #ffd700; + --chart-5: #ff6b6b; + + /* Sidebar */ + --sidebar: #0f0f0f; + --sidebar-foreground: #e5e5e5; --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-primary-foreground: #0a0a0a; + --sidebar-accent: #151515; + --sidebar-accent-foreground: #e5e5e5; + --sidebar-border: #262626; --sidebar-ring: #00f0ff; + + /* Radius */ + --radius: 0.5rem; } .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%); + --background: #0a0a0a; + --foreground: #e5e5e5; + --card: #0f0f0f; + --card-foreground: #e5e5e5; + --popover: #0f0f0f; + --popover-foreground: #e5e5e5; + --primary: #00f0ff; + --primary-foreground: #0a0a0a; + --secondary: #151515; + --secondary-foreground: #e5e5e5; + --muted: #151515; + --muted-foreground: #737373; + --accent: #b026ff; + --accent-foreground: #e5e5e5; + --destructive: #ff6b6b; + --border: #262626; + --input: #262626; --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); + --chart-1: #00f0ff; + --chart-2: #b026ff; + --chart-3: #39ff14; + --chart-4: #ffd700; + --chart-5: #ff6b6b; + --sidebar: #0f0f0f; + --sidebar-foreground: #e5e5e5; --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-primary-foreground: #0a0a0a; + --sidebar-accent: #151515; + --sidebar-accent-foreground: #e5e5e5; + --sidebar-border: #262626; --sidebar-ring: #00f0ff; } -/* Mass Effect-inspired theme customizations */ +/* Custom neon accent colors */ :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); + --neon-cyan: #00f0ff; + --neon-purple: #b026ff; + --neon-lime: #39ff14; + --neon-gold: #ffd700; + --neon-coral: #ff6b6b; + + /* Glow effects */ + --glow-cyan: rgba(0, 240, 255, 0.4); + --glow-purple: rgba(176, 38, 255, 0.4); + --glow-lime: rgba(57, 255, 20, 0.4); + --glow-gold: rgba(255, 215, 0, 0.4); + --glow-coral: rgba(255, 107, 107, 0.4); + + /* Transitions */ + --transition-fast: 0.15s ease; + --transition-normal: 0.3s ease; + + /* Glassmorphism */ + --glass-bg: rgba(15, 15, 15, 0.8); + --glass-border: rgba(255, 255, 255, 0.1); } /* Glassmorphism effect */ .glass { background: var(--glass-bg); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid var(--glass-border); } /* Glow effects */ .glow-cyan { - box-shadow: 0 0 10px var(--mass-effect-cyan-glow); + box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2); } -.glow-cyan-intense { - box-shadow: 0 0 20px var(--mass-effect-cyan-glow), 0 0 40px var(--mass-effect-cyan); +.glow-purple { + box-shadow: 0 0 20px var(--glow-purple), 0 0 40px rgba(176, 38, 255, 0.2); +} + +.glow-lime { + box-shadow: 0 0 20px var(--glow-lime), 0 0 40px rgba(57, 255, 20, 0.2); } .glow-gold { - box-shadow: 0 0 10px var(--mass-effect-gold-glow); + box-shadow: 0 0 20px var(--glow-gold), 0 0 40px rgba(255, 215, 0, 0.2); } -/* Text effects */ +.glow-coral { + box-shadow: 0 0 20px var(--glow-coral), 0 0 40px rgba(255, 107, 107, 0.2); +} + +/* Text glow effects */ .text-glow-cyan { - text-shadow: 0 0 10px var(--mass-effect-cyan-glow); + text-shadow: 0 0 10px var(--glow-cyan), 0 0 20px rgba(0, 240, 255, 0.3); +} + +.text-glow-purple { + text-shadow: 0 0 10px var(--glow-purple), 0 0 20px rgba(176, 38, 255, 0.3); +} + +.text-glow-lime { + text-shadow: 0 0 10px var(--glow-lime), 0 0 20px rgba(57, 255, 20, 0.3); } .text-glow-gold { - text-shadow: 0 0 10px var(--mass-effect-gold-glow); + text-shadow: 0 0 10px var(--glow-gold), 0 0 20px rgba(255, 215, 0, 0.3); } -/* Holographic effect */ -.holographic { - position: relative; - overflow: hidden; +/* Gradient text */ +.gradient-text { + background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -.holographic::before { +.gradient-text-gold { + background: linear-gradient(135deg, var(--neon-gold), var(--neon-coral)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Card hover effects */ +.card-hover { + transition: transform var(--transition-fast), box-shadow var(--transition-normal); +} + +.card-hover:hover { + transform: translateY(-4px) scale(1.01); +} + +/* Thick bottom border for cards */ +.card-border-cyan { + border-bottom: 3px solid var(--neon-cyan); +} + +.card-border-purple { + border-bottom: 3px solid var(--neon-purple); +} + +.card-border-lime { + border-bottom: 3px solid var(--neon-lime); +} + +.card-border-gold { + border-bottom: 3px solid var(--neon-gold); +} + +.card-border-coral { + border-bottom: 3px solid var(--neon-coral); +} + +/* Category badges */ +.category-badge { + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +/* Staggered animation */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-up { + animation: fadeInUp 0.5s ease forwards; +} + +/* Pulse animation for system status */ +@keyframes pulse-glow { + 0%, 100% { + opacity: 1; + box-shadow: 0 0 10px var(--glow-cyan); + } + 50% { + opacity: 0.7; + box-shadow: 0 0 20px var(--glow-cyan), 0 0 30px rgba(0, 240, 255, 0.4); + } +} + +.pulse-glow { + animation: pulse-glow 2s infinite; +} + +/* Scanline effect */ +.scanline::before { content: ""; position: absolute; top: 0; @@ -174,13 +293,13 @@ background: linear-gradient( 90deg, transparent, - rgba(0, 240, 255, 0.2), + rgba(0, 240, 255, 0.1), transparent ); - animation: holographic-scan 3s infinite; + animation: scanline 3s infinite; } -@keyframes holographic-scan { +@keyframes scanline { 0% { left: -100%; } @@ -189,27 +308,6 @@ } } -/* 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; @@ -219,14 +317,14 @@ 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); + radial-gradient(1px 1px at 20px 30px, rgba(255,255,255,0.3), transparent), + radial-gradient(1px 1px at 40px 70px, rgba(255,255,255,0.2), transparent), + radial-gradient(1px 1px at 50px 50px, rgba(255,255,255,0.4), transparent), + radial-gradient(1px 1px at 80px 10px, rgba(255,255,255,0.2), transparent), + radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.3), transparent); background-repeat: repeat; background-size: 200px 200px; - opacity: 0.3; + opacity: 0.5; animation: starfield-move 120s linear infinite; } @@ -239,27 +337,21 @@ } } -/* 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; +/* Hover glow effect */ +.hover-glow:hover { + box-shadow: 0 0 20px var(--glow-cyan); + transform: translateY(-2px); + transition: all var(--transition-normal); +} + +/* Button hover effects */ +.btn-neon { position: relative; overflow: hidden; - transition: all 0.3s ease; + transition: all var(--transition-normal); } -.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 { +.btn-neon::before { content: ""; position: absolute; top: 0; @@ -275,23 +367,180 @@ transition: left 0.5s; } -.btn-mission:hover::before { +.btn-neon: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); +.btn-neon:hover { + transform: scale(1.02); + box-shadow: 0 0 20px var(--glow-cyan); } +/* Icon glow on hover */ +.icon-glow:hover { + filter: drop-shadow(0 0 8px var(--glow-cyan)); + transition: filter var(--transition-fast); +} + +/* Dense grid layout */ +.grid-dense { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +/* Bento grid layout */ +.bento-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; +} + +.bento-item-large { + grid-column: span 2; +} + +@media (max-width: 768px) { + .bento-item-large { + grid-column: span 1; + } +} + +/* Responsive typography */ +.text-responsive-xl { + font-size: clamp(1.5rem, 4vw, 2rem); +} + +.text-responsive-2xl { + font-size: clamp(2rem, 5vw, 3rem); +} + +.text-responsive-3xl { + font-size: clamp(2.5rem, 6vw, 4rem); +} + +/* Number display for stats */ +.stat-number { + font-family: var(--font-mono); + font-weight: 700; + letter-spacing: -0.02em; +} + +/* Gradient border */ +.gradient-border { + position: relative; +} + +.gradient-border::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple)); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +/* Base styles */ @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; - font-family: 'Inter', sans-serif; + font-family: var(--font-display), sans-serif; overflow-x: hidden; } -} \ No newline at end of file + + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display), sans-serif; + } + + code, pre, .mono { + font-family: var(--font-mono), monospace; + } + + /* Game Cover Styles */ + .game-cover-wrapper { + animation: fadeInUp 0.5s ease forwards; + opacity: 0; + } + + .game-cover-wrapper:hover { + transform: translateY(-4px) scale(1.02); + transition: transform 0.2s ease-out; + z-index: 10; + } + + .game-cover-image img { + transition: transform 0.3s ease-out, filter 0.3s ease-out; + } + + /* Discovery Wall Grid - Dense Masonry Layout */ + .discovery-wall { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0; + margin: 0 -1px; + } + + @media (min-width: 640px) { + .discovery-wall { + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + } + } + + @media (min-width: 1024px) { + .discovery-wall { + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + } + } + + /* Empty State Animation */ + @keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } + } + + .empty-icon { + animation: float 3s ease-in-out infinite; + } + + /* Pulse animation for empty state button */ + @keyframes neon-pulse { + 0%, 100% { + box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2); + } + 50% { + box-shadow: 0 0 30px var(--glow-cyan), 0 0 60px rgba(0, 240, 255, 0.4); + } + } + + .btn-neon-pulse:hover { + animation: neon-pulse 1.5s infinite; + } + + /* Section header with gradient line */ + .section-header { + position: relative; + padding-bottom: 0.5rem; + } + + .section-header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 3px; + background: linear-gradient(90deg, var(--neon-cyan), var(--neon-purple)); + border-radius: 2px; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 3ff9cfd..9dee455 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,15 +1,17 @@ import type { Metadata } from 'next'; -import { Geist, Geist_Mono } from 'next/font/google'; +import { Space_Grotesk, JetBrains_Mono } from 'next/font/google'; import './globals.css'; -const geistSans = Geist({ - variable: '--font-geist-sans', +const spaceGrotesk = Space_Grotesk({ + variable: '--font-display', subsets: ['latin'], + display: 'swap', }); -const geistMono = Geist_Mono({ - variable: '--font-geist-mono', +const jetBrainsMono = JetBrains_Mono({ + variable: '--font-mono', subsets: ['latin'], + display: 'swap', }); export const metadata: Metadata = { @@ -32,7 +34,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index de7de3c..f11c62e 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,145 +1,156 @@ -import Link from 'next/link'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; +'use client'; + +import { useEffect, useState } from 'react'; +import { Game } from '@/lib/api'; +import { gamesApi } from '@/lib/api'; +import { Gamepad2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -import { Gamepad2, HardDrive, Import, Clock, Activity, Database } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { GameCover } from '@/components/landing/GameCover'; +import { EmptyState } from '@/components/landing/EmptyState'; +import { ArrowRight, Grid3X3 } from 'lucide-react'; export default function Home() { + const [games, setGames] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchGames() { + try { + const allGames = await gamesApi.getAll(); + // Ordenar por fecha de creaci贸n (m谩s recientes primero) + const sortedGames = allGames.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + // Limitar a los 煤ltimos 16 juegos para la p谩gina principal + setGames(sortedGames.slice(0, 16)); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Error desconocido'; + setError(`Error al cargar juegos: ${errorMessage}`); + console.error('Error fetching games:', err); + } finally { + setIsLoading(false); + } + } + + fetchGames(); + }, []); + return (
+ {/* Starfield background */} +
+ {/* Header */} -
+
-
- -

Quasar

+ +
+ +
+
+

QUASAR

+ + GAME LIBRARY SYSTEM + +
+ +
+ + + + + v1.0.0 +
- v1.0.0
{/* Main Content */} -
-
- {/* Statistics Cards */} -
-

- - Estad铆sticas -

-
- - - Total de Juegos - 0 - - -
- - Base de datos -
-
-
- - - - Juegos Importados - 0 - - -
- - Desde archivos ROM -
-
-
- - - - 脷ltima Importaci贸n - - - - -
- - Sin registros -
-
-
- - - - Estado del Sistema - - Activo - - - -
- - Operativo -
-
-
+
+ {isLoading ? ( +
+
+
+

CARGANDO...

-
- - {/* Quick Actions */} -
-

- - Acciones R谩pidas -

-
- - - Gesti贸n de Juegos - Ver y administrar tu biblioteca de videojuegos - - - - - - - - - - - Importar Juegos - Importa juegos desde archivos ROM - - - - - - - +
+ ) : error ? ( +
+
+

{error}

+
-
- - {/* Recent Activity */} -
-

- - Actividad Reciente -

- - -
- -

No hay actividad reciente

-

Las importaciones y cambios aparecer谩n aqu铆

+
+ ) : games.length === 0 ? ( + + ) : ( +
+ {/* Section Header */} +
+
+
+

+ 脷LTIMOS JUEGOS +

+

+ {games.length} JUEGOS EN TU BIBLIOTECA +

- - -
-
+ + + +
+
+ + {/* Discovery Wall - Dense Grid */} +
+ {games.map((game, index) => ( + + ))} +
+ + {/* Load More Button */} + {games.length >= 16 && ( +
+ + + +
+ )} + + )} ); diff --git a/frontend/src/components/landing/EmptyState.tsx b/frontend/src/components/landing/EmptyState.tsx new file mode 100644 index 0000000..720a372 --- /dev/null +++ b/frontend/src/components/landing/EmptyState.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Gamepad2, Sparkles, ArrowRight } from 'lucide-react'; +import Link from 'next/link'; + +export function EmptyState() { + return ( +
+ {/* Icono animado */} +
+
+
+ +
+ {/* Sparkles decorativos */} + + +
+ + {/* T铆tulo principal */} +

+ TU BIBLIOTECA +
+ EST脕 VAC脥A +

+ + {/* Descripci贸n motivadora */} +

+ Es hora de comenzar tu colecci贸n de videojuegos. +
+ + Importa tus ROMs o a帽ade juegos manualmente + +

+ + {/* Botones de acci贸n */} +
+ + + + + + + +
+ + {/* Info adicional */} +
+

+ Compatible con archivos ROM, IGDB, RAWG y TheGamesDB +

+
+
+ ); +} diff --git a/frontend/src/components/landing/GameCover.tsx b/frontend/src/components/landing/GameCover.tsx new file mode 100644 index 0000000..f50ba15 --- /dev/null +++ b/frontend/src/components/landing/GameCover.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { Game } from '@/lib/api'; +import { Badge } from '@/components/ui/badge'; +import { ExternalLink } from 'lucide-react'; +import Link from 'next/link'; +import { useState } from 'react'; + +interface GameCoverProps { + game: Game; + index: number; +} + +export function GameCover({ game, index }: GameCoverProps) { + const [isHovered, setIsHovered] = useState(false); + + // Colores de acento por fuente + const sourceColors: Record = { + igdb: { bg: 'rgba(0, 240, 255, 0.15)', text: '#00f0ff', border: '#00f0ff' }, + rawg: { bg: 'rgba(176, 38, 255, 0.15)', text: '#b026ff', border: '#b026ff' }, + thegamesdb: { bg: 'rgba(57, 255, 20, 0.15)', text: '#39ff14', border: '#39ff14' }, + rom: { bg: 'rgba(255, 215, 0, 0.15)', text: '#ffd700', border: '#ffd700' }, + manual: { bg: 'rgba(255, 107, 107, 0.15)', text: '#ff6b6b', border: '#ff6b6b' }, + }; + + const colors = sourceColors[game.source] || sourceColors.manual; + + // URL de la portada o placeholder + const coverUrl = game.cover || '/placeholder-game-cover.png'; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Imagen de portada */} +
+ {game.title} +
+ + {/* Overlay en hover */} +
+ {/* Badge de fuente */} +
+ + {game.source} + +
+ + {/* Contenido del overlay */} +
+ {/* T铆tulo del juego */} +

+ {game.title} +

+ + {/* Metadatos */} +
+ {game.year && ( + + {game.year} + + )} + {game.platform && ( + + {game.platform} + + )} + {game.genre && ( + + {game.genre} + + )} +
+ + {/* Indicador de enlace */} +
+ + VER DETALLES +
+
+
+ + {/* Glow effect en hover */} +
+
+
+ + {/* Borde brillante en hover */} +
+ + ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1f37b94..2882abd 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,4 @@ -const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; +const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3003/api'; // Tipos para relaciones del juego export interface Artwork {