Full Code of mosh-hamedani/game-hub for AI

main 716dc259ca61 cached
35 files
54.1 KB
17.6k tokens
19 symbols
1 requests
Download .txt
Repository: mosh-hamedani/game-hub
Branch: main
Commit: 716dc259ca61
Files: 35
Total size: 54.1 KB

Directory structure:
gitextract_acqhe0vy/

├── .gitignore
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── components/
│   │   ├── ColorModeSwitch.tsx
│   │   ├── CriticScore.tsx
│   │   ├── Emoji.tsx
│   │   ├── GameCard.tsx
│   │   ├── GameCardContainer.tsx
│   │   ├── GameCardSkeleton.tsx
│   │   ├── GameGrid.tsx
│   │   ├── GameHeading.tsx
│   │   ├── GenreList.tsx
│   │   ├── NavBar.tsx
│   │   ├── PlatformIconList.tsx
│   │   ├── PlatformSelector.tsx
│   │   ├── SearchInput.tsx
│   │   └── SortSelector.tsx
│   ├── data/
│   │   ├── genres.ts
│   │   └── platforms.ts
│   ├── hooks/
│   │   ├── useData.ts
│   │   ├── useGames.ts
│   │   ├── useGenres.ts
│   │   └── usePlatforms.ts
│   ├── index.css
│   ├── main.tsx
│   ├── services/
│   │   ├── api-client.ts
│   │   └── image-url.ts
│   ├── theme.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vercel


================================================
FILE: README.md
================================================
# GameHub

GameHub is a video game discovery web app that helps you find new and interesting games to play. With GameHub, you can search for games by platform, genre, and more. 

This is the project we build in my Ultimate React course. You can find the full course at: https://codewithmosh.com 

## Getting Started

To get started with GameHub, follow these steps:


1. Clone this repository to your local machine.
2. Run `npm install` to install the required dependencies.
3. Get a RAWG API key at https://rawg.io/apidocs. You'll have to create an account first. 
4. Add the API key to **src/services/api-client.ts**
5. Run `npm run dev` to start the web server. 

## About the Course 

I have designed this course to teach you everything you need to know to become a proficient React developer. This course is the first part of a two-part series, covering the fundamentals. You'll learn how to:

- Build front-end apps with React and TypeScript
- Build reusable function components
- Style your components using vanilla CSS, CSS modules, and CSS-in-JS
- Manage component state
- Build forms with React Hook Forms
- Implement form validation using Zod
- Connect your React apps to the backend
- Deploy your React apps
- Use VSCode shortcuts to increase your productivity
- Write clean code like a pro
- Apply best practices


By the end of this course, you'll have a solid understanding of React and be able to build real-world applications with React and TypeScript.

You can find the full course at: https://codewithmosh.com 


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>GameHub</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "game-hub",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@chakra-ui/react": "^2.5.1",
    "@emotion/react": "^11.10.6",
    "@emotion/styled": "^11.10.6",
    "axios": "^1.3.4",
    "framer-motion": "^10.0.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.7.1"
  },
  "devDependencies": {
    "@types/react": "^18.0.27",
    "@types/react-dom": "^18.0.10",
    "@vitejs/plugin-react": "^3.1.0",
    "typescript": "^4.9.3",
    "vite": "^4.1.0"
  }
}


================================================
FILE: src/App.css
================================================
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}


================================================
FILE: src/App.tsx
================================================
import { Box, Flex, Grid, GridItem, HStack, Show } from "@chakra-ui/react";
import { useState } from "react";
import GameGrid from "./components/GameGrid";
import GameHeading from "./components/GameHeading";
import GenreList from "./components/GenreList";
import NavBar from "./components/NavBar";
import PlatformSelector from "./components/PlatformSelector";
import SortSelector from "./components/SortSelector";
import { Platform } from "./hooks/useGames";
import { Genre } from "./hooks/useGenres";


export interface GameQuery { 
  genre: Genre | null;
  platform: Platform | null;
  sortOrder: string;
  searchText: string;
}

function App() {
  const [gameQuery, setGameQuery] = useState<GameQuery>({} as GameQuery);
  
  return (
    <Grid
      templateAreas={{
        base: `"nav" "main"`,
        lg: `"nav nav" "aside main"`,
      }}
      templateColumns={{
        base: '1fr',
        lg: '250px 1fr'
      }}
    >
      <GridItem area="nav">
        <NavBar onSearch={(searchText) => setGameQuery({ ...gameQuery, searchText })} />
      </GridItem>
      <Show above="lg">
        <GridItem area="aside" paddingX={5}>
          <GenreList selectedGenre={gameQuery.genre} onSelectGenre={(genre) => setGameQuery({ ...gameQuery, genre})} />
        </GridItem>
      </Show>
      <GridItem area="main">
        <Box paddingLeft={2}>
          <GameHeading gameQuery={gameQuery} />
          <Flex marginBottom={5}>
            <Box marginRight={5}>
              <PlatformSelector selectedPlatform={gameQuery.platform} onSelectPlatform={(platform) => setGameQuery({ ...gameQuery, platform}) } />
            </Box>
            <SortSelector sortOrder={gameQuery.sortOrder} onSelectSortOrder={(sortOrder) => setGameQuery({ ...gameQuery, sortOrder })} />
          </Flex>
        </Box>
        <GameGrid gameQuery={gameQuery} />
      </GridItem>
    </Grid>
  );
}

export default App;


================================================
FILE: src/components/ColorModeSwitch.tsx
================================================
import { HStack, Switch, Text, useColorMode } from '@chakra-ui/react'

const ColorModeSwitch = () => {
  const {toggleColorMode, colorMode} = useColorMode();

  return (
    <HStack>
      <Switch colorScheme='green' isChecked={colorMode === 'dark'} onChange={toggleColorMode} />
      <Text whiteSpace='nowrap'>Dark Mode</Text>
    </HStack>
  )
}

export default ColorModeSwitch

================================================
FILE: src/components/CriticScore.tsx
================================================
import { Badge } from '@chakra-ui/react';

interface Props { 
  score: number;
}

const CriticScore = ({ score }: Props) => {
  let color = score > 75 ? 'green' : score > 60 ? 'yellow' : '';

  return (
    <Badge colorScheme={color} fontSize='14px' paddingX={2} borderRadius='4px'>{score}</Badge>
  )
}

export default CriticScore

================================================
FILE: src/components/Emoji.tsx
================================================
import bullsEye from '../assets/bulls-eye.webp';
import thumbsUp from '../assets/thumbs-up.webp';
import meh from '../assets/meh.webp';
import { Image, ImageProps } from '@chakra-ui/react';

interface Props {
  rating: number;
}

const Emoji = ({ rating }: Props) => {
  if (rating < 3) return null;

  const emojiMap: { [key: number]: ImageProps } = {
    3: { src: meh, alt: 'meh', boxSize: '25px' },
    4: { src: thumbsUp, alt: 'recommended', boxSize: '25px' },
    5: { src: bullsEye, alt: 'exceptional', boxSize: '35px' },
  }

  return (
    <Image {...emojiMap[rating]} marginTop={1} />
  )
}

export default Emoji

================================================
FILE: src/components/GameCard.tsx
================================================
import { Card, CardBody, Heading, HStack, Image, Text } from '@chakra-ui/react'
import React from 'react'
import { Game } from '../hooks/useGames'
import getCroppedImageUrl from '../services/image-url'
import CriticScore from './CriticScore'
import Emoji from './Emoji'
import PlatformIconList from './PlatformIconList'

interface Props {
  game: Game
}

const GameCard = ({ game }: Props) => {
  return (
    <Card>
      <Image src={getCroppedImageUrl(game.background_image)} />
      <CardBody>
        <HStack justifyContent='space-between' marginBottom={3}>
          <PlatformIconList platforms={game.parent_platforms?.map(p => p.platform)} />
          <CriticScore score={game.metacritic} />
        </HStack>
        <Heading fontSize='2xl'>{game.name}<Emoji rating={game.rating_top}/></Heading>
      </CardBody>
    </Card>
  )
}

export default GameCard

================================================
FILE: src/components/GameCardContainer.tsx
================================================
import { Box } from "@chakra-ui/react";
import { ReactNode } from "react";

interface Props {
  children: ReactNode;
}

const GameCardContainer = ({ children }: Props) => {
  return (
    <Box borderRadius={10} overflow="hidden">
      {children}
    </Box>
  );
};

export default GameCardContainer;


================================================
FILE: src/components/GameCardSkeleton.tsx
================================================
import { Card, CardBody, Skeleton, SkeletonText } from '@chakra-ui/react'

const GameCardSkeleton = () => {
  return (
    <Card>
      <Skeleton height="200px" />
      <CardBody>
        <SkeletonText />
      </CardBody>
    </Card>
  )
}

export default GameCardSkeleton

================================================
FILE: src/components/GameGrid.tsx
================================================
import { SimpleGrid, Text } from "@chakra-ui/react";
import { GameQuery } from "../App";
import useGames, { Platform } from "../hooks/useGames";
import { Genre } from "../hooks/useGenres";
import GameCard from "./GameCard";
import GameCardContainer from "./GameCardContainer";
import GameCardSkeleton from "./GameCardSkeleton";

interface Props {
  gameQuery: GameQuery;
}

const GameGrid = ({ gameQuery }: Props) => {
  const { data, error, isLoading } = useGames(gameQuery);
  const skeletons = [1, 2, 3, 4, 5, 6];

  if (error) return <Text>{error}</Text>;

  return (
    <SimpleGrid
      columns={{ sm: 1, md: 2, lg: 3, xl: 4 }}
      padding="10px"
      spacing={6}
    >
      {isLoading &&
        skeletons.map((skeleton) => (
          <GameCardContainer key={skeleton}>
            <GameCardSkeleton />
          </GameCardContainer>
        ))}
      {data.map((game) => (
        <GameCardContainer key={game.id}>
          <GameCard game={game} />
        </GameCardContainer>
      ))}
    </SimpleGrid>
  );
};

export default GameGrid;


================================================
FILE: src/components/GameHeading.tsx
================================================
import { Heading } from '@chakra-ui/react'
import { GameQuery } from '../App'

interface Props {
  gameQuery: GameQuery
}

const GameHeading = ({ gameQuery }: Props) => {
  const heading = `${gameQuery.platform?.name || ''} ${gameQuery.genre?.name || ''} Games`;

  return (
    <Heading as='h1' marginY={5} fontSize='5xl'>{heading}</Heading>
  )
}

export default GameHeading

================================================
FILE: src/components/GenreList.tsx
================================================
import {
  Button,
  Heading,
  HStack,
  Image,
  List,
  ListItem,
  Spinner,
  Text,
} from "@chakra-ui/react";
import useGenres, { Genre } from "../hooks/useGenres";
import getCroppedImageUrl from "../services/image-url";

interface Props {
  onSelectGenre: (genre: Genre) => void;
  selectedGenre: Genre | null;
}

const GenreList = ({ selectedGenre, onSelectGenre }: Props) => {
  const { data, isLoading, error } = useGenres();

  if (error) return null;

  if (isLoading) return <Spinner />;

  return (
    <>
      <Heading fontSize="2xl" marginTop={9} marginBottom={3}>
        Genres
      </Heading>
      <List>
        {data.map((genre) => (
          <ListItem key={genre.id} paddingY="5px">
            <HStack>
              <Image
                boxSize="32px"
                borderRadius={8}
                objectFit="cover"
                src={getCroppedImageUrl(genre.image_background)}
              />
              <Button
                whiteSpace="normal"
                textAlign="left"
                fontWeight={genre.id === selectedGenre?.id ? "bold" : "normal"}
                onClick={() => onSelectGenre(genre)}
                fontSize="md"
                variant="link"
              >
                {genre.name}
              </Button>
            </HStack>
          </ListItem>
        ))}
      </List>
    </>
  );
};

export default GenreList;


================================================
FILE: src/components/NavBar.tsx
================================================
import { HStack, Image } from '@chakra-ui/react'
import logo from '../assets/logo.webp';
import ColorModeSwitch from './ColorModeSwitch';
import SearchInput from './SearchInput';

interface Props {
  onSearch: (searchText: string) => void;
}

const NavBar = ({ onSearch }: Props) => {
  return (
    <HStack padding='10px'>
      <Image src={logo} boxSize='60px' />
      <SearchInput onSearch={onSearch} />
      <ColorModeSwitch />
    </HStack>
  )
}

export default NavBar

================================================
FILE: src/components/PlatformIconList.tsx
================================================
import {
  FaWindows,
  FaPlaystation,
  FaXbox,
  FaApple,
  FaLinux,
  FaAndroid,
} from "react-icons/fa";
import { MdPhoneIphone } from 'react-icons/md';
import { SiNintendo } from 'react-icons/si';
import { BsGlobe } from 'react-icons/bs';
import { HStack, Icon } from "@chakra-ui/react";
import { Platform } from "../hooks/useGames";
import { IconType } from "react-icons";

interface Props {
  platforms: Platform[];
}

const PlatformIconList = ({ platforms = [] }: Props) => {
  const iconMap: { [key: string]: IconType } = { 
    pc: FaWindows,
    playstation: FaPlaystation,
    xbox: FaXbox,
    nintendo: SiNintendo,
    mac: FaApple,
    linux: FaLinux, 
    android: FaAndroid,
    ios: MdPhoneIphone,
    web: BsGlobe
  }

  return (
    <HStack marginY={1}> 
      {platforms.map((platform) => (
        <Icon key={platform.id} as={iconMap[platform.slug]} color='gray.500'/>
      ))}
    </HStack>
  );
};

export default PlatformIconList;


================================================
FILE: src/components/PlatformSelector.tsx
================================================
import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { BsChevronDown } from "react-icons/bs";
import { Platform } from "../hooks/useGames";
import usePlatforms from "../hooks/usePlatforms";

interface Props { 
  onSelectPlatform: (platform: Platform) => void;
  selectedPlatform: Platform | null;
}

const PlatformSelector = ({ onSelectPlatform, selectedPlatform }: Props) => {
  const { data, error } = usePlatforms();

  if (error) return null;
  
  return (
    <Menu>
      <MenuButton as={Button} rightIcon={<BsChevronDown />}>
        {selectedPlatform?.name || 'Platforms'}
      </MenuButton>
      <MenuList>
        {data.map(platform => <MenuItem onClick={() => onSelectPlatform(platform)} key={platform.id}>{platform.name}</MenuItem>)}
      </MenuList>
    </Menu>
  );
};

export default PlatformSelector;


================================================
FILE: src/components/SearchInput.tsx
================================================
import { Input, InputGroup, InputLeftElement } from "@chakra-ui/react";
import { useRef } from "react";
import { BsSearch } from "react-icons/bs";

interface Props {
  onSearch: (searchText: string) => void;
}

const SearchInput = ({ onSearch }: Props) => {
  const ref = useRef<HTMLInputElement>(null);

  return (
    <form onSubmit={(event) => {
      event.preventDefault();
      if (ref.current) onSearch(ref.current.value);
    }}>
      <InputGroup>
        <InputLeftElement children={<BsSearch />} />
        <Input ref={ref} borderRadius={20} placeholder="Search games..." variant="filled" />
      </InputGroup>
    </form>
  );
};

export default SearchInput;


================================================
FILE: src/components/SortSelector.tsx
================================================
import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { BsChevronDown } from "react-icons/bs";

interface Props {
  onSelectSortOrder: (sortOrder: string) => void;
  sortOrder: string;
}

const SortSelector = ({ onSelectSortOrder, sortOrder }: Props) => {
  const sortOrders = [
    { value: "", label: "Relevance" },
    { value: "-added", label: "Date added" },
    { value: "name", label: "Name" },
    { value: "-released", label: "Release date" },
    { value: "-metacritic", label: "Popularity" },
    { value: "-rating", label: "Average rating" },
  ];

  const currentSortOrder = sortOrders.find(order => order.value === sortOrder);

  return (
    <Menu>
      <MenuButton as={Button} rightIcon={<BsChevronDown />}>
        Order by: {currentSortOrder?.label || 'Relevance'}
      </MenuButton>
      <MenuList>
        {sortOrders.map((order) => (
          <MenuItem onClick={() => onSelectSortOrder(order.value)} key={order.value} value={order.value}>
            {order.label}
          </MenuItem>
        ))}
      </MenuList>
    </Menu>
  );
};

export default SortSelector;


================================================
FILE: src/data/genres.ts
================================================
export default [
  {
    "id": 4,
    "name": "Action",
    "slug": "action",
    "games_count": 177189,
    "image_background": "https://media.rawg.io/media/games/26d/26d4437715bee60138dab4a7c8c59c92.jpg",
    "games": [
      {
        "id": 3498,
        "slug": "grand-theft-auto-v",
        "name": "Grand Theft Auto V",
        "added": 18995
      },
      {
        "id": 3328,
        "slug": "the-witcher-3-wild-hunt",
        "name": "The Witcher 3: Wild Hunt",
        "added": 18127
      },
      {
        "id": 5286,
        "slug": "tomb-raider",
        "name": "Tomb Raider (2013)",
        "added": 14967
      },
      {
        "id": 4291,
        "slug": "counter-strike-global-offensive",
        "name": "Counter-Strike: Global Offensive",
        "added": 14774
      },
      {
        "id": 12020,
        "slug": "left-4-dead-2",
        "name": "Left 4 Dead 2",
        "added": 14489
      },
      {
        "id": 5679,
        "slug": "the-elder-scrolls-v-skyrim",
        "name": "The Elder Scrolls V: Skyrim",
        "added": 14391
      }
    ]
  },
  {
    "id": 51,
    "name": "Indie",
    "slug": "indie",
    "games_count": 51112,
    "image_background": "https://media.rawg.io/media/games/f46/f466571d536f2e3ea9e815ad17177501.jpg",
    "games": [
      {
        "id": 1030,
        "slug": "limbo",
        "name": "Limbo",
        "added": 12272
      },
      {
        "id": 3272,
        "slug": "rocket-league",
        "name": "Rocket League",
        "added": 11164
      },
      {
        "id": 422,
        "slug": "terraria",
        "name": "Terraria",
        "added": 10975
      },
      {
        "id": 9767,
        "slug": "hollow-knight",
        "name": "Hollow Knight",
        "added": 9542
      },
      {
        "id": 3612,
        "slug": "hotline-miami",
        "name": "Hotline Miami",
        "added": 9376
      },
      {
        "id": 3790,
        "slug": "outlast",
        "name": "Outlast",
        "added": 9296
      }
    ]
  },
  {
    "id": 3,
    "name": "Adventure",
    "slug": "adventure",
    "games_count": 136213,
    "image_background": "https://media.rawg.io/media/games/021/021c4e21a1824d2526f925eff6324653.jpg",
    "games": [
      {
        "id": 3498,
        "slug": "grand-theft-auto-v",
        "name": "Grand Theft Auto V",
        "added": 18995
      },
      {
        "id": 3328,
        "slug": "the-witcher-3-wild-hunt",
        "name": "The Witcher 3: Wild Hunt",
        "added": 18127
      },
      {
        "id": 5286,
        "slug": "tomb-raider",
        "name": "Tomb Raider (2013)",
        "added": 14967
      },
      {
        "id": 13536,
        "slug": "portal",
        "name": "Portal",
        "added": 14517
      },
      {
        "id": 28,
        "slug": "red-dead-redemption-2",
        "name": "Red Dead Redemption 2",
        "added": 13743
      },
      {
        "id": 3439,
        "slug": "life-is-strange-episode-1-2",
        "name": "Life is Strange",
        "added": 13719
      }
    ]
  },
  {
    "id": 5,
    "name": "RPG",
    "slug": "role-playing-games-rpg",
    "games_count": 53575,
    "image_background": "https://media.rawg.io/media/games/f6b/f6bed028b02369d4cab548f4f9337e81.jpg",
    "games": [
      {
        "id": 3328,
        "slug": "the-witcher-3-wild-hunt",
        "name": "The Witcher 3: Wild Hunt",
        "added": 18127
      },
      {
        "id": 5679,
        "slug": "the-elder-scrolls-v-skyrim",
        "name": "The Elder Scrolls V: Skyrim",
        "added": 14391
      },
      {
        "id": 802,
        "slug": "borderlands-2",
        "name": "Borderlands 2",
        "added": 13720
      },
      {
        "id": 58175,
        "slug": "god-of-war-2",
        "name": "God of War (2018)",
        "added": 12068
      },
      {
        "id": 3070,
        "slug": "fallout-4",
        "name": "Fallout 4",
        "added": 12065
      },
      {
        "id": 278,
        "slug": "horizon-zero-dawn",
        "name": "Horizon Zero Dawn",
        "added": 11512
      }
    ]
  },
  {
    "id": 10,
    "name": "Strategy",
    "slug": "strategy",
    "games_count": 53393,
    "image_background": "https://media.rawg.io/media/games/40a/40ab95c1639aa1d7ec04d4cd523af6b1.jpg",
    "games": [
      {
        "id": 13633,
        "slug": "civilization-v",
        "name": "Sid Meier's Civilization V",
        "added": 8490
      },
      {
        "id": 10243,
        "slug": "company-of-heroes-2",
        "name": "Company of Heroes 2",
        "added": 8371
      },
      {
        "id": 13910,
        "slug": "xcom-enemy-unknown",
        "name": "XCOM: Enemy Unknown",
        "added": 7567
      },
      {
        "id": 5525,
        "slug": "brutal-legend",
        "name": "Brutal Legend",
        "added": 7502
      },
      {
        "id": 10065,
        "slug": "cities-skylines",
        "name": "Cities: Skylines",
        "added": 7299
      },
      {
        "id": 11147,
        "slug": "ark-survival-of-the-fittest",
        "name": "ARK: Survival Of The Fittest",
        "added": 6962
      }
    ]
  },
  {
    "id": 2,
    "name": "Shooter",
    "slug": "shooter",
    "games_count": 62697,
    "image_background": "https://media.rawg.io/media/games/120/1201a40e4364557b124392ee50317b99.jpg",
    "games": [
      {
        "id": 4200,
        "slug": "portal-2",
        "name": "Portal 2",
        "added": 17084
      },
      {
        "id": 4291,
        "slug": "counter-strike-global-offensive",
        "name": "Counter-Strike: Global Offensive",
        "added": 14774
      },
      {
        "id": 12020,
        "slug": "left-4-dead-2",
        "name": "Left 4 Dead 2",
        "added": 14489
      },
      {
        "id": 4062,
        "slug": "bioshock-infinite",
        "name": "BioShock Infinite",
        "added": 13894
      },
      {
        "id": 802,
        "slug": "borderlands-2",
        "name": "Borderlands 2",
        "added": 13720
      },
      {
        "id": 13537,
        "slug": "half-life-2",
        "name": "Half-Life 2",
        "added": 12970
      }
    ]
  },
  {
    "id": 40,
    "name": "Casual",
    "slug": "casual",
    "games_count": 43416,
    "image_background": "https://media.rawg.io/media/screenshots/42d/42d770eb49f2ba01cd4045e0d92af7a9.jpg",
    "games": [
      {
        "id": 9721,
        "slug": "garrys-mod",
        "name": "Garry's Mod",
        "added": 8560
      },
      {
        "id": 326292,
        "slug": "fall-guys",
        "name": "Fall Guys: Ultimate Knockout",
        "added": 7532
      },
      {
        "id": 9830,
        "slug": "brawlhalla",
        "name": "Brawlhalla",
        "added": 6465
      },
      {
        "id": 356714,
        "slug": "among-us",
        "name": "Among Us",
        "added": 6137
      },
      {
        "id": 1959,
        "slug": "goat-simulator",
        "name": "Goat Simulator",
        "added": 5693
      },
      {
        "id": 16343,
        "slug": "a-story-about-my-uncle",
        "name": "A Story About My Uncle",
        "added": 5377
      }
    ]
  },
  {
    "id": 14,
    "name": "Simulation",
    "slug": "simulation",
    "games_count": 66761,
    "image_background": "https://media.rawg.io/media/games/997/997ab4d67e96fb20a4092383477d4463.jpg",
    "games": [
      {
        "id": 10035,
        "slug": "hitman",
        "name": "Hitman",
        "added": 9640
      },
      {
        "id": 654,
        "slug": "stardew-valley",
        "name": "Stardew Valley",
        "added": 8632
      },
      {
        "id": 9721,
        "slug": "garrys-mod",
        "name": "Garry's Mod",
        "added": 8560
      },
      {
        "id": 10243,
        "slug": "company-of-heroes-2",
        "name": "Company of Heroes 2",
        "added": 8371
      },
      {
        "id": 9882,
        "slug": "dont-starve-together",
        "name": "Don't Starve Together",
        "added": 8007
      },
      {
        "id": 22509,
        "slug": "minecraft",
        "name": "Minecraft",
        "added": 7333
      }
    ]
  },
  {
    "id": 7,
    "name": "Puzzle",
    "slug": "puzzle",
    "games_count": 99805,
    "image_background": "https://media.rawg.io/media/games/8cd/8cd179c85bd3de8f79bef245b15075fb.jpg",
    "games": [
      {
        "id": 4200,
        "slug": "portal-2",
        "name": "Portal 2",
        "added": 17084
      },
      {
        "id": 13536,
        "slug": "portal",
        "name": "Portal",
        "added": 14517
      },
      {
        "id": 1030,
        "slug": "limbo",
        "name": "Limbo",
        "added": 12272
      },
      {
        "id": 19709,
        "slug": "half-life-2-episode-two",
        "name": "Half-Life 2: Episode Two",
        "added": 9682
      },
      {
        "id": 18080,
        "slug": "half-life",
        "name": "Half-Life",
        "added": 8892
      },
      {
        "id": 1450,
        "slug": "inside",
        "name": "INSIDE",
        "added": 7112
      }
    ]
  },
  {
    "id": 11,
    "name": "Arcade",
    "slug": "arcade",
    "games_count": 22544,
    "image_background": "https://media.rawg.io/media/games/238/238e2b2b24c9838626700c69cacf1e3a.jpg",
    "games": [
      {
        "id": 3612,
        "slug": "hotline-miami",
        "name": "Hotline Miami",
        "added": 9376
      },
      {
        "id": 17540,
        "slug": "injustice-gods-among-us-ultimate-edition",
        "name": "Injustice: Gods Among Us Ultimate Edition",
        "added": 8582
      },
      {
        "id": 22509,
        "slug": "minecraft",
        "name": "Minecraft",
        "added": 7333
      },
      {
        "id": 4003,
        "slug": "grid-2",
        "name": "GRID 2",
        "added": 6775
      },
      {
        "id": 3408,
        "slug": "hotline-miami-2-wrong-number",
        "name": "Hotline Miami 2: Wrong Number",
        "added": 5476
      },
      {
        "id": 16343,
        "slug": "a-story-about-my-uncle",
        "name": "A Story About My Uncle",
        "added": 5377
      }
    ]
  },
  {
    "id": 83,
    "name": "Platformer",
    "slug": "platformer",
    "games_count": 105652,
    "image_background": "https://media.rawg.io/media/games/fd7/fd794a9f0ffe816038d981b3acc3eec9.jpg",
    "games": [
      {
        "id": 1030,
        "slug": "limbo",
        "name": "Limbo",
        "added": 12272
      },
      {
        "id": 422,
        "slug": "terraria",
        "name": "Terraria",
        "added": 10975
      },
      {
        "id": 9767,
        "slug": "hollow-knight",
        "name": "Hollow Knight",
        "added": 9542
      },
      {
        "id": 41,
        "slug": "little-nightmares",
        "name": "Little Nightmares",
        "added": 9422
      },
      {
        "id": 18080,
        "slug": "half-life",
        "name": "Half-Life",
        "added": 8892
      },
      {
        "id": 3144,
        "slug": "super-meat-boy",
        "name": "Super Meat Boy",
        "added": 8559
      }
    ]
  },
  {
    "id": 1,
    "name": "Racing",
    "slug": "racing",
    "games_count": 24448,
    "image_background": "https://media.rawg.io/media/games/23d/23d78acedbb5f40c9fb64e73af5af65d.jpg",
    "games": [
      {
        "id": 3272,
        "slug": "rocket-league",
        "name": "Rocket League",
        "added": 11164
      },
      {
        "id": 4003,
        "slug": "grid-2",
        "name": "GRID 2",
        "added": 6775
      },
      {
        "id": 2572,
        "slug": "dirt-rally",
        "name": "DiRT Rally",
        "added": 6068
      },
      {
        "id": 58753,
        "slug": "forza-horizon-4",
        "name": "Forza Horizon 4",
        "added": 5375
      },
      {
        "id": 5578,
        "slug": "grid",
        "name": "Race Driver: Grid",
        "added": 4981
      },
      {
        "id": 4347,
        "slug": "dirt-showdown",
        "name": "DiRT Showdown",
        "added": 4332
      }
    ]
  },
  {
    "id": 59,
    "name": "Massively Multiplayer",
    "slug": "massively-multiplayer",
    "games_count": 3152,
    "image_background": "https://media.rawg.io/media/games/651/651ae84f2d5e36206aad90976a453329.jpg",
    "games": [
      {
        "id": 32,
        "slug": "destiny-2",
        "name": "Destiny 2",
        "added": 12059
      },
      {
        "id": 10213,
        "slug": "dota-2",
        "name": "Dota 2",
        "added": 11021
      },
      {
        "id": 766,
        "slug": "warframe",
        "name": "Warframe",
        "added": 10860
      },
      {
        "id": 290856,
        "slug": "apex-legends",
        "name": "Apex Legends",
        "added": 9605
      },
      {
        "id": 10533,
        "slug": "path-of-exile",
        "name": "Path of Exile",
        "added": 8685
      },
      {
        "id": 10142,
        "slug": "playerunknowns-battlegrounds",
        "name": "PlayerUnknown’s Battlegrounds",
        "added": 8537
      }
    ]
  },
  {
    "id": 15,
    "name": "Sports",
    "slug": "sports",
    "games_count": 20711,
    "image_background": "https://media.rawg.io/media/screenshots/f5a/f5abab52c4d606551cd5ec3ab709e501.jpg",
    "games": [
      {
        "id": 3272,
        "slug": "rocket-league",
        "name": "Rocket League",
        "added": 11164
      },
      {
        "id": 326292,
        "slug": "fall-guys",
        "name": "Fall Guys: Ultimate Knockout",
        "added": 7532
      },
      {
        "id": 2572,
        "slug": "dirt-rally",
        "name": "DiRT Rally",
        "added": 6068
      },
      {
        "id": 53341,
        "slug": "jet-set-radio-2012",
        "name": "Jet Set Radio",
        "added": 4740
      },
      {
        "id": 9575,
        "slug": "vrchat",
        "name": "VRChat",
        "added": 3909
      },
      {
        "id": 622492,
        "slug": "forza-horizon-5",
        "name": "Forza Horizon 5",
        "added": 3873
      }
    ]
  },
  {
    "id": 6,
    "name": "Fighting",
    "slug": "fighting",
    "games_count": 12308,
    "image_background": "https://media.rawg.io/media/games/cc5/cc576aa274780702ef93463f5410031e.jpg",
    "games": [
      {
        "id": 17540,
        "slug": "injustice-gods-among-us-ultimate-edition",
        "name": "Injustice: Gods Among Us Ultimate Edition",
        "added": 8582
      },
      {
        "id": 108,
        "slug": "mortal-kombat-x",
        "name": "Mortal Kombat X",
        "added": 7908
      },
      {
        "id": 28179,
        "slug": "sega-mega-drive-and-genesis-classics",
        "name": "SEGA Mega Drive and Genesis Classics",
        "added": 7280
      },
      {
        "id": 9830,
        "slug": "brawlhalla",
        "name": "Brawlhalla",
        "added": 6465
      },
      {
        "id": 274480,
        "slug": "mortal-kombat-11",
        "name": "Mortal Kombat 11",
        "added": 4685
      },
      {
        "id": 44525,
        "slug": "yakuza-kiwami",
        "name": "Yakuza Kiwami",
        "added": 4038
      }
    ]
  },
  {
    "id": 19,
    "name": "Family",
    "slug": "family",
    "games_count": 5378,
    "image_background": "https://media.rawg.io/media/games/9f7/9f750cb69a31a42648f45e3681abed3a.jpg",
    "games": [
      {
        "id": 3254,
        "slug": "journey",
        "name": "Journey",
        "added": 7784
      },
      {
        "id": 2597,
        "slug": "lego-lord-of-the-rings",
        "name": "LEGO The Lord of the Rings",
        "added": 4950
      },
      {
        "id": 3350,
        "slug": "broken-age",
        "name": "Broken Age",
        "added": 4625
      },
      {
        "id": 3729,
        "slug": "lego-the-hobbit",
        "name": "LEGO The Hobbit",
        "added": 4549
      },
      {
        "id": 1259,
        "slug": "machinarium",
        "name": "Machinarium",
        "added": 4105
      },
      {
        "id": 1140,
        "slug": "world-of-goo",
        "name": "World of Goo",
        "added": 4042
      }
    ]
  },
  {
    "id": 28,
    "name": "Board Games",
    "slug": "board-games",
    "games_count": 8272,
    "image_background": "https://media.rawg.io/media/screenshots/768/768e36d4b62a1481fef737c6920fbfc7.jpg",
    "games": [
      {
        "id": 23557,
        "slug": "gwent-the-witcher-card-game",
        "name": "Gwent: The Witcher Card Game",
        "added": 4218
      },
      {
        "id": 327999,
        "slug": "dota-underlords",
        "name": "Dota Underlords",
        "added": 3557
      },
      {
        "id": 2055,
        "slug": "adventure-capitalist",
        "name": "AdVenture Capitalist",
        "added": 2972
      },
      {
        "id": 2306,
        "slug": "poker-night-2",
        "name": "Poker Night 2",
        "added": 1914
      },
      {
        "id": 3187,
        "slug": "armello",
        "name": "Armello",
        "added": 1802
      },
      {
        "id": 758,
        "slug": "hue",
        "name": "Hue",
        "added": 1743
      }
    ]
  },
  {
    "id": 34,
    "name": "Educational",
    "slug": "educational",
    "games_count": 16155,
    "image_background": "https://media.rawg.io/media/screenshots/6cd/6cd13ed3dcb6b44b8bc995850f2861e6.jpg",
    "games": [
      {
        "id": 1358,
        "slug": "papers-please",
        "name": "Papers, Please",
        "added": 6053
      },
      {
        "id": 1140,
        "slug": "world-of-goo",
        "name": "World of Goo",
        "added": 4042
      },
      {
        "id": 2778,
        "slug": "surgeon-simulator-cpr",
        "name": "Surgeon Simulator",
        "added": 3545
      },
      {
        "id": 9768,
        "slug": "gameguru",
        "name": "GameGuru",
        "added": 2260
      },
      {
        "id": 13777,
        "slug": "sid-meiers-civilization-iv-colonization",
        "name": "Sid Meier's Civilization IV: Colonization",
        "added": 2105
      },
      {
        "id": 6885,
        "slug": "pirates-3",
        "name": "Sid Meier's Pirates!",
        "added": 1995
      }
    ]
  },
  {
    "id": 17,
    "name": "Card",
    "slug": "card",
    "games_count": 4461,
    "image_background": "https://media.rawg.io/media/screenshots/ca2/ca257e3481af0b2c8149d6600440aa85.jpeg",
    "games": [
      {
        "id": 23557,
        "slug": "gwent-the-witcher-card-game",
        "name": "Gwent: The Witcher Card Game",
        "added": 4218
      },
      {
        "id": 28121,
        "slug": "slay-the-spire",
        "name": "Slay the Spire",
        "added": 4182
      },
      {
        "id": 18852,
        "slug": "poker-night-at-the-inventory",
        "name": "Poker Night at the Inventory",
        "added": 2539
      },
      {
        "id": 8923,
        "slug": "faeria",
        "name": "Faeria",
        "added": 1998
      },
      {
        "id": 332,
        "slug": "the-elder-scrolls-legends",
        "name": "The Elder Scrolls: Legends",
        "added": 1932
      },
      {
        "id": 2306,
        "slug": "poker-night-2",
        "name": "Poker Night 2",
        "added": 1914
      }
    ]
  }
];

================================================
FILE: src/data/platforms.ts
================================================
export default [
  {
    "id": 1,
    "name": "PC",
    "slug": "pc",
    "platforms": [
      {
        "id": 4,
        "name": "PC",
        "slug": "pc",
        "games_count": 534727,
        "image_background": "https://media.rawg.io/media/games/26d/26d4437715bee60138dab4a7c8c59c92.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": 819,
        "image_background": "https://media.rawg.io/media/games/d89/d89bd0cf4fcdc10820892980cbba0f49.jpg",
        "image": null,
        "year_start": 2020,
        "year_end": null
      },
      {
        "id": 18,
        "name": "PlayStation 4",
        "slug": "playstation4",
        "games_count": 6590,
        "image_background": "https://media.rawg.io/media/games/c4b/c4b0cab189e73432de3a250d8cf1c84e.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 16,
        "name": "PlayStation 3",
        "slug": "playstation3",
        "games_count": 3323,
        "image_background": "https://media.rawg.io/media/games/456/456dea5e1c7e3cd07060c14e96612001.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 15,
        "name": "PlayStation 2",
        "slug": "playstation2",
        "games_count": 1970,
        "image_background": "https://media.rawg.io/media/games/683/6833fbb183fd72a61c032501e3bc6d36.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 27,
        "name": "PlayStation",
        "slug": "playstation1",
        "games_count": 1606,
        "image_background": "https://media.rawg.io/media/games/0c5/0c5fcdf04048200da14b90e0e6cfaf6b.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 19,
        "name": "PS Vita",
        "slug": "ps-vita",
        "games_count": 1667,
        "image_background": "https://media.rawg.io/media/games/be0/be084b850302abe81675bc4ffc08a0d0.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 17,
        "name": "PSP",
        "slug": "psp",
        "games_count": 1438,
        "image_background": "https://media.rawg.io/media/games/a6c/a6cd31267a20a615d35f618e766208fc.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": 5485,
        "image_background": "https://media.rawg.io/media/games/f46/f466571d536f2e3ea9e815ad17177501.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 186,
        "name": "Xbox Series S/X",
        "slug": "xbox-series-x",
        "games_count": 730,
        "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": 2780,
        "image_background": "https://media.rawg.io/media/games/942/9424d6bb763dc38d9378b488603c87fa.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": 76450,
        "image_background": "https://media.rawg.io/media/games/8d6/8d69eb6c32ed6acfd75f82d532144993.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      }
    ]
  },
  {
    "id": 8,
    "name": "Android",
    "slug": "android",
    "platforms": [
      {
        "id": 21,
        "name": "Android",
        "slug": "android",
        "games_count": 54864,
        "image_background": "https://media.rawg.io/media/games/e74/e74458058b35e01c1ae3feeb39a3f724.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": 106165,
        "image_background": "https://media.rawg.io/media/games/7fa/7fa0b586293c5861ee32490e953a4996.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 55,
        "name": "Classic Macintosh",
        "slug": "macintosh",
        "games_count": 677,
        "image_background": "https://media.rawg.io/media/games/dd7/dd72d8a527cd9245c7eb7cd05aa53efa.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/510/510ad66178757fcafc467d6d01c3b425.jpeg",
        "image": null,
        "year_start": null,
        "year_end": null
      }
    ]
  },
  {
    "id": 6,
    "name": "Linux",
    "slug": "linux",
    "platforms": [
      {
        "id": 6,
        "name": "Linux",
        "slug": "linux",
        "games_count": 78912,
        "image_background": "https://media.rawg.io/media/games/f46/f466571d536f2e3ea9e815ad17177501.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": 5190,
        "image_background": "https://media.rawg.io/media/games/fc1/fc1307a2774506b5bd65d7e8424664a7.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 8,
        "name": "Nintendo 3DS",
        "slug": "nintendo-3ds",
        "games_count": 1731,
        "image_background": "https://media.rawg.io/media/games/369/36914d895c20e35f273286145c267764.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 9,
        "name": "Nintendo DS",
        "slug": "nintendo-ds",
        "games_count": 2476,
        "image_background": "https://media.rawg.io/media/screenshots/157/1571cdfb52888191eabaf53c2b897240.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": 1203,
        "image_background": "https://media.rawg.io/media/games/849/849414b978db37d4563ff9e4b0d3a787.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 11,
        "name": "Wii",
        "slug": "wii",
        "games_count": 2269,
        "image_background": "https://media.rawg.io/media/screenshots/f10/f10e7cafed6665861c58187b2ae3b310.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 105,
        "name": "GameCube",
        "slug": "gamecube",
        "games_count": 642,
        "image_background": "https://media.rawg.io/media/games/4f5/4f57124f7c0285150626cd1411c45b6e.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 83,
        "name": "Nintendo 64",
        "slug": "nintendo-64",
        "games_count": 364,
        "image_background": "https://media.rawg.io/media/games/f62/f62d090119e5ff05c59036480123fd83.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 24,
        "name": "Game Boy Advance",
        "slug": "game-boy-advance",
        "games_count": 956,
        "image_background": "https://media.rawg.io/media/games/dc6/dc68ca77e06ad993aade7faf645f5ec2.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 43,
        "name": "Game Boy Color",
        "slug": "game-boy-color",
        "games_count": 414,
        "image_background": "https://media.rawg.io/media/games/a9a/a9a2472f862b041d2980103ddbb61c91.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/screenshots/1e5/1e5e083780bb330479f7c778e6a0b7f0.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 79,
        "name": "SNES",
        "slug": "snes",
        "games_count": 940,
        "image_background": "https://media.rawg.io/media/games/0df/0dfe8852fa43d58cbdeb973765a9828d.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      },
      {
        "id": 49,
        "name": "NES",
        "slug": "nes",
        "games_count": 970,
        "image_background": "https://media.rawg.io/media/games/98e/98e3ce9d1be0f578d120168fb6c1e0a0.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/games/23e/23eecccb588a4a9c97f35ebf8f9f00ef.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/screenshots/b12/b12ed274eed80e4aced37badf228d1cf.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/f7a/f7a70f1b271de9b92a9714db33e4c8ba.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/screenshots/7dd/7dd630a9b38257450b53099932d3047d.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": 2061,
        "image_background": "https://media.rawg.io/media/games/a9a/a9a2472f862b041d2980103ddbb61c91.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/47b/47b50d880be8453bf9cda6e5c007bc26.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": 223,
        "image_background": "https://media.rawg.io/media/screenshots/f9a/f9ac59bb4af2ca2193ee9ffb979577cf.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/1cf/1cf9e301f1d27172546dcabc2f6cb597.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": 113,
        "image_background": "https://media.rawg.io/media/screenshots/488/488788e787a69d5ecf3c74884548ec24.jpg",
        "image": null,
        "year_start": null,
        "year_end": null
      }
    ]
  },
  {
    "id": 14,
    "name": "Web",
    "slug": "web",
    "platforms": [
      {
        "id": 171,
        "name": "Web",
        "slug": "web",
        "games_count": 269156,
        "image_background": "https://media.rawg.io/media/screenshots/78d/78d2dc36ce3b03af2e3000a078da8185.jpeg",
        "image": null,
        "year_start": null,
        "year_end": null
      }
    ]
  }
]

================================================
FILE: src/hooks/useData.ts
================================================
import { AxiosRequestConfig, CanceledError } from "axios";
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";

interface FetchResponse<T> {
  count: number;
  results: T[];
}

const useData = <T>(endpoint: string, requestConfig?: AxiosRequestConfig, deps?: any[]) => {
  const [data, setData] = useState<T[]>([]);
  const [error, setError] = useState("");
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<FetchResponse<T>>(endpoint, { signal: controller.signal, ...requestConfig })
      .then((res) => {
        setData(res.data.results);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message)
        setLoading(false);
      });

    return () => controller.abort();
  }, deps ? [...deps] : []);

  return { data, error, isLoading };
};

export default useData;

================================================
FILE: src/hooks/useGames.ts
================================================
import { GameQuery } from "../App";
import useData from "./useData";
import { Genre } from "./useGenres";

export interface Platform {
  id: number;
  name: string;
  slug: string;
}

export interface Game {
  id: number;
  name: string;
  background_image: string;
  parent_platforms: { platform: Platform }[];
  metacritic: number;
  rating_top: number;
}

const useGames = (gameQuery: GameQuery) =>
  useData<Game>(
    "/games",
    {
      params: {
        genres: gameQuery.genre?.id,
        platforms: gameQuery.platform?.id,
        ordering: gameQuery.sortOrder,
        search: gameQuery.searchText
      },
    },
    [gameQuery]
  );

export default useGames;


================================================
FILE: src/hooks/useGenres.ts
================================================
import genres from "../data/genres";

export interface Genre {
  id: number;
  name: string;
  image_background: string;
}

const useGenres = () => ({ data: genres, isLoading: false, error: null })

export default useGenres;

================================================
FILE: src/hooks/usePlatforms.ts
================================================
import platforms from "../data/platforms";

interface Platform {
  id: number;
  name: string;
  slug: string;
}

const usePlatforms = () => ({ data: platforms, isLoading: false, error: null });

export default usePlatforms;


================================================
FILE: src/index.css
================================================
form {
  width: 100%;
}

================================================
FILE: src/main.tsx
================================================
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react'
import App from './App'
import theme from './theme'
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <ChakraProvider theme={theme}>
      <ColorModeScript initialColorMode={theme.config.initialColorMode}/>
      <App />
    </ChakraProvider>
  </React.StrictMode>,
)


================================================
FILE: src/services/api-client.ts
================================================
import axios from "axios";

export default axios.create({
  baseURL: "https://api.rawg.io/api",
  params: {
    key: "c7b18323a47d40c394ea5b019646b1f5",
  },
});


================================================
FILE: src/services/image-url.ts
================================================
import noImage from '../assets/no-image-placeholder.webp';

const getCroppedImageUrl = (url: string) => {
  if (!url) return noImage;
  
  const target = 'media/';
  const index = url.indexOf(target) + target.length;
  return url.slice(0, index) + 'crop/600/400/' + url.slice(index);
}

export default getCroppedImageUrl;

================================================
FILE: src/theme.ts
================================================
import { extendTheme, ThemeConfig } from "@chakra-ui/react";

const config: ThemeConfig = {
  initialColorMode: 'dark'
};

const theme = extendTheme({ 
  config,
  colors: {
    gray: {
      50: '#f9f9f9',
      100: '#ededed',
      200: '#d3d3d3',
      300: '#b3b3b3',
      400: '#a0a0a0',
      500: '#898989',
      600: '#6c6c6c',
      700: '#202020',
      800: '#121212',
      900: '#111'
    }
  }
 });

export default theme;

================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}


================================================
FILE: tsconfig.node.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}


================================================
FILE: vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})
Download .txt
gitextract_acqhe0vy/

├── .gitignore
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── components/
│   │   ├── ColorModeSwitch.tsx
│   │   ├── CriticScore.tsx
│   │   ├── Emoji.tsx
│   │   ├── GameCard.tsx
│   │   ├── GameCardContainer.tsx
│   │   ├── GameCardSkeleton.tsx
│   │   ├── GameGrid.tsx
│   │   ├── GameHeading.tsx
│   │   ├── GenreList.tsx
│   │   ├── NavBar.tsx
│   │   ├── PlatformIconList.tsx
│   │   ├── PlatformSelector.tsx
│   │   ├── SearchInput.tsx
│   │   └── SortSelector.tsx
│   ├── data/
│   │   ├── genres.ts
│   │   └── platforms.ts
│   ├── hooks/
│   │   ├── useData.ts
│   │   ├── useGames.ts
│   │   ├── useGenres.ts
│   │   └── usePlatforms.ts
│   ├── index.css
│   ├── main.tsx
│   ├── services/
│   │   ├── api-client.ts
│   │   └── image-url.ts
│   ├── theme.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (19 symbols across 17 files)

FILE: src/App.tsx
  type GameQuery (line 13) | interface GameQuery {
  function App (line 20) | function App() {

FILE: src/components/CriticScore.tsx
  type Props (line 3) | interface Props {

FILE: src/components/Emoji.tsx
  type Props (line 6) | interface Props {

FILE: src/components/GameCard.tsx
  type Props (line 9) | interface Props {

FILE: src/components/GameCardContainer.tsx
  type Props (line 4) | interface Props {

FILE: src/components/GameGrid.tsx
  type Props (line 9) | interface Props {

FILE: src/components/GameHeading.tsx
  type Props (line 4) | interface Props {

FILE: src/components/GenreList.tsx
  type Props (line 14) | interface Props {

FILE: src/components/NavBar.tsx
  type Props (line 6) | interface Props {

FILE: src/components/PlatformIconList.tsx
  type Props (line 16) | interface Props {

FILE: src/components/PlatformSelector.tsx
  type Props (line 6) | interface Props {

FILE: src/components/SearchInput.tsx
  type Props (line 5) | interface Props {

FILE: src/components/SortSelector.tsx
  type Props (line 4) | interface Props {

FILE: src/hooks/useData.ts
  type FetchResponse (line 5) | interface FetchResponse<T> {

FILE: src/hooks/useGames.ts
  type Platform (line 5) | interface Platform {
  type Game (line 11) | interface Game {

FILE: src/hooks/useGenres.ts
  type Genre (line 3) | interface Genre {

FILE: src/hooks/usePlatforms.ts
  type Platform (line 3) | interface Platform {
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (63K chars).
[
  {
    "path": ".gitignore",
    "chars": 261,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "README.md",
    "chars": 1530,
    "preview": "# GameHub\n\nGameHub is a video game discovery web app that helps you find new and interesting games to play. With GameHub"
  },
  {
    "path": "index.html",
    "chars": 356,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
  },
  {
    "path": "package.json",
    "chars": 635,
    "preview": "{\n  \"name\": \"game-hub\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n "
  },
  {
    "path": "src/App.css",
    "chars": 606,
    "preview": "#root {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n  text-align: center;\n}\n\n.logo {\n  height: 6em;\n  paddin"
  },
  {
    "path": "src/App.tsx",
    "chars": 1903,
    "preview": "import { Box, Flex, Grid, GridItem, HStack, Show } from \"@chakra-ui/react\";\nimport { useState } from \"react\";\nimport Gam"
  },
  {
    "path": "src/components/ColorModeSwitch.tsx",
    "chars": 380,
    "preview": "import { HStack, Switch, Text, useColorMode } from '@chakra-ui/react'\n\nconst ColorModeSwitch = () => {\n  const {toggleCo"
  },
  {
    "path": "src/components/CriticScore.tsx",
    "chars": 331,
    "preview": "import { Badge } from '@chakra-ui/react';\n\ninterface Props { \n  score: number;\n}\n\nconst CriticScore = ({ score }: Props)"
  },
  {
    "path": "src/components/Emoji.tsx",
    "chars": 622,
    "preview": "import bullsEye from '../assets/bulls-eye.webp';\nimport thumbsUp from '../assets/thumbs-up.webp';\nimport meh from '../as"
  },
  {
    "path": "src/components/GameCard.tsx",
    "chars": 865,
    "preview": "import { Card, CardBody, Heading, HStack, Image, Text } from '@chakra-ui/react'\nimport React from 'react'\nimport { Game "
  },
  {
    "path": "src/components/GameCardContainer.tsx",
    "chars": 301,
    "preview": "import { Box } from \"@chakra-ui/react\";\nimport { ReactNode } from \"react\";\n\ninterface Props {\n  children: ReactNode;\n}\n\n"
  },
  {
    "path": "src/components/GameCardSkeleton.tsx",
    "chars": 274,
    "preview": "import { Card, CardBody, Skeleton, SkeletonText } from '@chakra-ui/react'\n\nconst GameCardSkeleton = () => {\n  return (\n "
  },
  {
    "path": "src/components/GameGrid.tsx",
    "chars": 1055,
    "preview": "import { SimpleGrid, Text } from \"@chakra-ui/react\";\nimport { GameQuery } from \"../App\";\nimport useGames, { Platform } f"
  },
  {
    "path": "src/components/GameHeading.tsx",
    "chars": 376,
    "preview": "import { Heading } from '@chakra-ui/react'\nimport { GameQuery } from '../App'\n\ninterface Props {\n  gameQuery: GameQuery\n"
  },
  {
    "path": "src/components/GenreList.tsx",
    "chars": 1397,
    "preview": "import {\n  Button,\n  Heading,\n  HStack,\n  Image,\n  List,\n  ListItem,\n  Spinner,\n  Text,\n} from \"@chakra-ui/react\";\nimpor"
  },
  {
    "path": "src/components/NavBar.tsx",
    "chars": 476,
    "preview": "import { HStack, Image } from '@chakra-ui/react'\nimport logo from '../assets/logo.webp';\nimport ColorModeSwitch from './"
  },
  {
    "path": "src/components/PlatformIconList.tsx",
    "chars": 957,
    "preview": "import {\n  FaWindows,\n  FaPlaystation,\n  FaXbox,\n  FaApple,\n  FaLinux,\n  FaAndroid,\n} from \"react-icons/fa\";\nimport { Md"
  },
  {
    "path": "src/components/PlatformSelector.tsx",
    "chars": 855,
    "preview": "import { Button, Menu, MenuButton, MenuItem, MenuList } from \"@chakra-ui/react\";\nimport { BsChevronDown } from \"react-ic"
  },
  {
    "path": "src/components/SearchInput.tsx",
    "chars": 673,
    "preview": "import { Input, InputGroup, InputLeftElement } from \"@chakra-ui/react\";\nimport { useRef } from \"react\";\nimport { BsSearc"
  },
  {
    "path": "src/components/SortSelector.tsx",
    "chars": 1126,
    "preview": "import { Button, Menu, MenuButton, MenuItem, MenuList } from \"@chakra-ui/react\";\nimport { BsChevronDown } from \"react-ic"
  },
  {
    "path": "src/data/genres.ts",
    "chars": 19056,
    "preview": "export default [\n  {\n    \"id\": 4,\n    \"name\": \"Action\",\n    \"slug\": \"action\",\n    \"games_count\": 177189,\n    \"image_back"
  },
  {
    "path": "src/data/platforms.ts",
    "chars": 16876,
    "preview": "export default [\n  {\n    \"id\": 1,\n    \"name\": \"PC\",\n    \"slug\": \"pc\",\n    \"platforms\": [\n      {\n        \"id\": 4,\n      "
  },
  {
    "path": "src/hooks/useData.ts",
    "chars": 1008,
    "preview": "import { AxiosRequestConfig, CanceledError } from \"axios\";\nimport { useEffect, useState } from \"react\";\nimport apiClient"
  },
  {
    "path": "src/hooks/useGames.ts",
    "chars": 674,
    "preview": "import { GameQuery } from \"../App\";\nimport useData from \"./useData\";\nimport { Genre } from \"./useGenres\";\n\nexport interf"
  },
  {
    "path": "src/hooks/useGenres.ts",
    "chars": 224,
    "preview": "import genres from \"../data/genres\";\n\nexport interface Genre {\n  id: number;\n  name: string;\n  image_background: string;"
  },
  {
    "path": "src/hooks/usePlatforms.ts",
    "chars": 225,
    "preview": "import platforms from \"../data/platforms\";\n\ninterface Platform {\n  id: number;\n  name: string;\n  slug: string;\n}\n\nconst "
  },
  {
    "path": "src/index.css",
    "chars": 23,
    "preview": "form {\n  width: 100%;\n}"
  },
  {
    "path": "src/main.tsx",
    "chars": 474,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport { ChakraProvider, ColorModeScript } from '@chak"
  },
  {
    "path": "src/services/api-client.ts",
    "chars": 162,
    "preview": "import axios from \"axios\";\n\nexport default axios.create({\n  baseURL: \"https://api.rawg.io/api\",\n  params: {\n    key: \"c7"
  },
  {
    "path": "src/services/image-url.ts",
    "chars": 321,
    "preview": "import noImage from '../assets/no-image-placeholder.webp';\n\nconst getCroppedImageUrl = (url: string) => {\n  if (!url) re"
  },
  {
    "path": "src/theme.ts",
    "chars": 438,
    "preview": "import { extendTheme, ThemeConfig } from \"@chakra-ui/react\";\n\nconst config: ThemeConfig = {\n  initialColorMode: 'dark'\n}"
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 559,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\","
  },
  {
    "path": "tsconfig.node.json",
    "chars": 184,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"allowSynthe"
  },
  {
    "path": "vite.config.ts",
    "chars": 163,
    "preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
  }
]

About this extraction

This page contains the full source code of the mosh-hamedani/game-hub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (54.1 KB), approximately 17.6k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!