204 lines
9.9 KiB
TypeScript
204 lines
9.9 KiB
TypeScript
'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>
|
|
);
|
|
}
|