load initial scores server sided to prevent flashing
All checks were successful
Deploy SSR / deploy (push) Successful in 1m12s

This commit is contained in:
Lee 2024-09-12 12:47:21 +01:00
parent 4d19fd3bfd
commit aba0d4ba57
4 changed files with 38 additions and 42 deletions

@ -43,6 +43,7 @@ export default async function Search({ params: { slug } }: Props) {
const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method
const page = parseInt(slug[2]) || 1; // The page number const page = parseInt(slug[2]) || 1; // The page number
const player = await scoresaberFetcher.lookupPlayer(id, false); const player = await scoresaberFetcher.lookupPlayer(id, false);
const scores = await scoresaberFetcher.lookupPlayerScores(id, sort, page);
if (player == undefined) { if (player == undefined) {
// Invalid player id // Invalid player id
@ -51,7 +52,7 @@ export default async function Search({ params: { slug } }: Props) {
return ( return (
<div className="flex flex-col h-full w-full"> <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> </div>
); );
} }

@ -18,11 +18,12 @@ class BeatSaverFetcher extends DataFetcher {
* @returns the map that match the query, or undefined if no map were found * @returns the map that match the query, or undefined if no map were found
*/ */
async getMapBsr(query: string, useProxy = true): Promise<string | undefined> { 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); const map = await db.beatSaverMaps.get(query);
// The map is cached // The map is cached
if (map != undefined) { if (map != undefined) {
this.log(`Found cached bsr ${map.bsr} for map hash ${query}`);
return map.bsr; return map.bsr;
} }
@ -42,6 +43,7 @@ class BeatSaverFetcher extends DataFetcher {
hash: query, hash: query,
bsr: bsr, bsr: bsr,
}); });
this.log(`Looked up bsr ${bsr} for map hash ${query}`);
return bsr; return bsr;
} }
} }

@ -3,6 +3,7 @@
import { scoresaberFetcher } from "@/common/data-fetcher/impl/scoresaber"; import { scoresaberFetcher } from "@/common/data-fetcher/impl/scoresaber";
import { ScoreSort } from "@/common/data-fetcher/sort"; import { ScoreSort } from "@/common/data-fetcher/sort";
import ScoreSaberPlayer from "@/common/data-fetcher/types/scoresaber/scoresaber-player"; 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 { useQuery } from "@tanstack/react-query";
import PlayerHeader from "./player-header"; import PlayerHeader from "./player-header";
import PlayerRankChart from "./player-rank-chart"; import PlayerRankChart from "./player-rank-chart";
@ -12,11 +13,12 @@ const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
type Props = { type Props = {
initalPlayerData: ScoreSaberPlayer; initalPlayerData: ScoreSaberPlayer;
initialScoreData?: ScoreSaberPlayerScoresPage;
sort: ScoreSort; sort: ScoreSort;
page: number; page: number;
}; };
export default function PlayerData({ initalPlayerData, sort, page }: Props) { export default function PlayerData({ initalPlayerData, initialScoreData, sort, page }: Props) {
let player = initalPlayerData; let player = initalPlayerData;
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["player", player.id], queryKey: ["player", player.id],
@ -36,7 +38,7 @@ export default function PlayerData({ initalPlayerData, sort, page }: Props) {
<PlayerRankChart player={player} /> <PlayerRankChart player={player} />
</> </>
)} )}
<PlayerScores player={player} sort={sort} page={page} /> <PlayerScores initialScoreData={initialScoreData} player={player} sort={sort} page={page} />
</div> </div>
); );
} }

@ -15,70 +15,63 @@ import { Button } from "../ui/button";
import Score from "./score/score"; import Score from "./score/score";
type Props = { type Props = {
initialScoreData?: ScoreSaberPlayerScoresPage;
player: ScoreSaberPlayer; player: ScoreSaberPlayer;
sort: ScoreSort; sort: ScoreSort;
page: number; page: number;
}; };
export default function PlayerScores({ player, sort, page }: Props) { export default function PlayerScores({ initialScoreData, player, sort, page }: Props) {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const controls = useAnimation(); const controls = useAnimation();
const [currentSort, setCurrentSort] = useState(sort); const [currentSort, setCurrentSort] = useState(sort);
const [currentPage, setCurrentPage] = useState(page); 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], queryKey: ["playerScores", player.id, currentSort, currentPage],
queryFn: () => scoresaberFetcher.lookupPlayerScores(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(() => { const handleAnimation = useCallback(() => {
controls.set({ controls.set({ x: -50, opacity: 0 });
x: -50, controls.start({ x: 0, opacity: 1, transition: { duration: 0.25 } });
opacity: 0,
});
controls.start({
x: 0,
opacity: 1,
transition: { duration: 0.25 },
});
}, [controls]); }, [controls]);
useEffect(() => { useEffect(() => {
if (data == undefined) { if (scores) {
return; handleAnimation();
} }
setPreviousScores(data); }, [scores, handleAnimation]);
handleAnimation();
}, [data, handleAnimation]);
useEffect(() => { useEffect(() => {
// Update URL and refetch data when currentSort or currentPage changes
const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`; const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`;
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl); window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl);
refetch(); refetch();
}, [currentSort, currentPage, refetch, player.id]); }, [currentSort, currentPage, refetch, player.id]);
/** const handleSortChange = (newSort: ScoreSort) => {
* Updates the current sort and resets the page to 1
*/
function handleSortChange(newSort: ScoreSort) {
if (newSort !== currentSort) { if (newSort !== currentSort) {
setCurrentSort(newSort); setCurrentSort(newSort);
setCurrentPage(1); // Reset the page setCurrentPage(1); // Reset page to 1 on sort change
} }
} };
if (previousScores === undefined) { if (scores === undefined) {
return null; return undefined;
} }
if (isError) { if (isError) {
return ( return (
<Card className="gap-2"> <Card className="gap-2">
<p>Oopsies!</p> <p>Oopsies! Something went wrong.</p>
</Card> </Card>
); );
} }
@ -86,20 +79,20 @@ export default function PlayerScores({ player, sort, page }: Props) {
return ( return (
<Card className="flex gap-4"> <Card className="flex gap-4">
<div className="flex items-center flex-row w-full gap-2 justify-center"> <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 <Button
variant={sort == currentSort ? "default" : "outline"} variant={sortOption === currentSort ? "default" : "outline"}
key={index} key={sortOption}
onClick={() => handleSortChange(sort as ScoreSort)} onClick={() => handleSortChange(sortOption)}
> >
{capitalizeFirstLetter(sort)} {capitalizeFirstLetter(sortOption)}
</Button> </Button>
))} ))}
</div> </div>
<motion.div animate={controls}> <motion.div animate={controls}>
<div className="grid min-w-full grid-cols-1 divide-y divide-border"> <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} /> <Score key={index} playerScore={playerScore} />
))} ))}
</div> </div>
@ -108,11 +101,9 @@ export default function PlayerScores({ player, sort, page }: Props) {
<Pagination <Pagination
mobilePagination={width < 768} mobilePagination={width < 768}
page={currentPage} page={currentPage}
totalPages={Math.ceil(previousScores.metadata.total / previousScores.metadata.itemsPerPage)} totalPages={Math.ceil(scores.metadata.total / scores.metadata.itemsPerPage)}
loadingPage={isLoading ? currentPage : undefined} loadingPage={isLoading ? currentPage : undefined}
onPageChange={(newPage) => { onPageChange={setCurrentPage}
setCurrentPage(newPage);
}}
/> />
</Card> </Card>
); );