add simple live score feed page
This commit is contained in:
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
) : (
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user