From f93cb9d25fd186a161a160f8299516db383a96f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yusuf=20Akg=C3=BCl?= <s86116@bht-berlin.de>
Date: Sun, 14 May 2023 03:30:44 +0200
Subject: [PATCH] infinity scroll implemented + started using mui, + some
 cleanup and code form changes

---
 .env.example                |  13 ++++
 .env.sample                 |  14 -----
 .gitignore                  |   5 +-
 app/api/games/route.ts      |   7 ++-
 app/games/Game.tsx          |  15 +++--
 app/games/[gameid]/page.tsx |   2 +-
 app/games/page.tsx          |  73 +++++++++++++++++-----
 app/layout.tsx              |  56 +++++++++++++----
 app/page.tsx                |   8 ++-
 lib/igdb.ts                 |  13 ++--
 lib/utils.ts                |  14 +++++
 package-lock.json           | 120 ++++++++++++++++++++++++++++++++++--
 package.json                |   4 +-
 13 files changed, 279 insertions(+), 65 deletions(-)
 create mode 100644 .env.example
 delete mode 100644 .env.sample

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a362d12
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+# Example .env file
+
+# Database for connecting to Prisma
+DATABASE_URL="file:./dev.db"
+
+# Some URLs
+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"
+
+# For Authentication
+TWITCH_CLIENT_ID="imdb_client_id"
+TWITCH_CLIENT_SECRET="imdb_auth_id"
\ No newline at end of file
diff --git a/.env.sample b/.env.sample
deleted file mode 100644
index 6db1037..0000000
--- a/.env.sample
+++ /dev/null
@@ -1,14 +0,0 @@
-# Environment variables declared in this file are automatically made available to Prisma.
-# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
-
-# 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="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
-
-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/.gitignore b/.gitignore
index 45c1abc..56674d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,12 +25,13 @@ yarn-debug.log*
 yarn-error.log*
 
 # local env files
-.env*.local
 .env
+.env*.local
+.env*.production
 
 # vercel
 .vercel
 
 # typescript
 *.tsbuildinfo
-next-env.d.ts
+next-env.d.ts
\ No newline at end of file
diff --git a/app/api/games/route.ts b/app/api/games/route.ts
index 0a73881..842198f 100644
--- a/app/api/games/route.ts
+++ b/app/api/games/route.ts
@@ -1,7 +1,8 @@
 import { getGames } from "@/lib/igdb";
 import { NextRequest, NextResponse } from "next/server";
 
-export async function GET() {
-    const games = await getGames();
+export async function GET(req: NextRequest) {
+    const p = req.nextUrl.searchParams;
+    const games = await getGames(p.get('page') ? parseInt(p.get('page') as string) : undefined);
     return NextResponse.json(games);
-}
+}
\ No newline at end of file
diff --git a/app/games/Game.tsx b/app/games/Game.tsx
index 03e3bb3..4f72f68 100644
--- a/app/games/Game.tsx
+++ b/app/games/Game.tsx
@@ -1,14 +1,17 @@
+import { Card, CardContent, Typography } from "@mui/material";
 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 }) {
+export default function Game({ id, name, cover }: { id: number, name: string, cover: { url: string } }) {
     return (
-        <div>
-            <h1>{name}</h1>
+        <Card sx={{ maxWidth: 264 }} variant="outlined" >
             <Link href={`/games/${id}`}>
-                <Image src={cover.url} alt={name} width={264} height={374} priority={true} />
+                <Image src={cover.url} alt={name} width={264} height={374} priority={true} style={{ width: '100%', height: '100%' }} />
             </Link>
-        </div>
+            <CardContent>
+                <Typography noWrap={true}>{name}</Typography>
+            </CardContent>
+        </Card>
     )
-}
+}
\ No newline at end of file
diff --git a/app/games/[gameid]/page.tsx b/app/games/[gameid]/page.tsx
index d86eb4a..31e205b 100644
--- a/app/games/[gameid]/page.tsx
+++ b/app/games/[gameid]/page.tsx
@@ -8,7 +8,7 @@ export default async function GameDetail({ params }: { params: { gameid: string
 
     return (
         <div>
-            Game Detail
+            <h1>Game Detail</h1>
             <h1>{data[0].name}</h1>
             <Image src={data[0].cover.url} alt={data[0].name} width={264} height={374} priority={true} />
             <p>{data[0].summary}</p>
diff --git a/app/games/page.tsx b/app/games/page.tsx
index 1d520e5..6f0ce94 100644
--- a/app/games/page.tsx
+++ b/app/games/page.tsx
@@ -1,26 +1,65 @@
 "use client"
 
+import { getBaseURL } from "@/lib/utils";
 import { IGame } from "@/types/types";
-import { useEffect, useState } from "react";
+import { Grid } from "@mui/material";
+import { Fragment } from "react";
+import InfiniteScroll from "react-infinite-scroll-component";
+import { useInfiniteQuery } from "react-query";
 import Game from "./Game";
 
-// renders a list of games
+// renders a list of games infinitely (presumably)
 export default function GamesList() {
-    const [data, setData] = useState<IGame[]>([])
-
-    async function load() {
-        const data = await fetch("http://localhost:3000/api/games").then((res) => res.json())
-        setData(data)
-    }
-
-    useEffect(() => { load() }, [])
+    const {
+        data,
+        error,
+        fetchNextPage,
+        hasNextPage,
+        isFetching,
+        isFetchingNextPage,
+        status,
+    } = useInfiniteQuery(
+        'infiniteGames',
+        async ({ pageParam = 1 }) =>
+            await fetch(
+                `${getBaseURL()}/api/games/?page=${pageParam}`,
+                { cache: 'force-cache', }
+            ).then((result) => result.json() as Promise<IGame[]>),
+        {
+            getNextPageParam: (lastPage, pages) => {
+                return lastPage.length > 0 ? pages.length + 1 : undefined;
+            },
+        }
+    );
 
     return (
-        <div>
-            Games List Page
-            {data.map((game: any) => (
-                <Game key={game.id} id={game.id} name={game.name} cover={game.cover} />
-            ))}
-        </div>
+        <>
+            <h1>Games List Page</h1>
+            {status === 'success' && (
+                <InfiniteScroll
+                    dataLength={data?.pages.length * 20}
+                    next={fetchNextPage}
+                    hasMore={hasNextPage ? true : false}
+                    loader={<h4>Loading...</h4>}
+                    endMessage={
+                        <p style={{ textAlign: 'center' }}>
+                            <b>Yay! You have seen it all</b>
+                        </p>
+                    }
+                >
+                    <Grid container spacing={2} justifyContent="center">
+                        {data?.pages.map((page, i) => (
+                            <Fragment key={i}>
+                                {page.map((game: IGame) => (
+                                    <Grid item xs={12} ss={6} sm={4} md={3} lg={2} key={game.id}>
+                                        <Game id={game.id} name={game.name} cover={game.cover} key={game.id} />
+                                    </Grid>
+                                ))}
+                            </Fragment>
+                        ))}
+                    </Grid>
+                </InfiniteScroll>
+            )}
+        </>
     )
-}
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index f4f1f87..70082c1 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,13 +1,12 @@
 "use client"
 
-import { Container } from '@mui/material'
-import { Inter } from 'next/font/google'
-
-const inter = Inter({ subsets: ['latin'] })
+import { Container, CssBaseline, ThemeProvider, createTheme, useMediaQuery } from "@mui/material"
+import { useMemo, useState } from "react"
+import { QueryClient, QueryClientProvider } from "react-query"
 
 export const metadata = {
-  title: 'Create Next App',
-  description: 'Generated by create next app',
+  title: 'GameUnity',
+  description: 'Soon',
 }
 
 // this is the root layout for all pages ({children})
@@ -16,13 +15,48 @@ export default function RootLayout({
 }: {
   children: React.ReactNode
 }) {
+  const [queryClient] = useState(() => new QueryClient());
+
+  const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+  const theme = useMemo(
+    () =>
+      createTheme({
+        palette: {
+          mode: prefersDarkMode ? 'dark' : 'light',
+        },
+        breakpoints: {
+          values: {
+            xs: 0,
+            ss: 300,
+            sm: 600,
+            md: 900,
+            lg: 1200,
+            xl: 1536,
+          },
+        },
+      }),
+    [prefersDarkMode],
+  );
+
   return (
     <html lang="en">
-      <body className={inter.className}>
-        <Container>
-          {children}
-        </Container>
-      </body>
+      <QueryClientProvider client={queryClient}>
+        <ThemeProvider theme={theme}>
+          <CssBaseline />
+          <body>
+            <Container>
+              {children}
+            </Container>
+          </body>
+        </ThemeProvider>
+      </QueryClientProvider>
     </html>
   )
 }
+
+// custom super small breakpoint for responsive design
+declare module '@mui/material/styles' {
+  interface BreakpointOverrides {
+    ss: true;
+  }
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 04c430e..bca80d6 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,10 +1,14 @@
+import Link from "next/link";
+
 // renders home page
 export default function Home() {
   return (
     <main>
       <div>
-        Hello World!
+        <h1>Welcome to GameUnity!</h1>
+        <p>This will be our Home Page and is still WIP</p>
+        <Link href="/games">Games List Progress</Link>
       </div>
     </main>
   )
-}
+}
\ No newline at end of file
diff --git a/lib/igdb.ts b/lib/igdb.ts
index f9a5396..82e3f41 100644
--- a/lib/igdb.ts
+++ b/lib/igdb.ts
@@ -1,5 +1,5 @@
 import { IAuth, IGame } from "@/types/types"
-import { getImageURL } from "./utils"
+import { calculateOffset, getImageURL } from "./utils"
 
 const TWITCH_AUTH_BASE_URL = process.env.TWITCH_AUTH_BASE_URL ?? ''
 const IGDB_BASE_URL = process.env.IGDB_BASE_URL ?? ''
@@ -7,6 +7,8 @@ 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 ?? ''
 
+const limit = 200
+
 let _auth: IAuth
 let _lastUpdate = 0
 
@@ -26,21 +28,24 @@ async function getToken(): Promise<IAuth> {
 }
 
 // fetches the top 200 games with a rating of 96 or higher
-export async function getGames(offset = 0): Promise<IGame[]> {
+export async function getGames(page = 1): Promise<IGame[]> {
     const auth = await getToken()
     const url = new URL(`${IGDB_BASE_URL}/games`)
 
+    let offset = calculateOffset(page, limit)
+
     const response = await fetch(url, {
         method: 'POST',
         headers: {
             'Client-ID': CLIENT_ID,
             'Authorization': `Bearer ${auth.access_token}`
         },
-        body: `fields name, cover.*; limit 200; offset ${offset};
-            sort total_rating desc; where total_rating_count > 200
+        body: `fields name, cover.*; limit ${limit}; offset ${offset};
+            sort total_rating desc; where total_rating_count > 2
             & cover != null & total_rating != null & rating != null;`
     })
     const games = await response.json() as IGame[]
+
     games.forEach(game => {
         game.cover.url = getImageURL(game.cover.image_id, 'cover_big')
     })
diff --git a/lib/utils.ts b/lib/utils.ts
index be0edbb..1aebb95 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -3,4 +3,18 @@ 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`
+}
+
+// returns the base url for the current environment, even considering current port
+export function getBaseURL(): string {
+    return process.env.NODE_ENV === 'production'
+        ? process.env.PROD_URL ?? ''
+        : (typeof window !== 'undefined'
+            ? `http://${window.location.hostname}:${window.location.port}`
+            : 'http://localhost:3000')
+}
+
+// calculates the offset for the query
+export function calculateOffset(page: number, limit: number): number {
+    return (page - 1) * limit
 }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8298931..76e0654 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
         "@emotion/react": "^11.11.0",
         "@emotion/styled": "^11.11.0",
         "@mui/material": "^5.13.0",
-        "@types/node": "20.1.3",
+        "@types/node": "20.1.4",
         "@types/react": "18.2.6",
         "@types/react-dom": "18.2.4",
         "eslint": "8.40.0",
@@ -20,6 +20,8 @@
         "next": "13.4.2",
         "react": "18.2.0",
         "react-dom": "18.2.0",
+        "react-infinite-scroll-component": "^6.1.0",
+        "react-query": "^3.39.3",
         "typescript": "5.0.4"
       },
       "devDependencies": {
@@ -1076,9 +1078,9 @@
       "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
     },
     "node_modules/@types/node": {
-      "version": "20.1.3",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.3.tgz",
-      "integrity": "sha512-NP2yfZpgmf2eDRPmgGq+fjGjSwFgYbihA8/gK+ey23qT9RkxsgNTZvGOEpXgzIGqesTYkElELLgtKoMQTys5vA=="
+      "version": "20.1.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz",
+      "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q=="
     },
     "node_modules/@types/node-fetch": {
       "version": "2.6.2",
@@ -1533,6 +1535,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/broadcast-channel": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
+      "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
+      "dependencies": {
+        "@babel/runtime": "^7.7.2",
+        "detect-node": "^2.1.0",
+        "js-sha3": "0.8.0",
+        "microseconds": "0.2.0",
+        "nano-time": "1.0.0",
+        "oblivious-set": "1.0.0",
+        "rimraf": "3.0.2",
+        "unload": "2.2.0"
+      }
+    },
     "node_modules/bundle-name": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
@@ -1855,6 +1872,11 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/detect-node": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -3296,6 +3318,11 @@
         "url": "https://opencollective.com/js-sdsl"
       }
     },
+    "node_modules/js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3440,6 +3467,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/match-sorter": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz",
+      "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "remove-accents": "0.4.2"
+      }
+    },
     "node_modules/merge-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -3465,6 +3501,11 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/microseconds": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
+      "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
+    },
     "node_modules/mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -3519,6 +3560,14 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/nano-time": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
+      "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
+      "dependencies": {
+        "big-integer": "^1.6.16"
+      }
+    },
     "node_modules/nanoid": {
       "version": "3.3.6",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@@ -3746,6 +3795,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/oblivious-set": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
+      "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -4047,11 +4101,47 @@
         "react": "^18.2.0"
       }
     },
+    "node_modules/react-infinite-scroll-component": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
+      "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==",
+      "dependencies": {
+        "throttle-debounce": "^2.1.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0"
+      }
+    },
     "node_modules/react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "node_modules/react-query": {
+      "version": "3.39.3",
+      "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
+      "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "broadcast-channel": "^3.4.1",
+        "match-sorter": "^6.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        },
+        "react-native": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-transition-group": {
       "version": "4.4.5",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -4088,6 +4178,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/remove-accents": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
+      "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+    },
     "node_modules/resolve": {
       "version": "1.22.2",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -4587,6 +4682,14 @@
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
     },
+    "node_modules/throttle-debounce": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
+      "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/titleize": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
@@ -4734,6 +4837,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/unload": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
+      "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
+      "dependencies": {
+        "@babel/runtime": "^7.6.2",
+        "detect-node": "^2.0.4"
+      }
+    },
     "node_modules/untildify": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
diff --git a/package.json b/package.json
index 5e9df34..9bd9dd3 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "@emotion/react": "^11.11.0",
     "@emotion/styled": "^11.11.0",
     "@mui/material": "^5.13.0",
-    "@types/node": "20.1.3",
+    "@types/node": "20.1.4",
     "@types/react": "18.2.6",
     "@types/react-dom": "18.2.4",
     "eslint": "8.40.0",
@@ -21,6 +21,8 @@
     "next": "13.4.2",
     "react": "18.2.0",
     "react-dom": "18.2.0",
+    "react-infinite-scroll-component": "^6.1.0",
+    "react-query": "^3.39.3",
     "typescript": "5.0.4"
   },
   "devDependencies": {
-- 
GitLab