Compare commits

1 Commits

Author SHA1 Message Date
4040988e75 pre-main 2026-01-30 15:23:59 +03:00
19 changed files with 648 additions and 1085 deletions

BIN
.DS_Store vendored

Binary file not shown.

12
.env Normal file
View File

@@ -0,0 +1,12 @@
# NextAuth Configuration
# Generate a secret with: openssl rand -base64 32
NEXTAUTH_URL=http://localhost:3001
NEXTAUTH_SECRET=your-secret-key-here
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:3000/api
# Auth Mode: true = login required, false = public access with optional login
NEXT_PUBLIC_AUTH_REQUIRED=false
NEXT_PUBLIC_GOOGLE_API_KEY='api-key'

View File

@@ -10,7 +10,7 @@ const nextConfig: NextConfig = {
return [
{
source: "/api/backend/:path*",
destination: "http://localhost:4000/api/:path*",
destination: "http://localhost:3000/api/:path*",
},
];
},

960
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev --webpack --experimental-https -p 3001",
"dev": "next dev --webpack -p 3001",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint"

View File

@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation';
import { Container, Heading, Text, Box, Image, Badge, HStack, VStack, Icon, Flex } from '@chakra-ui/react';
import { MOCK_GAMES } from '@/lib/api/mock-data';
import { gamesApi } from '@/lib/api/games';
import { FaCalendar, FaGamepad } from 'react-icons/fa';
interface PageProps {
@@ -12,16 +12,30 @@ interface PageProps {
export default async function GameDetailsPage({ params }: PageProps) {
const { slug } = await params;
const game = MOCK_GAMES.find((g) => g.slug === slug);
let game = null;
try {
const res = await gamesApi.getBySlug(slug);
game = res.data?.data;
} catch (error) {
console.error('Game not found or error', error);
}
if (!game) {
notFound();
}
// Map platforms to string array for display if they are objects
const platforms = game.platforms?.map((p: any) => p.platform?.name || p) || [];
// Valid date check
const releaseDateObj = game.releaseDate ? new Date(game.releaseDate) : null;
// Format date
const formattedDate = new Intl.DateTimeFormat('en-US', {
const formattedDate = releaseDateObj ? new Intl.DateTimeFormat('en-US', {
dateStyle: 'long',
}).format(game.releaseDate);
}).format(releaseDateObj) : 'TBD';
return (
<Container maxW="6xl" py={12} minH="100vh">
@@ -36,7 +50,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
boxShadow="2xl"
>
<Image
src={game.coverImage}
src={game.coverImage || 'https://placehold.co/1280x720?text=No+Image'}
alt={game.title}
objectFit="cover"
w="full"
@@ -55,9 +69,9 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Heading size="3xl" color="white" textShadow="lg">{game.title}</Heading>
<HStack gap={4}>
<Badge colorScheme="purple" fontSize="0.9em" px={3} py={1} borderRadius="full">
{new Date() < game.releaseDate ? 'Upcoming' : 'Released'}
{releaseDateObj && new Date() < releaseDateObj ? 'Upcoming' : 'Released'}
</Badge>
{game.platforms.map(p => (
{platforms.map((p: string) => (
<Badge key={p} variant="outline" colorScheme="cyan" px={2}>
{p}
</Badge>
@@ -73,10 +87,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box flex="1">
<Heading size="lg" mb={4}>About</Heading>
<Text fontSize="lg" lineHeight="tall" color="whiteAlpha.800">
{/* Fallback description since mock doesn't have it yet */}
{game.title} is an anticipated title releasing on {formattedDate}.
Experience the next chapter in this gaming masterpiece.
Prepare to embark on a journey like no other.
{game.description || `${game.title} is an upcoming title.`}
</Text>
</Box>
@@ -95,7 +106,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box>
<HStack mb={1} color="gray.400">
<Icon as={FaCalendar} />
<Icon><FaCalendar/></Icon>
<Text fontSize="sm">Release Date</Text>
</HStack>
<Text fontSize="xl" fontWeight="bold">{formattedDate}</Text>
@@ -103,11 +114,11 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box>
<HStack mb={1} color="gray.400">
<Icon as={FaGamepad} />
<Icon><FaGamepad/></Icon>
<Text fontSize="sm">Platforms</Text>
</HStack>
<Flex gap={2} wrap="wrap">
{game.platforms.map(p => (
{platforms.map((p: string) => (
<Badge key={p} colorScheme="green">{p}</Badge>
))}
</Flex>

View File

@@ -1,28 +1,61 @@
import { Container, VStack, Heading, Text, Box } from '@chakra-ui/react';
import { Container, VStack, Heading, Text, Box, Flex, Badge } from '@chakra-ui/react';
import { GameCalendar } from '@/components/features/calendar/game-calendar';
import { MOCK_EVENTS, MOCK_GAMES } from '@/lib/api/mock-data';
import { gamesApi, Game } from '@/lib/api/games';
import { eventsApi, Event } from '@/lib/api/events';
import { ThemeSwitcher } from '@/components/features/theme-switcher';
import { useTranslations } from 'next-intl';
import { SyncButton } from '@/components/features/sync/sync-button';
export default function HomePage() {
// Static for now, in future useTranslations
// const t = useTranslations('dashboard');
export default async function HomePage() {
// Fetch data
let games: Game[] = [];
let events: Event[] = [];
try {
const gamesRes = await gamesApi.getAll({ limit: 100 });
games = gamesRes.data?.items || [];
// Attempt to fetch events, fallback to empty if fails
try {
const eventsRes = await eventsApi.getAll({ limit: 100 });
events = eventsRes.data?.items || [];
} catch (e) {
console.error('Failed to fetch events', e);
}
} catch (error) {
console.error('Failed to fetch games', error);
}
return (
<Container maxW="8xl" py={8} position="relative" minH="100vh">
<VStack spaceY={8} align="stretch">
{/* Hero Section / GOTY Banner could go here */}
<Container maxW="8xl" py={12} position="relative" minH="100vh">
<VStack spaceY={12} align="stretch">
{/* Hero / Header Section */}
<Flex justify="space-between" align="flex-end" pb={4} borderBottom="1px solid" borderColor="whiteAlpha.100">
<Box>
<Heading size="3xl" mb={2} color="white">Calendar</Heading>
<Text fontSize="lg" color="whiteAlpha.800">Track releases, events, and showcases in one place.</Text>
<Badge mb={2} colorScheme="brand" variant="solid" rounded="full" px={3}>Official Calendar</Badge>
<Heading
size="4xl"
fontWeight="900"
bgGradient="linear(to-r, white, brand.300)"
bgClip="text"
letterSpacing="tight"
>
Game Calendar
</Heading>
<Text fontSize="xl" color="whiteAlpha.700" mt={2} maxW="2xl">
Track the latest game releases, industry events, and showcases in one unified timeline.
</Text>
</Box>
<SyncButton />
</Flex>
{/* Calendar */}
<GameCalendar games={MOCK_GAMES} events={MOCK_EVENTS} />
<GameCalendar games={games} events={events} />
</VStack>
{/* Debug Switcher */}
<Box position="fixed" bottom={4} right={4} opacity={0.5} _hover={{ opacity: 1 }} transition="opacity 0.2s">
<ThemeSwitcher />
</Box>
</Container>
);
}

View File

@@ -4,9 +4,14 @@ import { Container, Flex } from '@chakra-ui/react';
import Header from '@/components/layout/header/header';
import Footer from '@/components/layout/footer/footer';
import BackToTop from '@/components/ui/back-to-top';
import { GamerBackground } from '@/components/ui/gamer-background';
import { PageBackground } from '@/components/layout/page-background';
function MainLayout({ children }: { children: React.ReactNode }) {
return (
<>
<GamerBackground />
<PageBackground>
<Flex minH='100vh' direction='column'>
<Header />
<Container as='main' maxW='8xl' flex='1' py={4}>
@@ -15,6 +20,8 @@ function MainLayout({ children }: { children: React.ReactNode }) {
<BackToTop />
<Footer />
</Flex>
</PageBackground>
</>
);
}

View File

@@ -1,12 +0,0 @@
import { GameDetail } from "@/components/features/games/GameDetail";
type Props = {
params: Promise<{ slug: string }>;
};
export default async function GamePage({ params }: Props) {
const { slug } = await params;
console.log('Server Page received slug:', slug);
return <GameDetail slug={slug} />;
}

View File

@@ -33,28 +33,47 @@ export function FilterBar({ filters, onFilterChange }: FilterBarProps) {
};
return (
<HStack w="full" gap={4} mb={6} bg="whiteAlpha.100" p={4} borderRadius="xl" backdropFilter="blur(10px)">
<InputGroup maxW="400px" startElement={<Icon as={FaSearch} color="gray.400" />}>
<HStack w="full" gap={4} mb={0} bg="transparent" p={0}>
<InputGroup maxW="300px" startElement={<Icon as={FaSearch} color="whiteAlpha.400" />}>
<Input
placeholder="Search games..."
value={filters.search}
onChange={handleSearchChange}
variant="subtle"
bg="blackAlpha.300"
_hover={{ bg: "blackAlpha.400" }}
_focus={{ bg: "blackAlpha.400", borderColor: "purple.400" }}
variant="flushed"
bg="blackAlpha.200"
borderBottom="1px solid"
borderColor="whiteAlpha.300"
_hover={{ borderColor: "brand.400" }}
_focus={{ borderColor: "brand.300", bg: "blackAlpha.400" }}
color="white"
py={2}
/>
</InputGroup>
<MenuRoot closeOnSelect={false}>
<MenuTrigger asChild>
<Button variant="outline" colorScheme="purple">
Filters <Icon as={FaFilter} ml={2} />
<Button
variant="subtle"
colorPalette="brand"
size="sm"
border="1px solid"
borderColor="whiteAlpha.200"
_hover={{ bg: "whiteAlpha.100", borderColor: "brand.400" }}
>
Filters <Icon as={FaFilter} />
</Button>
</MenuTrigger>
<MenuContent bg="gray.800" borderColor="whiteAlpha.200">
<Box px={4} py={2}>
<Text fontSize="xs" fontWeight="bold" color="gray.400" mb={2} textTransform="uppercase">Platforms</Text>
<MenuContent
bg="rgba(10, 12, 20, 0.9)"
backdropFilter="blur(20px)"
borderColor="whiteAlpha.200"
boxShadow="xl"
rounded="xl"
>
<Box px={4} py={3}>
<Text fontSize="xs" fontWeight="bold" color="brand.200" mb={3} textTransform="uppercase" letterSpacing="widest">
Platforms
</Text>
<VStack align="start" gap={2}>
{['PC', 'PS5', 'Xbox', 'Switch'].map(p => (
<MenuCheckboxItem

View File

@@ -1,19 +1,16 @@
'use client';
import { Link } from '@/i18n/navigation';
import { Box, Grid, Heading, Text, VStack, Badge, Flex, Image as ChakraImage, SimpleGrid } from '@chakra-ui/react';
import { Box, Heading, Text, VStack, Badge, Flex, IconButton, SimpleGrid, GridItem, Tooltip } from '@chakra-ui/react';
import { useState } from 'react';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths, getDay } from 'date-fns';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths, getDay, isToday } from 'date-fns';
import { enUS, tr } from 'date-fns/locale';
import { useLocale } from 'next-intl';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { IconButton } from '@chakra-ui/react';
import { FaChevronLeft, FaChevronRight, FaGamepad, FaCalendarAlt } from 'react-icons/fa';
import { FilterBar, FilterState } from './filter-bar';
// import { Game, Event } from '@prisma/client'; // Removed dependency
// Note: In a real monorepo we'd share types. For now we will mock or use any.
interface CalendarProps {
games: any[]; // Replace with correct type
games: any[];
events: any[];
}
@@ -30,33 +27,20 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(monthStart);
const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd });
// Add padding days for start of month
const startDayOfWeek = getDay(monthStart); // 0 (Sun) to 6 (Sat)
// Adjust for Monday start if needed. Let's assume standard Sunday start for grid.
const startDayOfWeek = getDay(monthStart);
const paddingDays = Array.from({ length: startDayOfWeek });
const nextMonth = () => setCurrentDate(addMonths(currentDate, 1));
const prevMonth = () => setCurrentDate(subMonths(currentDate, 1));
const getItemsForDay = (date: Date) => {
// Filter Games
const dayGames = games.filter(g => {
if (!g.releaseDate || !isSameDay(new Date(g.releaseDate), date)) return false;
// Search filter
if (filters.search && !g.title.toLowerCase().includes(filters.search.toLowerCase())) return false;
// Platform filter (if game has platforms)
if (g.platforms && g.platforms.length > 0) {
const hasPlatform = g.platforms.some((p: string) => filters.platforms.includes(p) || filters.platforms.some(fp => p.includes(fp)));
if (!hasPlatform) return false;
}
// Platform filter logic can be refined
return true;
});
// Filter Events
const dayEvents = filters.showEvents ? events.filter(e => {
if (!isSameDay(new Date(e.startTime), date)) return false;
if (filters.search && !e.title.toLowerCase().includes(filters.search.toLowerCase())) return false;
@@ -67,98 +51,181 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
};
return (
<Box w="full" bg="whiteAlpha.300" rounded="xl" p={6} backdropFilter="blur(10px)" border="1px solid" borderColor="whiteAlpha.200">
{/* Header */}
<Flex justify="space-between" align="center" mb={6}>
<Heading size="lg" color="white">
<Box
w="full"
position="relative"
isolation="isolate"
>
{/* Header / Controls */}
<Flex
direction={{ base: 'column', md: 'row' }}
justify="space-between"
align="center"
mb={8}
gap={4}
bg="whiteAlpha.50"
p={4}
rounded="2xl"
border="1px solid"
borderColor="whiteAlpha.100"
backdropFilter="blur(12px)"
boxShadow="lg"
>
<Flex align="center" gap={4}>
<Heading size="xl" fontWeight="900" bgGradient="linear(to-r, white, brand.200)" bgClip="text">
{format(currentDate, 'MMMM yyyy', { locale: dateLocale })}
</Heading>
<Flex gap={2}>
<IconButton aria-label="Previous month" onClick={prevMonth} variant="ghost" color="white">
<Flex bg="blackAlpha.400" rounded="full" p={1} border="1px solid" borderColor="whiteAlpha.200">
<IconButton
aria-label="Previous month"
onClick={prevMonth}
variant="ghost"
size="sm"
rounded="full"
color="gray.400"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
>
<FaChevronLeft />
</IconButton>
<IconButton aria-label="Next month" onClick={nextMonth} variant="ghost" color="white">
<IconButton
aria-label="Next month"
onClick={nextMonth}
variant="ghost"
size="sm"
rounded="full"
color="gray.400"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
>
<FaChevronRight />
</IconButton>
</Flex>
</Flex>
{/* Filters */}
<Box w={{ base: "full", md: "auto" }}>
<FilterBar filters={filters} onFilterChange={setFilters} />
</Box>
</Flex>
{/* Calendar Grid Container with Glassmorphism */}
<Box
bg="rgba(10, 10, 15, 0.4)"
backdropFilter="blur(16px)"
border="1px solid"
borderColor="whiteAlpha.100"
rounded="3xl"
p={6}
boxShadow="2xl"
position="relative"
overflow="hidden"
>
{/* Decorative border gradient */}
<Box
position="absolute" inset="0" rounded="3xl" p="1px"
bgGradient="linear(to-br, whiteAlpha.200, transparent)"
pointerEvents="none"
zIndex="0"
/>
{/* Days Header */}
<SimpleGrid columns={7} mb={2}>
<SimpleGrid columns={7} mb={4} textAlign="center" position="relative" zIndex="1">
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
<Text key={day} textAlign="center" color="gray.400" fontWeight="bold" fontSize="sm">
<Text key={day} color="gray.500" fontWeight="bold" fontSize="xs" textTransform="uppercase" letterSpacing="wider">
{day}
</Text>
))}
</SimpleGrid>
{/* Calendar Grid */}
<SimpleGrid columns={7} gap={1} minH="500px">
{/* Grid */}
<SimpleGrid columns={7} gap={3} position="relative" zIndex="1">
{paddingDays.map((_, i) => (
<Box key={`padding-${i}`} bg="transparent" />
<Box key={`padding-${i}`} />
))}
{daysInMonth.map((date) => {
const items = getItemsForDay(date);
const isToday = isSameDay(date, new Date());
const isCurrentDay = isToday(date);
const hasItems = items.length > 0;
return (
<Box
key={date.toString()}
bg={isToday ? 'primary.900' : 'whiteAlpha.200'}
minH="140px"
bg={isCurrentDay ? 'brand.900' : 'whiteAlpha.50'}
border="1px solid"
borderColor={isToday ? 'primary.500' : 'whiteAlpha.100'}
rounded="md"
p={2}
minH="100px"
transition="all 0.2s"
_hover={{ bg: 'whiteAlpha.200', transform: 'scale(1.02)', zIndex: 1 }}
cursor="pointer"
borderColor={isCurrentDay ? 'brand.500' : 'whiteAlpha.50'}
rounded="xl"
p={3}
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
_hover={{
bg: 'whiteAlpha.100',
borderColor: 'brand.400',
transform: 'translateY(-2px)',
boxShadow: '0 4px 20px rgba(0,0,0,0.2)'
}}
position="relative"
overflow="hidden"
data-group
>
{/* Date Number */}
<Text
fontSize="sm"
fontWeight={isCurrentDay ? "900" : "medium"}
color={isCurrentDay ? "brand.300" : "gray.500"}
mb={2}
>
<Text fontSize="sm" color={isToday ? 'primary.300' : 'gray.400'} mb={1} fontWeight={isToday ? 'bold' : 'normal'}>
{format(date, 'd')}
</Text>
<VStack align="stretch" gap={1}>
{/* Items Stack */}
<VStack align="stretch" gap={1.5}>
{items.map((item: any, idx) => {
const badge = (
<Badge
key={`${item.id}-${idx}`}
size="sm"
variant="solid"
colorPalette={item.type === 'game' ? 'blue' : 'purple'}
truncate
fontSize="xs"
px={1}
cursor="pointer"
_hover={{ opacity: 0.8 }}
const isGame = item.type === 'game';
const content = (
<Flex
align="center"
gap={2}
bg={isGame ? "linear-gradient(90deg, rgba(64, 114, 255, 0.1), transparent)" : "linear-gradient(90deg, rgba(235, 64, 255, 0.1), transparent)"}
borderLeft="2px solid"
borderColor={isGame ? "brand.400" : "purple.400"}
p={1.5}
rounded="md"
_hover={{ bg: "whiteAlpha.100" }}
transition="all 0.2s"
>
<Box as={isGame ? FaGamepad : FaCalendarAlt} color={isGame ? "brand.300" : "purple.300"} w="12px" h="12px" />
<Text fontSize="10px" fontWeight="bold" color="whiteAlpha.900" lineClamp={1}>
{item.title}
</Badge>
</Text>
</Flex>
);
if (item.type === 'game') {
if (isGame) {
return (
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`}>
{badge}
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`} style={{ textDecoration: 'none' }}>
{content}
</Link>
);
}
return badge;
return <Box key={`${item.id}-${idx}`}>{content}</Box>;
})}
</VStack>
{/* Background image effect for heavy days? Optional polish */}
{/* Glow effect on hover */}
<Box
position="absolute"
inset="0"
bgGradient={isCurrentDay ? "radial(brand.500 0%, transparent 70%)" : "radial(white 0%, transparent 70%)"}
opacity="0"
_groupHover={{ opacity: 0.05 }}
transition="opacity 0.3s"
pointerEvents="none"
/>
</Box>
);
})}
</SimpleGrid>
</Box>
</Box >
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import { useState } from 'react';
import { IconButton, Tooltip } from '@chakra-ui/react';
import { FaSync } from 'react-icons/fa';
import { useRouter } from 'next/navigation';
import { syncApi } from '@/lib/api/sync';
// We'll use a simple alert or toast if available, or just console for MVP.
// Chakra v3 (which might be used here based on previous files having 'colorPalette')
// Let's stick to standard Chakra props I saw in other files.
export function SyncButton() {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleSync = async () => {
setIsLoading(true);
try {
await syncApi.trigger();
// Refresh the current route to fetch new data
router.refresh();
} catch (error) {
console.error('Sync failed:', error);
} finally {
setIsLoading(false);
}
};
return (
<IconButton
aria-label="Sync Games"
onClick={handleSync}
loading={isLoading}
bgGradient="linear(to-r, brand.500, brand.600)"
_hover={{
bgGradient: "linear(to-r, brand.400, brand.500)",
transform: "scale(1.05)",
boxShadow: "0 0 20px rgba(64, 114, 255, 0.5)"
}}
_active={{ transform: "scale(0.95)" }}
color="white"
variant="solid"
size="md"
rounded="full"
transition="all 0.2s"
>
<FaSync />
</IconButton>
);
}

View File

@@ -142,22 +142,24 @@ export default function Header() {
<>
<Box
as="nav"
bg={isSticky ? "rgba(255, 255, 255, 0.6)" : "white"}
bg={isSticky ? "rgba(5, 5, 10, 0.6)" : "transparent"}
_dark={{
bg: isSticky ? "rgba(1, 1, 1, 0.6)" : "black",
bg: isSticky ? "rgba(5, 5, 10, 0.6)" : "transparent",
}}
shadow={isSticky ? "sm" : "none"}
backdropFilter="blur(12px) saturate(180%)"
border="1px solid"
borderColor={isSticky ? "whiteAlpha.300" : "transparent"}
borderBottomRadius={isSticky ? "xl" : "none"}
transition="all 0.4s ease-in-out"
shadow={isSticky ? "lg" : "none"}
backdropFilter={isSticky ? "blur(16px) saturate(180%)" : "none"}
borderBottom="1px solid"
borderColor={isSticky ? "whiteAlpha.100" : "transparent"}
borderBottomRadius={isSticky ? "2xl" : "none"}
transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)"
mx={isSticky ? { base: 4, md: 8 } : 0}
mt={isSticky ? 4 : 0}
px={{ base: 4, md: 8 }}
py="3"
py="4"
position="sticky"
top={0}
zIndex={10}
w="full"
zIndex={100}
w={isSticky ? "auto" : "full"}
>
<Flex justify="space-between" align="center" maxW="8xl" mx="auto">
{/* Logo */}
@@ -166,11 +168,11 @@ export default function Header() {
as={Link}
href="/home"
fontSize="3xl"
fontWeight="extrabold"
letterSpacing="wide"
fontWeight="900"
letterSpacing="tight"
bgGradient="to-r"
gradientFrom="primary.400"
gradientTo="primary.600"
gradientFrom="brand.300"
gradientTo="brand.500"
bgClip="text"
bgSize="200% auto"
animation="text-gradient 12s linear infinite"

View File

@@ -0,0 +1,11 @@
'use client';
import { Box } from '@chakra-ui/react';
export function PageBackground({ children }: { children: React.ReactNode }) {
return (
<Box minH="100vh" position="relative" zIndex="1" color="white">
{children}
</Box>
)
}

View File

@@ -0,0 +1,84 @@
'use client';
import { Box } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
export function GamerBackground() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<Box
position="fixed"
top="0"
left="0"
right="0"
bottom="0"
zIndex="-1"
bg="black"
overflow="hidden"
>
{/* Main dark gradient base */}
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
bgGradient="linear(to-b, #050505 0%, #0a0a0f 100%)"
/>
{/* Top Left Glow - Purple/Blue */}
<Box
position="absolute"
top="-20%"
left="-10%"
w="800px"
h="800px"
bg="brand.600"
filter="blur(160px)"
opacity="0.15"
borderRadius="full"
animation="pulse 10s infinite alternate"
/>
{/* Bottom Right Glow - Cyan/Teal */}
<Box
position="absolute"
bottom="-20%"
right="-10%"
w="600px"
h="600px"
bg="accent.500"
filter="blur(140px)"
opacity="0.1"
borderRadius="full"
animation="pulse 15s infinite reverse"
/>
{/* Center Subtle Mesh */}
<Box
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
w="100%"
h="100%"
bgGradient="radial(circle, rgba(64, 114, 255, 0.03) 0%, transparent 70%)"
pointerEvents="none"
/>
<style jsx global>{`
@keyframes pulse {
0% { transform: scale(1); opacity: 0.15; }
100% { transform: scale(1.1); opacity: 0.25; }
}
`}</style>
</Box>
);
}

20
src/lib/api/events.ts Normal file
View File

@@ -0,0 +1,20 @@
import { createApiClient } from './create-api-client';
import baseUrl from '@/config/base-url';
export interface Event {
id: string;
slug: string;
title: string;
description?: string;
startTime: string;
endTime?: string;
type: 'SHOWCASE' | 'RELEASE' | 'TOURNAMENT' | 'OTHER';
coverImage?: string;
}
const client = createApiClient(`${baseUrl.core}/events`);
export const eventsApi = {
getAll: (params?: any) => client.get<{ items: Event[]; meta: any }>('/', { params }),
getBySlug: (slug: string) => client.get<{ data: Event }>(`/${slug}`),
};

View File

@@ -1,5 +1,5 @@
import { createApiClient } from './create-api-client';
import baseUrl from '@/config/base-url';
export interface Game {
id: string;
@@ -16,7 +16,7 @@ export interface Game {
screenshots?: { url: string }[];
}
const client = createApiClient('/games');
const client = createApiClient(`${baseUrl.core}/games`);
export const gamesApi = {
getAll: (params?: any) => client.get<{ items: Game[]; meta: any }>('/', { params }),

8
src/lib/api/sync.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createApiClient } from './create-api-client';
import baseUrl from '@/config/base-url';
const client = createApiClient(`${baseUrl.core}/sync`);
export const syncApi = {
trigger: () => client.post('/trigger'),
};

View File

@@ -14,67 +14,52 @@ const customConfig: SystemConfig = {
mono: { value: 'var(--font-bricolage)' },
},
colors: {
primary: {
50: { value: '#E6FFFA' },
100: { value: '#B2F5EA' },
200: { value: '#81E6D9' },
300: { value: '#4FD1C5' },
400: { value: '#38B2AC' },
500: { value: '#319795' },
600: { value: '#2C7A7B' },
700: { value: '#285E61' },
800: { value: '#234E52' },
900: { value: '#1D4044' },
950: { value: '#132E30' },
brand: {
50: { value: '#F0F4FF' },
100: { value: '#D9E2FF' },
200: { value: '#B3C6FF' },
300: { value: '#8DAAFF' },
400: { value: '#668EFF' },
500: { value: '#4072FF' },
600: { value: '#1A56FF' },
700: { value: '#003EBD' },
800: { value: '#002B85' },
900: { value: '#00194D' },
950: { value: '#000D29' },
},
accent: {
50: { value: '#E0FDFF' },
100: { value: '#B3F8FF' },
200: { value: '#80EFFF' },
300: { value: '#4DE6FF' },
400: { value: '#1ADDFF' },
500: { value: '#00C8EB' },
600: { value: '#009AB5' },
700: { value: '#006D80' },
800: { value: '#00404B' },
900: { value: '#00161A' },
},
dark: {
bg: { value: '#050505' },
glass: { value: 'rgba(20, 20, 25, 0.7)' }
}
},
},
semanticTokens: {
colors: {
primary: {
solid: {
value: {
_light: '{colors.primary.600}',
_dark: '{colors.primary.600}',
},
},
contrast: {
value: {
_light: '{colors.white}',
_dark: '{colors.white}',
},
},
fg: {
value: {
_light: '{colors.primary.700}',
_dark: '{colors.primary.300}',
},
},
muted: {
value: {
_light: '{colors.primary.200}',
_dark: '{colors.primary.800}',
},
},
subtle: {
value: {
_light: '{colors.primary.100}',
_dark: '{colors.primary.900}',
},
},
emphasize: {
value: {
_light: '{colors.primary.300}',
_dark: '{colors.primary.700}',
},
},
focusRing: {
value: {
_light: '{colors.primary.500}',
_dark: '{colors.primary.500}',
},
},
solid: { value: '{colors.brand.600}' },
contrast: { value: '{colors.white}' },
fg: { value: '{colors.brand.700}' },
muted: { value: '{colors.brand.200}' },
subtle: { value: '{colors.brand.100}' },
emphasize: { value: '{colors.brand.300}' },
focusRing: { value: '{colors.brand.500}' },
},
bg: {
canvas: { value: '{colors.dark.bg}' },
surface: { value: '{colors.dark.glass}' }
}
},
},
},