add simple live score feed page
Some checks are pending
Deploy Website / deploy (push) Waiting to run
Deploy Backend / deploy (push) Successful in 4m15s

This commit is contained in:
Lee
2024-10-13 04:40:04 +01:00
parent ee212150fd
commit 4cc5893757
8 changed files with 224 additions and 24 deletions

View File

@ -4,7 +4,7 @@ import Link from "next/link";
import React from "react";
import NavbarButton from "./navbar-button";
import ProfileButton from "./profile-button";
import { TrendingUpIcon } from "lucide-react";
import { SwordIcon, TrendingUpIcon } from "lucide-react";
type NavbarItem = {
name: string;
@ -26,6 +26,12 @@ const items: NavbarItem[] = [
align: "left",
icon: <TrendingUpIcon className="h-5 w-5" />,
},
{
name: "Score Feed",
link: "/scores",
align: "left",
icon: <SwordIcon className="h-5 w-5" />,
},
{
name: "Search",
link: "/search",

View File

@ -0,0 +1,65 @@
"use client";
import { useScoreSaberWebsocket } from "@/hooks/use-scoresaber-websocket";
import { useEffect, useState } from "react";
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token";
import Score from "@/components/score/score";
import { parseDate } from "@ssr/common/utils/time-utils";
import Link from "next/link";
export default function ScoreFeed() {
const { connected, message } = useScoreSaberWebsocket();
const [scores, setScores] = useState<ScoreSaberPlayerScoreToken[]>([]);
useEffect(() => {
if (!message) {
return;
}
const { commandName, commandData } = message;
if (commandName !== "score") {
return;
}
setScores(prev => {
const newScores = [...prev, message.commandData];
if (newScores.length > 8) {
newScores.pop();
}
// Newest to oldest
return newScores.sort((a, b) => parseDate(b.score.timeSet).getTime() - parseDate(a.score.timeSet).getTime());
});
}, [message]);
if (!connected) {
return <p>Not Connected to the ScoreSaber Websocket :(</p>;
}
if (scores.length == 0) {
return <p>Waiting for scores...</p>;
}
return (
<div className="flex flex-col divide-y divide-border">
{scores.map(score => {
const player = score.score.leaderboardPlayerInfo;
return (
<div key={score.score.id} className="flex flex-col py-2">
<p className="text-sm">
Set by{" "}
<Link href={`/player/${player.id}`}>
<span className="text-pp hover:brightness-75 transition-all transform-gpu">{player.name}</span>
</Link>
</p>
<Score
playerScore={score}
settings={{
noScoreButtons: true,
}}
/>
</div>
);
})}
</div>
);
}

View File

@ -18,20 +18,15 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
const mappersProfile =
beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` : undefined;
const starCount = leaderboard.stars;
return (
<div className="flex gap-3 items-center">
<div className="relative flex justify-center h-[64px]">
<Tooltip
display={
<>
<p>
Difficulty: <span className="font-bold">{diff}</span>
</p>
{leaderboard.stars > 0 && (
<p>
Stars: <span className="font-bold">{leaderboard.stars}</span>
</p>
)}
<p>Difficulty: {diff}</p>
{starCount > 0 && <p>Stars: {starCount.toFixed(2)}</p>}
</>
}
>
@ -41,9 +36,9 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
backgroundColor: songDifficultyToColor(diff) + "f0", // Transparency value (in hex 0-255)
}}
>
{leaderboard.stars > 0 ? (
{starCount > 0 ? (
<div className="flex gap-1 items-center justify-center">
<p>{leaderboard.stars}</p>
<p>{starCount.toFixed(2)}</p>
<StarIcon className="w-[14px] h-[14px]" />
</div>
) : (

View File

@ -24,15 +24,26 @@ type Props = {
* The score to display.
*/
playerScore: ScoreSaberPlayerScoreToken;
/**
* Score settings
*/
settings?: {
noScoreButtons: boolean;
};
};
export default function Score({ player, playerScore }: Props) {
export default function Score({ player, playerScore, settings }: Props) {
const { score, leaderboard } = playerScore;
const [baseScore, setBaseScore] = useState<number>(score.baseScore);
const [beatSaverMap, setBeatSaverMap] = useState<BeatSaverMap | undefined>();
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
const fetchBeatSaverData = useCallback(async () => {
// No need to fetch if no buttons
if (!settings?.noScoreButtons) {
return;
}
const beatSaverMapData = await lookupBeatSaverMap(leaderboard.songHash);
setBeatSaverMap(beatSaverMapData);
}, [leaderboard.songHash]);
@ -48,22 +59,30 @@ export default function Score({ player, playerScore }: Props) {
}, [fetchBeatSaverData]);
const accuracy = (baseScore / leaderboard.maxScore) * 100;
const pp = baseScore == score.baseScore ? score.pp : scoresaberService.getPp(leaderboard.stars, accuracy);
const pp = baseScore === score.baseScore ? score.pp : scoresaberService.getPp(leaderboard.stars, accuracy);
// Dynamic grid column classes
const gridColsClass = settings?.noScoreButtons
? "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_300px]" // Fewer columns if no buttons
: "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_1fr_300px]"; // Original with buttons
return (
<div className="pb-2 pt-2">
{/* Score Info */}
<div className="grid w-full gap-2 lg:gap-0 grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_1fr_300px]">
<div className={`grid w-full gap-2 lg:gap-0 ${gridColsClass}`}>
<ScoreRankInfo score={score} leaderboard={leaderboard} />
<ScoreSongInfo leaderboard={leaderboard} beatSaverMap={beatSaverMap} />
<ScoreButtons
leaderboard={leaderboard}
beatSaverMap={beatSaverMap}
score={score}
setIsLeaderboardExpanded={setIsLeaderboardExpanded}
updateScore={score => {
setBaseScore(score.baseScore);
}}
/>
{settings?.noScoreButtons !== true && (
<ScoreButtons
leaderboard={leaderboard}
beatSaverMap={beatSaverMap}
score={score}
setIsLeaderboardExpanded={setIsLeaderboardExpanded}
updateScore={score => {
setBaseScore(score.baseScore);
}}
/>
)}
<ScoreStats
score={{
...score,