cleanup stat changes and add ranked and total scores change
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 46s
Deploy Website / docker (ubuntu-latest) (push) Successful in 2m13s

This commit is contained in:
Lee 2024-10-20 19:17:17 +01:00
parent a68e53734d
commit 336518ff70
8 changed files with 214 additions and 105 deletions

@ -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<string, PlayerStatValue> = {
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",
},
};

@ -0,0 +1 @@
export type StatTimeframe = "daily" | "weekly" | "monthly";

@ -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);
}

@ -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,

@ -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 (
<Tooltip display={tooltip} side="bottom">
<p className={`text-sm ${change > 0 ? "text-green-400" : "text-red-400"}`}>
{change > 0 ? "+" : ""}
{format(change)}
</p>
</Tooltip>
);
};
/**
* 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 (
<p>
{capitalizeFirstLetter(timeFrame)} Change:{" "}
<span
className={`${value == undefined ? "" : value >= 0 ? (value == 0 ? "" : "text-green-500") : "text-red-500"}`}
>
{format(value)}
</span>
</p>
);
};
// Don't show change if the player is banned or inactive
if (player.banned || player.inactive) {
return children;
}
return (
<Tooltip
side="bottom"
display={
<div>
{renderChange(todayStat, "daily")}
{renderChange(weeklyStat, "weekly")}
{renderChange(monthlyStat, "monthly")}
</div>
}
>
{children}
</Tooltip>
);
};
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 (
<div className="text-gray-300 flex gap-1 items-center">
{renderChange(
player,
"rank",
<ChangeOverTime player={player} type={PlayerStat.Rank}>
<Link href={`/ranking/${player.rankPages.global}`}>
<p className="hover:brightness-[66%] transition-all transform-gpu">
#{formatNumberWithCommas(player.rank)}
</p>
</Link>
)}
{rankChange != 0 && renderDailyChange(rankChange, <p>The change in rank compared to yesterday</p>)}
</ChangeOverTime>
<DailyChange type={PlayerStat.Rank} change={rankChange} />
</div>
);
},
@ -131,16 +50,14 @@ const playerData = [
return (
<div className="text-gray-300 flex gap-1 items-center">
{renderChange(
player,
"countryRank",
<ChangeOverTime player={player} type={PlayerStat.CountryRank}>
<Link href={`/ranking/${player.country}/${player.rankPages.country}`}>
<p className="hover:brightness-[66%] transition-all transform-gpu">
#{formatNumberWithCommas(player.countryRank)}
</p>
</Link>
)}
{rankChange != 0 && renderDailyChange(rankChange, <p>The change in country rank compared to yesterday</p>)}
</ChangeOverTime>
<DailyChange type={PlayerStat.CountryRank} change={rankChange} />
</div>
);
},
@ -153,23 +70,24 @@ const playerData = [
return (
<div className="text-gray-300 flex gap-1 items-center">
{renderChange(
player,
"pp",
<p className="hover:brightness-[66%] transition-all transform-gpu text-pp">{formatPp(player.pp)}pp</p>
)}
{ppChange != 0 && renderDailyChange(ppChange, <p>The change in pp compared to yesterday</p>)}
<ChangeOverTime player={player} type={PlayerStat.PerformancePoints}>
<p className="hover:brightness-[66%] transition-all transform-gpu">{formatPp(player.pp)}pp</p>
</ChangeOverTime>
<DailyChange type={PlayerStat.PerformancePoints} change={ppChange} />
</div>
);
},
},
];
type Props = {
type PlayerHeaderProps = {
/**
* The player to display.
*/
player: ScoreSaberPlayer;
};
export default function PlayerHeader({ player }: Props) {
export default function PlayerHeader({ player }: PlayerHeaderProps) {
return (
<Card>
<div className="flex gap-3 flex-col items-center text-center lg:flex-row lg:items-start lg:text-start relative select-none">

@ -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)}{" "}
<DailyChange type={PlayerStat.RankedPlayCount} change={rankedScores} />
</>
),
};
},
},
@ -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)}{" "}
<DailyChange type={PlayerStat.TotalPlayCount} change={totalChange} />
</>
),
};
},
},
@ -84,7 +105,7 @@ type Props = {
export default function PlayerStats({ player }: Props) {
return (
<div className={`flex flex-wrap gap-2 w-full justify-center lg:justify-start`}>
{badges.map((badge, index) => {
{playerStats.map((badge, index) => {
const toRender = badge.create(player);
if (toRender === undefined) {
return <div key={index} />;

@ -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") => (
<p>
{capitalizeFirstLetter(timeFrame)} Change:{" "}
<span className={value === undefined ? "" : value >= 0 ? (value === 0 ? "" : "text-green-500") : "text-red-500"}>
{formatChangeValue(value)}
</span>
</p>
);
// Return children if player is banned or inactive
if (player.banned || player.inactive) {
return children;
}
return (
<Tooltip
side="bottom"
display={
<div>
{renderChange(todayStat, "daily")}
{renderChange(weeklyStat, "weekly")}
{renderChange(monthlyStat, "monthly")}
</div>
}
>
{children}
</Tooltip>
);
}

@ -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 = (
<p className={`text-sm ${change > 0 ? "text-green-400" : "text-red-400"}`}>
{change > 0 ? "+" : ""}
{formatValue(change)}
</p>
);
if (!tooltip) {
tooltip = `${type.displayName} change compared to yesterday`;
}
return (
<Tooltip display={tooltip} side="bottom">
{value}
</Tooltip>
);
}