From 1f123b9f65b0633812f5528c49901aa60f3cd80c Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Fri, 30 Jan 2026 04:48:46 +0300 Subject: [PATCH] main --- messages/en.json | 6 +- messages/tr.json | 6 +- next-env.d.ts | 2 +- next.config.ts | 2 +- src/app/[locale]/(site)/home/page.tsx | 2 +- src/app/[locale]/games/[slug]/page.tsx | 12 ++ src/app/[locale]/global.css | 14 ++ .../features/calendar/game-calendar.tsx | 45 ++-- src/components/features/games/GameDetail.tsx | 203 ++++++++++++++++++ src/components/features/theme-switcher.tsx | 63 +++--- src/components/layout/footer/footer.tsx | 4 +- src/components/layout/header/header.tsx | 16 +- src/components/site/home/home-card.tsx | 22 +- src/components/ui/dynamic-theme-provider.tsx | 35 ++- src/components/ui/locale-switcher.tsx | 12 ++ src/lib/api/games.ts | 25 +++ src/lib/api/notifications.ts | 8 + src/lib/api/theme.ts | 9 + src/middleware.ts | 6 +- src/theme/palettes.ts | 74 +++++++ 20 files changed, 490 insertions(+), 76 deletions(-) create mode 100644 src/app/[locale]/games/[slug]/page.tsx create mode 100644 src/components/features/games/GameDetail.tsx create mode 100644 src/lib/api/games.ts create mode 100644 src/lib/api/notifications.ts create mode 100644 src/lib/api/theme.ts create mode 100644 src/theme/palettes.ts diff --git a/messages/en.json b/messages/en.json index 390ae21..3974242 100644 --- a/messages/en.json +++ b/messages/en.json @@ -28,5 +28,9 @@ "name": "Name", "low": "Low", "medium": "Medium", - "high": "High" + "high": "High", + "predictions": "Predictions", + "games": "Games", + "events": "Events", + "calendar": "Calendar" } \ No newline at end of file diff --git a/messages/tr.json b/messages/tr.json index e8433d5..beda1f6 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -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" } \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -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. diff --git a/next.config.ts b/next.config.ts index 11a5810..db56568 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,7 +10,7 @@ const nextConfig: NextConfig = { return [ { source: "/api/backend/:path*", - destination: "http://localhost:3000/api/:path*", + destination: "http://localhost:4000/api/:path*", }, ]; }, diff --git a/src/app/[locale]/(site)/home/page.tsx b/src/app/[locale]/(site)/home/page.tsx index 6a9867a..7eadb0e 100644 --- a/src/app/[locale]/(site)/home/page.tsx +++ b/src/app/[locale]/(site)/home/page.tsx @@ -13,7 +13,7 @@ export default function HomePage() { {/* Hero Section / GOTY Banner could go here */} - Game Calendar + Calendar Track releases, events, and showcases in one place. diff --git a/src/app/[locale]/games/[slug]/page.tsx b/src/app/[locale]/games/[slug]/page.tsx new file mode 100644 index 0000000..b8456f8 --- /dev/null +++ b/src/app/[locale]/games/[slug]/page.tsx @@ -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 ; +} diff --git a/src/app/[locale]/global.css b/src/app/[locale]/global.css index c7cbb5b..dbe9be4 100644 --- a/src/app/[locale]/global.css +++ b/src/app/[locale]/global.css @@ -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%; + } +} \ No newline at end of file diff --git a/src/components/features/calendar/game-calendar.tsx b/src/components/features/calendar/game-calendar.tsx index 90d93d6..e42e6f4 100644 --- a/src/components/features/calendar/game-calendar.tsx +++ b/src/components/features/calendar/game-calendar.tsx @@ -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 ( - + {/* Header */} @@ -107,7 +108,7 @@ export function GameCalendar({ games = [], events = [] }: CalendarProps) { return ( - {items.map((item: any, idx) => ( - - {item.title} - - ))} + {items.map((item: any, idx) => { + const badge = ( + + {item.title} + + ); + + if (item.type === 'game') { + return ( + + {badge} + + ); + } + + return badge; + })} {/* Background image effect for heavy days? Optional polish */} diff --git a/src/components/features/games/GameDetail.tsx b/src/components/features/games/GameDetail.tsx new file mode 100644 index 0000000..5484d52 --- /dev/null +++ b/src/components/features/games/GameDetail.tsx @@ -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 Loading...; + if (error || !response?.data?.data) return ( + + Game not found + + + ); + + const game = response.data.data; + + return ( + + {/* Hero Section */} + + {/* Background Blur */} + + + + + + {game.title} + + + + {game.title} + + {game.rating && ( + + {Math.round(game.rating).toFixed(1)} + + )} + {game.releaseDate && ( + + + {new Date(game.releaseDate).toLocaleDateString()} + + )} + + + + {game.platforms?.map(p => ( + + {p.platform.name} + + ))} + + + + {game.genres?.map(g => ( + + {g.genre.name} + + ))} + + + + + + + + + + + + + {/* Content Section */} + + + + About + + {game.description || "No description available."} + + + {game.screenshots && game.screenshots.length > 0 && ( + + Gallery + + {game.screenshots.map((shot, idx) => ( + + + {`Screenshot + + + ))} + + + )} + + + + + Information + + + Developer + {game.developer || "Unknown"} + + + Publisher + {game.publisher || "Unknown"} + + + Release Date + {game.releaseDate ? new Date(game.releaseDate).toLocaleDateString(undefined, { dateStyle: 'long' }) : 'TBD'} + + + + + + + + ); +} diff --git a/src/components/features/theme-switcher.tsx b/src/components/features/theme-switcher.tsx index 1e015c5..585afcc 100644 --- a/src/components/features/theme-switcher.tsx +++ b/src/components/features/theme-switcher.tsx @@ -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 ( - Simulate GOTY: - - - + + + + + + setTheme(defaultTheme)}> + Default + + {additionalThemes.map((theme) => ( + setTheme(theme)} + > + + + + {theme.gameTitle} + + + ))} + + ); } diff --git a/src/components/layout/footer/footer.tsx b/src/components/layout/footer/footer.tsx index ad35a98..c22ed9a 100644 --- a/src/components/layout/footer/footer.tsx +++ b/src/components/layout/footer/footer.tsx @@ -23,12 +23,12 @@ export default function Footer() { - {"FCS"} + {"Game Calendar"} . {t("all-right-reserved")} diff --git a/src/components/layout/header/header.tsx b/src/components/layout/header/header.tsx index 6e1041d..e000103 100644 --- a/src/components/layout/header/header.tsx +++ b/src/components/layout/header/header.tsx @@ -165,17 +165,23 @@ export default function Header() { - {"FCS "} + {"Game Calendar"} diff --git a/src/components/site/home/home-card.tsx b/src/components/site/home/home-card.tsx index 71880c7..454cd3b 100644 --- a/src/components/site/home/home-card.tsx +++ b/src/components/site/home/home-card.tsx @@ -1004,15 +1004,15 @@ function HomeCard() { css={ index === 0 ? { - fontStyle: "inherit", - color: "primary.500", - position: "relative", - } + fontStyle: "inherit", + color: "primary.500", + position: "relative", + } : { - fontStyle: "italic", - color: "red.500", - position: "relative", - } + fontStyle: "italic", + color: "red.500", + position: "relative", + } } > {chunk.text} @@ -2325,10 +2325,10 @@ function HomeCard() { )} - {(variant) => } + {(variant) => } - - + + (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); } diff --git a/src/components/ui/locale-switcher.tsx b/src/components/ui/locale-switcher.tsx index a647ca7..9f8451f 100644 --- a/src/components/ui/locale-switcher.tsx +++ b/src/components/ui/locale-switcher.tsx @@ -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 ( client.get<{ items: Game[]; meta: any }>('/', { params }), + getBySlug: (slug: string) => client.get<{ data: Game }>(`/${slug}`), + sync: (slug: string) => client.post(`/${slug}/sync`), +}; diff --git a/src/lib/api/notifications.ts b/src/lib/api/notifications.ts new file mode 100644 index 0000000..6936c94 --- /dev/null +++ b/src/lib/api/notifications.ts @@ -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 }), +}; diff --git a/src/lib/api/theme.ts b/src/lib/api/theme.ts new file mode 100644 index 0000000..49789ff --- /dev/null +++ b/src/lib/api/theme.ts @@ -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) => client.patch<{ data: ThemeConfig; success: boolean }>('/', data), +}; diff --git a/src/middleware.ts b/src/middleware.ts index b4f3ad3..3a4b4e5 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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|.*\\..*).*)'] }; diff --git a/src/theme/palettes.ts b/src/theme/palettes.ts new file mode 100644 index 0000000..cfaa1c4 --- /dev/null +++ b/src/theme/palettes.ts @@ -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, +];