From 33b931b5f1ab61e76b8fae2c673bf5d7a6995535 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 23 Oct 2024 15:33:25 +0100 Subject: [PATCH] add map stats from beat saver --- .../backend/src/service/beatsaver.service.ts | 59 ++++++-- .../src/service/leaderboard.service.ts | 2 +- projects/backend/src/service/score.service.ts | 16 +-- .../impl/scoresaber-leaderboard.ts | 5 +- .../src/leaderboard/leaderboard-difficulty.ts | 9 +- projects/common/src/model/beatsaver/author.ts | 27 ++++ .../src/model/beatsaver/beatsaver-author.ts | 13 -- .../src/model/beatsaver/beatsaver-map.ts | 57 -------- .../src/model/beatsaver/map-difficulty.ts | 128 ++++++++++++++++++ .../src/model/beatsaver/map-metadata.ts | 55 ++++++++ .../common/src/model/beatsaver/map-version.ts | 31 +++++ projects/common/src/model/beatsaver/map.ts | 99 ++++++++++++++ .../src/response/leaderboard-response.ts | 2 +- .../response/leaderboard-scores-response.ts | 2 +- projects/common/src/score/difficulty.ts | 1 - projects/common/src/score/map-difficulty.ts | 1 + projects/common/src/score/player-score.ts | 2 +- projects/common/src/service/impl/beatsaver.ts | 2 +- .../common/src/types/map-characteristic.ts | 1 + projects/common/src/types/page.ts | 13 -- ...beat-saver-account-token.ts => account.ts} | 0 .../token/beatsaver/beat-saver-map-token.ts | 24 ---- .../beatsaver/difficulty-parity-summary.ts | 16 +++ .../types/token/beatsaver/map-difficulty.ts | 90 ++++++++++++ ...-map-metadata-token.ts => map-metadata.ts} | 0 ...-saver-map-stats-token.ts => map-stats.ts} | 0 .../src/types/token/beatsaver/map-version.ts | 43 ++++++ .../common/src/types/token/beatsaver/map.ts | 96 +++++++++++++ projects/common/src/utils/beatsaver.util.ts | 20 ++- projects/common/src/utils/player-utils.ts | 2 +- projects/common/src/utils/scoresaber-utils.ts | 6 +- projects/common/src/utils/time-utils.ts | 17 +++ projects/website/src/app/layout.tsx | 19 ++- projects/website/src/common/song-utils.ts | 59 +++----- .../src/components/input/pagination.tsx | 79 +++-------- .../src/components/input/search-player.tsx | 4 +- .../leaderboard/leaderboard-info.tsx | 2 +- .../leaderboard/leaderboard-scores.tsx | 16 +-- .../leaderboard-song-star-count.tsx | 4 +- .../src/components/navbar/friends-button.tsx | 2 +- .../src/components/score/map-stats.tsx | 53 ++++++++ .../src/components/score/score-buttons.tsx | 2 +- .../src/components/score/score-modifiers.tsx | 2 +- .../{score-info.tsx => score-song-info.tsx} | 18 +-- .../website/src/components/score/score.tsx | 12 +- .../website/src/components/stat-value.tsx | 8 +- projects/website/src/components/tooltip.tsx | 5 +- 47 files changed, 835 insertions(+), 289 deletions(-) create mode 100644 projects/common/src/model/beatsaver/author.ts delete mode 100644 projects/common/src/model/beatsaver/beatsaver-author.ts delete mode 100644 projects/common/src/model/beatsaver/beatsaver-map.ts create mode 100644 projects/common/src/model/beatsaver/map-difficulty.ts create mode 100644 projects/common/src/model/beatsaver/map-metadata.ts create mode 100644 projects/common/src/model/beatsaver/map-version.ts create mode 100644 projects/common/src/model/beatsaver/map.ts delete mode 100644 projects/common/src/score/difficulty.ts create mode 100644 projects/common/src/score/map-difficulty.ts create mode 100644 projects/common/src/types/map-characteristic.ts delete mode 100644 projects/common/src/types/page.ts rename projects/common/src/types/token/beatsaver/{beat-saver-account-token.ts => account.ts} (100%) delete mode 100644 projects/common/src/types/token/beatsaver/beat-saver-map-token.ts create mode 100644 projects/common/src/types/token/beatsaver/difficulty-parity-summary.ts create mode 100644 projects/common/src/types/token/beatsaver/map-difficulty.ts rename projects/common/src/types/token/beatsaver/{beat-saver-map-metadata-token.ts => map-metadata.ts} (100%) rename projects/common/src/types/token/beatsaver/{beat-saver-map-stats-token.ts => map-stats.ts} (100%) create mode 100644 projects/common/src/types/token/beatsaver/map-version.ts create mode 100644 projects/common/src/types/token/beatsaver/map.ts create mode 100644 projects/website/src/components/score/map-stats.tsx rename projects/website/src/components/score/{score-info.tsx => score-song-info.tsx} (83%) diff --git a/projects/backend/src/service/beatsaver.service.ts b/projects/backend/src/service/beatsaver.service.ts index 2537a16..5d5435a 100644 --- a/projects/backend/src/service/beatsaver.service.ts +++ b/projects/backend/src/service/beatsaver.service.ts @@ -1,5 +1,5 @@ import { beatsaverService } from "@ssr/common/service/impl/beatsaver"; -import { BeatSaverMap, BeatSaverMapModel } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap, BeatSaverMapModel } from "@ssr/common/model/beatsaver/map"; export default class BeatSaverService { /** @@ -12,28 +12,71 @@ export default class BeatSaverService { let map = await BeatSaverMapModel.findById(hash); if (map != undefined) { const toObject = map.toObject() as BeatSaverMap; - if (toObject.unknownMap) { + if (toObject.notFound) { return undefined; } return toObject; } const token = await beatsaverService.lookupMap(hash); + const uploader = token?.uploader; + const metadata = token?.metadata; + map = await BeatSaverMapModel.create( - token - ? { + token && uploader && metadata + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + ({ _id: hash, bsr: token.id, + name: token.name, + description: token.description, author: { - id: token.uploader.id, + id: uploader.id, + name: uploader.name, + avatar: uploader.avatar, }, - } + metadata: { + bpm: metadata.bpm, + duration: metadata.duration, + levelAuthorName: metadata.levelAuthorName, + songAuthorName: metadata.songAuthorName, + songName: metadata.songName, + songSubName: metadata.songSubName, + }, + versions: token.versions.map(version => { + return { + hash: version.hash.toUpperCase(), + difficulties: version.diffs.map(diff => { + return { + njs: diff.njs, + offset: diff.offset, + notes: diff.notes, + bombs: diff.bombs, + obstacles: diff.obstacles, + nps: diff.nps, + characteristic: diff.characteristic, + difficulty: diff.difficulty, + events: diff.events, + chroma: diff.chroma, + mappingExtensions: diff.me, + noodleExtensions: diff.ne, + cinema: diff.cinema, + maxScore: diff.maxScore, + label: diff.label, + }; + }), + createdAt: new Date(version.createdAt), + }; + }), + lastRefreshed: new Date(), + } as BeatSaverMap) : { _id: hash, - unknownMap: true, + notFound: true, } ); - if (map.unknownMap) { + if (map.notFound) { return undefined; } return map.toObject() as BeatSaverMap; diff --git a/projects/backend/src/service/leaderboard.service.ts b/projects/backend/src/service/leaderboard.service.ts index 74fcae2..f8f775e 100644 --- a/projects/backend/src/service/leaderboard.service.ts +++ b/projects/backend/src/service/leaderboard.service.ts @@ -7,7 +7,7 @@ import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score import { NotFoundError } from "elysia"; import { getScoreSaberLeaderboardFromToken } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import BeatSaverService from "./beatsaver.service"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; const leaderboardCache = new SSRCache({ ttl: 1000 * 60 * 60 * 24, diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 1c1b31d..0565419 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -13,7 +13,7 @@ import { ScoreSort } from "@ssr/common/score/score-sort"; import { Leaderboards } from "@ssr/common/leaderboard"; import Leaderboard from "@ssr/common/leaderboard/leaderboard"; import LeaderboardService from "./leaderboard.service"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { PlayerScore } from "@ssr/common/score/player-score"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import Score from "@ssr/common/score/score"; @@ -188,7 +188,7 @@ export class ScoreService { }; const difficulty = leaderboard.difficulty; - const difficultyKey = `${difficulty.difficultyName.replace("Plus", "+")}-${difficulty.modeName}`; + const difficultyKey = `${difficulty.difficultyName}-${difficulty.modeName}`; const rawScoreImprovement = score.scoreImprovement; const data = { playerId: playerId, @@ -312,15 +312,15 @@ export class ScoreService { if (score == undefined) { continue; } - const tokenLeaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard); - if (tokenLeaderboard == undefined) { + const leaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard); + if (leaderboard == undefined) { continue; } const additionalData = await this.getAdditionalScoreData( id, - tokenLeaderboard.songHash, - `${tokenLeaderboard.difficulty.difficulty}-${tokenLeaderboard.difficulty.gameMode}`, + leaderboard.songHash, + `${leaderboard.difficulty.difficulty}-${leaderboard.difficulty.characteristic}`, score.score ); if (additionalData !== undefined) { @@ -329,8 +329,8 @@ export class ScoreService { scores.push({ score: score, - leaderboard: tokenLeaderboard, - beatSaver: await BeatSaverService.getMap(tokenLeaderboard.songHash), + leaderboard: leaderboard, + beatSaver: await BeatSaverService.getMap(leaderboard.songHash), }); } break; diff --git a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts index 9ca59ed..8de5306 100644 --- a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts +++ b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts @@ -4,6 +4,7 @@ import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber import { getDifficultyFromScoreSaberDifficulty } from "../../utils/scoresaber-utils"; import { parseDate } from "../../utils/time-utils"; import { LeaderboardStatus } from "../leaderboard-status"; +import { MapCharacteristic } from "../../types/map-characteristic"; export default interface ScoreSaberLeaderboard extends Leaderboard { /** @@ -41,7 +42,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo const difficulty: LeaderboardDifficulty = { leaderboardId: token.difficulty.leaderboardId, difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty), - gameMode: token.difficulty.gameMode.replace("Solo", ""), + characteristic: token.difficulty.gameMode.replace("Solo", "") as MapCharacteristic, difficultyRaw: token.difficulty.difficultyRaw, }; @@ -66,7 +67,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo return { leaderboardId: difficulty.leaderboardId, difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty), - gameMode: difficulty.gameMode.replace("Solo", ""), + characteristic: difficulty.gameMode.replace("Solo", "") as MapCharacteristic, difficultyRaw: difficulty.difficultyRaw, }; }) diff --git a/projects/common/src/leaderboard/leaderboard-difficulty.ts b/projects/common/src/leaderboard/leaderboard-difficulty.ts index e7b26d2..316c5a3 100644 --- a/projects/common/src/leaderboard/leaderboard-difficulty.ts +++ b/projects/common/src/leaderboard/leaderboard-difficulty.ts @@ -1,4 +1,5 @@ -import { Difficulty } from "../score/difficulty"; +import { MapDifficulty } from "../score/map-difficulty"; +import { MapCharacteristic } from "../types/map-characteristic"; export default interface LeaderboardDifficulty { /** @@ -9,12 +10,12 @@ export default interface LeaderboardDifficulty { /** * The difficulty of the leaderboard. */ - difficulty: Difficulty; + difficulty: MapDifficulty; /** - * The game mode of the leaderboard. + * The characteristic of the leaderboard. */ - gameMode: string; + characteristic: MapCharacteristic; /** * The raw difficulty of the leaderboard. diff --git a/projects/common/src/model/beatsaver/author.ts b/projects/common/src/model/beatsaver/author.ts new file mode 100644 index 0000000..4b970d4 --- /dev/null +++ b/projects/common/src/model/beatsaver/author.ts @@ -0,0 +1,27 @@ +import { prop } from "@typegoose/typegoose"; + +export default class BeatSaverAuthor { + /** + * The id of the author. + */ + @prop({ required: true }) + id: number; + + /** + * The name of the mapper. + */ + @prop({ required: true }) + name: string; + + /** + * The avatar URL for the mapper. + */ + @prop({ required: true }) + avatar: string; + + constructor(id: number, name: string, avatar: string) { + this.id = id; + this.name = name; + this.avatar = avatar; + } +} diff --git a/projects/common/src/model/beatsaver/beatsaver-author.ts b/projects/common/src/model/beatsaver/beatsaver-author.ts deleted file mode 100644 index a88e4ab..0000000 --- a/projects/common/src/model/beatsaver/beatsaver-author.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { prop } from "@typegoose/typegoose"; - -export default class BeatsaverAuthor { - /** - * The id of the author. - */ - @prop({ required: true }) - id: number; - - constructor(id: number) { - this.id = id; - } -} diff --git a/projects/common/src/model/beatsaver/beatsaver-map.ts b/projects/common/src/model/beatsaver/beatsaver-map.ts deleted file mode 100644 index 8cbbfe0..0000000 --- a/projects/common/src/model/beatsaver/beatsaver-map.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose"; -import { Document } from "mongoose"; -import BeatsaverAuthor from "./beatsaver-author"; - -/** - * The model for a BeatSaver map. - */ -@modelOptions({ - options: { allowMixed: Severity.ALLOW }, - schemaOptions: { - toObject: { - virtuals: true, - transform: function (_, ret) { - ret.id = ret._id; - delete ret._id; - delete ret.__v; - return ret; - }, - }, - }, -}) -export class BeatSaverMap { - /** - * The internal MongoDB ID (_id). - */ - @prop({ required: true }) - private _id!: string; - - /** - * The bsr code for the map. - * @private - */ - @prop({ required: false }) - public bsr!: string; - - /** - * The author of the map. - */ - @prop({ required: false, _id: false, type: () => BeatsaverAuthor }) - public author!: BeatsaverAuthor; - - /** - * True if the map is unknown on beatsaver. - */ - @prop({ required: false }) - public unknownMap?: boolean; - - /** - * Exposes `id` as a virtual field mapped from `_id`. - */ - public get id(): string { - return this._id; - } -} - -export type BeatSaverMapDocument = BeatSaverMap & Document; -export const BeatSaverMapModel: ReturnModelType = getModelForClass(BeatSaverMap); diff --git a/projects/common/src/model/beatsaver/map-difficulty.ts b/projects/common/src/model/beatsaver/map-difficulty.ts new file mode 100644 index 0000000..b391e7b --- /dev/null +++ b/projects/common/src/model/beatsaver/map-difficulty.ts @@ -0,0 +1,128 @@ +import { prop } from "@typegoose/typegoose"; +import { MapDifficulty } from "../../score/map-difficulty"; + +export default class BeatSaverMapDifficulty { + /** + * The NJS of this difficulty. + */ + @prop({ required: true }) + njs: number; + + /** + * The NJS offset of this difficulty. + */ + @prop({ required: true }) + offset: number; + + /** + * The amount of notes in this difficulty. + */ + @prop({ required: true }) + notes: number; + + /** + * The amount of bombs in this difficulty. + */ + @prop({ required: true }) + bombs: number; + + /** + * The amount of obstacles in this difficulty. + */ + @prop({ required: true }) + obstacles: number; + + /** + * The notes per second in this difficulty. + */ + @prop({ required: true }) + nps: number; + + /** + * The characteristic of this difficulty. + */ + @prop({ required: true, enum: ["Standard", "Lawless"] }) + characteristic: "Standard" | "Lawless"; + + /** + * The difficulty level. + */ + @prop({ required: true }) + difficulty: MapDifficulty; + + /** + * The amount of lighting events in this difficulty. + */ + @prop({ required: true }) + events: number; + + /** + * Whether this difficulty uses Chroma. + */ + @prop({ required: true, default: false }) + chroma: boolean; + + /** + * Does this difficulty use Mapping Extensions. + */ + @prop({ required: true, default: false }) + mappingExtensions: boolean; + + /** + * Does this difficulty use Noodle Extensions. + */ + @prop({ required: true, default: false }) + noodleExtensions: boolean; + + /** + * Whether this difficulty uses cinema mode. + */ + @prop({ required: true, default: false }) + cinema: boolean; + + /** + * The maximum score achievable in this difficulty. + */ + @prop({ required: true }) + maxScore: number; + + /** + * The custom label for this difficulty. + */ + @prop() + label: string; + + constructor( + njs: number, + offset: number, + notes: number, + bombs: number, + obstacles: number, + nps: number, + characteristic: "Standard" | "Lawless", + difficulty: MapDifficulty, + events: number, + chroma: boolean, + mappingExtensions: boolean, + noodleExtensions: boolean, + cinema: boolean, + maxScore: number, + label: string + ) { + this.njs = njs; + this.offset = offset; + this.notes = notes; + this.bombs = bombs; + this.obstacles = obstacles; + this.nps = nps; + this.characteristic = characteristic; + this.difficulty = difficulty; + this.events = events; + this.chroma = chroma; + this.mappingExtensions = mappingExtensions; + this.noodleExtensions = noodleExtensions; + this.cinema = cinema; + this.maxScore = maxScore; + this.label = label; + } +} diff --git a/projects/common/src/model/beatsaver/map-metadata.ts b/projects/common/src/model/beatsaver/map-metadata.ts new file mode 100644 index 0000000..0bf6e39 --- /dev/null +++ b/projects/common/src/model/beatsaver/map-metadata.ts @@ -0,0 +1,55 @@ +import { prop } from "@typegoose/typegoose"; + +export default class BeatSaverMapMetadata { + /** + * The bpm of the song. + */ + @prop({ required: true }) + bpm: number; + + /** + * The song's length in seconds. + */ + @prop({ required: true }) + duration: number; + + /** + * The song's name. + */ + @prop({ required: true }) + songName: string; + + /** + * The song's sub name. + */ + @prop({ required: false }) + songSubName: string; + + /** + * The artist(s) name. + */ + @prop({ required: true }) + songAuthorName: string; + + /** + * The level mapper(s) name. + */ + @prop({ required: true }) + levelAuthorName: string; + + constructor( + bpm: number, + duration: number, + songName: string, + songSubName: string, + songAuthorName: string, + levelAuthorName: string + ) { + this.bpm = bpm; + this.duration = duration; + this.songName = songName; + this.songSubName = songSubName; + this.songAuthorName = songAuthorName; + this.levelAuthorName = levelAuthorName; + } +} diff --git a/projects/common/src/model/beatsaver/map-version.ts b/projects/common/src/model/beatsaver/map-version.ts new file mode 100644 index 0000000..d538dca --- /dev/null +++ b/projects/common/src/model/beatsaver/map-version.ts @@ -0,0 +1,31 @@ +import { modelOptions, prop, Severity } from "@typegoose/typegoose"; +import BeatSaverMapDifficulty from "./map-difficulty"; + +@modelOptions({ + options: { allowMixed: Severity.ALLOW }, +}) +export default class BeatSaverMapVersion { + /** + * The hash of this map. + */ + @prop({ required: true }) + hash: string; + + /** + * The date the map was created. + */ + @prop({ required: true }) + createdAt: Date; + + /** + * The difficulties of this map. + */ + @prop({ required: true }) + difficulties: BeatSaverMapDifficulty[]; + + constructor(hash: string, createdAt: Date, difficulties: BeatSaverMapDifficulty[]) { + this.hash = hash; + this.createdAt = createdAt; + this.difficulties = difficulties; + } +} diff --git a/projects/common/src/model/beatsaver/map.ts b/projects/common/src/model/beatsaver/map.ts new file mode 100644 index 0000000..2e7b60b --- /dev/null +++ b/projects/common/src/model/beatsaver/map.ts @@ -0,0 +1,99 @@ +import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose"; +import { Document } from "mongoose"; +import BeatSaverAuthor from "./author"; +import BeatSaverMapVersion from "./map-version"; +import BeatSaverMapMetadata from "./map-metadata"; + +/** + * The model for a BeatSaver map. + */ +@modelOptions({ + options: { allowMixed: Severity.ALLOW }, + schemaOptions: { + collection: "beatsaver-maps", + toObject: { + virtuals: true, + transform: function (_, ret) { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + }, + }, + }, +}) +export class BeatSaverMap { + /** + * The internal MongoDB ID (_id). + */ + @prop({ required: true }) + protected _id!: string; + + /** + * The name of the map. + */ + @prop({ required: false }) + public name!: string; + + /** + * The description of the map. + */ + @prop({ required: false }) + public description!: string; + + /** + * The bsr code for the map. + */ + @prop({ required: false }) + public bsr!: string; + + /** + * The author of the map. + */ + @prop({ required: false, _id: false, type: () => BeatSaverAuthor }) + public author!: BeatSaverAuthor; + + /** + * The versions of the map. + */ + @prop({ required: false, _id: false, type: () => [BeatSaverMapVersion] }) + public versions!: BeatSaverMapVersion[]; + + /** + * The metadata of the map. + */ + @prop({ required: false, _id: false, type: () => BeatSaverMapMetadata }) + public metadata!: BeatSaverMapMetadata; + + /** + * True if the map is not found on beatsaver. + */ + @prop({ required: false }) + public notFound?: boolean; + + /** + * The last time the map data was refreshed. + */ + @prop({ required: true }) + public lastRefreshed!: Date; + + /** + * Exposes `id` as a virtual field mapped from `_id`. + */ + public get id(): string { + return this._id; + } + + /** + * Should the map data be refreshed? + * + * @returns true if the map data should be refreshed + */ + public shouldRefresh(): boolean { + const now = new Date(); + return now.getTime() - this.lastRefreshed.getTime() > 1000 * 60 * 60 * 24 * 3; // 3 days + } +} + +export type BeatSaverMapDocument = BeatSaverMap & Document; +export const BeatSaverMapModel: ReturnModelType = getModelForClass(BeatSaverMap); diff --git a/projects/common/src/response/leaderboard-response.ts b/projects/common/src/response/leaderboard-response.ts index d1e101b..8c146bb 100644 --- a/projects/common/src/response/leaderboard-response.ts +++ b/projects/common/src/response/leaderboard-response.ts @@ -1,4 +1,4 @@ -import { BeatSaverMap } from "../model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "../model/beatsaver/map"; export type LeaderboardResponse = { /** diff --git a/projects/common/src/response/leaderboard-scores-response.ts b/projects/common/src/response/leaderboard-scores-response.ts index c396088..a956b19 100644 --- a/projects/common/src/response/leaderboard-scores-response.ts +++ b/projects/common/src/response/leaderboard-scores-response.ts @@ -1,5 +1,5 @@ import { Metadata } from "../types/metadata"; -import { BeatSaverMap } from "../model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "../model/beatsaver/map"; export default interface LeaderboardScoresResponse { /** diff --git a/projects/common/src/score/difficulty.ts b/projects/common/src/score/difficulty.ts deleted file mode 100644 index 13c47d3..0000000 --- a/projects/common/src/score/difficulty.ts +++ /dev/null @@ -1 +0,0 @@ -export type Difficulty = "Easy" | "Normal" | "Hard" | "Expert" | "Expert+" | "Unknown"; diff --git a/projects/common/src/score/map-difficulty.ts b/projects/common/src/score/map-difficulty.ts new file mode 100644 index 0000000..f71e7ec --- /dev/null +++ b/projects/common/src/score/map-difficulty.ts @@ -0,0 +1 @@ +export type MapDifficulty = "Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus" | "Unknown"; diff --git a/projects/common/src/score/player-score.ts b/projects/common/src/score/player-score.ts index 4712fc5..afbc8a4 100644 --- a/projects/common/src/score/player-score.ts +++ b/projects/common/src/score/player-score.ts @@ -1,4 +1,4 @@ -import { BeatSaverMap } from "../model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "../model/beatsaver/map"; export interface PlayerScore { /** diff --git a/projects/common/src/service/impl/beatsaver.ts b/projects/common/src/service/impl/beatsaver.ts index f93f371..e1b810d 100644 --- a/projects/common/src/service/impl/beatsaver.ts +++ b/projects/common/src/service/impl/beatsaver.ts @@ -1,5 +1,5 @@ import Service from "../service"; -import { BeatSaverMapToken } from "../../types/token/beatsaver/beat-saver-map-token"; +import { BeatSaverMapToken } from "../../types/token/beatsaver/map"; const API_BASE = "https://api.beatsaver.com"; const LOOKUP_MAP_BY_HASH_ENDPOINT = `${API_BASE}/maps/hash/:query`; diff --git a/projects/common/src/types/map-characteristic.ts b/projects/common/src/types/map-characteristic.ts new file mode 100644 index 0000000..f9f6451 --- /dev/null +++ b/projects/common/src/types/map-characteristic.ts @@ -0,0 +1 @@ +export type MapCharacteristic = "Standard" | "Lawless"; diff --git a/projects/common/src/types/page.ts b/projects/common/src/types/page.ts deleted file mode 100644 index 313cfad..0000000 --- a/projects/common/src/types/page.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Metadata } from "./metadata"; - -export type Page = { - /** - * The data to return. - */ - data: T[]; - - /** - * The metadata of the page. - */ - metadata: Metadata; -}; diff --git a/projects/common/src/types/token/beatsaver/beat-saver-account-token.ts b/projects/common/src/types/token/beatsaver/account.ts similarity index 100% rename from projects/common/src/types/token/beatsaver/beat-saver-account-token.ts rename to projects/common/src/types/token/beatsaver/account.ts diff --git a/projects/common/src/types/token/beatsaver/beat-saver-map-token.ts b/projects/common/src/types/token/beatsaver/beat-saver-map-token.ts deleted file mode 100644 index 5696367..0000000 --- a/projects/common/src/types/token/beatsaver/beat-saver-map-token.ts +++ /dev/null @@ -1,24 +0,0 @@ -import BeatSaverAccountToken from "./beat-saver-account-token"; -import BeatSaverMapMetadataToken from "./beat-saver-map-metadata-token"; -import BeatSaverMapStatsToken from "./beat-saver-map-stats-token"; - -export interface BeatSaverMapToken { - id: string; - name: string; - description: string; - uploader: BeatSaverAccountToken; - metadata: BeatSaverMapMetadataToken; - stats: BeatSaverMapStatsToken; - uploaded: string; - automapper: boolean; - ranked: boolean; - qualified: boolean; - // todo: versions - createdAt: string; - updatedAt: string; - lastPublishedAt: string; - tags: string[]; - declaredAi: string; - blRanked: boolean; - blQualified: boolean; -} diff --git a/projects/common/src/types/token/beatsaver/difficulty-parity-summary.ts b/projects/common/src/types/token/beatsaver/difficulty-parity-summary.ts new file mode 100644 index 0000000..ab1a9cf --- /dev/null +++ b/projects/common/src/types/token/beatsaver/difficulty-parity-summary.ts @@ -0,0 +1,16 @@ +export type MapDifficultyParitySummaryToken = { + /** + * The amount of parity errors. + */ + errors: number; + + /** + * The amount of parity warnings. + */ + warns: number; + + /** + * The amount of resets in the difficulty. + */ + resets: number; +}; diff --git a/projects/common/src/types/token/beatsaver/map-difficulty.ts b/projects/common/src/types/token/beatsaver/map-difficulty.ts new file mode 100644 index 0000000..b26254b --- /dev/null +++ b/projects/common/src/types/token/beatsaver/map-difficulty.ts @@ -0,0 +1,90 @@ +import { MapDifficulty } from "../../../score/map-difficulty"; +import { MapDifficultyParitySummaryToken } from "./difficulty-parity-summary"; + +export type BeatSaverMapDifficultyToken = { + /** + * The NJS of this difficulty. + */ + njs: number; + + /** + * The NJS offset of this difficulty. + */ + offset: number; + + /** + * The amount of notes in this difficulty. + */ + notes: number; + + /** + * The amount of bombs in this difficulty. + */ + bombs: number; + + /** + * The amount of obstacles in this difficulty. + */ + obstacles: number; + + /** + * The notes per second in this difficulty. + */ + nps: number; + + /** + * The length of this difficulty in seconds. + */ + length: number; + + /** + * The characteristic of this difficulty. + */ + characteristic: "Standard" | "Lawless"; + + /** + * The difficulty of this difficulty. + */ + difficulty: MapDifficulty; + + /** + * The amount of lighting events in this difficulty. + */ + events: number; + + /** + * Whether this difficulty uses Chroma. + */ + chroma: boolean; + + /** + * Quite frankly I have no fucking idea what these are. + */ + me: boolean; + ne: boolean; + + /** + * Does this difficulty use cinema? + */ + cinema: boolean; + + /** + * The length of this difficulty in seconds. + */ + seconds: number; + + /** + * The parity summary of this difficulty. + */ + paritySummary: MapDifficultyParitySummaryToken; + + /** + * The maximum score of this difficulty. + */ + maxScore: number; + + /** + * The custom difficulty label. + */ + label: string; +}; diff --git a/projects/common/src/types/token/beatsaver/beat-saver-map-metadata-token.ts b/projects/common/src/types/token/beatsaver/map-metadata.ts similarity index 100% rename from projects/common/src/types/token/beatsaver/beat-saver-map-metadata-token.ts rename to projects/common/src/types/token/beatsaver/map-metadata.ts diff --git a/projects/common/src/types/token/beatsaver/beat-saver-map-stats-token.ts b/projects/common/src/types/token/beatsaver/map-stats.ts similarity index 100% rename from projects/common/src/types/token/beatsaver/beat-saver-map-stats-token.ts rename to projects/common/src/types/token/beatsaver/map-stats.ts diff --git a/projects/common/src/types/token/beatsaver/map-version.ts b/projects/common/src/types/token/beatsaver/map-version.ts new file mode 100644 index 0000000..0c42969 --- /dev/null +++ b/projects/common/src/types/token/beatsaver/map-version.ts @@ -0,0 +1,43 @@ +import { BeatSaverMapDifficultyToken } from "./map-difficulty"; + +export type BeatSaverMapVersionToken = { + /** + * The hash of the map. + */ + hash: string; + + /** + * The stage of the map. + */ + stage: "Published"; // todo: find the rest of these + + /** + * The date the map was created. + */ + createdAt: string; + + /** + * The sage score of the map. (no idea what this is x.x) + */ + sageScore: number; + + /** + * The difficulties in the map. + */ + diffs: BeatSaverMapDifficultyToken[]; + + /** + * The URL to the download of the map. + */ + downloadURL: string; + + /** + * The URL to the cover image. + */ + coverURL: string; + + /** + * The URL to the preview of the map. + */ + previewURL: string; +}; diff --git a/projects/common/src/types/token/beatsaver/map.ts b/projects/common/src/types/token/beatsaver/map.ts new file mode 100644 index 0000000..0421222 --- /dev/null +++ b/projects/common/src/types/token/beatsaver/map.ts @@ -0,0 +1,96 @@ +import BeatSaverAccountToken from "./account"; +import BeatSaverMapMetadataToken from "./map-metadata"; +import BeatSaverMapStatsToken from "./map-stats"; +import { BeatSaverMapVersionToken } from "./map-version"; + +export interface BeatSaverMapToken { + /** + * The id of the map. + */ + id: string; + + /** + * The name of the map. + */ + name: string; + + /** + * The description of the map. + */ + description: string; + + /** + * The uploader of the map. + */ + uploader: BeatSaverAccountToken; + + /** + * The metadata of the map. + */ + metadata: BeatSaverMapMetadataToken; + + /** + * The stats of the map. + */ + stats: BeatSaverMapStatsToken; + + /** + * The date the map was uploaded. + */ + uploaded: string; + + /** + * Whether the map was mapped by an automapper. + */ + automapper: boolean; + + /** + * Whether the map is ranked on ScoreSaber. + */ + ranked: boolean; + + /** + * Whether the map is qualified on ScoreSaber. + */ + qualified: boolean; + + /** + * The versions of the map. + */ + versions: BeatSaverMapVersionToken[]; + + /** + * The date the map was created. + */ + createdAt: string; + + /** + * The date the map was last updated. + */ + updatedAt: string; + + /** + * The date the map was last published. + */ + lastPublishedAt: string; + + /** + * The tags of the map. + */ + tags: string[]; + + /** + * Whether the map is declared to be mapped by an AI. + */ + declaredAi: string; + + /** + * Whether the map is ranked on BeatLeader. + */ + blRanked: boolean; + + /** + * Whether the map is qualified on BeatLeader. + */ + blQualified: boolean; +} diff --git a/projects/common/src/utils/beatsaver.util.ts b/projects/common/src/utils/beatsaver.util.ts index af4833c..e7aaceb 100644 --- a/projects/common/src/utils/beatsaver.util.ts +++ b/projects/common/src/utils/beatsaver.util.ts @@ -1,7 +1,8 @@ -import { BeatSaverMap } from "../model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "../model/beatsaver/map"; +import { MapDifficulty } from "../score/map-difficulty"; /** - * Gets the beatSaver mapper profile url. + * Gets the BeatSaver mapper profile url. * * @param map the beatsaver map * @returns the beatsaver mapper profile url @@ -9,3 +10,18 @@ import { BeatSaverMap } from "../model/beatsaver/beatsaver-map"; export function getBeatSaverMapperProfileUrl(map?: BeatSaverMap) { return map != undefined ? `https://beatsaver.com/profile/${map?.author.id}` : undefined; } + +/** + * Gets a BeatSaver difficulty from a map. + * + * @param map the map to get the difficulty from + * @param hash the hash of the map + * @param difficulty the difficulty to get + */ +export function getBeatSaverDifficulty(map: BeatSaverMap, hash: string, difficulty: MapDifficulty) { + const version = map.versions.find(v => v.hash === hash); + if (version == undefined) { + return undefined; + } + return version.difficulties.find(d => d.difficulty === difficulty); +} diff --git a/projects/common/src/utils/player-utils.ts b/projects/common/src/utils/player-utils.ts index 35604cf..d46e4ec 100644 --- a/projects/common/src/utils/player-utils.ts +++ b/projects/common/src/utils/player-utils.ts @@ -60,7 +60,7 @@ export function sortPlayerHistory(history: Map) { * @param id the player id */ export async function trackPlayer(id: string) { - await kyFetch(`${Config.apiUrl}/player/history/1/${id}?createIfMissing=true`); + await kyFetch(`${Config.apiUrl}/player/history/${id}/1?createIfMissing=true`); } /** diff --git a/projects/common/src/utils/scoresaber-utils.ts b/projects/common/src/utils/scoresaber-utils.ts index ea433ce..650690c 100644 --- a/projects/common/src/utils/scoresaber-utils.ts +++ b/projects/common/src/utils/scoresaber-utils.ts @@ -1,11 +1,11 @@ -import { Difficulty } from "../score/difficulty"; +import { MapDifficulty } from "../score/map-difficulty"; /** * Formats the ScoreSaber difficulty number * * @param diff the diffuiclity number */ -export function getDifficultyFromScoreSaberDifficulty(diff: number): Difficulty { +export function getDifficultyFromScoreSaberDifficulty(diff: number): MapDifficulty { switch (diff) { case 1: { return "Easy"; @@ -20,7 +20,7 @@ export function getDifficultyFromScoreSaberDifficulty(diff: number): Difficulty return "Expert"; } case 9: { - return "Expert+"; + return "ExpertPlus"; } default: { return "Unknown"; diff --git a/projects/common/src/utils/time-utils.ts b/projects/common/src/utils/time-utils.ts index ff8994e..d647186 100644 --- a/projects/common/src/utils/time-utils.ts +++ b/projects/common/src/utils/time-utils.ts @@ -134,3 +134,20 @@ export function getDaysAgo(date: Date): number { export function parseDate(date: string): Date { return new Date(date); } + +/** + * Formats the time in the format "MM:SS" + * + * @param seconds the time to format in seconds + * @returns the formatted time in "MM:SS" format + */ +export function formatTime(seconds: number): string { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + // Zero pad minutes and seconds to ensure two digits + const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`; + const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`; + + return `${formattedMinutes}:${formattedSeconds}`; +} diff --git a/projects/website/src/app/layout.tsx b/projects/website/src/app/layout.tsx index 1b0fa25..055c7b6 100644 --- a/projects/website/src/app/layout.tsx +++ b/projects/website/src/app/layout.tsx @@ -5,7 +5,6 @@ import { QueryProvider } from "@/components/providers/query-provider"; import { ThemeProvider } from "@/components/providers/theme-provider"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { AnimatePresence } from "framer-motion"; import type { Metadata, Viewport } from "next"; import localFont from "next/font/local"; import BackgroundCover from "../components/background-cover"; @@ -79,16 +78,14 @@ export default function RootLayout({ - - -
- -
- {children} -
-
-
-
+ +
+ +
+ {children} +
+
+
diff --git a/projects/website/src/common/song-utils.ts b/projects/website/src/common/song-utils.ts index 3c61417..da5d607 100644 --- a/projects/website/src/common/song-utils.ts +++ b/projects/website/src/common/song-utils.ts @@ -1,17 +1,23 @@ +import { MapDifficulty } from "@ssr/common/score/map-difficulty"; + type Difficulty = { - name: DifficultyName; - gamemode?: string; + /** + * The name of the difficulty + */ + name: MapDifficulty; + + /** + * The color of the difficulty + */ color: string; }; -type DifficultyName = "Easy" | "Normal" | "Hard" | "Expert" | "Expert+"; - const difficulties: Difficulty[] = [ { name: "Easy", color: "#3cb371" }, { name: "Normal", color: "#59b0f4" }, { name: "Hard", color: "#FF6347" }, { name: "Expert", color: "#bf2a42" }, - { name: "Expert+", color: "#8f48db" }, + { name: "ExpertPlus", color: "#8f48db" }, ]; export type ScoreBadge = { @@ -22,7 +28,7 @@ export type ScoreBadge = { }; const scoreBadges: ScoreBadge[] = [ - { name: "SS+", min: 95, max: null, color: getDifficulty("Expert+")!.color }, + { name: "SS+", min: 95, max: null, color: getDifficulty("ExpertPlus")!.color }, { name: "SS", min: 90, max: 95, color: getDifficulty("Expert")!.color }, { name: "S+", min: 85, max: 90, color: getDifficulty("Hard")!.color }, { name: "S", min: 80, max: 85, color: getDifficulty("Normal")!.color }, @@ -57,45 +63,16 @@ export function getScoreBadgeFromAccuracy(acc: number): ScoreBadge { return scoreBadges[scoreBadges.length - 1]; } -/** - * Parses a raw difficulty into a {@link Difficulty} - * Example: _Easy_SoloStandard -> { name: "Easy", type: "Standard", color: "#59b0f4" } - * - * @param rawDifficulty the raw difficulty to parse - * @return the parsed difficulty - */ -export function getDifficultyFromRawDifficulty(rawDifficulty: string): Difficulty { - const [name, ...type] = rawDifficulty - .replace("Plus", "+") // Replaces Plus with + so we can match it to our difficulty names - .replace("Solo", "") // Removes "Solo" - .replace(/^_+|_+$/g, "") // Removes leading and trailing underscores - .split("_"); - const difficulty = difficulties.find(d => d.name === name); - if (!difficulty) { - throw new Error(`Unknown difficulty: ${rawDifficulty}`); - } - return { - ...difficulty, - gamemode: type.join("_"), - }; -} - /** * Gets a {@link Difficulty} from its name * * @param diff the name of the difficulty * @returns the difficulty */ -export function getDifficulty(diff: DifficultyName) { - return difficulties.find(d => d.name === diff); -} - -/** - * Turns the difficulty of a song into a color - * - * @param diff the difficulty to get the color for - * @returns the color for the difficulty - */ -export function songDifficultyToColor(diff: string) { - return getDifficultyFromRawDifficulty(diff).color; +export function getDifficulty(diff: MapDifficulty) { + const difficulty = difficulties.find(d => d.name === diff); + if (!difficulty) { + throw new Error(`Unknown difficulty: ${diff}`); + } + return difficulty; } diff --git a/projects/website/src/components/input/pagination.tsx b/projects/website/src/components/input/pagination.tsx index 530465b..3499e29 100644 --- a/projects/website/src/components/input/pagination.tsx +++ b/projects/website/src/components/input/pagination.tsx @@ -14,14 +14,7 @@ import { import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon } from "@heroicons/react/16/solid"; type PaginationItemWrapperProps = { - /** - * Whether a page is currently loading. - */ isLoadingPage: boolean; - - /** - * The children to render. - */ children: React.ReactNode; }; @@ -38,34 +31,11 @@ function PaginationItemWrapper({ isLoadingPage, children }: PaginationItemWrappe } type Props = { - /** - * If true, the pagination will be rendered as a mobile-friendly pagination. - */ mobilePagination: boolean; - - /** - * The current page. - */ page: number; - - /** - * The total number of pages. - */ totalPages: number; - - /** - * The page to show a loading icon on. - */ loadingPage: number | undefined; - - /** - * Callback function that is called when the user clicks on a page number. - */ onPageChange: (page: number) => void; - - /** - * Optional callback to generate the URL for each page. - */ generatePageUrl?: (page: number) => string; }; @@ -89,15 +59,12 @@ export default function Pagination({ if (newPage < 1 || newPage > totalPages || newPage === currentPage || isLoading) { return; } - setCurrentPage(newPage); onPageChange(newPage); }; const handleLinkClick = (newPage: number, event: React.MouseEvent) => { - event.preventDefault(); // Prevent default navigation behavior - - // Check if the new page is valid + event.preventDefault(); if (newPage < 1 || newPage > totalPages || newPage === currentPage || isLoading) { return; } @@ -116,26 +83,26 @@ export default function Pagination({ if (startPage > 1) { pageNumbers.push( - <> - - {!mobilePagination && ( - handleLinkClick(1, e)}> - 1 - - )} - - {startPage > 2 && !mobilePagination && ( - - - + + {!mobilePagination && ( + handleLinkClick(1, e)}> + 1 + )} - + ); + if (startPage > 2 && !mobilePagination) { + pageNumbers.push( + + + + ); + } } for (let i = startPage; i <= endPage; i++) { pageNumbers.push( - + - {/* ">>" before the Previous button in mobile mode */} {mobilePagination && ( - + handleLinkClick(1, e)}> )} - {/* Previous button - disabled on the first page */} - + 1 && generatePageUrl ? generatePageUrl(currentPage - 1) : ""} onClick={e => handleLinkClick(currentPage - 1, e)} @@ -176,10 +141,10 @@ export default function Pagination({ {!mobilePagination && currentPage < totalPages && totalPages - currentPage > 2 && ( <> - + - + handleLinkClick(totalPages, e)} @@ -190,8 +155,7 @@ export default function Pagination({ )} - {/* Next button - disabled on the last page */} - + handleLinkClick(currentPage + 1, e)} @@ -200,9 +164,8 @@ export default function Pagination({ /> - {/* ">>" after the Next button in mobile mode */} {mobilePagination && ( - + handleLinkClick(totalPages, e)} diff --git a/projects/website/src/components/input/search-player.tsx b/projects/website/src/components/input/search-player.tsx index 6af3817..4fc3f55 100644 --- a/projects/website/src/components/input/search-player.tsx +++ b/projects/website/src/components/input/search-player.tsx @@ -66,11 +66,11 @@ export default function SearchPlayer() { {results !== undefined && (
- {results?.map(player => { + {results?.map((player, index) => { return ( diff --git a/projects/website/src/components/leaderboard/leaderboard-info.tsx b/projects/website/src/components/leaderboard/leaderboard-info.tsx index a909de0..5924e7a 100644 --- a/projects/website/src/components/leaderboard/leaderboard-info.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-info.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import { LeaderboardSongStarCount } from "@/components/leaderboard/leaderboard-song-star-count"; import ScoreButtons from "@/components/score/score-buttons"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "../../../../common/src/model/beatsaver/map"; import { getBeatSaverMapperProfileUrl } from "@ssr/common/utils/beatsaver.util"; import FallbackLink from "@/components/fallback-link"; import { formatNumber } from "@ssr/common/utils/number-utils"; diff --git a/projects/website/src/components/leaderboard/leaderboard-scores.tsx b/projects/website/src/components/leaderboard/leaderboard-scores.tsx index ce3da37..3ce6fd4 100644 --- a/projects/website/src/components/leaderboard/leaderboard-scores.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-scores.tsx @@ -8,7 +8,7 @@ import Pagination from "../input/pagination"; import LeaderboardScore from "./leaderboard-score"; import { scoreAnimation } from "@/components/score/score-animation"; import { Button } from "@/components/ui/button"; -import { getDifficultyFromRawDifficulty } from "@/common/song-utils"; +import { getDifficulty } from "@/common/song-utils"; import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; @@ -140,28 +140,26 @@ export default function LeaderboardScores({ {showDifficulties && (
- {leaderboard.difficulties.map(({ difficultyRaw, leaderboardId }) => { - const difficulty = getDifficultyFromRawDifficulty(difficultyRaw); - // todo: add support for other gamemodes? - if (difficulty.gamemode !== "Standard") { + {leaderboard.difficulties.map(({ difficulty, characteristic, leaderboardId }, index) => { + if (characteristic !== "Standard") { return null; } const isSelected = leaderboardId === selectedLeaderboardId; return ( ); })} diff --git a/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx b/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx index ac2476d..edd854e 100644 --- a/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx @@ -1,4 +1,4 @@ -import { songDifficultyToColor } from "@/common/song-utils"; +import { getDifficulty } from "@/common/song-utils"; import { StarIcon } from "@heroicons/react/24/solid"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; @@ -18,7 +18,7 @@ export function LeaderboardSongStarCount({ leaderboard }: LeaderboardSongStarCou
diff --git a/projects/website/src/components/navbar/friends-button.tsx b/projects/website/src/components/navbar/friends-button.tsx index 659f63e..ab99539 100644 --- a/projects/website/src/components/navbar/friends-button.tsx +++ b/projects/website/src/components/navbar/friends-button.tsx @@ -26,7 +26,7 @@ export default function FriendsButton() { {friends && friends.length > 0 ? ( - friends.map(friend => setOpen(false)} />) + friends.map((friend, index) => setOpen(false)} />) ) : (

You don't have any friends :(

diff --git a/projects/website/src/components/score/map-stats.tsx b/projects/website/src/components/score/map-stats.tsx new file mode 100644 index 0000000..57f79ee --- /dev/null +++ b/projects/website/src/components/score/map-stats.tsx @@ -0,0 +1,53 @@ +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; +import StatValue from "@/components/stat-value"; +import { getBeatSaverDifficulty } from "@ssr/common/utils/beatsaver.util"; +import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; +import { formatTime } from "@ssr/common/utils/time-utils"; +import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; +import { BombIcon, BrickWallIcon, DrumIcon, MusicIcon, TimerIcon } from "lucide-react"; +import { BsSpeedometer } from "react-icons/bs"; +import { CubeIcon } from "@heroicons/react/24/solid"; + +type MapAndScoreData = { + /** + * The leaderboard that the score was set on. + */ + leaderboard: ScoreSaberLeaderboard; + + /** + * The map that the score was set on. + */ + beatSaver?: BeatSaverMap; +}; + +export function MapStats({ leaderboard, beatSaver }: MapAndScoreData) { + const metadata = beatSaver?.metadata; + const mapDiff = beatSaver + ? getBeatSaverDifficulty(beatSaver, leaderboard.songHash, leaderboard.difficulty.difficulty) + : undefined; + + return ( +
+ {/* Map Stats */} + {mapDiff && metadata && ( +
+ } value={formatTime(metadata.duration)} /> + } value={formatNumberWithCommas(metadata.bpm)} /> + } value={mapDiff.nps.toFixed(2)} /> + } value={mapDiff.njs.toFixed(2)} /> + } + value={formatNumberWithCommas(mapDiff.notes)} + /> + } + value={formatNumberWithCommas(mapDiff.bombs)} + /> + } value={mapDiff.obstacles} /> +
+ )} +
+ ); +} diff --git a/projects/website/src/components/score/score-buttons.tsx b/projects/website/src/components/score/score-buttons.tsx index 2056a0b..fbc25cd 100644 --- a/projects/website/src/components/score/score-buttons.tsx +++ b/projects/website/src/components/score/score-buttons.tsx @@ -12,7 +12,7 @@ import clsx from "clsx"; import ScoreEditorButton from "@/components/score/score-editor-button"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import BeatSaberPepeLogo from "@/components/logos/beatsaber-pepe-logo"; type Props = { diff --git a/projects/website/src/components/score/score-modifiers.tsx b/projects/website/src/components/score/score-modifiers.tsx index a22832f..9a075a6 100644 --- a/projects/website/src/components/score/score-modifiers.tsx +++ b/projects/website/src/components/score/score-modifiers.tsx @@ -21,7 +21,7 @@ type ScoreModifiersProps = { export function ScoreModifiers({ score, type, limit }: ScoreModifiersProps) { const modifiers = score.modifiers; if (modifiers.length === 0) { - return

-

; + return -; } switch (type) { diff --git a/projects/website/src/components/score/score-info.tsx b/projects/website/src/components/score/score-song-info.tsx similarity index 83% rename from projects/website/src/components/score/score-info.tsx rename to projects/website/src/components/score/score-song-info.tsx index 0c4de58..0556e0a 100644 --- a/projects/website/src/components/score/score-info.tsx +++ b/projects/website/src/components/score/score-song-info.tsx @@ -2,10 +2,10 @@ import FallbackLink from "@/components/fallback-link"; import Tooltip from "@/components/tooltip"; import { StarIcon } from "@heroicons/react/24/solid"; import Image from "next/image"; -import { songDifficultyToColor } from "@/common/song-utils"; import Link from "next/link"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; +import { getDifficulty } from "@/common/song-utils"; type Props = { leaderboard: ScoreSaberLeaderboard; @@ -14,25 +14,25 @@ type Props = { export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) { const mappersProfile = - beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.author.id}` : undefined; + beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap.author.id}` : undefined; const starCount = leaderboard.stars; - const difficulty = leaderboard.difficulty; + const difficulty = leaderboard.difficulty.difficulty.replace("Plus", "+"); return (
-

Difficulty: {difficulty.difficulty}

+
+

Difficulty: {difficulty}

{starCount > 0 &&

Stars: {starCount.toFixed(2)}

} - +
} >
{starCount > 0 ? ( @@ -41,7 +41,7 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
) : ( -

{difficulty.difficulty}

+

{difficulty}

)}
diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index e906e53..3015cb2 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -3,7 +3,7 @@ import LeaderboardScores from "@/components/leaderboard/leaderboard-scores"; import { useEffect, useState } from "react"; import ScoreButtons from "./score-buttons"; -import ScoreSongInfo from "./score-info"; +import ScoreSongInfo from "./score-song-info"; import ScoreRankInfo from "./score-rank-info"; import ScoreStats from "./score-stats"; import { motion } from "framer-motion"; @@ -11,10 +11,10 @@ import { getPageFromRank } from "@ssr/common/utils/utils"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; -import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; +import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { useIsMobile } from "@/hooks/use-is-mobile"; import Card from "@/components/card"; -import StatValue from "@/components/stat-value"; +import { MapStats } from "@/components/score/map-stats"; type Props = { /** @@ -106,11 +106,7 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr className="w-full mt-2" > - {score.additionalData && ( -
- -
- )} + + {icon} {name && ( <>

{name}

diff --git a/projects/website/src/components/tooltip.tsx b/projects/website/src/components/tooltip.tsx index 5052bd1..63738c3 100644 --- a/projects/website/src/components/tooltip.tsx +++ b/projects/website/src/components/tooltip.tsx @@ -37,8 +37,7 @@ export default function Tooltip({ children, display, asChild = true, side = "top return ( - +
{display}