├── 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 |
21 | ) : null;
22 | };
23 |
24 | export default GameTrailer;
25 |
--------------------------------------------------------------------------------
/src/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Image } from '@chakra-ui/react';
2 | import { Link } from 'react-router-dom';
3 | import logo from '../assets/logo.webp';
4 | import ColorModeSwitch from './ColorModeSwitch';
5 | import SearchInput from './SearchInput';
6 |
7 | const NavBar = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default NavBar;
20 |
--------------------------------------------------------------------------------
/src/hooks/usePlatforms.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import ms from 'ms';
3 | import platforms from '../data/platforms';
4 | import APIClient from '../services/api-client';
5 | import Platform from '../entities/Platform';
6 |
7 | const apiClient = new APIClient(
8 | '/platforms/lists/parents'
9 | );
10 |
11 | const usePlatforms = () =>
12 | useQuery({
13 | queryKey: ['platforms'],
14 | queryFn: apiClient.getAll,
15 | staleTime: ms('24h'),
16 | initialData: platforms,
17 | });
18 |
19 | export default usePlatforms;
20 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { createBrowserRouter } from "react-router-dom";
2 | import ErrorPage from "./pages/ErrorPage";
3 | import GameDetailPage from "./pages/GameDetailPage";
4 | import HomePage from "./pages/HomePage";
5 | import Layout from "./pages/Layout";
6 |
7 | const router = createBrowserRouter([
8 | {
9 | path: '/',
10 | element: ,
11 | errorElement: ,
12 | children: [
13 | { index: true, element: },
14 | { path: 'games/:slug', element: }
15 | ]
16 | }
17 | ]);
18 |
19 | export default router;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/GameScreenshots.tsx:
--------------------------------------------------------------------------------
1 | import { Image, SimpleGrid } from '@chakra-ui/react';
2 | import React from 'react'
3 | import useScreenshots from '../hooks/useScreenshots';
4 |
5 | interface Props {
6 | gameId: number;
7 | }
8 |
9 | const GameScreenshots = ({ gameId }: Props) => {
10 | const {data, isLoading, error } = useScreenshots(gameId);
11 |
12 | if (isLoading) return null;
13 |
14 | if (error) throw error;
15 |
16 | return (
17 |
18 | {data?.results.map(file =>
19 | )}
20 |
21 | )
22 | }
23 |
24 | export default GameScreenshots
--------------------------------------------------------------------------------
/src/pages/ErrorPage.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Heading, Text } from '@chakra-ui/react';
2 | import React from 'react';
3 | import {
4 | isRouteErrorResponse,
5 | useRouteError,
6 | } from 'react-router-dom';
7 | import NavBar from '../components/NavBar';
8 |
9 | const ErrorPage = () => {
10 | const error = useRouteError();
11 |
12 | return (
13 | <>
14 |
15 |
16 | Oops
17 |
18 | {isRouteErrorResponse(error)
19 | ? 'This page does not exist.'
20 | : 'An unexpected error occurred.'}
21 |
22 |
23 | >
24 | );
25 | };
26 |
27 | export default ErrorPage;
28 |
--------------------------------------------------------------------------------
/src/components/Emoji.tsx:
--------------------------------------------------------------------------------
1 | import bullsEye from '../assets/bulls-eye.webp';
2 | import thumbsUp from '../assets/thumbs-up.webp';
3 | import meh from '../assets/meh.webp';
4 | import { Image, ImageProps } from '@chakra-ui/react';
5 |
6 | interface Props {
7 | rating: number;
8 | }
9 |
10 | const Emoji = ({ rating }: Props) => {
11 | if (rating < 3) return null;
12 |
13 | const emojiMap: { [key: number]: ImageProps } = {
14 | 3: { src: meh, alt: 'meh', boxSize: '25px' },
15 | 4: { src: thumbsUp, alt: 'recommended', boxSize: '25px' },
16 | 5: { src: bullsEye, alt: 'exceptional', boxSize: '35px' },
17 | }
18 |
19 | return (
20 |
21 | )
22 | }
23 |
24 | export default Emoji
--------------------------------------------------------------------------------
/src/components/GameHeading.tsx:
--------------------------------------------------------------------------------
1 | import { Heading } from '@chakra-ui/react';
2 | import useGenre from '../hooks/useGenre';
3 | import usePlatform from '../hooks/usePlatform';
4 | import useGameQueryStore from '../store';
5 |
6 | const GameHeading = () => {
7 | const genreId = useGameQueryStore(s => s.gameQuery.genreId);
8 | const genre = useGenre(genreId);
9 |
10 | const platformId = useGameQueryStore(s => s.gameQuery.platformId);
11 | const platform = usePlatform(platformId);
12 |
13 | const heading = `${platform?.name || ''} ${
14 | genre?.name || ''
15 | } Games`;
16 |
17 | return (
18 |
19 | {heading}
20 |
21 | );
22 | };
23 |
24 | export default GameHeading;
25 |
--------------------------------------------------------------------------------
/src/services/api-client.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig } from 'axios';
2 |
3 | export interface FetchResponse {
4 | count: number;
5 | next: string | null;
6 | results: T[];
7 | }
8 |
9 | const axiosInstance = axios.create({
10 | baseURL: 'https://api.rawg.io/api',
11 | params: {
12 | key: '443b62f091f84b1db5be335fe9b5bd43',
13 | },
14 | });
15 |
16 | class APIClient {
17 | endpoint: string;
18 |
19 | constructor(endpoint: string) {
20 | this.endpoint = endpoint;
21 | }
22 |
23 | getAll = (config: AxiosRequestConfig) => {
24 | return axiosInstance
25 | .get>(this.endpoint, config)
26 | .then((res) => res.data);
27 | };
28 |
29 | get = (id: number | string) => {
30 | return axiosInstance
31 | .get(this.endpoint + '/' + id)
32 | .then((res) => res.data);
33 | };
34 | }
35 |
36 | export default APIClient;
37 |
--------------------------------------------------------------------------------
/src/components/ExpandableText.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Text } from '@chakra-ui/react';
2 | import React, { useState } from 'react';
3 |
4 | interface Props {
5 | children: string;
6 | }
7 |
8 | const ExpandableText = ({ children }: Props) => {
9 | const [expanded, setExpanded] = useState(false);
10 | const limit = 300;
11 |
12 | if (!children) return null;
13 |
14 | if (children.length <= limit) return {children};
15 |
16 | const summary = expanded ? children : children.substring(0, limit) + '...';
17 |
18 | return (
19 |
20 | {summary}
21 |
30 |
31 | );
32 | };
33 |
34 | export default ExpandableText;
35 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'
4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
6 | import theme from './theme'
7 | import './index.css'
8 | import { RouterProvider } from 'react-router-dom'
9 | import router from './routes'
10 |
11 | const queryClient = new QueryClient();
12 |
13 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | )
24 |
--------------------------------------------------------------------------------
/src/components/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from "@chakra-ui/react";
2 | import { useRef } from "react";
3 | import { BsSearch } from "react-icons/bs";
4 | import { useNavigate } from "react-router-dom";
5 | import useGameQueryStore from "../store";
6 |
7 | const SearchInput = () => {
8 | const ref = useRef(null);
9 | const setSearchText = useGameQueryStore(s => s.setSearchText);
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
25 | );
26 | };
27 |
28 | export default SearchInput;
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "game-hub",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@chakra-ui/react": "^2.5.1",
13 | "@emotion/react": "^11.10.6",
14 | "@emotion/styled": "^11.10.6",
15 | "@tanstack/react-query": "4.28",
16 | "@tanstack/react-query-devtools": "4.28",
17 | "axios": "^1.3.4",
18 | "framer-motion": "^10.0.1",
19 | "ms": "^2.1.3",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-icons": "^4.7.1",
23 | "react-infinite-scroll-component": "6.1",
24 | "react-router-dom": "^6.10.0",
25 | "zustand": "^4.3.7"
26 | },
27 | "devDependencies": {
28 | "@types/ms": "^0.7.31",
29 | "@types/react": "^18.0.27",
30 | "@types/react-dom": "^18.0.10",
31 | "@vitejs/plugin-react": "^3.1.0",
32 | "typescript": "^4.9.3",
33 | "vite": "^4.1.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/hooks/useGames.ts:
--------------------------------------------------------------------------------
1 | import { useInfiniteQuery } from '@tanstack/react-query';
2 | import ms from 'ms';
3 | import APIClient, {
4 | FetchResponse,
5 | } from '../services/api-client';
6 | import useGameQueryStore from '../store';
7 | import Game from '../entities/Game';
8 |
9 | const apiClient = new APIClient('/games');
10 |
11 | const useGames = () => {
12 | const gameQuery = useGameQueryStore((s) => s.gameQuery);
13 |
14 | return useInfiniteQuery, Error>({
15 | queryKey: ['games', gameQuery],
16 | queryFn: ({ pageParam = 1 }) =>
17 | apiClient.getAll({
18 | params: {
19 | genres: gameQuery.genreId,
20 | parent_platforms: gameQuery.platformId,
21 | ordering: gameQuery.sortOrder,
22 | search: gameQuery.searchText,
23 | page: pageParam,
24 | },
25 | }),
26 | getNextPageParam: (lastPage, allPages) => {
27 | return lastPage.next ? allPages.length + 1 : undefined;
28 | },
29 | staleTime: ms('24h'),
30 | });
31 | };
32 |
33 | export default useGames;
34 |
--------------------------------------------------------------------------------
/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Grid, Show, GridItem, Flex } from '@chakra-ui/react'
2 | import GameGrid from '../components/GameGrid'
3 | import GameHeading from '../components/GameHeading'
4 | import GenreList from '../components/GenreList'
5 | import PlatformSelector from '../components/PlatformSelector'
6 | import SortSelector from '../components/SortSelector'
7 |
8 | const HomePage = () => {
9 | return (
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default HomePage
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface GameQuery {
4 | genreId?: number;
5 | platformId?: number;
6 | sortOrder?: string;
7 | searchText?: string;
8 | }
9 |
10 | interface GameQueryStore {
11 | gameQuery: GameQuery;
12 | setSearchText: (searchText: string) => void;
13 | setGenreId: (genreId: number) => void;
14 | setPlatformId: (platformId: number) => void;
15 | setSortOrder: (sortOrder: string) => void;
16 | }
17 |
18 | const useGameQueryStore = create((set) => ({
19 | gameQuery: {},
20 | setSearchText: (searchText) =>
21 | set(() => ({ gameQuery: { searchText } })),
22 | setGenreId: (genreId) =>
23 | set((store) => ({
24 | gameQuery: { ...store.gameQuery, genreId, searchText: undefined },
25 | })),
26 | setPlatformId: (platformId) =>
27 | set((store) => ({
28 | gameQuery: {
29 | ...store.gameQuery,
30 | platformId,
31 | searchText: undefined,
32 | },
33 | })),
34 | setSortOrder: (sortOrder) =>
35 | set((store) => ({
36 | gameQuery: { ...store.gameQuery, sortOrder },
37 | })),
38 | }));
39 |
40 | export default useGameQueryStore;
41 |
--------------------------------------------------------------------------------
/src/components/GameAttributes.tsx:
--------------------------------------------------------------------------------
1 | import { SimpleGrid, Text } from '@chakra-ui/react';
2 | import Game from '../entities/Game';
3 | import CriticScore from './CriticScore';
4 | import DefinitionItem from './DefinitionItem';
5 |
6 | interface Props {
7 | game: Game;
8 | }
9 |
10 | const GameAttributes = ({ game }: Props) => {
11 | return (
12 |
13 |
14 | {game.parent_platforms?.map(({ platform }) => (
15 | {platform.name}
16 | ))}
17 |
18 |
19 |
20 |
21 |
22 | {game.genres.map((genre) => (
23 | {genre.name}
24 | ))}
25 |
26 |
27 | {game.publishers?.map((publisher) => (
28 | {publisher.name}
29 | ))}
30 |
31 |
32 | );
33 | };
34 |
35 | export default GameAttributes;
36 |
--------------------------------------------------------------------------------
/src/components/GameCard.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardBody,
4 | Heading,
5 | HStack,
6 | Image
7 | } from '@chakra-ui/react';
8 | import { Link } from 'react-router-dom';
9 | import Game from '../entities/Game';
10 | import getCroppedImageUrl from '../services/image-url';
11 | import CriticScore from './CriticScore';
12 | import Emoji from './Emoji';
13 | import PlatformIconList from './PlatformIconList';
14 |
15 | interface Props {
16 | game: Game;
17 | }
18 |
19 | const GameCard = ({ game }: Props) => {
20 | return (
21 |
22 |
23 |
24 |
25 | p.platform
28 | )}
29 | />
30 |
31 |
32 |
33 | {game.name}
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default GameCard;
42 |
--------------------------------------------------------------------------------
/src/components/PlatformIconList.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FaWindows,
3 | FaPlaystation,
4 | FaXbox,
5 | FaApple,
6 | FaLinux,
7 | FaAndroid,
8 | } from 'react-icons/fa';
9 | import { MdPhoneIphone } from 'react-icons/md';
10 | import { SiNintendo } from 'react-icons/si';
11 | import { BsGlobe } from 'react-icons/bs';
12 | import { HStack, Icon } from '@chakra-ui/react';
13 | import Platform from '../entities/Platform';
14 | import { IconType } from 'react-icons';
15 |
16 | interface Props {
17 | platforms: Platform[];
18 | }
19 |
20 | const PlatformIconList = ({ platforms = [] }: Props) => {
21 | const iconMap: { [key: string]: IconType } = {
22 | pc: FaWindows,
23 | playstation: FaPlaystation,
24 | xbox: FaXbox,
25 | nintendo: SiNintendo,
26 | mac: FaApple,
27 | linux: FaLinux,
28 | android: FaAndroid,
29 | ios: MdPhoneIphone,
30 | web: BsGlobe,
31 | };
32 |
33 | return (
34 |
35 | {platforms.map((platform) => (
36 |
41 | ))}
42 |
43 | );
44 | };
45 |
46 | export default PlatformIconList;
47 |
--------------------------------------------------------------------------------
/src/components/PlatformSelector.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Menu,
4 | MenuButton,
5 | MenuItem,
6 | MenuList
7 | } from '@chakra-ui/react';
8 | import { BsChevronDown } from 'react-icons/bs';
9 | import usePlatform from '../hooks/usePlatform';
10 | import usePlatforms from '../hooks/usePlatforms';
11 | import useGameQueryStore from '../store';
12 |
13 | const PlatformSelector = () => {
14 | const { data, error } = usePlatforms();
15 | const setSelectedPlatformId = useGameQueryStore(s => s.setPlatformId);
16 | const selectedPlatformId = useGameQueryStore(s => s.gameQuery.platformId);
17 | const selectedPlatform = usePlatform(selectedPlatformId);
18 |
19 | if (error) return null;
20 |
21 | return (
22 |
37 | );
38 | };
39 |
40 | export default PlatformSelector;
41 |
--------------------------------------------------------------------------------
/src/pages/GameDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | GridItem,
3 | Heading,
4 | SimpleGrid,
5 | Spinner,
6 | Text,
7 | } from '@chakra-ui/react';
8 | import React from 'react';
9 | import { useParams } from 'react-router-dom';
10 | import CriticScore from '../components/CriticScore';
11 | import DefinitionItem from '../components/DefinitionItem';
12 | import ExpandableText from '../components/ExpandableText';
13 | import GameAttributes from '../components/GameAttributes';
14 | import GameScreenshots from '../components/GameScreenshots';
15 | import GameTrailer from '../components/GameTrailer';
16 | import useGame from '../hooks/useGame';
17 |
18 | const GameDetailPage = () => {
19 | const { slug } = useParams();
20 | const { data: game, isLoading, error } = useGame(slug!);
21 |
22 | if (isLoading) return ;
23 |
24 | if (error || !game) throw error;
25 |
26 | return (
27 |
28 |
29 | {game.name}
30 | {game.description_raw}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default GameDetailPage;
42 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/SortSelector.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Menu,
4 | MenuButton,
5 | MenuItem,
6 | MenuList,
7 | } from '@chakra-ui/react';
8 | import { BsChevronDown } from 'react-icons/bs';
9 | import useGameQueryStore from '../store';
10 |
11 | const SortSelector = () => {
12 | const sortOrders = [
13 | { value: '', label: 'Relevance' },
14 | { value: '-added', label: 'Date added' },
15 | { value: 'name', label: 'Name' },
16 | { value: '-released', label: 'Release date' },
17 | { value: '-metacritic', label: 'Popularity' },
18 | { value: '-rating', label: 'Average rating' },
19 | ];
20 |
21 | const setSortOrder = useGameQueryStore((s) => s.setSortOrder);
22 | const sortOrder = useGameQueryStore(
23 | (s) => s.gameQuery.sortOrder
24 | );
25 | const currentSortOrder = sortOrders.find(
26 | (order) => order.value === sortOrder
27 | );
28 |
29 | return (
30 |
46 | );
47 | };
48 |
49 | export default SortSelector;
50 |
--------------------------------------------------------------------------------
/src/components/GenreList.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Heading,
4 | HStack,
5 | Image,
6 | List,
7 | ListItem,
8 | Spinner
9 | } from '@chakra-ui/react';
10 | import useGenres from '../hooks/useGenres';
11 | import getCroppedImageUrl from '../services/image-url';
12 | import useGameQueryStore from '../store';
13 |
14 | const GenreList = () => {
15 | const { data, isLoading, error } = useGenres();
16 | const selectedGenreId = useGameQueryStore(s => s.gameQuery.genreId);
17 | const setSelectedGenreId = useGameQueryStore(s => s.setGenreId);
18 |
19 | if (error) return null;
20 |
21 | if (isLoading) return ;
22 |
23 | return (
24 | <>
25 |
26 | Genres
27 |
28 |
29 | {data?.results.map((genre) => (
30 |
31 |
32 |
38 |
52 |
53 |
54 | ))}
55 |
56 | >
57 | );
58 | };
59 |
60 | export default GenreList;
61 |
--------------------------------------------------------------------------------
/src/components/GameGrid.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | SimpleGrid,
3 | Spinner,
4 | Text
5 | } from '@chakra-ui/react';
6 | import React from 'react';
7 | import InfiniteScroll from 'react-infinite-scroll-component';
8 | import useGames from '../hooks/useGames';
9 | import GameCard from './GameCard';
10 | import GameCardContainer from './GameCardContainer';
11 | import GameCardSkeleton from './GameCardSkeleton';
12 |
13 | const GameGrid = () => {
14 | const {
15 | data,
16 | error,
17 | isLoading,
18 | isFetchingNextPage,
19 | fetchNextPage,
20 | hasNextPage,
21 | } = useGames();
22 | const skeletons = [1, 2, 3, 4, 5, 6];
23 |
24 | if (error) return {error.message};
25 |
26 | const fetchedGamesCount =
27 | data?.pages.reduce(
28 | (total, page) => total + page.results.length,
29 | 0
30 | ) || 0;
31 |
32 | return (
33 | fetchNextPage()}
37 | loader={}
38 | >
39 |
44 | {isLoading &&
45 | skeletons.map((skeleton) => (
46 |
47 |
48 |
49 | ))}
50 | {data?.pages.map((page, index) => (
51 |
52 | {page.results.map((game) => (
53 |
54 |
55 |
56 | ))}
57 |
58 | ))}
59 |
60 |
61 | );
62 | };
63 |
64 | export default GameGrid;
65 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/data/platforms.ts:
--------------------------------------------------------------------------------
1 | export default {"count":14,"next":null,"previous":null,"results":[{"id":1,"name":"PC","slug":"pc","platforms":[{"id":4,"name":"PC","slug":"pc","games_count":540846,"image_background":"https://media.rawg.io/media/games/456/456dea5e1c7e3cd07060c14e96612001.jpg","image":null,"year_start":null,"year_end":null}]},{"id":2,"name":"PlayStation","slug":"playstation","platforms":[{"id":187,"name":"PlayStation 5","slug":"playstation5","games_count":828,"image_background":"https://media.rawg.io/media/games/559/559bc0768f656ad0c63c54b80a82d680.jpg","image":null,"year_start":2020,"year_end":null},{"id":18,"name":"PlayStation 4","slug":"playstation4","games_count":6598,"image_background":"https://media.rawg.io/media/games/16b/16b1b7b36e2042d1128d5a3e852b3b2f.jpg","image":null,"year_start":null,"year_end":null},{"id":16,"name":"PlayStation 3","slug":"playstation3","games_count":3257,"image_background":"https://media.rawg.io/media/games/bc0/bc06a29ceac58652b684deefe7d56099.jpg","image":null,"year_start":null,"year_end":null},{"id":15,"name":"PlayStation 2","slug":"playstation2","games_count":1975,"image_background":"https://media.rawg.io/media/games/13a/13a528ac9cf48bbb6be5d35fe029336d.jpg","image":null,"year_start":null,"year_end":null},{"id":27,"name":"PlayStation","slug":"playstation1","games_count":1617,"image_background":"https://media.rawg.io/media/games/826/82626e2d7ee7d96656fb9838c2ef7302.jpg","image":null,"year_start":null,"year_end":null},{"id":19,"name":"PS Vita","slug":"ps-vita","games_count":1570,"image_background":"https://media.rawg.io/media/games/c47/c4796c4c49e7e06ad328e07aa8944cdd.jpg","image":null,"year_start":null,"year_end":null},{"id":17,"name":"PSP","slug":"psp","games_count":1381,"image_background":"https://media.rawg.io/media/games/dd7/dd72d8a527cd9245c7eb7cd05aa53efa.jpg","image":null,"year_start":null,"year_end":null}]},{"id":3,"name":"Xbox","slug":"xbox","platforms":[{"id":1,"name":"Xbox One","slug":"xbox-one","games_count":5490,"image_background":"https://media.rawg.io/media/games/562/562553814dd54e001a541e4ee83a591c.jpg","image":null,"year_start":null,"year_end":null},{"id":186,"name":"Xbox Series S/X","slug":"xbox-series-x","games_count":733,"image_background":"https://media.rawg.io/media/games/d47/d479582ed0a46496ad34f65c7099d7e5.jpg","image":null,"year_start":2020,"year_end":null},{"id":14,"name":"Xbox 360","slug":"xbox360","games_count":2777,"image_background":"https://media.rawg.io/media/games/736/73619bd336c894d6941d926bfd563946.jpg","image":null,"year_start":null,"year_end":null},{"id":80,"name":"Xbox","slug":"xbox-old","games_count":722,"image_background":"https://media.rawg.io/media/games/bc7/bc77b1eb8e35df2d90b952bac5342c75.jpg","image":null,"year_start":null,"year_end":null}]},{"id":4,"name":"iOS","slug":"ios","platforms":[{"id":3,"name":"iOS","slug":"ios","games_count":76566,"image_background":"https://media.rawg.io/media/games/d4b/d4bcd78873edd9992d93aff9cc8db0c8.jpg","image":null,"year_start":null,"year_end":null}]},{"id":8,"name":"Android","slug":"android","platforms":[{"id":21,"name":"Android","slug":"android","games_count":55432,"image_background":"https://media.rawg.io/media/games/6e0/6e0c19bb111bd4fa20cf0eb72a049519.jpg","image":null,"year_start":null,"year_end":null}]},{"id":5,"name":"Apple Macintosh","slug":"mac","platforms":[{"id":5,"name":"macOS","slug":"macos","games_count":107094,"image_background":"https://media.rawg.io/media/games/6cd/6cd653e0aaef5ff8bbd295bf4bcb12eb.jpg","image":null,"year_start":null,"year_end":null},{"id":55,"name":"Classic Macintosh","slug":"macintosh","games_count":676,"image_background":"https://media.rawg.io/media/games/104/104e82a52297cc7ddd3b05b7e68be04f.jpg","image":null,"year_start":null,"year_end":null},{"id":41,"name":"Apple II","slug":"apple-ii","games_count":422,"image_background":"https://media.rawg.io/media/screenshots/53d/53d57cbd135807d5b9945b3076bd2c9a.jpg","image":null,"year_start":null,"year_end":null}]},{"id":6,"name":"Linux","slug":"linux","platforms":[{"id":6,"name":"Linux","slug":"linux","games_count":79725,"image_background":"https://media.rawg.io/media/games/9dd/9ddabb34840ea9227556670606cf8ea3.jpg","image":null,"year_start":null,"year_end":null}]},{"id":7,"name":"Nintendo","slug":"nintendo","platforms":[{"id":7,"name":"Nintendo Switch","slug":"nintendo-switch","games_count":5205,"image_background":"https://media.rawg.io/media/games/be0/be01c3d7d8795a45615da139322ca080.jpg","image":null,"year_start":null,"year_end":null},{"id":8,"name":"Nintendo 3DS","slug":"nintendo-3ds","games_count":1724,"image_background":"https://media.rawg.io/media/games/be5/be51faf9bec778b4ea1b06e9b084792c.jpg","image":null,"year_start":null,"year_end":null},{"id":9,"name":"Nintendo DS","slug":"nintendo-ds","games_count":2474,"image_background":"https://media.rawg.io/media/games/fc0/fc076b974197660a582abd34ebccc27f.jpg","image":null,"year_start":null,"year_end":null},{"id":13,"name":"Nintendo DSi","slug":"nintendo-dsi","games_count":37,"image_background":"https://media.rawg.io/media/screenshots/b45/b452e9b20e969a64d0088ae467d1dcab.jpg","image":null,"year_start":null,"year_end":null},{"id":10,"name":"Wii U","slug":"wii-u","games_count":1201,"image_background":"https://media.rawg.io/media/games/b5a/b5a1226bfd971284a735a4a0969086b3.jpg","image":null,"year_start":null,"year_end":null},{"id":11,"name":"Wii","slug":"wii","games_count":2271,"image_background":"https://media.rawg.io/media/games/dd7/dd72d8a527cd9245c7eb7cd05aa53efa.jpg","image":null,"year_start":null,"year_end":null},{"id":105,"name":"GameCube","slug":"gamecube","games_count":641,"image_background":"https://media.rawg.io/media/screenshots/002/002c8c5f9eb52d936bbc02f4c943a8c0.jpeg","image":null,"year_start":null,"year_end":null},{"id":83,"name":"Nintendo 64","slug":"nintendo-64","games_count":363,"image_background":"https://media.rawg.io/media/screenshots/6ca/6ca508f924da59e8cab1610b30117b56.jpeg","image":null,"year_start":null,"year_end":null},{"id":24,"name":"Game Boy Advance","slug":"game-boy-advance","games_count":957,"image_background":"https://media.rawg.io/media/games/3c8/3c872330c4e9966a5a06c1371525e760.jpg","image":null,"year_start":null,"year_end":null},{"id":43,"name":"Game Boy Color","slug":"game-boy-color","games_count":412,"image_background":"https://media.rawg.io/media/screenshots/a51/a519f93600f1427375260522f47e2e7b.jpg","image":null,"year_start":null,"year_end":null},{"id":26,"name":"Game Boy","slug":"game-boy","games_count":604,"image_background":"https://media.rawg.io/media/games/e40/e4043e92866d08ec9fdd212dcd3a1224.jpg","image":null,"year_start":null,"year_end":null},{"id":79,"name":"SNES","slug":"snes","games_count":944,"image_background":"https://media.rawg.io/media/games/bb1/bb182c89b9d4e19b41e5d0f81caa0f42.jpg","image":null,"year_start":null,"year_end":null},{"id":49,"name":"NES","slug":"nes","games_count":969,"image_background":"https://media.rawg.io/media/games/a75/a75e4cb9742bb172d6bd3deb4cc4109e.jpg","image":null,"year_start":null,"year_end":null}]},{"id":9,"name":"Atari","slug":"atari","platforms":[{"id":28,"name":"Atari 7800","slug":"atari-7800","games_count":64,"image_background":"https://media.rawg.io/media/screenshots/565/56504b28b184dbc630a7de118e39d822.jpg","image":null,"year_start":null,"year_end":null},{"id":31,"name":"Atari 5200","slug":"atari-5200","games_count":64,"image_background":"https://media.rawg.io/media/screenshots/61a/61a60e3ee55941387681eaa59e3becbf.jpg","image":null,"year_start":null,"year_end":null},{"id":23,"name":"Atari 2600","slug":"atari-2600","games_count":286,"image_background":"https://media.rawg.io/media/screenshots/b12/b12ed274eed80e4aced37badf228d1cf.jpg","image":null,"year_start":null,"year_end":null},{"id":22,"name":"Atari Flashback","slug":"atari-flashback","games_count":30,"image_background":"https://media.rawg.io/media/screenshots/2aa/2aa07f58491e14b0183333f8956bc802.jpg","image":null,"year_start":null,"year_end":null},{"id":25,"name":"Atari 8-bit","slug":"atari-8-bit","games_count":306,"image_background":"https://media.rawg.io/media/games/876/8764efc52fba503a00af64a2cd51f66c.jpg","image":null,"year_start":null,"year_end":null},{"id":34,"name":"Atari ST","slug":"atari-st","games_count":834,"image_background":"https://media.rawg.io/media/screenshots/8dd/8dd23d8d30d988035a06a8f8c462f135.jpg","image":null,"year_start":null,"year_end":null},{"id":46,"name":"Atari Lynx","slug":"atari-lynx","games_count":56,"image_background":"https://media.rawg.io/media/screenshots/575/575b2838392ed177dd7d2c734c682f93.jpg","image":null,"year_start":null,"year_end":null},{"id":50,"name":"Atari XEGS","slug":"atari-xegs","games_count":22,"image_background":"https://media.rawg.io/media/screenshots/769/7691726d70c23c029903df08858df001.jpg","image":null,"year_start":null,"year_end":null},{"id":112,"name":"Jaguar","slug":"jaguar","games_count":37,"image_background":"https://media.rawg.io/media/games/855/8552687245f888ba388bc6ec0dcc3947.jpg","image":null,"year_start":null,"year_end":null}]},{"id":10,"name":"Commodore / Amiga","slug":"commodore-amiga","platforms":[{"id":166,"name":"Commodore / Amiga","slug":"commodore-amiga","games_count":2075,"image_background":"https://media.rawg.io/media/screenshots/f6b/f6b3338889ec877c9d3d89fc4f665152.jpg","image":null,"year_start":null,"year_end":null}]},{"id":11,"name":"SEGA","slug":"sega","platforms":[{"id":167,"name":"Genesis","slug":"genesis","games_count":824,"image_background":"https://media.rawg.io/media/games/373/373a9a1f664de6e4c31f08644729e2db.jpg","image":null,"year_start":null,"year_end":null},{"id":107,"name":"SEGA Saturn","slug":"sega-saturn","games_count":347,"image_background":"https://media.rawg.io/media/games/7ca/7ca0df41799243443a4e3887720fdf2a.jpg","image":null,"year_start":null,"year_end":null},{"id":119,"name":"SEGA CD","slug":"sega-cd","games_count":161,"image_background":"https://media.rawg.io/media/screenshots/b45/b452e9b20e969a64d0088ae467d1dcab.jpg","image":null,"year_start":null,"year_end":null},{"id":117,"name":"SEGA 32X","slug":"sega-32x","games_count":47,"image_background":"https://media.rawg.io/media/games/0df/0dfe8852fa43d58cbdeb973765a9828d.jpg","image":null,"year_start":null,"year_end":null},{"id":74,"name":"SEGA Master System","slug":"sega-master-system","games_count":224,"image_background":"https://media.rawg.io/media/screenshots/0e3/0e3fd5a5436b82095044e5a5f8a20d51.jpg","image":null,"year_start":null,"year_end":null},{"id":106,"name":"Dreamcast","slug":"dreamcast","games_count":353,"image_background":"https://media.rawg.io/media/games/96a/96a48ac7487d9db9179d83170afcb16a.jpg","image":null,"year_start":null,"year_end":null},{"id":77,"name":"Game Gear","slug":"game-gear","games_count":217,"image_background":"https://media.rawg.io/media/games/2c3/2c3363eb1ae202b9e4e7520d3f14ab2e.jpg","image":null,"year_start":null,"year_end":null}]},{"id":12,"name":"3DO","slug":"3do","platforms":[{"id":111,"name":"3DO","slug":"3do","games_count":95,"image_background":"https://media.rawg.io/media/screenshots/180/180b5f6e5d8c770bbbf941b9875046b6.jpg","image":null,"year_start":null,"year_end":null}]},{"id":13,"name":"Neo Geo","slug":"neo-geo","platforms":[{"id":12,"name":"Neo Geo","slug":"neogeo","games_count":115,"image_background":"https://media.rawg.io/media/games/5c9/5c9d21b8de81ec72c1717a52d2951319.jpg","image":null,"year_start":null,"year_end":null}]},{"id":14,"name":"Web","slug":"web","platforms":[{"id":171,"name":"Web","slug":"web","games_count":273085,"image_background":"https://media.rawg.io/media/screenshots/6aa/6aa8cfccfa7f8d7acbe1a6e24dfb45dd.jpeg","image":null,"year_start":null,"year_end":null}]}]}
--------------------------------------------------------------------------------
/src/data/genres.ts:
--------------------------------------------------------------------------------
1 | export default {"count":19,"next":null,"previous":null,"results":[{"id":4,"name":"Action","slug":"action","games_count":179051,"image_background":"https://media.rawg.io/media/games/b8c/b8c243eaa0fbac8115e0cdccac3f91dc.jpg","games":[{"id":3498,"slug":"grand-theft-auto-v","name":"Grand Theft Auto V","added":19107},{"id":3328,"slug":"the-witcher-3-wild-hunt","name":"The Witcher 3: Wild Hunt","added":18264},{"id":5286,"slug":"tomb-raider","name":"Tomb Raider (2013)","added":15070},{"id":4291,"slug":"counter-strike-global-offensive","name":"Counter-Strike: Global Offensive","added":14884},{"id":12020,"slug":"left-4-dead-2","name":"Left 4 Dead 2","added":14585},{"id":5679,"slug":"the-elder-scrolls-v-skyrim","name":"The Elder Scrolls V: Skyrim","added":14481}]},{"id":51,"name":"Indie","slug":"indie","games_count":51548,"image_background":"https://media.rawg.io/media/games/713/713269608dc8f2f40f5a670a14b2de94.jpg","games":[{"id":1030,"slug":"limbo","name":"Limbo","added":12348},{"id":3272,"slug":"rocket-league","name":"Rocket League","added":11250},{"id":422,"slug":"terraria","name":"Terraria","added":11056},{"id":9767,"slug":"hollow-knight","name":"Hollow Knight","added":9622},{"id":3612,"slug":"hotline-miami","name":"Hotline Miami","added":9420},{"id":3790,"slug":"outlast","name":"Outlast","added":9353}]},{"id":3,"name":"Adventure","slug":"adventure","games_count":137837,"image_background":"https://media.rawg.io/media/games/b7d/b7d3f1715fa8381a4e780173a197a615.jpg","games":[{"id":3498,"slug":"grand-theft-auto-v","name":"Grand Theft Auto V","added":19107},{"id":3328,"slug":"the-witcher-3-wild-hunt","name":"The Witcher 3: Wild Hunt","added":18264},{"id":5286,"slug":"tomb-raider","name":"Tomb Raider (2013)","added":15070},{"id":13536,"slug":"portal","name":"Portal","added":14617},{"id":28,"slug":"red-dead-redemption-2","name":"Red Dead Redemption 2","added":13838},{"id":3439,"slug":"life-is-strange-episode-1-2","name":"Life is Strange","added":13815}]},{"id":5,"name":"RPG","slug":"role-playing-games-rpg","games_count":54150,"image_background":"https://media.rawg.io/media/games/da1/da1b267764d77221f07a4386b6548e5a.jpg","games":[{"id":3328,"slug":"the-witcher-3-wild-hunt","name":"The Witcher 3: Wild Hunt","added":18264},{"id":5679,"slug":"the-elder-scrolls-v-skyrim","name":"The Elder Scrolls V: Skyrim","added":14481},{"id":802,"slug":"borderlands-2","name":"Borderlands 2","added":13802},{"id":58175,"slug":"god-of-war-2","name":"God of War (2018)","added":12155},{"id":3070,"slug":"fallout-4","name":"Fallout 4","added":12138},{"id":278,"slug":"horizon-zero-dawn","name":"Horizon Zero Dawn","added":11592}]},{"id":10,"name":"Strategy","slug":"strategy","games_count":53888,"image_background":"https://media.rawg.io/media/games/a88/a886c37bf112d009e318b106db9d420a.jpg","games":[{"id":13633,"slug":"civilization-v","name":"Sid Meier's Civilization V","added":8539},{"id":10243,"slug":"company-of-heroes-2","name":"Company of Heroes 2","added":8424},{"id":13910,"slug":"xcom-enemy-unknown","name":"XCOM: Enemy Unknown","added":7602},{"id":5525,"slug":"brutal-legend","name":"Brutal Legend","added":7530},{"id":10065,"slug":"cities-skylines","name":"Cities: Skylines","added":7362},{"id":11147,"slug":"ark-survival-of-the-fittest","name":"ARK: Survival Of The Fittest","added":7037}]},{"id":2,"name":"Shooter","slug":"shooter","games_count":63421,"image_background":"https://media.rawg.io/media/games/120/1201a40e4364557b124392ee50317b99.jpg","games":[{"id":4200,"slug":"portal-2","name":"Portal 2","added":17209},{"id":4291,"slug":"counter-strike-global-offensive","name":"Counter-Strike: Global Offensive","added":14884},{"id":12020,"slug":"left-4-dead-2","name":"Left 4 Dead 2","added":14585},{"id":4062,"slug":"bioshock-infinite","name":"BioShock Infinite","added":13977},{"id":802,"slug":"borderlands-2","name":"Borderlands 2","added":13802},{"id":13537,"slug":"half-life-2","name":"Half-Life 2","added":13060}]},{"id":40,"name":"Casual","slug":"casual","games_count":43757,"image_background":"https://media.rawg.io/media/screenshots/054/054bf49d9e736edfda5aa8e9015faf9b.jpeg","games":[{"id":9721,"slug":"garrys-mod","name":"Garry's Mod","added":8614},{"id":326292,"slug":"fall-guys","name":"Fall Guys: Ultimate Knockout","added":7606},{"id":9830,"slug":"brawlhalla","name":"Brawlhalla","added":6529},{"id":356714,"slug":"among-us","name":"Among Us","added":6204},{"id":1959,"slug":"goat-simulator","name":"Goat Simulator","added":5735},{"id":16343,"slug":"a-story-about-my-uncle","name":"A Story About My Uncle","added":5404}]},{"id":14,"name":"Simulation","slug":"simulation","games_count":67385,"image_background":"https://media.rawg.io/media/games/179/179245a3693049a11a25b900ab18f8f7.jpg","games":[{"id":10035,"slug":"hitman","name":"Hitman","added":9698},{"id":654,"slug":"stardew-valley","name":"Stardew Valley","added":8708},{"id":9721,"slug":"garrys-mod","name":"Garry's Mod","added":8614},{"id":10243,"slug":"company-of-heroes-2","name":"Company of Heroes 2","added":8424},{"id":9882,"slug":"dont-starve-together","name":"Don't Starve Together","added":8058},{"id":22509,"slug":"minecraft","name":"Minecraft","added":7409}]},{"id":7,"name":"Puzzle","slug":"puzzle","games_count":100740,"image_background":"https://media.rawg.io/media/games/328/3283617cb7d75d67257fc58339188742.jpg","games":[{"id":4200,"slug":"portal-2","name":"Portal 2","added":17209},{"id":13536,"slug":"portal","name":"Portal","added":14617},{"id":1030,"slug":"limbo","name":"Limbo","added":12348},{"id":19709,"slug":"half-life-2-episode-two","name":"Half-Life 2: Episode Two","added":9743},{"id":18080,"slug":"half-life","name":"Half-Life","added":8957},{"id":1450,"slug":"inside","name":"INSIDE","added":7164}]},{"id":11,"name":"Arcade","slug":"arcade","games_count":22544,"image_background":"https://media.rawg.io/media/games/363/36306deef81e7955a5d0f5c3b43fccee.jpg","games":[{"id":3612,"slug":"hotline-miami","name":"Hotline Miami","added":9420},{"id":17540,"slug":"injustice-gods-among-us-ultimate-edition","name":"Injustice: Gods Among Us Ultimate Edition","added":8629},{"id":22509,"slug":"minecraft","name":"Minecraft","added":7409},{"id":4003,"slug":"grid-2","name":"GRID 2","added":6806},{"id":3408,"slug":"hotline-miami-2-wrong-number","name":"Hotline Miami 2: Wrong Number","added":5504},{"id":16343,"slug":"a-story-about-my-uncle","name":"A Story About My Uncle","added":5404}]},{"id":83,"name":"Platformer","slug":"platformer","games_count":106974,"image_background":"https://media.rawg.io/media/games/569/569ea25d2b56bd05c7fa309ddabe81ff.jpg","games":[{"id":1030,"slug":"limbo","name":"Limbo","added":12348},{"id":422,"slug":"terraria","name":"Terraria","added":11056},{"id":9767,"slug":"hollow-knight","name":"Hollow Knight","added":9622},{"id":41,"slug":"little-nightmares","name":"Little Nightmares","added":9509},{"id":18080,"slug":"half-life","name":"Half-Life","added":8957},{"id":3144,"slug":"super-meat-boy","name":"Super Meat Boy","added":8606}]},{"id":1,"name":"Racing","slug":"racing","games_count":24635,"image_background":"https://media.rawg.io/media/games/082/082365507ff04d456c700157072d35db.jpg","games":[{"id":3272,"slug":"rocket-league","name":"Rocket League","added":11250},{"id":4003,"slug":"grid-2","name":"GRID 2","added":6806},{"id":2572,"slug":"dirt-rally","name":"DiRT Rally","added":6096},{"id":58753,"slug":"forza-horizon-4","name":"Forza Horizon 4","added":5428},{"id":5578,"slug":"grid","name":"Race Driver: Grid","added":5001},{"id":4347,"slug":"dirt-showdown","name":"DiRT Showdown","added":4344}]},{"id":59,"name":"Massively Multiplayer","slug":"massively-multiplayer","games_count":3169,"image_background":"https://media.rawg.io/media/games/d0f/d0f91fe1d92332147e5db74e207cfc7a.jpg","games":[{"id":32,"slug":"destiny-2","name":"Destiny 2","added":12150},{"id":10213,"slug":"dota-2","name":"Dota 2","added":11083},{"id":766,"slug":"warframe","name":"Warframe","added":10942},{"id":290856,"slug":"apex-legends","name":"Apex Legends","added":9697},{"id":10533,"slug":"path-of-exile","name":"Path of Exile","added":8742},{"id":10142,"slug":"playerunknowns-battlegrounds","name":"PlayerUnknown’s Battlegrounds","added":8602}]},{"id":15,"name":"Sports","slug":"sports","games_count":20844,"image_background":"https://media.rawg.io/media/games/d16/d160819f22de73d29813f7b6dad815f9.jpg","games":[{"id":3272,"slug":"rocket-league","name":"Rocket League","added":11250},{"id":326292,"slug":"fall-guys","name":"Fall Guys: Ultimate Knockout","added":7606},{"id":2572,"slug":"dirt-rally","name":"DiRT Rally","added":6096},{"id":53341,"slug":"jet-set-radio-2012","name":"Jet Set Radio","added":4755},{"id":9575,"slug":"vrchat","name":"VRChat","added":3936},{"id":622492,"slug":"forza-horizon-5","name":"Forza Horizon 5","added":3923}]},{"id":6,"name":"Fighting","slug":"fighting","games_count":12465,"image_background":"https://media.rawg.io/media/games/aa3/aa36ba4b486a03ddfaef274fb4f5afd4.jpg","games":[{"id":17540,"slug":"injustice-gods-among-us-ultimate-edition","name":"Injustice: Gods Among Us Ultimate Edition","added":8629},{"id":108,"slug":"mortal-kombat-x","name":"Mortal Kombat X","added":7958},{"id":28179,"slug":"sega-mega-drive-and-genesis-classics","name":"SEGA Mega Drive and Genesis Classics","added":7310},{"id":9830,"slug":"brawlhalla","name":"Brawlhalla","added":6529},{"id":274480,"slug":"mortal-kombat-11","name":"Mortal Kombat 11","added":4720},{"id":44525,"slug":"yakuza-kiwami","name":"Yakuza Kiwami","added":4065}]},{"id":19,"name":"Family","slug":"family","games_count":5380,"image_background":"https://media.rawg.io/media/games/0c1/0c1c9965ba59166ab986a663ab2252dc.jpg","games":[{"id":3254,"slug":"journey","name":"Journey","added":7816},{"id":2597,"slug":"lego-lord-of-the-rings","name":"LEGO The Lord of the Rings","added":4974},{"id":3350,"slug":"broken-age","name":"Broken Age","added":4646},{"id":3729,"slug":"lego-the-hobbit","name":"LEGO The Hobbit","added":4573},{"id":1259,"slug":"machinarium","name":"Machinarium","added":4120},{"id":1140,"slug":"world-of-goo","name":"World of Goo","added":4059}]},{"id":28,"name":"Board Games","slug":"board-games","games_count":8284,"image_background":"https://media.rawg.io/media/screenshots/f64/f6470e82e699c6f4d6e151ecaf13e256.jpg","games":[{"id":23557,"slug":"gwent-the-witcher-card-game","name":"Gwent: The Witcher Card Game","added":4252},{"id":327999,"slug":"dota-underlords","name":"Dota Underlords","added":3589},{"id":2055,"slug":"adventure-capitalist","name":"AdVenture Capitalist","added":2997},{"id":2306,"slug":"poker-night-2","name":"Poker Night 2","added":1921},{"id":3187,"slug":"armello","name":"Armello","added":1814},{"id":758,"slug":"hue","name":"Hue","added":1755}]},{"id":34,"name":"Educational","slug":"educational","games_count":16381,"image_background":"https://media.rawg.io/media/screenshots/9d4/9d45ba1c76712ad692fadda67f2777a9.jpeg","games":[{"id":1358,"slug":"papers-please","name":"Papers, Please","added":6106},{"id":1140,"slug":"world-of-goo","name":"World of Goo","added":4059},{"id":2778,"slug":"surgeon-simulator-cpr","name":"Surgeon Simulator","added":3567},{"id":9768,"slug":"gameguru","name":"GameGuru","added":2278},{"id":13777,"slug":"sid-meiers-civilization-iv-colonization","name":"Sid Meier's Civilization IV: Colonization","added":2117},{"id":6885,"slug":"pirates-3","name":"Sid Meier's Pirates!","added":2016}]},{"id":17,"name":"Card","slug":"card","games_count":4474,"image_background":"https://media.rawg.io/media/screenshots/8ff/8ffe8f19d2e764867c8ed625ddf4e368.jpg","games":[{"id":23557,"slug":"gwent-the-witcher-card-game","name":"Gwent: The Witcher Card Game","added":4252},{"id":28121,"slug":"slay-the-spire","name":"Slay the Spire","added":4228},{"id":18852,"slug":"poker-night-at-the-inventory","name":"Poker Night at the Inventory","added":2549},{"id":8923,"slug":"faeria","name":"Faeria","added":2011},{"id":332,"slug":"the-elder-scrolls-legends","name":"The Elder Scrolls: Legends","added":1946},{"id":2306,"slug":"poker-night-2","name":"Poker Night 2","added":1921}]}]}
--------------------------------------------------------------------------------