From 6824575306b6b4c1a741ac2fd3c5b14415295285 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 23 Oct 2023 18:51:53 +0100 Subject: [PATCH] improve loading time for player page --- package-lock.json | 13 - package.json | 1 - .../[id]/[leaderboard]/[sort]/[page]/page.tsx | 124 ++++++++ src/app/player/[id]/page.tsx | 52 ---- src/components/Navbar.tsx | 2 +- src/components/player/Player.tsx | 109 ------- src/components/player/PlayerInfo.tsx | 268 +++++++++--------- src/components/player/Scores.tsx | 159 +++-------- src/utils/fetchWithQueue.ts | 2 +- 9 files changed, 298 insertions(+), 432 deletions(-) create mode 100644 src/app/player/[id]/[leaderboard]/[sort]/[page]/page.tsx delete mode 100644 src/app/player/[id]/page.tsx delete mode 100644 src/components/player/Player.tsx diff --git a/package-lock.json b/package-lock.json index cc9e9a3..8a79e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "react-dom": "^18", "react-toastify": "^9.1.3", "sharp": "^0.32.6", - "swr": "^2.2.4", "zustand": "^4.4.3" }, "devDependencies": { @@ -5418,18 +5417,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swr": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz", - "integrity": "sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==", - "dependencies": { - "client-only": "^0.0.1", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/tailwindcss": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", diff --git a/package.json b/package.json index 97a5ac2..7d8bc96 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "react-dom": "^18", "react-toastify": "^9.1.3", "sharp": "^0.32.6", - "swr": "^2.2.4", "zustand": "^4.4.3" }, "devDependencies": { diff --git a/src/app/player/[id]/[leaderboard]/[sort]/[page]/page.tsx b/src/app/player/[id]/[leaderboard]/[sort]/[page]/page.tsx new file mode 100644 index 0000000..522793f --- /dev/null +++ b/src/app/player/[id]/[leaderboard]/[sort]/[page]/page.tsx @@ -0,0 +1,124 @@ +import Card from "@/components/Card"; +import Container from "@/components/Container"; +import Error from "@/components/Error"; +import PlayerInfo from "@/components/player/PlayerInfo"; +import Scores from "@/components/player/Scores"; +import { ssrSettings } from "@/ssrSettings"; +import { SortTypes } from "@/types/SortTypes"; +import { formatNumber } from "@/utils/number"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; +import { normalizedRegionName } from "@/utils/utils"; +import { Metadata } from "next"; + +type PlayerPageProps = { + params: { id: string; leaderboard: string; sort: string; page: string }; +}; + +const DEFAULT_SORT_TYPE = SortTypes.top; + +// Get data from API (server-sided) +async function getData(playerId: string, sort?: string, page?: number) { + const playerData = await ScoreSaberAPI.fetchPlayerData(playerId); + if (!playerData) { + return undefined; + } + + const scores = []; + let totalPages = -1; + if (sort && page) { + const scoresData = await ScoreSaberAPI.fetchScores( + playerId, + page, + sort, + 10, + ); + if (scoresData) { + scores.push(...scoresData.scores); + totalPages = scoresData.pageInfo.totalPages; + } + } + + return { player: playerData, scores: scores, totalPages: totalPages }; +} + +export async function generateMetadata({ + params: { id, leaderboard, sort, page }, +}: PlayerPageProps): Promise { + const data = await getData(id, sort, Number(page)); + if (!data) { + return { + title: "Player not found", + }; + } + + const player = data.player; + + const description = ` + View ${player.name}'s scores, top plays, and more. + + Rank: #${formatNumber(player.rank)} (#${formatNumber( + player.countryRank, + )} - ${normalizedRegionName(player.country)}) + PP: ${formatNumber(player.pp)} + Play Count: ${formatNumber(player.scoreStats.totalPlayCount)}`; + + return { + title: `${player.name}`, + description: `View ${player.name}'s scores, top plays, and more.`, + openGraph: { + siteName: ssrSettings.siteName, + title: `${player.name}`, + description: description, + images: [ + { + url: player.profilePicture, + }, + ], + }, + twitter: { + card: "summary", + }, + }; +} + +export default async function Player({ + params: { id, leaderboard, sort, page }, +}: PlayerPageProps) { + const playerData = await getData(id, sort, Number(page)); + const currentPage = Number(page); + const sortType = SortTypes[sort] || DEFAULT_SORT_TYPE; + + if (!playerData) { + return ( +
+ + + + + +
+ ); + } + + const player = playerData.player; + const scores = playerData.scores; + + return ( +
+ + + + + + + + +
+ ); +} diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx deleted file mode 100644 index b83ed5d..0000000 --- a/src/app/player/[id]/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import PlayerPage from "@/components/player/Player"; -import { ssrSettings } from "@/ssrSettings"; -import { formatNumber } from "@/utils/number"; -import { ScoreSaberAPI } from "@/utils/scoresaber/api"; -import { normalizedRegionName } from "@/utils/utils"; -import { Metadata } from "next"; - -type Props = { - params: { id: string }; -}; - -export async function generateMetadata({ - params: { id }, -}: Props): Promise { - const player = await ScoreSaberAPI.fetchPlayerData(id); - if (!player) { - return { - title: "Player not found", - }; - } - - const description = ` - View ${player.name}'s scores, top plays, and more. - - Rank: #${formatNumber(player.rank)} (#${formatNumber( - player.countryRank, - )} - ${normalizedRegionName(player.country)}) - PP: ${formatNumber(player.pp)} - Play Count: ${formatNumber(player.scoreStats.totalPlayCount)}`; - - return { - title: `${player.name}`, - description: `View ${player.name}'s scores, top plays, and more.`, - openGraph: { - siteName: ssrSettings.siteName, - title: `${player.name}`, - description: description, - images: [ - { - url: player.profilePicture, - }, - ], - }, - twitter: { - card: "summary", - }, - }; -} - -export default function Player({ params: { id } }: Props) { - return ; -} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bacc142..695b953 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -56,7 +56,7 @@ export default function Navbar() { size={32} /> } - href={`/player/${settingsStore.player.id}`} + href={`/player/${settingsStore.player.id}/scoresaber/top/1`} /> )} diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx deleted file mode 100644 index cff5d26..0000000 --- a/src/components/player/Player.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client"; - -import Card from "@/components/Card"; -import Container from "@/components/Container"; -import Spinner from "@/components/Spinner"; -import Scores from "@/components/player/Scores"; -import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { useSettingsStore } from "@/store/settingsStore"; -import { SortType, SortTypes } from "@/types/SortTypes"; -import { ScoreSaberAPI } from "@/utils/scoresaber/api"; -import useStore from "@/utils/useStore"; -import dynamic from "next/dynamic"; -import { useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import PlayerInfo from "./PlayerInfo"; - -const Error = dynamic(() => import("@/components/Error")); - -type PlayerInfo = { - loading: boolean; - player: ScoresaberPlayer | undefined; -}; - -type PlayerPageProps = { - id: string; -}; - -const DEFAULT_SORT_TYPE = SortTypes.top; - -export default function PlayerPage({ id }: PlayerPageProps) { - const settingsStore = useStore(useSettingsStore, (store) => store); - const searchParams = useSearchParams(); - - const [mounted, setMounted] = useState(false); - const [error, setError] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - - const [player, setPlayer] = useState({ - loading: true, - player: undefined, - }); - - let page; - const pageString = searchParams.get("page"); - if (pageString == null) { - page = 1; - } else { - page = Number.parseInt(pageString) || 1; - } - - let sortType: SortType; - const sortTypeString = searchParams.get("sort"); - if (sortTypeString == null) { - sortType = settingsStore?.lastUsedSortType || DEFAULT_SORT_TYPE; - } else { - sortType = SortTypes[sortTypeString] || DEFAULT_SORT_TYPE; - } - - useEffect(() => { - setMounted(true); - if (error || !player.loading) { - return; - } - - if (mounted == true) { - return; - } - - ScoreSaberAPI.fetchPlayerData(id).then((playerResponse) => { - if (!playerResponse) { - setError(true); - setErrorMessage("Failed to fetch player. Is the ID correct?"); - setPlayer({ ...player, loading: false }); - return; - } - setPlayer({ ...player, player: playerResponse, loading: false }); - }); - }, [error, mounted, id, player]); - - if (player.loading || error || !player.player) { - return ( -
- - -
-
-
- {error && } - {!error && } -
-
-
-
-
-
- ); - } - - const playerData = player.player; - - return ( -
- - - - -
- ); -} diff --git a/src/components/player/PlayerInfo.tsx b/src/components/player/PlayerInfo.tsx index d5fd2d4..5022222 100644 --- a/src/components/player/PlayerInfo.tsx +++ b/src/components/player/PlayerInfo.tsx @@ -1,3 +1,5 @@ +"use client"; + import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore"; import { useSettingsStore } from "@/store/settingsStore"; @@ -18,7 +20,6 @@ import { useRef } from "react"; import { toast } from "react-toastify"; import { useStore } from "zustand"; import Avatar from "../Avatar"; -import Card from "../Card"; import Label from "../Label"; const PlayerChart = dynamic(() => import("./PlayerChart")); @@ -95,10 +96,6 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) { }); } } - - console.log( - `Fetching scores for ${playerId} (${page}/${totalPages})`, - ); }, ); if (reponse?.error) { @@ -112,145 +109,138 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) { const isOwnProfile = settingsStore.player?.id == playerId; return ( - - {/* Player Info */} -
-
- {/* Avatar */} -
- -
- - {/* Settings Buttons */} -
- {!isOwnProfile && ( - - )} - - {!isOwnProfile && ( - <> - {!settingsStore?.isFriend(playerId) && ( - - )} - - {settingsStore.isFriend(playerId) && ( - - )} - - )} -
+
+
+ {/* Avatar */} +
+
-
- {/* Name */} -

{playerData.name}

-
- {/* Global Rank */} -
- + {/* Settings Buttons */} +
+ {!isOwnProfile && ( + + )} - -

#{formatNumber(playerData.rank)}

-
-
+ {!isOwnProfile && ( + <> + {!settingsStore?.isFriend(playerId) && ( + + )} - {/* Country Rank */} - - - {/* PP */} -
-

{formatNumber(playerData.pp)}pp

-
-
- {/* Labels */} -
-
- - {/* Chart */} - + {settingsStore.isFriend(playerId) && ( + + )} + + )}
- +
+ {/* Name */} +

{playerData.name}

+ +
+ {/* Global Rank */} + + + {/* Country Rank */} + + + {/* PP */} +
+

{formatNumber(playerData.pp)}pp

+
+
+ {/* Labels */} +
+
+ + {/* Chart */} + +
+
); } diff --git a/src/components/player/Scores.tsx b/src/components/player/Scores.tsx index bce2f1f..c3e634d 100644 --- a/src/components/player/Scores.tsx +++ b/src/components/player/Scores.tsx @@ -1,109 +1,46 @@ +"use client"; + import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; import { useSettingsStore } from "@/store/settingsStore"; import { SortType, SortTypes } from "@/types/SortTypes"; -import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import useStore from "@/utils/useStore"; -import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; import Card from "../Card"; -import Error from "../Error"; import Pagination from "../Pagination"; import Score from "./Score"; -const Spinner = dynamic(() => import("@/components/Spinner")); - -type PageInfo = { - loading: boolean; +type ScoresProps = { + playerData: ScoresaberPlayer; page: number; totalPages: number; sortType: SortType; scores: ScoresaberPlayerScore[]; }; -type ScoresProps = { - playerData: ScoresaberPlayer; - page: number; - sortType: SortType; -}; - -export default function Scores({ playerData, page, sortType }: ScoresProps) { +export default function Scores({ + playerData, + page, + totalPages, + sortType, + scores, +}: ScoresProps) { const settingsStore = useStore(useSettingsStore, (store) => store); const playerId = playerData.id; const router = useRouter(); - const [mounted, setMounted] = useState(false); - const [error, setError] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - - const [scores, setScores] = useState({ - loading: true, - page: page, - totalPages: 1, - sortType: sortType, - scores: [], - }); - - const updateScoresPage = useCallback( - (sortType: SortType, page: any) => { - ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then( - (scoresResponse) => { - if (!scoresResponse) { - setError(true); - setErrorMessage("No Scores"); - setScores({ ...scores, loading: false }); - return; - } - setScores({ - ...scores, - scores: scoresResponse.scores, - totalPages: scoresResponse.pageInfo.totalPages, - loading: false, - page: page, - sortType: sortType, - }); - settingsStore?.setLastUsedSortType(sortType); - - if (page > 1) { - router.push( - `/player/${playerId}?page=${page}&sort=${sortType.value}`, - { - scroll: false, - }, - ); - } else { - router.push(`/player/${playerId}?sort=${sortType.value}`, { - scroll: false, - }); - } - - console.log(`Switched page to ${page} with sort ${sortType.value}`); - }, - ); - }, - [playerId, router, scores, settingsStore], - ); - - useEffect(() => { - if (mounted) return; - setMounted(true); - - updateScoresPage(scores.sortType, scores.page); - }, [mounted, updateScoresPage, scores.sortType, scores.page]); - - if (error) { - return ( - -
-
-
- {error && } -
-
-
-
+ function setPage(page: number, sortTypee?: SortType) { + if (sortTypee) { + settingsStore?.setLastUsedSortType(sortTypee); + } + router.push( + `/player/${playerId}/scoresaber/${ + sortTypee ? sortTypee.value : sortType.value + }/${page}`, + { + scroll: false, + }, ); } @@ -112,21 +49,21 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) { {/* Sort */}
- {Object.values(SortTypes).map((sortType) => { + {Object.values(SortTypes).map((sortTypee) => { return ( ); })} @@ -134,40 +71,30 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
- {scores.loading ? ( -
- -
- ) : ( -
- {!scores.loading && scores.scores.length == 0 ? ( -

{errorMessage}

- ) : ( - scores.scores.map((scoreData, id) => { - const { score, leaderboard } = scoreData; +
+ {scores.map((scoreData, id) => { + const { score, leaderboard } = scoreData; - return ( - - ); - }) - )} -
- )} + return ( + + ); + })} +
{/* Pagination */}
{ - updateScoresPage(scores.sortType, page); + setPage(page); }} />
diff --git a/src/utils/fetchWithQueue.ts b/src/utils/fetchWithQueue.ts index 5d2f543..d7cac66 100644 --- a/src/utils/fetchWithQueue.ts +++ b/src/utils/fetchWithQueue.ts @@ -25,7 +25,7 @@ export class FetchQueue { const response = await fetch(url, { next: { - revalidate: 3600, // Keep the data for 1 hour, then fetch new data + revalidate: 60 * 5, // Keep the data for 5 minutes }, }); if (response.status === 429) {