import { formatNumberWithCommas, 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"; import { PlayerRankingSkeleton } from "@/components/ranking/player-ranking-skeleton"; import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; import { ScoreSaberPlayersPageToken } from "@ssr/common/types/token/scoresaber/score-saber-players-page-token"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; const PLAYER_NAME_MAX_LENGTH = 18; type MiniProps = { /** * The type of ranking to display. */ type: "Global" | "Country"; /** * The player on this profile. */ player: ScoreSaberPlayer; /** * Whether the data should be updated */ shouldUpdate?: boolean; }; type Variants = { [key: string]: { itemsPerPage: number; icon: (player: ScoreSaberPlayer) => ReactElement; getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => number; query: (page: number, country: string) => Promise; }; }; const miniVariants: Variants = { Global: { itemsPerPage: 50, icon: () => , getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => { return Math.floor((player.rank - 1) / itemsPerPage) + 1; }, query: (page: number) => { return scoresaberService.lookupPlayers(page); }, }, Country: { itemsPerPage: 50, icon: (player: ScoreSaberPlayer) => { return ; }, getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => { return Math.floor((player.countryRank - 1) / itemsPerPage) + 1; }, query: (page: number, country: string) => { return scoresaberService.lookupPlayersByCountry(page, country); }, }, }; export default function Mini({ type, player, shouldUpdate }: MiniProps) { if (shouldUpdate == undefined) { // Default to true shouldUpdate = true; } 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 > 1) { // Allow page 1 to be valid // 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; }, enabled: shouldUpdate, }); let players = data; // So we can update it later if (players && (!isLoading || !isError)) { // Find the player's position in the list const playerPosition = players.findIndex(p => p.id === player.id); // Ensure we always show 5 players const start = Math.max(0, playerPosition - 3); // Start showing 3 players above the player, but not less than index 0 const end = Math.min(players.length, start + 5); // Ensure there are 5 players shown players = players.slice(start, end); // If there are less than 5 players at the top, append more players from below if (players.length < 5 && start === 0) { const additionalPlayers = players.slice(playerPosition + 1, playerPosition + (5 - players.length + 1)); players = [...players, ...additionalPlayers]; } } if (isLoading) { return ; } return (
{icon}

{type} Ranking

{isError &&

Error

} {players?.map((playerRanking, index) => { const rank = type == "Global" ? playerRanking.rank : playerRanking.countryRank; const playerName = playerRanking.name.length > PLAYER_NAME_MAX_LENGTH ? playerRanking.name.substring(0, PLAYER_NAME_MAX_LENGTH) + "..." : playerRanking.name; const ppDifference = playerRanking.pp - player.pp; return (

#{formatNumberWithCommas(rank)}

{playerName}

{formatPp(playerRanking.pp)}pp

{playerRanking.id !== player.id && (

0 ? "text-green-400" : "text-red-400"}`}> {ppDifference > 0 ? "+" : ""} {formatPp(ppDifference)}

)}
); })}
); }