feat: Refactor Games and Import pages with new Navbar and improved UI
Some checks failed
CI / lint (push) Failing after 11s
CI / test-backend (push) Has been skipped
CI / test-frontend (push) Has been skipped
CI / test-e2e (push) Has been skipped

- Removed ImportSheet component from GamesPage and integrated Navbar for consistent navigation.
- Enhanced layout with a starfield background and responsive design elements.
- Updated GameTable and GameCard components for better game display.
- Added loading indicators and improved empty state messages.
- Refactored ImportPage to include Navbar and updated styling for import settings.
- Introduced SettingsPage for configuring ROM directory with a new glassmorphic design.
- Updated Navbar component to streamline navigation and improve mobile responsiveness.
- Added glassmorphic styles to enhance UI aesthetics.
This commit is contained in:
2026-03-21 17:34:58 +01:00
parent 9f5569a838
commit 5eaf320fc5
7 changed files with 706 additions and 505 deletions

View File

@@ -18,6 +18,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import Navbar from '@/components/landing/Navbar';
export default function GameDetailPage() { export default function GameDetailPage() {
const params = useParams(); const params = useParams();
@@ -79,26 +80,57 @@ export default function GameDetailPage() {
if (loading) { if (loading) {
return ( return (
<div className="container mx-auto py-8"> <div className="min-h-screen bg-background">
<div className="starfield" />
<Navbar />
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="text-muted-foreground">Cargando juego...</div> <div className="text-center">
<div className="w-16 h-16 border-4 border-[var(--neon-cyan)]/20 border-t-[var(--neon-cyan)] rounded-full animate-spin mx-auto mb-4" />
<p className="text-muted-foreground mono text-sm tracking-wider">
CARGANDO JUEGO...
</p>
</div> </div>
</div> </div>
</div>
</main>
</div>
); );
} }
if (error || !game) { if (error || !game) {
return ( return (
<div className="container mx-auto py-8"> <div className="min-h-screen bg-background">
<div className="starfield" />
<Navbar />
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="text-destructive">{error || 'Juego no encontrado'}</div> <div className="text-center">
<p className="text-[var(--neon-coral)] mono text-sm tracking-wider mb-4">
{error || 'JUEGO NO ENCONTRADO'}
</p>
<Button
onClick={() => router.push('/games')}
className="btn-neon bg-[var(--neon-cyan)] text-background hover:bg-[var(--neon-cyan)]/90 transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_0_15px_rgba(34,211,238,0.4)]"
>
<span className="mono text-xs tracking-wider">VOLVER A JUEGOS</span>
</Button>
</div> </div>
</div> </div>
</div>
</main>
</div>
); );
} }
return ( return (
<div className="container mx-auto py-8"> <div className="min-h-screen bg-background">
<div className="starfield" />
<Navbar />
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -107,24 +139,35 @@ export default function GameDetailPage() {
<ArrowLeftIcon className="size-4" /> <ArrowLeftIcon className="size-4" />
</Button> </Button>
<div> <div>
<h1 className="text-3xl font-bold">{game.title}</h1> <h1 className="text-responsive-3xl font-bold mb-2">
<p className="text-muted-foreground">{game.slug}</p> <span className="gradient-text">{game.title.toUpperCase()}</span>
</h1>
<p className="text-muted-foreground mono text-sm tracking-wider">{game.slug}</p>
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={loadGame} disabled={refreshing}> <Button
variant="outline"
onClick={loadGame}
disabled={refreshing}
className="transition-all duration-300 hover:translate-y-[-2px]"
>
<RefreshCwIcon className={`size-4 ${refreshing ? 'animate-spin' : ''}`} /> <RefreshCwIcon className={`size-4 ${refreshing ? 'animate-spin' : ''}`} />
</Button> </Button>
<Button variant="destructive" onClick={() => setDeleteDialogOpen(true)}> <Button
Eliminar Juego variant="destructive"
onClick={() => setDeleteDialogOpen(true)}
className="transition-all duration-300 hover:translate-y-[-2px]"
>
<span className="mono text-xs tracking-wider">ELIMINAR JUEGO</span>
</Button> </Button>
</div> </div>
</div> </div>
{/* Game Info */} {/* Game Info */}
<Card> <Card className="glassmorphic">
<CardHeader> <CardHeader>
<CardTitle>Información del Juego</CardTitle> <CardTitle className="mono text-sm tracking-wider">INFORMACIÓN DEL JUEGO</CardTitle>
<CardDescription>Detalles y metadatos</CardDescription> <CardDescription>Detalles y metadatos</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -133,54 +176,91 @@ export default function GameDetailPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarIcon className="size-4 text-muted-foreground" /> <CalendarIcon className="size-4 text-muted-foreground" />
<span className="text-sm"> <span className="text-sm">
<span className="text-muted-foreground">Fecha de lanzamiento:</span>{' '} <span className="text-muted-foreground mono">FECHA DE LANZAMIENTO:</span>{' '}
{formatDate(game.releaseDate)} {formatDate(game.releaseDate)}
</span> </span>
</div> </div>
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="secondary">Fuente: {game.source}</Badge> <Badge
variant="secondary"
className="mono text-xs tracking-wider"
style={{
backgroundColor: 'var(--neon-purple)/20',
color: 'var(--neon-purple)',
}}
>
FUENTE: {game.source.toUpperCase()}
</Badge>
</div> </div>
{game.genre && ( {game.genre && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Género: {game.genre}</Badge> <Badge
variant="outline"
className="mono text-xs tracking-wider"
style={{ borderColor: 'var(--neon-cyan)', color: 'var(--neon-cyan)' }}
>
GÉNERO: {game.genre.toUpperCase()}
</Badge>
</div> </div>
)} )}
{game.platform && ( {game.platform && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Plataforma: {game.platform}</Badge> <Badge
variant="outline"
className="mono text-xs tracking-wider"
style={{ borderColor: 'var(--neon-lime)', color: 'var(--neon-lime)' }}
>
PLATAFORMA: {game.platform.toUpperCase()}
</Badge>
</div> </div>
)} )}
{game.year && ( {game.year && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Año: {game.year}</Badge> <Badge
variant="outline"
className="mono text-xs tracking-wider"
style={{ borderColor: 'var(--neon-gold)', color: 'var(--neon-gold)' }}
>
AÑO: {game.year}
</Badge>
</div> </div>
)} )}
{game.sourceId && ( {game.sourceId && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">ID de fuente: {game.sourceId}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
ID DE FUENTE: {game.sourceId}
</Badge>
</div> </div>
)} )}
{game.igdbId && ( {game.igdbId && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">IGDB ID: {game.igdbId}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
IGDB ID: {game.igdbId}
</Badge>
</div> </div>
)} )}
{game.rawgId && ( {game.rawgId && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">RAWG ID: {game.rawgId}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
RAWG ID: {game.rawgId}
</Badge>
</div> </div>
)} )}
{game.thegamesdbId && ( {game.thegamesdbId && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">TheGamesDB ID: {game.thegamesdbId}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
THEGAMESDB ID: {game.thegamesdbId}
</Badge>
</div> </div>
)} )}
</div> </div>
{game.description && ( {game.description && (
<div className="mt-4"> <div className="mt-4">
<h3 className="font-semibold mb-2">Descripción</h3> <h3 className="font-semibold mb-2 mono text-sm tracking-wider">DESCRIPCIÓN</h3>
<p className="text-sm text-muted-foreground">{game.description}</p> <p className="text-sm text-muted-foreground leading-relaxed">
{game.description}
</p>
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -188,9 +268,11 @@ export default function GameDetailPage() {
{/* ROM Info (if source = rom) */} {/* ROM Info (if source = rom) */}
{game.source === 'rom' && ( {game.source === 'rom' && (
<Card> <Card className="glassmorphic">
<CardHeader> <CardHeader>
<CardTitle>Información del Archivo ROM</CardTitle> <CardTitle className="mono text-sm tracking-wider">
INFORMACIÓN DEL ARCHIVO ROM
</CardTitle>
<CardDescription>Detalles del archivo importado</CardDescription> <CardDescription>Detalles del archivo importado</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -199,7 +281,8 @@ export default function GameDetailPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileIcon className="size-4 text-muted-foreground" /> <FileIcon className="size-4 text-muted-foreground" />
<span className="text-sm"> <span className="text-sm">
<span className="text-muted-foreground">Archivo:</span> {game.romFilename} <span className="text-muted-foreground mono">ARCHIVO:</span>{' '}
{game.romFilename}
</span> </span>
</div> </div>
)} )}
@@ -207,23 +290,33 @@ export default function GameDetailPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileIcon className="size-4 text-muted-foreground" /> <FileIcon className="size-4 text-muted-foreground" />
<span className="text-sm"> <span className="text-sm">
<span className="text-muted-foreground">Ruta:</span> {game.romPath} <span className="text-muted-foreground mono">RUTA:</span> {game.romPath}
</span> </span>
</div> </div>
)} )}
{game.romSize && ( {game.romSize && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Tamaño: {formatFileSize(game.romSize)}</Badge> <Badge
variant="outline"
className="mono text-xs tracking-wider"
style={{ borderColor: 'var(--neon-coral)', color: 'var(--neon-coral)' }}
>
TAMAÑO: {formatFileSize(game.romSize)}
</Badge>
</div> </div>
)} )}
{game.romFormat && ( {game.romFormat && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Formato: {game.romFormat}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
FORMATO: {game.romFormat.toUpperCase()}
</Badge>
</div> </div>
)} )}
{game.romChecksum && ( {game.romChecksum && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline">Checksum: {game.romChecksum}</Badge> <Badge variant="outline" className="mono text-xs tracking-wider">
CHECKSUM: {game.romChecksum}
</Badge>
</div> </div>
)} )}
</div> </div>
@@ -236,23 +329,29 @@ export default function GameDetailPage() {
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle> <AlertDialogTitle className="mono text-sm tracking-wider">
¿ELIMINAR JUEGO?
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
¿Estás seguro de que deseas eliminar &ldquo;{game.title}&rdquo;? Esta acción no se ¿Estás seguro de que deseas eliminar &ldquo;{game.title}&rdquo;? Esta acción no se
puede deshacer. puede deshacer.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel className="mono text-xs tracking-wider">
CANCELAR
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={handleDeleteGame} onClick={handleDeleteGame}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" className="bg-destructive text-destructive-foreground hover:bg-destructive/90 mono text-xs tracking-wider transition-all duration-300 hover:translate-y-[-2px]"
> >
Eliminar ELIMINAR
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
</main>
</div>
); );
} }

View File

@@ -6,7 +6,6 @@ import { Game, gamesApi } from '@/lib/api';
import { GameTable } from '@/components/games/GameTable'; import { GameTable } from '@/components/games/GameTable';
import { GameDialog } from '@/components/games/GameDialog'; import { GameDialog } from '@/components/games/GameDialog';
import { GameFilters } from '@/components/games/GameFilters'; import { GameFilters } from '@/components/games/GameFilters';
import { ImportSheet } from '@/components/games/ImportSheet';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { PlusIcon, LayoutGridIcon, TableIcon } from 'lucide-react'; import { PlusIcon, LayoutGridIcon, TableIcon } from 'lucide-react';
import { import {
@@ -19,6 +18,8 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import Navbar from '@/components/landing/Navbar';
import { GameCard } from '@/components/games/GameCard';
export default function GamesPage() { export default function GamesPage() {
const router = useRouter(); const router = useRouter();
@@ -96,22 +97,35 @@ export default function GamesPage() {
}; };
return ( return (
<div className="container mx-auto py-8"> <div className="min-h-screen bg-background">
{/* Starfield background */}
<div className="starfield" />
{/* Navbar */}
<Navbar />
{/* Main Content */}
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold">Gestión de Videojuegos</h1> <h1 className="text-responsive-3xl font-bold mb-2">
<p className="text-muted-foreground"> <span className="gradient-text">GESTIÓN DE VIDEOJUEGOS</span>
{filteredGames.length} {filteredGames.length === 1 ? 'juego' : 'juegos'} en tu </h1>
biblioteca <p className="text-muted-foreground mono text-sm tracking-wider">
{filteredGames.length} {filteredGames.length === 1 ? 'JUEGO' : 'JUEGOS'} EN TU
BIBLIOTECA
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<ImportSheet onSuccess={loadGames} /> <Button
<Button onClick={handleCreate}> onClick={handleCreate}
className="btn-neon bg-[var(--neon-cyan)] text-background hover:bg-[var(--neon-cyan)]/90 transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_0_15px_rgba(34,211,238,0.4)]"
>
<PlusIcon data-icon="inline-start" /> <PlusIcon data-icon="inline-start" />
Nuevo Juego <span className="mono text-xs tracking-wider ml-2">NUEVO JUEGO</span>
</Button> </Button>
</div> </div>
</div> </div>
@@ -129,6 +143,7 @@ export default function GamesPage() {
variant={viewMode === 'table' ? 'default' : 'outline'} variant={viewMode === 'table' ? 'default' : 'outline'}
size="icon" size="icon"
onClick={() => setViewMode('table')} onClick={() => setViewMode('table')}
className="transition-all duration-300 hover:translate-y-[-2px]"
> >
<TableIcon className="size-4" /> <TableIcon className="size-4" />
</Button> </Button>
@@ -136,6 +151,7 @@ export default function GamesPage() {
variant={viewMode === 'grid' ? 'default' : 'outline'} variant={viewMode === 'grid' ? 'default' : 'outline'}
size="icon" size="icon"
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className="transition-all duration-300 hover:translate-y-[-2px]"
> >
<LayoutGridIcon className="size-4" /> <LayoutGridIcon className="size-4" />
</Button> </Button>
@@ -144,7 +160,12 @@ export default function GamesPage() {
{/* Content */} {/* Content */}
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="text-muted-foreground">Cargando juegos...</div> <div className="text-center">
<div className="w-16 h-16 border-4 border-[var(--neon-cyan)]/20 border-t-[var(--neon-cyan)] rounded-full animate-spin mx-auto mb-4" />
<p className="text-muted-foreground mono text-sm tracking-wider">
CARGANDO JUEGOS...
</p>
</div>
</div> </div>
) : viewMode === 'table' ? ( ) : viewMode === 'table' ? (
<GameTable <GameTable
@@ -154,7 +175,7 @@ export default function GamesPage() {
onDelete={handleDelete} onDelete={handleDelete}
/> />
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="discovery-wall">
{filteredGames.map((game) => ( {filteredGames.map((game) => (
<GameCard <GameCard
key={game.id} key={game.id}
@@ -170,15 +191,18 @@ export default function GamesPage() {
{/* Empty State */} {/* Empty State */}
{!loading && filteredGames.length === 0 && ( {!loading && filteredGames.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mono text-sm tracking-wider mb-4">
{searchQuery {searchQuery
? 'No se encontraron juegos que coincidan con tu búsqueda.' ? 'NO SE ENCONTRARON JUEGOS QUE COINCIDAN CON TU BÚSQUEDA.'
: 'No hay juegos en tu biblioteca.'} : 'NO HAY JUEGOS EN TU BIBLIOTECA.'}
</p> </p>
{!searchQuery && ( {!searchQuery && (
<Button onClick={handleCreate}> <Button
onClick={handleCreate}
className="btn-neon bg-[var(--neon-cyan)] text-background hover:bg-[var(--neon-cyan)]/90 transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_0_15px_rgba(34,211,238,0.4)]"
>
<PlusIcon data-icon="inline-start" /> <PlusIcon data-icon="inline-start" />
Agregar primer juego <span className="mono text-xs tracking-wider ml-2">AGREGAR PRIMER JUEGO</span>
</Button> </Button>
)} )}
</div> </div>
@@ -199,8 +223,8 @@ export default function GamesPage() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle> <AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
¿Estás seguro de que deseas eliminar "{gameToDelete?.title}"? Esta acción no se puede ¿Estás seguro de que deseas eliminar &ldquo;{gameToDelete?.title}&rdquo;? Esta
deshacer. acción no se puede deshacer.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -215,5 +239,7 @@ export default function GamesPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
</main>
</div>
); );
} }

View File

@@ -157,6 +157,13 @@
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
} }
.glassmorphic {
background: rgba(15, 15, 15, 0.6);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Glow effects */ /* Glow effects */
.glow-cyan { .glow-cyan {
box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2); box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2);

View File

@@ -16,6 +16,7 @@ import {
ArrowLeftIcon, ArrowLeftIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Navbar from '@/components/landing/Navbar';
export default function ImportPage() { export default function ImportPage() {
const router = useRouter(); const router = useRouter();
@@ -56,32 +57,47 @@ export default function ImportPage() {
}; };
return ( return (
<div className="container mx-auto py-8"> <div className="min-h-screen bg-background">
<div className="starfield" />
<Navbar />
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" onClick={() => router.back()}> <Button
variant="ghost"
size="icon"
onClick={() => router.back()}
className="transition-all duration-300 hover:translate-y-[-2px]"
>
<ArrowLeftIcon className="size-4" /> <ArrowLeftIcon className="size-4" />
</Button> </Button>
<div> <div>
<h1 className="text-3xl font-bold">Importar Juegos</h1> <h1 className="text-responsive-3xl font-bold mb-2">
<p className="text-muted-foreground"> <span className="gradient-text">IMPORTAR JUEGOS</span>
</h1>
<p className="text-muted-foreground mono text-sm tracking-wider">
Importa juegos desde archivos ROM en un directorio local Importa juegos desde archivos ROM en un directorio local
</p> </p>
</div> </div>
</div> </div>
{/* Import Form */} {/* Import Form */}
<Card> <Card className="glassmorphic">
<CardHeader> <CardHeader>
<CardTitle>Configuración de Importación</CardTitle> <CardTitle className="mono text-sm tracking-wider">
CONFIGURACIÓN DE IMPORTACIÓN
</CardTitle>
<CardDescription> <CardDescription>
Especifica el directorio que contiene los archivos ROM que deseas importar Especifica el directorio que contiene los archivos ROM que deseas importar
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="directory">Directorio *</Label> <Label htmlFor="directory" className="mono text-xs tracking-wider">
DIRECTORIO *
</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="directory" id="directory"
@@ -208,9 +224,13 @@ export default function ImportPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="text-sm text-muted-foreground space-y-1"> <ul className="text-sm text-muted-foreground space-y-1">
<li> El sistema escaneará el directorio especificado en busca de archivos ROM</li> <li>
El sistema escaneará el directorio especificado en busca de archivos ROM
</li>
<li> Se calcularán los checksums (CRC32, MD5, SHA1) para cada archivo</li> <li> Se calcularán los checksums (CRC32, MD5, SHA1) para cada archivo</li>
<li> Las ROMs se asociarán automáticamente con juegos existentes si coinciden</li> <li>
Las ROMs se asociarán automáticamente con juegos existentes si coinciden
</li>
<li> Los archivos duplicados se detectarán y omitirán</li> <li> Los archivos duplicados se detectarán y omitirán</li>
<li> La importación puede tardar dependiendo de la cantidad de archivos</li> <li> La importación puede tardar dependiendo de la cantidad de archivos</li>
</ul> </ul>
@@ -218,5 +238,7 @@ export default function ImportPage() {
</Card> </Card>
</div> </div>
</div> </div>
</main>
</div>
); );
} }

View File

@@ -10,6 +10,7 @@ import Link from 'next/link';
import { GameCover } from '@/components/landing/GameCover'; import { GameCover } from '@/components/landing/GameCover';
import { EmptyState } from '@/components/landing/EmptyState'; import { EmptyState } from '@/components/landing/EmptyState';
import { ArrowRight, Grid3X3 } from 'lucide-react'; import { ArrowRight, Grid3X3 } from 'lucide-react';
import Navbar from '@/components/landing/Navbar';
export default function Home() { export default function Home() {
const [games, setGames] = useState<Game[]>([]); const [games, setGames] = useState<Game[]>([]);
@@ -43,43 +44,11 @@ export default function Home() {
{/* Starfield background */} {/* Starfield background */}
<div className="starfield" /> <div className="starfield" />
{/* Header */} {/* Navbar */}
<header className="glass sticky top-0 z-50 border-b border-border/50"> <Navbar />
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3">
<div className="relative">
<Gamepad2 className="w-8 h-8 text-[var(--neon-cyan)] icon-glow" />
</div>
<div>
<h1 className="text-responsive-xl font-bold gradient-text tracking-tight">QUASAR</h1>
<span className="text-xs text-muted-foreground mono tracking-wider">
GAME LIBRARY SYSTEM
</span>
</div>
</Link>
<div className="flex items-center gap-3">
<Link href="/games">
<Button
variant="ghost"
size="sm"
className="text-[var(--neon-cyan)] hover:bg-[var(--neon-cyan)]/10 hover:text-[var(--neon-cyan)]"
>
<Grid3X3 className="w-4 h-4 mr-2" />
<span className="mono text-xs tracking-wider">VER TODOS</span>
</Button>
</Link>
<Badge
variant="outline"
className="category-badge border-[var(--neon-purple)] text-[var(--neon-purple)] hover-glow"
>
v1.0.0
</Badge>
</div>
</div>
</header>
{/* Main Content */} {/* Main Content */}
<main className="relative z-10"> <main className="relative z-10 pt-20">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center"> <div className="text-center">

View File

@@ -0,0 +1,103 @@
'use client';
import { useState } from 'react';
import Navbar from '@/components/landing/Navbar';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { FolderIcon, SaveIcon } from 'lucide-react';
export default function SettingsPage() {
const [romPath, setRomPath] = useState('');
const [saved, setSaved] = useState(false);
const handleSave = () => {
// Aquí se implementaría la lógica para guardar la configuración
console.log('Guardando path:', romPath);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div className="min-h-screen bg-background">
{/* Starfield background */}
<div className="starfield" />
{/* Navbar */}
<Navbar />
{/* Main Content */}
<main className="relative z-10 pt-20">
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-responsive-3xl font-bold mb-2">
<span className="gradient-text">CONFIGURACIÓN</span>
</h1>
<p className="text-muted-foreground mono text-sm tracking-wider">
CONFIGURA EL DIRECTORIO DE TUS ROMS
</p>
</div>
{/* Settings Card */}
<Card className="glassmorphic-card border-white/10 bg-black/40 backdrop-blur-md">
<CardHeader>
<CardTitle className="mono text-lg tracking-wider flex items-center gap-2">
<FolderIcon className="size-5 text-[var(--neon-purple)]" />
DIRECTORIO DE ROMS
</CardTitle>
<CardDescription className="mono text-xs tracking-wider text-white/60">
ESPECIFICA LA RUTA DEL DIRECTORIO DONDE SE ALMACENAN TUS ARCHIVOS DE JUEGO
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Path Input */}
<div className="space-y-2">
<Label
htmlFor="rom-path"
className="mono text-xs tracking-wider text-white/80 uppercase"
>
Path del Directorio
</Label>
<Input
id="rom-path"
type="text"
placeholder="/path/to/roms"
value={romPath}
onChange={(e) => setRomPath(e.target.value)}
className="mono text-sm bg-black/60 border-white/20 text-white placeholder:text-white/40 focus:border-[var(--neon-purple)] focus:ring-[var(--neon-purple)]/20 transition-all duration-300"
/>
</div>
{/* Save Button */}
<div className="flex justify-end">
<Button
onClick={handleSave}
disabled={!romPath.trim()}
className="btn-neon bg-[var(--neon-purple)] text-background hover:bg-[var(--neon-purple)]/90 transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_0_15px_rgba(192,132,252,0.4)] disabled:opacity-50 disabled:cursor-not-allowed disabled:translate-y-0 disabled:shadow-none"
>
<SaveIcon data-icon="inline-start" />
<span className="mono text-xs tracking-wider ml-2">
{saved ? 'GUARDADO' : 'GUARDAR'}
</span>
</Button>
</div>
{/* Info Box */}
<div className="mt-6 p-4 rounded-lg bg-white/5 border border-white/10">
<p className="mono text-xs text-white/60 leading-relaxed">
<span className="text-[var(--neon-cyan)]">NOTA:</span> El directorio debe ser
accesible desde el servidor backend. Asegúrate de que la ruta sea correcta y
tenga los permisos necesarios para leer los archivos.
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
</div>
);
}

View File

@@ -1,60 +1,45 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
const Navbar = () => { const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const toggleMenu = () => { const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen); setIsMenuOpen(!isMenuOpen);
}; };
return ( return (
<nav className="fixed top-0 left-0 right-0 z-50 glass"> <nav className="fixed top-0 left-0 right-0 z-50 backdrop-blur-md bg-black/70 border-b border-white/10">
<div className="container mx-auto px-4 py-4"> <div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Logo */} {/* Logo */}
<div className="flex items-center space-x-2"> <Link href="/" className="flex items-center space-x-2">
<h1 <h1
className="text-2xl font-bold text-glow-cyan" className="text-2xl font-bold text-glow-cyan hover:scale-105 transition-transform duration-200 cursor-pointer"
style={{ color: 'var(--mass-effect-cyan)' }} style={{ color: 'var(--mass-effect-cyan)' }}
> >
QUASAR QUASAR
</h1> </h1>
</div> </Link>
{/* Search Bar - Desktop */}
<div className="hidden md:flex flex-1 max-w-md mx-8">
<Input
type="text"
placeholder="SEARCH GAMES..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-glow bg-transparent border border-gray-600 text-white placeholder-gray-400"
style={{ borderColor: 'var(--mass-effect-cyan)' }}
aria-label="Campo de búsqueda de juegos"
/>
</div>
{/* Navigation Links - Desktop */} {/* Navigation Links - Desktop */}
<div className="hidden md:flex items-center space-x-6"> <div className="hidden md:flex items-center space-x-4">
<a <Link
href="/games" href="/games"
className="text-white hover:text-glow-cyan transition-colors" className="font-mono text-sm text-white/80 hover:text-cyan-400 transition-all duration-300 hover:translate-y-[-2px]"
style={{ textShadow: '0 0 5px var(--mass-effect-cyan-glow)' }} style={{ textShadow: '0 0 10px rgba(34, 211, 238, 0.3)' }}
> >
GAMES GAMES
</a> </Link>
<a <Link
href="/import" href="/settings"
className="text-white hover:text-glow-cyan transition-colors" className="font-mono text-sm text-white/80 hover:text-purple-400 transition-all duration-300 hover:translate-y-[-2px]"
style={{ textShadow: '0 0 5px var(--mass-effect-cyan-glow)' }} style={{ textShadow: '0 0 10px rgba(192, 132, 252, 0.3)' }}
> >
IMPORT SETTINGS
</a> </Link>
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@@ -94,35 +79,25 @@ const Navbar = () => {
{/* Mobile Menu */} {/* Mobile Menu */}
{isMenuOpen && ( {isMenuOpen && (
<div className="md:hidden mt-4 glass rounded-lg p-4"> <div className="md:hidden mt-4 backdrop-blur-md bg-black/70 border border-white/10 rounded-lg p-4">
{/* Search Bar - Mobile */}
<div className="mb-4">
<Input
type="text"
placeholder="SEARCH GAMES..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-glow bg-transparent border border-gray-600 text-white placeholder-gray-400 w-full"
style={{ borderColor: 'var(--mass-effect-cyan)' }}
/>
</div>
{/* Navigation Links - Mobile */} {/* Navigation Links - Mobile */}
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<a <Link
href="/games" href="/games"
className="text-white hover:text-glow-cyan transition-colors py-2" className="font-mono text-sm text-white/80 hover:text-cyan-400 transition-all duration-300 py-2"
tabIndex={isMenuOpen ? 0 : -1} tabIndex={isMenuOpen ? 0 : -1}
style={{ textShadow: '0 0 10px rgba(34, 211, 238, 0.3)' }}
> >
GAMES GAMES
</a> </Link>
<a <Link
href="/import" href="/settings"
className="text-white hover:text-glow-cyan transition-colors py-2" className="font-mono text-sm text-white/80 hover:text-purple-400 transition-all duration-300 py-2"
tabIndex={isMenuOpen ? 0 : -1} tabIndex={isMenuOpen ? 0 : -1}
style={{ textShadow: '0 0 10px rgba(192, 132, 252, 0.3)' }}
> >
IMPORT SETTINGS
</a> </Link>
</div> </div>
</div> </div>
)} )}