This repository has been archived on 2024-10-29. You can view files and clone it, but cannot push or open issues or pull requests.
Files
Liam a7465a6dcd
All checks were successful
Deploy / deploy (push) Successful in 2m23s
add global and country ranking cards on the player page
2024-09-27 21:19:44 +01:00

132 lines
4.6 KiB
TypeScript

import { leaderboards } from "@/common/leaderboards";
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
import { ScoreSaberPlayersPageToken } from "@/common/model/token/scoresaber/score-saber-players-page-token";
import { formatPp } from "@/common/number-utils";
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { ReactElement } from "react";
import Card from "../card";
import CountryFlag from "../country-flag";
import { Avatar, AvatarImage } from "../ui/avatar";
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
type MiniProps = {
type: "Global" | "Country";
player: ScoreSaberPlayerToken;
};
type Variants = {
[key: string]: {
itemsPerPage: number;
icon: (player: ScoreSaberPlayerToken) => ReactElement;
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => number;
query: (page: number, country: string) => Promise<ScoreSaberPlayersPageToken | undefined>;
};
};
const miniVariants: Variants = {
Global: {
itemsPerPage: 50,
icon: () => <GlobeAmericasIcon className="w-6 h-6" />,
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => {
return Math.floor((player.rank - 1) / itemsPerPage) + 1;
},
query: (page: number) => {
return leaderboards.ScoreSaber.queries.lookupGlobalPlayers(page);
},
},
Country: {
itemsPerPage: 50,
icon: (player: ScoreSaberPlayerToken) => {
return <CountryFlag code={player.country} size={12} />;
},
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => {
return Math.floor((player.countryRank - 1) / itemsPerPage) + 1;
},
query: (page: number, country: string) => {
return leaderboards.ScoreSaber.queries.lookupGlobalPlayersByCountry(page, country);
},
},
};
export default function Mini({ type, player }: MiniProps) {
const variant = miniVariants[type];
const icon = variant.icon(player);
const itemsPerPage = variant.itemsPerPage;
const page = variant.getPage(player, itemsPerPage);
const rankWithinPage = player.rank % itemsPerPage;
const { data, isLoading, isError } = useQuery({
queryKey: ["player-" + type, player.id, type, page],
queryFn: async () => {
// Determine pages to search based on player's rank within the page
const pagesToSearch = [page];
if (rankWithinPage < 5 && page > 0) {
// Player is near the start of the page, so search the previous page too
pagesToSearch.push(page - 1);
}
if (rankWithinPage > itemsPerPage - 5) {
// Player is near the end of the page, so search the next page too
pagesToSearch.push(page + 1);
}
// Fetch players from the determined pages
const players: ScoreSaberPlayerToken[] = [];
for (const p of pagesToSearch) {
const response = await variant.query(p, player.country);
if (response === undefined) {
return undefined;
}
players.push(...response.players);
}
return players;
},
refetchInterval: REFRESH_INTERVAL,
});
let players = data; // So we can update it later
if (players && (!isLoading || !isError)) {
// Find the player's position and show 3 players above and 1 below
const playerPosition = players.findIndex((p) => p.id === player.id);
players = players.slice(playerPosition - 3, playerPosition + 2);
}
return (
<Card className="w-full flex gap-2">
<div className="flex gap-2">
{icon}
<p>{type} Ranking</p>
</div>
<div className="flex flex-col">
{isLoading && <p className="text-gray-400">Loading...</p>}
{isError && <p className="text-red-500">Error</p>}
{players?.map((player, index) => {
const rank = type == "Global" ? player.rank : player.countryRank;
return (
<Link
key={index}
href={`/player/${player.id}`}
className="flex justify-between gap-2 bg-accent px-2 py-1.5 cursor-pointer transform-gpu transition-all hover:brightness-75 first:rounded-t last:rounded-b"
>
<div className="flex gap-2">
<p className="text-gray-400">#{rank}</p>
<Avatar className="w-6 h-6 pointer-events-none">
<AvatarImage alt="Profile Picture" src={player.profilePicture} />
</Avatar>
<p>{player.name}</p>
</div>
<p className="text-pp">{formatPp(player.pp)}pp</p>
</Link>
);
})}
</div>
</Card>
);
}