├── 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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------