Compare commits
65 Commits
96496edfdf
...
renovate/c
Author | SHA1 | Date | |
---|---|---|---|
30bc560a2e | |||
2fedf0e6ff | |||
a14ff2f343 | |||
be9328bab5 | |||
73046d46d4 | |||
6fcb561085 | |||
fac75f1e6a | |||
9b5d38896d | |||
1b42f9f6d9 | |||
136fc06469 | |||
e6f2e6a03d | |||
c0c8b625be | |||
ae482a48c0 | |||
75bf876fb6 | |||
2d9de44fa9 | |||
8c9a5de93b | |||
56f7255918 | |||
f58ea539f9 | |||
0f70a50bd4 | |||
738a95180f | |||
4e3ba2d0c7 | |||
9e73ff3937 | |||
3b7f458d5c | |||
d769b0d15e | |||
cf75dfb06d | |||
41090360e1 | |||
b5629c0169 | |||
4c0775b000 | |||
407bcc866b | |||
b74b22c0b6 | |||
0f7b28ca02 | |||
d5afdbde69 | |||
31d37feb8e | |||
b3c3be4b7c | |||
14f67af125 | |||
bb45a4bd6b | |||
65e8df76a2 | |||
925e423955 | |||
ef7c6f4878 | |||
fd7cbf73a7 | |||
a793847b91 | |||
8c50b484cf | |||
3c5b5be02f | |||
da701cf046 | |||
443b4ce2f7 | |||
cbcaaa2d8c | |||
4a03b0c97f | |||
dbfc6a93d0 | |||
992b97a35d | |||
d1b0d85ecf | |||
7e730ed0c9 | |||
bdb2ffc7ba | |||
95a9e103eb | |||
5e817186de | |||
75afdfed7d | |||
9c2bf54426 | |||
0c9fc07e83 | |||
24c787969c | |||
000812b2e5 | |||
c22dabeb9b | |||
7a31e158ea | |||
b42ba1afdd | |||
426a2b5a2f | |||
350fe875fe | |||
13b814700f |
4
.env
4
.env
@ -1,4 +0,0 @@
|
||||
SENTRY_AUTH_TOKEN=hi
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://:bigtitsyes7@10.0.0.203:30004
|
@ -1,4 +1,4 @@
|
||||
SENTRY_AUTH_TOKEN=hi
|
||||
INFISICAL_TOKEN=hi
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -46,3 +46,5 @@ analyze
|
||||
|
||||
# Sitemap
|
||||
public/sitemap*
|
||||
|
||||
.env
|
5
.infisical.json
Normal file
5
.infisical.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"workspaceId": "6551ad1ded9edd83540488e0",
|
||||
"defaultEnvironment": "",
|
||||
"gitBranchToEnvironmentMapping": null
|
||||
}
|
@ -17,6 +17,9 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ARG GIT_REV
|
||||
ENV GIT_REV ${GIT_REV}
|
||||
|
||||
# ARG REDIS_URL
|
||||
ENV REDIS_URL redis://:bigtitsyes7@10.0.0.203:30004
|
||||
|
||||
# Build the app
|
||||
RUN pnpm run build
|
||||
|
||||
|
@ -4,16 +4,20 @@ const { getCodeList } = require("country-list");
|
||||
const SS_API_URL = ssrSettings.proxy + "/https://scoresaber.com/api";
|
||||
const SS_GET_PLAYERS_URL = SS_API_URL + "/players?page={}";
|
||||
|
||||
// todo: cache this somehow?
|
||||
// todo: cache this on a file somehow?
|
||||
async function getTopPlayers() {
|
||||
console.log("Fetching top players...");
|
||||
const players = [];
|
||||
const pagesToFetch = 30;
|
||||
const pagesToFetch = 10;
|
||||
for (let i = 0; i < pagesToFetch; i++) {
|
||||
console.log(`Fetching page ${i + 1} of ${pagesToFetch}...`);
|
||||
try {
|
||||
const response = await fetch(SS_GET_PLAYERS_URL.replace("{}", i));
|
||||
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.");
|
||||
return players;
|
||||
|
121
next.config.js
121
next.config.js
@ -1,16 +1,22 @@
|
||||
const nextBuildId = require("next-build-id");
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: false,
|
||||
});
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: false });
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
generateEtags: true,
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
experimental: {
|
||||
webpackBuildWorker: true,
|
||||
optimizePackageImports: [
|
||||
// Define remote patterns for images
|
||||
const remotePatterns = [
|
||||
{ protocol: "https", hostname: "cdn.fascinated.cc", pathname: "/**" },
|
||||
{ protocol: "https", hostname: "cdn.scoresaber.com", pathname: "/**" },
|
||||
{ protocol: "https", hostname: "cdn.jsdelivr.net", pathname: "/**" },
|
||||
{ protocol: "https", hostname: "eu.cdn.beatsaver.com", pathname: "/**" },
|
||||
{ protocol: "https", hostname: "na.cdn.beatsaver.com", pathname: "/**" },
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.akamai.steamstatic.com",
|
||||
pathname: "/**",
|
||||
},
|
||||
];
|
||||
|
||||
// Define optimized package imports
|
||||
const optimizePackageImports = [
|
||||
"react",
|
||||
"react-dom",
|
||||
"next-themes",
|
||||
@ -21,10 +27,19 @@ const nextConfig = {
|
||||
"react-chartjs-2",
|
||||
"country-list",
|
||||
"@sentry/nextjs",
|
||||
],
|
||||
},
|
||||
compress: false,
|
||||
];
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
generateEtags: true,
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
compress: true,
|
||||
poweredByHeader: false,
|
||||
experimental: {
|
||||
webpackBuildWorker: true,
|
||||
optimizePackageImports,
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_BUILD_ID:
|
||||
process.env.GIT_REV || nextBuildId.sync({ dir: __dirname }),
|
||||
@ -36,83 +51,7 @@ const nextConfig = {
|
||||
minute: "numeric",
|
||||
}),
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.fascinated.cc",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.scoresaber.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.jsdelivr.net",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "eu.cdn.beatsaver.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "na.cdn.beatsaver.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.akamai.steamstatic.com",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
images: { remotePatterns },
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(nextConfig);
|
||||
|
||||
// // Injected content via Sentry wizard below
|
||||
|
||||
// const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
// module.exports = withSentryConfig(
|
||||
// module.exports,
|
||||
// {
|
||||
// // For all available options, see:
|
||||
// // https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
// // Suppresses source map uploading logs during build
|
||||
// silent: true,
|
||||
// org: "sentry",
|
||||
// project: "scoresaber-reloaded",
|
||||
// url: "https://sentry.fascinated.cc/",
|
||||
// },
|
||||
// {
|
||||
// // For all available options, see:
|
||||
// // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// // Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
// widenClientFileUpload: false,
|
||||
|
||||
// // Transpiles SDK to be compatible with IE11 (increases bundle size)
|
||||
// transpileClientSDK: false,
|
||||
|
||||
// // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
// tunnelRoute: "/monitoring",
|
||||
|
||||
// // Hides source maps from generated client bundles
|
||||
// hideSourceMaps: true,
|
||||
|
||||
// // Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
// disableLogger: true,
|
||||
// },
|
||||
// );
|
||||
|
56
package.json
56
package.json
@ -11,8 +11,8 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@boiseitguru/cookie-cutter": "^0.2.1",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@boiseitguru/cookie-cutter": "^0.2.3",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
@ -21,45 +21,45 @@
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.74.1",
|
||||
"chart.js": "^4.4.0",
|
||||
"chart.js": "^4.4.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"clsx": "^2.1.0",
|
||||
"country-list": "^2.3.0",
|
||||
"critters": "^0.0.20",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"encoding": "^0.1.13",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next": "14.0.2",
|
||||
"infisical-node": "^1.5.1",
|
||||
"lucide-react": "^0.320.0",
|
||||
"next": "^14.1.0",
|
||||
"next-build-id": "^3.0.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^9.1.3",
|
||||
"redis": "^4.6.10",
|
||||
"sharp": "^0.32.6",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-toastify": "^10.0.4",
|
||||
"redis": "^4.6.12",
|
||||
"sharp": "^0.33.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"websocket": "^1.0.34",
|
||||
"zustand": "^4.4.3"
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^14.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/websocket": "^1.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"@next/bundle-analyzer": "^14.1.0",
|
||||
"@types/node": "^20.11.14",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.2",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5"
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
6198
pnpm-lock.yaml
generated
6198
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
@ -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,
|
||||
});
|
@ -1,87 +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 { 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 +
|
||||
`
|
||||
|
||||
Players currently online: ${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>
|
||||
);
|
||||
}
|
@ -1,39 +1,90 @@
|
||||
import { Redis } from "@/db/redis";
|
||||
import { Redis } from "@/lib/db/redis";
|
||||
import { BeatsaverMap } from "@/schemas/beatsaver/BeatsaverMap";
|
||||
import { BeatsaverAPI } from "@/utils/beatsaver/api";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mapHashes = searchParams.get("hashes")?.split(",") ?? undefined;
|
||||
const mapHashes = searchParams.get("hashes");
|
||||
|
||||
if (!mapHashes) {
|
||||
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";
|
||||
let totalInCache = 0;
|
||||
|
||||
const maps: Record<string, BeatsaverMap | { id: string }> = {};
|
||||
for (const mapHash of mapHashes) {
|
||||
|
||||
const fetchMapFromCache = async (mapHash: string) => {
|
||||
const cachedMap = await (
|
||||
await Redis.client
|
||||
).get(`beatsaver:map:${mapHash}`);
|
||||
if (cachedMap) {
|
||||
maps[mapHash] = JSON.parse(cachedMap);
|
||||
totalInCache++;
|
||||
} else {
|
||||
const map = await BeatsaverAPI.fetchMapByHash(mapHash);
|
||||
if (!map) {
|
||||
continue;
|
||||
return cachedMap;
|
||||
};
|
||||
|
||||
const fetchAndCacheMap = async (mapHash: string) => {
|
||||
const beatSaverMap = await BeatsaverAPI.fetchMapByHash(mapHash);
|
||||
|
||||
if (beatSaverMap) {
|
||||
addMap(mapHash, beatSaverMap);
|
||||
await cacheMap(mapHash, beatSaverMap);
|
||||
}
|
||||
maps[mapHash] = map;
|
||||
};
|
||||
|
||||
const cacheMap = async (mapHash: string, map: BeatsaverMap) => {
|
||||
await (
|
||||
await Redis.client
|
||||
).set("beatsaver:map:" + mapHash, JSON.stringify(map), {
|
||||
EX: 60 * 60 * 24 * 7, // 7 days
|
||||
});
|
||||
).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);
|
||||
if (map !== null) {
|
||||
const json = JSON.parse(map);
|
||||
addMap(mapHash, json);
|
||||
totalInCache++;
|
||||
}
|
||||
}
|
||||
|
||||
if (idOnly) {
|
||||
maps[mapHash] = { id: (maps[mapHash] as BeatsaverMap).id };
|
||||
if (totalInCache === 0 && toFetch.length > 1) {
|
||||
const beatSaverMaps = await BeatsaverAPI.fetchMapsByHash(...toFetch);
|
||||
if (beatSaverMaps) {
|
||||
for (const mapHash of toFetch) {
|
||||
const beatSaverMap = beatSaverMaps[mapHash.toLowerCase()];
|
||||
|
||||
if (beatSaverMap) {
|
||||
await cacheMap(mapHash, beatSaverMap);
|
||||
addMap(mapHash, beatSaverMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const mapHash of toFetch) {
|
||||
if (!maps[mapHash]) {
|
||||
await fetchAndCacheMap(mapHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ export const viewport: Viewport = {
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(ssrSettings.siteUrl),
|
||||
title: {
|
||||
template: ssrSettings.siteName + " - %s",
|
||||
template: ssrSettings.siteNameShort + " - %s",
|
||||
default: ssrSettings.siteName,
|
||||
},
|
||||
description: ssrSettings.description,
|
||||
|
@ -4,8 +4,8 @@ import Spinner from "@/components/Spinner";
|
||||
import PlayerStats from "@/components/overlay/PlayerStats";
|
||||
import ScoreStats from "@/components/overlay/ScoreStats";
|
||||
import SongInfo from "@/components/overlay/SongInfo";
|
||||
import { HttpSiraStatus } from "@/overlay/httpSiraStatus";
|
||||
import { OverlayPlayer } from "@/overlay/type/overlayPlayer";
|
||||
import { HttpSiraStatus } from "@/lib/overlay/httpSiraStatus";
|
||||
import { OverlayPlayer } from "@/lib/overlay/type/overlayPlayer";
|
||||
import { BeatLeaderAPI } from "@/utils/beatleader/api";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { Component } from "react";
|
||||
|
@ -69,12 +69,7 @@ export async function generateMetadata({
|
||||
*/
|
||||
async function getData(id: string, page: number, sort: string) {
|
||||
const playerData = await ScoreSaberAPI.fetchPlayerData(id);
|
||||
const playerScores = await ScoreSaberAPI.fetchScoresWithBeatsaverData(
|
||||
id,
|
||||
page,
|
||||
sort,
|
||||
10,
|
||||
);
|
||||
const playerScores = await ScoreSaberAPI.fetchScores(id, page, sort, 10);
|
||||
return {
|
||||
playerData: playerData,
|
||||
playerScores: playerScores,
|
||||
|
@ -14,7 +14,7 @@ export default function Card({
|
||||
}: CardProps) {
|
||||
return (
|
||||
<CardBase className={outerClassName}>
|
||||
<CardContent className={clsx(className, "pb-4 pt-2")}>
|
||||
<CardContent className={clsx(className, "p-3 pb-4 pt-2")}>
|
||||
{children}
|
||||
</CardContent>
|
||||
</CardBase>
|
||||
|
@ -22,7 +22,7 @@ const buildId = process.env.NEXT_PUBLIC_BUILD_ID
|
||||
const buildTime = process.env.NEXT_PUBLIC_BUILD_TIME;
|
||||
const gitUrl = isProduction()
|
||||
? `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() {
|
||||
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">
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
|
@ -57,7 +57,7 @@ export default function GlobalRanking({
|
||||
{players.map((player) => (
|
||||
<tr key={player.rank} className="border-b border-border">
|
||||
<PlayerRanking
|
||||
showCountryFlag={country ? false : true}
|
||||
isCountry={country == undefined ? false : true}
|
||||
player={player}
|
||||
/>
|
||||
</tr>
|
||||
|
@ -5,12 +5,15 @@ import { formatNumber } from "@/utils/numberUtils";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
export default function SearchPlayer() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [players, setPlayers] = useState([] as ScoresaberPlayer[]);
|
||||
const [players, setPlayers] = useState(
|
||||
undefined as ScoresaberPlayer[] | undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't search if the query is too short
|
||||
@ -28,14 +31,20 @@ export default function SearchPlayer() {
|
||||
if (id == undefined) return;
|
||||
|
||||
const player = await ScoreSaberAPI.fetchPlayerData(id);
|
||||
if (player == undefined) return;
|
||||
if (player == undefined) {
|
||||
setPlayers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPlayers([player]);
|
||||
}
|
||||
|
||||
// Search by name
|
||||
const players = await ScoreSaberAPI.searchByName(search);
|
||||
if (players == undefined) return;
|
||||
if (players == undefined) {
|
||||
setPlayers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPlayers(players);
|
||||
}
|
||||
@ -44,7 +53,7 @@ export default function SearchPlayer() {
|
||||
e.preventDefault();
|
||||
|
||||
// Take the user to the first account
|
||||
if (players.length > 0) {
|
||||
if (players && players.length > 0) {
|
||||
window.location.href = `/player/${players[0].id}/top/1`;
|
||||
}
|
||||
}
|
||||
@ -68,11 +77,12 @@ export default function SearchPlayer() {
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute z-20 mt-7 flex max-h-[200px] min-w-[14rem] flex-col divide-y overflow-y-auto rounded-md bg-popover shadow-sm md:max-h-[300px]",
|
||||
players.length > 0 ? "flex" : "hidden",
|
||||
players ? "flex" : "hidden",
|
||||
)}
|
||||
>
|
||||
{players.map((player: ScoresaberPlayer) => (
|
||||
<a
|
||||
{players && players.length > 0
|
||||
? players.map((player: ScoresaberPlayer) => (
|
||||
<Link
|
||||
key={player.id}
|
||||
className="flex min-w-[14rem] items-center gap-2 p-2 transition-all hover:bg-background"
|
||||
href={`/player/${player.id}/top/1`}
|
||||
@ -85,8 +95,9 @@ export default function SearchPlayer() {
|
||||
</p>
|
||||
<p className="text-sm">{player.name}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Link>
|
||||
))
|
||||
: search.length > 0 && <div className="p-2">No players found</div>}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
33
src/components/icons/YouTubeLogo.tsx
Normal file
33
src/components/icons/YouTubeLogo.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
type YouTubeLogoProps = {
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function YouTubeLogo({
|
||||
size = 32,
|
||||
className,
|
||||
}: YouTubeLogoProps) {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
width={size}
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 461.001 461.001"
|
||||
xmlSpace="preserve"
|
||||
className={className}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill="#F61C0D"
|
||||
d="M365.257,67.393H95.744C42.866,67.393,0,110.259,0,163.137v134.728
|
||||
c0,52.878,42.866,95.744,95.744,95.744h269.513c52.878,0,95.744-42.866,95.744-95.744V163.137
|
||||
C461.001,110.259,418.135,67.393,365.257,67.393z M300.506,237.056l-126.06,60.123c-3.359,1.602-7.239-0.847-7.239-4.568V168.607
|
||||
c0-3.774,3.982-6.22,7.348-4.514l126.06,63.881C304.363,229.873,304.298,235.248,300.506,237.056z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -98,7 +98,7 @@ export default function Leaderboard({ id, page }: LeaderboardProps) {
|
||||
<Container>
|
||||
<div className="mt-2 flex flex-col gap-2 xl:flex-row">
|
||||
<Card outerClassName="h-fit pt-3" className="flex">
|
||||
<div className="flex min-w-[320px] flex-wrap justify-between gap-2 md:justify-start">
|
||||
<div className="flex min-w-[300px] flex-wrap justify-between gap-2 md:justify-start">
|
||||
<div className="flex gap-2">
|
||||
<Image
|
||||
src={coverImage}
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
CogIcon,
|
||||
MagnifyingGlassIcon,
|
||||
ServerIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
|
||||
import { Card } from "../ui/card";
|
||||
import FriendsButton from "./FriendsButton";
|
||||
@ -29,12 +25,6 @@ export default function Navbar() {
|
||||
icon={<TvIcon height={23} width={23} />}
|
||||
href="/overlay/builder"
|
||||
/>
|
||||
<NavbarButton
|
||||
ariaLabel="View analytics for Scoresaber"
|
||||
text="Analytics"
|
||||
icon={<ServerIcon height={23} width={23} />}
|
||||
href="/analytics"
|
||||
/>
|
||||
|
||||
<div className="m-auto" />
|
||||
|
||||
@ -44,12 +34,12 @@ export default function Navbar() {
|
||||
icon={<MagnifyingGlassIcon height={23} width={23} />}
|
||||
href="/search"
|
||||
/>
|
||||
<NavbarButton
|
||||
{/* <NavbarButton
|
||||
ariaLabel="View your settings"
|
||||
text="Settings"
|
||||
icon={<CogIcon height={23} width={23} />}
|
||||
href="/settings"
|
||||
/>
|
||||
/> */}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OverlayPlayer } from "@/overlay/type/overlayPlayer";
|
||||
import { OverlayPlayer } from "@/lib/overlay/type/overlayPlayer";
|
||||
import { formatNumber } from "@/utils/numberUtils";
|
||||
import { GlobeAltIcon } from "@heroicons/react/20/solid";
|
||||
import Image from "next/image";
|
||||
|
@ -8,25 +8,26 @@ import CountyFlag from "../CountryFlag";
|
||||
|
||||
type PlayerRankingProps = {
|
||||
player: ScoresaberPlayer;
|
||||
showCountryFlag?: boolean;
|
||||
isCountry?: boolean;
|
||||
};
|
||||
|
||||
const Avatar = dynamic(() => import("@/components/Avatar"));
|
||||
|
||||
export default function PlayerRanking({
|
||||
player,
|
||||
showCountryFlag,
|
||||
isCountry,
|
||||
}: PlayerRankingProps) {
|
||||
const settingsStore = useStore(useSettingsStore, (store) => store);
|
||||
|
||||
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">
|
||||
<Avatar url={player.profilePicture} label="Avatar" size={24} />
|
||||
{showCountryFlag && (
|
||||
<CountyFlag countryCode={player.country} className="!h-5 !w-5" />
|
||||
)}
|
||||
<Link
|
||||
className="transform-gpu transition-all hover:text-blue-500"
|
||||
href={`/player/${player.id}/top/1`}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { ScoresaberScoreWithBeatsaverData } from "@/schemas/scoresaber/scoreWithBeatsaverData";
|
||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||
import { useSettingsStore } from "@/store/settingsStore";
|
||||
import { SortType, SortTypes } from "@/types/SortTypes";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
@ -16,11 +16,11 @@ type PageInfo = {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
sortType: SortType;
|
||||
scores: Record<string, ScoresaberScoreWithBeatsaverData>;
|
||||
scores: ScoresaberPlayerScore[] | undefined;
|
||||
};
|
||||
|
||||
type ScoresProps = {
|
||||
initalScores: Record<string, ScoresaberScoreWithBeatsaverData> | undefined;
|
||||
initalScores: ScoresaberPlayerScore[] | 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,
|
||||
});
|
||||
const [changedPage, setChangedPage] = useState(false);
|
||||
|
||||
@ -61,12 +61,8 @@ export default function Scores({
|
||||
return;
|
||||
}
|
||||
|
||||
ScoreSaberAPI.fetchScoresWithBeatsaverData(
|
||||
playerId,
|
||||
page,
|
||||
sortType.value,
|
||||
10,
|
||||
).then((scoresResponse) => {
|
||||
ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then(
|
||||
(scoresResponse) => {
|
||||
if (!scoresResponse) {
|
||||
setError(true);
|
||||
setErrorMessage("No Scores");
|
||||
@ -89,7 +85,8 @@ export default function Scores({
|
||||
setChangedPage(true);
|
||||
|
||||
console.log(`Switched page to ${page} with sort ${sortType.value}`);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
[
|
||||
changedPage,
|
||||
@ -150,10 +147,12 @@ export default function Scores({
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<>
|
||||
{scores.scores ? (
|
||||
<>
|
||||
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
|
||||
{Object.values(scores.scores).map((scoreData, id) => {
|
||||
const { score, leaderboard, mapId } = scoreData;
|
||||
{scores.scores.map((scoreData, id) => {
|
||||
const { score, leaderboard } = scoreData;
|
||||
|
||||
return (
|
||||
<Score
|
||||
@ -161,8 +160,6 @@ export default function Scores({
|
||||
player={playerData}
|
||||
score={score}
|
||||
leaderboard={leaderboard}
|
||||
mapId={mapId}
|
||||
ownProfile={settingsStore?.player}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -178,6 +175,10 @@ export default function Scores({
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>No Scores!</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
98
src/components/player/score/MapButtons.tsx
Normal file
98
src/components/player/score/MapButtons.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -17,28 +17,19 @@ import {
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import BeatSaverLogo from "../../icons/BeatSaverLogo";
|
||||
import { Suspense } from "react";
|
||||
import HeadsetIcon from "../../icons/HeadsetIcon";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/Tooltip";
|
||||
import { Button } from "../../ui/button";
|
||||
import ScoreStatLabel from "../ScoreStatLabel";
|
||||
import CopyBsrButton from "./CopyBsrButton";
|
||||
import MapButtons from "./MapButtons";
|
||||
|
||||
type ScoreProps = {
|
||||
score: ScoresaberScore;
|
||||
player: ScoresaberPlayer;
|
||||
leaderboard: ScoresaberLeaderboardInfo;
|
||||
ownProfile?: ScoresaberPlayer;
|
||||
mapId?: string;
|
||||
};
|
||||
|
||||
export default function Score({
|
||||
score,
|
||||
player,
|
||||
leaderboard,
|
||||
ownProfile,
|
||||
mapId,
|
||||
}: ScoreProps) {
|
||||
export default function Score({ score, player, leaderboard }: ScoreProps) {
|
||||
const isFullCombo = score.missedNotes + score.badCuts === 0;
|
||||
const diffName = scoresaberDifficultyNumberToName(
|
||||
leaderboard.difficulty.difficulty,
|
||||
@ -122,32 +113,9 @@ 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}`}
|
||||
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>
|
||||
<Suspense fallback={<div />}>
|
||||
<MapButtons leaderboard={leaderboard} />
|
||||
</Suspense>
|
||||
|
||||
<div className="flex items-center justify-between p-1 md:items-start md:justify-end">
|
||||
<div className="flex flex-col md:hidden">
|
||||
|
@ -3,7 +3,7 @@ import { createClient } from "redis";
|
||||
let redisClient = await connectRedis();
|
||||
|
||||
async function connectRedis(): Promise<any> {
|
||||
console.log("Connecting to redis");
|
||||
// console.log("Connecting to redis");
|
||||
const client = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
@ -20,8 +20,6 @@ async function connectRedis(): Promise<any> {
|
||||
return client;
|
||||
}
|
||||
|
||||
// todo: add disconnect handler
|
||||
|
||||
export const Redis = {
|
||||
client: redisClient,
|
||||
connectRedis,
|
@ -16,6 +16,7 @@ async function loadIntoSong(data: any) {
|
||||
beatsaverMapData.versions[beatsaverMapData.versions.length - 1].coverURL;
|
||||
|
||||
overlayDataStore.setState({
|
||||
paused: false,
|
||||
scoreStats: {
|
||||
accuracy: performance.relativeScore * 100,
|
||||
score: performance.rawScore,
|
||||
@ -33,6 +34,16 @@ async function loadIntoSong(data: any) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the state of the overlay
|
||||
*/
|
||||
function resetState() {
|
||||
overlayDataStore.setState({
|
||||
scoreStats: undefined,
|
||||
songInfo: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
type Handlers = {
|
||||
[key: string]: (data: any) => void;
|
||||
};
|
||||
@ -64,16 +75,10 @@ const handlers: Handlers = {
|
||||
|
||||
// Left the song
|
||||
finished: (data: any) => {
|
||||
overlayDataStore.setState({
|
||||
scoreStats: undefined,
|
||||
songInfo: undefined,
|
||||
});
|
||||
resetState();
|
||||
},
|
||||
menu: (data: any) => {
|
||||
overlayDataStore.setState({
|
||||
scoreStats: undefined,
|
||||
songInfo: undefined,
|
||||
});
|
||||
resetState();
|
||||
},
|
||||
|
||||
// pause & resume
|
||||
@ -106,6 +111,7 @@ function connectWebSocket() {
|
||||
console.log(
|
||||
"Lost connection to HttpSiraStatus, reconnecting in 5 seconds...",
|
||||
);
|
||||
resetState();
|
||||
setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, 5000); // 5 seconds
|
8
src/secrets.ts
Normal file
8
src/secrets.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import InfisicalClient from "infisical-node";
|
||||
|
||||
const infisicalClient = new InfisicalClient({
|
||||
token: process.env.INFISICAL_TOKEN,
|
||||
siteURL: "https://secrets.fascinated.cc",
|
||||
});
|
||||
|
||||
export default infisicalClient;
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"siteUrl": "https://ssr.fascinated.cc",
|
||||
"proxy": "https://proxy.fascinated.cc"
|
||||
|
@ -9,15 +9,34 @@ if (typeof window !== "undefined") {
|
||||
}
|
||||
|
||||
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> => {
|
||||
//console.log(name, "has been retrieved");
|
||||
|
||||
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> => {
|
||||
//console.log(name, "with value", value, "has been saved");
|
||||
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> => {
|
||||
//console.log(name, "has been deleted");
|
||||
await del(name, storage);
|
||||
|
@ -11,16 +11,21 @@ const BS_API_URL = ssrSettings.proxy + "/https://api.beatsaver.com";
|
||||
export const BS_GET_MAP_BY_HASH_URL = BS_API_URL + "/maps/hash/{}";
|
||||
|
||||
/**
|
||||
* Returns the map info for the provided hash
|
||||
* Returns the map info for the provided hashes
|
||||
*
|
||||
* @param hash the hash of the map
|
||||
* @param hash the hashes for the maps
|
||||
* @returns the map info
|
||||
*/
|
||||
async function fetchMapByHash(
|
||||
hash: string,
|
||||
): Promise<BeatsaverMap | undefined | null> {
|
||||
async function fetchMapsByHash(
|
||||
...hash: string[]
|
||||
): Promise<{ [key: string]: BeatsaverMap } | undefined> {
|
||||
const hashes = hash.join(",");
|
||||
const response = await BeatsaverFetchQueue.fetch(
|
||||
formatString(BS_GET_MAP_BY_HASH_URL, true, hash),
|
||||
formatString(
|
||||
BS_GET_MAP_BY_HASH_URL,
|
||||
true,
|
||||
hashes.substring(0, hashes.length - 1),
|
||||
),
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
@ -29,9 +34,31 @@ async function fetchMapByHash(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return json as BeatsaverMap;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the map info for the provided hash
|
||||
*
|
||||
* @param hash the hash of the map
|
||||
* @returns the map info
|
||||
*/
|
||||
async function fetchMapByHash(hash: string): Promise<BeatsaverMap | undefined> {
|
||||
const response = await BeatsaverFetchQueue.fetch(
|
||||
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();
|
||||
|
||||
// Check if there was an error fetching the user data
|
||||
if (json.error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export const BeatsaverAPI = {
|
||||
fetchMapsByHash,
|
||||
fetchMapByHash,
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ export class FetchQueue {
|
||||
* @param url - The URL to fetch.
|
||||
* @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();
|
||||
|
||||
if (now < this.rateLimitReset) {
|
||||
|
@ -178,6 +178,8 @@ async function fetchScoresWithBeatsaverData(
|
||||
for (const score of scores) {
|
||||
url += `${score.leaderboard.songHash},`;
|
||||
}
|
||||
url = url.substring(0, url.length - 1);
|
||||
url += "&idonly=true";
|
||||
const mapResponse = await fetch(url, {
|
||||
next: {
|
||||
revalidate: 60 * 60 * 24 * 7, // 1 week
|
||||
|
@ -48,3 +48,30 @@ export function scoresaberDifficultyNumberToName(
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a song name and author into a YouTube link
|
||||
*
|
||||
* @param name the name of the song
|
||||
* @param songSubName the sub name of the song
|
||||
* @param author the author of the song
|
||||
* @returns the YouTube link for the song
|
||||
*/
|
||||
export function songNameToYouTubeLink(
|
||||
name: string,
|
||||
songSubName: string,
|
||||
author: string,
|
||||
) {
|
||||
const baseUrl = "https://www.youtube.com/results?search_query=";
|
||||
let query = "";
|
||||
if (name) {
|
||||
query += `${name} `;
|
||||
}
|
||||
if (songSubName) {
|
||||
query += `${songSubName} `;
|
||||
}
|
||||
if (author) {
|
||||
query += `${author} `;
|
||||
}
|
||||
return encodeURI(baseUrl + query.trim().replaceAll(" ", "+"));
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export function formatTimeAgo(timestamp: string) {
|
||||
* @param timestamp the timestamp to format
|
||||
* @returns the formatted timestamp
|
||||
*/
|
||||
export function formatDate(timestamp: string) {
|
||||
export function formatDate(timestamp: any) {
|
||||
const date = parseISO(timestamp);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
|
@ -23,6 +23,12 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"src/secrets.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user