cleanup top scores and add timeframes to them
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 45s
Deploy Website / docker (ubuntu-latest) (push) Failing after 32s

This commit is contained in:
Lee
2024-10-29 18:34:58 +00:00
parent 9e96d2f0ba
commit b68de0552f
14 changed files with 244 additions and 145 deletions

View File

@ -0,0 +1,23 @@
import { Metadata } from "next";
import { Timeframe } from "@ssr/common/timeframe";
import { TopScoresData } from "@/components/score/top/top-scores-data";
export const metadata: Metadata = {
title: "Top Scores",
openGraph: {
title: "ScoreSaber Reloaded - Top Scores",
description: "View the top 50 scores set by players on ScoreSaber.",
},
};
type TopScoresPageProps = {
params: Promise<{
timeframe: Timeframe;
}>;
};
export default async function TopScoresPage({ params }: TopScoresPageProps) {
const { timeframe } = await params;
return <TopScoresData timeframe={timeframe} />;
}

View File

@ -1,59 +0,0 @@
import { Metadata } from "next";
import Card from "@/components/card";
import { kyFetch } from "@ssr/common/utils/utils";
import { Config } from "@ssr/common/config";
import { TopScoresResponse } from "@ssr/common/response/top-scores-response";
import Score from "@/components/score/score";
import Link from "next/link";
export const metadata: Metadata = {
title: "Top Scores",
openGraph: {
title: "ScoreSaber Reloaded - Top Scores",
description: "View the top 100 scores set by players on ScoreSaber.",
},
};
export default async function TopScoresPage() {
const scores = await kyFetch<TopScoresResponse>(`${Config.apiUrl}/scores/top`);
return (
<Card className="flex flex-col gap-2 w-full xl:w-[75%]">
<div>
<p className="font-semibold'">Top 100 ScoreSaber Scores</p>
<p className="text-gray-400">This will only show scores that have been tracked.</p>
</div>
{!scores ? (
<p>No scores found</p>
) : (
<div className="flex flex-col gap-2 divide-y divide-border">
{scores.scores.map(({ score, leaderboard, beatSaver }, index) => {
const player = score.playerInfo;
const name = score.playerInfo ? player.name || player.id : score.playerId;
return (
<div key={index} className="flex flex-col pt-2">
<p className="text-sm">
Set by{" "}
<Link href={`/player/${player.id}`}>
<span className="text-ssr hover:brightness-[66%] transition-all transform-gpu">{name}</span>
</Link>
</p>
<Score
score={score}
leaderboard={leaderboard}
beatSaverMap={beatSaver}
settings={{
hideLeaderboardDropdown: true,
hideAccuracyChanger: true,
}}
/>
</div>
);
})}
</div>
)}
</Card>
);
}

View File

@ -44,7 +44,7 @@ const items: NavbarItem[] = [
},
{
name: "Top Scores",
link: "/scores/top",
link: "/scores/top/weekly",
openInNewTab: false,
},
];

View File

@ -0,0 +1,6 @@
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import * as React from "react";
export function LoadingIcon() {
return <ArrowPathIcon className="w-5 h-5 animate-spin" />;
}

View File

@ -0,0 +1,125 @@
"use client";
import Card from "@/components/card";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Score from "@/components/score/score";
import { useEffect, useState } from "react";
import { Timeframe } from "@ssr/common/timeframe";
import { TopScoresResponse } from "@ssr/common/response/top-scores-response";
import { Config } from "@ssr/common/config";
import { kyFetch } from "@ssr/common/utils/utils";
import { useQuery } from "@tanstack/react-query";
import { LoadingIcon } from "@/components/loading-icon";
import { capitalizeFirstLetter } from "@/common/string-utils";
type TimeframesType = {
timeframe: Timeframe;
display: string;
};
const timeframes: TimeframesType[] = [
{
timeframe: "daily",
display: "Today",
},
{
timeframe: "weekly",
display: "This Week",
},
{
timeframe: "monthly",
display: "This Month",
},
{
timeframe: "all",
display: "All Time",
},
];
type TopScoresDataProps = {
timeframe: Timeframe;
};
export function TopScoresData({ timeframe }: TopScoresDataProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(timeframe);
const [scores, setScores] = useState<TopScoresResponse | null>(null);
const { data, isLoading } = useQuery({
queryKey: ["top-scores", selectedTimeframe],
queryFn: async () => {
return kyFetch<TopScoresResponse>(`${Config.apiUrl}/scores/top?limit=50&timeframe=${selectedTimeframe}`);
},
});
useEffect(() => {
// Update the URL
window.history.replaceState(null, "", `/scores/top/${selectedTimeframe}`);
}, [selectedTimeframe]);
useEffect(() => {
if (data) {
setScores(data);
}
}, [data]);
return (
<Card className="flex flex-col gap-2 w-full xl:w-[75%] justify-center">
<div className="flex flex-row flex-wrap gap-2 justify-center">
{timeframes.map((timeframe, index) => {
return (
<Button
key={index}
className="w-32"
variant={selectedTimeframe === timeframe.timeframe ? "default" : "outline"}
onClick={() => {
setScores(null);
setSelectedTimeframe(timeframe.timeframe);
}}
>
{timeframe.display}
</Button>
);
})}
</div>
<div className="flex justify-center flex-col text-center">
<p className="font-semibold'">Top 50 ScoreSaber Scores ({capitalizeFirstLetter(selectedTimeframe)})</p>
<p className="text-gray-400">This will only show scores that have been tracked.</p>
</div>
{(isLoading || !scores) && (
<div className="flex justify-center items-center">
<LoadingIcon />
</div>
)}
{scores && !isLoading && (
<div className="flex flex-col gap-2 divide-y divide-border">
{scores.scores.map(({ score, leaderboard, beatSaver }, index) => {
const player = score.playerInfo;
const name = score.playerInfo ? player.name || player.id : score.playerId;
return (
<div key={index} className="flex flex-col pt-2">
<p className="text-sm">
Set by{" "}
<Link href={`/player/${player.id}`}>
<span className="text-ssr hover:brightness-[66%] transition-all transform-gpu">{name}</span>
</Link>
</p>
<Score
score={score}
leaderboard={leaderboard}
beatSaverMap={beatSaver}
settings={{
hideLeaderboardDropdown: true,
hideAccuracyChanger: true,
}}
/>
</div>
);
})}
</div>
)}
</Card>
);
}

View File

@ -3,8 +3,8 @@ import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils
import { capitalizeFirstLetter } from "@/common/string-utils";
import Tooltip from "@/components/tooltip";
import { ReactElement } from "react";
import { ChangeRange } from "@ssr/common/player/player";
import { PlayerStatValue } from "@ssr/common/player/player-stat-change";
import { Timeframe } from "@ssr/common/timeframe";
type ChangeOverTimeProps = {
/**
@ -40,7 +40,7 @@ export function ChangeOverTime({ player, type, children }: ChangeOverTimeProps)
};
// Renders the change for a given time frame
const renderChange = (value: number | undefined, range: ChangeRange) => (
const renderChange = (value: number | undefined, range: Timeframe) => (
<p>
{capitalizeFirstLetter(range)} Change:{" "}
<span className={value === undefined ? "" : value >= 0 ? (value === 0 ? "" : "text-green-500") : "text-red-500"}>