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 ================================================ GameHub
================================================ 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({} as GameQuery); return ( setGameQuery({ ...gameQuery, searchText })} /> setGameQuery({ ...gameQuery, genre})} /> setGameQuery({ ...gameQuery, platform}) } /> setGameQuery({ ...gameQuery, sortOrder })} /> ); } export default App; ================================================ FILE: src/components/ColorModeSwitch.tsx ================================================ import { HStack, Switch, Text, useColorMode } from '@chakra-ui/react' const ColorModeSwitch = () => { const {toggleColorMode, colorMode} = useColorMode(); return ( Dark Mode ) } 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 ( {score} ) } 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 ( ) } 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 ( p.platform)} /> {game.name} ) } 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 ( {children} ); }; export default GameCardContainer; ================================================ FILE: src/components/GameCardSkeleton.tsx ================================================ import { Card, CardBody, Skeleton, SkeletonText } from '@chakra-ui/react' const GameCardSkeleton = () => { return ( ) } 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 {error}; return ( {isLoading && skeletons.map((skeleton) => ( ))} {data.map((game) => ( ))} ); }; 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} ) } 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 ; return ( <> Genres {data.map((genre) => ( ))} ); }; 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 ( ) } 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 ( {platforms.map((platform) => ( ))} ); }; 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 ( }> {selectedPlatform?.name || 'Platforms'} {data.map(platform => onSelectPlatform(platform)} key={platform.id}>{platform.name})} ); }; 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(null); return (
{ event.preventDefault(); if (ref.current) onSearch(ref.current.value); }}> } />
); }; 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 ( }> Order by: {currentSortOrder?.label || 'Relevance'} {sortOrders.map((order) => ( onSelectSortOrder(order.value)} key={order.value} value={order.value}> {order.label} ))} ); }; 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 { count: number; results: T[]; } const useData = (endpoint: string, requestConfig?: AxiosRequestConfig, deps?: any[]) => { const [data, setData] = useState([]); const [error, setError] = useState(""); const [isLoading, setLoading] = useState(false); useEffect(() => { const controller = new AbortController(); setLoading(true); apiClient .get>(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( "/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( , ) ================================================ 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 ================================================ /// ================================================ 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()], })