├── src ├── index.css ├── vite-env.d.ts ├── assets │ ├── meh.webp │ ├── logo.webp │ ├── bulls-eye.webp │ ├── thumbs-up.webp │ ├── no-image-placeholder.webp │ └── react.svg ├── services │ ├── api-clint.ts │ └── image-url.ts ├── hooks │ ├── useGenres.ts │ ├── usePlatforms.ts │ ├── useGames.ts │ └── useData.ts ├── components │ ├── GameCardSkeleton.tsx │ ├── CriticScore.tsx │ ├── GameHeading.tsx │ ├── GameCardContainer.tsx │ ├── ColorModeSwitch.tsx │ ├── Navbar.tsx │ ├── Emoji.tsx │ ├── SearchInput.tsx │ ├── GameCard.tsx │ ├── PlatformSelector.tsx │ ├── PlatformIconList.tsx │ ├── SortSelector.tsx │ ├── GameGrid.tsx │ └── GenreList.tsx ├── main.tsx ├── theme.ts ├── App.css ├── App.tsx └── data │ └── genres.ts ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── package.json ├── README.md └── public └── vite.svg /src/index.css: -------------------------------------------------------------------------------- 1 | form { 2 | width: 100%; 3 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/assets/meh.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimahdi-t/game-hub/HEAD/src/assets/meh.webp -------------------------------------------------------------------------------- /src/assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimahdi-t/game-hub/HEAD/src/assets/logo.webp -------------------------------------------------------------------------------- /src/assets/bulls-eye.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimahdi-t/game-hub/HEAD/src/assets/bulls-eye.webp -------------------------------------------------------------------------------- /src/assets/thumbs-up.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimahdi-t/game-hub/HEAD/src/assets/thumbs-up.webp -------------------------------------------------------------------------------- /src/assets/no-image-placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alimahdi-t/game-hub/HEAD/src/assets/no-image-placeholder.webp -------------------------------------------------------------------------------- /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 | }) 8 | -------------------------------------------------------------------------------- /src/services/api-clint.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default axios.create({ 4 | baseURL: "https://api.rawg.io/api", 5 | params: { 6 | key: "0a0192b9be6f4f94b1797fae455cb945", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/hooks/useGenres.ts: -------------------------------------------------------------------------------- 1 | import useData from "./useData.ts"; 2 | 3 | export interface Genre { 4 | id: number; 5 | name: string; 6 | image_background: string; 7 | } 8 | 9 | const useGenres = () => useData("/genres"); 10 | 11 | export default useGenres; 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/usePlatforms.ts: -------------------------------------------------------------------------------- 1 | import useData from "./useData.ts"; 2 | 3 | interface Platforms { 4 | id: number; 5 | name: string; 6 | slug: string; 7 | } 8 | 9 | const usePlatforms = () => useData("platforms/lists/parents"); 10 | 11 | export default usePlatforms; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /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; 15 | -------------------------------------------------------------------------------- /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 | const target = "media/"; 6 | const index = url.indexOf(target) + target.length; 7 | return url.slice(0, index) + "crop/600/400/" + url.slice(index); 8 | }; 9 | 10 | export default getCroppedImageUrl; 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 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 | const color = score > 75 ? "green" : score > 60 ? "yellow" : ""; 9 | return ( 10 | 11 | {score} 12 | 13 | ); 14 | }; 15 | 16 | export default CriticScore; 17 | -------------------------------------------------------------------------------- /src/components/GameHeading.tsx: -------------------------------------------------------------------------------- 1 | import { Heading } from "@chakra-ui/react"; 2 | import { GameQuery } from "../App.tsx"; 3 | interface Props { 4 | gameQuery: GameQuery; 5 | } 6 | 7 | const GameHeading = ({ gameQuery }: Props) => { 8 | const heading = `${gameQuery.platform?.name || ""} ${ 9 | gameQuery.genre?.name || "" 10 | } Games`; 11 | return ( 12 | 13 | {heading} 14 | 15 | ); 16 | }; 17 | 18 | export default GameHeading; 19 | -------------------------------------------------------------------------------- /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 | const GameCardContainer = ({ children }: Props) => { 8 | return ( 9 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export default GameCardContainer; 21 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /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 App from "./App.tsx"; 5 | import theme from "./theme.ts"; 6 | import "./index.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | , 15 | ); 16 | -------------------------------------------------------------------------------- /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; 26 | -------------------------------------------------------------------------------- /src/components/ColorModeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Switch, useColorMode } from "@chakra-ui/react"; 2 | import { SunIcon, MoonIcon } from "@chakra-ui/icons"; 3 | 4 | const ColorModeSwitch = () => { 5 | const { toggleColorMode, colorMode } = useColorMode(); 6 | return ( 7 | 8 | 13 | {colorMode === "dark" ? : } 14 | 15 | ); 16 | }; 17 | 18 | export default ColorModeSwitch; 19 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Image } from "@chakra-ui/react"; 2 | import logo from "../assets/logo.webp"; 3 | import ColorModeSwitch from "./ColorModeSwitch.tsx"; 4 | import SearchInput from "./SearchInput.tsx"; 5 | interface Props { 6 | onSearch: (searchText: string) => void; 7 | } 8 | const Navbar = ({ onSearch }: Props) => { 9 | return ( 10 | 11 | 12 | onSearch(searchText)} /> 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default Navbar; 19 | -------------------------------------------------------------------------------- /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 | interface Props { 6 | rating: number; 7 | } 8 | 9 | const Emoji = ({ rating }: Props) => { 10 | if (rating < 3) return null; 11 | 12 | const emojiMap: { [key: number]: ImageProps } = { 13 | 3: { src: meh, alt: "meh", boxSize: "25px" }, 14 | 4: { src: thumbsUp, alt: "recommended", boxSize: "25px" }, 15 | 5: { src: bullsEye, alt: "exceptional", boxSize: "35px" }, 16 | }; 17 | return ; 18 | }; 19 | 20 | export default Emoji; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/hooks/useGames.ts: -------------------------------------------------------------------------------- 1 | import useData from "./useData.ts"; 2 | import { GameQuery } from "../App.tsx"; 3 | 4 | export interface Platform { 5 | id: number; 6 | name: string; 7 | slug: string; 8 | } 9 | export interface Game { 10 | id: number; 11 | name: string; 12 | background_image: string; 13 | parent_platforms: { platform: Platform }[]; 14 | metacritic: number; 15 | rating_top: number; 16 | } 17 | 18 | const useGames = (gameQuery: GameQuery) => 19 | useData( 20 | "games", 21 | { 22 | params: { 23 | genres: gameQuery.genre?.id, 24 | platforms: gameQuery.platform?.id, 25 | ordering: gameQuery.sortOrder, 26 | search: gameQuery.searchText, 27 | }, 28 | }, 29 | [gameQuery], 30 | ); 31 | 32 | export default useGames; 33 | -------------------------------------------------------------------------------- /src/components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input, InputGroup, InputLeftElement } from "@chakra-ui/react"; 2 | import { BsSearch } from "react-icons/bs"; 3 | import { useRef } from "react"; 4 | interface Props { 5 | onSearch: (searchText: string) => void; 6 | } 7 | const SearchInput = ({ onSearch }: Props) => { 8 | const ref = useRef(null); 9 | return ( 10 |
{ 12 | event.preventDefault(); 13 | if (ref.current) onSearch(ref.current.value); 14 | }} 15 | > 16 | 17 | } /> 18 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default SearchInput; 30 | -------------------------------------------------------------------------------- /src/components/GameCard.tsx: -------------------------------------------------------------------------------- 1 | import { Game } from "../hooks/useGames.ts"; 2 | import { Card, CardBody, Heading, HStack, Image } from "@chakra-ui/react"; 3 | import PlatformIconList from "./PlatformIconList.tsx"; 4 | import CriticScore from "./CriticScore.tsx"; 5 | import getCroppedImageUrl from "../services/image-url.ts"; 6 | import Emoji from "./Emoji.tsx"; 7 | 8 | interface Props { 9 | game: Game; 10 | } 11 | 12 | const GameCard = ({ game }: Props) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | p.platform)} 20 | /> 21 | 22 | 23 | 24 | {game.name} 25 | {} 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default GameCard; 33 | -------------------------------------------------------------------------------- /src/components/PlatformSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"; 2 | import { BsChevronDown } from "react-icons/bs"; 3 | import usePlatforms from "../hooks/usePlatforms.ts"; 4 | import { Platform } from "../hooks/useGames.ts"; 5 | 6 | interface Props { 7 | onSelectPlatform: (platform: Platform) => void; 8 | selectedPlatform: Platform | null; 9 | } 10 | const PlatformSelector = ({ onSelectPlatform, selectedPlatform }: Props) => { 11 | const { data, error } = usePlatforms(); 12 | if (error) return null; 13 | return ( 14 | 15 | }> 16 | {selectedPlatform?.name || "Platform"} 17 | 18 | 19 | {data?.map((platform) => ( 20 | onSelectPlatform(platform)} 22 | key={platform.id} 23 | > 24 | {platform.name} 25 | 26 | ))} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default PlatformSelector; 33 | -------------------------------------------------------------------------------- /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 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/icons": "^2.1.0", 14 | "@chakra-ui/react": "^2.8.0", 15 | "@emotion/react": "^11.11.1", 16 | "@emotion/styled": "^11.11.0", 17 | "axios": "^1.4.0", 18 | "framer-motion": "^10.15.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-icons": "^4.10.1" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.15", 25 | "@types/react-dom": "^18.2.7", 26 | "@typescript-eslint/eslint-plugin": "^6.0.0", 27 | "@typescript-eslint/parser": "^6.0.0", 28 | "@vitejs/plugin-react": "^4.0.3", 29 | "eslint": "^8.45.0", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.4.3", 32 | "typescript": "^5.0.2", 33 | "vite": "^4.4.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 "../hooks/useGames.ts"; 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/SortSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"; 2 | import { BsChevronDown } from "react-icons/bs"; 3 | 4 | interface Props { 5 | onSelectSortOrder: (sortOrder: string) => void; 6 | sortOrder: string; 7 | } 8 | const SortSelector = ({ onSelectSortOrder, sortOrder }: Props) => { 9 | const sortOrders = [ 10 | { value: "", label: "Relevance" }, 11 | { value: "-added", label: "Date added" }, 12 | { value: "name", label: "Name" }, 13 | { value: "-released", label: "Release date" }, 14 | { value: "-metacritic", label: "Popularity" }, 15 | { value: "-rating", label: "Average rating" }, 16 | ]; 17 | 18 | const current = sortOrders.find((order) => order.value === sortOrder); 19 | return ( 20 | 21 | }> 22 | Order by: {current?.label || "Relevance"} 23 | 24 | 25 | {sortOrders.map((order, index) => ( 26 | onSelectSortOrder(order.value)} 28 | value={order.value} 29 | key={index} 30 | > 31 | {order.label} 32 | 33 | ))} 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default SortSelector; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /src/hooks/useData.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import apiClint from "../services/api-clint.ts"; 3 | import { AxiosRequestConfig, CanceledError } from "axios"; 4 | 5 | interface FetchResponse { 6 | count: number; 7 | results: T[]; 8 | } 9 | 10 | const useData = ( 11 | endpoint: string, 12 | requestConfig?: AxiosRequestConfig, 13 | deps?: any[], 14 | ) => { 15 | const [data, setData] = useState(); 16 | const [error, setError] = useState(""); 17 | const [isLoading, setLoading] = useState(false); 18 | 19 | useEffect( 20 | () => { 21 | const controller = new AbortController(); 22 | 23 | setLoading(true); 24 | apiClint 25 | .get>(endpoint, { 26 | signal: controller.signal, 27 | ...requestConfig, 28 | }) 29 | .then((res) => { 30 | setData(res.data.results); 31 | setLoading(false); 32 | }) 33 | .catch((error) => { 34 | if (error instanceof CanceledError) return; 35 | setError(error.message); 36 | setLoading(false); 37 | }) 38 | .finally(() => { 39 | // Not working on strict mode 40 | // setLoading(false) 41 | }); 42 | 43 | return () => controller.abort(); 44 | }, 45 | deps ? [...deps] : [], 46 | ); 47 | 48 | return { data, error, isLoading }; 49 | }; 50 | 51 | export default useData; 52 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/GameGrid.tsx: -------------------------------------------------------------------------------- 1 | import { SimpleGrid, Text } from "@chakra-ui/react"; 2 | import useGames from "../hooks/useGames.ts"; 3 | import GameCard from "./GameCard.tsx"; 4 | import GameCardSkeleton from "./GameCardSkeleton.tsx"; 5 | import GameCardContainer from "./GameCardContainer.tsx"; 6 | 7 | import { GameQuery } from "../App.tsx"; 8 | 9 | interface Props { 10 | gameQuery: GameQuery; 11 | } 12 | 13 | const GameGrid = ({ gameQuery }: Props) => { 14 | const { data, error, isLoading } = useGames(gameQuery); 15 | const skeletons = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; 16 | if (error) return {error}; 17 | return ( 18 | 35 | {isLoading && 36 | skeletons.map((skeleton) => ( 37 | 38 | 39 | 40 | ))} 41 | {data?.map((game) => ( 42 | 43 | 44 | 45 | ))} 46 | 47 | ); 48 | }; 49 | 50 | export default GameGrid; 51 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, GridItem, HStack, Show } from "@chakra-ui/react"; 2 | import Navbar from "./components/Navbar.tsx"; 3 | import GameGrid from "./components/GameGrid.tsx"; 4 | import GenreList from "./components/GenreList.tsx"; 5 | import { useState } from "react"; 6 | import { Genre } from "./hooks/useGenres.ts"; 7 | import PlatformSelector from "./components/PlatformSelector.tsx"; 8 | import { Platform } from "./hooks/useGames.ts"; 9 | import SortSelector from "./components/SortSelector.tsx"; 10 | import GameHeading from "./components/GameHeading.tsx"; 11 | 12 | export interface GameQuery { 13 | genre: Genre | null; 14 | platform: Platform | null; 15 | sortOrder: string; 16 | searchText: string; 17 | } 18 | 19 | function App() { 20 | const [gameQuery, setGameQuery] = useState({} as GameQuery); 21 | 22 | return ( 23 | <> 24 | 34 | 35 | 37 | setGameQuery({ ...gameQuery, searchText }) 38 | } 39 | /> 40 | 41 | 42 | 43 | 44 | setGameQuery({ ...gameQuery, genre })} 47 | /> 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | setGameQuery({ ...gameQuery, platform }) 57 | } 58 | selectedPlatform={gameQuery.platform} 59 | /> 60 | 62 | setGameQuery({ ...gameQuery, sortOrder }) 63 | } 64 | sortOrder={gameQuery.sortOrder} 65 | /> 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | 74 | export default App; 75 | -------------------------------------------------------------------------------- /src/components/GenreList.tsx: -------------------------------------------------------------------------------- 1 | import useGenres, { Genre } from "../hooks/useGenres.ts"; 2 | import { 3 | Box, 4 | Button, 5 | Collapse, 6 | Heading, 7 | HStack, 8 | IconButton, 9 | Image, 10 | List, 11 | ListItem, 12 | Skeleton, 13 | SkeletonText, 14 | Text, 15 | useDisclosure, 16 | } from "@chakra-ui/react"; 17 | import getCroppedImageUrl from "../services/image-url.ts"; 18 | import { BsChevronDown, BsChevronUp } from "react-icons/bs"; 19 | 20 | interface Props { 21 | onSelectGenre: (genre: Genre) => void; 22 | selectedGenre: Genre | null; 23 | } 24 | 25 | const GenreList = ({ onSelectGenre, selectedGenre }: Props) => { 26 | const { data, isLoading, error } = useGenres(); 27 | const { isOpen, onToggle } = useDisclosure(); 28 | const skeleton = [ 29 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 30 | ]; 31 | if (error) return null; 32 | return ( 33 | <> 34 | 35 | Genres 36 | 37 | 38 | {isLoading && 39 | skeleton.map((skeleton) => ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ))} 48 | 49 | 50 | 51 | {data?.map((genre) => ( 52 | 53 | 54 | 60 | 72 | 73 | 74 | ))} 75 | 76 | 77 | 78 | : } 82 | /> 83 | {isOpen ? "Hide" : "Show All"} 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default GenreList; 91 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/genres.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 4, 4 | name: "Action", 5 | slug: "action", 6 | games_count: 173843, 7 | image_background: 8 | "https://media.rawg.io/media/games/456/456dea5e1c7e3cd07060c14e96612001.jpg", 9 | games: [ 10 | { 11 | id: 3498, 12 | slug: "grand-theft-auto-v", 13 | name: "Grand Theft Auto V", 14 | added: 19876, 15 | }, 16 | { 17 | id: 3328, 18 | slug: "the-witcher-3-wild-hunt", 19 | name: "The Witcher 3: Wild Hunt", 20 | added: 19136, 21 | }, 22 | { 23 | id: 5286, 24 | slug: "tomb-raider", 25 | name: "Tomb Raider (2013)", 26 | added: 15721, 27 | }, 28 | { 29 | id: 4291, 30 | slug: "counter-strike-global-offensive", 31 | name: "Counter-Strike: Global Offensive", 32 | added: 15702, 33 | }, 34 | { 35 | id: 12020, 36 | slug: "left-4-dead-2", 37 | name: "Left 4 Dead 2", 38 | added: 15276, 39 | }, 40 | { 41 | id: 5679, 42 | slug: "the-elder-scrolls-v-skyrim", 43 | name: "The Elder Scrolls V: Skyrim", 44 | added: 15025, 45 | }, 46 | ], 47 | }, 48 | { 49 | id: 51, 50 | name: "Indie", 51 | slug: "indie", 52 | games_count: 54633, 53 | image_background: 54 | "https://media.rawg.io/media/games/6a2/6a2e48933245e2cd3c92248c75c925e1.jpg", 55 | games: [ 56 | { 57 | id: 1030, 58 | slug: "limbo", 59 | name: "Limbo", 60 | added: 12782, 61 | }, 62 | { 63 | id: 3272, 64 | slug: "rocket-league", 65 | name: "Rocket League", 66 | added: 11707, 67 | }, 68 | { 69 | id: 422, 70 | slug: "terraria", 71 | name: "Terraria", 72 | added: 11662, 73 | }, 74 | { 75 | id: 9767, 76 | slug: "hollow-knight", 77 | name: "Hollow Knight", 78 | added: 10109, 79 | }, 80 | { 81 | id: 3612, 82 | slug: "hotline-miami", 83 | name: "Hotline Miami", 84 | added: 9809, 85 | }, 86 | { 87 | id: 3790, 88 | slug: "outlast", 89 | name: "Outlast", 90 | added: 9738, 91 | }, 92 | ], 93 | }, 94 | { 95 | id: 3, 96 | name: "Adventure", 97 | slug: "adventure", 98 | games_count: 133542, 99 | image_background: 100 | "https://media.rawg.io/media/games/b54/b54598d1d5cc31899f4f0a7e3122a7b0.jpg", 101 | games: [ 102 | { 103 | id: 3498, 104 | slug: "grand-theft-auto-v", 105 | name: "Grand Theft Auto V", 106 | added: 19876, 107 | }, 108 | { 109 | id: 3328, 110 | slug: "the-witcher-3-wild-hunt", 111 | name: "The Witcher 3: Wild Hunt", 112 | added: 19136, 113 | }, 114 | { 115 | id: 5286, 116 | slug: "tomb-raider", 117 | name: "Tomb Raider (2013)", 118 | added: 15721, 119 | }, 120 | { 121 | id: 13536, 122 | slug: "portal", 123 | name: "Portal", 124 | added: 15326, 125 | }, 126 | { 127 | id: 28, 128 | slug: "red-dead-redemption-2", 129 | name: "Red Dead Redemption 2", 130 | added: 14484, 131 | }, 132 | { 133 | id: 3439, 134 | slug: "life-is-strange-episode-1-2", 135 | name: "Life is Strange", 136 | added: 14330, 137 | }, 138 | ], 139 | }, 140 | { 141 | id: 5, 142 | name: "RPG", 143 | slug: "role-playing-games-rpg", 144 | games_count: 53007, 145 | image_background: 146 | "https://media.rawg.io/media/games/d82/d82990b9c67ba0d2d09d4e6fa88885a7.jpg", 147 | games: [ 148 | { 149 | id: 3328, 150 | slug: "the-witcher-3-wild-hunt", 151 | name: "The Witcher 3: Wild Hunt", 152 | added: 19136, 153 | }, 154 | { 155 | id: 5679, 156 | slug: "the-elder-scrolls-v-skyrim", 157 | name: "The Elder Scrolls V: Skyrim", 158 | added: 15025, 159 | }, 160 | { 161 | id: 802, 162 | slug: "borderlands-2", 163 | name: "Borderlands 2", 164 | added: 14339, 165 | }, 166 | { 167 | id: 58175, 168 | slug: "god-of-war-2", 169 | name: "God of War (2018)", 170 | added: 12717, 171 | }, 172 | { 173 | id: 3070, 174 | slug: "fallout-4", 175 | name: "Fallout 4", 176 | added: 12679, 177 | }, 178 | { 179 | id: 278, 180 | slug: "horizon-zero-dawn", 181 | name: "Horizon Zero Dawn", 182 | added: 12022, 183 | }, 184 | ], 185 | }, 186 | { 187 | id: 10, 188 | name: "Strategy", 189 | slug: "strategy", 190 | games_count: 53203, 191 | image_background: 192 | "https://media.rawg.io/media/games/d4b/d4bcd78873edd9992d93aff9cc8db0c8.jpg", 193 | games: [ 194 | { 195 | id: 13633, 196 | slug: "civilization-v", 197 | name: "Sid Meier's Civilization V", 198 | added: 8848, 199 | }, 200 | { 201 | id: 10243, 202 | slug: "company-of-heroes-2", 203 | name: "Company of Heroes 2", 204 | added: 8791, 205 | }, 206 | { 207 | id: 13910, 208 | slug: "xcom-enemy-unknown", 209 | name: "XCOM: Enemy Unknown", 210 | added: 7845, 211 | }, 212 | { 213 | id: 5525, 214 | slug: "brutal-legend", 215 | name: "Brutal Legend", 216 | added: 7768, 217 | }, 218 | { 219 | id: 10065, 220 | slug: "cities-skylines", 221 | name: "Cities: Skylines", 222 | added: 7708, 223 | }, 224 | { 225 | id: 11147, 226 | slug: "ark-survival-of-the-fittest", 227 | name: "ARK: Survival Of The Fittest", 228 | added: 7519, 229 | }, 230 | ], 231 | }, 232 | { 233 | id: 2, 234 | name: "Shooter", 235 | slug: "shooter", 236 | games_count: 59341, 237 | image_background: 238 | "https://media.rawg.io/media/games/157/15742f2f67eacff546738e1ab5c19d20.jpg", 239 | games: [ 240 | { 241 | id: 4200, 242 | slug: "portal-2", 243 | name: "Portal 2", 244 | added: 18043, 245 | }, 246 | { 247 | id: 4291, 248 | slug: "counter-strike-global-offensive", 249 | name: "Counter-Strike: Global Offensive", 250 | added: 15702, 251 | }, 252 | { 253 | id: 12020, 254 | slug: "left-4-dead-2", 255 | name: "Left 4 Dead 2", 256 | added: 15276, 257 | }, 258 | { 259 | id: 4062, 260 | slug: "bioshock-infinite", 261 | name: "BioShock Infinite", 262 | added: 14503, 263 | }, 264 | { 265 | id: 802, 266 | slug: "borderlands-2", 267 | name: "Borderlands 2", 268 | added: 14339, 269 | }, 270 | { 271 | id: 13537, 272 | slug: "half-life-2", 273 | name: "Half-Life 2", 274 | added: 13634, 275 | }, 276 | ], 277 | }, 278 | { 279 | id: 40, 280 | name: "Casual", 281 | slug: "casual", 282 | games_count: 46046, 283 | image_background: 284 | "https://media.rawg.io/media/games/3ef/3eff92562640e452d3487c04ba6d7fae.jpg", 285 | games: [ 286 | { 287 | id: 9721, 288 | slug: "garrys-mod", 289 | name: "Garry's Mod", 290 | added: 9088, 291 | }, 292 | { 293 | id: 326292, 294 | slug: "fall-guys", 295 | name: "Fall Guys: Ultimate Knockout", 296 | added: 8033, 297 | }, 298 | { 299 | id: 9830, 300 | slug: "brawlhalla", 301 | name: "Brawlhalla", 302 | added: 6972, 303 | }, 304 | { 305 | id: 356714, 306 | slug: "among-us", 307 | name: "Among Us", 308 | added: 6624, 309 | }, 310 | { 311 | id: 1959, 312 | slug: "goat-simulator", 313 | name: "Goat Simulator", 314 | added: 5963, 315 | }, 316 | { 317 | id: 16343, 318 | slug: "a-story-about-my-uncle", 319 | name: "A Story About My Uncle", 320 | added: 5598, 321 | }, 322 | ], 323 | }, 324 | { 325 | id: 14, 326 | name: "Simulation", 327 | slug: "simulation", 328 | games_count: 66313, 329 | image_background: 330 | "https://media.rawg.io/media/games/1f5/1f5ddf7199f2778ff83663b93b5cb330.jpg", 331 | games: [ 332 | { 333 | id: 10035, 334 | slug: "hitman", 335 | name: "Hitman", 336 | added: 10026, 337 | }, 338 | { 339 | id: 654, 340 | slug: "stardew-valley", 341 | name: "Stardew Valley", 342 | added: 9206, 343 | }, 344 | { 345 | id: 9721, 346 | slug: "garrys-mod", 347 | name: "Garry's Mod", 348 | added: 9088, 349 | }, 350 | { 351 | id: 10243, 352 | slug: "company-of-heroes-2", 353 | name: "Company of Heroes 2", 354 | added: 8791, 355 | }, 356 | { 357 | id: 9882, 358 | slug: "dont-starve-together", 359 | name: "Don't Starve Together", 360 | added: 8538, 361 | }, 362 | { 363 | id: 22509, 364 | slug: "minecraft", 365 | name: "Minecraft", 366 | added: 7845, 367 | }, 368 | ], 369 | }, 370 | { 371 | id: 7, 372 | name: "Puzzle", 373 | slug: "puzzle", 374 | games_count: 97139, 375 | image_background: 376 | "https://media.rawg.io/media/games/2e1/2e187b31e5cee21c110bd16798d75fab.jpg", 377 | games: [ 378 | { 379 | id: 4200, 380 | slug: "portal-2", 381 | name: "Portal 2", 382 | added: 18043, 383 | }, 384 | { 385 | id: 13536, 386 | slug: "portal", 387 | name: "Portal", 388 | added: 15326, 389 | }, 390 | { 391 | id: 1030, 392 | slug: "limbo", 393 | name: "Limbo", 394 | added: 12782, 395 | }, 396 | { 397 | id: 19709, 398 | slug: "half-life-2-episode-two", 399 | name: "Half-Life 2: Episode Two", 400 | added: 10177, 401 | }, 402 | { 403 | id: 18080, 404 | slug: "half-life", 405 | name: "Half-Life", 406 | added: 9391, 407 | }, 408 | { 409 | id: 1450, 410 | slug: "inside", 411 | name: "INSIDE", 412 | added: 7452, 413 | }, 414 | ], 415 | }, 416 | { 417 | id: 11, 418 | name: "Arcade", 419 | slug: "arcade", 420 | games_count: 22574, 421 | image_background: 422 | "https://media.rawg.io/media/games/9fa/9fa63622543e5d4f6d99aa9d73b043de.jpg", 423 | games: [ 424 | { 425 | id: 3612, 426 | slug: "hotline-miami", 427 | name: "Hotline Miami", 428 | added: 9809, 429 | }, 430 | { 431 | id: 17540, 432 | slug: "injustice-gods-among-us-ultimate-edition", 433 | name: "Injustice: Gods Among Us Ultimate Edition", 434 | added: 8962, 435 | }, 436 | { 437 | id: 22509, 438 | slug: "minecraft", 439 | name: "Minecraft", 440 | added: 7845, 441 | }, 442 | { 443 | id: 4003, 444 | slug: "grid-2", 445 | name: "GRID 2", 446 | added: 7061, 447 | }, 448 | { 449 | id: 3408, 450 | slug: "hotline-miami-2-wrong-number", 451 | name: "Hotline Miami 2: Wrong Number", 452 | added: 5755, 453 | }, 454 | { 455 | id: 16343, 456 | slug: "a-story-about-my-uncle", 457 | name: "A Story About My Uncle", 458 | added: 5598, 459 | }, 460 | ], 461 | }, 462 | { 463 | id: 83, 464 | name: "Platformer", 465 | slug: "platformer", 466 | games_count: 100625, 467 | image_background: 468 | "https://media.rawg.io/media/games/85c/85c8ae70e7cdf0105f06ef6bdce63b8b.jpg", 469 | games: [ 470 | { 471 | id: 1030, 472 | slug: "limbo", 473 | name: "Limbo", 474 | added: 12782, 475 | }, 476 | { 477 | id: 422, 478 | slug: "terraria", 479 | name: "Terraria", 480 | added: 11662, 481 | }, 482 | { 483 | id: 9767, 484 | slug: "hollow-knight", 485 | name: "Hollow Knight", 486 | added: 10109, 487 | }, 488 | { 489 | id: 41, 490 | slug: "little-nightmares", 491 | name: "Little Nightmares", 492 | added: 10050, 493 | }, 494 | { 495 | id: 18080, 496 | slug: "half-life", 497 | name: "Half-Life", 498 | added: 9391, 499 | }, 500 | { 501 | id: 3144, 502 | slug: "super-meat-boy", 503 | name: "Super Meat Boy", 504 | added: 8903, 505 | }, 506 | ], 507 | }, 508 | { 509 | id: 59, 510 | name: "Massively Multiplayer", 511 | slug: "massively-multiplayer", 512 | games_count: 3302, 513 | image_background: 514 | "https://media.rawg.io/media/screenshots/848/848253347dc93c762bfd51c7e4989b8f.jpg", 515 | games: [ 516 | { 517 | id: 32, 518 | slug: "destiny-2", 519 | name: "Destiny 2", 520 | added: 12776, 521 | }, 522 | { 523 | id: 10213, 524 | slug: "dota-2", 525 | name: "Dota 2", 526 | added: 11585, 527 | }, 528 | { 529 | id: 766, 530 | slug: "warframe", 531 | name: "Warframe", 532 | added: 11483, 533 | }, 534 | { 535 | id: 290856, 536 | slug: "apex-legends", 537 | name: "Apex Legends", 538 | added: 10281, 539 | }, 540 | { 541 | id: 10533, 542 | slug: "path-of-exile", 543 | name: "Path of Exile", 544 | added: 9243, 545 | }, 546 | { 547 | id: 10142, 548 | slug: "playerunknowns-battlegrounds", 549 | name: "PlayerUnknown’s Battlegrounds", 550 | added: 9104, 551 | }, 552 | ], 553 | }, 554 | { 555 | id: 1, 556 | name: "Racing", 557 | slug: "racing", 558 | games_count: 24106, 559 | image_background: 560 | "https://media.rawg.io/media/games/640/6409857596fe6553d3bb5af9a17f6160.jpg", 561 | games: [ 562 | { 563 | id: 3272, 564 | slug: "rocket-league", 565 | name: "Rocket League", 566 | added: 11707, 567 | }, 568 | { 569 | id: 4003, 570 | slug: "grid-2", 571 | name: "GRID 2", 572 | added: 7061, 573 | }, 574 | { 575 | id: 2572, 576 | slug: "dirt-rally", 577 | name: "DiRT Rally", 578 | added: 6355, 579 | }, 580 | { 581 | id: 58753, 582 | slug: "forza-horizon-4", 583 | name: "Forza Horizon 4", 584 | added: 5687, 585 | }, 586 | { 587 | id: 5578, 588 | slug: "grid", 589 | name: "Race Driver: Grid", 590 | added: 5152, 591 | }, 592 | { 593 | id: 19491, 594 | slug: "burnout-paradise-the-ultimate-box", 595 | name: "Burnout Paradise: The Ultimate Box", 596 | added: 4458, 597 | }, 598 | ], 599 | }, 600 | { 601 | id: 15, 602 | name: "Sports", 603 | slug: "sports", 604 | games_count: 20709, 605 | image_background: 606 | "https://media.rawg.io/media/games/b59/b59560a7277b16b53e4786b4abe45baa.jpg", 607 | games: [ 608 | { 609 | id: 3272, 610 | slug: "rocket-league", 611 | name: "Rocket League", 612 | added: 11707, 613 | }, 614 | { 615 | id: 326292, 616 | slug: "fall-guys", 617 | name: "Fall Guys: Ultimate Knockout", 618 | added: 8033, 619 | }, 620 | { 621 | id: 2572, 622 | slug: "dirt-rally", 623 | name: "DiRT Rally", 624 | added: 6355, 625 | }, 626 | { 627 | id: 53341, 628 | slug: "jet-set-radio-2012", 629 | name: "Jet Set Radio", 630 | added: 4887, 631 | }, 632 | { 633 | id: 9575, 634 | slug: "vrchat", 635 | name: "VRChat", 636 | added: 4240, 637 | }, 638 | { 639 | id: 622492, 640 | slug: "forza-horizon-5", 641 | name: "Forza Horizon 5", 642 | added: 4183, 643 | }, 644 | ], 645 | }, 646 | { 647 | id: 6, 648 | name: "Fighting", 649 | slug: "fighting", 650 | games_count: 11701, 651 | image_background: 652 | "https://media.rawg.io/media/games/2c8/2c89e43515ed12aee51becc3dcfd8e7e.jpg", 653 | games: [ 654 | { 655 | id: 17540, 656 | slug: "injustice-gods-among-us-ultimate-edition", 657 | name: "Injustice: Gods Among Us Ultimate Edition", 658 | added: 8962, 659 | }, 660 | { 661 | id: 108, 662 | slug: "mortal-kombat-x", 663 | name: "Mortal Kombat X", 664 | added: 8269, 665 | }, 666 | { 667 | id: 28179, 668 | slug: "sega-mega-drive-and-genesis-classics", 669 | name: "SEGA Mega Drive and Genesis Classics", 670 | added: 7605, 671 | }, 672 | { 673 | id: 9830, 674 | slug: "brawlhalla", 675 | name: "Brawlhalla", 676 | added: 6972, 677 | }, 678 | { 679 | id: 274480, 680 | slug: "mortal-kombat-11", 681 | name: "Mortal Kombat 11", 682 | added: 4995, 683 | }, 684 | { 685 | id: 44525, 686 | slug: "yakuza-kiwami", 687 | name: "Yakuza Kiwami", 688 | added: 4236, 689 | }, 690 | ], 691 | }, 692 | { 693 | id: 19, 694 | name: "Family", 695 | slug: "family", 696 | games_count: 5390, 697 | image_background: 698 | "https://media.rawg.io/media/games/89a/89a700d3c6a76bd0610ca89ccd20da54.jpg", 699 | games: [ 700 | { 701 | id: 3254, 702 | slug: "journey", 703 | name: "Journey", 704 | added: 8029, 705 | }, 706 | { 707 | id: 2597, 708 | slug: "lego-lord-of-the-rings", 709 | name: "LEGO The Lord of the Rings", 710 | added: 5178, 711 | }, 712 | { 713 | id: 3350, 714 | slug: "broken-age", 715 | name: "Broken Age", 716 | added: 4785, 717 | }, 718 | { 719 | id: 3729, 720 | slug: "lego-the-hobbit", 721 | name: "LEGO The Hobbit", 722 | added: 4749, 723 | }, 724 | { 725 | id: 1259, 726 | slug: "machinarium", 727 | name: "Machinarium", 728 | added: 4268, 729 | }, 730 | { 731 | id: 1140, 732 | slug: "world-of-goo", 733 | name: "World of Goo", 734 | added: 4202, 735 | }, 736 | ], 737 | }, 738 | { 739 | id: 28, 740 | name: "Board Games", 741 | slug: "board-games", 742 | games_count: 8326, 743 | image_background: 744 | "https://media.rawg.io/media/screenshots/220/22073cc438f0fb97967a1b53fd58c920.jpg", 745 | games: [ 746 | { 747 | id: 23557, 748 | slug: "gwent-the-witcher-card-game", 749 | name: "Gwent: The Witcher Card Game", 750 | added: 4471, 751 | }, 752 | { 753 | id: 327999, 754 | slug: "dota-underlords", 755 | name: "Dota Underlords", 756 | added: 3760, 757 | }, 758 | { 759 | id: 2055, 760 | slug: "adventure-capitalist", 761 | name: "AdVenture Capitalist", 762 | added: 3149, 763 | }, 764 | { 765 | id: 758, 766 | slug: "hue", 767 | name: "Hue", 768 | added: 2103, 769 | }, 770 | { 771 | id: 2306, 772 | slug: "poker-night-2", 773 | name: "Poker Night 2", 774 | added: 1980, 775 | }, 776 | { 777 | id: 3187, 778 | slug: "armello", 779 | name: "Armello", 780 | added: 1889, 781 | }, 782 | ], 783 | }, 784 | { 785 | id: 34, 786 | name: "Educational", 787 | slug: "educational", 788 | games_count: 15650, 789 | image_background: 790 | "https://media.rawg.io/media/screenshots/312/3123c2b86ff947d3b6a846c771817e06.jpeg", 791 | games: [ 792 | { 793 | id: 1358, 794 | slug: "papers-please", 795 | name: "Papers, Please", 796 | added: 6391, 797 | }, 798 | { 799 | id: 1140, 800 | slug: "world-of-goo", 801 | name: "World of Goo", 802 | added: 4202, 803 | }, 804 | { 805 | id: 2778, 806 | slug: "surgeon-simulator-cpr", 807 | name: "Surgeon Simulator", 808 | added: 3705, 809 | }, 810 | { 811 | id: 9768, 812 | slug: "gameguru", 813 | name: "GameGuru", 814 | added: 2388, 815 | }, 816 | { 817 | id: 13777, 818 | slug: "sid-meiers-civilization-iv-colonization", 819 | name: "Sid Meier's Civilization IV: Colonization", 820 | added: 2189, 821 | }, 822 | { 823 | id: 6885, 824 | slug: "pirates-3", 825 | name: "Sid Meier's Pirates!", 826 | added: 2108, 827 | }, 828 | ], 829 | }, 830 | { 831 | id: 17, 832 | name: "Card", 833 | slug: "card", 834 | games_count: 4507, 835 | image_background: 836 | "https://media.rawg.io/media/games/891/8916b6d4d41c5931d1d5b6b1f525da7b.jpg", 837 | games: [ 838 | { 839 | id: 23557, 840 | slug: "gwent-the-witcher-card-game", 841 | name: "Gwent: The Witcher Card Game", 842 | added: 4471, 843 | }, 844 | { 845 | id: 28121, 846 | slug: "slay-the-spire", 847 | name: "Slay the Spire", 848 | added: 4470, 849 | }, 850 | { 851 | id: 18852, 852 | slug: "poker-night-at-the-inventory", 853 | name: "Poker Night at the Inventory", 854 | added: 2629, 855 | }, 856 | { 857 | id: 8923, 858 | slug: "faeria", 859 | name: "Faeria", 860 | added: 2066, 861 | }, 862 | { 863 | id: 332, 864 | slug: "the-elder-scrolls-legends", 865 | name: "The Elder Scrolls: Legends", 866 | added: 2021, 867 | }, 868 | { 869 | id: 2306, 870 | slug: "poker-night-2", 871 | name: "Poker Night 2", 872 | added: 1980, 873 | }, 874 | ], 875 | }, 876 | ]; 877 | --------------------------------------------------------------------------------