redesign leaderboard scores
This commit is contained in:
parent
caf5f01a09
commit
16c34adc19
@ -45,10 +45,7 @@ export default class LeaderboardService {
|
||||
* @param id the players id
|
||||
* @returns the scores
|
||||
*/
|
||||
public static async getLeaderboard(
|
||||
leaderboardName: Leaderboards,
|
||||
id: string
|
||||
): Promise<LeaderboardResponse<Leaderboard>> {
|
||||
public static async getLeaderboard<L>(leaderboardName: Leaderboards, id: string): Promise<LeaderboardResponse<L>> {
|
||||
let leaderboard: Leaderboard | undefined;
|
||||
let beatSaverMap: BeatSaverMap | undefined;
|
||||
|
||||
@ -71,7 +68,7 @@ export default class LeaderboardService {
|
||||
}
|
||||
|
||||
return {
|
||||
leaderboard: leaderboard,
|
||||
leaderboard: leaderboard as L,
|
||||
beatsaver: beatSaverMap,
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import { isProduction } from "@ssr/common/utils/utils";
|
||||
import { Metadata } from "@ssr/common/types/metadata";
|
||||
import { NotFoundError } from "elysia";
|
||||
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 { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||
import { ScoreSort } from "@ssr/common/score/score-sort";
|
||||
@ -154,7 +156,7 @@ export class ScoreService {
|
||||
);
|
||||
|
||||
for (const token of leaderboardScores.playerScores) {
|
||||
const score = getScoreSaberScoreFromToken(token.score);
|
||||
const score = getScoreSaberScoreFromToken(token.score, token.leaderboard);
|
||||
if (score == undefined) {
|
||||
continue;
|
||||
}
|
||||
@ -206,7 +208,10 @@ export class ScoreService {
|
||||
|
||||
switch (leaderboardName) {
|
||||
case "scoresaber": {
|
||||
const leaderboardResponse = await LeaderboardService.getLeaderboard(leaderboardName, id);
|
||||
const leaderboardResponse = await LeaderboardService.getLeaderboard<ScoreSaberLeaderboard>(
|
||||
leaderboardName,
|
||||
id
|
||||
);
|
||||
if (leaderboardResponse == undefined) {
|
||||
throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`);
|
||||
}
|
||||
@ -219,7 +224,7 @@ export class ScoreService {
|
||||
}
|
||||
|
||||
for (const token of leaderboardScores.scores) {
|
||||
const score = getScoreSaberScoreFromToken(token);
|
||||
const score = getScoreSaberScoreFromToken(token, leaderboardResponse.leaderboard);
|
||||
if (score == undefined) {
|
||||
continue;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { Modifier } from "../modifier";
|
||||
import ScoreSaberScoreToken from "../../types/token/scoresaber/score-saber-score-token";
|
||||
import ScoreSaberLeaderboardPlayerInfoToken from "../../types/token/scoresaber/score-saber-leaderboard-player-info-token";
|
||||
import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token";
|
||||
import ScoreSaberLeaderboard from "../../leaderboard/impl/scoresaber-leaderboard";
|
||||
|
||||
export default interface ScoreSaberScore extends Score {
|
||||
/**
|
||||
@ -41,7 +42,7 @@ export default interface ScoreSaberScore extends Score {
|
||||
*/
|
||||
export function getScoreSaberScoreFromToken(
|
||||
token: ScoreSaberScoreToken,
|
||||
leaderboard?: ScoreSaberLeaderboardToken
|
||||
leaderboard?: ScoreSaberLeaderboardToken | ScoreSaberLeaderboard
|
||||
): ScoreSaberScore {
|
||||
const modifiers: Modifier[] =
|
||||
token.modifiers == undefined || token.modifiers === ""
|
||||
|
@ -123,5 +123,11 @@ export default async function LeaderboardPage(props: Props) {
|
||||
if (response == undefined) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -6,7 +6,7 @@ import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
|
||||
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
||||
import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
|
||||
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
|
||||
|
||||
@ -22,12 +22,17 @@ type LeaderboardDataProps = {
|
||||
* The initial score data.
|
||||
*/
|
||||
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 [currentLeaderboard, setCurrentLeaderboard] = useState(initialLeaderboard);
|
||||
|
||||
let leaderboard = initialLeaderboard;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["leaderboard", currentLeaderboardId],
|
||||
queryFn: async (): Promise<LeaderboardResponse<ScoreSaberLeaderboard> | undefined> => {
|
||||
@ -37,20 +42,23 @@ export function LeaderboardData({ initialLeaderboard, initialScores }: Leaderboa
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
|
||||
if (data && (!isLoading || !isError)) {
|
||||
leaderboard = data;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setCurrentLeaderboard(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col-reverse xl:flex-row w-full gap-2">
|
||||
<LeaderboardScores
|
||||
leaderboard={leaderboard.leaderboard}
|
||||
leaderboard={currentLeaderboard.leaderboard}
|
||||
initialScores={initialScores}
|
||||
initialPage={initialPage}
|
||||
leaderboardChanged={newId => setCurrentLeaderboardId(newId)}
|
||||
showDifficulties
|
||||
isLeaderboardPage
|
||||
/>
|
||||
<LeaderboardInfo leaderboard={leaderboard.leaderboard} beatSaverMap={leaderboard.beatsaver} />
|
||||
<LeaderboardInfo leaderboard={currentLeaderboard.leaderboard} beatSaverMap={currentLeaderboard.beatsaver} />
|
||||
</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 { 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 = {
|
||||
/**
|
||||
* The player who set the score.
|
||||
*/
|
||||
player?: ScoreSaberPlayer;
|
||||
|
||||
/**
|
||||
* The score to display.
|
||||
*/
|
||||
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 (
|
||||
<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]">
|
||||
<ScoreRankInfo score={score} leaderboard={leaderboard} />
|
||||
<LeaderboardPlayer player={player} score={score} />
|
||||
<LeaderboardScoreStats score={score} leaderboard={leaderboard} />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{/* Score Rank */}
|
||||
<td className="px-4 py-2 whitespace-nowrap">#{score.rank}</td>
|
||||
|
||||
{/* Player */}
|
||||
<td className="px-4 py-2 flex gap-2 whitespace-nowrap">
|
||||
<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 { Button } from "@/components/ui/button";
|
||||
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 ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
|
||||
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 useDatabase from "@/hooks/use-database";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
|
||||
type LeaderboardScoresProps = {
|
||||
/**
|
||||
* The page to show when opening the leaderboard.
|
||||
*/
|
||||
initialPage?: number;
|
||||
|
||||
/**
|
||||
* The initial scores to show.
|
||||
*/
|
||||
initialScores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>;
|
||||
|
||||
/**
|
||||
* The leaderboard to display.
|
||||
*/
|
||||
leaderboard: ScoreSaberLeaderboard;
|
||||
|
||||
/**
|
||||
* The player who set the score.
|
||||
*/
|
||||
player?: ScoreSaberPlayer;
|
||||
|
||||
/**
|
||||
* Whether to show the difficulties.
|
||||
*/
|
||||
showDifficulties?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this is the full leaderboard page.
|
||||
*/
|
||||
isLeaderboardPage?: boolean;
|
||||
|
||||
/**
|
||||
* Called when the leaderboard changes.
|
||||
*
|
||||
* @param id the new leaderboard id
|
||||
*/
|
||||
leaderboardChanged?: (id: number) => void;
|
||||
};
|
||||
|
||||
export default function LeaderboardScores({
|
||||
initialPage,
|
||||
initialScores,
|
||||
player,
|
||||
leaderboard,
|
||||
showDifficulties,
|
||||
isLeaderboardPage,
|
||||
@ -68,6 +38,9 @@ export default function LeaderboardScores({
|
||||
if (!initialPage) {
|
||||
initialPage = 1;
|
||||
}
|
||||
const database = useDatabase();
|
||||
const claimedPlayer = useLiveQuery(() => database.getClaimedPlayer());
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const controls = useAnimation();
|
||||
|
||||
@ -78,7 +51,7 @@ export default function LeaderboardScores({
|
||||
LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard> | undefined
|
||||
>(initialScores);
|
||||
const topOfScoresRef = useRef<HTMLDivElement>(null);
|
||||
const [shouldFetch, setShouldFetch] = useState(true);
|
||||
const [shouldFetch, setShouldFetch] = useState(false);
|
||||
|
||||
const { data, isError, isLoading } = useQuery({
|
||||
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 () => {
|
||||
await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft");
|
||||
@ -189,20 +162,28 @@ export default function LeaderboardScores({
|
||||
})}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={controls}
|
||||
variants={scoreAnimation}
|
||||
className="grid min-w-full grid-cols-1 divide-y divide-border"
|
||||
>
|
||||
{currentScores.scores.map((playerScore, index) => {
|
||||
return (
|
||||
<motion.div key={index} variants={scoreAnimation}>
|
||||
<LeaderboardScore key={index} player={player} score={playerScore} leaderboard={leaderboard} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full table-auto border-spacing-2 border-none text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2">Rank</th>
|
||||
<th className="px-4 py-2">Player</th>
|
||||
<th className="px-4 py-2 text-center">Time Set</th>
|
||||
<th className="px-4 py-2 text-center">Score</th>
|
||||
<th className="px-4 py-2 text-center">Accuracy</th>
|
||||
<th className="px-4 py-2 text-center">Misses</th>
|
||||
<th className="px-4 py-2 text-center">PP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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
|
||||
mobilePagination={width < 768}
|
||||
|
@ -222,12 +222,7 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
|
||||
>
|
||||
{scores.scores.map((score, index) => (
|
||||
<motion.div key={score.score.id} variants={scoreAnimation}>
|
||||
<Score
|
||||
player={player}
|
||||
score={score.score}
|
||||
leaderboard={score.leaderboard}
|
||||
beatSaverMap={score.beatSaver}
|
||||
/>
|
||||
<Score score={score.score} leaderboard={score.leaderboard} beatSaverMap={score.beatSaver} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
|
||||
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 Link from "next/link";
|
||||
import useDatabase from "@/hooks/use-database";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
import { clsx } from "clsx";
|
||||
import { TablePlayer } from "@/components/table-player";
|
||||
|
||||
type PlayerRankingProps = {
|
||||
player: ScoreSaberPlayerToken;
|
||||
@ -16,7 +14,7 @@ type PlayerRankingProps = {
|
||||
|
||||
export function PlayerRanking({ player, isCountry }: PlayerRankingProps) {
|
||||
const database = useDatabase();
|
||||
const settings = useLiveQuery(() => database.getSettings());
|
||||
const claimedPlayer = useLiveQuery(() => database.getClaimedPlayer());
|
||||
|
||||
const history = player.histories.split(",").map(Number);
|
||||
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>
|
||||
</td>
|
||||
<td className="flex items-center gap-2 px-4 py-2">
|
||||
<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 == settings?.playerId ? "transform-gpu text-pp transition-all hover:brightness-75" : ""
|
||||
}
|
||||
>
|
||||
{player.name}
|
||||
</p>
|
||||
</Link>
|
||||
<TablePlayer player={player} claimedPlayer={claimedPlayer} />
|
||||
</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>
|
||||
|
@ -7,7 +7,6 @@ import ScoreSongInfo from "./score-info";
|
||||
import ScoreRankInfo from "./score-rank-info";
|
||||
import ScoreStats from "./score-stats";
|
||||
import { motion } from "framer-motion";
|
||||
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||
import { getPageFromRank } from "@ssr/common/utils/utils";
|
||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The player who set the score.
|
||||
*/
|
||||
player?: ScoreSaberPlayer;
|
||||
|
||||
/**
|
||||
* 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 [baseScore, setBaseScore] = useState<number>(score.score);
|
||||
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
|
||||
@ -109,7 +103,7 @@ export default function Score({ player, leaderboard, beatSaverMap, score, settin
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
36
projects/website/src/components/table-player.tsx
Normal file
36
projects/website/src/components/table-player.tsx
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user