Green Tree Near Green Plants and Moss
Evan Stern
Evan Stern
July 10, 20235 min
Table of Contents

In this tutorial, we'll be building out the game details view for our games app. As before, we'll be using the Remix framework to build out our app. If you haven't already, I recommend reading the first article in this series and starting from there.

What Are We Building?

Here's an overview of what we're about to build:

  • A game details view that shows the game's title, description, cover image, release date, platforms, and a few screenshots.
  • The ability to click on a game from the search list and be taken to the game details view.

First, Let's Get The Data.

The first thing we will do is make sure we can access the data! We'll be using the RAWG API to get our data. We'll be using the same API key we used in the previous tutorial.

We'll need to make two API calls to get the data we need. The first call will be to get the game details, and the second will be to get the game screenshots. We'll be using the getGameDetails and getGameScreenshots functions from the ~/app/modules/games/service.server.tsx file.

// ~/app/modules/games/service.server.tsx
export async function getGameDetails(gameId: number): Promise<GameDetails> {
const response = await fetch(
`https://api.rawg.io/api/games/${gameId}?key=${process.env.RAWG_API_KEY}`,
{
method: 'GET',
}
);
const json = (await response.json()) as GameDetails;
return json;
}
export async function getGameScreenshots(gameId: number): Promise<string[]> {
const response = await fetch(
`https://api.rawg.io/api/games/${gameId}/screenshots?key=${process.env.RAWG_API_KEY}`,
{
method: 'GET',
}
);
const json = (await response.json()) as {
results: {
id: number;
image: string;
}[];
};
return json.results.map((screenshot) => screenshot.image);
}

We'll use these functions to gather the details and screenshots for the game we display when the user clicks on a game from the search list.

Let's Build The Game Details View

Now that we have the data, let's build out the game details view. We'll need to create a new route at ~/app/routes/games.$gameId.tsx which will let us resolve routes like /games/1 or /games/2 and so on.

// ~/app/routes/games.$gameId.tsx
import { json, type LoaderArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getGameDetails, getGameScreenshots } from '~/modules/games';
export const loader = async ({ request, params }: LoaderArgs) => {
const { gameId } = params;
let gameDetails;
let screenshots: string[] = [];
if (gameId) {
gameDetails = await getGameDetails(Number.parseInt(gameId, 10));
screenshots = await getGameScreenshots(Number.parseInt(gameId, 10));
}
return json({
gameDetails,
screenshots,
});
};
export default function GameDetailsPage() {
const { gameDetails, screenshots } = useLoaderData<typeof loader>() || {};
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<div className="flex flex-1 flex-col">
<div className="mb-6">
<img
className="h-96 w-full sm:mx-auto sm:w-auto"
src={gameDetails?.background_image}
alt={gameDetails?.name}
/>
</div>
<h1 className="mb-6 text-center text-3xl font-bold sm:text-6xl">
{gameDetails?.name}
</h1>
<h4 className="mb-6 text-center text-sm text-slate-600">
Released {dateFormatter.format(new Date(gameDetails?.released || ''))}
</h4>
<h2 className="mb-6 text-center text-2xl font-bold sm:text-4xl">
Platforms
</h2>
<div className="mb-6 flex flex-wrap items-center justify-around">
{gameDetails?.platforms.map((platform) => (
<div
key={platform.platform.id}
className="mb-2 rounded-full bg-slate-200 px-2 py-1 font-bold"
>
<h4 className="text-center text-sm text-slate-600">
{platform.platform.name}
</h4>
</div>
))}
</div>
<h2 className="mb-6 text-center text-2xl font-bold sm:text-4xl">
Description
</h2>
<div
className="mb-6 px-4 text-lg"
dangerouslySetInnerHTML={{ __html: gameDetails?.description || '' }}
/>
<h2 className="mb-6 text-center text-2xl font-bold sm:text-4xl">
Screenshots
</h2>
{screenshots.length > 0 ? (
<div className="mb-6 flex flex-wrap items-center justify-around">
{screenshots.map((screenshot) => (
<div
key={screenshot}
className="mb-2 overflow-hidden rounded-lg border border-slate-400"
>
<img
className="h-60 w-full"
src={screenshot}
alt={gameDetails?.name}
/>
</div>
))}
</div>
) : (
<div className="mb-6 flex h-60 w-full items-center justify-center border border-slate-400">
<h3 className="text-center text-xl font-bold text-slate-600">
No screenshots available
</h3>
</div>
)}
</div>
);
}

There are a few things to note here:

  • We're gathering the game details and screenshots in the loader function.
  • We're using the useLoaderData hook to get the data we returned from the loader function.
  • We're using the dangerouslySetInnerHTML prop to render the game description. This is because the description is returned as HTML from the API.
  • We're using the Intl.DateTimeFormat class to format the release date.

Now that we have the game details view, let's add a link to it from the game search list. We'll do this by adding a Link component to GameCard component.

// ~/app/game/components/game-card/GameCard.tsx
import { Link } from '@remix-run/react';
import type { Game } from '../../types';
interface GameCardProps {
game: Game;
}
export const GameCard: React.FC<GameCardProps> = ({ game }) => {
return (
<Link
to={`/games/${game.id}`}
className="mb-8 flex flex-col gap-4 overflow-hidden rounded-lg border border-slate-400 lg:flex-row"
>
<div className="flex-none lg:w-96">
{game.background_image ? (
<img
className="h-full w-full"
src={game.background_image}
alt={game.name}
/>
) : (
<div className="flex h-60 w-full items-center justify-center border-b border-slate-400 lg:border-b-0 lg:border-r">
<h3 className="text-center text-xl font-bold text-slate-600">
No image available
</h3>
</div>
)}
</div>
<div className="flex-1 px-4 pb-4 lg:px-0 lg:py-4">
<h3 className="text-xl font-bold">{game.name}</h3>
</div>
</Link>
);
};

Conclusion

That's it for this article. You can now click on a game in the search list and see the game details. In the next article, we'll add some OptimisticUI to the various routes!

(Update: 1/26/2024 - I've decided to skip the OptimisticUI article for now. I may come back to it in the future.)

You can view the code for this post here

You can see the final project here



Read Next
Macro Photography of Dragonfly on Plant
Evan Stern
Evan Stern
January 26, 2023

The Remix React Framework, an open-source framework released late in 2021, is winning fans with its minimalist approach and impressive power. It's simple to…

Photo of the outdoors
Evan Stern
Evan Stern
February 02, 2023

GraphQL is a modern and flexible data query language that was developed by Facebook. It provides an efficient and powerful alternative to traditional REST APIs…

Green grass field with pine trees
Evan Stern
Evan Stern
July 09, 2023

Ben likes to play video games. He wants to be able to search for games and add them to his list of games. We'll need to add a search bar to our app, and then…


MachineServant
MachineServant