This commit is contained in:
parent
e3450e23b1
commit
b9fd569196
18
src/app/leaderboard/[id]/[page]/page.tsx
Normal file
18
src/app/leaderboard/[id]/[page]/page.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Leaderboard from "@/components/leaderboard/Leaderboard";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: { id: string; page: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params: { id },
|
||||||
|
}: Props): Promise<Metadata> {
|
||||||
|
return {
|
||||||
|
title: `Leaderboard - name`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RankingGlobal({ params: { id, page } }: Props) {
|
||||||
|
return <Leaderboard id={id} page={Number(page)} />;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import GlobalRanking from "@/components/player/GlobalRanking";
|
import GlobalRanking from "@/components/GlobalRanking";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import GlobalRanking from "@/components/player/GlobalRanking";
|
import GlobalRanking from "@/components/GlobalRanking";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
@ -2,7 +2,7 @@ import Card from "@/components/Card";
|
|||||||
import Container from "@/components/Container";
|
import Container from "@/components/Container";
|
||||||
import UnknownAvatar from "@/components/UnknownAvatar";
|
import UnknownAvatar from "@/components/UnknownAvatar";
|
||||||
|
|
||||||
import SearchPlayer from "@/components/player/SearchPlayer";
|
import SearchPlayer from "@/components/SearchPlayer";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
@ -15,7 +15,7 @@ export default function Card({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"rounded-md bg-gray-800 p-3 opacity-90",
|
"w-full rounded-md bg-gray-800 p-3 opacity-90",
|
||||||
innerClassName,
|
innerClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -7,13 +7,13 @@ import dynamic from "next/dynamic";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Card from "../Card";
|
import Card from "./Card";
|
||||||
import Container from "../Container";
|
import Container from "./Container";
|
||||||
import CountyFlag from "../CountryFlag";
|
import CountyFlag from "./CountryFlag";
|
||||||
import Pagination from "../Pagination";
|
import Pagination from "./Pagination";
|
||||||
import Spinner from "../Spinner";
|
import Spinner from "./Spinner";
|
||||||
import PlayerRanking from "./PlayerRanking";
|
import PlayerRanking from "./player/PlayerRanking";
|
||||||
import PlayerRankingMobile from "./PlayerRankingMobile";
|
import PlayerRankingMobile from "./player/PlayerRankingMobile";
|
||||||
|
|
||||||
const Error = dynamic(() => import("@/components/Error"));
|
const Error = dynamic(() => import("@/components/Error"));
|
||||||
|
|
@ -6,7 +6,7 @@ import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
|||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "./Avatar";
|
||||||
|
|
||||||
export default function SearchPlayer() {
|
export default function SearchPlayer() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
178
src/components/leaderboard/Leaderboard.tsx
Normal file
178
src/components/leaderboard/Leaderboard.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
|
||||||
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import { ScoresaberScore } from "@/schemas/scoresaber/score";
|
||||||
|
import { formatNumber } from "@/utils/number";
|
||||||
|
import { scoresaberDifficultyNumberToName } from "@/utils/songUtils";
|
||||||
|
import { StarIcon } from "@heroicons/react/20/solid";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import Card from "../Card";
|
||||||
|
import Container from "../Container";
|
||||||
|
import Pagination from "../Pagination";
|
||||||
|
import LeaderboardScore from "./LeaderboardScore";
|
||||||
|
|
||||||
|
type LeaderboardProps = {
|
||||||
|
id: string;
|
||||||
|
page: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PageInfo = {
|
||||||
|
loading: boolean;
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
scores: ScoresaberScore[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Leaderboard({ id, page }: LeaderboardProps) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [leaderboardData, setLeaderboardData] = useState(
|
||||||
|
undefined as ScoresaberLeaderboardInfo | undefined,
|
||||||
|
);
|
||||||
|
const [leaderboardScoredsData, setLeaderboardScoredsData] =
|
||||||
|
useState<PageInfo>({
|
||||||
|
loading: true,
|
||||||
|
page: page,
|
||||||
|
totalPages: 1,
|
||||||
|
scores: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchLeaderboard = useCallback(async () => {
|
||||||
|
const leaderboard = await ScoreSaberAPI.fetchLeaderboardInfo(id);
|
||||||
|
setLeaderboardData(leaderboard);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const updateScoresPage = useCallback(
|
||||||
|
async (page: number) => {
|
||||||
|
const leaderboardScores = await ScoreSaberAPI.fetchLeaderboardScores(
|
||||||
|
id,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
if (!leaderboardScores) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLeaderboardScoredsData({
|
||||||
|
...leaderboardScoredsData,
|
||||||
|
scores: leaderboardScores.scores,
|
||||||
|
totalPages: leaderboardScores.pageInfo.totalPages,
|
||||||
|
loading: false,
|
||||||
|
page: page,
|
||||||
|
});
|
||||||
|
window.history.pushState({}, "", `/leaderboard/${id}/${page}`);
|
||||||
|
|
||||||
|
console.log(`Switched page to ${page}`);
|
||||||
|
},
|
||||||
|
[id, leaderboardScoredsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted) return;
|
||||||
|
fetchLeaderboard();
|
||||||
|
updateScoresPage(1);
|
||||||
|
|
||||||
|
setMounted(true);
|
||||||
|
}, [fetchLeaderboard, mounted, updateScoresPage]);
|
||||||
|
|
||||||
|
if (!leaderboardData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaderboardScores = leaderboardScoredsData.scores;
|
||||||
|
const {
|
||||||
|
coverImage,
|
||||||
|
songName,
|
||||||
|
songSubName,
|
||||||
|
levelAuthorName,
|
||||||
|
stars,
|
||||||
|
plays,
|
||||||
|
dailyPlays,
|
||||||
|
ranked,
|
||||||
|
difficulties,
|
||||||
|
} = leaderboardData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className="flex flex-col gap-2 md:flex-row">
|
||||||
|
<Card className="mt-2 flex">
|
||||||
|
<div className="flex min-w-[300px] flex-wrap justify-between gap-2 md:justify-start">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Image
|
||||||
|
src={coverImage}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
alt="Song Cover"
|
||||||
|
className="rounded-xl"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-xl font-bold">{songName}</p>
|
||||||
|
<p className="text-lg">{songSubName}</p>
|
||||||
|
<p>Mapped By: {levelAuthorName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p>Status: {ranked ? "Ranked" : "Unranked"}</p>
|
||||||
|
<div className="flex">
|
||||||
|
<p>Stars:</p>
|
||||||
|
<StarIcon width={20} height={20} className="ml-1" />
|
||||||
|
<p>{stars}</p>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Plays: {formatNumber(plays)} ({dailyPlays} in the last day)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="mt-2 h-fit">
|
||||||
|
<div className="flex justify-center gap-2">
|
||||||
|
{difficulties.map((diff) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={diff.difficulty}
|
||||||
|
className={`flex transform-gpu flex-row items-center gap-1 rounded-md p-[0.35rem] transition-all hover:opacity-80 ${
|
||||||
|
Number(id) === diff.leaderboardId
|
||||||
|
? "bg-blue-500"
|
||||||
|
: "bg-gray-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link href={`/leaderboard/${diff.leaderboardId}/1`}>
|
||||||
|
{scoresaberDifficultyNumberToName(diff.difficulty)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 divide-y divide-gray-500">
|
||||||
|
{leaderboardScores?.map((score, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<LeaderboardScore
|
||||||
|
score={score}
|
||||||
|
player={score.leaderboardPlayerInfo}
|
||||||
|
leaderboard={leaderboardData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div>
|
||||||
|
<div className="p-3">
|
||||||
|
<Pagination
|
||||||
|
currentPage={leaderboardScoredsData.page}
|
||||||
|
totalPages={leaderboardScoredsData.totalPages}
|
||||||
|
onPageChange={(page) => {
|
||||||
|
updateScoresPage(page);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
84
src/components/leaderboard/LeaderboardScore.tsx
Normal file
84
src/components/leaderboard/LeaderboardScore.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
|
||||||
|
import { ScoresaberScore } from "@/schemas/scoresaber/score";
|
||||||
|
import { formatNumber } from "@/utils/number";
|
||||||
|
import { scoresaberDifficultyNumberToName } from "@/utils/songUtils";
|
||||||
|
import { formatDate, formatTimeAgo } from "@/utils/timeUtils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ScoreStatLabel from "../player/ScoreStatLabel";
|
||||||
|
|
||||||
|
type ScoreProps = {
|
||||||
|
score: ScoresaberScore;
|
||||||
|
player: LeaderboardPlayerInfo;
|
||||||
|
leaderboard: ScoresaberLeaderboardInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LeaderboardScore({
|
||||||
|
score,
|
||||||
|
player,
|
||||||
|
leaderboard,
|
||||||
|
}: ScoreProps) {
|
||||||
|
const diffName = scoresaberDifficultyNumberToName(
|
||||||
|
leaderboard.difficulty.difficulty,
|
||||||
|
);
|
||||||
|
const accuracy = ((score.baseScore / leaderboard.maxScore) * 100).toFixed(2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-1 mt-1 grid grid-cols-[0.5fr_3fr_1.3fr] first:pt-0 last:pb-0 md:grid-cols-[1.2fr_6fr_1.3fr]">
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="flex w-fit flex-row items-center justify-center gap-1">
|
||||||
|
<p>#{formatNumber(score.rank)}</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="hidden text-sm text-gray-200 md:block"
|
||||||
|
title={formatDate(score.timeSet)}
|
||||||
|
>
|
||||||
|
{formatTimeAgo(score.timeSet)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Song Image */}
|
||||||
|
<div className="flex w-full items-center gap-2 ">
|
||||||
|
<div className="flex">
|
||||||
|
<Image
|
||||||
|
src={player.profilePicture}
|
||||||
|
alt={player.name}
|
||||||
|
className="flex h-fit rounded-md"
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Player Info */}
|
||||||
|
<Link
|
||||||
|
href={`/leaderboard/${leaderboard.id}/1`}
|
||||||
|
className="transform-gpu transition-all hover:opacity-70"
|
||||||
|
>
|
||||||
|
<div className="w-fit truncate text-blue-500">
|
||||||
|
<p className="font-bold">{player.name}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-1 md:items-start md:justify-end">
|
||||||
|
{/* PP */}
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
{score.pp > 0 && (
|
||||||
|
<ScoreStatLabel
|
||||||
|
className="bg-blue-500 text-center"
|
||||||
|
value={formatNumber(score.pp.toFixed(2)) + "pp"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Percentage score */}
|
||||||
|
<ScoreStatLabel
|
||||||
|
value={
|
||||||
|
!leaderboard.maxScore
|
||||||
|
? formatNumber(score.baseScore)
|
||||||
|
: accuracy + "%"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -15,7 +15,8 @@ import {
|
|||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import HeadsetIcon from "../HeadsetIcon";
|
import Link from "next/link";
|
||||||
|
import HeadsetIcon from "../icons/HeadsetIcon";
|
||||||
import ScoreStatLabel from "./ScoreStatLabel";
|
import ScoreStatLabel from "./ScoreStatLabel";
|
||||||
|
|
||||||
type ScoreProps = {
|
type ScoreProps = {
|
||||||
@ -76,13 +77,20 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Song Info */}
|
{/* Song Info */}
|
||||||
<div className="w-fit truncate text-blue-500">
|
<Link
|
||||||
<p className="font-bold">{leaderboard.songName}</p>
|
href={`/leaderboard/${leaderboard.id}/1`}
|
||||||
<p className="text-blue-300">
|
className="transform-gpu transition-all hover:opacity-70"
|
||||||
{leaderboard.songAuthorName}{" "}
|
>
|
||||||
<span className="text-gray-200">{leaderboard.levelAuthorName}</span>
|
<div className="w-fit truncate text-blue-500">
|
||||||
</p>
|
<p className="font-bold">{leaderboard.songName}</p>
|
||||||
</div>
|
<p className="text-blue-300">
|
||||||
|
{leaderboard.songAuthorName}{" "}
|
||||||
|
<span className="text-gray-200">
|
||||||
|
{leaderboard.levelAuthorName}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-1 md:items-start md:justify-end">
|
<div className="flex items-center justify-between p-1 md:items-start md:justify-end">
|
||||||
|
8
src/schemas/scoresaber/leaderboardPlayerInfo.ts
Normal file
8
src/schemas/scoresaber/leaderboardPlayerInfo.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
type LeaderboardPlayerInfo = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
profilePicture: string;
|
||||||
|
country: string;
|
||||||
|
permissions: number;
|
||||||
|
role: string;
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
export type ScoresaberScore = {
|
export type ScoresaberScore = {
|
||||||
id: number;
|
id: number;
|
||||||
leaderboardPlayerInfo: string;
|
leaderboardPlayerInfo: LeaderboardPlayerInfo;
|
||||||
rank: number;
|
rank: number;
|
||||||
baseScore: number;
|
baseScore: number;
|
||||||
modifiedScore: number;
|
modifiedScore: number;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||||
|
import { ScoresaberScore } from "@/schemas/scoresaber/score";
|
||||||
import { ssrSettings } from "@/ssrSettings";
|
import { ssrSettings } from "@/ssrSettings";
|
||||||
import { FetchQueue } from "../fetchWithQueue";
|
import { FetchQueue } from "../fetchWithQueue";
|
||||||
import { formatString } from "../string";
|
import { formatString } from "../string";
|
||||||
@ -17,6 +19,10 @@ export const SS_GET_PLAYER_DATA_FULL = SS_API_URL + "/player/{}/full";
|
|||||||
export const SS_GET_PLAYERS_URL = SS_API_URL + "/players?page={}";
|
export const SS_GET_PLAYERS_URL = SS_API_URL + "/players?page={}";
|
||||||
export const SS_GET_PLAYERS_BY_COUNTRY_URL =
|
export const SS_GET_PLAYERS_BY_COUNTRY_URL =
|
||||||
SS_API_URL + "/players?page={}&countries={}";
|
SS_API_URL + "/players?page={}&countries={}";
|
||||||
|
export const SS_GET_LEADERBOARD_INFO_URL =
|
||||||
|
SS_API_URL + "/leaderboard/by-id/{}/info";
|
||||||
|
export const SS_GET_LEADERBOARD_SCORES_URL =
|
||||||
|
SS_API_URL + "/leaderboard/by-id/{}/scores?page={}";
|
||||||
|
|
||||||
const SearchType = {
|
const SearchType = {
|
||||||
RECENT: "recent",
|
RECENT: "recent",
|
||||||
@ -200,10 +206,77 @@ async function fetchTopPlayers(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the leaderboard info for the given leaderboard id
|
||||||
|
*
|
||||||
|
* @param leaderboardId the id of the leaderboard
|
||||||
|
* @returns the leaderboard info
|
||||||
|
*/
|
||||||
|
async function fetchLeaderboardInfo(
|
||||||
|
leaderboardId: string,
|
||||||
|
): Promise<ScoresaberLeaderboardInfo | undefined> {
|
||||||
|
const response = await ScoresaberFetchQueue.fetch(
|
||||||
|
formatString(SS_GET_LEADERBOARD_INFO_URL, true, leaderboardId),
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
// Check if there was an error fetching the user data
|
||||||
|
if (json.errorMessage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json as ScoresaberLeaderboardInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the leaderboard scores from the given page
|
||||||
|
*
|
||||||
|
* @param leaderboardId the id of the leaderboard
|
||||||
|
* @param page the page to get the scores from
|
||||||
|
* @returns a list of scores
|
||||||
|
*/
|
||||||
|
async function fetchLeaderboardScores(
|
||||||
|
leaderboardId: string,
|
||||||
|
page: number = 1,
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
scores: ScoresaberScore[];
|
||||||
|
pageInfo: {
|
||||||
|
totalScores: number;
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
const response = await ScoresaberFetchQueue.fetch(
|
||||||
|
formatString(SS_GET_LEADERBOARD_SCORES_URL, true, leaderboardId, page),
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
// Check if there was an error fetching the user data
|
||||||
|
if (json.errorMessage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scores = json.scores as ScoresaberScore[];
|
||||||
|
const metadata = json.metadata;
|
||||||
|
return {
|
||||||
|
scores: scores,
|
||||||
|
pageInfo: {
|
||||||
|
totalScores: metadata.total,
|
||||||
|
page: metadata.page,
|
||||||
|
totalPages: Math.ceil(metadata.total / metadata.itemsPerPage),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const ScoreSaberAPI = {
|
export const ScoreSaberAPI = {
|
||||||
searchByName,
|
searchByName,
|
||||||
fetchPlayerData,
|
fetchPlayerData,
|
||||||
fetchScores,
|
fetchScores,
|
||||||
fetchAllScores,
|
fetchAllScores,
|
||||||
fetchTopPlayers,
|
fetchTopPlayers,
|
||||||
|
fetchLeaderboardInfo,
|
||||||
|
fetchLeaderboardScores,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user