feat: update server port to 3003 and enhance logging; refactor frontend styles and components for improved UI/UX
- Changed server port from 3000 to 3003 in backend. - Updated logging message for server startup. - Refactored global CSS styles for a neon theme with new color variables. - Introduced responsive typography and layout adjustments in frontend. - Added new components: EmptyState and GameCover for better game display. - Implemented loading states and error handling in the Home page. - Updated API base URL to match new server port.
This commit is contained in:
@@ -3,14 +3,15 @@ import { buildApp } from './app';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const port = Number(process.env.PORT ?? 3000);
|
const port = Number(process.env.PORT ?? 3003);
|
||||||
const app = buildApp();
|
const app = buildApp();
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
|
const host = '0.0.0.0';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.listen({ port, host: '0.0.0.0' });
|
await app.listen({ port, host });
|
||||||
// eslint-disable-next-line no-console
|
console.log(`🚀 Server ready and listening on http://${host}:${port}`);
|
||||||
console.log(`Server listening on http://0.0.0.0:${port}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
app.log.error(err);
|
app.log.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-display: var(--font-display);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-mono);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -48,123 +48,242 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
/* Rich dark backgrounds */
|
||||||
--background: #0a0a12;
|
--background: #0a0a0a;
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: #e5e5e5;
|
||||||
--card: oklch(0.11 0 0);
|
--card: #0f0f0f;
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: #e5e5e5;
|
||||||
--popover: oklch(0.11 0 0);
|
--popover: #0f0f0f;
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: #e5e5e5;
|
||||||
--primary: #00d0e0;
|
|
||||||
--primary-foreground: #0a0a12;
|
/* Neon accents */
|
||||||
--secondary: oklch(0.18 0 0);
|
--primary: #00f0ff;
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--primary-foreground: #0a0a0a;
|
||||||
--muted: oklch(0.18 0 0);
|
--secondary: #151515;
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--secondary-foreground: #e5e5e5;
|
||||||
--accent: #f0c040;
|
--muted: #151515;
|
||||||
--accent-foreground: #0a0a12;
|
--muted-foreground: #737373;
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--accent: #b026ff;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--accent-foreground: #e5e5e5;
|
||||||
--input: oklch(1 0 0 / 15%);
|
--destructive: #ff6b6b;
|
||||||
|
--border: #262626;
|
||||||
|
--input: #262626;
|
||||||
--ring: #00f0ff;
|
--ring: #00f0ff;
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
/* Chart colors */
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-1: #00f0ff;
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-2: #b026ff;
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-3: #39ff14;
|
||||||
--sidebar: oklch(0.11 0 0);
|
--chart-4: #ffd700;
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--chart-5: #ff6b6b;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar: #0f0f0f;
|
||||||
|
--sidebar-foreground: #e5e5e5;
|
||||||
--sidebar-primary: #00f0ff;
|
--sidebar-primary: #00f0ff;
|
||||||
--sidebar-primary-foreground: #0a0a12;
|
--sidebar-primary-foreground: #0a0a0a;
|
||||||
--sidebar-accent: oklch(0.18 0 0);
|
--sidebar-accent: #151515;
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: #e5e5e5;
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: #262626;
|
||||||
--sidebar-ring: #00f0ff;
|
--sidebar-ring: #00f0ff;
|
||||||
|
|
||||||
|
/* Radius */
|
||||||
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: #0a0a12;
|
--background: #0a0a0a;
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: #e5e5e5;
|
||||||
--card: oklch(0.11 0 0);
|
--card: #0f0f0f;
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: #e5e5e5;
|
||||||
--popover: oklch(0.11 0 0);
|
--popover: #0f0f0f;
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: #e5e5e5;
|
||||||
--primary: #00d0e0;
|
--primary: #00f0ff;
|
||||||
--primary-foreground: #0a0a12;
|
--primary-foreground: #0a0a0a;
|
||||||
--secondary: oklch(0.18 0 0);
|
--secondary: #151515;
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: #e5e5e5;
|
||||||
--muted: oklch(0.18 0 0);
|
--muted: #151515;
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: #737373;
|
||||||
--accent: #f0c040;
|
--accent: #b026ff;
|
||||||
--accent-foreground: #0a0a12;
|
--accent-foreground: #e5e5e5;
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: #ff6b6b;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: #262626;
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: #262626;
|
||||||
--ring: #00f0ff;
|
--ring: #00f0ff;
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: #00f0ff;
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: #b026ff;
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: #39ff14;
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: #ffd700;
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: #ff6b6b;
|
||||||
--sidebar: oklch(0.11 0 0);
|
--sidebar: #0f0f0f;
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: #e5e5e5;
|
||||||
--sidebar-primary: #00f0ff;
|
--sidebar-primary: #00f0ff;
|
||||||
--sidebar-primary-foreground: #0a0a12;
|
--sidebar-primary-foreground: #0a0a0a;
|
||||||
--sidebar-accent: oklch(0.18 0 0);
|
--sidebar-accent: #151515;
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: #e5e5e5;
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: #262626;
|
||||||
--sidebar-ring: #00f0ff;
|
--sidebar-ring: #00f0ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mass Effect-inspired theme customizations */
|
/* Custom neon accent colors */
|
||||||
:root {
|
:root {
|
||||||
/* Custom colors for Mass Effect theme */
|
--neon-cyan: #00f0ff;
|
||||||
--mass-effect-dark: #0a0a12;
|
--neon-purple: #b026ff;
|
||||||
--mass-effect-cyan: #00d0e0;
|
--neon-lime: #39ff14;
|
||||||
--mass-effect-gold: #f0c040;
|
--neon-gold: #ffd700;
|
||||||
--mass-effect-cyan-glow: rgba(0, 208, 224, 0.5);
|
--neon-coral: #ff6b6b;
|
||||||
--mass-effect-gold-glow: rgba(240, 192, 64, 0.5);
|
|
||||||
--glass-bg: rgba(10, 10, 18, 0.7);
|
/* Glow effects */
|
||||||
--glass-border: rgba(0, 208, 224, 0.2);
|
--glow-cyan: rgba(0, 240, 255, 0.4);
|
||||||
|
--glow-purple: rgba(176, 38, 255, 0.4);
|
||||||
|
--glow-lime: rgba(57, 255, 20, 0.4);
|
||||||
|
--glow-gold: rgba(255, 215, 0, 0.4);
|
||||||
|
--glow-coral: rgba(255, 107, 107, 0.4);
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 0.15s ease;
|
||||||
|
--transition-normal: 0.3s ease;
|
||||||
|
|
||||||
|
/* Glassmorphism */
|
||||||
|
--glass-bg: rgba(15, 15, 15, 0.8);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glassmorphism effect */
|
/* Glassmorphism effect */
|
||||||
.glass {
|
.glass {
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glow effects */
|
/* Glow effects */
|
||||||
.glow-cyan {
|
.glow-cyan {
|
||||||
box-shadow: 0 0 10px var(--mass-effect-cyan-glow);
|
box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glow-cyan-intense {
|
.glow-purple {
|
||||||
box-shadow: 0 0 20px var(--mass-effect-cyan-glow), 0 0 40px var(--mass-effect-cyan);
|
box-shadow: 0 0 20px var(--glow-purple), 0 0 40px rgba(176, 38, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-lime {
|
||||||
|
box-shadow: 0 0 20px var(--glow-lime), 0 0 40px rgba(57, 255, 20, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glow-gold {
|
.glow-gold {
|
||||||
box-shadow: 0 0 10px var(--mass-effect-gold-glow);
|
box-shadow: 0 0 20px var(--glow-gold), 0 0 40px rgba(255, 215, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text effects */
|
.glow-coral {
|
||||||
|
box-shadow: 0 0 20px var(--glow-coral), 0 0 40px rgba(255, 107, 107, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text glow effects */
|
||||||
.text-glow-cyan {
|
.text-glow-cyan {
|
||||||
text-shadow: 0 0 10px var(--mass-effect-cyan-glow);
|
text-shadow: 0 0 10px var(--glow-cyan), 0 0 20px rgba(0, 240, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-purple {
|
||||||
|
text-shadow: 0 0 10px var(--glow-purple), 0 0 20px rgba(176, 38, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-lime {
|
||||||
|
text-shadow: 0 0 10px var(--glow-lime), 0 0 20px rgba(57, 255, 20, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-glow-gold {
|
.text-glow-gold {
|
||||||
text-shadow: 0 0 10px var(--mass-effect-gold-glow);
|
text-shadow: 0 0 10px var(--glow-gold), 0 0 20px rgba(255, 215, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Holographic effect */
|
/* Gradient text */
|
||||||
.holographic {
|
.gradient-text {
|
||||||
position: relative;
|
background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple));
|
||||||
overflow: hidden;
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.holographic::before {
|
.gradient-text-gold {
|
||||||
|
background: linear-gradient(135deg, var(--neon-gold), var(--neon-coral));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effects */
|
||||||
|
.card-hover {
|
||||||
|
transition: transform var(--transition-fast), box-shadow var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-4px) scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thick bottom border for cards */
|
||||||
|
.card-border-cyan {
|
||||||
|
border-bottom: 3px solid var(--neon-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-border-purple {
|
||||||
|
border-bottom: 3px solid var(--neon-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-border-lime {
|
||||||
|
border-bottom: 3px solid var(--neon-lime);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-border-gold {
|
||||||
|
border-bottom: 3px solid var(--neon-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-border-coral {
|
||||||
|
border-bottom: 3px solid var(--neon-coral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category badges */
|
||||||
|
.category-badge {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Staggered animation */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.5s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse animation for system status */
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 10px var(--glow-cyan);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
box-shadow: 0 0 20px var(--glow-cyan), 0 0 30px rgba(0, 240, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-glow {
|
||||||
|
animation: pulse-glow 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline effect */
|
||||||
|
.scanline::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -174,13 +293,13 @@
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
transparent,
|
transparent,
|
||||||
rgba(0, 240, 255, 0.2),
|
rgba(0, 240, 255, 0.1),
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
animation: holographic-scan 3s infinite;
|
animation: scanline 3s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes holographic-scan {
|
@keyframes scanline {
|
||||||
0% {
|
0% {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
}
|
}
|
||||||
@@ -189,27 +308,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pulse animation for system status */
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse {
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover glow effect */
|
|
||||||
.hover-glow:hover {
|
|
||||||
box-shadow: 0 0 15px var(--mass-effect-cyan-glow);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Starfield background */
|
/* Starfield background */
|
||||||
.starfield {
|
.starfield {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -219,14 +317,14 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(2px 2px at 20px 30px, #eee, transparent),
|
radial-gradient(1px 1px at 20px 30px, rgba(255,255,255,0.3), transparent),
|
||||||
radial-gradient(2px 2px at 40px 70px, #eee, transparent),
|
radial-gradient(1px 1px at 40px 70px, rgba(255,255,255,0.2), transparent),
|
||||||
radial-gradient(1px 1px at 50px 50px, #eee, transparent),
|
radial-gradient(1px 1px at 50px 50px, rgba(255,255,255,0.4), transparent),
|
||||||
radial-gradient(1px 1px at 80px 10px, #eee, transparent),
|
radial-gradient(1px 1px at 80px 10px, rgba(255,255,255,0.2), transparent),
|
||||||
radial-gradient(2px 2px at 130px 80px, #eee, transparent);
|
radial-gradient(1px 1px at 130px 80px, rgba(255,255,255,0.3), transparent);
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
background-size: 200px 200px;
|
background-size: 200px 200px;
|
||||||
opacity: 0.3;
|
opacity: 0.5;
|
||||||
animation: starfield-move 120s linear infinite;
|
animation: starfield-move 120s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,27 +337,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom button styles */
|
/* Hover glow effect */
|
||||||
.btn-mission {
|
.hover-glow:hover {
|
||||||
background: linear-gradient(45deg, var(--mass-effect-cyan), var(--mass-effect-gold));
|
box-shadow: 0 0 20px var(--glow-cyan);
|
||||||
border: none;
|
transform: translateY(-2px);
|
||||||
color: var(--mass-effect-dark);
|
transition: all var(--transition-normal);
|
||||||
font-weight: bold;
|
}
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
/* Button hover effects */
|
||||||
padding: 12px 24px;
|
.btn-neon {
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-mission:hover {
|
.btn-neon::before {
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 0 20px var(--mass-effect-cyan-glow), 0 0 40px var(--mass-effect-gold-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-mission::before {
|
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -275,23 +367,180 @@
|
|||||||
transition: left 0.5s;
|
transition: left 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-mission:hover::before {
|
.btn-neon:hover::before {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search bar glow effect */
|
.btn-neon:hover {
|
||||||
.search-glow:focus {
|
transform: scale(1.02);
|
||||||
box-shadow: 0 0 0 1px var(--mass-effect-cyan), 0 0 15px var(--mass-effect-cyan-glow);
|
box-shadow: 0 0 20px var(--glow-cyan);
|
||||||
border-color: var(--mass-effect-cyan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icon glow on hover */
|
||||||
|
.icon-glow:hover {
|
||||||
|
filter: drop-shadow(0 0 8px var(--glow-cyan));
|
||||||
|
transition: filter var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dense grid layout */
|
||||||
|
.grid-dense {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bento grid layout */
|
||||||
|
.bento-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-item-large {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bento-item-large {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive typography */
|
||||||
|
.text-responsive-xl {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-responsive-2xl {
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-responsive-3xl {
|
||||||
|
font-size: clamp(2.5rem, 6vw, 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Number display for stats */
|
||||||
|
.stat-number {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient border */
|
||||||
|
.gradient-border {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-border::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple));
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: var(--font-display), sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-display), sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre, .mono {
|
||||||
|
font-family: var(--font-mono), monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game Cover Styles */
|
||||||
|
.game-cover-wrapper {
|
||||||
|
animation: fadeInUp 0.5s ease forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-wrapper:hover {
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-image img {
|
||||||
|
transition: transform 0.3s ease-out, filter 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Discovery Wall Grid - Dense Masonry Layout */
|
||||||
|
.discovery-wall {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 0;
|
||||||
|
margin: 0 -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.discovery-wall {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.discovery-wall {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State Animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse animation for empty state button */
|
||||||
|
@keyframes neon-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 20px var(--glow-cyan), 0 0 40px rgba(0, 240, 255, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 30px var(--glow-cyan), 0 0 60px rgba(0, 240, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon-pulse:hover {
|
||||||
|
animation: neon-pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section header with gradient line */
|
||||||
|
.section-header {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--neon-cyan), var(--neon-purple));
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Space_Grotesk, JetBrains_Mono } from 'next/font/google';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
variable: '--font-geist-sans',
|
variable: '--font-display',
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const jetBrainsMono = JetBrains_Mono({
|
||||||
variable: '--font-geist-mono',
|
variable: '--font-mono',
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -32,7 +34,9 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="es" suppressHydrationWarning>
|
<html lang="es" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
<body className={`${spaceGrotesk.variable} ${jetBrainsMono.variable} antialiased`}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +1,156 @@
|
|||||||
import Link from 'next/link';
|
'use client';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Game } from '@/lib/api';
|
||||||
|
import { gamesApi } from '@/lib/api';
|
||||||
|
import { Gamepad2 } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Gamepad2, HardDrive, Import, Clock, Activity, Database } from 'lucide-react';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { GameCover } from '@/components/landing/GameCover';
|
||||||
|
import { EmptyState } from '@/components/landing/EmptyState';
|
||||||
|
import { ArrowRight, Grid3X3 } from 'lucide-react';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [games, setGames] = useState<Game[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchGames() {
|
||||||
|
try {
|
||||||
|
const allGames = await gamesApi.getAll();
|
||||||
|
// Ordenar por fecha de creación (más recientes primero)
|
||||||
|
const sortedGames = allGames.sort(
|
||||||
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
|
// Limitar a los últimos 16 juegos para la página principal
|
||||||
|
setGames(sortedGames.slice(0, 16));
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Error desconocido';
|
||||||
|
setError(`Error al cargar juegos: ${errorMessage}`);
|
||||||
|
console.error('Error fetching games:', err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchGames();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Starfield background */}
|
||||||
|
<div className="starfield" />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="border-b border-border">
|
<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">
|
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-3">
|
||||||
<Gamepad2 className="w-6 h-6 text-primary" />
|
<div className="relative">
|
||||||
<h1 className="text-xl font-bold">Quasar</h1>
|
<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>
|
||||||
<Badge variant="outline">v1.0.0</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="relative z-10">
|
||||||
<div className="space-y-8">
|
{isLoading ? (
|
||||||
{/* Statistics Cards */}
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<section>
|
<div className="text-center">
|
||||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
<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" />
|
||||||
<Activity className="w-5 h-5" />
|
<p className="text-muted-foreground mono text-sm tracking-wider">CARGANDO...</p>
|
||||||
Estadísticas
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardDescription>Total de Juegos</CardDescription>
|
|
||||||
<CardTitle className="text-3xl">0</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Database className="w-4 h-4" />
|
|
||||||
<span>Base de datos</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardDescription>Juegos Importados</CardDescription>
|
|
||||||
<CardTitle className="text-3xl">0</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Database className="w-4 h-4" />
|
|
||||||
<span>Desde archivos ROM</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardDescription>Última Importación</CardDescription>
|
|
||||||
<CardTitle className="text-3xl">-</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Clock className="w-4 h-4" />
|
|
||||||
<span>Sin registros</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<CardDescription>Estado del Sistema</CardDescription>
|
|
||||||
<CardTitle className="text-3xl">
|
|
||||||
<Badge variant="default">Activo</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Activity className="w-4 h-4" />
|
|
||||||
<span>Operativo</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
) : error ? (
|
||||||
{/* Quick Actions */}
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<section>
|
<div className="text-center">
|
||||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
<p className="text-[var(--neon-coral)] mono text-sm tracking-wider mb-4">{error}</p>
|
||||||
<Import className="w-5 h-5" />
|
<Button
|
||||||
Acciones Rápidas
|
onClick={() => window.location.reload()}
|
||||||
</h2>
|
className="btn-neon bg-[var(--neon-cyan)] text-background hover:bg-[var(--neon-cyan)]/90"
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
>
|
||||||
<Card>
|
REINTENTAR
|
||||||
<CardHeader>
|
</Button>
|
||||||
<CardTitle>Gestión de Juegos</CardTitle>
|
|
||||||
<CardDescription>Ver y administrar tu biblioteca de videojuegos</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Link href="/games">
|
|
||||||
<Button className="w-full">
|
|
||||||
<Gamepad2 className="w-4 h-4 mr-2" />
|
|
||||||
Ir a Juegos
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Importar Juegos</CardTitle>
|
|
||||||
<CardDescription>Importa juegos desde archivos ROM</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Link href="/import">
|
|
||||||
<Button className="w-full" variant="outline">
|
|
||||||
<Import className="w-4 h-4 mr-2" />
|
|
||||||
Importar Archivos
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
) : games.length === 0 ? (
|
||||||
{/* Recent Activity */}
|
<EmptyState />
|
||||||
<section>
|
) : (
|
||||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<Clock className="w-5 h-5" />
|
{/* Section Header */}
|
||||||
Actividad Reciente
|
<div className="section-header mb-8">
|
||||||
</h2>
|
<div className="flex items-center justify-between">
|
||||||
<Card>
|
<div>
|
||||||
<CardContent className="py-6">
|
<h2 className="text-responsive-2xl font-bold mb-2">
|
||||||
<div className="text-center text-muted-foreground py-8">
|
<span className="gradient-text">ÚLTIMOS JUEGOS</span>
|
||||||
<Activity className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
</h2>
|
||||||
<p>No hay actividad reciente</p>
|
<p className="text-muted-foreground mono text-sm tracking-wider">
|
||||||
<p className="text-sm">Las importaciones y cambios aparecerán aquí</p>
|
{games.length} JUEGOS EN TU BIBLIOTECA
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
<Link href="/games">
|
||||||
</Card>
|
<Button
|
||||||
</section>
|
variant="ghost"
|
||||||
</div>
|
size="sm"
|
||||||
|
className="text-[var(--neon-purple)] hover:bg-[var(--neon-purple)]/10 hover:text-[var(--neon-purple)] group"
|
||||||
|
>
|
||||||
|
<span className="mono text-xs tracking-wider mr-2">VER TODOS</span>
|
||||||
|
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discovery Wall - Dense Grid */}
|
||||||
|
<div className="discovery-wall">
|
||||||
|
{games.map((game, index) => (
|
||||||
|
<GameCover key={game.id} game={game} index={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Load More Button */}
|
||||||
|
{games.length >= 16 && (
|
||||||
|
<div className="flex justify-center mt-8">
|
||||||
|
<Link href="/games">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="btn-neon bg-transparent border-2 border-[var(--neon-cyan)] text-[var(--neon-cyan)] hover:bg-[var(--neon-cyan)] hover:text-background font-bold"
|
||||||
|
>
|
||||||
|
<Grid3X3 className="w-5 h-5 mr-2" />
|
||||||
|
EXPLORAR MÁS JUEGOS
|
||||||
|
<ArrowRight className="w-5 h-5 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
66
frontend/src/components/landing/EmptyState.tsx
Normal file
66
frontend/src/components/landing/EmptyState.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Gamepad2, Sparkles, ArrowRight } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function EmptyState() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4">
|
||||||
|
{/* Icono animado */}
|
||||||
|
<div className="relative mb-8">
|
||||||
|
<div className="absolute inset-0 blur-3xl bg-[var(--neon-cyan)]/20" />
|
||||||
|
<div className="empty-icon relative">
|
||||||
|
<Gamepad2 className="w-32 h-32 text-[var(--neon-cyan)]" />
|
||||||
|
</div>
|
||||||
|
{/* Sparkles decorativos */}
|
||||||
|
<Sparkles className="absolute -top-4 -right-4 w-8 h-8 text-[var(--neon-purple)] animate-pulse" />
|
||||||
|
<Sparkles
|
||||||
|
className="absolute -bottom-4 -left-4 w-6 h-6 text-[var(--neon-lime)] animate-pulse"
|
||||||
|
style={{ animationDelay: '0.5s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Título principal */}
|
||||||
|
<h1 className="text-responsive-3xl font-bold text-center mb-4">
|
||||||
|
<span className="gradient-text">TU BIBLIOTECA</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-white">ESTÁ VACÍA</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Descripción motivadora */}
|
||||||
|
<p className="text-lg text-muted-foreground text-center max-w-md mb-8 leading-relaxed">
|
||||||
|
Es hora de comenzar tu colección de videojuegos.
|
||||||
|
<br />
|
||||||
|
<span className="text-[var(--neon-cyan)] font-mono text-sm tracking-wider">
|
||||||
|
Importa tus ROMs o añade juegos manualmente
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Botones de acción */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 w-full max-w-md">
|
||||||
|
<Link href="/import" className="flex-1">
|
||||||
|
<Button className="w-full btn-neon btn-neon-pulse bg-[var(--neon-cyan)] text-background hover:bg-[var(--neon-cyan)]/90 font-bold text-lg py-6">
|
||||||
|
<Gamepad2 className="w-5 h-5 mr-2" />
|
||||||
|
IMPORTAR JUEGOS
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/games" className="flex-1">
|
||||||
|
<Button className="w-full btn-neon bg-transparent border-2 border-[var(--neon-purple)] text-[var(--neon-purple)] hover:bg-[var(--neon-purple)] hover:text-background font-bold text-lg py-6">
|
||||||
|
<Sparkles className="w-5 h-5 mr-2" />
|
||||||
|
AÑADIR MANUAL
|
||||||
|
<ArrowRight className="w-5 h-5 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info adicional */}
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground mono tracking-wider">
|
||||||
|
Compatible con archivos ROM, IGDB, RAWG y TheGamesDB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
frontend/src/components/landing/GameCover.tsx
Normal file
121
frontend/src/components/landing/GameCover.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Game } from '@/lib/api';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface GameCoverProps {
|
||||||
|
game: Game;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameCover({ game, index }: GameCoverProps) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
// Colores de acento por fuente
|
||||||
|
const sourceColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||||
|
igdb: { bg: 'rgba(0, 240, 255, 0.15)', text: '#00f0ff', border: '#00f0ff' },
|
||||||
|
rawg: { bg: 'rgba(176, 38, 255, 0.15)', text: '#b026ff', border: '#b026ff' },
|
||||||
|
thegamesdb: { bg: 'rgba(57, 255, 20, 0.15)', text: '#39ff14', border: '#39ff14' },
|
||||||
|
rom: { bg: 'rgba(255, 215, 0, 0.15)', text: '#ffd700', border: '#ffd700' },
|
||||||
|
manual: { bg: 'rgba(255, 107, 107, 0.15)', text: '#ff6b6b', border: '#ff6b6b' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = sourceColors[game.source] || sourceColors.manual;
|
||||||
|
|
||||||
|
// URL de la portada o placeholder
|
||||||
|
const coverUrl = game.cover || '/placeholder-game-cover.png';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/games/${game.id}`}
|
||||||
|
className="game-cover-wrapper relative group block overflow-hidden"
|
||||||
|
style={{
|
||||||
|
animationDelay: `${index * 50}ms`,
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{/* Imagen de portada */}
|
||||||
|
<div className="game-cover-image aspect-[3/4] w-full overflow-hidden bg-[#0a0a0a]">
|
||||||
|
<img
|
||||||
|
src={coverUrl}
|
||||||
|
alt={game.title}
|
||||||
|
className="w-full h-full object-cover transition-all duration-300 ease-out group-hover:scale-110 group-hover:brightness-110"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overlay en hover */}
|
||||||
|
<div className="game-cover-overlay absolute inset-0 bg-gradient-to-t from-black via-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out pointer-events-none">
|
||||||
|
{/* Badge de fuente */}
|
||||||
|
<div className="absolute top-3 left-3 right-3">
|
||||||
|
<Badge
|
||||||
|
className="category-badge font-mono text-xs tracking-wider uppercase"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
color: colors.text,
|
||||||
|
borderColor: colors.border,
|
||||||
|
borderWidth: '1px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{game.source}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenido del overlay */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||||
|
{/* Título del juego */}
|
||||||
|
<h3 className="font-display font-bold text-lg text-white mb-2 line-clamp-2 leading-tight">
|
||||||
|
{game.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Metadatos */}
|
||||||
|
<div className="flex items-center gap-3 text-xs mono text-gray-300">
|
||||||
|
{game.year && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="text-[var(--neon-cyan)]">{game.year}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{game.platform && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="text-[var(--neon-purple)]">{game.platform}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{game.genre && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="text-[var(--neon-lime)]">{game.genre}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Indicador de enlace */}
|
||||||
|
<div className="mt-3 flex items-center gap-1 text-xs text-[var(--neon-cyan)] font-mono tracking-wider">
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
<span>VER DETALLES</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Glow effect en hover */}
|
||||||
|
<div className="game-cover-glow absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
boxShadow: `inset 0 0 40px ${colors.text}20`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Borde brillante en hover */}
|
||||||
|
<div
|
||||||
|
className="game-cover-border absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
boxShadow: `inset 0 0 0 1px ${colors.text}40`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api';
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3003/api';
|
||||||
|
|
||||||
// Tipos para relaciones del juego
|
// Tipos para relaciones del juego
|
||||||
export interface Artwork {
|
export interface Artwork {
|
||||||
|
|||||||
Reference in New Issue
Block a user