From 16c34adc19376a3c6daf1a800199d24ebf2e0d5b Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 19 Oct 2024 14:11:43 +0100 Subject: [PATCH] redesign leaderboard scores --- .../src/service/leaderboard.service.ts | 7 +- projects/backend/src/service/score.service.ts | 13 ++- .../common/src/score/impl/scoresaber-score.ts | 3 +- .../(pages)/leaderboard/[...slug]/page.tsx | 8 +- .../website/src/common/database/database.ts | 11 +++ .../leaderboard/leaderboard-data.tsx | 24 ++++-- .../leaderboard/leaderboard-player.tsx | 42 ---------- .../leaderboard/leaderboard-score-stats.tsx | 74 ----------------- .../leaderboard/leaderboard-score.tsx | 53 ++++++++----- .../leaderboard/leaderboard-scores.tsx | 79 +++++++------------ .../src/components/player/player-scores.tsx | 7 +- .../src/components/ranking/player-ranking.tsx | 23 +----- .../website/src/components/score/score.tsx | 10 +-- .../website/src/components/table-player.tsx | 36 +++++++++ 14 files changed, 152 insertions(+), 238 deletions(-) delete mode 100644 projects/website/src/components/leaderboard/leaderboard-player.tsx delete mode 100644 projects/website/src/components/leaderboard/leaderboard-score-stats.tsx create mode 100644 projects/website/src/components/table-player.tsx diff --git a/projects/backend/src/service/leaderboard.service.ts b/projects/backend/src/service/leaderboard.service.ts index cf1c773..74fcae2 100644 --- a/projects/backend/src/service/leaderboard.service.ts +++ b/projects/backend/src/service/leaderboard.service.ts @@ -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> { + public static async getLeaderboard(leaderboardName: Leaderboards, id: string): Promise> { 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, }; } diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 4fa0c10..4c12b97 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -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( + 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; } diff --git a/projects/common/src/score/impl/scoresaber-score.ts b/projects/common/src/score/impl/scoresaber-score.ts index d2b8520..4706a17 100644 --- a/projects/common/src/score/impl/scoresaber-score.ts +++ b/projects/common/src/score/impl/scoresaber-score.ts @@ -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 === "" diff --git a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx index a30a0ef..a6e7d48 100644 --- a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx @@ -123,5 +123,11 @@ export default async function LeaderboardPage(props: Props) { if (response == undefined) { return redirect("/"); } - return ; + return ( + + ); } diff --git a/projects/website/src/common/database/database.ts b/projects/website/src/common/database/database.ts index 93ea1ab..58fb6eb 100644 --- a/projects/website/src/common/database/database.ts +++ b/projects/website/src/common/database/database.ts @@ -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 { + const settings = await this.getSettings(); + if (settings == undefined || settings.playerId == undefined) { + return; + } + return scoresaberService.lookupPlayer(settings.playerId, true); + } + /** * Adds a friend * diff --git a/projects/website/src/components/leaderboard/leaderboard-data.tsx b/projects/website/src/components/leaderboard/leaderboard-data.tsx index 6d4d7e7..152b8fc 100644 --- a/projects/website/src/components/leaderboard/leaderboard-data.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-data.tsx @@ -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; + + /** + * 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 | 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 (
setCurrentLeaderboardId(newId)} showDifficulties isLeaderboardPage /> - +
); } diff --git a/projects/website/src/components/leaderboard/leaderboard-player.tsx b/projects/website/src/components/leaderboard/leaderboard-player.tsx deleted file mode 100644 index edd0726..0000000 --- a/projects/website/src/components/leaderboard/leaderboard-player.tsx +++ /dev/null @@ -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 ( -
- Song Artwork - -

{scorePlayer.name}

- -
- ); -} diff --git a/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx b/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx deleted file mode 100644 index 6e6c0e3..0000000 --- a/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx +++ /dev/null @@ -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 ( - <> - -

{accDetails}

- - } - > -

{acc.toFixed(2)}%

-
- - ); - }, - }, - { - name: "Full Combo", - create: (score: ScoreSaberScore) => { - return ; - }, - }, -]; - -type Props = { - score: ScoreSaberScore; - leaderboard: ScoreSaberLeaderboard; -}; - -export default function LeaderboardScoreStats({ score, leaderboard }: Props) { - return ( -
- -
- ); -} diff --git a/projects/website/src/components/leaderboard/leaderboard-score.tsx b/projects/website/src/components/leaderboard/leaderboard-score.tsx index 97208ad..8b344a2 100644 --- a/projects/website/src/components/leaderboard/leaderboard-score.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-score.tsx @@ -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 ( -
-
- - - -
-
+ <> + {/* Score Rank */} + #{score.rank} + + {/* Player */} + + + + + {/* Time Set */} + {timeAgo(score.timestamp)} + + {/* Score */} + {formatNumberWithCommas(score.score)} + + {/* Score Accuracy */} + {score.accuracy.toFixed(2)}% + + {/* Score Misses */} + {score.misses > 0 ? `${score.misses}x` : "FC"} + + {/* Score PP */} + {formatPp(score.pp)}pp + ); } diff --git a/projects/website/src/components/leaderboard/leaderboard-scores.tsx b/projects/website/src/components/leaderboard/leaderboard-scores.tsx index c0d365d..3e58913 100644 --- a/projects/website/src/components/leaderboard/leaderboard-scores.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-scores.tsx @@ -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; - - /** - * 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 | undefined >(initialScores); const topOfScoresRef = useRef(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({ })} - - {currentScores.scores.map((playerScore, index) => { - return ( - - - - ); - })} - +
+ + + + + + + + + + + + + + {currentScores.scores.map((playerScore, index) => ( + + + + ))} + +
RankPlayerTime SetScoreAccuracyMissesPP
+
{scores.scores.map((score, index) => ( - + ))} diff --git a/projects/website/src/components/ranking/player-ranking.tsx b/projects/website/src/components/ranking/player-ranking.tsx index 5f54fcb..2d20581 100644 --- a/projects/website/src/components/ranking/player-ranking.tsx +++ b/projects/website/src/components/ranking/player-ranking.tsx @@ -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) { {isCountry && "(#" + formatNumberWithCommas(player.rank) + ")"} - - - - - -

- {player.name} -

- + {formatPp(player.pp)}pp {formatNumberWithCommas(player.scoreStats.totalPlayCount)} diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index f39cf87..53600be 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -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(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" > - + )} diff --git a/projects/website/src/components/table-player.tsx b/projects/website/src/components/table-player.tsx new file mode 100644 index 0000000..725db02 --- /dev/null +++ b/projects/website/src/components/table-player.tsx @@ -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 ( + <> + + + + + +

+ {player.name} +

+ + + ); +}