track total score, total ranked score, replay watched count and add a score chart
This commit is contained in:
parent
696da236d5
commit
854f88c43a
@ -148,6 +148,7 @@ export class PlayerService {
|
|||||||
history.countryRank = player.countryRank;
|
history.countryRank = player.countryRank;
|
||||||
history.rank = player.rank;
|
history.rank = player.rank;
|
||||||
history.accuracy = {
|
history.accuracy = {
|
||||||
|
...history.accuracy,
|
||||||
averageRankedAccuracy: scoreStats.averageRankedAccuracy,
|
averageRankedAccuracy: scoreStats.averageRankedAccuracy,
|
||||||
};
|
};
|
||||||
history.scores = {
|
history.scores = {
|
||||||
@ -155,6 +156,11 @@ export class PlayerService {
|
|||||||
totalScores: scoreStats.totalPlayCount,
|
totalScores: scoreStats.totalPlayCount,
|
||||||
totalRankedScores: scoreStats.rankedPlayCount,
|
totalRankedScores: scoreStats.rankedPlayCount,
|
||||||
};
|
};
|
||||||
|
history.score = {
|
||||||
|
...history.score,
|
||||||
|
totalScore: scoreStats.totalScore,
|
||||||
|
totalRankedScore: scoreStats.totalRankedScore,
|
||||||
|
};
|
||||||
|
|
||||||
foundPlayer.setStatisticHistory(dateToday, history);
|
foundPlayer.setStatisticHistory(dateToday, history);
|
||||||
foundPlayer.sortStatisticHistory();
|
foundPlayer.sortStatisticHistory();
|
||||||
@ -181,12 +187,11 @@ export class PlayerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
let history = player.getHistoryByDate(today);
|
const history = player.getHistoryByDate(today);
|
||||||
if (history == undefined || Object.keys(history).length === 0) {
|
const scores = history.scores || {
|
||||||
history = { scores: { rankedScores: 0, unrankedScores: 0 } }; // Ensure initialization
|
rankedScores: 0,
|
||||||
}
|
unrankedScores: 0,
|
||||||
|
};
|
||||||
const scores = history.scores || {};
|
|
||||||
if (leaderboard.stars > 0) {
|
if (leaderboard.stars > 0) {
|
||||||
scores.rankedScores!++;
|
scores.rankedScores!++;
|
||||||
} else {
|
} else {
|
||||||
|
@ -110,18 +110,11 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
if (history) {
|
if (history) {
|
||||||
// Use the latest data for today
|
// Use the latest data for today
|
||||||
history[todayDate] = {
|
history[todayDate] = {
|
||||||
...{
|
|
||||||
scores: {
|
|
||||||
rankedScores: 0,
|
|
||||||
unrankedScores: 0,
|
|
||||||
totalScores: 0,
|
|
||||||
totalRankedScores: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...history[todayDate],
|
...history[todayDate],
|
||||||
rank: token.rank,
|
rank: token.rank,
|
||||||
countryRank: token.countryRank,
|
countryRank: token.countryRank,
|
||||||
pp: token.pp,
|
pp: token.pp,
|
||||||
|
replaysWatched: token.scoreStats.replaysWatched,
|
||||||
accuracy: {
|
accuracy: {
|
||||||
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
|
averageRankedAccuracy: token.scoreStats.averageRankedAccuracy,
|
||||||
},
|
},
|
||||||
@ -129,6 +122,10 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
totalScores: token.scoreStats.totalPlayCount,
|
totalScores: token.scoreStats.totalPlayCount,
|
||||||
totalRankedScores: token.scoreStats.rankedPlayCount,
|
totalRankedScores: token.scoreStats.rankedPlayCount,
|
||||||
},
|
},
|
||||||
|
score: {
|
||||||
|
totalScore: token.scoreStats.totalScore,
|
||||||
|
totalRankedScore: token.scoreStats.totalRankedScore,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
isBeingTracked = true;
|
isBeingTracked = true;
|
||||||
@ -158,10 +155,6 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
statisticHistory[dateKey] = {
|
statisticHistory[dateKey] = {
|
||||||
...statisticHistory[dateKey],
|
...statisticHistory[dateKey],
|
||||||
rank: rank,
|
rank: rank,
|
||||||
scores: {
|
|
||||||
totalScores: token.scoreStats.totalPlayCount,
|
|
||||||
totalRankedScores: token.scoreStats.rankedPlayCount,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,9 +240,15 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
rank: getStatisticChange("rank", true, daysAgo),
|
rank: getStatisticChange("rank", true, daysAgo),
|
||||||
countryRank: getStatisticChange("countryRank", true, daysAgo),
|
countryRank: getStatisticChange("countryRank", true, daysAgo),
|
||||||
pp: getStatisticChange("pp", false, daysAgo),
|
pp: getStatisticChange("pp", false, daysAgo),
|
||||||
|
replaysWatched: getStatisticChange("replaysWatched", false, daysAgo),
|
||||||
|
accuracy: {
|
||||||
|
averageRankedAccuracy: getStatisticChange("accuracy.averageRankedAccuracy", false, daysAgo),
|
||||||
|
},
|
||||||
scores: {
|
scores: {
|
||||||
totalScores: getStatisticChange("scores.totalScores", false, daysAgo),
|
totalScores: getStatisticChange("scores.totalScores", false, daysAgo),
|
||||||
totalRankedScores: getStatisticChange("scores.totalRankedScores", false, daysAgo),
|
totalRankedScores: getStatisticChange("scores.totalRankedScores", false, daysAgo),
|
||||||
|
rankedScores: getStatisticChange("scores.rankedScores", false, daysAgo),
|
||||||
|
unrankedScores: getStatisticChange("scores.unrankedScores", false, daysAgo),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -268,7 +267,6 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
daily: getStatisticChanges(1),
|
daily: getStatisticChanges(1),
|
||||||
weekly: getStatisticChanges(7),
|
weekly: getStatisticChanges(7),
|
||||||
monthly: getStatisticChanges(30),
|
monthly: getStatisticChanges(30),
|
||||||
yearly: getStatisticChanges(365),
|
|
||||||
},
|
},
|
||||||
role: token.role == null ? undefined : token.role,
|
role: token.role == null ? undefined : token.role,
|
||||||
badges: badges,
|
badges: badges,
|
||||||
|
@ -14,6 +14,26 @@ export interface PlayerHistory {
|
|||||||
*/
|
*/
|
||||||
pp?: number;
|
pp?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many times replays of the player scores have been watched
|
||||||
|
*/
|
||||||
|
replaysWatched?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The player's score stats.
|
||||||
|
*/
|
||||||
|
score?: {
|
||||||
|
/**
|
||||||
|
* The total amount of unranked and ranked score.
|
||||||
|
*/
|
||||||
|
totalScore?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of ranked score.
|
||||||
|
*/
|
||||||
|
totalRankedScore?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of scores set for this day.
|
* The amount of scores set for this day.
|
||||||
*/
|
*/
|
||||||
|
64
projects/common/src/player/player-stat-change.ts
Normal file
64
projects/common/src/player/player-stat-change.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import ScoreSaberPlayer from "@ssr/player/impl/scoresaber-player";
|
||||||
|
import { ChangeRange } from "@ssr/player/player";
|
||||||
|
|
||||||
|
export type PlayerStatValue = {
|
||||||
|
/**
|
||||||
|
* The type of the stat.
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the stat.
|
||||||
|
*/
|
||||||
|
value: (player: ScoreSaberPlayer, range: ChangeRange) => number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlayerStatChangeType =
|
||||||
|
| "Rank"
|
||||||
|
| "CountryRank"
|
||||||
|
| "PerformancePoints"
|
||||||
|
| "TotalPlayCount"
|
||||||
|
| "RankedPlayCount"
|
||||||
|
| "TotalScore"
|
||||||
|
| "TotalRankedScore"
|
||||||
|
| "AverageRankedAccuracy"
|
||||||
|
| "TotalReplaysWatched";
|
||||||
|
|
||||||
|
export const PlayerStatChange: Record<PlayerStatChangeType, PlayerStatValue> = {
|
||||||
|
Rank: {
|
||||||
|
type: "Rank",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].rank,
|
||||||
|
},
|
||||||
|
CountryRank: {
|
||||||
|
type: "Country Rank",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].countryRank,
|
||||||
|
},
|
||||||
|
PerformancePoints: {
|
||||||
|
type: "Performance Points",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].pp,
|
||||||
|
},
|
||||||
|
TotalPlayCount: {
|
||||||
|
type: "Total Play Count",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].scores?.totalScores,
|
||||||
|
},
|
||||||
|
RankedPlayCount: {
|
||||||
|
type: "Ranked Play Count",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].scores?.totalRankedScores,
|
||||||
|
},
|
||||||
|
TotalScore: {
|
||||||
|
type: "Total Score",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].score?.totalScore,
|
||||||
|
},
|
||||||
|
TotalRankedScore: {
|
||||||
|
type: "Total Ranked Score",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].scores?.totalRankedScores,
|
||||||
|
},
|
||||||
|
AverageRankedAccuracy: {
|
||||||
|
type: "Average Ranked Accuracy",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].accuracy?.averageRankedAccuracy,
|
||||||
|
},
|
||||||
|
TotalReplaysWatched: {
|
||||||
|
type: "Total Replays Watched",
|
||||||
|
value: (player, range) => player.statisticChange?.[range].replaysWatched,
|
||||||
|
},
|
||||||
|
};
|
@ -1,32 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
};
|
|
@ -55,9 +55,7 @@ export default class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChangeRange = "daily" | "weekly" | "monthly";
|
||||||
export type StatisticChange = {
|
export type StatisticChange = {
|
||||||
daily: PlayerHistory;
|
[key in ChangeRange]: PlayerHistory;
|
||||||
weekly: PlayerHistory;
|
|
||||||
monthly: PlayerHistory;
|
|
||||||
yearly: PlayerHistory;
|
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export type StatTimeframe = "daily" | "weekly" | "monthly";
|
|
@ -4,8 +4,9 @@ import Link from "next/link";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import NavbarButton from "./navbar-button";
|
import NavbarButton from "./navbar-button";
|
||||||
import ProfileButton from "./profile-button";
|
import ProfileButton from "./profile-button";
|
||||||
import { SwordIcon, TrendingUpIcon } from "lucide-react";
|
import { TrendingUpIcon } from "lucide-react";
|
||||||
import FriendsButton from "@/components/navbar/friends-button";
|
import FriendsButton from "@/components/navbar/friends-button";
|
||||||
|
import { PiSwordFill } from "react-icons/pi";
|
||||||
|
|
||||||
type NavbarItem = {
|
type NavbarItem = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -25,7 +26,7 @@ const items: NavbarItem[] = [
|
|||||||
name: "Score Feed",
|
name: "Score Feed",
|
||||||
link: "/scores",
|
link: "/scores",
|
||||||
align: "left",
|
align: "left",
|
||||||
icon: <SwordIcon className="h-5 w-5" />,
|
icon: <PiSwordFill className="h-5 w-5" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Search",
|
name: "Search",
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
|
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
||||||
|
import { scoreBarsDataset } from "@/components/player/chart/charts/player-scores-chart";
|
||||||
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
|
import { formatNumberWithCommas, isWholeNumber } from "@ssr/common/utils/number-utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
player: ScoreSaberPlayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dataset configuration for the chart
|
||||||
|
const datasetConfig: DatasetConfig[] = [
|
||||||
|
{
|
||||||
|
title: "Rank",
|
||||||
|
field: "rank",
|
||||||
|
color: "#3EC1D3",
|
||||||
|
axisId: "y",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: true,
|
||||||
|
display: true,
|
||||||
|
displayName: "Rank",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `Rank: #${formatNumberWithCommas(value)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Country Rank",
|
||||||
|
field: "countryRank",
|
||||||
|
color: "#FFEA00",
|
||||||
|
axisId: "y1",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: true,
|
||||||
|
display: false,
|
||||||
|
displayName: "Country Rank",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `Country Rank: #${formatNumberWithCommas(value)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PP",
|
||||||
|
field: "pp",
|
||||||
|
color: "#4858ff",
|
||||||
|
axisId: "y2",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: false,
|
||||||
|
display: true,
|
||||||
|
hideOnMobile: true,
|
||||||
|
displayName: "PP",
|
||||||
|
position: "right",
|
||||||
|
valueFormatter: value => {
|
||||||
|
if (isWholeNumber(value)) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
return value.toFixed(1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `PP: ${formatNumberWithCommas(value)}pp`,
|
||||||
|
},
|
||||||
|
...scoreBarsDataset,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PlayerRankingChart({ player }: Props) {
|
||||||
|
return <GenericPlayerChart player={player} datasetConfig={datasetConfig} />;
|
||||||
|
}
|
@ -1,96 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { formatNumberWithCommas, isWholeNumber } from "@ssr/common/utils/number-utils";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DatasetConfig } from "@/components/chart/generic-chart";
|
import { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
||||||
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
|
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dataset configuration for the chart
|
export const scoreBarsDataset: DatasetConfig[] = [
|
||||||
const datasetConfig: DatasetConfig[] = [
|
|
||||||
{
|
|
||||||
title: "Rank",
|
|
||||||
field: "rank",
|
|
||||||
color: "#3EC1D3",
|
|
||||||
axisId: "y",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: true,
|
|
||||||
display: true,
|
|
||||||
displayName: "Rank",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Rank: #${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Country Rank",
|
|
||||||
field: "countryRank",
|
|
||||||
color: "#FFEA00",
|
|
||||||
axisId: "y1",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: true,
|
|
||||||
display: false,
|
|
||||||
displayName: "Country Rank",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Country Rank: #${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PP",
|
|
||||||
field: "pp",
|
|
||||||
color: "#4858ff",
|
|
||||||
axisId: "y2",
|
|
||||||
axisConfig: {
|
|
||||||
reverse: false,
|
|
||||||
display: true,
|
|
||||||
hideOnMobile: true,
|
|
||||||
displayName: "PP",
|
|
||||||
position: "right",
|
|
||||||
valueFormatter: value => {
|
|
||||||
if (isWholeNumber(value)) {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
return value.toFixed(1);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `PP: ${formatNumberWithCommas(value)}pp`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Total Scores",
|
|
||||||
field: "scores.totalScores",
|
|
||||||
color: "#616161",
|
|
||||||
axisId: "y3",
|
|
||||||
showLegend: false,
|
|
||||||
axisConfig: {
|
|
||||||
reverse: false,
|
|
||||||
display: false,
|
|
||||||
displayName: "Total Scores",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Total Scores: ${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Total Ranked Scores",
|
|
||||||
field: "scores.totalRankedScores",
|
|
||||||
color: "#6773ff",
|
|
||||||
axisId: "y4",
|
|
||||||
showLegend: false,
|
|
||||||
axisConfig: {
|
|
||||||
reverse: false,
|
|
||||||
display: false,
|
|
||||||
displayName: "Total Ranked Scores",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
labelFormatter: (value: number) => `Total Ranked Scores: ${formatNumberWithCommas(value)}`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Ranked Scores",
|
title: "Ranked Scores",
|
||||||
field: "scores.rankedScores",
|
field: "scores.rankedScores",
|
||||||
color: "#ffae4d",
|
color: "#ffae4d",
|
||||||
axisId: "y5",
|
axisId: "y100",
|
||||||
axisConfig: {
|
axisConfig: {
|
||||||
reverse: false,
|
reverse: false,
|
||||||
display: false,
|
display: false,
|
||||||
@ -104,7 +29,7 @@ const datasetConfig: DatasetConfig[] = [
|
|||||||
title: "Unranked Scores",
|
title: "Unranked Scores",
|
||||||
field: "scores.unrankedScores",
|
field: "scores.unrankedScores",
|
||||||
color: "#616161",
|
color: "#616161",
|
||||||
axisId: "y5",
|
axisId: "y100",
|
||||||
axisConfig: {
|
axisConfig: {
|
||||||
reverse: false,
|
reverse: false,
|
||||||
display: false,
|
display: false,
|
||||||
@ -116,6 +41,37 @@ const datasetConfig: DatasetConfig[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function PlayerRankingChart({ player }: Props) {
|
// Dataset configuration for the chart
|
||||||
|
const datasetConfig: DatasetConfig[] = [
|
||||||
|
...scoreBarsDataset,
|
||||||
|
{
|
||||||
|
title: "Total Scores",
|
||||||
|
field: "scores.totalScores",
|
||||||
|
color: "#616161",
|
||||||
|
axisId: "y1",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: false,
|
||||||
|
display: true,
|
||||||
|
displayName: "Total Scores",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `Total Scores: ${formatNumberWithCommas(value)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Total Ranked Scores",
|
||||||
|
field: "scores.totalRankedScores",
|
||||||
|
color: "#6773ff",
|
||||||
|
axisId: "y2",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: false,
|
||||||
|
display: true,
|
||||||
|
displayName: "Total Ranked Scores",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `Total Ranked Scores: ${formatNumberWithCommas(value)}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PlayerScoresChart({ player }: Props) {
|
||||||
return <GenericPlayerChart player={player} datasetConfig={datasetConfig} />;
|
return <GenericPlayerChart player={player} datasetConfig={datasetConfig} />;
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import PlayerRankingChart from "@/components/player/chart/player-ranking-chart";
|
import PlayerRankingChart from "@/components/player/chart/charts/player-ranking-chart";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import PlayerAccuracyChart from "@/components/player/chart/player-accuracy-chart";
|
import PlayerAccuracyChart from "@/components/player/chart/charts/player-accuracy-chart";
|
||||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
import { TrendingUpIcon } from "lucide-react";
|
import { TrendingUpIcon } from "lucide-react";
|
||||||
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
|
import PlayerScoresChart from "@/components/player/chart/charts/player-scores-chart";
|
||||||
|
import { PiSwordFill } from "react-icons/pi";
|
||||||
|
|
||||||
type PlayerChartsProps = {
|
type PlayerChartsProps = {
|
||||||
/**
|
/**
|
||||||
@ -53,6 +55,12 @@ export default function PlayerCharts({ player }: PlayerChartsProps) {
|
|||||||
icon: <TrendingUpIcon className="w-[18px] h-[18px]" />,
|
icon: <TrendingUpIcon className="w-[18px] h-[18px]" />,
|
||||||
chart: PlayerAccuracyChart,
|
chart: PlayerAccuracyChart,
|
||||||
});
|
});
|
||||||
|
charts.push({
|
||||||
|
index: 2,
|
||||||
|
label: "Scores",
|
||||||
|
icon: <PiSwordFill className="w-[18px] h-[18px]" />,
|
||||||
|
chart: PlayerScoresChart,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [selectedChart, setSelectedChart] = useState<SelectedChart>(charts[0]);
|
const [selectedChart, setSelectedChart] = useState<SelectedChart>(charts[0]);
|
||||||
|
@ -13,7 +13,7 @@ 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 { DailyChange } from "@/components/statistic/daily-change";
|
||||||
import { ChangeOverTime } from "@/components/statistic/change-over-time";
|
import { ChangeOverTime } from "@/components/statistic/change-over-time";
|
||||||
import { PlayerStat } from "@ssr/common/player/player-stat";
|
import { PlayerStatChange } from "@ssr/common/player/player-stat-change";
|
||||||
|
|
||||||
const playerData = [
|
const playerData = [
|
||||||
{
|
{
|
||||||
@ -27,14 +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">
|
||||||
<ChangeOverTime player={player} type={PlayerStat.Rank}>
|
<ChangeOverTime player={player} type={PlayerStatChange.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>
|
</ChangeOverTime>
|
||||||
<DailyChange type={PlayerStat.Rank} change={rankChange} />
|
<DailyChange type={PlayerStatChange.Rank} change={rankChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -50,14 +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">
|
||||||
<ChangeOverTime player={player} type={PlayerStat.CountryRank}>
|
<ChangeOverTime player={player} type={PlayerStatChange.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>
|
</ChangeOverTime>
|
||||||
<DailyChange type={PlayerStat.CountryRank} change={rankChange} />
|
<DailyChange type={PlayerStatChange.CountryRank} change={rankChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -70,10 +70,10 @@ const playerData = [
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-gray-300 flex gap-1 items-center">
|
<div className="text-gray-300 flex gap-1 items-center">
|
||||||
<ChangeOverTime player={player} type={PlayerStat.PerformancePoints}>
|
<ChangeOverTime player={player} type={PlayerStatChange.PerformancePoints}>
|
||||||
<p className="hover:brightness-[66%] transition-all transform-gpu text-pp">{formatPp(player.pp)}pp</p>
|
<p className="hover:brightness-[66%] transition-all transform-gpu text-pp">{formatPp(player.pp)}pp</p>
|
||||||
</ChangeOverTime>
|
</ChangeOverTime>
|
||||||
<DailyChange type={PlayerStat.PerformancePoints} change={ppChange} />
|
<DailyChange type={PlayerStatChange.PerformancePoints} change={ppChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,8 @@ 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 { DailyChange } from "@/components/statistic/daily-change";
|
import { DailyChange } from "@/components/statistic/daily-change";
|
||||||
import { PlayerStat } from "@ssr/common/player/player-stat";
|
|
||||||
import { getScoreSaberRole } from "@ssr/common/utils/scoresaber.util";
|
import { getScoreSaberRole } from "@ssr/common/utils/scoresaber.util";
|
||||||
|
import { PlayerStatChange } from "@ssr/common/player/player-stat-change";
|
||||||
|
|
||||||
type Stat = {
|
type Stat = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -22,13 +22,11 @@ const playerStats: Stat[] = [
|
|||||||
name: "Ranked Play Count",
|
name: "Ranked Play Count",
|
||||||
color: "bg-pp",
|
color: "bg-pp",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
const rankedScoresChange = player.statisticChange?.daily.scores?.totalRankedScores;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: (
|
value: (
|
||||||
<>
|
<>
|
||||||
{formatNumberWithCommas(player.statistics.rankedPlayCount)}{" "}
|
{formatNumberWithCommas(player.statistics.rankedPlayCount)}{" "}
|
||||||
<DailyChange type={PlayerStat.RankedPlayCount} change={rankedScoresChange} />
|
<DailyChange type={PlayerStatChange.RankedPlayCount} player={player} />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -39,7 +37,12 @@ const playerStats: Stat[] = [
|
|||||||
color: "bg-pp",
|
color: "bg-pp",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
return {
|
return {
|
||||||
value: formatNumberWithCommas(player.statistics.totalRankedScore),
|
value: (
|
||||||
|
<>
|
||||||
|
{formatNumberWithCommas(player.statistics.totalRankedScore)}{" "}
|
||||||
|
<DailyChange type={PlayerStatChange.TotalRankedScore} player={player} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -48,20 +51,23 @@ const playerStats: Stat[] = [
|
|||||||
color: "bg-pp",
|
color: "bg-pp",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
return {
|
return {
|
||||||
value: player.statistics.averageRankedAccuracy.toFixed(2) + "%",
|
value: (
|
||||||
|
<>
|
||||||
|
{player.statistics.averageRankedAccuracy.toFixed(2) + "%"}{" "}
|
||||||
|
<DailyChange type={PlayerStatChange.AverageRankedAccuracy} player={player} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Total Play Count",
|
name: "Total Play Count",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
const scoresChange = player.statisticChange?.daily.scores;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: (
|
value: (
|
||||||
<>
|
<>
|
||||||
{formatNumberWithCommas(player.statistics.totalPlayCount)}{" "}
|
{formatNumberWithCommas(player.statistics.totalPlayCount)}{" "}
|
||||||
<DailyChange type={PlayerStat.TotalPlayCount} change={scoresChange?.totalScores} />
|
<DailyChange type={PlayerStatChange.TotalPlayCount} player={player} />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -71,7 +77,12 @@ const playerStats: Stat[] = [
|
|||||||
name: "Total Score",
|
name: "Total Score",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
return {
|
return {
|
||||||
value: formatNumberWithCommas(player.statistics.totalScore),
|
value: (
|
||||||
|
<>
|
||||||
|
{formatNumberWithCommas(player.statistics.totalScore)}{" "}
|
||||||
|
<DailyChange type={PlayerStatChange.TotalScore} player={player} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -79,7 +90,12 @@ const playerStats: Stat[] = [
|
|||||||
name: "Total Replays Watched",
|
name: "Total Replays Watched",
|
||||||
create: (player: ScoreSaberPlayer) => {
|
create: (player: ScoreSaberPlayer) => {
|
||||||
return {
|
return {
|
||||||
value: formatNumberWithCommas(player.statistics.replaysWatched),
|
value: (
|
||||||
|
<>
|
||||||
|
{formatNumberWithCommas(player.statistics.replaysWatched)}{" "}
|
||||||
|
<DailyChange type={PlayerStatChange.TotalReplaysWatched} player={player} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,9 @@ import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
|||||||
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
||||||
import { capitalizeFirstLetter } from "@/common/string-utils";
|
import { capitalizeFirstLetter } from "@/common/string-utils";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import { PlayerStatValue } from "@ssr/common/player/player-stat";
|
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { ChangeRange } from "@ssr/common/player/player";
|
||||||
|
import { PlayerStatValue } from "@ssr/common/player/player-stat-change";
|
||||||
|
|
||||||
type ChangeOverTimeProps = {
|
type ChangeOverTimeProps = {
|
||||||
/**
|
/**
|
||||||
@ -23,13 +24,9 @@ type ChangeOverTimeProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps) {
|
export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps) {
|
||||||
const todayStats = player.statisticChange?.daily;
|
const daily = type.value(player, "daily");
|
||||||
const weeklyStats = player.statisticChange?.weekly;
|
const weekly = type.value(player, "weekly");
|
||||||
const monthlyStats = player.statisticChange?.monthly;
|
const monthly = type.value(player, "monthly");
|
||||||
|
|
||||||
const todayStat = todayStats?.[type.value!];
|
|
||||||
const weeklyStat = weeklyStats?.[type.value!];
|
|
||||||
const monthlyStat = monthlyStats?.[type.value!];
|
|
||||||
|
|
||||||
// Format values based on stat type
|
// Format values based on stat type
|
||||||
const formatChangeValue = (value: number | undefined): string | number => {
|
const formatChangeValue = (value: number | undefined): string | number => {
|
||||||
@ -39,13 +36,13 @@ export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps)
|
|||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "No Data";
|
return "No Data";
|
||||||
}
|
}
|
||||||
return type.value === "pp" ? formatPp(value) + "pp" : formatNumberWithCommas(value);
|
return type.type === "Performance Points" ? formatPp(value) + "pp" : formatNumberWithCommas(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Renders the change for a given time frame
|
// Renders the change for a given time frame
|
||||||
const renderChange = (value: number | undefined, timeFrame: "daily" | "weekly" | "monthly") => (
|
const renderChange = (value: number | undefined, range: ChangeRange) => (
|
||||||
<p>
|
<p>
|
||||||
{capitalizeFirstLetter(timeFrame)} Change:{" "}
|
{capitalizeFirstLetter(range)} Change:{" "}
|
||||||
<span className={value === undefined ? "" : value >= 0 ? (value === 0 ? "" : "text-green-500") : "text-red-500"}>
|
<span className={value === undefined ? "" : value >= 0 ? (value === 0 ? "" : "text-green-500") : "text-red-500"}>
|
||||||
{formatChangeValue(value)}
|
{formatChangeValue(value)}
|
||||||
</span>
|
</span>
|
||||||
@ -62,9 +59,9 @@ export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps)
|
|||||||
side="bottom"
|
side="bottom"
|
||||||
display={
|
display={
|
||||||
<div>
|
<div>
|
||||||
{renderChange(todayStat, "daily")}
|
{renderChange(daily, "daily")}
|
||||||
{renderChange(weeklyStat, "weekly")}
|
{renderChange(weekly, "weekly")}
|
||||||
{renderChange(monthlyStat, "monthly")}
|
{renderChange(monthly, "monthly")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
|
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
||||||
import { PlayerStatValue } from "@ssr/common/player/player-stat";
|
import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player";
|
||||||
|
import { PlayerStatValue } from "@ssr/common/player/player-stat-change";
|
||||||
|
|
||||||
interface DailyChangeProps {
|
interface DailyChangeProps {
|
||||||
/**
|
/**
|
||||||
@ -10,38 +11,39 @@ interface DailyChangeProps {
|
|||||||
type: PlayerStatValue;
|
type: PlayerStatValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the change
|
* The player to get the change for
|
||||||
*/
|
*/
|
||||||
change: number | undefined;
|
player?: ScoreSaberPlayer | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The change (if not using player)
|
||||||
|
*/
|
||||||
|
change?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The tooltip to display
|
* The tooltip to display
|
||||||
*/
|
*/
|
||||||
tooltip?: React.ReactElement | string;
|
tooltip?: React.ReactElement | string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The formater for the change
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
format?: (value: number) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DailyChange({ type, change, tooltip, format }: DailyChangeProps) {
|
export function DailyChange({ type, player, change, tooltip }: DailyChangeProps) {
|
||||||
const formatValue = format ?? formatNumberWithCommas;
|
const formatValue = type.type == "Performance Points" ? formatPp : formatNumberWithCommas;
|
||||||
if (change === 0 || change === undefined) {
|
if (!change && player !== undefined) {
|
||||||
|
change = type.value?.(player, "daily");
|
||||||
|
}
|
||||||
|
if (change === 0 || (change && change < 0.01) || change === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = (
|
const value = (
|
||||||
<p className={`text-sm ${change > 0 ? "text-green-400" : "text-red-400"}`}>
|
<p className={`text-sm ${change > 0 ? "text-green-400" : "text-red-400"}`}>
|
||||||
{change > 0 ? "+" : ""}
|
{change > 0 ? "+" : ""}
|
||||||
{`${formatValue(change)}${type.value == "pp" ? "pp" : ""}`}
|
{`${formatValue(change)}${type.type == "Performance Points" ? "pp" : ""}`}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!tooltip) {
|
if (!tooltip) {
|
||||||
tooltip = `${type.displayName} change compared to yesterday`;
|
tooltip = `${type.type} change compared to yesterday`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user