Compare commits
1 Commits
renovate/n
...
renovate/l
Author | SHA1 | Date | |
---|---|---|---|
59699f05f8 |
@ -56,10 +56,15 @@ export async function initDiscordBot() {
|
||||
export async function logToChannel(channelId: DiscordChannels, message: EmbedBuilder) {
|
||||
try {
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
if (channel != undefined && channel.isSendable()) {
|
||||
channel.send({ embeds: [message] });
|
||||
if (channel == undefined) {
|
||||
throw new Error(`Channel "${channelId}" not found`);
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
if (!channel.isSendable()) {
|
||||
throw new Error(`Channel "${channelId}" is not sendable`);
|
||||
}
|
||||
|
||||
channel.send({ embeds: [message] });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export const app = new Elysia();
|
||||
app.use(
|
||||
cron({
|
||||
name: "player-statistics-tracker-cron",
|
||||
pattern: "0 1 * * *", // Every day at 00:01
|
||||
pattern: "0 1 * * * *", // Every day at 00:01
|
||||
timezone: "Europe/London", // UTC time
|
||||
protect: true,
|
||||
run: async () => {
|
||||
|
@ -488,8 +488,6 @@ export class ScoreService {
|
||||
const score = getScoreSaberScoreFromToken(token.score, leaderboard, playerId);
|
||||
if (!score) return undefined;
|
||||
|
||||
console.log("boobs");
|
||||
|
||||
// Fetch additional data, previous score, and BeatSaver map concurrently
|
||||
const [additionalData, previousScore, beatSaverMap] = await Promise.all([
|
||||
this.getAdditionalScoreData(
|
||||
@ -681,9 +679,7 @@ export class ScoreService {
|
||||
leaderboardId: string,
|
||||
timestamp: Date
|
||||
): Promise<ScoreSaberPreviousScore | undefined> {
|
||||
const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId }).sort({
|
||||
timestamp: -1,
|
||||
});
|
||||
const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId });
|
||||
if (scores == null || scores.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
@ -693,7 +689,7 @@ export class ScoreService {
|
||||
if (scoreIndex == -1 || score == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const previousScore = scores[scoreIndex + 1];
|
||||
const previousScore = scores[scoreIndex - 1];
|
||||
if (previousScore == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
@ -708,7 +704,6 @@ export class ScoreService {
|
||||
pp: previousScore.pp,
|
||||
weight: previousScore.weight,
|
||||
maxCombo: previousScore.maxCombo,
|
||||
timestamp: previousScore.timestamp,
|
||||
change: {
|
||||
score: score.score - previousScore.score,
|
||||
accuracy: score.accuracy - previousScore.accuracy,
|
||||
|
@ -11,6 +11,5 @@
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"jsx": "react",
|
||||
"incremental": true
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -35,9 +35,4 @@ export type PreviousScore = {
|
||||
* The full combo of the previous score.
|
||||
*/
|
||||
fullCombo?: boolean;
|
||||
|
||||
/**
|
||||
* When the previous score was set.
|
||||
*/
|
||||
timestamp: Date;
|
||||
};
|
||||
|
@ -14,8 +14,6 @@ WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
ARG GIT_REV
|
||||
ENV GIT_REV=${GIT_REV}
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
|
||||
# Copy the depends
|
||||
COPY --from=depends /app/package.json* /app/bun.lockb* ./
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
import { format } from "@formkit/tempo";
|
||||
import nextBundleAnalyzer from "@next/bundle-analyzer";
|
||||
import type { NextConfig } from "next";
|
||||
import { isProduction } from "@/common/website-utils";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
@ -38,22 +36,22 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const withBundleAnalyzer = nextBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: "scoresaber-reloaded",
|
||||
project: "frontend",
|
||||
sentryUrl: "https://glitchtip.fascinated.cc/",
|
||||
silent: !process.env.CI,
|
||||
reactComponentAnnotation: {
|
||||
enabled: true,
|
||||
},
|
||||
tunnelRoute: "/monitoring",
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
sourcemaps: {
|
||||
disable: true,
|
||||
},
|
||||
release: {
|
||||
create: false,
|
||||
finalize: false,
|
||||
},
|
||||
});
|
||||
|
||||
const config = withBundleAnalyzer(nextConfig);
|
||||
|
||||
export default isProduction()
|
||||
? withSentryConfig(config, {
|
||||
org: "fascinatedcc",
|
||||
project: "scoresaber-reloaded",
|
||||
silent: !process.env.CI,
|
||||
widenClientFileUpload: true,
|
||||
reactComponentAnnotation: {
|
||||
enabled: true,
|
||||
},
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
})
|
||||
: config;
|
||||
|
@ -6,7 +6,6 @@
|
||||
"dev": "next dev --turbo",
|
||||
"dev-debug": "cross-env NODE_OPTIONS='--inspect' next dev --turbo",
|
||||
"build": "next build",
|
||||
"build:analyze": "cross-env ANALYZE=true next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
@ -14,7 +13,6 @@
|
||||
"@formkit/tempo": "^0.1.2",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@next/bundle-analyzer": "^15.0.1",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
@ -24,7 +22,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@sentry/nextjs": "8",
|
||||
"@sentry/nextjs": "8.35.0",
|
||||
"@ssr/common": "workspace:*",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
@ -32,13 +30,12 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"comlink": "^4.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"framer-motion": "^11.5.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"ky": "^1.7.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "^15.0.1",
|
||||
"next-build-id": "^3.0.0",
|
||||
"next-themes": "^0.3.0",
|
||||
@ -55,7 +52,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
|
@ -5,24 +5,8 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://2b0d6c2e72099dee7db2ce9c030651bd@o4508202509205504.ingest.de.sentry.io/4508202511302736",
|
||||
|
||||
// Add optional integrations for additional features
|
||||
integrations: [
|
||||
Sentry.replayIntegration(),
|
||||
],
|
||||
|
||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Define how likely Replay events are sampled.
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// Define how likely Replay events are sampled when an error occurs.
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
dsn: "https://69aed8b4a32e45db8fcb1b4285b4370f@glitchtip.fascinated.cc/13",
|
||||
tracesSampleRate: 0.1,
|
||||
debug: false,
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
});
|
||||
|
@ -6,11 +6,8 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://2b0d6c2e72099dee7db2ce9c030651bd@o4508202509205504.ingest.de.sentry.io/4508202511302736",
|
||||
|
||||
// Define how likely traces are sampled. 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.
|
||||
dsn: "https://69aed8b4a32e45db8fcb1b4285b4370f@glitchtip.fascinated.cc/13",
|
||||
tracesSampleRate: 0.1,
|
||||
debug: false,
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
});
|
||||
|
@ -5,11 +5,8 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://2b0d6c2e72099dee7db2ce9c030651bd@o4508202509205504.ingest.de.sentry.io/4508202511302736",
|
||||
|
||||
// Define how likely traces are sampled. 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.
|
||||
dsn: "https://69aed8b4a32e45db8fcb1b4285b4370f@glitchtip.fascinated.cc/13",
|
||||
tracesSampleRate: 0.1,
|
||||
debug: false,
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
});
|
||||
|
@ -56,7 +56,6 @@ export default function PlayerScoreAccuracyChart({ scoreStats, leaderboard }: Pr
|
||||
axisConfig: {
|
||||
reverse: false,
|
||||
display: true,
|
||||
hideOnMobile: true,
|
||||
displayName: "PP",
|
||||
position: "right",
|
||||
},
|
||||
|
@ -21,7 +21,7 @@ export function HandAccuracyBadge({ score, hand }: HandAccuracyProps) {
|
||||
const formattedHand = capitalizeFirstLetter(hand);
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Tooltip
|
||||
display={
|
||||
<>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
|
||||
import { PauseIcon } from "@heroicons/react/24/solid";
|
||||
import { ScoreBadgeProps } from "@/components/score/badges/badge-props";
|
||||
import { ScoreMissesTooltip } from "@/components/score/score-misses-tooltip";
|
||||
import { Misses } from "@ssr/common/model/additional-score-data/misses";
|
||||
import Tooltip from "@/components/tooltip";
|
||||
|
||||
type ScoreMissesBadgeProps = ScoreBadgeProps & {
|
||||
/**
|
||||
@ -32,82 +34,69 @@ export default function ScoreMissesAndPausesBadge({ score, hideXMark }: ScoreMis
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={score.missedNotes}
|
||||
badCuts={score.badCuts}
|
||||
bombCuts={misses?.bombCuts}
|
||||
wallsHit={misses?.wallsHit}
|
||||
fullCombo={score.fullCombo}
|
||||
>
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={score.missedNotes}
|
||||
badCuts={score.badCuts}
|
||||
bombCuts={misses?.bombCuts}
|
||||
wallsHit={misses?.wallsHit}
|
||||
fullCombo={score.fullCombo}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
{score.fullCombo ? <span className="text-green-400">FC</span> : formatNumberWithCommas(score.misses)}
|
||||
{!hideXMark && !score.fullCombo && <span>x</span>}
|
||||
</p>
|
||||
</ScoreMissesTooltip>
|
||||
{additionalData && previousScoreMisses && scoreImprovement && misses && isMissImprovement && (
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={previousScoreMisses.missedNotes}
|
||||
badCuts={previousScoreMisses.badCuts}
|
||||
bombCuts={previousScoreMisses.bombCuts}
|
||||
wallsHit={previousScoreMisses.wallsHit}
|
||||
fullCombo={previousScoreFc}
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
{additionalData && !!pauses && pauses > 0 && (
|
||||
<>
|
||||
<p>|</p>
|
||||
<Tooltip
|
||||
display={
|
||||
<p>
|
||||
{pauses}x Pause{pauses > 1 ? "s" : ""}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="text-xs flex flex-row gap-1">
|
||||
<p>(vs {previousScoreFc ? "FC" : formatNumberWithCommas(previousScoreMisses.misses)}x)</p>
|
||||
<div className="flex gap-1 items-center">
|
||||
<p>{pauses && pauses}</p>
|
||||
<PauseIcon className="w-4 h-4" />
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
)}
|
||||
</div>
|
||||
{/*{additionalData && !!pauses && pauses > 0 && (*/}
|
||||
{/* <>*/}
|
||||
{/* <p>|</p>*/}
|
||||
{/* <Tooltip*/}
|
||||
{/* display={*/}
|
||||
{/* <p>*/}
|
||||
{/* {pauses}x Pause{pauses > 1 ? "s" : ""}*/}
|
||||
{/* </p>*/}
|
||||
{/* }*/}
|
||||
{/* >*/}
|
||||
{/* <div className="flex gap-1 items-center">*/}
|
||||
{/* <p>{pauses && pauses}</p>*/}
|
||||
{/* <PauseIcon className="w-4 h-4" />*/}
|
||||
{/* </div>*/}
|
||||
{/* </Tooltip>*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/*{additionalData && previousScoreMisses && scoreImprovement && misses && isMissImprovement && (*/}
|
||||
{/* <div className="flex gap-2 items-center justify-center">*/}
|
||||
{/* <ScoreMissesTooltip*/}
|
||||
{/* missedNotes={previousScoreMisses.missedNotes}*/}
|
||||
{/* badCuts={previousScoreMisses.badCuts}*/}
|
||||
{/* bombCuts={previousScoreMisses.bombCuts}*/}
|
||||
{/* wallsHit={previousScoreMisses.wallsHit}*/}
|
||||
{/* fullCombo={previousScoreFc}*/}
|
||||
{/* >*/}
|
||||
{/* <div className="flex gap-1 items-center text-xs">*/}
|
||||
{/* {previousScoreFc ? (*/}
|
||||
{/* <p className="text-green-400">FC</p>*/}
|
||||
{/* ) : (*/}
|
||||
{/* formatNumberWithCommas(previousScoreMisses.misses)*/}
|
||||
{/* )}*/}
|
||||
{/* </div>*/}
|
||||
{/* </ScoreMissesTooltip>*/}
|
||||
{/* <p>-></p>*/}
|
||||
{/* <ScoreMissesTooltip*/}
|
||||
{/* missedNotes={misses.missedNotes}*/}
|
||||
{/* badCuts={misses.badCuts}*/}
|
||||
{/* bombCuts={misses.bombCuts}*/}
|
||||
{/* wallsHit={misses.wallsHit}*/}
|
||||
{/* fullCombo={additionalData.fullCombo}*/}
|
||||
{/* >*/}
|
||||
{/* <div className="flex gap-1 items-center text-xs">*/}
|
||||
{/* {additionalData.fullCombo ? <p className="text-green-400">FC</p> : formatNumberWithCommas(misses.misses)}*/}
|
||||
{/* </div>*/}
|
||||
{/* </ScoreMissesTooltip>*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{additionalData && previousScoreMisses && scoreImprovement && misses && isMissImprovement && (
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={previousScoreMisses.missedNotes}
|
||||
badCuts={previousScoreMisses.badCuts}
|
||||
bombCuts={previousScoreMisses.bombCuts}
|
||||
wallsHit={previousScoreMisses.wallsHit}
|
||||
fullCombo={previousScoreFc}
|
||||
>
|
||||
<div className="flex gap-1 items-center text-xs">
|
||||
{previousScoreFc ? (
|
||||
<p className="text-green-400">FC</p>
|
||||
) : (
|
||||
formatNumberWithCommas(previousScoreMisses.misses)
|
||||
)}
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
<p>-></p>
|
||||
<ScoreMissesTooltip
|
||||
missedNotes={misses.missedNotes}
|
||||
badCuts={misses.badCuts}
|
||||
bombCuts={misses.bombCuts}
|
||||
wallsHit={misses.wallsHit}
|
||||
fullCombo={additionalData.fullCombo}
|
||||
>
|
||||
<div className="flex gap-1 items-center text-xs">
|
||||
{additionalData.fullCombo ? <p className="text-green-400">FC</p> : formatNumberWithCommas(misses.misses)}
|
||||
</div>
|
||||
</ScoreMissesTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import BeatSaverLogo from "@/components/logos/beatsaver-logo";
|
||||
import ScoreButton from "@/components/score/button/score-button";
|
||||
import * as React from "react";
|
||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||
|
||||
type BeatSaverMapProps = {
|
||||
beatSaverMap: BeatSaverMap;
|
||||
};
|
||||
|
||||
export function BeatSaverMapButton({ beatSaverMap }: BeatSaverMapProps) {
|
||||
return (
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(`https://beatsaver.com/maps/${beatSaverMap.bsr}`, "_blank");
|
||||
}}
|
||||
tooltip={<p>Click to open the map</p>}
|
||||
>
|
||||
<BeatSaverLogo />
|
||||
</ScoreButton>
|
||||
);
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { copyToClipboard } from "@/common/browser-utils";
|
||||
import ScoreButton from "@/components/score/button/score-button";
|
||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||
import * as React from "react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
type ScoreBsrButtonProps = {
|
||||
beatSaverMap: BeatSaverMap;
|
||||
};
|
||||
|
||||
export function ScoreBsrButton({ beatSaverMap }: ScoreBsrButtonProps) {
|
||||
return (
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: `Copied "!bsr ${beatSaverMap.bsr}" to your clipboard!`,
|
||||
});
|
||||
copyToClipboard(`!bsr ${beatSaverMap.bsr}`);
|
||||
}}
|
||||
tooltip={<p>Click to copy the bsr code</p>}
|
||||
>
|
||||
<p>!</p>
|
||||
</ScoreButton>
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import BeatSaberPepeLogo from "@/components/logos/beatsaber-pepe-logo";
|
||||
import ScoreButton from "@/components/score/button/score-button";
|
||||
import * as React from "react";
|
||||
import { AdditionalScoreData } from "@ssr/common/model/additional-score-data/additional-score-data";
|
||||
|
||||
type ScoreReplayButton = {
|
||||
additionalData: AdditionalScoreData;
|
||||
};
|
||||
|
||||
export function ScoreReplayButton({ additionalData }: ScoreReplayButton) {
|
||||
return (
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(`https://replay.beatleader.xyz/?scoreId=${additionalData.scoreId}`, "_blank");
|
||||
}}
|
||||
tooltip={<p>Click to view the score replay!</p>}
|
||||
>
|
||||
<BeatSaberPepeLogo />
|
||||
</ScoreButton>
|
||||
);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { songNameToYouTubeLink } from "@/common/youtube-utils";
|
||||
import YouTubeLogo from "@/components/logos/youtube-logo";
|
||||
import ScoreButton from "@/components/score/button/score-button";
|
||||
import { ScoreSaberLeaderboard } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import * as React from "react";
|
||||
|
||||
type SongOpenInYoutubeButtonProps = {
|
||||
leaderboard: ScoreSaberLeaderboard;
|
||||
};
|
||||
|
||||
export function SongOpenInYoutubeButton({ leaderboard }: SongOpenInYoutubeButtonProps) {
|
||||
return (
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(
|
||||
songNameToYouTubeLink(leaderboard.songName, leaderboard.songSubName, leaderboard.songAuthorName),
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
tooltip={<p>Click to open the song in YouTube</p>}
|
||||
>
|
||||
<YouTubeLogo />
|
||||
</ScoreButton>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import StatValue from "@/components/stat-value";
|
||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import clsx from "clsx";
|
||||
|
||||
/**
|
||||
* A badge to display in the score stats.
|
||||
@ -27,19 +27,6 @@ export function ScoreBadges({ badges, score, leaderboard }: ScoreBadgeProps) {
|
||||
if (toRender === undefined) {
|
||||
return <div key={index} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
"flex h-fit p-1 items-center justify-center rounded-md text-sm cursor-default",
|
||||
color ? color : "bg-accent"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: (!color?.includes("bg") && color) || undefined,
|
||||
}}
|
||||
>
|
||||
{toRender}
|
||||
</div>
|
||||
);
|
||||
return <StatValue key={index} color={color} value={toRender} />;
|
||||
});
|
||||
}
|
||||
|
@ -1,17 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { songNameToYouTubeLink } from "@/common/youtube-utils";
|
||||
import BeatSaverLogo from "@/components/logos/beatsaver-logo";
|
||||
import YouTubeLogo from "@/components/logos/youtube-logo";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import ScoreButton from "./score-button";
|
||||
import { copyToClipboard } from "@/common/browser-utils";
|
||||
import { ArrowDownIcon, ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import ScoreEditorButton from "@/components/score/button/score-editor-button";
|
||||
import { ScoreBsrButton } from "@/components/score/button/score-bsr-button";
|
||||
import { BeatSaverMapButton } from "@/components/score/button/beat-saver-map-button";
|
||||
import { SongOpenInYoutubeButton } from "@/components/score/button/song-open-in-youtube-button";
|
||||
import ScoreEditorButton from "@/components/score/score-editor-button";
|
||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||
import { ScoreSaberLeaderboard } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
|
||||
import { ScoreReplayButton } from "@/components/score/button/score-replay-button";
|
||||
import BeatSaberPepeLogo from "@/components/logos/beatsaber-pepe-logo";
|
||||
|
||||
type Props = {
|
||||
score?: ScoreSaberScore;
|
||||
@ -30,39 +33,78 @@ export default function ScoreButtons({
|
||||
leaderboard,
|
||||
beatSaverMap,
|
||||
alwaysSingleLine,
|
||||
setIsLeaderboardExpanded,
|
||||
isLeaderboardLoading,
|
||||
updateScore,
|
||||
hideLeaderboardDropdown,
|
||||
hideAccuracyChanger,
|
||||
isLeaderboardLoading,
|
||||
setIsLeaderboardExpanded,
|
||||
updateScore,
|
||||
}: Props) {
|
||||
const [leaderboardExpanded, setLeaderboardExpanded] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const additionalData = score?.additionalData;
|
||||
return (
|
||||
<div className={`flex justify-end gap-2 items-center mr-1`}>
|
||||
<div className={`flex lg:grid grid-cols-3 gap-1 items-center justify-center min-w-[92px]`}>
|
||||
<div className={`flex justify-end gap-2 items-center`}>
|
||||
<div
|
||||
className={`flex ${alwaysSingleLine ? "flex-nowrap" : "flex-wrap"} items-center lg:items-start justify-center lg:justify-end gap-1`}
|
||||
>
|
||||
{beatSaverMap != undefined && (
|
||||
<>
|
||||
<ScoreBsrButton beatSaverMap={beatSaverMap} />
|
||||
<BeatSaverMapButton beatSaverMap={beatSaverMap} />
|
||||
{/* Copy BSR */}
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: `Copied "!bsr ${beatSaverMap.bsr}" to your clipboard!`,
|
||||
});
|
||||
copyToClipboard(`!bsr ${beatSaverMap.bsr}`);
|
||||
}}
|
||||
tooltip={<p>Click to copy the bsr code</p>}
|
||||
>
|
||||
<p>!</p>
|
||||
</ScoreButton>
|
||||
|
||||
{/* Open map in BeatSaver */}
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(`https://beatsaver.com/maps/${beatSaverMap.bsr}`, "_blank");
|
||||
}}
|
||||
tooltip={<p>Click to open the map</p>}
|
||||
>
|
||||
<BeatSaverLogo />
|
||||
</ScoreButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SongOpenInYoutubeButton leaderboard={leaderboard} />
|
||||
|
||||
<div className="hidden lg:block" />
|
||||
<div className="hidden lg:block" />
|
||||
{/* Open song in YouTube */}
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(
|
||||
songNameToYouTubeLink(leaderboard.songName, leaderboard.songSubName, leaderboard.songAuthorName),
|
||||
"_blank"
|
||||
);
|
||||
}}
|
||||
tooltip={<p>Click to open the song in YouTube</p>}
|
||||
>
|
||||
<YouTubeLogo />
|
||||
</ScoreButton>
|
||||
|
||||
{additionalData != undefined && (
|
||||
<>
|
||||
<ScoreReplayButton additionalData={additionalData} />
|
||||
{/* Open score replay */}
|
||||
<ScoreButton
|
||||
onClick={() => {
|
||||
window.open(`https://replay.beatleader.xyz/?scoreId=${additionalData.scoreId}`, "_blank");
|
||||
}}
|
||||
tooltip={<p>Click to view the score replay!</p>}
|
||||
>
|
||||
<BeatSaberPepeLogo />
|
||||
</ScoreButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex gap-2 ${alwaysSingleLine ? "flex-row" : "flex-row lg:flex-col"} items-center justify-center pr-1`}
|
||||
className={`flex gap-2 ${alwaysSingleLine ? "flex-row" : "flex-row lg:flex-col"} items-center justify-center`}
|
||||
>
|
||||
{/* Edit score button */}
|
||||
{score && leaderboard && updateScore && !hideAccuracyChanger && (
|
||||
|
@ -6,8 +6,8 @@ import { Slider } from "@/components/ui/slider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ResetIcon } from "@radix-ui/react-icons";
|
||||
import Tooltip from "@/components/tooltip";
|
||||
import { ScoreSaberLeaderboard } from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
|
||||
type ScoreEditorButtonProps = {
|
||||
score: ScoreSaberScore;
|
@ -5,14 +5,13 @@ import { getPageFromRank } from "@ssr/common/utils/utils";
|
||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
|
||||
import { ScoreTimeSet } from "@/components/score/score-time-set";
|
||||
import { ScoreTimeSetVs } from "@/components/score/score-time-set-vs";
|
||||
|
||||
type Props = {
|
||||
score: ScoreSaberScore;
|
||||
leaderboard: ScoreSaberLeaderboard;
|
||||
};
|
||||
|
||||
export default function ScoreRankAndDateInfo({ score, leaderboard }: Props) {
|
||||
export default function ScoreRankInfo({ score, leaderboard }: Props) {
|
||||
return (
|
||||
<div className="flex w-full flex-row justify-between lg:w-[125px] lg:flex-col lg:justify-center items-center">
|
||||
<div className="flex gap-1 items-center">
|
||||
@ -23,10 +22,7 @@ export default function ScoreRankAndDateInfo({ score, leaderboard }: Props) {
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 lg:flex-col lg:gap-0">
|
||||
<ScoreTimeSet score={score} />
|
||||
<ScoreTimeSetVs score={score} />
|
||||
</div>
|
||||
<ScoreTimeSet score={score} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -56,20 +56,18 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="overflow-y-clip flex flex-col gap-1">
|
||||
<div className="overflow-y-clip">
|
||||
<Link
|
||||
href={`/leaderboard/${leaderboard.id}`}
|
||||
className="cursor-pointer select-none hover:brightness-[66%] transform-gpu transition-all text-ssr w-fit"
|
||||
>
|
||||
{leaderboard.songName} {leaderboard.songSubName}
|
||||
</Link>
|
||||
<div className="flex flex-row text-sm gap-1.5 items-end leading-none">
|
||||
<div className="flex flex-col text-sm">
|
||||
<p className="text-gray-400">{leaderboard.songAuthorName}</p>
|
||||
<FallbackLink
|
||||
href={mappersProfile}
|
||||
className={
|
||||
mappersProfile && "hover:brightness-[66%] transform-gpu transition-all w-fit text-xs leading-none"
|
||||
}
|
||||
className={mappersProfile && "hover:brightness-[66%] transform-gpu transition-all w-fit"}
|
||||
>
|
||||
{leaderboard.levelAuthorName}
|
||||
</FallbackLink>
|
||||
|
@ -72,10 +72,8 @@ type Props = {
|
||||
|
||||
export default function ScoreStats({ score, leaderboard }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center h-full">
|
||||
<div className={`grid grid-cols-3 gap-1 justify-center`}>
|
||||
<ScoreBadges badges={badges} score={score} leaderboard={leaderboard} />
|
||||
</div>
|
||||
<div className={`grid grid-cols-3 grid-rows-2 gap-1 ml-0 lg:ml-2 `}>
|
||||
<ScoreBadges badges={badges} score={score} leaderboard={leaderboard} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { format } from "@formkit/tempo";
|
||||
import { timeAgo } from "@ssr/common/utils/time-utils";
|
||||
import Tooltip from "@/components/tooltip";
|
||||
import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score";
|
||||
|
||||
type ScoreTimeSetProps = {
|
||||
/**
|
||||
* The score that was set.
|
||||
*/
|
||||
score: ScoreSaberScore;
|
||||
};
|
||||
|
||||
export function ScoreTimeSetVs({ score }: ScoreTimeSetProps) {
|
||||
if (!score.previousScore) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
display={
|
||||
<p>
|
||||
{format({
|
||||
date: new Date(score.previousScore.timestamp),
|
||||
format: "DD MMMM YYYY HH:mm a",
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 flex flex-row gap-2 items-center">
|
||||
<p>vs</p>
|
||||
<p>{timeAgo(new Date(score.previousScore.timestamp))}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
@ -7,7 +7,7 @@ import { CubeIcon } from "@heroicons/react/24/solid";
|
||||
import { TrendingUpIcon } from "lucide-react";
|
||||
import ScoreButtons from "./score-buttons";
|
||||
import ScoreSongInfo from "./score-song-info";
|
||||
import ScoreRankAndDateInfo from "./score-rank-and-date-info";
|
||||
import ScoreRankInfo from "./score-rank-info";
|
||||
import ScoreStats from "./score-stats";
|
||||
import Card from "@/components/card";
|
||||
import { MapStats } from "@/components/score/map-stats";
|
||||
@ -120,13 +120,13 @@ export default function Score({ leaderboard, beatSaverMap, score, settings, high
|
||||
};
|
||||
|
||||
const gridColsClass = settings?.noScoreButtons
|
||||
? "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_350px]" // Fewer columns if no buttons
|
||||
: "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_1fr_350px]"; // Original with buttons
|
||||
? "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_300px]" // Fewer columns if no buttons
|
||||
: "grid-cols-[20px 1fr_1fr] lg:grid-cols-[0.5fr_4fr_1fr_300px]"; // Original with buttons
|
||||
|
||||
return (
|
||||
<div className="pb-2 pt-2">
|
||||
<div className={`grid w-full gap-2 lg:gap-0 ${gridColsClass}`}>
|
||||
<ScoreRankAndDateInfo score={score} leaderboard={leaderboard} />
|
||||
<ScoreRankInfo score={score} leaderboard={leaderboard} />
|
||||
<ScoreSongInfo leaderboard={leaderboard} beatSaverMap={beatSaverMap} />
|
||||
{!settings?.noScoreButtons && (
|
||||
<ScoreButtons
|
||||
|
@ -16,24 +16,18 @@ type Props = {
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* The additional classes for the stat.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The value of the stat.
|
||||
*/
|
||||
value: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function StatValue({ name, icon, color, className, value }: Props) {
|
||||
export default function StatValue({ name, icon, color, value }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex min-w-16 gap-2 h-full p-1 items-center justify-center rounded-md text-sm cursor-default",
|
||||
color ? color : "bg-accent",
|
||||
className
|
||||
color ? color : "bg-accent"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: (!color?.includes("bg") && color) || undefined,
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import { Tooltip as ShadCnTooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
import { useState } from "react";
|
||||
import { clsx } from "clsx";
|
||||
import { cn } from "@/common/utils";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
@ -36,15 +36,16 @@ export default function Tooltip({ children, display, asChild = true, side = "top
|
||||
|
||||
return (
|
||||
<ShadCnTooltip open={open}>
|
||||
<TooltipTrigger
|
||||
className={clsx("cursor-default", className)}
|
||||
asChild={asChild}
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
onTouchStart={() => setOpen(!open)}
|
||||
onTouchEnd={() => setOpen(!open)}
|
||||
>
|
||||
{children}
|
||||
<TooltipTrigger className={className} asChild={asChild}>
|
||||
<div
|
||||
className={cn("cursor-default", className)}
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
onTouchStart={() => setOpen(!open)}
|
||||
onTouchEnd={() => setOpen(!open)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[350px]" side={side}>
|
||||
{display}
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
export function useIsMobile() {
|
||||
const checkMobile = () => {
|
||||
return window.innerWidth <= 1024;
|
||||
return window.innerWidth < 768;
|
||||
};
|
||||
const [isMobile, setIsMobile] = useState(checkMobile());
|
||||
|
||||
|
Reference in New Issue
Block a user