cleanup stat changes and add ranked and total scores change
This commit is contained in:
parent
a68e53734d
commit
336518ff70
32
projects/common/src/player/player-stat.ts
Normal file
32
projects/common/src/player/player-stat.ts
Normal file
@ -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",
|
||||
},
|
||||
};
|
1
projects/common/src/player/stat-timeframe.ts
Normal file
1
projects/common/src/player/stat-timeframe.ts
Normal file
@ -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>
|
||||
);
|
||||
}
|
36
projects/website/src/components/statistic/daily-change.tsx
Normal file
36
projects/website/src/components/statistic/daily-change.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user