This commit is contained in:
parent
0a4708c6bc
commit
1ff7c246c3
@ -10,6 +10,7 @@ import Score from "@/components/Score";
|
|||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||||
|
import { usePlayerScoresStore } from "@/store/playerScoresStore";
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import { formatNumber } from "@/utils/number";
|
import { formatNumber } from "@/utils/number";
|
||||||
import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api";
|
import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api";
|
||||||
@ -22,7 +23,7 @@ import {
|
|||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
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 ReactCountryFlag from "react-country-flag";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
@ -60,13 +61,9 @@ const sortTypes: { [key: string]: SortType } = {
|
|||||||
const DEFAULT_SORT_TYPE = sortTypes.top;
|
const DEFAULT_SORT_TYPE = sortTypes.top;
|
||||||
|
|
||||||
export default function Player({ params }: { params: { id: string } }) {
|
export default function Player({ params }: { params: { id: string } }) {
|
||||||
const settingsStore = useStore(useSettingsStore, (state) => {
|
const settingsStore = useStore(useSettingsStore, (store) => store);
|
||||||
return {
|
const playerScoreStore = useStore(usePlayerScoresStore, (store) => store);
|
||||||
userId: state.userId,
|
|
||||||
setUserId: state.setUserId,
|
|
||||||
refreshProfile: state.refreshProfile,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -141,9 +138,42 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
[params.id, router, scores],
|
[params.id, router, scores],
|
||||||
);
|
);
|
||||||
|
|
||||||
function claimProfile() {
|
const toastId: any = useRef(null);
|
||||||
|
|
||||||
|
async function claimProfile() {
|
||||||
settingsStore?.setUserId(params.id);
|
settingsStore?.setUserId(params.id);
|
||||||
settingsStore?.refreshProfile();
|
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");
|
toast.success("Successfully claimed profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,12 @@ import Navbar from "./Navbar";
|
|||||||
export default function Container({ children }: { children: React.ReactNode }) {
|
export default function Container({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
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]">
|
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="w-full flex-1">{children}</div>
|
<div className="w-full flex-1">{children}</div>
|
||||||
|
@ -41,12 +41,7 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const settingsStore = useStore(useSettingsStore, (state) => {
|
const settingsStore = useStore(useSettingsStore, (state) => state);
|
||||||
return {
|
|
||||||
profilePicture: state.profilePicture,
|
|
||||||
userId: state.userId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
160
src/store/playerScoresStore.ts
Normal file
160
src/store/playerScoresStore.ts
Normal 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),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
@ -22,7 +22,9 @@ export const useSettingsStore = create<SettingsStore>()(
|
|||||||
setUserId: (userId: string) => {
|
setUserId: (userId: string) => {
|
||||||
set({ userId });
|
set({ userId });
|
||||||
},
|
},
|
||||||
|
|
||||||
setProfilePicture: (profilePicture: string) => set({ profilePicture }),
|
setProfilePicture: (profilePicture: string) => set({ profilePicture }),
|
||||||
|
|
||||||
async refreshProfile() {
|
async refreshProfile() {
|
||||||
const id = useSettingsStore.getState().userId;
|
const id = useSettingsStore.getState().userId;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
@ -120,6 +120,7 @@ export async function fetchScores(
|
|||||||
export async function fetchAllScores(
|
export async function fetchAllScores(
|
||||||
playerId: string,
|
playerId: string,
|
||||||
searchType: string,
|
searchType: string,
|
||||||
|
callback?: (currentPage: number, totalPages: number) => void,
|
||||||
): Promise<ScoresaberPlayerScore[] | undefined> {
|
): Promise<ScoresaberPlayerScore[] | undefined> {
|
||||||
const scores = new Array();
|
const scores = new Array();
|
||||||
|
|
||||||
@ -131,15 +132,21 @@ export async function fetchAllScores(
|
|||||||
done = true;
|
done = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const { scores } = response;
|
const { scores: scoresFetched } = response;
|
||||||
if (scores.length === 0) {
|
if (scoresFetched.length === 0) {
|
||||||
done = true;
|
done = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
scores.push(...scores);
|
scores.push(...scoresFetched);
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(page, response.pageInfo.totalPages);
|
||||||
|
}
|
||||||
page++;
|
page++;
|
||||||
} while (!done);
|
} while (!done);
|
||||||
|
|
||||||
|
console.log(scores);
|
||||||
|
|
||||||
return scores as ScoresaberPlayerScore[];
|
return scores as ScoresaberPlayerScore[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user