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[];
}