add score acc chart
This commit is contained in:
parent
0731d20edc
commit
90c57ad086
@ -24,12 +24,12 @@ import { Config } from "@ssr/common/config";
|
|||||||
import { SSRCache } from "@ssr/common/cache";
|
import { SSRCache } from "@ssr/common/cache";
|
||||||
import { fetchWithCache } from "../common/cache.util";
|
import { fetchWithCache } from "../common/cache.util";
|
||||||
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
|
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
|
||||||
import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/beatleader-score-token";
|
import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/score/score";
|
||||||
import {
|
import {
|
||||||
AdditionalScoreData,
|
AdditionalScoreData,
|
||||||
AdditionalScoreDataModel,
|
AdditionalScoreDataModel,
|
||||||
} from "@ssr/common/model/additional-score-data/additional-score-data";
|
} from "@ssr/common/model/additional-score-data/additional-score-data";
|
||||||
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/beatleader-score-improvement-token";
|
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement";
|
||||||
|
|
||||||
const playerScoresCache = new SSRCache({
|
const playerScoresCache = new SSRCache({
|
||||||
ttl: 1000 * 60, // 1 minute
|
ttl: 1000 * 60, // 1 minute
|
||||||
|
35
projects/common/src/service/impl/beatleader.ts
Normal file
35
projects/common/src/service/impl/beatleader.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Service from "../service";
|
||||||
|
import { BeatSaverMapToken } from "../../types/token/beatsaver/map";
|
||||||
|
import { ScoreStatsToken } from "../../types/token/beatleader/score-stats/score-stats";
|
||||||
|
|
||||||
|
const LOOKUP_MAP_STATS_BY_SCORE_ID_ENDPOINT = `https://cdn.scorestats.beatleader.xyz/:scoreId.json`;
|
||||||
|
|
||||||
|
class BeatLeaderService extends Service {
|
||||||
|
constructor() {
|
||||||
|
super("BeatLeader");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the score stats for a score
|
||||||
|
*
|
||||||
|
* @param scoreId the score id to get
|
||||||
|
* @returns the score stats, or undefined if nothing was found
|
||||||
|
*/
|
||||||
|
async lookupScoreStats(scoreId: number): Promise<ScoreStatsToken | undefined> {
|
||||||
|
const before = performance.now();
|
||||||
|
this.log(`Looking score stats for "${scoreId}"...`);
|
||||||
|
|
||||||
|
const response = await this.fetch<ScoreStatsToken>(
|
||||||
|
LOOKUP_MAP_STATS_BY_SCORE_ID_ENDPOINT.replace(":scoreId", scoreId)
|
||||||
|
);
|
||||||
|
// Score stats not found
|
||||||
|
if (response == undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(`Found score stats for score "${scoreId}" in ${(performance.now() - before).toFixed(0)}ms`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const beatLeaderService = new BeatLeaderService();
|
@ -1,5 +1,5 @@
|
|||||||
import { BeatLeaderModifierToken } from "./beatleader-modifiers-token";
|
import { BeatLeaderModifierToken } from "./modifier/modifiers";
|
||||||
import { BeatLeaderModifierRatingToken } from "./beatleader-modifier-rating-token";
|
import { BeatLeaderModifierRatingToken } from "./modifier/modifier-rating";
|
||||||
|
|
||||||
export type BeatLeaderDifficultyToken = {
|
export type BeatLeaderDifficultyToken = {
|
||||||
id: number;
|
id: number;
|
@ -1,5 +1,5 @@
|
|||||||
import { BeatLeaderSongToken } from "./beatleader-song-token";
|
import { BeatLeaderSongToken } from "./score/song";
|
||||||
import { BeatLeaderDifficultyToken } from "./beatleader-difficulty-token";
|
import { BeatLeaderDifficultyToken } from "./difficulty";
|
||||||
|
|
||||||
export type BeatLeaderLeaderboardToken = {
|
export type BeatLeaderLeaderboardToken = {
|
||||||
id: string;
|
id: string;
|
@ -0,0 +1,66 @@
|
|||||||
|
export type ScoreStatsAccuracyTrackerToken = {
|
||||||
|
/**
|
||||||
|
* The accuracy of the right hand.
|
||||||
|
*/
|
||||||
|
accRight: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accuracy of the left hand.
|
||||||
|
*/
|
||||||
|
accLeft: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand pre-swing.
|
||||||
|
*/
|
||||||
|
leftPreswing: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand pre-swing.
|
||||||
|
*/
|
||||||
|
rightPreswing: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The average pre-swing.
|
||||||
|
*/
|
||||||
|
averagePreswing: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand post-swing.
|
||||||
|
*/
|
||||||
|
leftPostswing: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand post-swing.
|
||||||
|
*/
|
||||||
|
rightPostswing: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand time dependence.
|
||||||
|
*/
|
||||||
|
leftTimeDependence: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand time dependence.
|
||||||
|
*/
|
||||||
|
rightTimeDependence: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand average cut.
|
||||||
|
*/
|
||||||
|
leftAverageCut: number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand average cut.
|
||||||
|
*/
|
||||||
|
rightAverageCut: number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The grid accuracy.
|
||||||
|
*/
|
||||||
|
gridAcc: number[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full combo accuracy.
|
||||||
|
*/
|
||||||
|
fcAcc: number;
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
export type ScoreStatsHeadPositionToken = {
|
||||||
|
/**
|
||||||
|
* The X position of the head
|
||||||
|
*/
|
||||||
|
x: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Y position of the head
|
||||||
|
*/
|
||||||
|
y: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Z position of the head
|
||||||
|
*/
|
||||||
|
z: number;
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
export type ScoreStatsHitTrackerToken = {
|
||||||
|
/**
|
||||||
|
* The maximum combo achieved.
|
||||||
|
*/
|
||||||
|
maxCombo: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest amount of 115 notes hit in a row.
|
||||||
|
*/
|
||||||
|
maxStreak: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand timing.
|
||||||
|
*/
|
||||||
|
leftTiming: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand timing.
|
||||||
|
*/
|
||||||
|
rightTiming: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand misses.
|
||||||
|
*/
|
||||||
|
leftMiss: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand misses.
|
||||||
|
*/
|
||||||
|
rightMiss: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand bad cuts.
|
||||||
|
*/
|
||||||
|
leftBadCuts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand bad cuts.
|
||||||
|
*/
|
||||||
|
rightBadCuts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left hand bombs.
|
||||||
|
*/
|
||||||
|
leftBombs: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right hand bombs.
|
||||||
|
*/
|
||||||
|
rightBombs: number;
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
export type ScoreStatsGraphTrackerToken = {
|
||||||
|
/**
|
||||||
|
* The accuracy graph data.
|
||||||
|
*/
|
||||||
|
graph: number[];
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { ScoreStatsHitTrackerToken } from "./hit-tracker";
|
||||||
|
import { ScoreStatsAccuracyTrackerToken } from "./accuracy-tracker";
|
||||||
|
import { ScoreStatsWinTrackerToken } from "./win-tracker";
|
||||||
|
import { ScoreStatsGraphTrackerToken } from "./score-graph-tracker";
|
||||||
|
|
||||||
|
export type ScoreStatsToken = {
|
||||||
|
/**
|
||||||
|
* The hit tracker stats.
|
||||||
|
*/
|
||||||
|
hitTracker: ScoreStatsHitTrackerToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accuracy tracker stats.
|
||||||
|
*/
|
||||||
|
accuracyTracker: ScoreStatsAccuracyTrackerToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The win tracker stats.
|
||||||
|
*/
|
||||||
|
winTracker: ScoreStatsWinTrackerToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The score graph tracker stats.
|
||||||
|
*/
|
||||||
|
scoreGraphTracker: ScoreStatsGraphTrackerToken;
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
import { ScoreStatsHeadPositionToken } from "./head-position";
|
||||||
|
|
||||||
|
export type ScoreStatsWinTrackerToken = {
|
||||||
|
/**
|
||||||
|
* Whether the score was won. (not failed)
|
||||||
|
*/
|
||||||
|
won: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the score ended.
|
||||||
|
*/
|
||||||
|
endTime: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of pauses.
|
||||||
|
*/
|
||||||
|
nbOfPause: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total amount of pause time.
|
||||||
|
*/
|
||||||
|
totalPauseDuration: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The jump distance the score was played on.
|
||||||
|
*/
|
||||||
|
jumpDistance: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The average height of the player.
|
||||||
|
*/
|
||||||
|
averageHeight: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The average head position of the player.
|
||||||
|
*/
|
||||||
|
averageHeadPosition: ScoreStatsHeadPositionToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total score.
|
||||||
|
*/
|
||||||
|
totalScore: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum score for this song.
|
||||||
|
*/
|
||||||
|
maxScore: number;
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { BeatLeaderLeaderboardToken } from "./beatleader-leaderboard-token";
|
import { BeatLeaderLeaderboardToken } from "../leaderboard";
|
||||||
import { BeatLeaderScoreImprovementToken } from "./beatleader-score-improvement-token";
|
import { BeatLeaderScoreImprovementToken } from "./score-improvement";
|
||||||
import { BeatLeaderScoreOffsetsToken } from "./beatleader-score-offsets-token";
|
import { BeatLeaderScoreOffsetsToken } from "./score-offsets";
|
||||||
import { BeatLeaderPlayerToken } from "./beatleader-player-token";
|
import { BeatLeaderPlayerToken } from "../player";
|
||||||
|
|
||||||
export type BeatLeaderScoreToken = {
|
export type BeatLeaderScoreToken = {
|
||||||
myScore: null; // ??
|
myScore: null; // ??
|
@ -1,5 +1,5 @@
|
|||||||
import { connectWebSocket, WebsocketCallbacks } from "./websocket";
|
import { connectWebSocket, WebsocketCallbacks } from "./websocket";
|
||||||
import { BeatLeaderScoreToken } from "../types/token/beatleader/beatleader-score-token";
|
import { BeatLeaderScoreToken } from "../types/token/beatleader/score/score";
|
||||||
|
|
||||||
type BeatLeaderWebsocket = {
|
type BeatLeaderWebsocket = {
|
||||||
/**
|
/**
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
|
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
|
||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
|
||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
import { DualRangeSlider } from "@/components/ui/dual-range-slider";
|
import { DualRangeSlider } from "@/components/ui/dual-range-slider";
|
||||||
import { useDebounce } from "@uidotdev/usehooks";
|
import { useDebounce } from "@uidotdev/usehooks";
|
||||||
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
|
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score";
|
||||||
|
import { ScoreStatsToken } from "@ssr/common/types/token/beatleader/score-stats/score-stats";
|
||||||
|
import { formatTime } from "@ssr/common/utils/time-utils";
|
||||||
|
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/**
|
||||||
|
* The score stats to use in the chart
|
||||||
|
*/
|
||||||
|
scoreStats: ScoreStatsToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlayerScoreAccuracyChart({ scoreStats }: Props) {
|
||||||
|
const graph = scoreStats.scoreGraphTracker.graph;
|
||||||
|
|
||||||
|
const histories: Record<string, (number | null)[]> = {};
|
||||||
|
const labels: string[] = [];
|
||||||
|
|
||||||
|
for (let seconds = 0; seconds < graph.length; seconds++) {
|
||||||
|
labels.push(formatTime(seconds));
|
||||||
|
|
||||||
|
const history = histories["accuracy"];
|
||||||
|
if (!history) {
|
||||||
|
histories["accuracy"] = [];
|
||||||
|
}
|
||||||
|
histories["accuracy"].push(graph[seconds] * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasetConfig: DatasetConfig[] = [
|
||||||
|
{
|
||||||
|
title: "Accuracy",
|
||||||
|
field: "accuracy",
|
||||||
|
color: "#3EC1D3",
|
||||||
|
axisId: "y",
|
||||||
|
axisConfig: {
|
||||||
|
reverse: false,
|
||||||
|
display: true,
|
||||||
|
displayName: "Accuracy",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
labelFormatter: (value: number) => `${value.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <GenericChart labels={labels} datasetConfig={datasetConfig} histories={histories} />;
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
|
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
|
||||||
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
|
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
|
||||||
import LeaderboardPpChart from "@/components/leaderboard/leaderboard-pp-chart";
|
import LeaderboardPpChart from "@/components/leaderboard/chart/leaderboard-pp-chart";
|
||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
|
|
||||||
type LeaderboardDataProps = {
|
type LeaderboardDataProps = {
|
||||||
|
@ -62,8 +62,7 @@ export default function LeaderboardScores({
|
|||||||
selectedLeaderboardId + "",
|
selectedLeaderboardId + "",
|
||||||
currentPage
|
currentPage
|
||||||
),
|
),
|
||||||
staleTime: 30 * 1000,
|
enabled: shouldFetch,
|
||||||
enabled: (shouldFetch && isLeaderboardPage) || !isLeaderboardPage,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +15,12 @@ import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
|||||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
import { MapStats } from "@/components/score/map-stats";
|
import { MapStats } from "@/components/score/map-stats";
|
||||||
|
import PlayerScoreAccuracyChart from "@/components/leaderboard/chart/player-score-accuracy-chart";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils";
|
||||||
|
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
|
||||||
|
import { ScoreStatsToken } from "@ssr/common/types/token/beatleader/score-stats/score-stats";
|
||||||
|
import { beatLeaderService } from "@ssr/common/service/impl/beatleader";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
@ -40,10 +46,67 @@ type Props = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LeaderboardDropdownData = {
|
||||||
|
/**
|
||||||
|
* The initial scores.
|
||||||
|
*/
|
||||||
|
scores?: LeaderboardScoresResponse<ScoreSaberScore, ScoreSaberLeaderboard>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The score stats for this score,
|
||||||
|
*/
|
||||||
|
scoreStats?: ScoreStatsToken;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Score({ leaderboard, beatSaverMap, score, settings }: Props) {
|
export default function Score({ leaderboard, beatSaverMap, score, settings }: Props) {
|
||||||
|
const scoresPage = getPageFromRank(score.rank, 12);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [baseScore, setBaseScore] = useState<number>(score.score);
|
const [baseScore, setBaseScore] = useState<number>(score.score);
|
||||||
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
|
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [leaderboardDropdownData, setLeaderboardDropdownData] = useState<LeaderboardDropdownData | undefined>();
|
||||||
|
|
||||||
|
const { data, isError, isLoading } = useQuery<LeaderboardDropdownData>({
|
||||||
|
queryKey: ["leaderboardDropdownData", leaderboard.id, score.id, isLeaderboardExpanded],
|
||||||
|
queryFn: async () => {
|
||||||
|
const scores = await fetchLeaderboardScores<ScoreSaberScore, ScoreSaberLeaderboard>(
|
||||||
|
"scoresaber",
|
||||||
|
leaderboard.id + "",
|
||||||
|
scoresPage
|
||||||
|
);
|
||||||
|
const scoreStats = score.additionalData
|
||||||
|
? await beatLeaderService.lookupScoreStats(score.additionalData.scoreId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scores: scores,
|
||||||
|
scoreStats: scoreStats,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
enabled: loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setLeaderboardDropdownData({
|
||||||
|
...data,
|
||||||
|
scores: data.scores,
|
||||||
|
scoreStats: data.scoreStats,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleLeaderboardOpen = (isExpanded: boolean) => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
setLeaderboardDropdownData(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setIsLeaderboardExpanded(isExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the base score
|
* Set the base score
|
||||||
@ -59,6 +122,7 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLeaderboardExpanded(false);
|
setIsLeaderboardExpanded(false);
|
||||||
|
setLeaderboardDropdownData(undefined);
|
||||||
}, [score]);
|
}, [score]);
|
||||||
|
|
||||||
const accuracy = (baseScore / leaderboard.maxScore) * 100;
|
const accuracy = (baseScore / leaderboard.maxScore) * 100;
|
||||||
@ -81,7 +145,9 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr
|
|||||||
beatSaverMap={beatSaverMap}
|
beatSaverMap={beatSaverMap}
|
||||||
score={score}
|
score={score}
|
||||||
alwaysSingleLine={isMobile}
|
alwaysSingleLine={isMobile}
|
||||||
setIsLeaderboardExpanded={setIsLeaderboardExpanded}
|
setIsLeaderboardExpanded={(isExpanded: boolean) => {
|
||||||
|
handleLeaderboardOpen(isExpanded);
|
||||||
|
}}
|
||||||
updateScore={score => {
|
updateScore={score => {
|
||||||
setBaseScore(score.score);
|
setBaseScore(score.score);
|
||||||
}}
|
}}
|
||||||
@ -98,7 +164,7 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Leaderboard */}
|
{/* Leaderboard */}
|
||||||
{isLeaderboardExpanded && (
|
{isLeaderboardExpanded && leaderboardDropdownData && !loading && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -50 }}
|
initial={{ opacity: 0, y: -50 }}
|
||||||
exit={{ opacity: 0, y: -50 }}
|
exit={{ opacity: 0, y: -50 }}
|
||||||
@ -108,8 +174,15 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr
|
|||||||
<Card className="flex gap-4 w-full relative border border-input">
|
<Card className="flex gap-4 w-full relative border border-input">
|
||||||
<MapStats leaderboard={leaderboard} beatSaver={beatSaverMap} />
|
<MapStats leaderboard={leaderboard} beatSaver={beatSaverMap} />
|
||||||
|
|
||||||
|
{leaderboardDropdownData.scoreStats && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<PlayerScoreAccuracyChart scoreStats={leaderboardDropdownData.scoreStats} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<LeaderboardScores
|
<LeaderboardScores
|
||||||
initialPage={getPageFromRank(score.rank, 12)}
|
initialPage={scoresPage}
|
||||||
|
initialScores={leaderboardDropdownData.scores}
|
||||||
leaderboard={leaderboard}
|
leaderboard={leaderboard}
|
||||||
disableUrlChanging
|
disableUrlChanging
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user