added local score fetching
All checks were successful
deploy / deploy (push) Successful in 2m11s

This commit is contained in:
Lee 2023-10-22 02:17:21 +01:00
parent 0a4708c6bc
commit 1ff7c246c3
6 changed files with 218 additions and 19 deletions

View File

@ -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");
}

View File

@ -5,7 +5,12 @@ import Navbar from "./Navbar";
export default function Container({ children }: { children: React.ReactNode }) {
return (
<>
<ToastContainer className="z-50" position="top-right" theme="dark" />
<ToastContainer
className="z-50"
position="top-right"
theme="dark"
pauseOnFocusLoss={false}
/>
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
<Navbar />
<div className="w-full flex-1">{children}</div>

View File

@ -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 (
<>

View File

@ -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<PlayerScoresStore>()(
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),
},
),
);

View File

@ -22,7 +22,9 @@ export const useSettingsStore = create<SettingsStore>()(
setUserId: (userId: string) => {
set({ userId });
},
setProfilePicture: (profilePicture: string) => set({ profilePicture }),
async refreshProfile() {
const id = useSettingsStore.getState().userId;
if (!id) return;

View File

@ -120,6 +120,7 @@ export async function fetchScores(
export async function fetchAllScores(
playerId: string,
searchType: string,
callback?: (currentPage: number, totalPages: number) => void,
): Promise<ScoresaberPlayerScore[] | undefined> {
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[];
}