main
This commit is contained in:
@@ -28,5 +28,9 @@
|
||||
"name": "Name",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
"high": "High",
|
||||
"predictions": "Predictions",
|
||||
"games": "Games",
|
||||
"events": "Events",
|
||||
"calendar": "Calendar"
|
||||
}
|
||||
@@ -28,5 +28,9 @@
|
||||
"name": "İsim",
|
||||
"low": "Düşük",
|
||||
"medium": "Orta",
|
||||
"high": "Yüksek"
|
||||
"high": "Yüksek",
|
||||
"predictions": "Tahminler",
|
||||
"games": "Oyunlar",
|
||||
"events": "Etkinlikler",
|
||||
"calendar": "Takvim"
|
||||
}
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -10,7 +10,7 @@ const nextConfig: NextConfig = {
|
||||
return [
|
||||
{
|
||||
source: "/api/backend/:path*",
|
||||
destination: "http://localhost:3000/api/:path*",
|
||||
destination: "http://localhost:4000/api/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function HomePage() {
|
||||
<VStack spaceY={8} align="stretch">
|
||||
{/* Hero Section / GOTY Banner could go here */}
|
||||
<Box>
|
||||
<Heading size="3xl" mb={2} color="white">Game Calendar</Heading>
|
||||
<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>
|
||||
|
||||
|
||||
12
src/app/[locale]/games/[slug]/page.tsx
Normal file
12
src/app/[locale]/games/[slug]/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
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} />;
|
||||
}
|
||||
@@ -39,3 +39,17 @@ body {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--chakra-colors-primary-500);
|
||||
}
|
||||
|
||||
@keyframes text-gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
|
||||
import { Box, Grid, Heading, Text, VStack, Badge, Flex, Image as ChakraImage, SimpleGrid } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
@@ -66,7 +67,7 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box w="full" bg="whiteAlpha.50" rounded="xl" p={6} backdropFilter="blur(10px)" border="1px solid" borderColor="whiteAlpha.100">
|
||||
<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">
|
||||
@@ -107,7 +108,7 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
|
||||
return (
|
||||
<Box
|
||||
key={date.toString()}
|
||||
bg={isToday ? 'primary.900' : 'whiteAlpha.50'}
|
||||
bg={isToday ? 'primary.900' : 'whiteAlpha.200'}
|
||||
border="1px solid"
|
||||
borderColor={isToday ? 'primary.500' : 'whiteAlpha.100'}
|
||||
rounded="md"
|
||||
@@ -124,7 +125,8 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
|
||||
</Text>
|
||||
|
||||
<VStack align="stretch" gap={1}>
|
||||
{items.map((item: any, idx) => (
|
||||
{items.map((item: any, idx) => {
|
||||
const badge = (
|
||||
<Badge
|
||||
key={`${item.id}-${idx}`}
|
||||
size="sm"
|
||||
@@ -133,10 +135,23 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) {
|
||||
truncate
|
||||
fontSize="xs"
|
||||
px={1}
|
||||
cursor="pointer"
|
||||
_hover={{ opacity: 0.8 }}
|
||||
>
|
||||
{item.title}
|
||||
</Badge>
|
||||
))}
|
||||
);
|
||||
|
||||
if (item.type === 'game') {
|
||||
return (
|
||||
<Link key={`${item.id}-${idx}`} href={`/games/${item.slug}`}>
|
||||
{badge}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return badge;
|
||||
})}
|
||||
</VStack>
|
||||
|
||||
{/* Background image effect for heavy days? Optional polish */}
|
||||
|
||||
203
src/components/features/games/GameDetail.tsx
Normal file
203
src/components/features/games/GameDetail.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Container, Heading, Text, Image, Badge, Flex, Grid, GridItem, Button, Icon, Stack, Tag, TagLabel, AspectRatio } from '@chakra-ui/react';
|
||||
import { Game, gamesApi } from '@/lib/api/games';
|
||||
import { notificationsApi } from '@/lib/api/notifications';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaPlaystation, FaXbox, FaDesktop, FaGamepad, FaStar, FaBuilding, FaCalendar, FaSync, FaBell } from 'react-icons/fa'; // Assuming react-icons is installed, or use lucid-react if available in project
|
||||
import { useQuery } from '@tanstack/react-query'; // Assuming react-query is used
|
||||
import { toaster } from '@/components/ui/feedback/toaster'; // Updated path
|
||||
|
||||
// Import UI components (assuming they exist or use basic Chakra)
|
||||
// I will use basic Chakra v3 components. For icons, I'll fallback to text if icons missing or Import from react-icons if available.
|
||||
// The prompted file said Chakra UI v3.
|
||||
|
||||
interface GameDetailProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export function GameDetail({ slug }: GameDetailProps) {
|
||||
const { data: response, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['game', slug],
|
||||
queryFn: () => gamesApi.getBySlug(slug),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('GameDetail mounted for slug:', slug);
|
||||
}, [slug]);
|
||||
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = useState(false);
|
||||
|
||||
const handleSync = async () => {
|
||||
setIsSyncing(true);
|
||||
try {
|
||||
await gamesApi.sync(slug);
|
||||
toaster.create({ title: 'Game synced successfully', type: 'success' });
|
||||
refetch();
|
||||
} catch (e) {
|
||||
toaster.create({ title: 'Failed to sync game', type: 'error' });
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
setIsSubscribing(true);
|
||||
try {
|
||||
if (!game?.id) return;
|
||||
await notificationsApi.subscribe(game.id);
|
||||
toaster.create({ title: 'Subscribed to alerts!', type: 'success' });
|
||||
} catch (e: any) {
|
||||
// Handle "Already subscribed" specifically if possible, else generic error
|
||||
const msg = e.response?.data?.message || 'Failed to subscribe';
|
||||
toaster.create({ title: msg, type: msg.includes('Already') ? 'info' : 'error' });
|
||||
} finally {
|
||||
setIsSubscribing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <Box p={10}>Loading...</Box>;
|
||||
if (error || !response?.data?.data) return (
|
||||
<Container centerContent py={20}>
|
||||
<Heading>Game not found</Heading>
|
||||
<Button mt={4} onClick={handleSync} loading={isSyncing}>Try Syncing from External Source</Button>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const game = response.data.data;
|
||||
|
||||
return (
|
||||
<Box pb={20}>
|
||||
{/* Hero Section */}
|
||||
<Box position="relative" h={{ base: "400px", md: "500px" }} w="full" overflow="hidden">
|
||||
{/* Background Blur */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bgImage={`url(${game.coverImage})`}
|
||||
bgSize="cover"
|
||||
bgPos="center"
|
||||
filter="blur(20px) brightness(0.4)"
|
||||
zIndex={0}
|
||||
/>
|
||||
|
||||
<Container maxW="container.xl" position="relative" zIndex={1} h="full" display="flex" alignItems="center">
|
||||
<Grid templateColumns={{ base: "1fr", md: "300px 1fr" }} gap={8} alignItems="center" w="full">
|
||||
<GridItem display={{ base: "none", md: "block" }}>
|
||||
<Image
|
||||
src={game.coverImage?.replace('t_thumb', 't_cover_big_2x')} // Try to get higher res if possible by replacing IGDB logic if used, else just src
|
||||
alt={game.title}
|
||||
borderRadius="xl"
|
||||
boxShadow="2xl"
|
||||
objectFit="cover"
|
||||
h="400px"
|
||||
w="full"
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem color="white">
|
||||
<Stack>
|
||||
<Heading size="4xl" fontWeight="black" letterSpacing="tight">{game.title}</Heading>
|
||||
<Flex gap={4} alignItems="center" flexWrap="wrap">
|
||||
{game.rating && (
|
||||
<Badge colorPalette="yellow" size="lg" variant="solid">
|
||||
<Icon as={FaStar} mr={1} /> {Math.round(game.rating).toFixed(1)}
|
||||
</Badge>
|
||||
)}
|
||||
{game.releaseDate && (
|
||||
<Badge colorPalette="gray" size="lg" variant="surface">
|
||||
<Icon as={FaCalendar} mr={2} />
|
||||
{new Date(game.releaseDate).toLocaleDateString()}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex gap={2} mt={4} flexWrap="wrap">
|
||||
{game.platforms?.map(p => (
|
||||
<Tag.Root key={p.platform.slug} size="lg" variant="subtle">
|
||||
<Tag.Label>{p.platform.name}</Tag.Label>
|
||||
</Tag.Root>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex gap={2} mt={2} flexWrap="wrap">
|
||||
{game.genres?.map(g => (
|
||||
<Badge key={g.genre.slug} variant="outline" colorPalette="purple">
|
||||
{g.genre.name}
|
||||
</Badge>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex mt={6} gap={4}>
|
||||
<Button onClick={handleSubscribe} loading={isSubscribing} colorPalette="purple" variant="solid">
|
||||
<Icon as={FaBell} mr={2} /> Receive Alerts
|
||||
</Button>
|
||||
<Button onClick={handleSync} loading={isSyncing} variant="ghost" colorPalette="whiteAlpha">
|
||||
<Icon as={FaSync} mr={2} /> Refresh Data
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* Content Section */}
|
||||
<Container maxW="container.xl" py={12}>
|
||||
<Grid templateColumns={{ base: "1fr", lg: "2fr 1fr" }} gap={12}>
|
||||
<GridItem>
|
||||
<Heading size="xl" mb={6}>About</Heading>
|
||||
<Text fontSize="lg" lineHeight="tall" color="fg.muted">
|
||||
{game.description || "No description available."}
|
||||
</Text>
|
||||
|
||||
{game.screenshots && game.screenshots.length > 0 && (
|
||||
<Box mt={12}>
|
||||
<Heading size="xl" mb={6}>Gallery</Heading>
|
||||
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)" }} gap={4}>
|
||||
{game.screenshots.map((shot, idx) => (
|
||||
<GridItem key={idx}>
|
||||
<AspectRatio ratio={16 / 9} borderRadius="lg" overflow="hidden">
|
||||
<Image
|
||||
src={shot.url.replace('t_thumb', 't_screenshot_med')}
|
||||
alt={`Screenshot ${idx}`}
|
||||
objectFit="cover"
|
||||
transition="transform 0.2s"
|
||||
_hover={{ transform: 'scale(1.05)' }}
|
||||
/>
|
||||
</AspectRatio>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<Box p={6} borderRadius="xl" borderWidth="1px" borderColor="border">
|
||||
<Heading size="md" mb={6}>Information</Heading>
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={1} color="fg.muted">Developer</Text>
|
||||
<Text fontSize="lg">{game.developer || "Unknown"}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={1} color="fg.muted">Publisher</Text>
|
||||
<Text fontSize="lg">{game.publisher || "Unknown"}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={1} color="fg.muted">Release Date</Text>
|
||||
<Text fontSize="lg">{game.releaseDate ? new Date(game.releaseDate).toLocaleDateString(undefined, { dateStyle: 'long' }) : 'TBD'}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,45 @@
|
||||
'use client';
|
||||
"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'
|
||||
}
|
||||
import { HStack, Box, Text } from '@chakra-ui/react';
|
||||
import { useDynamicTheme } from '@/components/ui/dynamic-theme-provider';
|
||||
import { defaultTheme } from '@/types/theme';
|
||||
import { additionalThemes } from '@/theme/palettes';
|
||||
import { Button } from '@/components/ui/buttons/button';
|
||||
import {
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
} from "@/components/ui/overlays/menu";
|
||||
|
||||
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>
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<Button size="xs" variant="surface">Themes</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
<MenuItem value="default" onClick={() => setTheme(defaultTheme)}>
|
||||
Default
|
||||
</MenuItem>
|
||||
{additionalThemes.map((theme) => (
|
||||
<MenuItem
|
||||
key={theme.key}
|
||||
value={theme.key}
|
||||
onClick={() => setTheme(theme)}
|
||||
>
|
||||
<HStack gap="2">
|
||||
<Box boxSize="4" bg={theme.primaryColor} rounded="full" border="1px solid white" />
|
||||
<Box boxSize="4" bg={theme.backgroundColor} rounded="full" border="1px solid white" ml="-3" />
|
||||
<Text>{theme.gameTitle}</Text>
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ export default function Footer() {
|
||||
<ChakraLink
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.fcs.com.tr"
|
||||
href="/"
|
||||
color={{ base: "primary.500", _dark: "primary.300" }}
|
||||
focusRing="none"
|
||||
ml="1"
|
||||
>
|
||||
{"FCS"}
|
||||
{"Game Calendar"}
|
||||
</ChakraLink>
|
||||
. {t("all-right-reserved")}
|
||||
</Text>
|
||||
|
||||
@@ -165,17 +165,23 @@ export default function Header() {
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href="/home"
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={{ base: "primary.500", _dark: "primary.300" }}
|
||||
fontSize="3xl"
|
||||
fontWeight="extrabold"
|
||||
letterSpacing="wide"
|
||||
bgGradient="to-r"
|
||||
gradientFrom="primary.400"
|
||||
gradientTo="primary.600"
|
||||
bgClip="text"
|
||||
bgSize="200% auto"
|
||||
animation="text-gradient 12s linear infinite"
|
||||
focusRing="none"
|
||||
textDecor="none"
|
||||
transition="all 0.3s ease-in-out"
|
||||
_hover={{
|
||||
color: { base: "primary.900", _dark: "primary.50" },
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{"FCS "}
|
||||
{"Game Calendar"}
|
||||
</ChakraLink>
|
||||
</HStack>
|
||||
|
||||
|
||||
@@ -2325,10 +2325,10 @@ function HomeCard() {
|
||||
)}
|
||||
</For>
|
||||
<For each={["solid", "outline", "subtle"]}>
|
||||
{(variant) => <Avatar key={variant} variant={variant} name="FCS" />}
|
||||
{(variant) => <Avatar key={variant} variant={variant} name="GC" />}
|
||||
</For>
|
||||
<Avatar shape="square" name="FCS" />
|
||||
<Avatar shape="rounded" name="FCS" />
|
||||
<Avatar shape="square" name="GC" />
|
||||
<Avatar shape="rounded" name="GC" />
|
||||
<Avatar
|
||||
src="https://picsum.photos/id/23/200/300"
|
||||
css={{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ThemeConfig, defaultTheme } from '@/types/theme';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { themeApi } from '@/lib/api/theme';
|
||||
|
||||
interface DynamicThemeContextType {
|
||||
theme: ThemeConfig;
|
||||
@@ -23,25 +24,43 @@ interface DynamicThemeProviderProps {
|
||||
export function DynamicThemeProvider({ children, initialTheme }: DynamicThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<ThemeConfig>(initialTheme || defaultTheme);
|
||||
|
||||
// Fetch theme on mount
|
||||
useEffect(() => {
|
||||
const fetchTheme = async () => {
|
||||
try {
|
||||
const response = await themeApi.getTheme();
|
||||
// @ts-ignore - The API response wrapper might be generic, assuming response.data is the payload if wrapped, or response if not.
|
||||
// Based on standard axios + nestjs wrapper: response.data.data or response is the data.
|
||||
// Let's assume our client unwraps it or we check.
|
||||
// If createApiClient returns axios instance, .get returns AxiosResponse.
|
||||
// api-service.ts unwraps response.data.
|
||||
// BUT theme.ts calls client.get directly.
|
||||
// Let's fix theme.ts to use apiRequest or handle .data
|
||||
|
||||
// Correction: theme.ts uses client.get. client is axios instance.
|
||||
// So response is AxiosResponse. response.data is the body ({ success, data: theme }).
|
||||
if (response.data && response.data.success && response.data.data) {
|
||||
setTheme(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch theme:', error);
|
||||
}
|
||||
};
|
||||
fetchTheme();
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
// ... rest of logic
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,14 @@ const LocaleSwitcher = () => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
const params = useParams();
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
// Effect to handle hydration match
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const collections = createListCollection({
|
||||
items: [
|
||||
@@ -39,6 +46,11 @@ const LocaleSwitcher = () => {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectRoot
|
||||
disabled={isPending}
|
||||
|
||||
25
src/lib/api/games.ts
Normal file
25
src/lib/api/games.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import { createApiClient } from './create-api-client';
|
||||
|
||||
export interface Game {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
coverImage?: string;
|
||||
description?: string;
|
||||
releaseDate?: string;
|
||||
rating?: number;
|
||||
developer?: string;
|
||||
publisher?: string;
|
||||
platforms?: { platform: { name: string; slug: string; icon?: string } }[];
|
||||
genres?: { genre: { name: string; slug: string } }[];
|
||||
screenshots?: { url: string }[];
|
||||
}
|
||||
|
||||
const client = createApiClient('/games');
|
||||
|
||||
export const gamesApi = {
|
||||
getAll: (params?: any) => client.get<{ items: Game[]; meta: any }>('/', { params }),
|
||||
getBySlug: (slug: string) => client.get<{ data: Game }>(`/${slug}`),
|
||||
sync: (slug: string) => client.post(`/${slug}/sync`),
|
||||
};
|
||||
8
src/lib/api/notifications.ts
Normal file
8
src/lib/api/notifications.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createApiClient } from './create-api-client';
|
||||
|
||||
const client = createApiClient('/api/backend/notifications');
|
||||
|
||||
export const notificationsApi = {
|
||||
subscribe: (gameId: string) => client.post('/subscribe', { gameId }),
|
||||
unsubscribe: (gameId: string) => client.post('/unsubscribe', { gameId }),
|
||||
};
|
||||
9
src/lib/api/theme.ts
Normal file
9
src/lib/api/theme.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApiClient } from './create-api-client';
|
||||
import { ThemeConfig } from '@/types/theme';
|
||||
|
||||
const client = createApiClient('/api/backend/theme');
|
||||
|
||||
export const themeApi = {
|
||||
getTheme: () => client.get<{ data: ThemeConfig; success: boolean }>('/'),
|
||||
updateTheme: (data: Partial<ThemeConfig>) => client.patch<{ data: ThemeConfig; success: boolean }>('/', data),
|
||||
};
|
||||
@@ -4,6 +4,8 @@ import { routing } from './i18n/routing';
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: ['/', '/(tr|en)/:path*']
|
||||
// Match all pathnames except for
|
||||
// - … if they start with `/api`, `/_next` or `/_vercel`
|
||||
// - … the ones containing a dot (e.g. `favicon.ico`)
|
||||
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
|
||||
};
|
||||
|
||||
74
src/theme/palettes.ts
Normal file
74
src/theme/palettes.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ThemeConfig } from '@/types/theme';
|
||||
|
||||
export 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
|
||||
};
|
||||
|
||||
export 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'
|
||||
};
|
||||
|
||||
// Palette 1: https://colorhunt.co/palette/280905740a03c3110ce6501b
|
||||
// Colors: #280905, #740a03, #c3110c, #e6501b
|
||||
export const fireTheme: ThemeConfig = {
|
||||
key: 'fire',
|
||||
isActive: true,
|
||||
gameTitle: 'Blazing Fast',
|
||||
primaryColor: '#e6501b', // Bright orange
|
||||
secondaryColor: '#c3110c', // Darker red/orange
|
||||
backgroundColor: '#280905', // Deep dark brown/red
|
||||
};
|
||||
|
||||
// Palette 2: https://colorhunt.co/palette/3b060a8a0000c83f12fff287
|
||||
// Colors: #3b060a, #8a0000, #c83f12, #fff287
|
||||
export const crimsonTheme: ThemeConfig = {
|
||||
key: 'crimson',
|
||||
isActive: true,
|
||||
gameTitle: 'Crimson Tide',
|
||||
primaryColor: '#fff287', // Light yellow accent
|
||||
secondaryColor: '#c83f12', // Red/Orange
|
||||
backgroundColor: '#3b060a', // Deep dark red
|
||||
};
|
||||
|
||||
// Palette 3: https://colorhunt.co/palette/0054610c7779249e943bc1a8
|
||||
// Colors: #005461, #0c7779, #249e94, #3bc1a8
|
||||
export const oceanTheme: ThemeConfig = {
|
||||
key: 'ocean',
|
||||
isActive: true,
|
||||
gameTitle: 'Oceanic Depths',
|
||||
primaryColor: '#3bc1a8', // Light teal
|
||||
secondaryColor: '#249e94', // Teal
|
||||
backgroundColor: '#005461', // Dark blue/teal
|
||||
};
|
||||
|
||||
// Palette 4: https://colorhunt.co/palette/1a3263547792fab95be8e2db
|
||||
// Colors: #1a3263, #547792, #fab95b, #e8e2db
|
||||
export const nightTheme: ThemeConfig = {
|
||||
key: 'night',
|
||||
isActive: true,
|
||||
gameTitle: 'Starry Night',
|
||||
primaryColor: '#fab95b', // Golden yellow
|
||||
secondaryColor: '#547792', // Muted blue
|
||||
backgroundColor: '#1a3263', // Deep blue
|
||||
};
|
||||
|
||||
export const additionalThemes = [
|
||||
eldenRingTheme,
|
||||
cyberPunkTheme,
|
||||
fireTheme,
|
||||
crimsonTheme,
|
||||
oceanTheme,
|
||||
nightTheme,
|
||||
];
|
||||
Reference in New Issue
Block a user