From dff98e61f806f3efcbf40dd80626de735c8f2c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Akg=C3=BCl?= <s86116@bht-berlin.de> Date: Thu, 11 May 2023 20:28:08 +0200 Subject: [PATCH] created lib folder to store fetch requests and utils + created types file + code cleanup, comments --- .env.sample | 10 ++- app/games/Game.tsx | 3 +- app/games/[gameid]/loading.tsx | 1 + app/games/[gameid]/page.tsx | 39 ++++---- app/games/loading.tsx | 1 + app/games/page.tsx | 24 +---- app/layout.tsx | 10 ++- app/page.tsx | 1 + lib/igdb.ts | 70 +++++++++++++++ lib/utils.ts | 6 ++ types/types.ts | 159 +++++++++++++++++++++++++++++++++ 11 files changed, 275 insertions(+), 49 deletions(-) create mode 100644 lib/igdb.ts create mode 100644 lib/utils.ts create mode 100644 types/types.ts diff --git a/.env.sample b/.env.sample index 1c34c9d..6db1037 100644 --- a/.env.sample +++ b/.env.sample @@ -4,7 +4,11 @@ # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. # See the documentation for all the connection string options: https://pris.ly/d/connection-strings -DATABASE_URL="file:./dev.db" +DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" -IMDB_CLIENT_ID="imdb-client-id" -IMDB_AUTH="Bearer imdb-auth-id" \ No newline at end of file +TWITCH_AUTH_BASE_URL="https://id.twitch.tv/oauth2" +IGDB_BASE_URL="https://api.igdb.com/v4" +IGDB_IMG_BASE_URL="https://images.igdb.com/igdb/image/upload" + +TWITCH_CLIENT_ID="imdb_client_id" +TWITCH_CLIENT_SECRET="imdb_auth_id" \ No newline at end of file diff --git a/app/games/Game.tsx b/app/games/Game.tsx index 505c865..03e3bb3 100644 --- a/app/games/Game.tsx +++ b/app/games/Game.tsx @@ -1,12 +1,13 @@ import Image from "next/image"; import Link from "next/link"; +// this is a single game helper-component, only for design purposes export default function Game({ id, name, cover }: { id: any, name: any, cover: any }) { return ( <div> <h1>{name}</h1> <Link href={`/games/${id}`}> - <Image src={"https:" + cover.url} alt={name} width={200} height={200} /> + <Image src={cover.url} alt={name} width={264} height={374} priority={true} /> </Link> </div> ) diff --git a/app/games/[gameid]/loading.tsx b/app/games/[gameid]/loading.tsx index 21df37b..794f6fd 100644 --- a/app/games/[gameid]/loading.tsx +++ b/app/games/[gameid]/loading.tsx @@ -1,3 +1,4 @@ +// loading component, this renders when loading in /games happens export default function Loading() { return ( <div> diff --git a/app/games/[gameid]/page.tsx b/app/games/[gameid]/page.tsx index c385146..d86eb4a 100644 --- a/app/games/[gameid]/page.tsx +++ b/app/games/[gameid]/page.tsx @@ -1,35 +1,26 @@ +import { getGame, getGames } from "@/lib/igdb"; +import { IGame } from "@/types/types"; import Image from "next/image"; -type DetailView = { - id: number; - name: string; - cover: { url: string }; - summary: string; -} - -type DetailViewArray = DetailView[]; - - - -export default async function GameDetail({ params }: { params: any }) { - const res = await fetch("https://api.igdb.com/v4/games", { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Client-ID': `${process.env.IMDB_CLIENT_ID}`, - 'Authorization': `${process.env.IMDB_AUTH}`, - }, - body: `fields name,cover.*,summary; where cover != null; where id = ${params.gameid};`, - }); - const data: DetailViewArray = await res.json() +// renders a single game detail page +export default async function GameDetail({ params }: { params: { gameid: string } }) { + const data: IGame[] = await getGame(parseInt(params.gameid)) return ( <div> Game Detail <h1>{data[0].name}</h1> - <Image src={"https:" + data[0].cover.url} alt={data[0].name} width={200} height={200} priority /> + <Image src={data[0].cover.url} alt={data[0].name} width={264} height={374} priority={true} /> <p>{data[0].summary}</p> </div> ) } + +// pre-renders static paths for all fetched games for faster page loads +export async function generateStaticParams() { + const games = await getGames() + + return games.map((game) => ({ + gameid: game.id.toString(), + })); +} \ No newline at end of file diff --git a/app/games/loading.tsx b/app/games/loading.tsx index 9d1c057..abebd22 100644 --- a/app/games/loading.tsx +++ b/app/games/loading.tsx @@ -1,3 +1,4 @@ +// root loading component, this renders when any loading happens export default function Loading() { return ( <div> diff --git a/app/games/page.tsx b/app/games/page.tsx index 7cdd990..7c6eff8 100644 --- a/app/games/page.tsx +++ b/app/games/page.tsx @@ -1,26 +1,10 @@ +import { getGames } from "@/lib/igdb"; +import { IGame } from "@/types/types"; import Game from "./Game"; -type DetailView = { - id: number; - name: string; - cover: { url: string }; - summary: string; -} - -type DetailViewArray = DetailView[]; - +// renders a list of games export default async function GamesList() { - const res = await fetch("https://api.igdb.com/v4/games", { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Client-ID': `${process.env.IMDB_CLIENT_ID}`, - 'Authorization': `${process.env.IMDB_AUTH}`, - }, - body: `fields name,cover.*; limit 40; where cover != null;`, - }); - const data: DetailViewArray = await res.json() + const data: IGame[] = await getGames() return ( <div> diff --git a/app/layout.tsx b/app/layout.tsx index b7c5cfd..f4f1f87 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,3 +1,6 @@ +"use client" + +import { Container } from '@mui/material' import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'] }) @@ -7,6 +10,7 @@ export const metadata = { description: 'Generated by create next app', } +// this is the root layout for all pages ({children}) export default function RootLayout({ children, }: { @@ -14,7 +18,11 @@ export default function RootLayout({ }) { return ( <html lang="en"> - <body className={inter.className}>{children}</body> + <body className={inter.className}> + <Container> + {children} + </Container> + </body> </html> ) } diff --git a/app/page.tsx b/app/page.tsx index b98f3fa..04c430e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,4 @@ +// renders home page export default function Home() { return ( <main> diff --git a/lib/igdb.ts b/lib/igdb.ts new file mode 100644 index 0000000..972df6a --- /dev/null +++ b/lib/igdb.ts @@ -0,0 +1,70 @@ +import { IAuth, IGame } from "@/types/types" +import { getImageURL } from "./utils" + +const AUTH_BASE_URL = process.env.TWITCH_AUTH_BASE_URL ?? '' +const IGDB_BASE_URL = process.env.IGDB_BASE_URL ?? '' + +const CLIENT_ID = process.env.TWITCH_CLIENT_ID ?? '' +const CLIENT_SECRET = process.env.TWITCH_CLIENT_SECRET ?? '' + +let _auth: IAuth +let _lastUpdate = 0 + +// fetches a new token if the current one is expired +async function getToken(): Promise<IAuth> { + if (!_auth || Date.now() - _lastUpdate > _auth.expires_in) { + const url = new URL(`${AUTH_BASE_URL}/token`) + url.searchParams.set('client_id', CLIENT_ID) + url.searchParams.set('client_secret', CLIENT_SECRET) + url.searchParams.set('grant_type', 'client_credentials') + + const response = await fetch(url, { method: 'POST' }) + _auth = await response.json() as IAuth + _lastUpdate = Date.now() + } + return _auth +} + +// fetches the top 500 games with a rating of 96 or higher +export async function getGames(): Promise<IGame[]> { + const auth = await getToken() + const url = new URL(`${IGDB_BASE_URL}/games`) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Client-ID': CLIENT_ID, + 'Authorization': `Bearer ${auth.access_token}` + }, + body: `fields name, cover.*; limit 500; where rating > 96 & cover != null;` + }) + const games = await response.json() as IGame[] + + games.forEach(game => { + game.cover.url = getImageURL(game.cover.image_id, 'cover_big') + }) + + return games +} + +// fetches a single game by id +export async function getGame(id: number): Promise<IGame[]> { + const auth = await getToken() + const url = new URL(`${IGDB_BASE_URL}/games`) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Client-ID': CLIENT_ID, + 'Authorization': `Bearer ${auth.access_token}` + }, + body: `fields name, cover.*, summary; where cover != null; where id = ${id};` + }) + const games = await response.json() as IGame[] + + games.forEach(game => { + game.cover.url = getImageURL(game.cover.image_id, 'cover_big') + }) + + return games +} \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..be0edbb --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +const IGDB_IMG_BASE_URL = process.env.IGDB_IMG_BASE_URL ?? '' + +// changes the default size of the image to be fetched +export function getImageURL(hashId: string, size: string): string { + return `${IGDB_IMG_BASE_URL}/t_${size}_2x/${hashId}.jpg` +} \ No newline at end of file diff --git a/types/types.ts b/types/types.ts new file mode 100644 index 0000000..2cecc00 --- /dev/null +++ b/types/types.ts @@ -0,0 +1,159 @@ +export enum EAgeRatingCategory { + 'ESRB' = 0, + 'PEGI' = 2, + 'CERO' = 3, + 'USK' = 4, + 'GRAC' = 5, + 'CLASS_IND' = 6, + 'ACB' = 7, +} + +export enum EAgeRatingRating { + 'Three' = 1, + 'Seven' = 2, + 'Twelve' = 3, + 'Sixteen' = 4, + 'Eighteen' = 5, + 'RP' = 6, + 'EC' = 7, + 'E' = 8, + 'E10' = 9, + 'T' = 10, + 'M' = 11, + 'AO' = 12, + 'CERO_A' = 13, + 'CERO_B' = 14, + 'CERO_C' = 15, + 'CERO_D' = 16, + 'CERO_Z' = 17, + 'USK_0' = 18, + 'USK_6' = 19, + 'USK_12' = 20, + 'USK_16' = 21, + 'USK_18' = 22, + 'GRAC_ALL' = 23, + 'GRAC_Twelve' = 24, + 'GRAC_Fifteen' = 25, + 'GRAC_Eighteen' = 26, + 'GRAC_TESTING' = 27, + 'CLASS_IND_L' = 28, + 'CLASS_IND_Ten' = 29, + 'CLASS_IND_Twelve' = 30, + 'CLASS_IND_Fourteen' = 31, + 'CLASS_IND_Sixteen' = 32, + 'CLASS_IND_Eighteen' = 33, + 'ACB_G' = 34, + 'ACB_PG' = 35, + 'ACB_M' = 36, + 'ACB_MA15' = 37, + 'ACB_R18' = 38, + 'ACB_RC' = 39, +} + +export enum EGameCategory { + 'main_game' = 0, + 'dlc_addon' = 1, + 'expansion' = 2, + 'bundle' = 3, + 'standalone_expansion' = 4, + 'mod' = 5, + 'episode' = 6, + 'season' = 7, + 'remake' = 8, + 'remaster' = 9, + 'expanded_game' = 10, + 'port' = 11, + 'fork' = 12, + 'pack' = 13, + 'update' = 14, +} + +export enum EGameStatus { + 'released' = 0, + 'alpha' = 2, + 'beta' = 3, + 'early_access' = 4, + 'offline' = 5, + 'cancelled' = 6, + 'rumored' = 7, + 'delisted' = 8, +} + +export interface IAuth { + access_token: string; + expires_in: number; + token_type: 'bearer'; +} + +export interface IGame { + id: number; + age_ratings: EAgeRatingCategory[]; + aggregrated_rating: number; + aggregrated_rating_count: number; + alternative_names: number[]; + artworks: number[]; + bundles: number[]; + category: EGameCategory; + checksum: string; + collection: number; + cover: ICover; + created_at: number; + dlcs: number[]; + expanded_games: number[]; + expansions: number[]; + external_games: number[]; + first_release_date: number; + follows: number; + forks: number[]; + franchise: number; + franchises: number[]; + game_engines: number[]; + game_localizations: number[]; + game_modes: number[]; + genres: number[]; + hypes: number; + involved_companies: number[]; + keywords: number[]; + language_supports: number[]; + multiplayer_modes: number[]; + name: string; + parent_game: string; + platforms: number[]; + player_perspectives: number[]; + ports: number[]; + rating: number; + rating_count: number; + release_dates: number[]; + remakes: number[]; + remasters: number[]; + screenshots: number[]; + similar_games: number[]; + slug: string; + standalone_expansions: number[]; + status: EGameStatus; + storyline: string; + summary: string; + tags: number[]; + themes: number[]; + total_rating: number; + total_rating_count: number; + updated_at: number; + url: string; + version_parent: number; + version_title: string; + videos: number[]; + websites: number[]; +} + +export interface ICover { + id: number; + alpha_channel: boolean; + animated: boolean; + checksum: string; + game: number; + game_localization: number; + height: number; + image_id: string; + url: string; + width: number; +} \ No newline at end of file -- GitLab