redesign leaderboard scores
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 45s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m9s

This commit is contained in:
Lee 2024-10-19 14:11:43 +01:00
parent caf5f01a09
commit 16c34adc19
14 changed files with 152 additions and 238 deletions

@ -45,10 +45,7 @@ export default class LeaderboardService {
* @param id the players id * @param id the players id
* @returns the scores * @returns the scores
*/ */
public static async getLeaderboard( public static async getLeaderboard<L>(leaderboardName: Leaderboards, id: string): Promise<LeaderboardResponse<L>> {
leaderboardName: Leaderboards,
id: string
): Promise<LeaderboardResponse<Leaderboard>> {
let leaderboard: Leaderboard | undefined; let leaderboard: Leaderboard | undefined;
let beatSaverMap: BeatSaverMap | undefined; let beatSaverMap: BeatSaverMap | undefined;
@ -71,7 +68,7 @@ export default class LeaderboardService {
} }
return { return {
leaderboard: leaderboard, leaderboard: leaderboard as L,
beatsaver: beatSaverMap, beatsaver: beatSaverMap,
}; };
} }

@ -4,7 +4,9 @@ import { isProduction } from "@ssr/common/utils/utils";
import { Metadata } from "@ssr/common/types/metadata"; import { Metadata } from "@ssr/common/types/metadata";
import { NotFoundError } from "elysia"; import { NotFoundError } from "elysia";
import BeatSaverService from "./beatsaver.service"; import BeatSaverService from "./beatsaver.service";
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import ScoreSaberLeaderboard, {
getScoreSaberLeaderboardFromToken,
} from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import { getScoreSaberScoreFromToken } from "@ssr/common/score/impl/scoresaber-score"; import { getScoreSaberScoreFromToken } from "@ssr/common/score/impl/scoresaber-score";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { ScoreSort } from "@ssr/common/score/score-sort"; import { ScoreSort } from "@ssr/common/score/score-sort";
@ -154,7 +156,7 @@ export class ScoreService {
); );
for (const token of leaderboardScores.playerScores) { for (const token of leaderboardScores.playerScores) {
const score = getScoreSaberScoreFromToken(token.score); const score = getScoreSaberScoreFromToken(token.score, token.leaderboard);
if (score == undefined) { if (score == undefined) {
continue; continue;
} }
@ -206,7 +208,10 @@ export class ScoreService {
switch (leaderboardName) { switch (leaderboardName) {
case "scoresaber": { case "scoresaber": {
const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id); const leaderboardResponse = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>(
leaderboardName,
id
);
if (leaderboardResponse == undefined) { if (leaderboardResponse == undefined) {
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
} }
@ -219,7 +224,7 @@ export class ScoreService {
} }
for (const token of leaderboardScores.scores) { for (const token of leaderboardScores.scores) {
const score = getScoreSaberScoreFromToken(token); const score = getScoreSaberScoreFromToken(token, leaderboardResponse.leaderboard);
if (score == undefined) { if (score == undefined) {
continue; continue;
} }

@ -3,6 +3,7 @@ import { Modifier } from "../modifier";
import ScoreSaberScoreToken from "../../types/token/scoresaber/score-saber-score-token"; import ScoreSaberScoreToken from "../../types/token/scoresaber/score-saber-score-token";
import ScoreSaberLeaderboardPlayerInfoToken from "../../types/token/scoresaber/score-saber-leaderboard-player-info-token"; import ScoreSaberLeaderboardPlayerInfoToken from "../../types/token/scoresaber/score-saber-leaderboard-player-info-token";
import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token";
import ScoreSaberLeaderboard from "../../leaderboard/impl/scoresaber-leaderboard";
export default interface ScoreSaberScore extends Score { export default interface ScoreSaberScore extends Score {
/** /**
@ -41,7 +42,7 @@ export default interface ScoreSaberScore extends Score {
*/ */
export function getScoreSaberScoreFromToken( export function getScoreSaberScoreFromToken(
token: ScoreSaberScoreToken, token: ScoreSaberScoreToken,
leaderboard?: ScoreSaberLeaderboardToken leaderboard?: ScoreSaberLeaderboardToken | ScoreSaberLeaderboard
): ScoreSaberScore { ): ScoreSaberScore {
const modifiers: Modifier[] = const modifiers: Modifier[] =
token.modifiers == undefined || token.modifiers === "" token.modifiers == undefined || token.modifiers === ""

@ -123,5 +123,11 @@ export default async function LeaderboardPage(props: Props) {
if (response == undefined) { if (response == undefined) {
return redirect("/"); return redirect("/");
} }
return <LeaderboardData initialLeaderboard={response.leaderboardResponse} initialScores={response.scores} />; return (
<LeaderboardData
initialLeaderboard={response.leaderboardResponse}
initialScores={response.scores}
initialPage={response.page}
/>
);
} }

@ -71,6 +71,17 @@ export default class Database extends Dexie {
return this.settings.update(SETTINGS_ID, settings); return this.settings.update(SETTINGS_ID, settings);
} }
/**
* Gets the claimed player's scoresaber token
*/
async getClaimedPlayer(): Promise<ScoreSaberPlayerToken | undefined> {
const settings = await this.getSettings();
if (settings == undefined || settings.playerId == undefined) {
return;
}
return scoresaberService.lookupPlayer(settings.playerId, true);
}
/** /**
* Adds a friend * Adds a friend
* *

@ -6,7 +6,7 @@ import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response"; import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useEffect, useState } from "react";
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
@ -22,12 +22,17 @@ type LeaderboardDataProps = {
* The initial score data. * The initial score data.
*/ */
initialScores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>; initialScores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>;
/**
* The initial page.
*/
initialPage?: number;
}; };
export function LeaderboardData({ initialLeaderboard, initialScores }: LeaderboardDataProps) { export function LeaderboardData({ initialLeaderboard, initialScores, initialPage }: LeaderboardDataProps) {
const [currentLeaderboardId, setCurrentLeaderboardId] = useState(initialLeaderboard.leaderboard.id); const [currentLeaderboardId, setCurrentLeaderboardId] = useState(initialLeaderboard.leaderboard.id);
const [currentLeaderboard, setCurrentLeaderboard] = useState(initialLeaderboard);
let leaderboard = initialLeaderboard;
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["leaderboard", currentLeaderboardId], queryKey: ["leaderboard", currentLeaderboardId],
queryFn: async (): Promise<LeaderboardResponse<ScoreSaberLeaderboard> | undefined> => { queryFn: async (): Promise<LeaderboardResponse<ScoreSaberLeaderboard> | undefined> => {
@ -37,20 +42,23 @@ export function LeaderboardData({ initialLeaderboard, initialScores }: Leaderboa
refetchIntervalInBackground: false, refetchIntervalInBackground: false,
}); });
if (data && (!isLoading || !isError)) { useEffect(() => {
leaderboard = data; if (data) {
setCurrentLeaderboard(data);
} }
}, [data]);
return ( return (
<main className="flex flex-col-reverse xl:flex-row w-full gap-2"> <main className="flex flex-col-reverse xl:flex-row w-full gap-2">
<LeaderboardScores <LeaderboardScores
leaderboard={leaderboard.leaderboard} leaderboard={currentLeaderboard.leaderboard}
initialScores={initialScores} initialScores={initialScores}
initialPage={initialPage}
leaderboardChanged={newId => setCurrentLeaderboardId(newId)} leaderboardChanged={newId => setCurrentLeaderboardId(newId)}
showDifficulties showDifficulties
isLeaderboardPage isLeaderboardPage
/> />
<LeaderboardInfo leaderboard={leaderboard.leaderboard} beatSaverMap={leaderboard.beatsaver} /> <LeaderboardInfo leaderboard={currentLeaderboard.leaderboard} beatSaverMap={currentLeaderboard.beatsaver} />
</main> </main>
); );
} }

@ -1,42 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
type Props = {
/**
* The player who set the score.
*/
player?: ScoreSaberPlayer;
/**
* The score to display.
*/
score: ScoreSaberScore;
};
export default function LeaderboardPlayer({ player, score }: Props) {
const scorePlayer = score.playerInfo;
const isPlayerWhoSetScore = player && scorePlayer.id === player.id;
return (
<div className="flex gap-2">
<Image
unoptimized
src={`https://img.fascinated.cc/upload/w_48,h_48/${scorePlayer.profilePicture}`}
width={48}
height={48}
alt="Song Artwork"
className="rounded-md min-w-[48px]"
priority
/>
<Link
href={`/player/${scorePlayer.id}`}
target="_blank"
className="h-fit hover:brightness-75 transition-all transform-gpu"
>
<p className={`${isPlayerWhoSetScore && "text-pp"}`}>{scorePlayer.name}</p>
</Link>
</div>
);
}

@ -1,74 +0,0 @@
import { getScoreBadgeFromAccuracy } from "@/common/song-utils";
import Tooltip from "@/components/tooltip";
import { ScoreBadge, ScoreBadges } from "@/components/score/score-badge";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import FullComboBadge from "@/components/score/badges/full-combo";
const badges: ScoreBadge[] = [
{
name: "PP",
color: () => {
return "bg-pp";
},
create: (score: ScoreSaberScore) => {
const pp = score.pp;
if (pp === 0) {
return undefined;
}
return `${score.pp.toFixed(2)}pp`;
},
},
{
name: "Accuracy",
color: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => {
const acc = (score.score / leaderboard.maxScore) * 100;
return getScoreBadgeFromAccuracy(acc).color;
},
create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => {
const acc = (score.score / leaderboard.maxScore) * 100;
const scoreBadge = getScoreBadgeFromAccuracy(acc);
let accDetails = `Accuracy ${scoreBadge.name != "-" ? scoreBadge.name : ""}`;
if (scoreBadge.max == null) {
accDetails += ` (> ${scoreBadge.min}%)`;
} else if (scoreBadge.min == null) {
accDetails += ` (< ${scoreBadge.max}%)`;
} else {
accDetails += ` (${scoreBadge.min}% - ${scoreBadge.max}%)`;
}
return (
<>
<Tooltip
display={
<div>
<p>{accDetails}</p>
</div>
}
>
<p className="cursor-default">{acc.toFixed(2)}%</p>
</Tooltip>
</>
);
},
},
{
name: "Full Combo",
create: (score: ScoreSaberScore) => {
return <FullComboBadge score={score} />;
},
},
];
type Props = {
score: ScoreSaberScore;
leaderboard: ScoreSaberLeaderboard;
};
export default function LeaderboardScoreStats({ score, leaderboard }: Props) {
return (
<div className={`grid grid-cols-3 grid-rows-1 gap-1 ml-0 lg:ml-2`}>
<ScoreBadges badges={badges} score={score} leaderboard={leaderboard} />
</div>
);
}

@ -1,35 +1,48 @@
import LeaderboardPlayer from "./leaderboard-player";
import LeaderboardScoreStats from "./leaderboard-score-stats";
import ScoreRankInfo from "@/components/score/score-rank-info";
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
import { timeAgo } from "@ssr/common/utils/time-utils";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { TablePlayer } from "@/components/table-player";
type Props = { type Props = {
/**
* The player who set the score.
*/
player?: ScoreSaberPlayer;
/** /**
* The score to display. * The score to display.
*/ */
score: ScoreSaberScore; score: ScoreSaberScore;
/** /**
* The leaderboard to display. * The claimed player.
*/ */
leaderboard: ScoreSaberLeaderboard; claimedPlayer?: ScoreSaberPlayerToken;
}; };
export default function LeaderboardScore({ player, score, leaderboard }: Props) { export default function LeaderboardScore({ score, claimedPlayer }: Props) {
const scorePlayer = score.playerInfo;
return ( return (
<div className="py-1.5"> <>
<div className="grid items-center w-full gap-2 grid-cols-[20px 1fr_1fr] lg:grid-cols-[130px_4fr_300px]"> {/* Score Rank */}
<ScoreRankInfo score={score} leaderboard={leaderboard} /> <td className="px-4 py-2 whitespace-nowrap">#{score.rank}</td>
<LeaderboardPlayer player={player} score={score} />
<LeaderboardScoreStats score={score} leaderboard={leaderboard} /> {/* Player */}
</div> <td className="px-4 py-2 flex gap-2 whitespace-nowrap">
</div> <TablePlayer player={scorePlayer} claimedPlayer={claimedPlayer} />
</td>
{/* Time Set */}
<td className="px-4 py-2 text-center whitespace-nowrap">{timeAgo(score.timestamp)}</td>
{/* Score */}
<td className="px-4 py-2 text-center whitespace-nowrap">{formatNumberWithCommas(score.score)}</td>
{/* Score Accuracy */}
<td className="px-4 py-2 text-center whitespace-nowrap">{score.accuracy.toFixed(2)}%</td>
{/* Score Misses */}
<td className="px-4 py-2 text-center whitespace-nowrap">{score.misses > 0 ? `${score.misses}x` : "FC"}</td>
{/* Score PP */}
<td className="px-4 py-2 text-center text-pp whitespace-nowrap">{formatPp(score.pp)}pp</td>
</>
); );
} }

@ -10,56 +10,26 @@ import LeaderboardScore from "./leaderboard-score";
import { scoreAnimation } from "@/components/score/score-animation"; import { scoreAnimation } from "@/components/score/score-animation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { getDifficulty, getDifficultyFromRawDifficulty } from "@/common/song-utils"; import { getDifficultyFromRawDifficulty } from "@/common/song-utils";
import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
import useDatabase from "@/hooks/use-database";
import { useLiveQuery } from "dexie-react-hooks";
type LeaderboardScoresProps = { type LeaderboardScoresProps = {
/**
* The page to show when opening the leaderboard.
*/
initialPage?: number; initialPage?: number;
/**
* The initial scores to show.
*/
initialScores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>; initialScores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>;
/**
* The leaderboard to display.
*/
leaderboard: ScoreSaberLeaderboard; leaderboard: ScoreSaberLeaderboard;
/**
* The player who set the score.
*/
player?: ScoreSaberPlayer;
/**
* Whether to show the difficulties.
*/
showDifficulties?: boolean; showDifficulties?: boolean;
/**
* Whether this is the full leaderboard page.
*/
isLeaderboardPage?: boolean; isLeaderboardPage?: boolean;
/**
* Called when the leaderboard changes.
*
* @param id the new leaderboard id
*/
leaderboardChanged?: (id: number) => void; leaderboardChanged?: (id: number) => void;
}; };
export default function LeaderboardScores({ export default function LeaderboardScores({
initialPage, initialPage,
initialScores, initialScores,
player,
leaderboard, leaderboard,
showDifficulties, showDifficulties,
isLeaderboardPage, isLeaderboardPage,
@ -68,6 +38,9 @@ export default function LeaderboardScores({
if (!initialPage) { if (!initialPage) {
initialPage = 1; initialPage = 1;
} }
const database = useDatabase();
const claimedPlayer = useLiveQuery(() => database.getClaimedPlayer());
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const controls = useAnimation(); const controls = useAnimation();
@ -78,7 +51,7 @@ export default function LeaderboardScores({
LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard> | undefined LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard> | undefined
>(initialScores); >(initialScores);
const topOfScoresRef = useRef<HTMLDivElement>(null); const topOfScoresRef = useRef<HTMLDivElement>(null);
const [shouldFetch, setShouldFetch] = useState(true); const [shouldFetch, setShouldFetch] = useState(false);
const { data, isError, isLoading } = useQuery({ const { data, isError, isLoading } = useQuery({
queryKey: ["leaderboardScores", selectedLeaderboardId, currentPage], queryKey: ["leaderboardScores", selectedLeaderboardId, currentPage],
@ -93,7 +66,7 @@ export default function LeaderboardScores({
}); });
/** /**
* Starts the animation for the scores. * Starts the animation for the scores, but only after the initial load.
*/ */
const handleScoreAnimation = useCallback(async () => { const handleScoreAnimation = useCallback(async () => {
await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft"); await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft");
@ -189,20 +162,28 @@ export default function LeaderboardScores({
})} })}
</div> </div>
<motion.div <div className="overflow-x-auto">
initial="hidden" <table className="table w-full table-auto border-spacing-2 border-none text-left">
animate={controls} <thead>
variants={scoreAnimation} <tr>
className="grid min-w-full grid-cols-1 divide-y divide-border" <th className="px-4 py-2">Rank</th>
> <th className="px-4 py-2">Player</th>
{currentScores.scores.map((playerScore, index) => { <th className="px-4 py-2 text-center">Time Set</th>
return ( <th className="px-4 py-2 text-center">Score</th>
<motion.div key={index} variants={scoreAnimation}> <th className="px-4 py-2 text-center">Accuracy</th>
<LeaderboardScore key={index} player={player} score={playerScore} leaderboard={leaderboard} /> <th className="px-4 py-2 text-center">Misses</th>
</motion.div> <th className="px-4 py-2 text-center">PP</th>
); </tr>
})} </thead>
</motion.div> <motion.tbody initial="hidden" animate={controls} className="border-none" variants={scoreAnimation}>
{currentScores.scores.map((playerScore, index) => (
<motion.tr key={index} className="border-b border-border" variants={scoreAnimation}>
<LeaderboardScore score={playerScore} claimedPlayer={claimedPlayer} />
</motion.tr>
))}
</motion.tbody>
</table>
</div>
<Pagination <Pagination
mobilePagination={width < 768} mobilePagination={width < 768}

@ -222,12 +222,7 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
> >
{scores.scores.map((score, index) => ( {scores.scores.map((score, index) => (
<motion.div key={score.score.id} variants={scoreAnimation}> <motion.div key={score.score.id} variants={scoreAnimation}>
<Score <Score score={score.score} leaderboard={score.leaderboard} beatSaverMap={score.beatSaver} />
player={player}
score={score.score}
leaderboard={score.leaderboard}
beatSaverMap={score.beatSaver}
/>
</motion.div> </motion.div>
))} ))}
</motion.div> </motion.div>

@ -1,13 +1,11 @@
"use client"; "use client";
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils"; import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
import CountryFlag from "@/components/country-flag";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import Link from "next/link";
import useDatabase from "@/hooks/use-database"; import useDatabase from "@/hooks/use-database";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { TablePlayer } from "@/components/table-player";
type PlayerRankingProps = { type PlayerRankingProps = {
player: ScoreSaberPlayerToken; player: ScoreSaberPlayerToken;
@ -16,7 +14,7 @@ type PlayerRankingProps = {
export function PlayerRanking({ player, isCountry }: PlayerRankingProps) { export function PlayerRanking({ player, isCountry }: PlayerRankingProps) {
const database = useDatabase(); const database = useDatabase();
const settings = useLiveQuery(() => database.getSettings()); const claimedPlayer = useLiveQuery(() => database.getClaimedPlayer());
const history = player.histories.split(",").map(Number); const history = player.histories.split(",").map(Number);
const weeklyRankChange = history[history?.length - 6] - player.rank; const weeklyRankChange = history[history?.length - 6] - player.rank;
@ -28,22 +26,7 @@ export function PlayerRanking({ player, isCountry }: PlayerRankingProps) {
<span className="text-sm">{isCountry && "(#" + formatNumberWithCommas(player.rank) + ")"}</span> <span className="text-sm">{isCountry && "(#" + formatNumberWithCommas(player.rank) + ")"}</span>
</td> </td>
<td className="flex items-center gap-2 px-4 py-2"> <td className="flex items-center gap-2 px-4 py-2">
<Avatar className="w-[24px] h-[24px] pointer-events-none"> <TablePlayer player={player} claimedPlayer={claimedPlayer} />
<AvatarImage
alt="Profile Picture"
src={`https://img.fascinated.cc/upload/w_128,h_128/${player.profilePicture}`}
/>
</Avatar>
<CountryFlag code={player.country} size={12} />
<Link className="transform-gpu transition-all hover:text-blue-500" href={`/player/${player.id}`}>
<p
className={
player.id == settings?.playerId ? "transform-gpu text-pp transition-all hover:brightness-75" : ""
}
>
{player.name}
</p>
</Link>
</td> </td>
<td className="px-4 py-2 text-pp text-center">{formatPp(player.pp)}pp</td> <td className="px-4 py-2 text-pp text-center">{formatPp(player.pp)}pp</td>
<td className="px-4 py-2 text-center">{formatNumberWithCommas(player.scoreStats.totalPlayCount)}</td> <td className="px-4 py-2 text-center">{formatNumberWithCommas(player.scoreStats.totalPlayCount)}</td>

@ -7,7 +7,6 @@ import ScoreSongInfo from "./score-info";
import ScoreRankInfo from "./score-rank-info"; import ScoreRankInfo from "./score-rank-info";
import ScoreStats from "./score-stats"; import ScoreStats from "./score-stats";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
import { getPageFromRank } from "@ssr/common/utils/utils"; import { getPageFromRank } from "@ssr/common/utils/utils";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
@ -16,11 +15,6 @@ import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map";
import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsMobile } from "@/hooks/use-is-mobile";
type Props = { type Props = {
/**
* The player who set the score.
*/
player?: ScoreSaberPlayer;
/** /**
* The leaderboard. * The leaderboard.
*/ */
@ -44,7 +38,7 @@ type Props = {
}; };
}; };
export default function Score({ player, leaderboard, beatSaverMap, score, settings }: Props) { export default function Score({ leaderboard, beatSaverMap, score, settings }: Props) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [baseScore, setBaseScore] = useState<number>(score.score); const [baseScore, setBaseScore] = useState<number>(score.score);
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false); const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
@ -109,7 +103,7 @@ export default function Score({ player, leaderboard, beatSaverMap, score, settin
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="w-full mt-2" className="w-full mt-2"
> >
<LeaderboardScores initialPage={getPageFromRank(score.rank, 12)} player={player} leaderboard={leaderboard} /> <LeaderboardScores initialPage={getPageFromRank(score.rank, 12)} leaderboard={leaderboard} />
</motion.div> </motion.div>
)} )}
</div> </div>

@ -0,0 +1,36 @@
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import CountryFlag from "@/components/country-flag";
import Link from "next/link";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import ScoreSaberLeaderboardPlayerInfoToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-player-info-token";
type TablePlayerProps = {
/**
* The player to display.
*/
player: ScoreSaberPlayerToken | ScoreSaberLeaderboardPlayerInfoToken;
/**
* The claimed player.
*/
claimedPlayer?: ScoreSaberPlayerToken;
};
export function TablePlayer({ player, claimedPlayer }: TablePlayerProps) {
return (
<>
<Avatar className="w-[24px] h-[24px] pointer-events-none">
<AvatarImage
alt="Profile Picture"
src={`https://img.fascinated.cc/upload/w_128,h_128/${player.profilePicture}`}
/>
</Avatar>
<CountryFlag code={player.country} size={12} />
<Link className="transform-gpu transition-all hover:text-blue-500" href={`/player/${player.id}`}>
<p className={player.id == claimedPlayer?.id ? "transform-gpu text-pp transition-all hover:brightness-75" : ""}>
{player.name}
</p>
</Link>
</>
);
}