From 1ff7c246c30d58752af8b46d76742f234ff1cfec Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 22 Oct 2023 02:17:21 +0100 Subject: [PATCH] added local score fetching --- src/app/player/[id]/page.tsx | 48 ++++++++-- src/components/Container.tsx | 7 +- src/components/Navbar.tsx | 7 +- src/store/playerScoresStore.ts | 160 +++++++++++++++++++++++++++++++++ src/store/settingsStore.ts | 2 + src/utils/scoresaber/api.ts | 13 ++- 6 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 src/store/playerScoresStore.ts diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx index 6cc1cf0..715df11 100644 --- a/src/app/player/[id]/page.tsx +++ b/src/app/player/[id]/page.tsx @@ -10,6 +10,7 @@ import Score from "@/components/Score"; import { Spinner } from "@/components/Spinner"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; +import { usePlayerScoresStore } from "@/store/playerScoresStore"; import { useSettingsStore } from "@/store/settingsStore"; import { formatNumber } from "@/utils/number"; import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api"; @@ -22,7 +23,7 @@ import { } from "@heroicons/react/20/solid"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import ReactCountryFlag from "react-country-flag"; import { toast } from "react-toastify"; @@ -60,13 +61,9 @@ const sortTypes: { [key: string]: SortType } = { const DEFAULT_SORT_TYPE = sortTypes.top; export default function Player({ params }: { params: { id: string } }) { - const settingsStore = useStore(useSettingsStore, (state) => { - return { - userId: state.userId, - setUserId: state.setUserId, - refreshProfile: state.refreshProfile, - }; - }); + const settingsStore = useStore(useSettingsStore, (store) => store); + const playerScoreStore = useStore(usePlayerScoresStore, (store) => store); + const searchParams = useSearchParams(); const router = useRouter(); @@ -141,9 +138,42 @@ export default function Player({ params }: { params: { id: string } }) { [params.id, router, scores], ); - function claimProfile() { + const toastId: any = useRef(null); + + async function claimProfile() { settingsStore?.setUserId(params.id); settingsStore?.refreshProfile(); + + const reponse = await playerScoreStore?.addPlayer( + params.id, + (page, totalPages) => { + const autoClose = page == totalPages ? 5000 : false; + + if (page == 1) { + toastId.current = toast.info( + `Fetching scores ${page}/${totalPages}`, + { + autoClose: autoClose, + progress: page / totalPages, + }, + ); + } else { + toast.update(toastId.current, { + progress: page / totalPages, + render: `Fetching scores ${page}/${totalPages}`, + autoClose: autoClose, + }); + } + + console.log(`Fetching scores for ${params.id} (${page}/${totalPages})`); + }, + ); + if (reponse?.error) { + toast.error("Failed to claim profile"); + console.log(reponse.message); + return; + } + toast.success("Successfully claimed profile"); } diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 90e2d59..665514b 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -5,7 +5,12 @@ import Navbar from "./Navbar"; export default function Container({ children }: { children: React.ReactNode }) { return ( <> - +
{children}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 89a3ab1..6bfdfe1 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -41,12 +41,7 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) { } export default function Navbar() { - const settingsStore = useStore(useSettingsStore, (state) => { - return { - profilePicture: state.profilePicture, - userId: state.userId, - }; - }); + const settingsStore = useStore(useSettingsStore, (state) => state); return ( <> diff --git a/src/store/playerScoresStore.ts b/src/store/playerScoresStore.ts new file mode 100644 index 0000000..452f62d --- /dev/null +++ b/src/store/playerScoresStore.ts @@ -0,0 +1,160 @@ +"use client"; + +import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; +import { fetchAllScores, fetchScores } from "@/utils/scoresaber/api"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + +type Player = { + lastUpdated: Date; + id: string; + scores: ScoresaberPlayerScore[]; +}; + +interface PlayerScoresStore { + players: Player[]; + + exists: (playerId: string) => boolean; + get(playerId: string): Player | undefined; + + addPlayer: ( + playerId: string, + callback?: (page: number, totalPages: number) => void, + ) => Promise<{ + error: boolean; + message: string; + }>; + updatePlayerScores: () => void; +} + +const UPDATE_INTERVAL = 1000 * 60 * 60; // 1 hour + +export const usePlayerScoresStore = create()( + persist( + (set) => ({ + players: [], + + exists: (playerId: string) => { + const players: Player[] = usePlayerScoresStore.getState().players; + return players.some((player) => player.id == playerId); + }, + + get: (playerId: string) => { + const players: Player[] = usePlayerScoresStore.getState().players; + return players.find((player) => player.id == playerId); + }, + + addPlayer: async ( + playerId: string, + callback?: (page: number, totalPages: number) => void, + ) => { + const players = usePlayerScoresStore.getState().players; + + // Check if the player already exists + if (usePlayerScoresStore.getState().exists(playerId)) { + return { + error: true, + message: "Player already exists", + }; + } + + // Get all of the players scores + const scores = await fetchAllScores( + playerId, + "recent", + (page, totalPages) => { + if (callback) callback(page, totalPages); + }, + ); + + if (scores == undefined) { + return { + error: true, + message: "Could not fetch scores for player", + }; + } + + console.log(scores); + + set({ + players: [ + ...players, + { + id: playerId, + lastUpdated: new Date(), + scores: scores, + }, + ], + }); + return { + error: false, + message: "Player added successfully", + }; + }, + + updatePlayerScores: async () => { + const players = usePlayerScoresStore.getState().players; + + for (const player of players) { + if (player == undefined) continue; + + // Skip if the player was already updated recently + if (player.lastUpdated > new Date(Date.now() - UPDATE_INTERVAL)) + continue; + + console.log(`Updating scores for ${player.id}...`); + + let oldScores = player.scores; + + // Sort the scores by id, so we know when to stop searching for new scores + oldScores = oldScores.sort((a, b) => b.score.id - a.score.id); + + const mostRecentScore = oldScores?.[0].score; + if (mostRecentScore == undefined) continue; + let search = true; + + let page = 0; + let newScoresCount = 0; + while (search) { + page++; + const newScores = await fetchScores(player.id, page); + if (newScores == undefined) continue; + + for (const newScore of newScores.scores) { + if (newScore.score.id == mostRecentScore.id) { + search = false; + break; + } + + // remove the old score + const oldScoreIndex = oldScores.findIndex( + (score) => score.score.id == newScore.score.id, + ); + if (oldScoreIndex != -1) { + oldScores = oldScores.splice(oldScoreIndex, 1); + } + oldScores.push(newScore); + newScoresCount++; + } + } + + let newPlayers = players; + // Remove the player if it already exists + newPlayers = newPlayers.filter((playerr) => playerr.id != player.id); + // Add the player + newPlayers.push({ + lastUpdated: new Date(), + id: player.id, + scores: oldScores, + }); + + console.log(`Found ${newScoresCount} new scores for ${player.id}`); + } + }, + }), + { + name: "playerScores", + storage: createJSONStorage(() => localStorage), + }, + ), +); diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index 4399ba7..c7725e1 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -22,7 +22,9 @@ export const useSettingsStore = create()( setUserId: (userId: string) => { set({ userId }); }, + setProfilePicture: (profilePicture: string) => set({ profilePicture }), + async refreshProfile() { const id = useSettingsStore.getState().userId; if (!id) return; diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index cfc6ae5..92b93bb 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -120,6 +120,7 @@ export async function fetchScores( export async function fetchAllScores( playerId: string, searchType: string, + callback?: (currentPage: number, totalPages: number) => void, ): Promise { const scores = new Array(); @@ -131,15 +132,21 @@ export async function fetchAllScores( done = true; break; } - const { scores } = response; - if (scores.length === 0) { + const { scores: scoresFetched } = response; + if (scoresFetched.length === 0) { done = true; break; } - scores.push(...scores); + scores.push(...scoresFetched); + + if (callback) { + callback(page, response.pageInfo.totalPages); + } page++; } while (!done); + console.log(scores); + return scores as ScoresaberPlayerScore[]; }