feat: Refactor Games and Import pages with new Navbar and improved UI
- 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:
@@ -18,6 +18,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import Navbar from '@/components/landing/Navbar';
|
||||
|
||||
export default function GameDetailPage() {
|
||||
const params = useParams();
|
||||
@@ -79,180 +80,278 @@ export default function GameDetailPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-muted-foreground">Cargando juego...</div>
|
||||
</div>
|
||||
<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="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>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !game) {
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-destructive">{error || 'Juego no encontrado'}</div>
|
||||
</div>
|
||||
<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="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>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{game.title}</h1>
|
||||
<p className="text-muted-foreground">{game.slug}</p>
|
||||
<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">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-responsive-3xl font-bold mb-2">
|
||||
<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 className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={loadGame}
|
||||
disabled={refreshing}
|
||||
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||
>
|
||||
<RefreshCwIcon className={`size-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={loadGame} disabled={refreshing}>
|
||||
<RefreshCwIcon className={`size-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setDeleteDialogOpen(true)}>
|
||||
Eliminar Juego
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Información del Juego</CardTitle>
|
||||
<CardDescription>Detalles y metadatos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{formatDate(game.releaseDate) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground">Fecha de lanzamiento:</span>{' '}
|
||||
{formatDate(game.releaseDate)}
|
||||
</span>
|
||||
{/* Game Info */}
|
||||
<Card className="glassmorphic">
|
||||
<CardHeader>
|
||||
<CardTitle className="mono text-sm tracking-wider">INFORMACIÓN DEL JUEGO</CardTitle>
|
||||
<CardDescription>Detalles y metadatos</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{formatDate(game.releaseDate) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground mono">FECHA DE LANZAMIENTO:</span>{' '}
|
||||
{formatDate(game.releaseDate)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
{game.genre && (
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
)}
|
||||
{game.platform && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="mono text-xs tracking-wider"
|
||||
style={{ borderColor: 'var(--neon-lime)', color: 'var(--neon-lime)' }}
|
||||
>
|
||||
PLATAFORMA: {game.platform.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.year && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="mono text-xs tracking-wider"
|
||||
style={{ borderColor: 'var(--neon-gold)', color: 'var(--neon-gold)' }}
|
||||
>
|
||||
AÑO: {game.year}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.sourceId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
ID DE FUENTE: {game.sourceId}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.igdbId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
IGDB ID: {game.igdbId}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.rawgId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
RAWG ID: {game.rawgId}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.thegamesdbId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
THEGAMESDB ID: {game.thegamesdbId}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">Fuente: {game.source}</Badge>
|
||||
</div>
|
||||
{game.genre && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Género: {game.genre}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.platform && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Plataforma: {game.platform}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.year && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Año: {game.year}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.sourceId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">ID de fuente: {game.sourceId}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.igdbId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">IGDB ID: {game.igdbId}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.rawgId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">RAWG ID: {game.rawgId}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.thegamesdbId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">TheGamesDB ID: {game.thegamesdbId}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{game.description && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-semibold mb-2">Descripción</h3>
|
||||
<p className="text-sm text-muted-foreground">{game.description}</p>
|
||||
</div>
|
||||
{game.description && (
|
||||
<div className="mt-4">
|
||||
<h3 className="font-semibold mb-2 mono text-sm tracking-wider">DESCRIPCIÓN</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{game.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ROM Info (if source = rom) */}
|
||||
{game.source === 'rom' && (
|
||||
<Card className="glassmorphic">
|
||||
<CardHeader>
|
||||
<CardTitle className="mono text-sm tracking-wider">
|
||||
INFORMACIÓN DEL ARCHIVO ROM
|
||||
</CardTitle>
|
||||
<CardDescription>Detalles del archivo importado</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{game.romFilename && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground mono">ARCHIVO:</span>{' '}
|
||||
{game.romFilename}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{game.romPath && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground mono">RUTA:</span> {game.romPath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{game.romSize && (
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
)}
|
||||
{game.romFormat && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
FORMATO: {game.romFormat.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.romChecksum && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="mono text-xs tracking-wider">
|
||||
CHECKSUM: {game.romChecksum}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* ROM Info (if source = rom) */}
|
||||
{game.source === 'rom' && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Información del Archivo ROM</CardTitle>
|
||||
<CardDescription>Detalles del archivo importado</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{game.romFilename && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground">Archivo:</span> {game.romFilename}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{game.romPath && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
<span className="text-muted-foreground">Ruta:</span> {game.romPath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{game.romSize && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Tamaño: {formatFileSize(game.romSize)}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.romFormat && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Formato: {game.romFormat}</Badge>
|
||||
</div>
|
||||
)}
|
||||
{game.romChecksum && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">Checksum: {game.romChecksum}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Game Confirmation */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
¿Estás seguro de que deseas eliminar “{game.title}”? Esta acción no se
|
||||
puede deshacer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteGame}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Eliminar
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* Delete Game Confirmation */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="mono text-sm tracking-wider">
|
||||
¿ELIMINAR JUEGO?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
¿Estás seguro de que deseas eliminar “{game.title}”? Esta acción no se
|
||||
puede deshacer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="mono text-xs tracking-wider">
|
||||
CANCELAR
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteGame}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 mono text-xs tracking-wider transition-all duration-300 hover:translate-y-[-2px]"
|
||||
>
|
||||
ELIMINAR
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Game, gamesApi } from '@/lib/api';
|
||||
import { GameTable } from '@/components/games/GameTable';
|
||||
import { GameDialog } from '@/components/games/GameDialog';
|
||||
import { GameFilters } from '@/components/games/GameFilters';
|
||||
import { ImportSheet } from '@/components/games/ImportSheet';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PlusIcon, LayoutGridIcon, TableIcon } from 'lucide-react';
|
||||
import {
|
||||
@@ -19,6 +18,8 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import Navbar from '@/components/landing/Navbar';
|
||||
import { GameCard } from '@/components/games/GameCard';
|
||||
|
||||
export default function GamesPage() {
|
||||
const router = useRouter();
|
||||
@@ -96,124 +97,149 @@ export default function GamesPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Gestión de Videojuegos</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{filteredGames.length} {filteredGames.length === 1 ? 'juego' : 'juegos'} en tu
|
||||
biblioteca
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ImportSheet onSuccess={loadGames} />
|
||||
<Button onClick={handleCreate}>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
Nuevo Juego
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Starfield background */}
|
||||
<div className="starfield" />
|
||||
|
||||
{/* Filters */}
|
||||
<GameFilters
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onClear={() => setSearchQuery('')}
|
||||
/>
|
||||
{/* Navbar */}
|
||||
<Navbar />
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex gap-2 self-end">
|
||||
<Button
|
||||
variant={viewMode === 'table' ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('table')}
|
||||
>
|
||||
<TableIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('grid')}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* 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">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-responsive-3xl font-bold mb-2">
|
||||
<span className="gradient-text">GESTIÓN DE VIDEOJUEGOS</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground mono text-sm tracking-wider">
|
||||
{filteredGames.length} {filteredGames.length === 1 ? 'JUEGO' : 'JUEGOS'} EN TU
|
||||
BIBLIOTECA
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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" />
|
||||
<span className="mono text-xs tracking-wider ml-2">NUEVO JUEGO</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-muted-foreground">Cargando juegos...</div>
|
||||
</div>
|
||||
) : viewMode === 'table' ? (
|
||||
<GameTable
|
||||
games={filteredGames}
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredGames.map((game) => (
|
||||
<GameCard
|
||||
key={game.id}
|
||||
game={game}
|
||||
{/* Filters */}
|
||||
<GameFilters
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onClear={() => setSearchQuery('')}
|
||||
/>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex gap-2 self-end">
|
||||
<Button
|
||||
variant={viewMode === 'table' ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('table')}
|
||||
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||
>
|
||||
<TableIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
||||
size="icon"
|
||||
onClick={() => setViewMode('grid')}
|
||||
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<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>
|
||||
) : viewMode === 'table' ? (
|
||||
<GameTable
|
||||
games={filteredGames}
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="discovery-wall">
|
||||
{filteredGames.map((game) => (
|
||||
<GameCard
|
||||
key={game.id}
|
||||
game={game}
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && filteredGames.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{searchQuery
|
||||
? 'No se encontraron juegos que coincidan con tu búsqueda.'
|
||||
: 'No hay juegos en tu biblioteca.'}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
<Button onClick={handleCreate}>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
Agregar primer juego
|
||||
</Button>
|
||||
{/* Empty State */}
|
||||
{!loading && filteredGames.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground mono text-sm tracking-wider mb-4">
|
||||
{searchQuery
|
||||
? 'NO SE ENCONTRARON JUEGOS QUE COINCIDAN CON TU BÚSQUEDA.'
|
||||
: 'NO HAY JUEGOS EN TU BIBLIOTECA.'}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
<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" />
|
||||
<span className="mono text-xs tracking-wider ml-2">AGREGAR PRIMER JUEGO</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialog */}
|
||||
<GameDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
game={editingGame}
|
||||
onSuccess={loadGames}
|
||||
/>
|
||||
{/* Dialog */}
|
||||
<GameDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
game={editingGame}
|
||||
onSuccess={loadGames}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
¿Estás seguro de que deseas eliminar "{gameToDelete?.title}"? Esta acción no se puede
|
||||
deshacer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Eliminar
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* Delete Confirmation */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>¿Eliminar juego?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
¿Estás seguro de que deseas eliminar “{gameToDelete?.title}”? Esta
|
||||
acción no se puede deshacer.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Eliminar
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,13 @@
|
||||
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-cyan {
|
||||
box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ArrowLeftIcon,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Navbar from '@/components/landing/Navbar';
|
||||
|
||||
export default function ImportPage() {
|
||||
const router = useRouter();
|
||||
@@ -56,167 +57,188 @@ export default function ImportPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Importar Juegos</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Importa juegos desde archivos ROM en un directorio local
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Configuración de Importación</CardTitle>
|
||||
<CardDescription>
|
||||
Especifica el directorio que contiene los archivos ROM que deseas importar
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="directory">Directorio *</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="directory"
|
||||
value={directory}
|
||||
onChange={(e) => setDirectory(e.target.value)}
|
||||
placeholder="/path/to/roms"
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={isImporting}
|
||||
title="Seleccionar directorio"
|
||||
>
|
||||
<FolderOpenIcon className="size-4" />
|
||||
</Button>
|
||||
<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">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => router.back()}
|
||||
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-responsive-3xl font-bold mb-2">
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ruta absoluta del directorio que contiene las ROMs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="recursive"
|
||||
checked={recursive}
|
||||
onCheckedChange={(checked) => setRecursive(checked === true)}
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<Label htmlFor="recursive" className="cursor-pointer">
|
||||
Incluir subdirectorios recursivamente
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
disabled={isImporting || !directory}
|
||||
>
|
||||
Limpiar
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={handleImport}
|
||||
disabled={isImporting || !directory.trim()}
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<LoaderIcon className="size-4 animate-spin mr-2" />
|
||||
Importando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Importar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Result */}
|
||||
{result && (
|
||||
<Card
|
||||
className={`mt-6 ${result.success ? 'border-emerald-500/50' : 'border-destructive/50'}`}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{result.success ? (
|
||||
<CheckCircleIcon className="size-5 text-emerald-500" />
|
||||
) : (
|
||||
<XCircleIcon className="size-5 text-destructive" />
|
||||
)}
|
||||
{result.success ? 'Importación Exitosa' : 'Error en la Importación'}
|
||||
</CardTitle>
|
||||
<CardDescription>{result.message}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">ROMs importadas</p>
|
||||
<p className="text-2xl font-bold">{result.imported}</p>
|
||||
{/* Import Form */}
|
||||
<Card className="glassmorphic">
|
||||
<CardHeader>
|
||||
<CardTitle className="mono text-sm tracking-wider">
|
||||
CONFIGURACIÓN DE IMPORTACIÓN
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Especifica el directorio que contiene los archivos ROM que deseas importar
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="directory" className="mono text-xs tracking-wider">
|
||||
DIRECTORIO *
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="directory"
|
||||
value={directory}
|
||||
onChange={(e) => setDirectory(e.target.value)}
|
||||
placeholder="/path/to/roms"
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={isImporting}
|
||||
title="Seleccionar directorio"
|
||||
>
|
||||
<FolderOpenIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ruta absoluta del directorio que contiene las ROMs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{result.errors.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Errores encontrados</p>
|
||||
<div className="bg-destructive/10 rounded-lg p-3 max-h-60 overflow-y-auto">
|
||||
<ul className="text-sm text-destructive space-y-1">
|
||||
{result.errors.map((error, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<span className="text-destructive-foreground">•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="recursive"
|
||||
checked={recursive}
|
||||
onCheckedChange={(checked) => setRecursive(checked === true)}
|
||||
disabled={isImporting}
|
||||
/>
|
||||
<Label htmlFor="recursive" className="cursor-pointer">
|
||||
Incluir subdirectorios recursivamente
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
disabled={isImporting || !directory}
|
||||
>
|
||||
Limpiar
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={handleImport}
|
||||
disabled={isImporting || !directory.trim()}
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<LoaderIcon className="size-4 animate-spin mr-2" />
|
||||
Importando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploadIcon data-icon="inline-start" />
|
||||
Importar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Result */}
|
||||
{result && (
|
||||
<Card
|
||||
className={`mt-6 ${result.success ? 'border-emerald-500/50' : 'border-destructive/50'}`}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{result.success ? (
|
||||
<CheckCircleIcon className="size-5 text-emerald-500" />
|
||||
) : (
|
||||
<XCircleIcon className="size-5 text-destructive" />
|
||||
)}
|
||||
{result.success ? 'Importación Exitosa' : 'Error en la Importación'}
|
||||
</CardTitle>
|
||||
<CardDescription>{result.message}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">ROMs importadas</p>
|
||||
<p className="text-2xl font-bold">{result.imported}</p>
|
||||
</div>
|
||||
|
||||
{result.errors.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Errores encontrados</p>
|
||||
<div className="bg-destructive/10 rounded-lg p-3 max-h-60 overflow-y-auto">
|
||||
<ul className="text-sm text-destructive space-y-1">
|
||||
{result.errors.map((error, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<span className="text-destructive-foreground">•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" onClick={handleReset} className="flex-1">
|
||||
Nueva Importación
|
||||
</Button>
|
||||
{result.success && (
|
||||
<Button onClick={() => router.push('/games')} className="flex-1">
|
||||
Ver Juegos
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" onClick={handleReset} className="flex-1">
|
||||
Nueva Importación
|
||||
</Button>
|
||||
{result.success && (
|
||||
<Button onClick={() => router.push('/games')} className="flex-1">
|
||||
Ver Juegos
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<Card className="mt-6 bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Información</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<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>• 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>• La importación puede tardar dependiendo de la cantidad de archivos</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Info */}
|
||||
<Card className="mt-6 bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Información</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<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>
|
||||
• 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>• La importación puede tardar dependiendo de la cantidad de archivos</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Link from 'next/link';
|
||||
import { GameCover } from '@/components/landing/GameCover';
|
||||
import { EmptyState } from '@/components/landing/EmptyState';
|
||||
import { ArrowRight, Grid3X3 } from 'lucide-react';
|
||||
import Navbar from '@/components/landing/Navbar';
|
||||
|
||||
export default function Home() {
|
||||
const [games, setGames] = useState<Game[]>([]);
|
||||
@@ -43,43 +44,11 @@ export default function Home() {
|
||||
{/* Starfield background */}
|
||||
<div className="starfield" />
|
||||
|
||||
{/* Header */}
|
||||
<header className="glass sticky top-0 z-50 border-b border-border/50">
|
||||
<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>
|
||||
{/* Navbar */}
|
||||
<Navbar />
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative z-10">
|
||||
<main className="relative z-10 pt-20">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
|
||||
103
frontend/src/app/settings/page.tsx
Normal file
103
frontend/src/app/settings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
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 (
|
||||
<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="flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<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)' }}
|
||||
>
|
||||
QUASAR
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links - Desktop */}
|
||||
<div className="hidden md:flex items-center space-x-6">
|
||||
<a
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<Link
|
||||
href="/games"
|
||||
className="text-white hover:text-glow-cyan transition-colors"
|
||||
style={{ textShadow: '0 0 5px var(--mass-effect-cyan-glow)' }}
|
||||
className="font-mono text-sm text-white/80 hover:text-cyan-400 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
style={{ textShadow: '0 0 10px rgba(34, 211, 238, 0.3)' }}
|
||||
>
|
||||
GAMES
|
||||
</a>
|
||||
<a
|
||||
href="/import"
|
||||
className="text-white hover:text-glow-cyan transition-colors"
|
||||
style={{ textShadow: '0 0 5px var(--mass-effect-cyan-glow)' }}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="font-mono text-sm text-white/80 hover:text-purple-400 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
style={{ textShadow: '0 0 10px rgba(192, 132, 252, 0.3)' }}
|
||||
>
|
||||
IMPORT
|
||||
</a>
|
||||
SETTINGS
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -94,35 +79,25 @@ const Navbar = () => {
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{isMenuOpen && (
|
||||
<div className="md:hidden mt-4 glass 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>
|
||||
|
||||
<div className="md:hidden mt-4 backdrop-blur-md bg-black/70 border border-white/10 rounded-lg p-4">
|
||||
{/* Navigation Links - Mobile */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<a
|
||||
<Link
|
||||
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}
|
||||
style={{ textShadow: '0 0 10px rgba(34, 211, 238, 0.3)' }}
|
||||
>
|
||||
GAMES
|
||||
</a>
|
||||
<a
|
||||
href="/import"
|
||||
className="text-white hover:text-glow-cyan transition-colors py-2"
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="font-mono text-sm text-white/80 hover:text-purple-400 transition-all duration-300 py-2"
|
||||
tabIndex={isMenuOpen ? 0 : -1}
|
||||
style={{ textShadow: '0 0 10px rgba(192, 132, 252, 0.3)' }}
|
||||
>
|
||||
IMPORT
|
||||
</a>
|
||||
SETTINGS
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user