Table of Contents
Setting up the project
npm create vite@4.1.0
✔ Project name: … game-hub
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /Users/shazdeh/Downloads/React-js/game-hub...
Done. Now run:
cd game-hub
npm install
npm run dev
cd game-hub
npm i
added 77 packages, and audited 78 packages in 7s
7 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
npm run dev
git init
git add .
git commit -m "Initial commit"
Installing Charka UI
- Go to the Chakra UI website,
- Click on the Get Started button.
- Select Vite on the bottom of the page from the Framework Guide section.
- Follow the instructions on the page.
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
);
App.tsx
import { Button, ButtonGroup } from "@chakra-ui/react";
function App() {
return ;
}
export default App;
To remove the ugly border around the button, clear the index.css default contents for Vite
To check the commits use the following command.
git log --oneline
Creating a responsive layout
Using the Grid System of Chakra UI to set the layout of the page
import { Grid, GridItem } from "@chakra-ui/react";
function App() {
return (
{/* to add more than one string we use back ticks */}
Nav
Aside
Main
);
}
export default App;
To make the layout responsive do the following changes in the code:
import { Grid, GridItem, Show } from "@chakra-ui/react";
function App() {
return (
Nav
Aside
Main
);
}
export default App;
Building the navigation bar
App.tsx
import { Grid, GridItem, Show } from "@chakra-ui/react";
import NavBar from "./components/NavBar";
function App() {
return (
Aside
Main
);
}
export default App;
src/components/NavBar.tsx
import { HStack, Image, Text } from "@chakra-ui/react";
import logo from "../assets/logo.webp";
const NavBar = () => {
return (
NavBar
);
};
export default NavBar;
Implementing the dark mode
Follow the Chakra color mode instruction from the following link:
src/theme.ts
import { extendTheme, ThemeConfig } from "@chakra-ui/react";
const config: ThemeConfig = {
initialColorMode: 'dark'
}
const theme = extendTheme({ config });
export default theme;
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(
);
After implementing the dark mode, you might not see the changes. For that, check the local storage of your browser and see the value of chakra-ui-color-mode and delete it, then refresh the page.
You can find it in your developer mode of your browser that are different in each browser:
Building the color mode switch
components/ColorModeSwitch.tsx
import { HStack, Switch, Text, useColorMode } from "@chakra-ui/react";
const ColorModeSwitch = () => {
const { toggleColorMode, colorMode } = useColorMode();
return (
Dark mode
);
};
export default ColorModeSwitch;
components/NavBar.tsx
import { HStack, Image } from "@chakra-ui/react";
import logo from "../assets/logo.webp";
import ColorModeSwitch from "./ColorModeSwitch";
const NavBar = () => {
return (
);
};
export default NavBar;
Fetching the Games
Go to RAWG website and get your API key after signing up. And copy the provided API key.
Install Axios (Axios is a promise-based HTTP Client for node.js
and the browser.)
npm i axios
services/api-client.ts
import axios from "axios";
// Create an Axios instance with a custom configuration
export default axios.create({
baseURL: 'https://api.rawg.io/api',
params: {
// The key will be included in the query string of every HTTP request we send to our backend.
key: '4a0759f4sdjghaa1bd7skdhgb8893a1f'
}
})
Copy the url of the baseURL form the following link:
component/GameGrid.tsx
import React, { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { Text } from "@chakra-ui/react";
interface Game {
id: number;
name: string;
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const GameGrid = () => {
// State a variable for storing our game objects.
const [games, setGames] = useState([]);
// State a variable for error messages.
const [error, setError] = useState("");
// Effect hook to send a fetch request to the backend
useEffect(() => {
apiClient
.get("/gamess")
.then((res) => setGames(res.data.results))
.catch((err) => setError(err.message));
});
return (
<>
{error && {error} }
{games.map((game) => (
- {game.name}
))}
>
);
};
export default GameGrid;
App.tsx
import { Button, ButtonGroup, Show } from "@chakra-ui/react";
import { Grid, GridItem } from "@chakra-ui/react";
import NavBar from "./components/NavBar";
import GameGrid from "./components/GameGrid";
function App() {
return (
ASIDE
);
}
export default App;
Custom Hook for Fetching Games
Components are responsible for returning markup and handling user interactions at a high level.
Here we have two options.
- One way is to move the logic for making HTTP requests inside a service as we did in the previous section.
- The other option is to move the entire logic meaning the state variables and the effect inside a hook.
So hooks are not necessarily for sharing functionality across different components, We can also use them to separate concerns and making our codes more modular and reusable.
src/hooks/useGames.ts
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
interface Game {
id: number;
name: string;
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const useGames = () => {
const [games, setGames] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
apiClient
.get("/games", { signal: controller.signal })
.then((res) => setGames(res.data.results))
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
});
return () => controller.abort();
}, []);
return { games, error };
};
export default useGames;
component/GameGrid.tsx
import { Text } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
const GameGrid = () => {
const { games, error } = useGames();
return (
<>
{error && {error} }
{games.map((game) => (
- {game.name}
))}
>
);
};
export default GameGrid;
Building Game Cards
component/GameCard.tsx
import React from "react";
import { Game } from "../hooks/useGames";
import { Card, CardBody, Heading, Image } from "@chakra-ui/react";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
);
};
export default GameCard;
src/hooks/useGames.ts
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
export interface Game {
id: number;
name: string;
background_image: string;
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const useGames = () => {
const [games, setGames] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
apiClient
.get("/games", { signal: controller.signal })
.then((res) => setGames(res.data.results))
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
});
return () => controller.abort();
// An array of dependencies and the effect hook. Without this, we constantly send a request to our backend.
}, []);
return { games, error };
};
export default useGames;
component/GameGrid.tsx
import { SimpleGrid, Text } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
import GameCard from "./GameCard";
const GameGrid = () => {
const { games, error } = useGames();
return (
<>
{error && {error} }
{games.map((game) => (
))}
>
);
};
export default GameGrid;
Displaying platform icons
npm i react-icons@4.7.1
component/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, Text } from "@chakra-ui/react";
import { Platform } from "../hooks/useGames";
import { IconType } from "react-icons";
interface Props {
platforms: Platform[];
}
const PlatformIconList = ({ platforms }: Props) => {
// Add index signature: [key: string]
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) => (
// {platform.name}
))}
);
};
export default PlatformIconList;
component/GameCard.tsx
import React from "react";
import { Game, Platform } from "../hooks/useGames";
import { Card, CardBody, Heading, Image, Text } from "@chakra-ui/react";
import PlatformIconList from "./PlatformIconList";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
{/* Solution 01 */}
{/* {game.parent_platforms.map((platform) => (
{platform.platform.name}
))} */}
{/* Solution 02 */}
{/* {game.parent_platforms.map(({ platform }) => (
{platform.name}
))} */}
p.platform)}
/>
);
};
export default GameCard;
src/hooks/useGames.ts
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
export interface Platform {
id: number;
name: string;
slug: string;
}
export interface Game {
id: number;
name: string;
background_image: string;
parent_platforms: { platform: Platform }[];
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const useGames = () => {
const [games, setGames] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
apiClient
.get("/games", { signal: controller.signal })
.then((res) => setGames(res.data.results))
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
});
return () => controller.abort();
// An array of dependencies and the effect hook. Without this, we constantly send a request to our backend.
}, []);
return { games, error };
};
export default useGames;
Displaying Critic Score
component/CriticScore.tsx
import { Badge } from "@chakra-ui/react";
interface Props {
score: number;
}
const CriticScore = ({ score }: Props) => {
let color = score > 90 ? "green" : score > 80 ? "orange" : "";
return (
{score}
);
};
export default CriticScore;
component/GameCard.tsx
import { Game, Platform } from "../hooks/useGames";
import { Card, CardBody, HStack, Heading, Image, Text } from "@chakra-ui/react";
import PlatformIconList from "./PlatformIconList";
import CriticScore from "./CriticScore";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
p.platform)}
/>
);
};
export default GameCard;
hooks/useGames.ts
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
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;
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const useGames = () => {
const [games, setGames] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
apiClient
.get("/games", { signal: controller.signal })
.then((res) => setGames(res.data.results))
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
});
return () => controller.abort();
// An array of dependencies and the effect hook. Without this, we constantly send a request to our backend.
}, []);
return { games, error };
};
export default useGames;
Getting optimized images
By adding crop/600/400/ after media you can get a cropped version of the images in the size of 600×400
services/image-url.ts
const getCroppedImageUrl = (url: string) => {
const target = "media/";
const index = url.indexOf(target) + target.length;
return url.slice(0, index) + "crop/600/400/" + url.slice(index);
};
export default getCroppedImageUrl;
component/GameCard.tsx
import { Game, Platform } from "../hooks/useGames";
import { Card, CardBody, HStack, Heading, Image, Text } from "@chakra-ui/react";
import PlatformIconList from "./PlatformIconList";
import CriticScore from "./CriticScore";
import getCroppedImageUrl from "../services/image-url";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
p.platform)}
/>
);
};
export default GameCard;
Loading Skeletons for UX improvement
hooks/UseGames.ts
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
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;
}
interface FetchGamesResponse {
count: number;
results: Game[];
}
const useGames = () => {
const [games, setGames] = useState([]);
const [error, setError] = useState("");
const [isLoading, setLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
apiClient
.get("/games", { signal: controller.signal })
.then((res) => {
setGames(res.data.results);
setLoading(false);
})
.catch((err) => {
if (err instanceof CanceledError) return;
setError(err.message);
setLoading(false);
});
return () => controller.abort();
// An array of dependencies and the effect hook. Without this, we constantly send a request to our backend.
}, []);
return { games, error, isLoading };
};
export default useGames;
component/GameCardSkeleton.tsx
import { Card, CardBody, Skeleton, SkeletonText } from "@chakra-ui/react";
const GameCardSkeleton = () => {
return (
);
};
export default GameCardSkeleton;
component/GameCard.tsx
import { Game, Platform } from "../hooks/useGames";
import { Card, CardBody, HStack, Heading, Image, Text } from "@chakra-ui/react";
import PlatformIconList from "./PlatformIconList";
import CriticScore from "./CriticScore";
import getCroppedImageUrl from "../services/image-url";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
p.platform)}
/>
);
};
export default GameCard;
component/GameGrid.tsx
import { SimpleGrid, Skeleton, Text } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
import GameCard from "./GameCard";
import GameCardSkeleton from "./GameCardSkeleton";
const GameGrid = () => {
const { games, error, isLoading } = useGames();
const skeletons = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];
return (
<>
{error && {error} }
{isLoading &&
skeletons.map((skeleton) => )}
{games.map((game) => (
))}
>
);
};
export default GameGrid;
Refactor - Removing Duplicated Styles
component/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;
component/GameCard.tsx
import { Game, Platform } from "../hooks/useGames";
import { Card, CardBody, HStack, Heading, Image, Text } from "@chakra-ui/react";
import PlatformIconList from "./PlatformIconList";
import CriticScore from "./CriticScore";
import getCroppedImageUrl from "../services/image-url";
interface Props {
game: Game;
}
const GameCard = ({ game }: Props) => {
return (
{game.name}
p.platform)}
/>
);
};
export default GameCard;
component/GameGrid.tsx
import { SimpleGrid, Skeleton, Text } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
import GameCard from "./GameCard";
import GameCardSkeleton from "./GameCardSkeleton";
import GameCardContainer from "./GameCardContainer";
const GameGrid = () => {
const { games, error, isLoading } = useGames();
const skeletons = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];
return (
<>
{error && {error} }
{isLoading &&
skeletons.map((skeleton) => (
))}
{games.map((game) => (
))}
>
);
};
export default GameGrid;
Troubleshooting:
Resolving EACCES permissions errors when installing packages globally
Correct npm permissions: It appears there may be permission issues with your npm cache directory. You can change the ownership of the npm cache directory to your user:
sudo chown -R $(whoami) ~/.npm
This will change the ownership of the .npm
directory to your user.
Make sure you configure your "user.name" and "user.email" in git
Correct npm permissions: It appears there may be permission issues with your npm cache directory. You can change the ownership of the npm cache directory to your user:
git config --global user.email i***@hoomaan.dev
git config --global user.name H******