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 [ return [
{ {
source: "/api/backend/:path*", 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", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --experimental-https -p 3001", "dev": "next dev --webpack -p 3001",
"build": "next build --webpack", "build": "next build --webpack",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint"

View File

@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { Container, Heading, Text, Box, Image, Badge, HStack, VStack, Icon, Flex } from '@chakra-ui/react'; 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'; import { FaCalendar, FaGamepad } from 'react-icons/fa';
interface PageProps { interface PageProps {
@@ -12,16 +12,30 @@ interface PageProps {
export default async function GameDetailsPage({ params }: PageProps) { export default async function GameDetailsPage({ params }: PageProps) {
const { slug } = await params; 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) { if (!game) {
notFound(); 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 // Format date
const formattedDate = new Intl.DateTimeFormat('en-US', { const formattedDate = releaseDateObj ? new Intl.DateTimeFormat('en-US', {
dateStyle: 'long', dateStyle: 'long',
}).format(game.releaseDate); }).format(releaseDateObj) : 'TBD';
return ( return (
<Container maxW="6xl" py={12} minH="100vh"> <Container maxW="6xl" py={12} minH="100vh">
@@ -36,7 +50,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
boxShadow="2xl" boxShadow="2xl"
> >
<Image <Image
src={game.coverImage} src={game.coverImage || 'https://placehold.co/1280x720?text=No+Image'}
alt={game.title} alt={game.title}
objectFit="cover" objectFit="cover"
w="full" w="full"
@@ -55,9 +69,9 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Heading size="3xl" color="white" textShadow="lg">{game.title}</Heading> <Heading size="3xl" color="white" textShadow="lg">{game.title}</Heading>
<HStack gap={4}> <HStack gap={4}>
<Badge colorScheme="purple" fontSize="0.9em" px={3} py={1} borderRadius="full"> <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> </Badge>
{game.platforms.map(p => ( {platforms.map((p: string) => (
<Badge key={p} variant="outline" colorScheme="cyan" px={2}> <Badge key={p} variant="outline" colorScheme="cyan" px={2}>
{p} {p}
</Badge> </Badge>
@@ -73,10 +87,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box flex="1"> <Box flex="1">
<Heading size="lg" mb={4}>About</Heading> <Heading size="lg" mb={4}>About</Heading>
<Text fontSize="lg" lineHeight="tall" color="whiteAlpha.800"> <Text fontSize="lg" lineHeight="tall" color="whiteAlpha.800">
{/* Fallback description since mock doesn't have it yet */} {game.description || `${game.title} is an upcoming title.`}
{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.
</Text> </Text>
</Box> </Box>
@@ -95,7 +106,7 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box> <Box>
<HStack mb={1} color="gray.400"> <HStack mb={1} color="gray.400">
<Icon as={FaCalendar} /> <Icon><FaCalendar/></Icon>
<Text fontSize="sm">Release Date</Text> <Text fontSize="sm">Release Date</Text>
</HStack> </HStack>
<Text fontSize="xl" fontWeight="bold">{formattedDate}</Text> <Text fontSize="xl" fontWeight="bold">{formattedDate}</Text>
@@ -103,11 +114,11 @@ export default async function GameDetailsPage({ params }: PageProps) {
<Box> <Box>
<HStack mb={1} color="gray.400"> <HStack mb={1} color="gray.400">
<Icon as={FaGamepad} /> <Icon><FaGamepad/></Icon>
<Text fontSize="sm">Platforms</Text> <Text fontSize="sm">Platforms</Text>
</HStack> </HStack>
<Flex gap={2} wrap="wrap"> <Flex gap={2} wrap="wrap">
{game.platforms.map(p => ( {platforms.map((p: string) => (
<Badge key={p} colorScheme="green">{p}</Badge> <Badge key={p} colorScheme="green">{p}</Badge>
))} ))}
</Flex> </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 { 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 { ThemeSwitcher } from '@/components/features/theme-switcher';
import { useTranslations } from 'next-intl'; import { SyncButton } from '@/components/features/sync/sync-button';
export default function HomePage() { export default async function HomePage() {
// Static for now, in future useTranslations // Fetch data
// const t = useTranslations('dashboard'); 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 ( return (
<Container maxW="8xl" py={8} position="relative" minH="100vh"> <Container maxW="8xl" py={12} position="relative" minH="100vh">
<VStack spaceY={8} align="stretch"> <VStack spaceY={12} align="stretch">
{/* Hero Section / GOTY Banner could go here */} {/* Hero / Header Section */}
<Box> <Flex justify="space-between" align="flex-end" pb={4} borderBottom="1px solid" borderColor="whiteAlpha.100">
<Heading size="3xl" mb={2} color="white">Calendar</Heading> <Box>
<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>
</Box> <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 */} {/* Calendar */}
<GameCalendar games={MOCK_GAMES} events={MOCK_EVENTS} /> <GameCalendar games={games} events={events} />
</VStack> </VStack>
{/* Debug Switcher */} {/* Debug Switcher */}
<ThemeSwitcher /> <Box position="fixed" bottom={4} right={4} opacity={0.5} _hover={{ opacity: 1 }} transition="opacity 0.2s">
<ThemeSwitcher />
</Box>
</Container> </Container>
); );
} }

View File

@@ -4,17 +4,24 @@ import { Container, Flex } from '@chakra-ui/react';
import Header from '@/components/layout/header/header'; import Header from '@/components/layout/header/header';
import Footer from '@/components/layout/footer/footer'; import Footer from '@/components/layout/footer/footer';
import BackToTop from '@/components/ui/back-to-top'; 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 }) { function MainLayout({ children }: { children: React.ReactNode }) {
return ( return (
<Flex minH='100vh' direction='column'> <>
<Header /> <GamerBackground />
<Container as='main' maxW='8xl' flex='1' py={4}> <PageBackground>
{children} <Flex minH='100vh' direction='column'>
</Container> <Header />
<BackToTop /> <Container as='main' maxW='8xl' flex='1' py={4}>
<Footer /> {children}
</Flex> </Container>
<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 ( return (
<HStack w="full" gap={4} mb={6} bg="whiteAlpha.100" p={4} borderRadius="xl" backdropFilter="blur(10px)"> <HStack w="full" gap={4} mb={0} bg="transparent" p={0}>
<InputGroup maxW="400px" startElement={<Icon as={FaSearch} color="gray.400" />}> <InputGroup maxW="300px" startElement={<Icon as={FaSearch} color="whiteAlpha.400" />}>
<Input <Input
placeholder="Search games..." placeholder="Search games..."
value={filters.search} value={filters.search}
onChange={handleSearchChange} onChange={handleSearchChange}
variant="subtle" variant="flushed"
bg="blackAlpha.300" bg="blackAlpha.200"
_hover={{ bg: "blackAlpha.400" }} borderBottom="1px solid"
_focus={{ bg: "blackAlpha.400", borderColor: "purple.400" }} borderColor="whiteAlpha.300"
_hover={{ borderColor: "brand.400" }}
_focus={{ borderColor: "brand.300", bg: "blackAlpha.400" }}
color="white"
py={2}
/> />
</InputGroup> </InputGroup>
<MenuRoot closeOnSelect={false}> <MenuRoot closeOnSelect={false}>
<MenuTrigger asChild> <MenuTrigger asChild>
<Button variant="outline" colorScheme="purple"> <Button
Filters <Icon as={FaFilter} ml={2} /> variant="subtle"
colorPalette="brand"
size="sm"
border="1px solid"
borderColor="whiteAlpha.200"
_hover={{ bg: "whiteAlpha.100", borderColor: "brand.400" }}
>
Filters <Icon as={FaFilter} />
</Button> </Button>
</MenuTrigger> </MenuTrigger>
<MenuContent bg="gray.800" borderColor="whiteAlpha.200"> <MenuContent
<Box px={4} py={2}> bg="rgba(10, 12, 20, 0.9)"
<Text fontSize="xs" fontWeight="bold" color="gray.400" mb={2} textTransform="uppercase">Platforms</Text> 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}> <VStack align="start" gap={2}>
{['PC', 'PS5', 'Xbox', 'Switch'].map(p => ( {['PC', 'PS5', 'Xbox', 'Switch'].map(p => (
<MenuCheckboxItem <MenuCheckboxItem

View File

@@ -1,19 +1,16 @@
'use client'; 'use client';
import { Link } from '@/i18n/navigation'; 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 { 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 { enUS, tr } from 'date-fns/locale';
import { useLocale } from 'next-intl'; import { useLocale } from 'next-intl';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; import { FaChevronLeft, FaChevronRight, FaGamepad, FaCalendarAlt } from 'react-icons/fa';
import { IconButton } from '@chakra-ui/react';
import { FilterBar, FilterState } from './filter-bar'; 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 { interface CalendarProps {
games: any[]; // Replace with correct type games: any[];
events: any[]; events: any[];
} }
@@ -30,33 +27,20 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
const monthStart = startOfMonth(currentDate); const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(monthStart); const monthEnd = endOfMonth(monthStart);
const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd }); const daysInMonth = eachDayOfInterval({ start: monthStart, end: monthEnd });
const startDayOfWeek = getDay(monthStart);
// 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 paddingDays = Array.from({ length: startDayOfWeek }); const paddingDays = Array.from({ length: startDayOfWeek });
const nextMonth = () => setCurrentDate(addMonths(currentDate, 1)); const nextMonth = () => setCurrentDate(addMonths(currentDate, 1));
const prevMonth = () => setCurrentDate(subMonths(currentDate, 1)); const prevMonth = () => setCurrentDate(subMonths(currentDate, 1));
const getItemsForDay = (date: Date) => { const getItemsForDay = (date: Date) => {
// Filter Games
const dayGames = games.filter(g => { const dayGames = games.filter(g => {
if (!g.releaseDate || !isSameDay(new Date(g.releaseDate), date)) return false; 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; if (filters.search && !g.title.toLowerCase().includes(filters.search.toLowerCase())) return false;
// Platform filter logic can be refined
// 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;
}
return true; return true;
}); });
// Filter Events
const dayEvents = filters.showEvents ? events.filter(e => { const dayEvents = filters.showEvents ? events.filter(e => {
if (!isSameDay(new Date(e.startTime), date)) return false; if (!isSameDay(new Date(e.startTime), date)) return false;
if (filters.search && !e.title.toLowerCase().includes(filters.search.toLowerCase())) 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 ( return (
<Box w="full" bg="whiteAlpha.300" rounded="xl" p={6} backdropFilter="blur(10px)" border="1px solid" borderColor="whiteAlpha.200"> <Box
{/* Header */} w="full"
<Flex justify="space-between" align="center" mb={6}> position="relative"
<Heading size="lg" color="white"> isolation="isolate"
{format(currentDate, 'MMMM yyyy', { locale: dateLocale })} >
</Heading> {/* Header / Controls */}
<Flex gap={2}> <Flex
<IconButton aria-label="Previous month" onClick={prevMonth} variant="ghost" color="white"> direction={{ base: 'column', md: 'row' }}
<FaChevronLeft /> justify="space-between"
</IconButton> align="center"
<IconButton aria-label="Next month" onClick={nextMonth} variant="ghost" color="white"> mb={8}
<FaChevronRight /> gap={4}
</IconButton> 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 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"
size="sm"
rounded="full"
color="gray.400"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
>
<FaChevronRight />
</IconButton>
</Flex>
</Flex> </Flex>
<Box w={{ base: "full", md: "auto" }}>
<FilterBar filters={filters} onFilterChange={setFilters} />
</Box>
</Flex> </Flex>
{/* Filters */} {/* Calendar Grid Container with Glassmorphism */}
<FilterBar filters={filters} onFilterChange={setFilters} /> <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 */} {/* 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 => ( {['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} {day}
</Text> </Text>
))} ))}
</SimpleGrid> </SimpleGrid>
{/* Calendar Grid */} {/* Grid */}
<SimpleGrid columns={7} gap={1} minH="500px"> <SimpleGrid columns={7} gap={3} position="relative" zIndex="1">
{paddingDays.map((_, i) => ( {paddingDays.map((_, i) => (
<Box key={`padding-${i}`} bg="transparent" /> <Box key={`padding-${i}`} />
))} ))}
{daysInMonth.map((date) => { {daysInMonth.map((date) => {
const items = getItemsForDay(date); const items = getItemsForDay(date);
const isToday = isSameDay(date, new Date()); const isCurrentDay = isToday(date);
const hasItems = items.length > 0;
return ( return (
<Box <Box
key={date.toString()} key={date.toString()}
bg={isToday ? 'primary.900' : 'whiteAlpha.200'} minH="140px"
border="1px solid" bg={isCurrentDay ? 'brand.900' : 'whiteAlpha.50'}
borderColor={isToday ? 'primary.500' : 'whiteAlpha.100'} border="1px solid"
rounded="md" borderColor={isCurrentDay ? 'brand.500' : 'whiteAlpha.50'}
p={2} rounded="xl"
minH="100px" p={3}
transition="all 0.2s" transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
_hover={{ bg: 'whiteAlpha.200', transform: 'scale(1.02)', zIndex: 1 }} _hover={{
cursor="pointer" bg: 'whiteAlpha.100',
position="relative" borderColor: 'brand.400',
overflow="hidden" transform: 'translateY(-2px)',
> boxShadow: '0 4px 20px rgba(0,0,0,0.2)'
<Text fontSize="sm" color={isToday ? 'primary.300' : 'gray.400'} mb={1} fontWeight={isToday ? 'bold' : 'normal'}> }}
{format(date, 'd')} position="relative"
</Text> overflow="hidden"
data-group
>
{/* Date Number */}
<Text
fontSize="sm"
fontWeight={isCurrentDay ? "900" : "medium"}
color={isCurrentDay ? "brand.300" : "gray.500"}
mb={2}
>
{format(date, 'd')}
</Text>
<VStack align="stretch" gap={1}> {/* Items Stack */}
{items.map((item: any, idx) => { <VStack align="stretch" gap={1.5}>
const badge = ( {items.map((item: any, idx) => {
<Badge const isGame = item.type === 'game';
key={`${item.id}-${idx}`}
size="sm" const content = (
variant="solid" <Flex
colorPalette={item.type === 'game' ? 'blue' : 'purple'} align="center"
truncate gap={2}
fontSize="xs" bg={isGame ? "linear-gradient(90deg, rgba(64, 114, 255, 0.1), transparent)" : "linear-gradient(90deg, rgba(235, 64, 255, 0.1), transparent)"}
px={1} borderLeft="2px solid"
cursor="pointer" borderColor={isGame ? "brand.400" : "purple.400"}
_hover={{ opacity: 0.8 }} p={1.5}
> rounded="md"
{item.title} _hover={{ bg: "whiteAlpha.100" }}
</Badge> 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}
</Text>
</Flex>
);
if (item.type === 'game') { if (isGame) {
return ( return (
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`}> <Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`} style={{ textDecoration: 'none' }}>
{badge} {content}
</Link> </Link>
); );
} }
return <Box key={`${item.id}-${idx}`}>{content}</Box>;
return badge; })}
})} </VStack>
</VStack>
{/* Glow effect on hover */}
{/* Background image effect for heavy days? Optional polish */} <Box
</Box> position="absolute"
); inset="0"
})} bgGradient={isCurrentDay ? "radial(brand.500 0%, transparent 70%)" : "radial(white 0%, transparent 70%)"}
</SimpleGrid> opacity="0"
_groupHover={{ opacity: 0.05 }}
transition="opacity 0.3s"
pointerEvents="none"
/>
</Box>
);
})}
</SimpleGrid>
</Box>
</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 <Box
as="nav" as="nav"
bg={isSticky ? "rgba(255, 255, 255, 0.6)" : "white"} bg={isSticky ? "rgba(5, 5, 10, 0.6)" : "transparent"}
_dark={{ _dark={{
bg: isSticky ? "rgba(1, 1, 1, 0.6)" : "black", bg: isSticky ? "rgba(5, 5, 10, 0.6)" : "transparent",
}} }}
shadow={isSticky ? "sm" : "none"} shadow={isSticky ? "lg" : "none"}
backdropFilter="blur(12px) saturate(180%)" backdropFilter={isSticky ? "blur(16px) saturate(180%)" : "none"}
border="1px solid" borderBottom="1px solid"
borderColor={isSticky ? "whiteAlpha.300" : "transparent"} borderColor={isSticky ? "whiteAlpha.100" : "transparent"}
borderBottomRadius={isSticky ? "xl" : "none"} borderBottomRadius={isSticky ? "2xl" : "none"}
transition="all 0.4s ease-in-out" 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 }} px={{ base: 4, md: 8 }}
py="3" py="4"
position="sticky" position="sticky"
top={0} top={0}
zIndex={10} zIndex={100}
w="full" w={isSticky ? "auto" : "full"}
> >
<Flex justify="space-between" align="center" maxW="8xl" mx="auto"> <Flex justify="space-between" align="center" maxW="8xl" mx="auto">
{/* Logo */} {/* Logo */}
@@ -166,11 +168,11 @@ export default function Header() {
as={Link} as={Link}
href="/home" href="/home"
fontSize="3xl" fontSize="3xl"
fontWeight="extrabold" fontWeight="900"
letterSpacing="wide" letterSpacing="tight"
bgGradient="to-r" bgGradient="to-r"
gradientFrom="primary.400" gradientFrom="brand.300"
gradientTo="primary.600" gradientTo="brand.500"
bgClip="text" bgClip="text"
bgSize="200% auto" bgSize="200% auto"
animation="text-gradient 12s linear infinite" 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 { createApiClient } from './create-api-client';
import baseUrl from '@/config/base-url';
export interface Game { export interface Game {
id: string; id: string;
@@ -16,7 +16,7 @@ export interface Game {
screenshots?: { url: string }[]; screenshots?: { url: string }[];
} }
const client = createApiClient('/games'); const client = createApiClient(`${baseUrl.core}/games`);
export const gamesApi = { export const gamesApi = {
getAll: (params?: any) => client.get<{ items: Game[]; meta: any }>('/', { params }), 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)' }, mono: { value: 'var(--font-bricolage)' },
}, },
colors: { colors: {
primary: { brand: {
50: { value: '#E6FFFA' }, 50: { value: '#F0F4FF' },
100: { value: '#B2F5EA' }, 100: { value: '#D9E2FF' },
200: { value: '#81E6D9' }, 200: { value: '#B3C6FF' },
300: { value: '#4FD1C5' }, 300: { value: '#8DAAFF' },
400: { value: '#38B2AC' }, 400: { value: '#668EFF' },
500: { value: '#319795' }, 500: { value: '#4072FF' },
600: { value: '#2C7A7B' }, 600: { value: '#1A56FF' },
700: { value: '#285E61' }, 700: { value: '#003EBD' },
800: { value: '#234E52' }, 800: { value: '#002B85' },
900: { value: '#1D4044' }, 900: { value: '#00194D' },
950: { value: '#132E30' }, 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: { semanticTokens: {
colors: { colors: {
primary: { primary: {
solid: { solid: { value: '{colors.brand.600}' },
value: { contrast: { value: '{colors.white}' },
_light: '{colors.primary.600}', fg: { value: '{colors.brand.700}' },
_dark: '{colors.primary.600}', muted: { value: '{colors.brand.200}' },
}, subtle: { value: '{colors.brand.100}' },
}, emphasize: { value: '{colors.brand.300}' },
contrast: { focusRing: { value: '{colors.brand.500}' },
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}',
},
},
}, },
bg: {
canvas: { value: '{colors.dark.bg}' },
surface: { value: '{colors.dark.glass}' }
}
}, },
}, },
}, },