diff --git a/projects/common/src/player/player-stat.ts b/projects/common/src/player/player-stat.ts new file mode 100644 index 0000000..d904b9f --- /dev/null +++ b/projects/common/src/player/player-stat.ts @@ -0,0 +1,32 @@ +export type PlayerStatValue = { + /** + * The display name of the stat. + */ + displayName: string; + + /** + * The value of the stat. + */ + value?: "rank" | "countryRank" | "pp"; +}; + +export const PlayerStat: Record = { + Rank: { + displayName: "Rank", + value: "rank", + }, + CountryRank: { + displayName: "Country Rank", + value: "countryRank", + }, + PerformancePoints: { + displayName: "Performance Points", + value: "pp", + }, + TotalPlayCount: { + displayName: "Total Play Count", + }, + RankedPlayCount: { + displayName: "Ranked Play Count", + }, +}; diff --git a/projects/common/src/player/stat-timeframe.ts b/projects/common/src/player/stat-timeframe.ts new file mode 100644 index 0000000..5c14785 --- /dev/null +++ b/projects/common/src/player/stat-timeframe.ts @@ -0,0 +1 @@ +export type StatTimeframe = "daily" | "weekly" | "monthly"; diff --git a/projects/common/src/utils/number-utils.ts b/projects/common/src/utils/number-utils.ts index 512694e..7f4830e 100644 --- a/projects/common/src/utils/number-utils.ts +++ b/projects/common/src/utils/number-utils.ts @@ -27,3 +27,17 @@ export function formatNumberWithCommas(num: number) { export function formatPp(num: number) { return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } + +/** + * Formats the number + * + * @param num the number to format + * @param type the type of number to format + * @returns the formatted number + */ +export function formatNumber(num: number, type: "number" | "pp" = "number") { + if (type == "pp") { + return formatPp(num); + } + return formatNumberWithCommas(num); +} diff --git a/projects/common/src/utils/player-utils.ts b/projects/common/src/utils/player-utils.ts index 3301adf..1b31d73 100644 --- a/projects/common/src/utils/player-utils.ts +++ b/projects/common/src/utils/player-utils.ts @@ -3,6 +3,19 @@ import { kyFetch } from "./utils"; import { Config } from "../config"; import { AroundPlayer } from "../types/around-player"; import { AroundPlayerResponse } from "../response/around-player-response"; +import ScoreSaberPlayer from "../player/impl/scoresaber-player"; +import { formatDateMinimal, getMidnightAlignedDate } from "./time-utils"; + +/** + * Gets the player's history for today. + * + * @param player the player to get the history for + * @returns the player's history + */ +export function getPlayerHistoryToday(player: ScoreSaberPlayer) { + const today = getMidnightAlignedDate(new Date()); + return player.statisticHistory[formatDateMinimal(today)] || {}; +} /** * Sorts the player history based on date, diff --git a/projects/website/src/components/player/player-header.tsx b/projects/website/src/components/player/player-header.tsx index c00f41c..81a3b53 100644 --- a/projects/website/src/components/player/player-header.tsx +++ b/projects/website/src/components/player/player-header.tsx @@ -5,94 +5,15 @@ import CountryFlag from "../country-flag"; import { Avatar, AvatarImage } from "../ui/avatar"; import ClaimProfile from "./claim-profile"; import PlayerStats from "./player-stats"; -import Tooltip from "@/components/tooltip"; -import { ReactElement } from "react"; import PlayerTrackedStatus from "@/components/player/player-tracked-status"; import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; import Link from "next/link"; -import { capitalizeFirstLetter } from "@/common/string-utils"; import AddFriend from "@/components/friend/add-friend"; import PlayerSteamProfile from "@/components/player/player-steam-profile"; import { getScoreSaberRole } from "@ssr/common/utils/scoresaber.util"; - -/** - * Renders the change for a stat. - * - * @param change the amount of change - * @param tooltip the tooltip to display - * @param format the function to format the value - */ -const renderDailyChange = (change: number, tooltip: ReactElement, format?: (value: number) => string) => { - format = format ?? formatNumberWithCommas; - - return ( - -

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

-
- ); -}; - -/** - * Renders the change over time a stat eg: rank, country rank - * - * @param player the player to get the stats for - * @param children the children to render - * @param type the type of stat to get the change for - */ -const renderChange = (player: ScoreSaberPlayer, type: "rank" | "countryRank" | "pp", children: ReactElement) => { - const todayStats = player.statisticChange?.daily; - const weeklyStats = player.statisticChange?.weekly; - const monthlyStats = player.statisticChange?.monthly; - const todayStat = todayStats?.[type]; - const weeklyStat = weeklyStats?.[type]; - const monthlyStat = monthlyStats?.[type]; - - const renderChange = (value: number | undefined, timeFrame: "daily" | "weekly" | "monthly") => { - const format = (value: number | undefined) => { - if (value == 0) { - return 0; - } - if (value == undefined) { - return "No Data"; - } - return type == "pp" ? formatPp(value) + "pp" : formatNumberWithCommas(value); - }; - - return ( -

- {capitalizeFirstLetter(timeFrame)} Change:{" "} - = 0 ? (value == 0 ? "" : "text-green-500") : "text-red-500"}`} - > - {format(value)} - -

- ); - }; - - // Don't show change if the player is banned or inactive - if (player.banned || player.inactive) { - return children; - } - - return ( - - {renderChange(todayStat, "daily")} - {renderChange(weeklyStat, "weekly")} - {renderChange(monthlyStat, "monthly")} - - } - > - {children} - - ); -}; +import { DailyChange } from "@/components/statistic/daily-change"; +import { ChangeOverTime } from "@/components/statistic/change-over-time"; +import { PlayerStat } from "@ssr/common/player/player-stat"; const playerData = [ { @@ -106,16 +27,14 @@ const playerData = [ return (
- {renderChange( - player, - "rank", +

#{formatNumberWithCommas(player.rank)}

- )} - {rankChange != 0 && renderDailyChange(rankChange,

The change in rank compared to yesterday

)} +
+
); }, @@ -131,16 +50,14 @@ const playerData = [ return (
- {renderChange( - player, - "countryRank", +

#{formatNumberWithCommas(player.countryRank)}

- )} - {rankChange != 0 && renderDailyChange(rankChange,

The change in country rank compared to yesterday

)} +
+
); }, @@ -153,23 +70,24 @@ const playerData = [ return (
- {renderChange( - player, - "pp", -

{formatPp(player.pp)}pp

- )} - {ppChange != 0 && renderDailyChange(ppChange,

The change in pp compared to yesterday

)} + +

{formatPp(player.pp)}pp

+
+
); }, }, ]; -type Props = { +type PlayerHeaderProps = { + /** + * The player to display. + */ player: ScoreSaberPlayer; }; -export default function PlayerHeader({ player }: Props) { +export default function PlayerHeader({ player }: PlayerHeaderProps) { return (
diff --git a/projects/website/src/components/player/player-stats.tsx b/projects/website/src/components/player/player-stats.tsx index 8f4b36d..9618306 100644 --- a/projects/website/src/components/player/player-stats.tsx +++ b/projects/website/src/components/player/player-stats.tsx @@ -4,8 +4,11 @@ import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; import { formatDate } from "@ssr/common/utils/time-utils"; import { ReactNode } from "react"; import Tooltip from "@/components/tooltip"; +import { getPlayerHistoryToday } from "@ssr/common/utils/player-utils"; +import { DailyChange } from "@/components/statistic/daily-change"; +import { PlayerStat } from "@ssr/common/player/player-stat"; -type Badge = { +type Stat = { name: string; color?: string; create: (player: ScoreSaberPlayer) => { @@ -14,13 +17,21 @@ type Badge = { }; }; -const badges: Badge[] = [ +const playerStats: Stat[] = [ { name: "Ranked Play Count", color: "bg-pp", create: (player: ScoreSaberPlayer) => { + const history = getPlayerHistoryToday(player); + const rankedScores = history.scores?.rankedScores; + return { - value: formatNumberWithCommas(player.statistics.rankedPlayCount), + value: ( + <> + {formatNumberWithCommas(player.statistics.rankedPlayCount)}{" "} + + + ), }; }, }, @@ -45,8 +56,18 @@ const badges: Badge[] = [ { name: "Total Play Count", create: (player: ScoreSaberPlayer) => { + const history = getPlayerHistoryToday(player); + const rankedScores = history.scores?.rankedScores; + const unrankedScores = history.scores?.unrankedScores; + const totalChange = (rankedScores ?? 0) + (unrankedScores ?? 0); + return { - value: formatNumberWithCommas(player.statistics.totalPlayCount), + value: ( + <> + {formatNumberWithCommas(player.statistics.totalPlayCount)}{" "} + + + ), }; }, }, @@ -84,7 +105,7 @@ type Props = { export default function PlayerStats({ player }: Props) { return (
- {badges.map((badge, index) => { + {playerStats.map((badge, index) => { const toRender = badge.create(player); if (toRender === undefined) { return
; diff --git a/projects/website/src/components/statistic/change-over-time.tsx b/projects/website/src/components/statistic/change-over-time.tsx new file mode 100644 index 0000000..f6976b5 --- /dev/null +++ b/projects/website/src/components/statistic/change-over-time.tsx @@ -0,0 +1,74 @@ +import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; +import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils"; +import { capitalizeFirstLetter } from "@/common/string-utils"; +import Tooltip from "@/components/tooltip"; +import { PlayerStatValue } from "@ssr/common/player/player-stat"; +import { ReactElement } from "react"; + +type ChangeOverTimeProps = { + /** + * The player to get the stats for + */ + player: ScoreSaberPlayer; + + /** + * The type of stat to get the change for + */ + type: PlayerStatValue; + + /** + * The children to render + */ + children: ReactElement; +}; + +export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps) { + const todayStats = player.statisticChange?.daily; + const weeklyStats = player.statisticChange?.weekly; + const monthlyStats = player.statisticChange?.monthly; + + const todayStat = todayStats?.[type.value!]; + const weeklyStat = weeklyStats?.[type.value!]; + const monthlyStat = monthlyStats?.[type.value!]; + + // Format values based on stat type + const formatChangeValue = (value: number | undefined): string | number => { + if (value === 0) { + return 0; + } + if (value === undefined) { + return "No Data"; + } + return type.value === "pp" ? formatPp(value) + "pp" : formatNumberWithCommas(value); + }; + + // Renders the change for a given time frame + const renderChange = (value: number | undefined, timeFrame: "daily" | "weekly" | "monthly") => ( +

+ {capitalizeFirstLetter(timeFrame)} Change:{" "} + = 0 ? (value === 0 ? "" : "text-green-500") : "text-red-500"}> + {formatChangeValue(value)} + +

+ ); + + // Return children if player is banned or inactive + if (player.banned || player.inactive) { + return children; + } + + return ( + + {renderChange(todayStat, "daily")} + {renderChange(weeklyStat, "weekly")} + {renderChange(monthlyStat, "monthly")} +
+ } + > + {children} + + ); +} diff --git a/projects/website/src/components/statistic/daily-change.tsx b/projects/website/src/components/statistic/daily-change.tsx new file mode 100644 index 0000000..3496c44 --- /dev/null +++ b/projects/website/src/components/statistic/daily-change.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import Tooltip from "@/components/tooltip"; +import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; +import { PlayerStatValue } from "@ssr/common/player/player-stat"; + +// Props for DailyChangeComponent +interface DailyChangeProps { + type: PlayerStatValue; + change: number | undefined; + tooltip?: React.ReactElement | string; + format?: (value: number) => string; +} + +export function DailyChange({ type, change, tooltip, format }: DailyChangeProps) { + const formatValue = format ?? formatNumberWithCommas; + if (change === 0 || change === undefined) { + return null; + } + + const value = ( +

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

+ ); + + if (!tooltip) { + tooltip = `${type.displayName} change compared to yesterday`; + } + + return ( + + {value} + + ); +}