main
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"axios": "^1.13.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.6.0",
|
||||
"next": "16.0.0",
|
||||
"next-auth": "^4.24.13",
|
||||
@@ -5368,6 +5369,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"axios": "^1.13.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"i18next": "^25.6.0",
|
||||
"next": "16.0.0",
|
||||
"next-auth": "^4.24.13",
|
||||
|
||||
114
src/app/[locale]/(site)/events/[slug]/page.tsx
Normal file
114
src/app/[locale]/(site)/events/[slug]/page.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { Container, Heading, Text, Box, Badge, HStack, VStack, Icon, Flex, Button } from '@chakra-ui/react';
|
||||
import { MOCK_EVENTS } from '@/lib/api/mock-data';
|
||||
import { FaCalendarAlt, FaClock, FaTwitch, FaYoutube } from 'react-icons/fa';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function EventDetailsPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const event = MOCK_EVENTS.find((e) => e.slug === slug);
|
||||
|
||||
if (!event) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Format date and time
|
||||
const formattedDate = new Intl.DateTimeFormat('en-US', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
}).format(event.startTime);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
}).format(event.startTime);
|
||||
|
||||
return (
|
||||
<Container maxW="5xl" py={20} minH="100vh">
|
||||
<VStack gap={8} align="center" textAlign="center">
|
||||
|
||||
{/* Event Type Badge */}
|
||||
<Badge
|
||||
colorScheme="pink"
|
||||
fontSize="lg"
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
variant="subtle"
|
||||
>
|
||||
{event.type}
|
||||
</Badge>
|
||||
|
||||
{/* Title */}
|
||||
<Heading
|
||||
size="4xl"
|
||||
bgGradient="linear(to-r, pink.400, purple.500)"
|
||||
bgClip="text"
|
||||
letterSpacing="tight"
|
||||
>
|
||||
{event.title}
|
||||
</Heading>
|
||||
|
||||
{/* Date & Time Box */}
|
||||
<Box
|
||||
p={8}
|
||||
bg="whiteAlpha.100"
|
||||
borderRadius="2xl"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
backdropFilter="blur(10px)"
|
||||
minW={{ base: 'full', md: '500px' }}
|
||||
>
|
||||
<VStack gap={4}>
|
||||
<HStack color="pink.300" gap={4}>
|
||||
<Icon as={FaCalendarAlt} boxSize={6} />
|
||||
<Text fontSize="2xl" fontWeight="bold">{formattedDate}</Text>
|
||||
</HStack>
|
||||
<HStack color="purple.300" gap={4}>
|
||||
<Icon as={FaClock} boxSize={6} />
|
||||
<Text fontSize="2xl">{formattedTime}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* Description (Placeholder) */}
|
||||
<Text fontSize="xl" maxW="2xl" color="whiteAlpha.800">
|
||||
Join us for the {event.title}. Expect world premieres, developer interviews, and exclusive gameplay reveals. Don't miss out on the biggest gaming news!
|
||||
</Text>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<HStack gap={4} pt={8}>
|
||||
<Button
|
||||
size="lg"
|
||||
colorScheme="purple"
|
||||
// leftIcon={<Icon as={FaTwitch} />}
|
||||
// as="a" href="..."
|
||||
>
|
||||
<Icon as={FaTwitch} mr={2} />
|
||||
Watch on Twitch
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
// leftIcon={<Icon as={FaYoutube} />}
|
||||
// as="a" href="..."
|
||||
>
|
||||
<Icon as={FaYoutube} mr={2} />
|
||||
Watch on YouTube
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
</VStack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
120
src/app/[locale]/(site)/games/[slug]/page.tsx
Normal file
120
src/app/[locale]/(site)/games/[slug]/page.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
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 { FaCalendar, FaGamepad } from 'react-icons/fa';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
locale: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function GameDetailsPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const game = MOCK_GAMES.find((g) => g.slug === slug);
|
||||
|
||||
if (!game) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Format date
|
||||
const formattedDate = new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'long',
|
||||
}).format(game.releaseDate);
|
||||
|
||||
return (
|
||||
<Container maxW="6xl" py={12} minH="100vh">
|
||||
{/* Hero / Cover Section */}
|
||||
<Box
|
||||
position="relative"
|
||||
h={{ base: "300px", md: "500px" }}
|
||||
w="full"
|
||||
overflow="hidden"
|
||||
borderRadius="2xl"
|
||||
mb={8}
|
||||
boxShadow="2xl"
|
||||
>
|
||||
<Image
|
||||
src={game.coverImage}
|
||||
alt={game.title}
|
||||
objectFit="cover"
|
||||
w="full"
|
||||
h="full"
|
||||
filter="brightness(0.7)"
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
w="full"
|
||||
p={8}
|
||||
bgGradient="linear(to-t, blackAlpha.900, transparent)"
|
||||
>
|
||||
<VStack align="flex-start" gap={4}>
|
||||
<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'}
|
||||
</Badge>
|
||||
{game.platforms.map(p => (
|
||||
<Badge key={p} variant="outline" colorScheme="cyan" px={2}>
|
||||
{p}
|
||||
</Badge>
|
||||
))}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Details Grid */}
|
||||
<Flex direction={{ base: 'column', lg: 'row' }} gap={12}>
|
||||
{/* Main Content */}
|
||||
<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.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Sidebar Info */}
|
||||
<Box w={{ base: 'full', lg: '350px' }}>
|
||||
<VStack
|
||||
align="stretch"
|
||||
p={6}
|
||||
bg="whiteAlpha.50"
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
gap={6}
|
||||
>
|
||||
<Heading size="md" mb={2}>Game Info</Heading>
|
||||
|
||||
<Box>
|
||||
<HStack mb={1} color="gray.400">
|
||||
<Icon as={FaCalendar} />
|
||||
<Text fontSize="sm">Release Date</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xl" fontWeight="bold">{formattedDate}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<HStack mb={1} color="gray.400">
|
||||
<Icon as={FaGamepad} />
|
||||
<Text fontSize="sm">Platforms</Text>
|
||||
</HStack>
|
||||
<Flex gap={2} wrap="wrap">
|
||||
{game.platforms.map(p => (
|
||||
<Badge key={p} colorScheme="green">{p}</Badge>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import HomeCard from "@/components/site/home/home-card";
|
||||
import { Container, VStack, Heading, Text, Box } from '@chakra-ui/react';
|
||||
import { GameCalendar } from '@/components/features/calendar/game-calendar';
|
||||
import { MOCK_EVENTS, MOCK_GAMES } from '@/lib/api/mock-data';
|
||||
import { ThemeSwitcher } from '@/components/features/theme-switcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getTranslations();
|
||||
export default function HomePage() {
|
||||
// Static for now, in future useTranslations
|
||||
// const t = useTranslations('dashboard');
|
||||
|
||||
return {
|
||||
title: `${t("home")} | FCS`,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return <HomeCard />;
|
||||
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">Game Calendar</Heading>
|
||||
<Text fontSize="lg" color="whiteAlpha.800">Track releases, events, and showcases in one place.</Text>
|
||||
</Box>
|
||||
|
||||
{/* Calendar */}
|
||||
<GameCalendar games={MOCK_GAMES} events={MOCK_EVENTS} />
|
||||
</VStack>
|
||||
|
||||
{/* Debug Switcher */}
|
||||
<ThemeSwitcher />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
@import "https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap";
|
||||
|
||||
:root {
|
||||
--font-bricolage: 'Bricolage Grotesque', sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
/* Use dynamic theme variable or fallback */
|
||||
background-color: var(--app-background, var(--chakra-colors-gray-950));
|
||||
background-image: var(--app-background-image, none);
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
color: var(--chakra-colors-gray-100);
|
||||
}
|
||||
|
||||
/* Scrollbar Styling for Gamer Aesthetic */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--chakra-colors-primary-600);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--chakra-colors-primary-500);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Provider } from '@/components/ui/provider';
|
||||
import { DynamicThemeProvider } from '@/components/ui/dynamic-theme-provider';
|
||||
import { Bricolage_Grotesque } from 'next/font/google';
|
||||
import { hasLocale, NextIntlClientProvider } from 'next-intl';
|
||||
import { notFound } from 'next/navigation';
|
||||
@@ -33,7 +34,11 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className={bricolage.variable}>
|
||||
<NextIntlClientProvider>
|
||||
<Provider>{children}</Provider>
|
||||
<Provider>
|
||||
<DynamicThemeProvider>
|
||||
{children}
|
||||
</DynamicThemeProvider>
|
||||
</Provider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import baseUrl from "@/config/base-url";
|
||||
import { authService } from "@/lib/api/Example/auth/service";
|
||||
import { authService } from "@/lib/api/example/auth/service";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
|
||||
85
src/components/features/calendar/filter-bar.tsx
Normal file
85
src/components/features/calendar/filter-bar.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Box, HStack, Input, Button, Text, Icon, VStack } from '@chakra-ui/react';
|
||||
import { FaSearch, FaFilter } from 'react-icons/fa';
|
||||
import { Checkbox } from '@/components/ui/forms/checkbox';
|
||||
import { InputGroup } from '@/components/ui/forms/input-group';
|
||||
import { MenuRoot, MenuTrigger, MenuContent, MenuCheckboxItem, MenuSeparator } from '@/components/ui/overlays/menu';
|
||||
|
||||
export interface FilterState {
|
||||
search: string;
|
||||
platforms: string[];
|
||||
showEvents: boolean;
|
||||
}
|
||||
|
||||
interface FilterBarProps {
|
||||
filters: FilterState;
|
||||
onFilterChange: (filters: FilterState) => void;
|
||||
}
|
||||
|
||||
export function FilterBar({ filters, onFilterChange }: FilterBarProps) {
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onFilterChange({ ...filters, search: e.target.value });
|
||||
};
|
||||
|
||||
const togglePlatform = (platform: string) => {
|
||||
const current = filters.platforms;
|
||||
const next = current.includes(platform)
|
||||
? current.filter(p => p !== platform)
|
||||
: [...current, platform];
|
||||
onFilterChange({ ...filters, platforms: next });
|
||||
};
|
||||
|
||||
const toggleEvents = (checked: boolean) => {
|
||||
onFilterChange({ ...filters, showEvents: checked });
|
||||
};
|
||||
|
||||
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" />}>
|
||||
<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" }}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<MenuRoot closeOnSelect={false}>
|
||||
<MenuTrigger asChild>
|
||||
<Button variant="outline" colorScheme="purple">
|
||||
Filters <Icon as={FaFilter} ml={2} />
|
||||
</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>
|
||||
<VStack align="start" gap={2}>
|
||||
{['PC', 'PS5', 'Xbox', 'Switch'].map(p => (
|
||||
<MenuCheckboxItem
|
||||
key={p}
|
||||
value={p}
|
||||
checked={filters.platforms.includes(p)}
|
||||
onCheckedChange={(e) => togglePlatform(p)}
|
||||
>
|
||||
{p}
|
||||
</MenuCheckboxItem>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
<MenuSeparator />
|
||||
<Box>
|
||||
<MenuCheckboxItem
|
||||
value="events"
|
||||
checked={filters.showEvents}
|
||||
onCheckedChange={(checked: boolean) => toggleEvents(checked)}
|
||||
>
|
||||
Show Events
|
||||
</MenuCheckboxItem>
|
||||
</Box>
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
149
src/components/features/calendar/game-calendar.tsx
Normal file
149
src/components/features/calendar/game-calendar.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Grid, Heading, Text, VStack, Badge, Flex, Image as ChakraImage, SimpleGrid } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths, getDay } 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 { 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
|
||||
events: any[];
|
||||
}
|
||||
|
||||
export function GameCalendar({ games = [], events = [] }: CalendarProps) {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
search: '',
|
||||
platforms: ['PC', 'PS5', 'Xbox', 'Switch'],
|
||||
showEvents: true
|
||||
});
|
||||
const locale = useLocale();
|
||||
const dateLocale = locale === 'tr' ? tr : enUS;
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
return true;
|
||||
}) : [];
|
||||
|
||||
return [...dayGames.map(g => ({ ...g, type: 'game' })), ...dayEvents.map(e => ({ ...e, type: 'event' }))];
|
||||
};
|
||||
|
||||
return (
|
||||
<Box w="full" bg="whiteAlpha.50" rounded="xl" p={6} backdropFilter="blur(10px)" border="1px solid" borderColor="whiteAlpha.100">
|
||||
{/* 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>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* Filters */}
|
||||
<FilterBar filters={filters} onFilterChange={setFilters} />
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<SimpleGrid columns={7} gap={1} minH="500px">
|
||||
{paddingDays.map((_, i) => (
|
||||
<Box key={`padding-${i}`} bg="transparent" />
|
||||
))}
|
||||
|
||||
{daysInMonth.map((date) => {
|
||||
const items = getItemsForDay(date);
|
||||
const isToday = isSameDay(date, new Date());
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={date.toString()}
|
||||
bg={isToday ? 'primary.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"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<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.map((item: any, idx) => (
|
||||
<Badge
|
||||
key={`${item.id}-${idx}`}
|
||||
size="sm"
|
||||
variant="solid"
|
||||
colorPalette={item.type === 'game' ? 'blue' : 'purple'}
|
||||
truncate
|
||||
fontSize="xs"
|
||||
px={1}
|
||||
>
|
||||
{item.title}
|
||||
</Badge>
|
||||
))}
|
||||
</VStack>
|
||||
|
||||
{/* Background image effect for heavy days? Optional polish */}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
38
src/components/features/theme-switcher.tsx
Normal file
38
src/components/features/theme-switcher.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { HStack, Button, Text } from '@chakra-ui/react';
|
||||
import { useDynamicTheme, DynamicThemeProvider } from '@/components/ui/dynamic-theme-provider';
|
||||
import { ThemeConfig, defaultTheme } from '@/types/theme';
|
||||
|
||||
const eldenRingTheme: ThemeConfig = {
|
||||
key: 'elden_ring',
|
||||
isActive: true,
|
||||
gameTitle: 'Elden Ring',
|
||||
primaryColor: '#C4A484', // Goldish/Beige
|
||||
secondaryColor: '#8B4513', // Brown
|
||||
backgroundColor: '#1a1815', // Dark brown/black
|
||||
backgroundImage: 'https://images.igdb.com/igdb/image/upload/t_1080p/co4jni.jpg', // Elden Ring Art
|
||||
};
|
||||
|
||||
const cyberPunkTheme: ThemeConfig = {
|
||||
key: 'cyberpunk',
|
||||
isActive: true,
|
||||
gameTitle: 'Cyberpunk 2077',
|
||||
primaryColor: '#FCEE0A', // Yellow
|
||||
secondaryColor: '#00F0FF', // Blue
|
||||
backgroundColor: '#0a0a0a',
|
||||
backgroundImage: 'https://images.igdb.com/igdb/image/upload/t_1080p/co2mjs.jpg'
|
||||
}
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { setTheme } = useDynamicTheme();
|
||||
|
||||
return (
|
||||
<HStack p={4} bg="blackAlpha.500" rounded="lg" position="fixed" bottom={4} right={4} zIndex={9999}>
|
||||
<Text fontSize="xs" color="white" fontWeight="bold">Simulate GOTY:</Text>
|
||||
<Button size="xs" onClick={() => setTheme(defaultTheme)}>Default</Button>
|
||||
<Button size="xs" colorPalette="yellow" onClick={() => setTheme(eldenRingTheme)}>Elden Ring</Button>
|
||||
<Button size="xs" colorPalette="cyan" onClick={() => setTheme(cyberPunkTheme)}>Cyberpunk</Button>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
61
src/components/ui/dynamic-theme-provider.tsx
Normal file
61
src/components/ui/dynamic-theme-provider.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeConfig, defaultTheme } from '@/types/theme';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface DynamicThemeContextType {
|
||||
theme: ThemeConfig;
|
||||
setTheme: (theme: ThemeConfig) => void;
|
||||
}
|
||||
|
||||
const DynamicThemeContext = createContext<DynamicThemeContextType>({
|
||||
theme: defaultTheme,
|
||||
setTheme: () => { },
|
||||
});
|
||||
|
||||
export const useDynamicTheme = () => useContext(DynamicThemeContext);
|
||||
|
||||
interface DynamicThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialTheme?: ThemeConfig;
|
||||
}
|
||||
|
||||
export function DynamicThemeProvider({ children, initialTheme }: DynamicThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<ThemeConfig>(initialTheme || defaultTheme);
|
||||
|
||||
// Apply theme to CSS variables
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
// We update the CSS variables that map to our Chakra tokens
|
||||
// Note: You might need to adjust the exact variable names based on your final theme.ts generation
|
||||
// Chakra v3 usually uses var(--chakra-colors-primary-500) etc.
|
||||
|
||||
// For this simple implementation, we will assume we can override specific brand colors
|
||||
// In a real generic system, we might need a more complex palette generator to generate 50-950 scales
|
||||
|
||||
root.style.setProperty('--chakra-colors-primary-500', theme.primaryColor);
|
||||
root.style.setProperty('--chakra-colors-primary-600', theme.secondaryColor);
|
||||
|
||||
// Backgrounds for the app
|
||||
if (theme.backgroundColor) {
|
||||
// We can create a custom variable for app-bg
|
||||
root.style.setProperty('--app-background', theme.backgroundColor);
|
||||
}
|
||||
|
||||
if (theme.backgroundImage) {
|
||||
root.style.setProperty('--app-background-image', `url(${theme.backgroundImage})`);
|
||||
} else {
|
||||
root.style.removeProperty('--app-background-image');
|
||||
}
|
||||
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<DynamicThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</DynamicThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
51
src/lib/api/mock-data.ts
Normal file
51
src/lib/api/mock-data.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export const MOCK_GAMES = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Elden Ring: Shadow of the Erdtree',
|
||||
slug: 'elden-ring-shadow-of-the-erdtree',
|
||||
releaseDate: new Date('2026-06-21'),
|
||||
coverImage: 'https://images.igdb.com/igdb/image/upload/t_cover_big/co848y.jpg',
|
||||
platforms: ['PS5', 'PC', 'Xbox Series X'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Grand Theft Auto VI',
|
||||
slug: 'gta-vi',
|
||||
releaseDate: new Date('2026-09-15'), // Speculative
|
||||
coverImage: 'https://images.igdb.com/igdb/image/upload/t_cover_big/co848z.jpg',
|
||||
platforms: ['PS5', 'Xbox Series X'],
|
||||
},
|
||||
// Add some for current month to show in calendar
|
||||
{
|
||||
id: '3',
|
||||
title: 'Hades II',
|
||||
slug: 'hades-ii',
|
||||
releaseDate: new Date(), // Today
|
||||
coverImage: 'https://images.igdb.com/igdb/image/upload/t_cover_big/co8490.jpg',
|
||||
platforms: ['PC'],
|
||||
}
|
||||
];
|
||||
|
||||
export const MOCK_EVENTS = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'PlayStation Showcase',
|
||||
slug: 'playstation-showcase-2026',
|
||||
startTime: new Date('2026-05-24T20:00:00Z'),
|
||||
type: 'SHOWCASE',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Summer Game Fest',
|
||||
slug: 'summer-game-fest-2026',
|
||||
startTime: new Date('2026-06-07T18:00:00Z'),
|
||||
type: 'SHOWCASE',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Indie World',
|
||||
slug: 'indie-world-april',
|
||||
startTime: new Date(), // Today
|
||||
type: 'SHOWCASE',
|
||||
}
|
||||
];
|
||||
9
src/middleware.ts
Normal file
9
src/middleware.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: ['/', '/(tr|en)/:path*']
|
||||
};
|
||||
19
src/types/theme.ts
Normal file
19
src/types/theme.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ThemeConfig {
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
gameTitle: string; // "Elden Ring"
|
||||
primaryColor: string; // Hex code for primary brand color
|
||||
secondaryColor: string; // Hex code for secondary
|
||||
backgroundColor: string; // Hex code for background
|
||||
backgroundImage?: string; // URL
|
||||
logoImage?: string; // URL
|
||||
}
|
||||
|
||||
export const defaultTheme: ThemeConfig = {
|
||||
key: 'default',
|
||||
isActive: true,
|
||||
gameTitle: 'Game Calendar',
|
||||
primaryColor: '#319795', // Chakra Teal 500
|
||||
secondaryColor: '#2C7A7B',
|
||||
backgroundColor: '#132E30', // Dark teal-ish black
|
||||
};
|
||||
@@ -33,7 +33,9 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
".next/dev/types/**/*.ts",
|
||||
".next\\dev/types/**/*.ts",
|
||||
".next\\dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
Reference in New Issue
Block a user