├── src ├── index.css ├── vite-env.d.ts ├── assets │ ├── logo.webp │ ├── meh.webp │ ├── bulls-eye.webp │ ├── thumbs-up.webp │ ├── no-image-placeholder.webp │ └── react.svg ├── entities │ ├── Publisher.ts │ ├── Platform.ts │ ├── Genre.ts │ ├── Screenshot.ts │ ├── Trailer.ts │ └── Game.ts ├── hooks │ ├── useGenre.ts │ ├── usePlatform.ts │ ├── useGame.ts │ ├── useTrailers.ts │ ├── useScreenshots.ts │ ├── useGenres.ts │ ├── usePlatforms.ts │ └── useGames.ts ├── components │ ├── GameCardSkeleton.tsx │ ├── CriticScore.tsx │ ├── ColorModeSwitch.tsx │ ├── DefinitionItem.tsx │ ├── GameCardContainer.tsx │ ├── GameTrailer.tsx │ ├── NavBar.tsx │ ├── GameScreenshots.tsx │ ├── Emoji.tsx │ ├── GameHeading.tsx │ ├── ExpandableText.tsx │ ├── SearchInput.tsx │ ├── GameAttributes.tsx │ ├── GameCard.tsx │ ├── PlatformIconList.tsx │ ├── PlatformSelector.tsx │ ├── SortSelector.tsx │ ├── GenreList.tsx │ └── GameGrid.tsx ├── pages │ ├── Layout.tsx │ ├── ErrorPage.tsx │ ├── HomePage.tsx │ └── GameDetailPage.tsx ├── services │ ├── image-url.ts │ └── api-client.ts ├── theme.ts ├── routes.tsx ├── main.tsx ├── store.ts └── data │ ├── platforms.ts │ └── genres.ts ├── .DS_Store ├── vite.config.ts ├── tsconfig.node.json ├── index.html ├── README.md ├── tsconfig.json ├── package.json └── public └── vite.svg /src/index.css: -------------------------------------------------------------------------------- 1 | form { 2 | width: 100%; 3 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/src/assets/logo.webp -------------------------------------------------------------------------------- /src/assets/meh.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/src/assets/meh.webp -------------------------------------------------------------------------------- /src/entities/Publisher.ts: -------------------------------------------------------------------------------- 1 | export default interface Publisher { 2 | id: number; 3 | name: string; 4 | } -------------------------------------------------------------------------------- /src/assets/bulls-eye.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/src/assets/bulls-eye.webp -------------------------------------------------------------------------------- /src/assets/thumbs-up.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/src/assets/thumbs-up.webp -------------------------------------------------------------------------------- /src/entities/Platform.ts: -------------------------------------------------------------------------------- 1 | export default interface Platform { 2 | id: number; 3 | name: string; 4 | slug: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/no-image-placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RicardoChaseCo/Game_League/HEAD/src/assets/no-image-placeholder.webp -------------------------------------------------------------------------------- /src/entities/Genre.ts: -------------------------------------------------------------------------------- 1 | export default interface Genre { 2 | id: number; 3 | name: string; 4 | image_background: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/entities/Screenshot.ts: -------------------------------------------------------------------------------- 1 | export default interface Screenshot { 2 | id: number; 3 | image: string; 4 | width: number; 5 | height: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/entities/Trailer.ts: -------------------------------------------------------------------------------- 1 | export default interface Trailer { 2 | id: number; 3 | name: string; 4 | preview: string; 5 | data: { 480: string; max: string }; 6 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: '/' 8 | }) 9 | -------------------------------------------------------------------------------- /src/hooks/useGenre.ts: -------------------------------------------------------------------------------- 1 | import useGenres from './useGenres'; 2 | 3 | const useGenre = (id?: number) => { 4 | const { data: genres } = useGenres(); 5 | return genres?.results.find((g) => g.id === id); 6 | }; 7 | 8 | export default useGenre; 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/usePlatform.ts: -------------------------------------------------------------------------------- 1 | import usePlatforms from './usePlatforms'; 2 | 3 | const usePlatform = (id?: number) => { 4 | const { data: platforms } = usePlatforms(); 5 | return platforms?.results.find((p) => p.id === id); 6 | }; 7 | 8 | export default usePlatform; 9 | -------------------------------------------------------------------------------- /src/components/GameCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, Skeleton, SkeletonText } from '@chakra-ui/react' 2 | 3 | const GameCardSkeleton = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default GameCardSkeleton -------------------------------------------------------------------------------- /src/pages/Layout.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from '../components/NavBar'; 2 | import { Outlet } from 'react-router-dom'; 3 | import { Box } from '@chakra-ui/react'; 4 | 5 | const Layout = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /src/services/image-url.ts: -------------------------------------------------------------------------------- 1 | import noImage from '../assets/no-image-placeholder.webp'; 2 | 3 | const getCroppedImageUrl = (url: string) => { 4 | if (!url) return noImage; 5 | 6 | const target = 'media/'; 7 | const index = url.indexOf(target) + target.length; 8 | return url.slice(0, index) + 'crop/600/400/' + url.slice(index); 9 | } 10 | 11 | export default getCroppedImageUrl; -------------------------------------------------------------------------------- /src/hooks/useGame.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import APIClient from '../services/api-client'; 3 | import Game from '../entities/Game'; 4 | 5 | const apiClient = new APIClient('/games'); 6 | 7 | const useGame = (slug: string) => 8 | useQuery({ 9 | queryKey: ['games', slug], 10 | queryFn: () => apiClient.get(slug), 11 | }); 12 | 13 | export default useGame; 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GameHub 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/CriticScore.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@chakra-ui/react'; 2 | 3 | interface Props { 4 | score: number; 5 | } 6 | 7 | const CriticScore = ({ score }: Props) => { 8 | let color = score > 75 ? 'green' : score > 60 ? 'yellow' : ''; 9 | 10 | return ( 11 | {score} 12 | ) 13 | } 14 | 15 | export default CriticScore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Link 2 | http://xingnanjiangprojectgame.online/ 3 | 4 | ### Tech Stacks 5 | * React Hook Forms for forms 6 | * Zod schema validation 7 | * Zustand for global state management (alternative to Redux) 8 | * React Query for caching and state management 9 | * React Router for routing 10 | * React Icons for icons 11 | * ChakraUI and Bootstrap for styling 12 | * webp for web image optimization 13 | * Multiple customized hooks for generic data fetching 14 | -------------------------------------------------------------------------------- /src/entities/Game.ts: -------------------------------------------------------------------------------- 1 | import Genre from './Genre'; 2 | import Platform from './Platform'; 3 | import Publisher from './Publisher'; 4 | 5 | export default interface Game { 6 | id: number; 7 | name: string; 8 | slug: string; 9 | genres: Genre[]; 10 | publishers: Publisher[]; 11 | description_raw: string; 12 | background_image: string; 13 | parent_platforms: { platform: Platform }[]; 14 | metacritic: number; 15 | rating_top: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ColorModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Switch, Text, useColorMode } from '@chakra-ui/react' 2 | 3 | const ColorModeSwitch = () => { 4 | const {toggleColorMode, colorMode} = useColorMode(); 5 | 6 | return ( 7 | 8 | 9 | Dark Mode 10 | 11 | ) 12 | } 13 | 14 | export default ColorModeSwitch -------------------------------------------------------------------------------- /src/hooks/useTrailers.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import Trailer from '../entities/Trailer'; 3 | import APIClient from '../services/api-client'; 4 | 5 | const useTrailers = (gameId: number) => { 6 | const apiClient = new APIClient( 7 | `/games/${gameId}/movies` 8 | ); 9 | 10 | return useQuery({ 11 | queryKey: ['trailers', gameId], 12 | queryFn: apiClient.getAll, 13 | }); 14 | }; 15 | 16 | export default useTrailers; 17 | -------------------------------------------------------------------------------- /src/hooks/useScreenshots.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import Screenshot from '../entities/Screenshot'; 3 | import APIClient from '../services/api-client'; 4 | 5 | const useScreenshots = (gameId: number) => { 6 | const apiClient = new APIClient( 7 | `/games/${gameId}/screenshots` 8 | ); 9 | 10 | return useQuery({ 11 | queryKey: ['screenshots', gameId], 12 | queryFn: apiClient.getAll, 13 | }); 14 | }; 15 | 16 | export default useScreenshots; 17 | -------------------------------------------------------------------------------- /src/hooks/useGenres.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import ms from 'ms'; 3 | import genres from '../data/genres'; 4 | import APIClient from '../services/api-client'; 5 | import Genre from '../entities/Genre'; 6 | 7 | const apiClient = new APIClient('/genres'); 8 | 9 | const useGenres = () => 10 | useQuery({ 11 | queryKey: ['genres'], 12 | queryFn: apiClient.getAll, 13 | staleTime: ms('24h'), 14 | initialData: genres, 15 | }); 16 | 17 | export default useGenres; 18 | -------------------------------------------------------------------------------- /src/components/DefinitionItem.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading } from '@chakra-ui/react'; 2 | import React, { ReactNode } from 'react' 3 | 4 | interface Props { 5 | term: string; 6 | children: ReactNode | ReactNode[]; 7 | } 8 | 9 | const DefinitionItem = ({ term, children }: Props) => { 10 | return ( 11 | 12 | 13 | {term} 14 | 15 |
16 | {children} 17 |
18 |
19 | ) 20 | } 21 | 22 | export default DefinitionItem -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme, ThemeConfig } from "@chakra-ui/react"; 2 | 3 | const config: ThemeConfig = { 4 | initialColorMode: 'dark' 5 | }; 6 | 7 | const theme = extendTheme({ 8 | config, 9 | colors: { 10 | gray: { 11 | 50: '#f9f9f9', 12 | 100: '#ededed', 13 | 200: '#d3d3d3', 14 | 300: '#b3b3b3', 15 | 400: '#a0a0a0', 16 | 500: '#898989', 17 | 600: '#6c6c6c', 18 | 700: '#202020', 19 | 800: '#121212', 20 | 900: '#111' 21 | } 22 | } 23 | }); 24 | 25 | export default theme; -------------------------------------------------------------------------------- /src/components/GameCardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import { ReactNode } from 'react'; 3 | 4 | interface Props { 5 | children: ReactNode; 6 | } 7 | 8 | const GameCardContainer = ({ children }: Props) => { 9 | return ( 10 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export default GameCardContainer; 24 | -------------------------------------------------------------------------------- /src/components/GameTrailer.tsx: -------------------------------------------------------------------------------- 1 | import useTrailers from '../hooks/useTrailers'; 2 | 3 | interface Props { 4 | gameId: number; 5 | } 6 | 7 | const GameTrailer = ({ gameId }: Props) => { 8 | const { data, error, isLoading } = useTrailers(gameId); 9 | 10 | if (isLoading) return null; 11 | 12 | if (error) throw error; 13 | 14 | const first = data?.results[0]; 15 | return first ? ( 16 |