31 Commits

Author SHA1 Message Date
c332c54ba1 chore(deps): update dependency autoprefixer to v10.4.18 2024-03-06 01:17:11 +00:00
2fedf0e6ff add shortened name for site name
All checks were successful
deploy / deploy (push) Successful in 1m8s
2024-02-01 03:20:44 +00:00
a14ff2f343 Merge branch 'master' of https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2
All checks were successful
deploy / deploy (push) Successful in 1m2s
2024-02-01 02:23:56 +00:00
be9328bab5 why was this disabled? 2024-02-01 02:23:55 +00:00
Lee
73046d46d4 Merge pull request 'fix(deps): update dependency sharp to ^0.33.0' (#42) from renovate/sharp-0.x into master
All checks were successful
deploy / deploy (push) Successful in 33s
Reviewed-on: #42
2024-02-01 02:11:11 +00:00
Lee
6fcb561085 Merge pull request 'fix(deps): update dependency lucide-react to ^0.320.0' (#41) from renovate/lucide-react-0.x into master
Some checks failed
deploy / deploy (push) Failing after 4s
Reviewed-on: #41
2024-02-01 02:11:04 +00:00
Lee
fac75f1e6a Merge pull request 'chore(deps): update dependency eslint-config-next to v14.1.0' (#43) from renovate/nextjs-monorepo into master
Some checks failed
deploy / deploy (push) Failing after 5s
Reviewed-on: #43
2024-02-01 02:10:56 +00:00
Lee
9b5d38896d Merge pull request 'fix(deps): update dependency date-fns to v3' (#44) from renovate/date-fns-3.x into master
Some checks failed
deploy / deploy (push) Has been cancelled
Reviewed-on: #44
2024-02-01 02:10:48 +00:00
1b42f9f6d9 fix(deps): update dependency date-fns to v3 2024-02-01 02:09:43 +00:00
136fc06469 fix(deps): update dependency sharp to ^0.33.0 2024-02-01 02:09:38 +00:00
e6f2e6a03d fix(deps): update dependency lucide-react to ^0.320.0 2024-02-01 02:09:29 +00:00
c0c8b625be chore(deps): update dependency eslint-config-next to v14.1.0 2024-02-01 02:09:25 +00:00
ae482a48c0 bump nextjs
All checks were successful
deploy / deploy (push) Successful in 1m35s
2024-02-01 01:52:57 +00:00
75bf876fb6 bump depends
All checks were successful
deploy / deploy (push) Successful in 1m31s
2024-02-01 01:51:03 +00:00
Lee
2d9de44fa9 Merge pull request 'fix(deps): update dependency react-toastify to v10' (#45) from renovate/react-toastify-10.x into master
Some checks failed
deploy / deploy (push) Failing after 46s
Reviewed-on: #45
2024-02-01 01:47:11 +00:00
8c9a5de93b i am a goof ball
All checks were successful
deploy / deploy (push) Successful in 1m3s
2024-02-01 01:45:07 +00:00
56f7255918 sentry stuff
All checks were successful
deploy / deploy (push) Successful in 1m14s
2024-02-01 01:41:10 +00:00
f58ea539f9 remove sentry
Some checks failed
deploy / deploy (push) Failing after 27s
2024-02-01 01:39:37 +00:00
0f70a50bd4 remove analytics page
Some checks failed
deploy / deploy (push) Failing after 21s
2024-02-01 01:37:52 +00:00
738a95180f fix(deps): update dependency react-toastify to v10 2024-01-14 18:04:03 +00:00
Lee
4e3ba2d0c7 Update src/components/Footer.tsx
Some checks failed
deploy / deploy (push) Has been cancelled
2023-12-07 16:33:06 +00:00
9e73ff3937 always show country flags in ranking pages
All checks were successful
deploy / deploy (push) Successful in 2m3s
2023-11-27 01:03:27 +00:00
3b7f458d5c update how player ranks are shown in countries
All checks were successful
deploy / deploy (push) Successful in 1m13s
2023-11-27 00:57:57 +00:00
d769b0d15e remove settings on navbar (for now)
All checks were successful
deploy / deploy (push) Successful in 1m10s
2023-11-26 14:31:18 +00:00
cf75dfb06d make padding slighly less on cards
All checks were successful
deploy / deploy (push) Successful in 2m10s
2023-11-26 01:56:05 +00:00
41090360e1 try/catch on sitemap
All checks were successful
deploy / deploy (push) Successful in 1m12s
2023-11-24 21:02:58 +00:00
b5629c0169 fix button alignment on score
Some checks failed
deploy / deploy (push) Failing after 36s
2023-11-24 21:00:50 +00:00
4c0775b000 fix(ssr): still show the youtube button even if the beatsaver map doesn't exist anymore
All checks were successful
deploy / deploy (push) Successful in 1m14s
2023-11-24 20:50:51 +00:00
407bcc866b defer beatsaver map data loading & fix it being fucked
All checks were successful
deploy / deploy (push) Successful in 1m15s
2023-11-24 20:43:23 +00:00
b74b22c0b6 Merge branch 'master' of https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2
All checks were successful
deploy / deploy (push) Successful in 1m14s
2023-11-24 19:29:40 +00:00
0f7b28ca02 only get ids for beatsaver maps 2023-11-24 19:29:39 +00:00
24 changed files with 1289 additions and 1600 deletions

View File

@ -4,16 +4,20 @@ const { getCodeList } = require("country-list");
const SS_API_URL = ssrSettings.proxy + "/https://scoresaber.com/api"; const SS_API_URL = ssrSettings.proxy + "/https://scoresaber.com/api";
const SS_GET_PLAYERS_URL = SS_API_URL + "/players?page={}"; const SS_GET_PLAYERS_URL = SS_API_URL + "/players?page={}";
// todo: cache this somehow? // todo: cache this on a file somehow?
async function getTopPlayers() { async function getTopPlayers() {
console.log("Fetching top players..."); console.log("Fetching top players...");
const players = []; const players = [];
const pagesToFetch = 30; const pagesToFetch = 10;
for (let i = 0; i < pagesToFetch; i++) { for (let i = 0; i < pagesToFetch; i++) {
console.log(`Fetching page ${i + 1} of ${pagesToFetch}...`); console.log(`Fetching page ${i + 1} of ${pagesToFetch}...`);
const response = await fetch(SS_GET_PLAYERS_URL.replace("{}", i)); try {
const data = await response.json(); const response = await fetch(SS_GET_PLAYERS_URL.replace("{}", i));
players.push(...data.players); const data = await response.json();
players.push(...data.players);
} catch (e) {
console.log(`Error fetching page ${i + 1} of ${pagesToFetch}: ${e}`);
}
} }
console.log("Done fetching top players."); console.log("Done fetching top players.");
return players; return players;

View File

@ -1,12 +1,5 @@
const nextBuildId = require("next-build-id"); const nextBuildId = require("next-build-id");
const { withSentryConfig } = require("@sentry/nextjs");
const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: false }); const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: false });
const InfisicalClient = require("infisical-node");
const infisicalClient = new InfisicalClient({
token: process.env.INFISICAL_TOKEN,
siteURL: "https://secrets.fascinated.cc",
});
// Define remote patterns for images // Define remote patterns for images
const remotePatterns = [ const remotePatterns = [
@ -41,7 +34,7 @@ const nextConfig = {
generateEtags: true, generateEtags: true,
reactStrictMode: true, reactStrictMode: true,
swcMinify: true, swcMinify: true,
compress: false, compress: true,
poweredByHeader: false, poweredByHeader: false,
experimental: { experimental: {
webpackBuildWorker: true, webpackBuildWorker: true,
@ -61,23 +54,4 @@ const nextConfig = {
images: { remotePatterns }, images: { remotePatterns },
}; };
module.exports = async () => module.exports = withBundleAnalyzer(nextConfig);
withSentryConfig(
withBundleAnalyzer(nextConfig),
{
silent: true,
org: "sentry",
project: "scoresaber-reloaded",
url: "https://sentry.fascinated.cc",
authToken: (await infisicalClient.getSecret("SENTRY_AUTH_TOKEN"))
.secretValue,
dryRun: process.env.NODE_ENV !== "development",
},
{
widenClientFileUpload: false,
transpileClientSDK: false,
tunnelRoute: "/monitoring",
hideSourceMaps: true,
disableLogger: true,
},
);

View File

@ -11,8 +11,8 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@boiseitguru/cookie-cutter": "^0.2.1", "@boiseitguru/cookie-cutter": "^0.2.3",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.1.1",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-radio-group": "^1.1.3",
@ -21,46 +21,45 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.74.1", "chart.js": "^4.4.1",
"chart.js": "^4.4.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.1.0",
"country-list": "^2.3.0", "country-list": "^2.3.0",
"critters": "^0.0.20", "critters": "^0.0.20",
"date-fns": "^2.30.0", "date-fns": "^3.0.0",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"infisical-node": "^1.5.0", "infisical-node": "^1.5.1",
"lucide-react": "^0.293.0", "lucide-react": "^0.320.0",
"next": "14.0.3", "next": "^14.1.0",
"next-build-id": "^3.0.0", "next-build-id": "^3.0.0",
"next-sitemap": "^4.2.3", "next-sitemap": "^4.2.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"react": "^18", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18", "react-dom": "^18.2.0",
"react-toastify": "^9.1.3", "react-toastify": "^10.0.4",
"redis": "^4.6.10", "redis": "^4.6.12",
"sharp": "^0.32.6", "sharp": "^0.33.0",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"websocket": "^1.0.34", "websocket": "^1.0.34",
"zustand": "^4.4.3" "zustand": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^14.0.0", "@next/bundle-analyzer": "^14.1.0",
"@types/node": "^20", "@types/node": "^20.11.14",
"@types/react": "^18", "@types/react": "^18.2.48",
"@types/react-dom": "^18", "@types/react-dom": "^18.2.18",
"@types/websocket": "^1.0.8", "@types/websocket": "^1.0.10",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.17",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8", "eslint": "^8.56.0",
"eslint-config-next": "14.0.3", "eslint-config-next": "14.1.0",
"postcss": "^8.4.31", "postcss": "^8.4.33",
"prettier": "^3.0.3", "prettier": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.6", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5.3.3"
} }
} }

2238
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://1922e9d5b729f1568ad1e9537a118056@sentry.fascinated.cc/2",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -1,16 +0,0 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://1922e9d5b729f1568ad1e9537a118056@sentry.fascinated.cc/2",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -1,15 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://1922e9d5b729f1568ad1e9537a118056@sentry.fascinated.cc/2",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@ -1,89 +0,0 @@
import AnalyticsChart from "@/components/AnalyticsChart";
import Card from "@/components/Card";
import Container from "@/components/Container";
import { ScoresaberMetricsHistory } from "@/schemas/fascinated/scoresaberMetricsHistory";
import ssrSettings from "@/ssrSettings.json";
import { formatNumber } from "@/utils/numberUtils";
import { formatDate } from "@/utils/timeUtils";
import { isProduction } from "@/utils/utils";
import { Metadata } from "next";
import Link from "next/link";
async function getData() {
const response = await fetch(
"https://bs-tracker.fascinated.cc/analytics?time=30d",
{
next: {
revalidate: isProduction() ? 600 : 0, // 10 minutes (0 seconds in dev)
},
},
);
const json = await response.json();
return {
data: json as ScoresaberMetricsHistory,
};
}
export async function generateMetadata(): Promise<Metadata> {
const { data } = await getData();
const description =
"View Scoresaber metrics and statistics over the last 30 days.";
const lastActivePlayers =
data.activePlayersHistory[data.activePlayersHistory.length - 1].value;
const lastScoreCount =
data.scoreCountHistory[data.scoreCountHistory.length - 1].value;
return {
title: `Analytics`,
description: description,
openGraph: {
siteName: ssrSettings.siteName,
title: `Analytics`,
description:
description +
`
Last Updated: ${formatDate(new Date().toISOString())}
Players Online Today: ${formatNumber(lastActivePlayers)}
Scores set Today: ${formatNumber(lastScoreCount)}`,
},
};
}
export default async function Analytics() {
const { data } = await getData();
return (
<main>
<Container>
<Card
outerClassName="mt-2"
className="flex flex-col items-center justify-center"
>
<h1 className="text-center text-3xl font-bold">Analytics</h1>
<p className="text-center">
Scoresaber metrics and statistics over the last 30 days.
</p>
<p className="text-gray-300">
Want more in-depth data? Click{" "}
<span className="text-pp-blue">
<Link
href="https://grafana.fascinated.cc/d/b3c6c28d-39e9-4fa9-8e2b-b0ddb10f875e/beatsaber-metrics"
target="_blank"
rel="noopener noreferrer"
>
here
</Link>
</span>
</p>
<div className="mt-3 h-[400px] w-full">
<AnalyticsChart historyData={data} />
</div>
</Card>
</Container>
</main>
);
}

View File

@ -4,11 +4,24 @@ import { BeatsaverAPI } from "@/utils/beatsaver/api";
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const mapHashes = searchParams.get("hashes")?.split(",") ?? undefined; const mapHashes = searchParams.get("hashes");
if (!mapHashes) { if (!mapHashes) {
return new Response("mapHashes parameter is required", { status: 400 }); return new Response("mapHashes parameter is required", { status: 400 });
} }
let toFetch: any[] = [];
if (mapHashes.includes(",")) {
const parts = mapHashes.substring(0, mapHashes.length - 1).split(",");
toFetch.push(...parts);
} else {
toFetch.push(mapHashes);
}
// Convert all hashes to uppercase
for (const hash of toFetch) {
toFetch[toFetch.indexOf(hash)] = hash.toUpperCase();
}
// Remove duplicates
toFetch = toFetch.filter((hash, index) => toFetch.indexOf(hash) === index);
const idOnly = searchParams.get("idonly") === "true"; const idOnly = searchParams.get("idonly") === "true";
let totalInCache = 0; let totalInCache = 0;
@ -24,39 +37,51 @@ export async function GET(request: Request) {
const fetchAndCacheMap = async (mapHash: string) => { const fetchAndCacheMap = async (mapHash: string) => {
const beatSaverMap = await BeatsaverAPI.fetchMapByHash(mapHash); const beatSaverMap = await BeatsaverAPI.fetchMapByHash(mapHash);
if (beatSaverMap) { if (beatSaverMap) {
maps[mapHash] = idOnly ? { id: beatSaverMap.id } : beatSaverMap; addMap(mapHash, beatSaverMap);
await ( await cacheMap(mapHash, beatSaverMap);
await Redis.client
).set(
`beatsaver:map:${mapHash}`,
JSON.stringify(idOnly ? { id: beatSaverMap.id } : beatSaverMap),
"EX",
60 * 60 * 24 * 7,
);
} }
}; };
for (const mapHash of mapHashes) { const cacheMap = async (mapHash: string, map: BeatsaverMap) => {
await (
await Redis.client
).set(
`beatsaver:map:${mapHash}`,
JSON.stringify(idOnly ? { id: map.id } : map),
"EX",
60 * 60 * 24 * 7,
);
};
const addMap = (mapHash: string, map: any) => {
maps[mapHash] = idOnly ? { id: map.id } : map;
};
for (const mapHash of toFetch) {
const map = await fetchMapFromCache(mapHash); const map = await fetchMapFromCache(mapHash);
if (map) { if (map !== null) {
maps[mapHash] = JSON.parse(map); const json = JSON.parse(map);
addMap(mapHash, json);
totalInCache++; totalInCache++;
} }
} }
if (totalInCache === 0) { if (totalInCache === 0 && toFetch.length > 1) {
const beatSaverMaps = await BeatsaverAPI.fetchMapsByHash(...mapHashes); const beatSaverMaps = await BeatsaverAPI.fetchMapsByHash(...toFetch);
if (beatSaverMaps) { if (beatSaverMaps) {
for (const mapHash of mapHashes) { for (const mapHash of toFetch) {
const beatSaverMap = beatSaverMaps[mapHash.toLowerCase()]; const beatSaverMap = beatSaverMaps[mapHash.toLowerCase()];
if (beatSaverMap) { if (beatSaverMap) {
await fetchAndCacheMap(mapHash); await cacheMap(mapHash, beatSaverMap);
addMap(mapHash, beatSaverMap);
} }
} }
} }
} else { } else {
for (const mapHash of mapHashes) { for (const mapHash of toFetch) {
if (!maps[mapHash]) { if (!maps[mapHash]) {
await fetchAndCacheMap(mapHash); await fetchAndCacheMap(mapHash);
} }

View File

@ -18,7 +18,7 @@ export const viewport: Viewport = {
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(ssrSettings.siteUrl), metadataBase: new URL(ssrSettings.siteUrl),
title: { title: {
template: ssrSettings.siteName + " - %s", template: ssrSettings.siteNameShort + " - %s",
default: ssrSettings.siteName, default: ssrSettings.siteName,
}, },
description: ssrSettings.description, description: ssrSettings.description,

View File

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

View File

@ -14,7 +14,7 @@ export default function Card({
}: CardProps) { }: CardProps) {
return ( return (
<CardBase className={outerClassName}> <CardBase className={outerClassName}>
<CardContent className={clsx(className, "pb-4 pt-2")}> <CardContent className={clsx(className, "p-3 pb-4 pt-2")}>
{children} {children}
</CardContent> </CardContent>
</CardBase> </CardBase>

View File

@ -22,7 +22,7 @@ const buildId = process.env.NEXT_PUBLIC_BUILD_ID
const buildTime = process.env.NEXT_PUBLIC_BUILD_TIME; const buildTime = process.env.NEXT_PUBLIC_BUILD_TIME;
const gitUrl = isProduction() const gitUrl = isProduction()
? `https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2/commit/${buildId}` ? `https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2/commit/${buildId}`
: "https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2"; : "https://s.fascinated.cc/s/ssr-gitea";
export default function Footer() { export default function Footer() {
return ( return (
@ -31,7 +31,7 @@ export default function Footer() {
<div className="flex flex-col items-center gap-1 md:flex-row md:items-start md:gap-3"> <div className="flex flex-col items-center gap-1 md:flex-row md:items-start md:gap-3">
<a <a
className="transform-gpu transition-all hover:text-blue-500" className="transform-gpu transition-all hover:text-blue-500"
href="https://git.fascinated.cc/Fascinated/scoresaber-reloaded-v2" href="https://s.fascinated.cc/s/ssr-gitea"
> >
{ssrSettings.siteName} {ssrSettings.siteName}
</a> </a>

View File

@ -57,7 +57,7 @@ export default function GlobalRanking({
{players.map((player) => ( {players.map((player) => (
<tr key={player.rank} className="border-b border-border"> <tr key={player.rank} className="border-b border-border">
<PlayerRanking <PlayerRanking
showCountryFlag={country ? false : true} isCountry={country == undefined ? false : true}
player={player} player={player}
/> />
</tr> </tr>

View File

@ -1,8 +1,4 @@
import { import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
CogIcon,
MagnifyingGlassIcon,
ServerIcon,
} from "@heroicons/react/20/solid";
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline"; import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
import { Card } from "../ui/card"; import { Card } from "../ui/card";
import FriendsButton from "./FriendsButton"; import FriendsButton from "./FriendsButton";
@ -29,12 +25,6 @@ export default function Navbar() {
icon={<TvIcon height={23} width={23} />} icon={<TvIcon height={23} width={23} />}
href="/overlay/builder" href="/overlay/builder"
/> />
<NavbarButton
ariaLabel="View analytics for Scoresaber"
text="Analytics"
icon={<ServerIcon height={23} width={23} />}
href="/analytics"
/>
<div className="m-auto" /> <div className="m-auto" />
@ -44,12 +34,12 @@ export default function Navbar() {
icon={<MagnifyingGlassIcon height={23} width={23} />} icon={<MagnifyingGlassIcon height={23} width={23} />}
href="/search" href="/search"
/> />
<NavbarButton {/* <NavbarButton
ariaLabel="View your settings" ariaLabel="View your settings"
text="Settings" text="Settings"
icon={<CogIcon height={23} width={23} />} icon={<CogIcon height={23} width={23} />}
href="/settings" href="/settings"
/> /> */}
</Card> </Card>
</> </>
); );

View File

@ -8,25 +8,26 @@ import CountyFlag from "../CountryFlag";
type PlayerRankingProps = { type PlayerRankingProps = {
player: ScoresaberPlayer; player: ScoresaberPlayer;
showCountryFlag?: boolean; isCountry?: boolean;
}; };
const Avatar = dynamic(() => import("@/components/Avatar")); const Avatar = dynamic(() => import("@/components/Avatar"));
export default function PlayerRanking({ export default function PlayerRanking({
player, player,
showCountryFlag, isCountry,
}: PlayerRankingProps) { }: PlayerRankingProps) {
const settingsStore = useStore(useSettingsStore, (store) => store); const settingsStore = useStore(useSettingsStore, (store) => store);
return ( return (
<> <>
<td className="px-4 py-2">#{formatNumber(player.rank)}</td> <td className="px-4 py-2">
#{formatNumber(isCountry ? player.countryRank : player.rank)}{" "}
<span className="text-sm">{isCountry && "(#" + player.rank + ")"}</span>
</td>
<td className="flex items-center gap-2 px-4 py-2"> <td className="flex items-center gap-2 px-4 py-2">
<Avatar url={player.profilePicture} label="Avatar" size={24} /> <Avatar url={player.profilePicture} label="Avatar" size={24} />
{showCountryFlag && ( <CountyFlag countryCode={player.country} className="!h-5 !w-5" />
<CountyFlag countryCode={player.country} className="!h-5 !w-5" />
)}
<Link <Link
className="transform-gpu transition-all hover:text-blue-500" className="transform-gpu transition-all hover:text-blue-500"
href={`/player/${player.id}/top/1`} href={`/player/${player.id}/top/1`}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberScoreWithBeatsaverData } from "@/schemas/scoresaber/scoreWithBeatsaverData"; import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { useSettingsStore } from "@/store/settingsStore"; import { useSettingsStore } from "@/store/settingsStore";
import { SortType, SortTypes } from "@/types/SortTypes"; import { SortType, SortTypes } from "@/types/SortTypes";
import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api";
@ -16,11 +16,11 @@ type PageInfo = {
page: number; page: number;
totalPages: number; totalPages: number;
sortType: SortType; sortType: SortType;
scores: Record<string, ScoresaberScoreWithBeatsaverData>; scores: ScoresaberPlayerScore[] | undefined;
}; };
type ScoresProps = { type ScoresProps = {
initalScores: Record<string, ScoresaberScoreWithBeatsaverData> | undefined; initalScores: ScoresaberPlayerScore[] | undefined;
initalPage: number; initalPage: number;
initalSortType: SortType; initalSortType: SortType;
initalTotalPages?: number; initalTotalPages?: number;
@ -45,7 +45,7 @@ export default function Scores({
page: initalPage, page: initalPage,
totalPages: initalTotalPages || 1, totalPages: initalTotalPages || 1,
sortType: initalSortType, sortType: initalSortType,
scores: initalScores ? initalScores : {}, scores: initalScores,
}); });
const [changedPage, setChangedPage] = useState(false); const [changedPage, setChangedPage] = useState(false);
@ -61,35 +61,32 @@ export default function Scores({
return; return;
} }
ScoreSaberAPI.fetchScoresWithBeatsaverData( ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then(
playerId, (scoresResponse) => {
page, if (!scoresResponse) {
sortType.value, setError(true);
10, setErrorMessage("No Scores");
).then((scoresResponse) => { setScores({ ...scores });
if (!scoresResponse) { return;
setError(true); }
setErrorMessage("No Scores"); setScores({
setScores({ ...scores }); ...scores,
return; scores: scoresResponse.scores,
} totalPages: scoresResponse.pageInfo.totalPages,
setScores({ page: page,
...scores, sortType: sortType,
scores: scoresResponse.scores, });
totalPages: scoresResponse.pageInfo.totalPages, settingsStore?.setLastUsedSortType(sortType);
page: page, window.history.pushState(
sortType: sortType, {},
}); "",
settingsStore?.setLastUsedSortType(sortType); `/player/${playerId}/${sortType.value}/${page}`,
window.history.pushState( );
{}, setChangedPage(true);
"",
`/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, changedPage,
@ -151,32 +148,36 @@ export default function Scores({
<div className="flex h-full w-full flex-col items-center justify-center"> <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 ? (
{Object.values(scores.scores).map((scoreData, id) => { <>
const { score, leaderboard, mapId } = scoreData; <div className="grid min-w-full grid-cols-1 divide-y divide-border">
{scores.scores.map((scoreData, id) => {
const { score, leaderboard } = scoreData;
return ( return (
<Score <Score
key={id} key={id}
player={playerData} player={playerData}
score={score} score={score}
leaderboard={leaderboard} leaderboard={leaderboard}
mapId={mapId} />
ownProfile={settingsStore?.player} );
})}
</div>
{/* Pagination */}
<div className="pt-3">
<Pagination
currentPage={scores.page}
totalPages={scores.totalPages}
onPageChange={(page) => {
updateScoresPage(scores.sortType, page);
}}
/> />
); </div>
})} </>
</div> ) : (
{/* Pagination */} <p>No Scores!</p>
<div className="pt-3"> )}
<Pagination
currentPage={scores.page}
totalPages={scores.totalPages}
onPageChange={(page) => {
updateScoresPage(scores.sortType, page);
}}
/>
</div>
</> </>
</div> </div>
</Card> </Card>

View File

@ -0,0 +1,98 @@
"use client";
import BeatSaverLogo from "@/components/icons/BeatSaverLogo";
import YouTubeLogo from "@/components/icons/YouTubeLogo";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/Tooltip";
import { Button } from "@/components/ui/button";
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
import { songNameToYouTubeLink } from "@/utils/songUtils";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import CopyBsrButton from "./CopyBsrButton";
type MapButtonsProps = {
leaderboard: ScoresaberLeaderboardInfo;
};
export default function MapButtons({ leaderboard }: MapButtonsProps) {
const [mapId, setMapId] = useState<string | undefined>(undefined);
const hash = leaderboard.songHash;
const getMapId = useCallback(async () => {
const beatSaberMap = await fetch(
`/api/beatsaver/mapdata?hashes=${hash}&idonly=true`,
);
if (!beatSaberMap) {
return;
}
const json = await beatSaberMap.json();
if (json.maps[hash] == null || json.maps[hash] == undefined) {
return;
}
console.log(json);
setMapId(json.maps[hash].id);
}, [hash]);
useEffect(() => {
getMapId();
}, [getMapId]);
return (
<div className="hidden flex-col items-center gap-2 p-1 md:flex md:items-start">
{mapId && (
<>
<div className="flex gap-2">
<Tooltip>
<TooltipTrigger>
<Link
href={`https://beatsaver.com/maps/${mapId}`}
target="_blank"
>
<Button
className="h-[30px] w-[30px] bg-neutral-700 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 gap-2">
<Tooltip>
<TooltipTrigger>
<Link
href={`${songNameToYouTubeLink(
leaderboard.songName,
leaderboard.songSubName,
leaderboard.songAuthorName,
)}`}
target="_blank"
>
<Button
className="h-[30px] w-[30px] bg-neutral-700 p-1"
variant={"secondary"}
>
<YouTubeLogo size={20} />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Click to view the song on YouTube</p>
</TooltipContent>
</Tooltip>
</div>
</>
)}
</div>
);
}

View File

@ -1,4 +1,3 @@
import YouTubeLogo from "@/components/icons/YouTubeLogo";
import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard"; import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard";
import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { ScoresaberScore } from "@/schemas/scoresaber/score"; import { ScoresaberScore } from "@/schemas/scoresaber/score";
@ -7,7 +6,6 @@ import { getPpGainedFromScore } from "@/utils/scoresaber/scores";
import { import {
scoresaberDifficultyNumberToName, scoresaberDifficultyNumberToName,
songDifficultyToColor, songDifficultyToColor,
songNameToYouTubeLink,
} from "@/utils/songUtils"; } from "@/utils/songUtils";
import { formatDate, formatTimeAgo } from "@/utils/timeUtils"; import { formatDate, formatTimeAgo } from "@/utils/timeUtils";
import { import {
@ -19,28 +17,19 @@ import {
import clsx from "clsx"; import clsx from "clsx";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import BeatSaverLogo from "../../icons/BeatSaverLogo"; import { Suspense } from "react";
import HeadsetIcon from "../../icons/HeadsetIcon"; import HeadsetIcon from "../../icons/HeadsetIcon";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/Tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/Tooltip";
import { Button } from "../../ui/button";
import ScoreStatLabel from "../ScoreStatLabel"; import ScoreStatLabel from "../ScoreStatLabel";
import CopyBsrButton from "./CopyBsrButton"; import MapButtons from "./MapButtons";
type ScoreProps = { type ScoreProps = {
score: ScoresaberScore; score: ScoresaberScore;
player: ScoresaberPlayer; player: ScoresaberPlayer;
leaderboard: ScoresaberLeaderboardInfo; leaderboard: ScoresaberLeaderboardInfo;
ownProfile?: ScoresaberPlayer;
mapId?: string;
}; };
export default function Score({ export default function Score({ score, player, leaderboard }: ScoreProps) {
score,
player,
leaderboard,
ownProfile,
mapId,
}: ScoreProps) {
const isFullCombo = score.missedNotes + score.badCuts === 0; const isFullCombo = score.missedNotes + score.badCuts === 0;
const diffName = scoresaberDifficultyNumberToName( const diffName = scoresaberDifficultyNumberToName(
leaderboard.difficulty.difficulty, leaderboard.difficulty.difficulty,
@ -124,58 +113,9 @@ export default function Score({
</Link> </Link>
</div> </div>
<div className="hidden flex-col items-center justify-between gap-1 p-1 md:flex md:items-start md:justify-end"> <Suspense fallback={<div />}>
{mapId && ( <MapButtons leaderboard={leaderboard} />
<> </Suspense>
<div className="flex gap-1">
<Tooltip>
<TooltipTrigger>
<Link
href={`https://beatsaver.com/maps/${mapId}`}
target="_blank"
>
<Button
className="h-[30px] w-[30px] bg-neutral-700 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 gap-1">
<Tooltip>
<TooltipTrigger>
<Link
href={`${songNameToYouTubeLink(
leaderboard.songName,
leaderboard.songSubName,
leaderboard.songAuthorName,
)}`}
target="_blank"
>
<Button
className="h-[30px] w-[30px] bg-neutral-700 p-1"
variant={"secondary"}
>
<YouTubeLogo size={20} />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Click to view the song on YouTube</p>
</TooltipContent>
</Tooltip>
</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">
<div className="flex flex-col md:hidden"> <div className="flex flex-col md:hidden">

View File

@ -1,5 +1,6 @@
{ {
"siteName": "ScoreSaber Reloaded", "siteName": "ScoreSaber Reloaded",
"siteNameShort": "SSR",
"description": "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", "description": "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays",
"siteUrl": "https://ssr.fascinated.cc", "siteUrl": "https://ssr.fascinated.cc",
"proxy": "https://proxy.fascinated.cc" "proxy": "https://proxy.fascinated.cc"

View File

@ -9,15 +9,34 @@ if (typeof window !== "undefined") {
} }
export const IDBStorage: StateStorage = { export const IDBStorage: StateStorage = {
/**
* Fetch an item from the storage
*
* @param name name of the item to be fetched
* @returns the value of the item or null if it doesn't exist
*/
getItem: async (name: string): Promise<string | null> => { getItem: async (name: string): Promise<string | null> => {
//console.log(name, "has been retrieved"); //console.log(name, "has been retrieved");
return (await get(name, storage)) || null; return (await get(name, storage)) || null;
}, },
/**
* Save an item to the storage
*
* @param name name of the item to be saved
* @param value value of the item to be saved
*/
setItem: async (name: string, value: string): Promise<void> => { setItem: async (name: string, value: string): Promise<void> => {
//console.log(name, "with value", value, "has been saved"); //console.log(name, "with value", value, "has been saved");
await set(name, value, storage); await set(name, value, storage);
}, },
/**
* Delete an item from the storage
*
* @param name name of the item to be deleted
*/
removeItem: async (name: string): Promise<void> => { removeItem: async (name: string): Promise<void> => {
//console.log(name, "has been deleted"); //console.log(name, "has been deleted");
await del(name, storage); await del(name, storage);

View File

@ -47,6 +47,7 @@ async function fetchMapByHash(hash: string): Promise<BeatsaverMap | undefined> {
const response = await BeatsaverFetchQueue.fetch( const response = await BeatsaverFetchQueue.fetch(
formatString(BS_GET_MAP_BY_HASH_URL, true, hash), formatString(BS_GET_MAP_BY_HASH_URL, true, hash),
); );
console.log(formatString(BS_GET_MAP_BY_HASH_URL, true, hash));
const json = await response.json(); const json = await response.json();
// Check if there was an error fetching the user data // Check if there was an error fetching the user data

View File

@ -13,7 +13,7 @@ export class FetchQueue {
* @param url - The URL to fetch. * @param url - The URL to fetch.
* @returns The response. * @returns The response.
*/ */
public async fetch(url: string, options?: any): Promise<any> { public async fetch(url: string, options?: any): Promise<Response> {
const now = Date.now(); const now = Date.now();
if (now < this.rateLimitReset) { if (now < this.rateLimitReset) {

View File

@ -178,6 +178,8 @@ async function fetchScoresWithBeatsaverData(
for (const score of scores) { for (const score of scores) {
url += `${score.leaderboard.songHash},`; url += `${score.leaderboard.songHash},`;
} }
url = url.substring(0, url.length - 1);
url += "&idonly=true";
const mapResponse = await fetch(url, { const mapResponse = await fetch(url, {
next: { next: {
revalidate: 60 * 60 * 24 * 7, // 1 week revalidate: 60 * 60 * 24 * 7, // 1 week