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) {
|
export function formatPp(num: number) {
|
||||||
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
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 { Config } from "../config";
|
||||||
import { AroundPlayer } from "../types/around-player";
|
import { AroundPlayer } from "../types/around-player";
|
||||||
import { AroundPlayerResponse } from "../response/around-player-response";
|
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,
|
* Sorts the player history based on date,
|
||||||
|
@ -5,94 +5,15 @@ import CountryFlag from "../country-flag";
|
|||||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarImage } from "../ui/avatar";
|
||||||
import ClaimProfile from "./claim-profile";
|
import ClaimProfile from "./claim-profile";
|
||||||
import PlayerStats from "./player-stats";
|
import PlayerStats from "./player-stats";
|
||||||
import Tooltip from "@/components/tooltip";
|
|
||||||
import { ReactElement } from "react";
|
|
||||||
import PlayerTrackedStatus from "@/components/player/player-tracked-status";
|
import PlayerTrackedStatus from "@/components/player/player-tracked-status";
|
||||||
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { capitalizeFirstLetter } from "@/common/string-utils";
|
|
||||||
import AddFriend from "@/components/friend/add-friend";
|
import AddFriend from "@/components/friend/add-friend";
|
||||||
import PlayerSteamProfile from "@/components/player/player-steam-profile";
|
import PlayerSteamProfile from "@/components/player/player-steam-profile";
|
||||||
import { getScoreSaberRole } from "@ssr/common/utils/scoresaber.util";
|
import { getScoreSaberRole } from "@ssr/common/utils/scoresaber.util";
|
||||||
|
import { DailyChange } from "@/components/statistic/daily-change";
|
||||||
/**
|
import { ChangeOverTime } from "@/components/statistic/change-over-time";
|
||||||
* Renders the change for a stat.
|
import { PlayerStat } from "@ssr/common/player/player-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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const playerData = [
|
const playerData = [
|
||||||
{
|
{
|
||||||
@ -106,16 +27,14 @@ const playerData = [
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-gray-300 flex gap-1 items-center">
|
<div className="text-gray-300 flex gap-1 items-center">
|
||||||
{renderChange(
|
<ChangeOverTime player={player} type={PlayerStat.Rank}>
|
||||||
player,
|
|
||||||
"rank",
|
|
||||||
<Link href={`/ranking/${player.rankPages.global}`}>
|
<Link href={`/ranking/${player.rankPages.global}`}>
|
||||||
<p className="hover:brightness-[66%] transition-all transform-gpu">
|
<p className="hover:brightness-[66%] transition-all transform-gpu">
|
||||||
#{formatNumberWithCommas(player.rank)}
|
#{formatNumberWithCommas(player.rank)}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
</ChangeOverTime>
|
||||||
{rankChange != 0 && renderDailyChange(rankChange, <p>The change in rank compared to yesterday</p>)}
|
<DailyChange type={PlayerStat.Rank} change={rankChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -131,16 +50,14 @@ const playerData = [
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-gray-300 flex gap-1 items-center">
|
<div className="text-gray-300 flex gap-1 items-center">
|
||||||
{renderChange(
|
<ChangeOverTime player={player} type={PlayerStat.CountryRank}>
|
||||||
player,
|
|
||||||
"countryRank",
|
|
||||||
<Link href={`/ranking/${player.country}/${player.rankPages.country}`}>
|
<Link href={`/ranking/${player.country}/${player.rankPages.country}`}>
|
||||||
<p className="hover:brightness-[66%] transition-all transform-gpu">
|
<p className="hover:brightness-[66%] transition-all transform-gpu">
|
||||||
#{formatNumberWithCommas(player.countryRank)}
|
#{formatNumberWithCommas(player.countryRank)}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
</ChangeOverTime>
|
||||||
{rankChange != 0 && renderDailyChange(rankChange, <p>The change in country rank compared to yesterday</p>)}
|
<DailyChange type={PlayerStat.CountryRank} change={rankChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -153,23 +70,24 @@ const playerData = [
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-gray-300 flex gap-1 items-center">
|
<div className="text-gray-300 flex gap-1 items-center">
|
||||||
{renderChange(
|
<ChangeOverTime player={player} type={PlayerStat.PerformancePoints}>
|
||||||
player,
|
<p className="hover:brightness-[66%] transition-all transform-gpu">{formatPp(player.pp)}pp</p>
|
||||||
"pp",
|
</ChangeOverTime>
|
||||||
<p className="hover:brightness-[66%] transition-all transform-gpu text-pp">{formatPp(player.pp)}pp</p>
|
<DailyChange type={PlayerStat.PerformancePoints} change={ppChange} />
|
||||||
)}
|
|
||||||
{ppChange != 0 && renderDailyChange(ppChange, <p>The change in pp compared to yesterday</p>)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type Props = {
|
type PlayerHeaderProps = {
|
||||||
|
/**
|
||||||
|
* The player to display.
|
||||||
|
*/
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PlayerHeader({ player }: Props) {
|
export default function PlayerHeader({ player }: PlayerHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex gap-3 flex-col items-center text-center lg:flex-row lg:items-start lg:text-start relative select-none">
|
<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 { formatDate } from "@ssr/common/utils/time-utils";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import Tooltip from "@/components/tooltip";
|
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;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
@ -14,13 +17,21 @@ type Badge = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const badges: Badge[] = [
|
const playerStats: Stat[] = [
|
||||||
{
|
{
|
||||||
name: "Ranked Play Count",
|
name: "Ranked Play Count",
|
||||||
color: "bg-pp",
|
color: "bg-pp",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
|
const history = getPlayerHistoryToday(player);
|
||||||
|
const rankedScores = history.scores?.rankedScores;
|
||||||
|
|
||||||
return {
|
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",
|
name: "Total Play Count",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
|
const history = getPlayerHistoryToday(player);
|
||||||
|
const rankedScores = history.scores?.rankedScores;
|
||||||
|
const unrankedScores = history.scores?.unrankedScores;
|
||||||
|
const totalChange = (rankedScores ?? 0) + (unrankedScores ?? 0);
|
||||||
|
|
||||||
return {
|
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) {
|
export default function PlayerStats({ player }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-wrap gap-2 w-full justify-center lg:justify-start`}>
|
<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);
|
const toRender = badge.create(player);
|
||||||
if (toRender === undefined) {
|
if (toRender === undefined) {
|
||||||
return <div key={index} />;
|
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