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,
|
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,180 +80,278 @@ 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="flex items-center justify-center py-12">
|
<div className="starfield" />
|
||||||
<div className="text-muted-foreground">Cargando juego...</div>
|
<Navbar />
|
||||||
</div>
|
<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>
|
</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="flex items-center justify-center py-12">
|
<div className="starfield" />
|
||||||
<div className="text-destructive">{error || 'Juego no encontrado'}</div>
|
<Navbar />
|
||||||
</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="starfield" />
|
||||||
{/* Header */}
|
<Navbar />
|
||||||
<div className="flex items-center justify-between">
|
<main className="relative z-10 pt-20">
|
||||||
<div className="flex items-center gap-4">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
<div className="flex flex-col gap-6">
|
||||||
<ArrowLeftIcon className="size-4" />
|
{/* Header */}
|
||||||
</Button>
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div className="flex items-center gap-4">
|
||||||
<h1 className="text-3xl font-bold">{game.title}</h1>
|
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||||
<p className="text-muted-foreground">{game.slug}</p>
|
<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>
|
|
||||||
<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 */}
|
{/* 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>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{formatDate(game.releaseDate) && (
|
{formatDate(game.releaseDate) && (
|
||||||
<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 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>
|
||||||
)}
|
{game.description && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="mt-4">
|
||||||
<Badge variant="secondary">Fuente: {game.source}</Badge>
|
<h3 className="font-semibold mb-2 mono text-sm tracking-wider">DESCRIPCIÓN</h3>
|
||||||
</div>
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
{game.genre && (
|
{game.description}
|
||||||
<div className="flex items-center gap-2">
|
</p>
|
||||||
<Badge variant="outline">Género: {game.genre}</Badge>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</CardContent>
|
||||||
{game.platform && (
|
</Card>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="outline">Plataforma: {game.platform}</Badge>
|
{/* ROM Info (if source = rom) */}
|
||||||
</div>
|
{game.source === 'rom' && (
|
||||||
)}
|
<Card className="glassmorphic">
|
||||||
{game.year && (
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<CardTitle className="mono text-sm tracking-wider">
|
||||||
<Badge variant="outline">Año: {game.year}</Badge>
|
INFORMACIÓN DEL ARCHIVO ROM
|
||||||
</div>
|
</CardTitle>
|
||||||
)}
|
<CardDescription>Detalles del archivo importado</CardDescription>
|
||||||
{game.sourceId && (
|
</CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<CardContent>
|
||||||
<Badge variant="outline">ID de fuente: {game.sourceId}</Badge>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
</div>
|
{game.romFilename && (
|
||||||
)}
|
<div className="flex items-center gap-2">
|
||||||
{game.igdbId && (
|
<FileIcon className="size-4 text-muted-foreground" />
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-sm">
|
||||||
<Badge variant="outline">IGDB ID: {game.igdbId}</Badge>
|
<span className="text-muted-foreground mono">ARCHIVO:</span>{' '}
|
||||||
</div>
|
{game.romFilename}
|
||||||
)}
|
</span>
|
||||||
{game.rawgId && (
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<Badge variant="outline">RAWG ID: {game.rawgId}</Badge>
|
{game.romPath && (
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
)}
|
<FileIcon className="size-4 text-muted-foreground" />
|
||||||
{game.thegamesdbId && (
|
<span className="text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-muted-foreground mono">RUTA:</span> {game.romPath}
|
||||||
<Badge variant="outline">TheGamesDB ID: {game.thegamesdbId}</Badge>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{game.romSize && (
|
||||||
{game.description && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="mt-4">
|
<Badge
|
||||||
<h3 className="font-semibold mb-2">Descripción</h3>
|
variant="outline"
|
||||||
<p className="text-sm text-muted-foreground">{game.description}</p>
|
className="mono text-xs tracking-wider"
|
||||||
</div>
|
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>
|
</div>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* ROM Info (if source = rom) */}
|
{/* Delete Game Confirmation */}
|
||||||
{game.source === 'rom' && (
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<Card>
|
<AlertDialogContent>
|
||||||
<CardHeader>
|
<AlertDialogHeader>
|
||||||
<CardTitle>Información del Archivo ROM</CardTitle>
|
<AlertDialogTitle className="mono text-sm tracking-wider">
|
||||||
<CardDescription>Detalles del archivo importado</CardDescription>
|
¿ELIMINAR JUEGO?
|
||||||
</CardHeader>
|
</AlertDialogTitle>
|
||||||
<CardContent>
|
<AlertDialogDescription>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
¿Estás seguro de que deseas eliminar “{game.title}”? Esta acción no se
|
||||||
{game.romFilename && (
|
puede deshacer.
|
||||||
<div className="flex items-center gap-2">
|
</AlertDialogDescription>
|
||||||
<FileIcon className="size-4 text-muted-foreground" />
|
</AlertDialogHeader>
|
||||||
<span className="text-sm">
|
<AlertDialogFooter>
|
||||||
<span className="text-muted-foreground">Archivo:</span> {game.romFilename}
|
<AlertDialogCancel className="mono text-xs tracking-wider">
|
||||||
</span>
|
CANCELAR
|
||||||
</div>
|
</AlertDialogCancel>
|
||||||
)}
|
<AlertDialogAction
|
||||||
{game.romPath && (
|
onClick={handleDeleteGame}
|
||||||
<div className="flex items-center gap-2">
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 mono text-xs tracking-wider transition-all duration-300 hover:translate-y-[-2px]"
|
||||||
<FileIcon className="size-4 text-muted-foreground" />
|
>
|
||||||
<span className="text-sm">
|
ELIMINAR
|
||||||
<span className="text-muted-foreground">Ruta:</span> {game.romPath}
|
</AlertDialogAction>
|
||||||
</span>
|
</AlertDialogFooter>
|
||||||
</div>
|
</AlertDialogContent>
|
||||||
)}
|
</AlertDialog>
|
||||||
{game.romSize && (
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
</main>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,124 +97,149 @@ export default function GamesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="flex flex-col gap-6">
|
{/* Starfield background */}
|
||||||
{/* Header */}
|
<div className="starfield" />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Navbar */}
|
||||||
<GameFilters
|
<Navbar />
|
||||||
searchQuery={searchQuery}
|
|
||||||
onSearchChange={setSearchQuery}
|
|
||||||
onClear={() => setSearchQuery('')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* View Mode Toggle */}
|
{/* Main Content */}
|
||||||
<div className="flex gap-2 self-end">
|
<main className="relative z-10 pt-20">
|
||||||
<Button
|
<div className="container mx-auto px-4 py-8">
|
||||||
variant={viewMode === 'table' ? 'default' : 'outline'}
|
<div className="flex flex-col gap-6">
|
||||||
size="icon"
|
{/* Header */}
|
||||||
onClick={() => setViewMode('table')}
|
<div className="flex items-center justify-between">
|
||||||
>
|
<div>
|
||||||
<TableIcon className="size-4" />
|
<h1 className="text-responsive-3xl font-bold mb-2">
|
||||||
</Button>
|
<span className="gradient-text">GESTIÓN DE VIDEOJUEGOS</span>
|
||||||
<Button
|
</h1>
|
||||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
<p className="text-muted-foreground mono text-sm tracking-wider">
|
||||||
size="icon"
|
{filteredGames.length} {filteredGames.length === 1 ? 'JUEGO' : 'JUEGOS'} EN TU
|
||||||
onClick={() => setViewMode('grid')}
|
BIBLIOTECA
|
||||||
>
|
</p>
|
||||||
<LayoutGridIcon className="size-4" />
|
</div>
|
||||||
</Button>
|
<div className="flex gap-2">
|
||||||
</div>
|
<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 */}
|
{/* Filters */}
|
||||||
{loading ? (
|
<GameFilters
|
||||||
<div className="flex items-center justify-center py-12">
|
searchQuery={searchQuery}
|
||||||
<div className="text-muted-foreground">Cargando juegos...</div>
|
onSearchChange={setSearchQuery}
|
||||||
</div>
|
onClear={() => setSearchQuery('')}
|
||||||
) : viewMode === 'table' ? (
|
/>
|
||||||
<GameTable
|
|
||||||
games={filteredGames}
|
{/* View Mode Toggle */}
|
||||||
onView={handleView}
|
<div className="flex gap-2 self-end">
|
||||||
onEdit={handleEdit}
|
<Button
|
||||||
onDelete={handleDelete}
|
variant={viewMode === 'table' ? 'default' : 'outline'}
|
||||||
/>
|
size="icon"
|
||||||
) : (
|
onClick={() => setViewMode('table')}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||||
{filteredGames.map((game) => (
|
>
|
||||||
<GameCard
|
<TableIcon className="size-4" />
|
||||||
key={game.id}
|
</Button>
|
||||||
game={game}
|
<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}
|
onView={handleView}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
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 */}
|
{/* 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
|
||||||
<PlusIcon data-icon="inline-start" />
|
onClick={handleCreate}
|
||||||
Agregar primer juego
|
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)]"
|
||||||
</Button>
|
>
|
||||||
|
<PlusIcon data-icon="inline-start" />
|
||||||
|
<span className="mono text-xs tracking-wider ml-2">AGREGAR PRIMER JUEGO</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dialog */}
|
{/* Dialog */}
|
||||||
<GameDialog
|
<GameDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={setDialogOpen}
|
onOpenChange={setDialogOpen}
|
||||||
game={editingGame}
|
game={editingGame}
|
||||||
onSuccess={loadGames}
|
onSuccess={loadGames}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Delete Confirmation */}
|
{/* Delete Confirmation */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<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 “{gameToDelete?.title}”? Esta
|
||||||
deshacer.
|
acción no se puede deshacer.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={confirmDelete}
|
onClick={confirmDelete}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
Eliminar
|
Eliminar
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,167 +57,188 @@ export default function ImportPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="starfield" />
|
||||||
{/* Header */}
|
<Navbar />
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<main className="relative z-10 pt-20">
|
||||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
<div className="container mx-auto px-4 py-8">
|
||||||
<ArrowLeftIcon className="size-4" />
|
<div className="max-w-2xl mx-auto">
|
||||||
</Button>
|
{/* Header */}
|
||||||
<div>
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<h1 className="text-3xl font-bold">Importar Juegos</h1>
|
<Button
|
||||||
<p className="text-muted-foreground">
|
variant="ghost"
|
||||||
Importa juegos desde archivos ROM en un directorio local
|
size="icon"
|
||||||
</p>
|
onClick={() => router.back()}
|
||||||
</div>
|
className="transition-all duration-300 hover:translate-y-[-2px]"
|
||||||
</div>
|
>
|
||||||
|
<ArrowLeftIcon className="size-4" />
|
||||||
{/* Import Form */}
|
</Button>
|
||||||
<Card>
|
<div>
|
||||||
<CardHeader>
|
<h1 className="text-responsive-3xl font-bold mb-2">
|
||||||
<CardTitle>Configuración de Importación</CardTitle>
|
<span className="gradient-text">IMPORTAR JUEGOS</span>
|
||||||
<CardDescription>
|
</h1>
|
||||||
Especifica el directorio que contiene los archivos ROM que deseas importar
|
<p className="text-muted-foreground mono text-sm tracking-wider">
|
||||||
</CardDescription>
|
Importa juegos desde archivos ROM en un directorio local
|
||||||
</CardHeader>
|
</p>
|
||||||
<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>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Ruta absoluta del directorio que contiene las ROMs
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
{/* Import Form */}
|
||||||
<Checkbox
|
<Card className="glassmorphic">
|
||||||
id="recursive"
|
<CardHeader>
|
||||||
checked={recursive}
|
<CardTitle className="mono text-sm tracking-wider">
|
||||||
onCheckedChange={(checked) => setRecursive(checked === true)}
|
CONFIGURACIÓN DE IMPORTACIÓN
|
||||||
disabled={isImporting}
|
</CardTitle>
|
||||||
/>
|
<CardDescription>
|
||||||
<Label htmlFor="recursive" className="cursor-pointer">
|
Especifica el directorio que contiene los archivos ROM que deseas importar
|
||||||
Incluir subdirectorios recursivamente
|
</CardDescription>
|
||||||
</Label>
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2 mt-4">
|
<Label htmlFor="directory" className="mono text-xs tracking-wider">
|
||||||
<Button
|
DIRECTORIO *
|
||||||
variant="outline"
|
</Label>
|
||||||
className="flex-1"
|
<div className="flex gap-2">
|
||||||
onClick={handleReset}
|
<Input
|
||||||
disabled={isImporting || !directory}
|
id="directory"
|
||||||
>
|
value={directory}
|
||||||
Limpiar
|
onChange={(e) => setDirectory(e.target.value)}
|
||||||
</Button>
|
placeholder="/path/to/roms"
|
||||||
<Button
|
disabled={isImporting}
|
||||||
className="flex-1"
|
/>
|
||||||
onClick={handleImport}
|
<Button
|
||||||
disabled={isImporting || !directory.trim()}
|
type="button"
|
||||||
>
|
variant="outline"
|
||||||
{isImporting ? (
|
size="icon"
|
||||||
<>
|
disabled={isImporting}
|
||||||
<LoaderIcon className="size-4 animate-spin mr-2" />
|
title="Seleccionar directorio"
|
||||||
Importando...
|
>
|
||||||
</>
|
<FolderOpenIcon className="size-4" />
|
||||||
) : (
|
</Button>
|
||||||
<>
|
</div>
|
||||||
<UploadIcon data-icon="inline-start" />
|
<p className="text-xs text-muted-foreground">
|
||||||
Importar
|
Ruta absoluta del directorio que contiene las ROMs
|
||||||
</>
|
</p>
|
||||||
)}
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{result.errors.length > 0 && (
|
<div className="flex items-center gap-2">
|
||||||
<div>
|
<Checkbox
|
||||||
<p className="text-sm font-medium mb-2">Errores encontrados</p>
|
id="recursive"
|
||||||
<div className="bg-destructive/10 rounded-lg p-3 max-h-60 overflow-y-auto">
|
checked={recursive}
|
||||||
<ul className="text-sm text-destructive space-y-1">
|
onCheckedChange={(checked) => setRecursive(checked === true)}
|
||||||
{result.errors.map((error, index) => (
|
disabled={isImporting}
|
||||||
<li key={index} className="flex items-start gap-2">
|
/>
|
||||||
<span className="text-destructive-foreground">•</span>
|
<Label htmlFor="recursive" className="cursor-pointer">
|
||||||
<span>{error}</span>
|
Incluir subdirectorios recursivamente
|
||||||
</li>
|
</Label>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
{/* Info */}
|
||||||
<Button variant="outline" onClick={handleReset} className="flex-1">
|
<Card className="mt-6 bg-muted/50">
|
||||||
Nueva Importación
|
<CardHeader>
|
||||||
</Button>
|
<CardTitle className="text-base">Información</CardTitle>
|
||||||
{result.success && (
|
</CardHeader>
|
||||||
<Button onClick={() => router.push('/games')} className="flex-1">
|
<CardContent>
|
||||||
Ver Juegos
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
</Button>
|
<li>
|
||||||
)}
|
• El sistema escaneará el directorio especificado en busca de archivos ROM
|
||||||
</div>
|
</li>
|
||||||
</div>
|
<li>• Se calcularán los checksums (CRC32, MD5, SHA1) para cada archivo</li>
|
||||||
</CardContent>
|
<li>
|
||||||
</Card>
|
• Las ROMs se asociarán automáticamente con juegos existentes si coinciden
|
||||||
)}
|
</li>
|
||||||
|
<li>• Los archivos duplicados se detectarán y omitirán</li>
|
||||||
{/* Info */}
|
<li>• La importación puede tardar dependiendo de la cantidad de archivos</li>
|
||||||
<Card className="mt-6 bg-muted/50">
|
</ul>
|
||||||
<CardHeader>
|
</CardContent>
|
||||||
<CardTitle className="text-base">Información</CardTitle>
|
</Card>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</div>
|
||||||
<ul className="text-sm text-muted-foreground space-y-1">
|
</main>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
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';
|
'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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user