diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index f8accd6..404d0ae 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -1,19 +1,9 @@ "use client"; -import { usePlayerScoresStore } from "@/store/playerScoresStore"; - type AppProviderProps = { children: React.ReactNode; }; -usePlayerScoresStore.getState().updatePlayerScores(); -setTimeout( - () => { - usePlayerScoresStore.getState().updatePlayerScores(); - }, - 1000 * 60 * 10, -); // fetch new scores every 10 minutes - export default function AppProvider({ children }: AppProviderProps) { return <>{children}; } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index edefd6e..a39f625 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,7 @@ import clsx from "clsx"; interface ButtonProps { - text: string; + text: JSX.Element | string; url: string; icon?: JSX.Element; className?: string; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 6bfdfe1..3bca09f 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -32,7 +32,7 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) { {children && ( -
+
{children}
)} @@ -61,7 +61,27 @@ export default function Navbar() { )} }> -

No friends, add someone!

+ {settingsStore?.friends.length == 0 ? ( +

No friends, add someone!

+ ) : ( + settingsStore?.friends.map((friend) => { + return ( + + )} + + {!settingsStore?.isFriend(playerId) && !isOwnProfile && ( + )}
diff --git a/src/store/playerScoresStore.ts b/src/store/playerScoresStore.ts index b6b22f5..2518728 100644 --- a/src/store/playerScoresStore.ts +++ b/src/store/playerScoresStore.ts @@ -2,21 +2,23 @@ import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; import { fetchAllScores, fetchScores } from "@/utils/scoresaber/api"; +import moment from "moment"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; +import { useSettingsStore } from "./settingsStore"; type Player = { - lastUpdated: number; id: string; scores: ScoresaberPlayerScore[]; }; interface PlayerScoresStore { + lastUpdated: number; players: Player[]; + setLastUpdated: (lastUpdated: number) => void; exists: (playerId: string) => boolean; get(playerId: string): Player | undefined; - addPlayer: ( playerId: string, callback?: (page: number, totalPages: number) => void, @@ -27,13 +29,18 @@ interface PlayerScoresStore { updatePlayerScores: () => void; } -const UPDATE_INTERVAL = 1000 * 60 * 60; // 1 hour +const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes export const usePlayerScoresStore = create()( persist( (set) => ({ + lastUpdated: 0, players: [], + setLastUpdated: (lastUpdated: number) => { + set({ lastUpdated }); + }, + exists: (playerId: string) => { const players: Player[] = usePlayerScoresStore.getState().players; return players.some((player) => player.id == playerId); @@ -73,15 +80,11 @@ export const usePlayerScoresStore = create()( message: "Could not fetch scores for player", }; } - - console.log(scores); - set({ players: [ ...players, { id: playerId, - lastUpdated: new Date().getTime(), scores: scores, }, ], @@ -93,18 +96,34 @@ export const usePlayerScoresStore = create()( }, updatePlayerScores: async () => { + // Skip if we refreshed the scores recently + const timeUntilRefreshMs = + UPDATE_INTERVAL - + (Date.now() - usePlayerScoresStore.getState().lastUpdated); + if (timeUntilRefreshMs > 0) { + console.log( + "Waiting", + moment.duration(timeUntilRefreshMs).humanize(), + "to refresh scores for players", + ); + setTimeout( + () => usePlayerScoresStore.getState().updatePlayerScores(), + timeUntilRefreshMs, + ); + return; + } + const players = usePlayerScoresStore.getState().players; + const friends = useSettingsStore.getState().friends; + for (const friend of friends) { + players.push({ + id: friend.id, + scores: [], + }); + } 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).getTime() - ) - continue; - console.log(`Updating scores for ${player.id}...`); let oldScores = player.scores; @@ -116,7 +135,9 @@ export const usePlayerScoresStore = create()( return bDate.getTime() - aDate.getTime(); }); - const mostRecentScore = oldScores?.[0].score; + if (!oldScores || oldScores.length == 0) continue; + + const mostRecentScore = oldScores[0].score; if (mostRecentScore == undefined) continue; let search = true; @@ -150,7 +171,6 @@ export const usePlayerScoresStore = create()( newPlayers = newPlayers.filter((playerr) => playerr.id != player.id); // Add the player newPlayers.push({ - lastUpdated: new Date().getTime(), id: player.id, scores: oldScores, }); @@ -158,6 +178,11 @@ export const usePlayerScoresStore = create()( if (newScoresCount > 0) { console.log(`Found ${newScoresCount} new scores for ${player.id}`); } + + set({ + players: newPlayers, + lastUpdated: Date.now(), + }); } }, }), @@ -167,3 +192,8 @@ export const usePlayerScoresStore = create()( }, ), ); + +usePlayerScoresStore.getState().updatePlayerScores(); +setInterval(() => { + usePlayerScoresStore.getState().updatePlayerScores(); +}, UPDATE_INTERVAL); diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index e991553..7963845 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -1,7 +1,9 @@ "use client"; +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { SortType, SortTypes } from "@/types/SortTypes"; import { getPlayerInfo } from "@/utils/scoresaber/api"; +import moment from "moment"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -9,19 +11,30 @@ interface SettingsStore { userId: string | undefined; profilePicture: string | undefined; lastUsedSortType: SortType; + friends: ScoresaberPlayer[]; + profilesLastUpdated: number; setUserId: (userId: string) => void; setProfilePicture: (profilePicture: string) => void; setLastUsedSortType: (sortType: SortType) => void; - refreshProfile: () => void; + addFriend: (friendId: string) => Promise; + removeFriend: (friendId: string) => void; + isFriend: (friendId: string) => boolean; + clearFriends: () => void; + setProfilesLastUpdated: (profilesLastUpdated: number) => void; + refreshProfiles: () => void; } +const UPDATE_INTERVAL = 1000 * 60 * 10; // 10 minutes + export const useSettingsStore = create()( persist( (set) => ({ userId: undefined, profilePicture: undefined, lastUsedSortType: SortTypes.top, + friends: [], + profilesLastUpdated: 0, setUserId: (userId: string) => { set({ userId }); @@ -32,18 +45,77 @@ export const useSettingsStore = create()( setLastUsedSortType: (sortType: SortType) => set({ lastUsedSortType: sortType }), - async refreshProfile() { - const id = useSettingsStore.getState().userId; - if (!id) return; + async addFriend(friendId: string) { + const friends = useSettingsStore.getState().friends; + if (friends.some((friend) => friend.id == friendId)) { + return false; + } - const profile = await getPlayerInfo(id); - if (profile == undefined || profile == null) return; + const friend = await getPlayerInfo(friendId); + if (friend == undefined || friend == null) return false; - useSettingsStore.setState({ - userId: profile.id, - profilePicture: profile.profilePicture, - }); - console.log("Updated profile:", profile.id); + set({ friends: [...friends, friend] }); + return true; + }, + + removeFriend: (friendId: string) => { + const friends = useSettingsStore.getState().friends; + set({ friends: friends.filter((friend) => friend.id != friendId) }); + + return friendId; + }, + + clearFriends: () => set({ friends: [] }), + + isFriend: (friendId: string) => { + const friends: ScoresaberPlayer[] = useSettingsStore.getState().friends; + return friends.some((friend) => friend.id == friendId); + }, + + setProfilesLastUpdated: (profilesLastUpdated: number) => + set({ profilesLastUpdated }), + + async refreshProfiles() { + const timeUntilRefreshMs = + UPDATE_INTERVAL - + (Date.now() - useSettingsStore.getState().profilesLastUpdated); + if (timeUntilRefreshMs > 0) { + console.log( + "Waiting", + moment.duration(timeUntilRefreshMs).humanize(), + "to refresh profiles", + ); + setTimeout(() => this.refreshProfiles(), timeUntilRefreshMs); + return; + } + + const userId = useSettingsStore.getState().userId; + const profiles = + useSettingsStore.getState().friends.map((f) => f.id) ?? []; + if (userId) { + profiles.push(userId); + } + + for (const profileId of profiles) { + const profile = await getPlayerInfo(profileId); + if (profile == undefined || profile == null) return; + + if (this.isFriend(profileId)) { + const friends = useSettingsStore.getState().friends; + const friendIndex = friends.findIndex( + (friend) => friend.id == profileId, + ); + friends[friendIndex] = profile; + set({ friends }); + } else { + this.setProfilePicture(profile.profilePicture); + set({ userId: profile.id }); + } + + console.log("Updated profile:", profile.id); + } + + useSettingsStore.setState({ profilesLastUpdated: Date.now() }); }, }), { @@ -53,4 +125,8 @@ export const useSettingsStore = create()( ), ); -useSettingsStore.getState().refreshProfile(); +useSettingsStore.getState().refreshProfiles(); +setInterval( + () => useSettingsStore.getState().refreshProfiles(), + UPDATE_INTERVAL, +); diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index 92b93bb..b131db7 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -145,8 +145,6 @@ export async function fetchAllScores( page++; } while (!done); - console.log(scores); - return scores as ScoresaberPlayerScore[]; }