pre-main
This commit is contained in:
12
.env
Normal file
12
.env
Normal 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'
|
||||
@@ -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
960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
<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>
|
||||
</Box>
|
||||
<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>
|
||||
<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 */}
|
||||
<ThemeSwitcher />
|
||||
<Box position="fixed" bottom={4} right={4} opacity={0.5} _hover={{ opacity: 1 }} transition="opacity 0.2s">
|
||||
<ThemeSwitcher />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,24 @@ 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 (
|
||||
<Flex minH='100vh' direction='column'>
|
||||
<Header />
|
||||
<Container as='main' maxW='8xl' flex='1' py={4}>
|
||||
{children}
|
||||
</Container>
|
||||
<BackToTop />
|
||||
<Footer />
|
||||
</Flex>
|
||||
<>
|
||||
<GamerBackground />
|
||||
<PageBackground>
|
||||
<Flex minH='100vh' direction='column'>
|
||||
<Header />
|
||||
<Container as='main' maxW='8xl' flex='1' py={4}>
|
||||
{children}
|
||||
</Container>
|
||||
<BackToTop />
|
||||
<Footer />
|
||||
</Flex>
|
||||
</PageBackground>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
{format(currentDate, 'MMMM yyyy', { locale: dateLocale })}
|
||||
</Heading>
|
||||
<Flex gap={2}>
|
||||
<IconButton aria-label="Previous month" onClick={prevMonth} variant="ghost" color="white">
|
||||
<FaChevronLeft />
|
||||
</IconButton>
|
||||
<IconButton aria-label="Next month" onClick={nextMonth} variant="ghost" color="white">
|
||||
<FaChevronRight />
|
||||
</IconButton>
|
||||
<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 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>
|
||||
|
||||
<Box w={{ base: "full", md: "auto" }}>
|
||||
<FilterBar filters={filters} onFilterChange={setFilters} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* Filters */}
|
||||
<FilterBar filters={filters} onFilterChange={setFilters} />
|
||||
{/* 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}>
|
||||
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
|
||||
<Text key={day} textAlign="center" color="gray.400" fontWeight="bold" fontSize="sm">
|
||||
{day}
|
||||
</Text>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{/* Days Header */}
|
||||
<SimpleGrid columns={7} mb={4} textAlign="center" position="relative" zIndex="1">
|
||||
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
|
||||
<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">
|
||||
{paddingDays.map((_, i) => (
|
||||
<Box key={`padding-${i}`} bg="transparent" />
|
||||
))}
|
||||
{/* Grid */}
|
||||
<SimpleGrid columns={7} gap={3} position="relative" zIndex="1">
|
||||
{paddingDays.map((_, i) => (
|
||||
<Box key={`padding-${i}`} />
|
||||
))}
|
||||
|
||||
{daysInMonth.map((date) => {
|
||||
const items = getItemsForDay(date);
|
||||
const isToday = isSameDay(date, new Date());
|
||||
{daysInMonth.map((date) => {
|
||||
const items = getItemsForDay(date);
|
||||
const isCurrentDay = isToday(date);
|
||||
const hasItems = items.length > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={date.toString()}
|
||||
bg={isToday ? 'primary.900' : 'whiteAlpha.200'}
|
||||
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"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Text fontSize="sm" color={isToday ? 'primary.300' : 'gray.400'} mb={1} fontWeight={isToday ? 'bold' : 'normal'}>
|
||||
{format(date, 'd')}
|
||||
</Text>
|
||||
return (
|
||||
<Box
|
||||
key={date.toString()}
|
||||
minH="140px"
|
||||
bg={isCurrentDay ? 'brand.900' : 'whiteAlpha.50'}
|
||||
border="1px solid"
|
||||
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}
|
||||
>
|
||||
{format(date, 'd')}
|
||||
</Text>
|
||||
|
||||
<VStack align="stretch" gap={1}>
|
||||
{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 }}
|
||||
>
|
||||
{item.title}
|
||||
</Badge>
|
||||
);
|
||||
{/* Items Stack */}
|
||||
<VStack align="stretch" gap={1.5}>
|
||||
{items.map((item: any, idx) => {
|
||||
const isGame = item.type === 'game';
|
||||
|
||||
if (item.type === 'game') {
|
||||
return (
|
||||
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`}>
|
||||
{badge}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
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}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return badge;
|
||||
})}
|
||||
</VStack>
|
||||
if (isGame) {
|
||||
return (
|
||||
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`} style={{ textDecoration: 'none' }}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <Box key={`${item.id}-${idx}`}>{content}</Box>;
|
||||
})}
|
||||
</VStack>
|
||||
|
||||
{/* Background image effect for heavy days? Optional polish */}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
{/* 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 >
|
||||
);
|
||||
}
|
||||
|
||||
50
src/components/features/sync/sync-button.tsx
Normal file
50
src/components/features/sync/sync-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
11
src/components/layout/page-background.tsx
Normal file
11
src/components/layout/page-background.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
84
src/components/ui/gamer-background.tsx
Normal file
84
src/components/ui/gamer-background.tsx
Normal 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
20
src/lib/api/events.ts
Normal 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}`),
|
||||
};
|
||||
@@ -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
8
src/lib/api/sync.ts
Normal 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'),
|
||||
};
|
||||
@@ -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}' }
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user