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