feat(ssr): add map and bsr button
All checks were successful
deploy / deploy (push) Successful in 1m3s

This commit is contained in:
Lee 2023-11-08 22:13:20 +00:00
parent c3889a4d5a
commit b9472ce982
8 changed files with 220 additions and 35 deletions

View File

@ -0,0 +1,32 @@
import { BeatsaverMap } from "@/schemas/beatsaver/BeatsaverMap";
import { BeatsaverAPI } from "@/utils/beatsaver/api";
const mapCache = new Map<string, BeatsaverMap>();
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const mapHashes = searchParams.get("hashes")?.split(",") ?? undefined;
if (!mapHashes) {
return new Response("mapHashes parameter is required", { status: 400 });
}
const idOnly = searchParams.get("idonly") === "true";
const maps: Record<string, BeatsaverMap | { id: string }> = {};
for (const mapHash of mapHashes) {
if (mapCache.has(mapHash)) {
maps[mapHash] = mapCache.get(mapHash)!;
} else {
const map = await BeatsaverAPI.fetchMapByHash(mapHash);
if (map) {
maps[mapHash] = map;
}
if (map && idOnly) {
maps[mapHash] = { id: map.id };
}
}
return new Response(JSON.stringify(maps), {
headers: { "content-type": "application/json;charset=UTF-8" },
});
}
}

View File

@ -5,6 +5,7 @@ import clsx from "clsx";
import { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import Script from "next/script";
import "react-toastify/dist/ReactToastify.css";
import "./globals.css";

View File

@ -69,7 +69,12 @@ export async function generateMetadata({
*/
async function getData(id: string, page: number, sort: string) {
const playerData = await ScoreSaberAPI.fetchPlayerData(id);
const playerScores = await ScoreSaberAPI.fetchScores(id, page, sort, 10);
const playerScores = await ScoreSaberAPI.fetchScoresWithBeatsaverData(
id,
page,
sort,
10,
);
return {
playerData: playerData,
playerScores: playerScores,

View File

@ -1,7 +1,7 @@
"use client";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { ScoresaberScoreWithBeatsaverData } from "@/schemas/scoresaber/scoreWithBeatsaverData";
import { useSettingsStore } from "@/store/settingsStore";
import { SortType, SortTypes } from "@/types/SortTypes";
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
@ -10,17 +10,17 @@ import { useCallback, useEffect, useState } from "react";
import Card from "../Card";
import Error from "../Error";
import Pagination from "../Pagination";
import Score from "./Score";
import Score from "../score/Score";
type PageInfo = {
page: number;
totalPages: number;
sortType: SortType;
scores: ScoresaberPlayerScore[];
scores: Record<string, ScoresaberScoreWithBeatsaverData>;
};
type ScoresProps = {
initalScores: ScoresaberPlayerScore[] | undefined;
initalScores: Record<string, ScoresaberScoreWithBeatsaverData> | undefined;
initalPage: number;
initalSortType: SortType;
initalTotalPages?: number;
@ -45,7 +45,7 @@ export default function Scores({
page: initalPage,
totalPages: initalTotalPages || 1,
sortType: initalSortType,
scores: initalScores ? initalScores : [],
scores: initalScores ? initalScores : {},
});
const [changedPage, setChangedPage] = useState(false);
@ -61,32 +61,35 @@ export default function Scores({
return;
}
ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then(
(scoresResponse) => {
if (!scoresResponse) {
setError(true);
setErrorMessage("No Scores");
setScores({ ...scores });
return;
}
setScores({
...scores,
scores: scoresResponse.scores,
totalPages: scoresResponse.pageInfo.totalPages,
page: page,
sortType: sortType,
});
settingsStore?.setLastUsedSortType(sortType);
window.history.pushState(
{},
"",
`/player/${playerId}/${sortType.value}/${page}`,
);
setChangedPage(true);
ScoreSaberAPI.fetchScoresWithBeatsaverData(
playerId,
page,
sortType.value,
10,
).then((scoresResponse) => {
if (!scoresResponse) {
setError(true);
setErrorMessage("No Scores");
setScores({ ...scores });
return;
}
setScores({
...scores,
scores: scoresResponse.scores,
totalPages: scoresResponse.pageInfo.totalPages,
page: page,
sortType: sortType,
});
settingsStore?.setLastUsedSortType(sortType);
window.history.pushState(
{},
"",
`/player/${playerId}/${sortType.value}/${page}`,
);
setChangedPage(true);
console.log(`Switched page to ${page} with sort ${sortType.value}`);
},
);
console.log(`Switched page to ${page} with sort ${sortType.value}`);
});
},
[
changedPage,
@ -149,8 +152,8 @@ export default function Scores({
<div className="flex h-full w-full flex-col items-center justify-center">
<>
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
{scores.scores.map((scoreData, id) => {
const { score, leaderboard } = scoreData;
{Object.values(scores.scores).map((scoreData, id) => {
const { score, leaderboard, mapId } = scoreData;
return (
<Score
@ -158,6 +161,7 @@ export default function Scores({
player={playerData}
score={score}
leaderboard={leaderboard}
mapId={mapId}
ownProfile={settingsStore?.player}
/>
);

View File

@ -0,0 +1,34 @@
"use client";
import { toast } from "react-toastify";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
import { Button } from "../ui/button";
type CopyBsrButtonProps = {
mapId: string;
};
export default function CopyBsrButton({ mapId }: CopyBsrButtonProps) {
return (
<Tooltip>
<TooltipTrigger>
<Button
className="h-[30px] w-[30px] p-1"
variant={"secondary"}
onClick={() => {
navigator.clipboard.writeText(`!bsr ${mapId}`);
toast.success("Copied BSR code to clipboard");
}}
>
<p>!</p>
</Button>
</TooltipTrigger>
<TooltipContent>
<div>
<p>Click to copy the BSR code</p>
<p>!bsr {mapId}</p>
</div>
</TooltipContent>
</Tooltip>
);
}

View File

@ -17,15 +17,19 @@ import {
import clsx from "clsx";
import Image from "next/image";
import Link from "next/link";
import BeatSaverLogo from "../icons/BeatSaverLogo";
import HeadsetIcon from "../icons/HeadsetIcon";
import ScoreStatLabel from "../player/ScoreStatLabel";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
import ScoreStatLabel from "./ScoreStatLabel";
import { Button } from "../ui/button";
import CopyBsrButton from "./CopyBsrButton";
type ScoreProps = {
score: ScoresaberScore;
player: ScoresaberPlayer;
leaderboard: ScoresaberLeaderboardInfo;
ownProfile?: ScoresaberPlayer;
mapId?: string;
};
export default function Score({
@ -33,6 +37,7 @@ export default function Score({
player,
leaderboard,
ownProfile,
mapId,
}: ScoreProps) {
const isFullCombo = score.missedNotes + score.badCuts === 0;
const diffName = scoresaberDifficultyNumberToName(
@ -44,7 +49,7 @@ export default function Score({
const weightedPp = formatNumber(getPpGainedFromScore(player.id, score), 2);
return (
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[0.85fr_6fr_1.3fr]">
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[0.85fr_6fr_0.5fr_1.3fr]">
<div className="flex flex-col items-center justify-center">
<div className="hidden w-fit flex-row items-center justify-center gap-1 md:flex">
<GlobeAsiaAustraliaIcon width={20} height={20} />
@ -117,6 +122,30 @@ export default function Score({
</Link>
</div>
<div className="hidden items-center justify-between gap-1 p-1 md:flex md:items-start md:justify-end">
{mapId && (
<>
<Tooltip>
<TooltipTrigger>
<Link href={`https://beatsaver.com/maps/${mapId}`}>
<Button
className="h-[30px] w-[30px] p-1"
variant={"secondary"}
>
<BeatSaverLogo size={20} />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Click to open the map page</p>
</TooltipContent>
</Tooltip>
<CopyBsrButton mapId={mapId} />
</>
)}
</div>
<div className="flex items-center justify-between p-1 md:items-start md:justify-end">
<div className="flex flex-col md:hidden">
{/* Score rank */}

View File

@ -0,0 +1,10 @@
import { ScoresaberLeaderboardInfo } from "./leaderboard";
import { ScoresaberScore } from "./score";
export type ScoresaberScoreWithBeatsaverData = {
score: ScoresaberScore;
leaderboard: ScoresaberLeaderboardInfo;
// Beatsaver data
mapId: string;
};

View File

@ -2,9 +2,11 @@ import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { ScoresaberScore } from "@/schemas/scoresaber/score";
import { ScoresaberScoreWithBeatsaverData } from "@/schemas/scoresaber/scoreWithBeatsaverData";
import ssrSettings from "@/ssrSettings.json";
import { FetchQueue } from "../fetchWithQueue";
import { formatString } from "../string";
import { isProduction } from "../utils";
// Create a fetch instance with a cache
export const ScoresaberFetchQueue = new FetchQueue();
@ -123,6 +125,73 @@ async function fetchScores(
};
}
async function fetchScoresWithBeatsaverData(
playerId: string,
page: number = 1,
searchType: string = SearchType.RECENT,
limit: number = 100,
): Promise<
| {
scores: Record<string, ScoresaberScoreWithBeatsaverData>;
pageInfo: {
totalScores: number;
page: number;
totalPages: number;
};
}
| undefined
> {
if (limit > 100) {
throw new Error("Limit cannot be greater than 100");
}
const response = await ScoresaberFetchQueue.fetch(
formatString(SS_PLAYER_SCORES, true, playerId, limit, searchType, page),
);
const json = await response.json();
// Check if there was an error fetching the user data
if (json.errorMessage) {
return undefined;
}
const scores = json.playerScores as ScoresaberPlayerScore[];
const metadata = json.metadata;
// Fetch the beatsaver data for each score
const scoresWithBeatsaverData: Record<
string,
ScoresaberScoreWithBeatsaverData
> = {};
for (const score of scores) {
const mapResponse = await fetch(
`${
isProduction() ? ssrSettings.siteUrl : "http://localhost:3000"
}/api/beatsaver/mapdata?hashes=${score.leaderboard.songHash}&idonly=true`,
{
next: {
revalidate: 60 * 60 * 24 * 7, // 1 week
},
},
);
const mapData = await mapResponse.json();
const mapId = mapData[score.leaderboard.songHash].id;
scoresWithBeatsaverData[score.score.id] = {
score: score.score,
leaderboard: score.leaderboard,
mapId: mapId,
};
}
return {
scores: scoresWithBeatsaverData,
pageInfo: {
totalScores: metadata.total,
page: metadata.page,
totalPages: Math.ceil(metadata.total / metadata.itemsPerPage),
},
};
}
/**
* Gets all of the players for the given player id
*
@ -275,6 +344,7 @@ export const ScoreSaberAPI = {
searchByName,
fetchPlayerData,
fetchScores,
fetchScoresWithBeatsaverData,
fetchAllScores,
fetchTopPlayers,
fetchLeaderboardInfo,