load initial scores server sided to prevent flashing
All checks were successful
Deploy SSR / deploy (push) Successful in 1m12s
All checks were successful
Deploy SSR / deploy (push) Successful in 1m12s
This commit is contained in:
parent
4d19fd3bfd
commit
aba0d4ba57
@ -43,6 +43,7 @@ export default async function Search({ params: { slug } }: Props) {
|
||||
const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method
|
||||
const page = parseInt(slug[2]) || 1; // The page number
|
||||
const player = await scoresaberFetcher.lookupPlayer(id, false);
|
||||
const scores = await scoresaberFetcher.lookupPlayerScores(id, sort, page);
|
||||
|
||||
if (player == undefined) {
|
||||
// Invalid player id
|
||||
@ -51,7 +52,7 @@ export default async function Search({ params: { slug } }: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<PlayerData initalPlayerData={player} sort={sort} page={page} />
|
||||
<PlayerData initalPlayerData={player} initialScoreData={scores} sort={sort} page={page} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -18,11 +18,12 @@ class BeatSaverFetcher extends DataFetcher {
|
||||
* @returns the map that match the query, or undefined if no map were found
|
||||
*/
|
||||
async getMapBsr(query: string, useProxy = true): Promise<string | undefined> {
|
||||
this.log(`Looking up the bsr for the map with hash ${query}...`);
|
||||
this.log(`Looking up the bsr for map hash ${query}...`);
|
||||
|
||||
const map = await db.beatSaverMaps.get(query);
|
||||
// The map is cached
|
||||
if (map != undefined) {
|
||||
this.log(`Found cached bsr ${map.bsr} for map hash ${query}`);
|
||||
return map.bsr;
|
||||
}
|
||||
|
||||
@ -42,6 +43,7 @@ class BeatSaverFetcher extends DataFetcher {
|
||||
hash: query,
|
||||
bsr: bsr,
|
||||
});
|
||||
this.log(`Looked up bsr ${bsr} for map hash ${query}`);
|
||||
return bsr;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { scoresaberFetcher } from "@/common/data-fetcher/impl/scoresaber";
|
||||
import { ScoreSort } from "@/common/data-fetcher/sort";
|
||||
import ScoreSaberPlayer from "@/common/data-fetcher/types/scoresaber/scoresaber-player";
|
||||
import ScoreSaberPlayerScoresPage from "@/common/data-fetcher/types/scoresaber/scoresaber-player-scores-page";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import PlayerHeader from "./player-header";
|
||||
import PlayerRankChart from "./player-rank-chart";
|
||||
@ -12,11 +13,12 @@ const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type Props = {
|
||||
initalPlayerData: ScoreSaberPlayer;
|
||||
initialScoreData?: ScoreSaberPlayerScoresPage;
|
||||
sort: ScoreSort;
|
||||
page: number;
|
||||
};
|
||||
|
||||
export default function PlayerData({ initalPlayerData, sort, page }: Props) {
|
||||
export default function PlayerData({ initalPlayerData, initialScoreData, sort, page }: Props) {
|
||||
let player = initalPlayerData;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["player", player.id],
|
||||
@ -36,7 +38,7 @@ export default function PlayerData({ initalPlayerData, sort, page }: Props) {
|
||||
<PlayerRankChart player={player} />
|
||||
</>
|
||||
)}
|
||||
<PlayerScores player={player} sort={sort} page={page} />
|
||||
<PlayerScores initialScoreData={initialScoreData} player={player} sort={sort} page={page} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,70 +15,63 @@ import { Button } from "../ui/button";
|
||||
import Score from "./score/score";
|
||||
|
||||
type Props = {
|
||||
initialScoreData?: ScoreSaberPlayerScoresPage;
|
||||
player: ScoreSaberPlayer;
|
||||
sort: ScoreSort;
|
||||
page: number;
|
||||
};
|
||||
|
||||
export default function PlayerScores({ player, sort, page }: Props) {
|
||||
export default function PlayerScores({ initialScoreData, player, sort, page }: Props) {
|
||||
const { width } = useWindowDimensions();
|
||||
const controls = useAnimation();
|
||||
|
||||
const [currentSort, setCurrentSort] = useState(sort);
|
||||
const [currentPage, setCurrentPage] = useState(page);
|
||||
const [previousScores, setPreviousScores] = useState<ScoreSaberPlayerScoresPage | undefined>();
|
||||
|
||||
const { data, isError, isLoading, refetch } = useQuery({
|
||||
const {
|
||||
data: scores,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["playerScores", player.id, currentSort, currentPage],
|
||||
queryFn: () => scoresaberFetcher.lookupPlayerScores(player.id, currentSort, currentPage),
|
||||
staleTime: 30 * 1000, // Data will be cached for 30 seconds
|
||||
staleTime: 30 * 1000, // Cache data for 30 seconds
|
||||
initialData: initialScoreData,
|
||||
});
|
||||
|
||||
const handleAnimation = useCallback(() => {
|
||||
controls.set({
|
||||
x: -50,
|
||||
opacity: 0,
|
||||
});
|
||||
controls.start({
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.25 },
|
||||
});
|
||||
controls.set({ x: -50, opacity: 0 });
|
||||
controls.start({ x: 0, opacity: 1, transition: { duration: 0.25 } });
|
||||
}, [controls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data == undefined) {
|
||||
return;
|
||||
if (scores) {
|
||||
handleAnimation();
|
||||
}
|
||||
setPreviousScores(data);
|
||||
handleAnimation();
|
||||
}, [data, handleAnimation]);
|
||||
}, [scores, handleAnimation]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update URL and refetch data when currentSort or currentPage changes
|
||||
const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`;
|
||||
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl);
|
||||
refetch();
|
||||
}, [currentSort, currentPage, refetch, player.id]);
|
||||
|
||||
/**
|
||||
* Updates the current sort and resets the page to 1
|
||||
*/
|
||||
function handleSortChange(newSort: ScoreSort) {
|
||||
const handleSortChange = (newSort: ScoreSort) => {
|
||||
if (newSort !== currentSort) {
|
||||
setCurrentSort(newSort);
|
||||
setCurrentPage(1); // Reset the page
|
||||
setCurrentPage(1); // Reset page to 1 on sort change
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (previousScores === undefined) {
|
||||
return null;
|
||||
if (scores === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Card className="gap-2">
|
||||
<p>Oopsies!</p>
|
||||
<p>Oopsies! Something went wrong.</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -86,20 +79,20 @@ export default function PlayerScores({ player, sort, page }: Props) {
|
||||
return (
|
||||
<Card className="flex gap-4">
|
||||
<div className="flex items-center flex-row w-full gap-2 justify-center">
|
||||
{Object.keys(ScoreSort).map((sort, index) => (
|
||||
{Object.values(ScoreSort).map((sortOption) => (
|
||||
<Button
|
||||
variant={sort == currentSort ? "default" : "outline"}
|
||||
key={index}
|
||||
onClick={() => handleSortChange(sort as ScoreSort)}
|
||||
variant={sortOption === currentSort ? "default" : "outline"}
|
||||
key={sortOption}
|
||||
onClick={() => handleSortChange(sortOption)}
|
||||
>
|
||||
{capitalizeFirstLetter(sort)}
|
||||
{capitalizeFirstLetter(sortOption)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div animate={controls}>
|
||||
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
|
||||
{previousScores.playerScores.map((playerScore, index) => (
|
||||
{scores.playerScores.map((playerScore, index) => (
|
||||
<Score key={index} playerScore={playerScore} />
|
||||
))}
|
||||
</div>
|
||||
@ -108,11 +101,9 @@ export default function PlayerScores({ player, sort, page }: Props) {
|
||||
<Pagination
|
||||
mobilePagination={width < 768}
|
||||
page={currentPage}
|
||||
totalPages={Math.ceil(previousScores.metadata.total / previousScores.metadata.itemsPerPage)}
|
||||
totalPages={Math.ceil(scores.metadata.total / scores.metadata.itemsPerPage)}
|
||||
loadingPage={isLoading ? currentPage : undefined}
|
||||
onPageChange={(newPage) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
Reference in New Issue
Block a user