2024-10-15 19:43:23 +01:00
|
|
|
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
2024-09-27 21:19:44 +01:00
|
|
|
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";
|
2024-10-04 18:25:37 +01:00
|
|
|
import { PlayerRankingSkeleton } from "@/components/ranking/player-ranking-skeleton";
|
2024-10-17 15:30:14 +01:00
|
|
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
2024-10-09 01:17:00 +01:00
|
|
|
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";
|
2024-10-12 03:37:54 +01:00
|
|
|
import { getPageFromRank } from "@ssr/common/utils/utils";
|
2024-09-27 21:19:44 +01:00
|
|
|
|
2024-09-28 05:57:35 +01:00
|
|
|
const PLAYER_NAME_MAX_LENGTH = 18;
|
2024-09-27 21:19:44 +01:00
|
|
|
|
|
|
|
type MiniProps = {
|
2024-10-04 18:25:37 +01:00
|
|
|
/**
|
|
|
|
* The type of ranking to display.
|
|
|
|
*/
|
2024-09-27 21:19:44 +01:00
|
|
|
type: "Global" | "Country";
|
2024-10-04 18:25:37 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The player on this profile.
|
|
|
|
*/
|
2024-09-27 23:04:14 +01:00
|
|
|
player: ScoreSaberPlayer;
|
2024-10-04 18:25:37 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the data should be updated
|
|
|
|
*/
|
|
|
|
shouldUpdate?: boolean;
|
2024-09-27 21:19:44 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
type Variants = {
|
|
|
|
[key: string]: {
|
|
|
|
itemsPerPage: number;
|
2024-09-27 23:04:14 +01:00
|
|
|
icon: (player: ScoreSaberPlayer) => ReactElement;
|
|
|
|
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => number;
|
2024-10-10 01:52:05 +01:00
|
|
|
getRank: (player: ScoreSaberPlayer) => number;
|
2024-09-30 22:16:55 +01:00
|
|
|
query: (page: number, country: string) => Promise<ScoreSaberPlayersPageToken | undefined>;
|
2024-09-27 21:19:44 +01:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const miniVariants: Variants = {
|
|
|
|
Global: {
|
|
|
|
itemsPerPage: 50,
|
|
|
|
icon: () => <GlobeAmericasIcon className="w-6 h-6" />,
|
2024-09-27 23:04:14 +01:00
|
|
|
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => {
|
2024-10-12 03:37:54 +01:00
|
|
|
return getPageFromRank(player.rank - 1, itemsPerPage);
|
2024-09-27 21:19:44 +01:00
|
|
|
},
|
2024-10-10 01:52:05 +01:00
|
|
|
getRank: (player: ScoreSaberPlayer) => {
|
|
|
|
return player.rank;
|
|
|
|
},
|
2024-09-27 21:19:44 +01:00
|
|
|
query: (page: number) => {
|
2024-09-28 12:59:54 +01:00
|
|
|
return scoresaberService.lookupPlayers(page);
|
2024-09-27 21:19:44 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Country: {
|
|
|
|
itemsPerPage: 50,
|
2024-09-27 23:04:14 +01:00
|
|
|
icon: (player: ScoreSaberPlayer) => {
|
2024-09-27 21:19:44 +01:00
|
|
|
return <CountryFlag code={player.country} size={12} />;
|
|
|
|
},
|
2024-09-27 23:04:14 +01:00
|
|
|
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => {
|
2024-10-12 03:37:54 +01:00
|
|
|
return getPageFromRank(player.countryRank - 1, itemsPerPage);
|
2024-09-27 21:19:44 +01:00
|
|
|
},
|
2024-10-10 01:52:05 +01:00
|
|
|
getRank: (player: ScoreSaberPlayer) => {
|
|
|
|
return player.countryRank;
|
|
|
|
},
|
2024-09-27 21:19:44 +01:00
|
|
|
query: (page: number, country: string) => {
|
2024-09-28 12:59:54 +01:00
|
|
|
return scoresaberService.lookupPlayersByCountry(page, country);
|
2024-09-27 21:19:44 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-10-04 18:25:37 +01:00
|
|
|
export default function Mini({ type, player, shouldUpdate }: MiniProps) {
|
|
|
|
if (shouldUpdate == undefined) {
|
|
|
|
shouldUpdate = true;
|
|
|
|
}
|
2024-10-10 01:52:05 +01:00
|
|
|
|
2024-09-27 21:19:44 +01:00
|
|
|
const variant = miniVariants[type];
|
|
|
|
const icon = variant.icon(player);
|
|
|
|
const itemsPerPage = variant.itemsPerPage;
|
2024-10-10 01:52:05 +01:00
|
|
|
|
|
|
|
// Calculate the page and the rank of the player within that page
|
2024-09-27 21:19:44 +01:00
|
|
|
const page = variant.getPage(player, itemsPerPage);
|
2024-10-10 01:52:05 +01:00
|
|
|
const rankWithinPage = variant.getRank(player) % itemsPerPage;
|
2024-09-27 21:19:44 +01:00
|
|
|
|
|
|
|
const { data, isLoading, isError } = useQuery({
|
2024-10-10 01:52:05 +01:00
|
|
|
queryKey: ["mini-ranking-" + type, player.id, type, page],
|
2024-09-27 21:19:44 +01:00
|
|
|
queryFn: async () => {
|
|
|
|
const pagesToSearch = [page];
|
2024-10-04 18:25:37 +01:00
|
|
|
if (rankWithinPage < 5 && page > 1) {
|
2024-09-27 21:19:44 +01:00
|
|
|
pagesToSearch.push(page - 1);
|
|
|
|
}
|
|
|
|
if (rankWithinPage > itemsPerPage - 5) {
|
|
|
|
pagesToSearch.push(page + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
const players: ScoreSaberPlayerToken[] = [];
|
|
|
|
for (const p of pagesToSearch) {
|
|
|
|
const response = await variant.query(p, player.country);
|
2024-10-10 01:52:05 +01:00
|
|
|
if (response) {
|
|
|
|
players.push(...response.players);
|
2024-09-27 21:19:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-10 01:52:05 +01:00
|
|
|
// Sort players by rank to ensure correct order
|
|
|
|
return players.sort((a, b) => {
|
|
|
|
// This is the wrong type but it still works.
|
|
|
|
return variant.getRank(a as unknown as ScoreSaberPlayer) - variant.getRank(b as unknown as ScoreSaberPlayer);
|
|
|
|
});
|
2024-09-27 21:19:44 +01:00
|
|
|
},
|
2024-10-04 18:25:37 +01:00
|
|
|
enabled: shouldUpdate,
|
2024-09-27 21:19:44 +01:00
|
|
|
});
|
|
|
|
|
2024-10-12 07:23:30 +01:00
|
|
|
if (isLoading || !data) {
|
2024-10-10 01:52:05 +01:00
|
|
|
return <PlayerRankingSkeleton />;
|
|
|
|
}
|
|
|
|
|
2024-10-12 07:23:30 +01:00
|
|
|
if (isError) {
|
2024-10-10 01:52:05 +01:00
|
|
|
return <p className="text-red-500">Error loading ranking</p>;
|
|
|
|
}
|
|
|
|
|
|
|
|
let players = data;
|
|
|
|
const playerPosition = players.findIndex(p => p.id === player.id);
|
2024-10-04 18:25:37 +01:00
|
|
|
|
2024-10-10 01:52:05 +01:00
|
|
|
// Always show 5 players: 3 above and 1 below the player
|
|
|
|
const start = Math.max(0, playerPosition - 3);
|
|
|
|
const end = Math.min(players.length, start + 5);
|
2024-10-04 18:25:37 +01:00
|
|
|
|
2024-10-10 01:52:05 +01:00
|
|
|
players = players.slice(start, end);
|
2024-10-04 18:25:37 +01:00
|
|
|
|
2024-10-10 01:52:05 +01:00
|
|
|
// If fewer than 5 players, append/prepend more
|
|
|
|
if (players.length < 5) {
|
|
|
|
const missingPlayers = 5 - players.length;
|
|
|
|
if (start === 0) {
|
|
|
|
const additionalPlayers = players.slice(playerPosition + 1, playerPosition + 1 + missingPlayers);
|
2024-10-04 18:25:37 +01:00
|
|
|
players = [...players, ...additionalPlayers];
|
2024-10-10 01:52:05 +01:00
|
|
|
} else if (end === players.length) {
|
|
|
|
const additionalPlayers = players.slice(Math.max(0, start - missingPlayers), start);
|
|
|
|
players = [...additionalPlayers, ...players];
|
2024-10-04 18:25:37 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-27 21:19:44 +01:00
|
|
|
return (
|
2024-09-27 22:17:10 +01:00
|
|
|
<Card className="w-full flex gap-2 sticky select-none">
|
2024-09-27 21:19:44 +01:00
|
|
|
<div className="flex gap-2">
|
|
|
|
{icon}
|
|
|
|
<p>{type} Ranking</p>
|
|
|
|
</div>
|
2024-09-30 13:21:31 +01:00
|
|
|
<div className="flex flex-col text-sm">
|
2024-10-10 01:52:05 +01:00
|
|
|
{players.map((playerRanking, index) => {
|
2024-09-30 22:16:55 +01:00
|
|
|
const rank = type == "Global" ? playerRanking.rank : playerRanking.countryRank;
|
2024-09-27 21:55:49 +01:00
|
|
|
const playerName =
|
2024-09-27 22:17:10 +01:00
|
|
|
playerRanking.name.length > PLAYER_NAME_MAX_LENGTH
|
|
|
|
? playerRanking.name.substring(0, PLAYER_NAME_MAX_LENGTH) + "..."
|
|
|
|
: playerRanking.name;
|
2024-09-30 13:21:31 +01:00
|
|
|
const ppDifference = playerRanking.pp - player.pp;
|
2024-09-27 21:19:44 +01:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Link
|
|
|
|
key={index}
|
2024-09-27 22:17:10 +01:00
|
|
|
href={`/player/${playerRanking.id}`}
|
2024-09-30 13:21:31 +01:00
|
|
|
className="grid gap-2 grid-cols-[auto_1fr_auto] items-center bg-accent px-2 py-1.5 cursor-pointer transform-gpu transition-all hover:brightness-75 first:rounded-t last:rounded-b"
|
2024-09-27 21:19:44 +01:00
|
|
|
>
|
2024-09-30 13:21:31 +01:00
|
|
|
<p className="text-gray-400">#{formatNumberWithCommas(rank)}</p>
|
2024-10-04 18:25:37 +01:00
|
|
|
<div className="flex gap-2 items-center">
|
2024-09-27 21:19:44 +01:00
|
|
|
<Avatar className="w-6 h-6 pointer-events-none">
|
2024-09-30 22:16:55 +01:00
|
|
|
<AvatarImage alt="Profile Picture" src={playerRanking.profilePicture} />
|
2024-09-27 21:19:44 +01:00
|
|
|
</Avatar>
|
2024-09-30 22:16:55 +01:00
|
|
|
<p className={playerRanking.id === player.id ? "text-pp font-semibold" : ""}>{playerName}</p>
|
2024-09-27 21:19:44 +01:00
|
|
|
</div>
|
2024-10-04 18:25:37 +01:00
|
|
|
<div className="inline-flex min-w-[11.5em] gap-2 items-center">
|
2024-09-30 22:16:55 +01:00
|
|
|
<p className="text-pp text-right">{formatPp(playerRanking.pp)}pp</p>
|
2024-09-30 13:21:31 +01:00
|
|
|
{playerRanking.id !== player.id && (
|
2024-09-30 22:16:55 +01:00
|
|
|
<p className={`text-xs text-right ${ppDifference > 0 ? "text-green-400" : "text-red-400"}`}>
|
2024-09-30 13:21:31 +01:00
|
|
|
{ppDifference > 0 ? "+" : ""}
|
|
|
|
{formatPp(ppDifference)}
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
</div>
|
2024-09-27 21:19:44 +01:00
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</Card>
|
|
|
|
);
|
|
|
|
}
|