start backend work
This commit is contained in:
18
projects/website/src/common/browser-utils.ts
Normal file
18
projects/website/src/common/browser-utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copies the given string to the clipboard
|
||||
*
|
||||
* @param str the string to copy
|
||||
*/
|
||||
export function copyToClipboard(str: string) {
|
||||
navigator.clipboard.writeText(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current context is a worker
|
||||
*/
|
||||
export function isRunningAsWorker() {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
return navigator.constructor.name === "WorkerNavigator";
|
||||
}
|
3
projects/website/src/common/colors.ts
Normal file
3
projects/website/src/common/colors.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const Colors = {
|
||||
primary: "#0070f3",
|
||||
};
|
85
projects/website/src/common/database/database.ts
Normal file
85
projects/website/src/common/database/database.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import Dexie, { EntityTable } from "dexie";
|
||||
import { setPlayerIdCookie } from "../website-utils";
|
||||
import BeatSaverMap from "./types/beatsaver-map";
|
||||
import Settings from "./types/settings";
|
||||
|
||||
const SETTINGS_ID = "SSR"; // DO NOT CHANGE
|
||||
|
||||
export default class Database extends Dexie {
|
||||
/**
|
||||
* The settings for the website.
|
||||
*/
|
||||
settings!: EntityTable<Settings, "id">;
|
||||
|
||||
/**
|
||||
* Cached BeatSaver maps
|
||||
*/
|
||||
beatSaverMaps!: EntityTable<BeatSaverMap, "hash">;
|
||||
|
||||
constructor() {
|
||||
super("ScoreSaberReloaded");
|
||||
|
||||
// Stores
|
||||
this.version(1).stores({
|
||||
settings: "id",
|
||||
beatSaverMaps: "hash",
|
||||
});
|
||||
|
||||
// Mapped tables
|
||||
this.settings.mapToClass(Settings);
|
||||
this.beatSaverMaps.mapToClass(BeatSaverMap);
|
||||
|
||||
// Populate default settings if the table is empty
|
||||
this.on("populate", () => this.populateDefaults());
|
||||
|
||||
this.on("ready", async () => {
|
||||
const settings = await this.getSettings();
|
||||
// If the settings are not found, return
|
||||
if (settings == undefined || settings.playerId == undefined) {
|
||||
return;
|
||||
}
|
||||
setPlayerIdCookie(settings.playerId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the default settings
|
||||
*/
|
||||
async populateDefaults() {
|
||||
await this.resetSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the settings from the database
|
||||
*
|
||||
* @returns the settings
|
||||
*/
|
||||
async getSettings(): Promise<Settings | undefined> {
|
||||
return this.settings.get(SETTINGS_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the settings in the database
|
||||
*
|
||||
* @param settings the settings to set
|
||||
* @returns the settings
|
||||
*/
|
||||
async setSettings(settings: Settings) {
|
||||
return this.settings.update(SETTINGS_ID, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the settings in the database
|
||||
*/
|
||||
async resetSettings() {
|
||||
this.settings.delete(SETTINGS_ID);
|
||||
this.settings.add({
|
||||
id: SETTINGS_ID, // Fixed ID for the single settings object
|
||||
backgroundCover: "/assets/background.jpg",
|
||||
});
|
||||
|
||||
return this.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new Database();
|
42
projects/website/src/common/database/types/settings.ts
Normal file
42
projects/website/src/common/database/types/settings.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Entity } from "dexie";
|
||||
import Database from "../database";
|
||||
|
||||
/**
|
||||
* The website settings.
|
||||
*/
|
||||
export default class Settings extends Entity<Database> {
|
||||
/**
|
||||
* This is just so we can fetch the settings
|
||||
*/
|
||||
id!: string;
|
||||
|
||||
/**
|
||||
* The ID of the tracked player
|
||||
*/
|
||||
playerId?: string;
|
||||
|
||||
/**
|
||||
* The background image or color to use
|
||||
*/
|
||||
backgroundCover?: string;
|
||||
|
||||
/**
|
||||
* Sets the players id
|
||||
*
|
||||
* @param id the new player id
|
||||
*/
|
||||
public setPlayerId(id: string) {
|
||||
this.playerId = id;
|
||||
this.db.setSettings(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background image
|
||||
*
|
||||
* @param image the new background image
|
||||
*/
|
||||
public setBackgroundImage(image: string) {
|
||||
this.backgroundCover = image;
|
||||
this.db.setSettings(this);
|
||||
}
|
||||
}
|
24
projects/website/src/common/image-utils.ts
Normal file
24
projects/website/src/common/image-utils.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { config } from "../../config";
|
||||
import { cache } from "react";
|
||||
|
||||
/**
|
||||
* Proxies all non-localhost images to make them load faster.
|
||||
*
|
||||
* @param originalUrl the original image url
|
||||
* @returns the new image url
|
||||
*/
|
||||
export function getImageUrl(originalUrl: string) {
|
||||
return `${!config.siteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average color of an image
|
||||
*
|
||||
* @param src the image url
|
||||
* @returns the average color
|
||||
*/
|
||||
export const getAverageColor = cache(async (src: string) => {
|
||||
return {
|
||||
hex: "#fff",
|
||||
};
|
||||
});
|
12
projects/website/src/common/mongo.ts
Normal file
12
projects/website/src/common/mongo.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as mongoose from "mongoose";
|
||||
|
||||
/**
|
||||
* Connects to the mongo database
|
||||
*/
|
||||
export async function connectMongo() {
|
||||
const connectionUri = process.env.MONGO_URI;
|
||||
if (!connectionUri) {
|
||||
throw new Error("Missing MONGO_URI");
|
||||
}
|
||||
await mongoose.connect(connectionUri);
|
||||
}
|
19
projects/website/src/common/number-utils.ts
Normal file
19
projects/website/src/common/number-utils.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Formats a number without trailing zeros.
|
||||
*
|
||||
* @param num the number to format
|
||||
* @returns the formatted number
|
||||
*/
|
||||
export function formatNumberWithCommas(num: number) {
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the pp value
|
||||
*
|
||||
* @param num the pp to format
|
||||
* @returns the formatted pp
|
||||
*/
|
||||
export function formatPp(num: number) {
|
||||
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
26
projects/website/src/common/player-utils.ts
Normal file
26
projects/website/src/common/player-utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { PlayerHistory } from "@/common/player/player-history";
|
||||
|
||||
/**
|
||||
* Gets a value from an {@link PlayerHistory}
|
||||
* based on the field
|
||||
*
|
||||
* @param history the history to get the value from
|
||||
* @param field the field to get
|
||||
*/
|
||||
export function getValueFromHistory(history: PlayerHistory, field: string): number | null {
|
||||
const keys = field.split(".");
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
let value: any = history;
|
||||
|
||||
// Navigate through the keys safely
|
||||
for (const key of keys) {
|
||||
if (value && key in value) {
|
||||
value = value[key];
|
||||
} else {
|
||||
return null; // Return null if the key doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we return a number or null
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
27
projects/website/src/common/scoresaber-utils.ts
Normal file
27
projects/website/src/common/scoresaber-utils.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Formats the ScoreSaber difficulty number
|
||||
*
|
||||
* @param diff the diffuiclity number
|
||||
*/
|
||||
export function getDifficultyFromScoreSaberDifficulty(diff: number) {
|
||||
switch (diff) {
|
||||
case 1: {
|
||||
return "Easy";
|
||||
}
|
||||
case 3: {
|
||||
return "Normal";
|
||||
}
|
||||
case 5: {
|
||||
return "Hard";
|
||||
}
|
||||
case 7: {
|
||||
return "Expert";
|
||||
}
|
||||
case 9: {
|
||||
return "Expert+";
|
||||
}
|
||||
default: {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
101
projects/website/src/common/song-utils.ts
Normal file
101
projects/website/src/common/song-utils.ts
Normal file
@ -0,0 +1,101 @@
|
||||
type Difficulty = {
|
||||
name: DifficultyName;
|
||||
gamemode?: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type DifficultyName = "Easy" | "Normal" | "Hard" | "Expert" | "Expert+";
|
||||
|
||||
const difficulties: Difficulty[] = [
|
||||
{ name: "Easy", color: "#59b0f4" },
|
||||
{ name: "Normal", color: "#59b0f4" },
|
||||
{ name: "Hard", color: "#FF6347" },
|
||||
{ name: "Expert", color: "#bf2a42" },
|
||||
{ name: "Expert+", color: "#8f48db" },
|
||||
];
|
||||
|
||||
export type ScoreBadge = {
|
||||
name: string;
|
||||
min: number | null;
|
||||
max: number | null;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const scoreBadges: ScoreBadge[] = [
|
||||
{ name: "SS+", min: 95, max: null, color: getDifficulty("Expert+")!.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 },
|
||||
{ name: "A", min: 70, max: 80, color: getDifficulty("Easy")!.color },
|
||||
{ name: "-", min: null, max: 70, color: "hsl(var(--accent))" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the color based on the accuracy provided.
|
||||
*
|
||||
* @param acc - The accuracy for the score
|
||||
* @returns The corresponding color for the accuracy.
|
||||
*/
|
||||
export function getScoreBadgeFromAccuracy(acc: number): ScoreBadge {
|
||||
// Check for SS+ first since it has no upper limit
|
||||
if (acc >= 95) {
|
||||
return scoreBadges[0]; // SS+ color
|
||||
}
|
||||
|
||||
// Iterate through the rest of the badges
|
||||
for (const badge of scoreBadges) {
|
||||
const min = badge.min ?? -Infinity; // Treat null `min` as -Infinity
|
||||
const max = badge.max ?? Infinity; // Treat null `max` as Infinity
|
||||
|
||||
// Check if the accuracy falls within the badge's range
|
||||
if (acc >= min && acc < (max === null ? Infinity : max)) {
|
||||
return badge; // Return the color of the matching badge
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback color if no badge matches (should not happen)
|
||||
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;
|
||||
}
|
9
projects/website/src/common/string-utils.ts
Normal file
9
projects/website/src/common/string-utils.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Capitalizes the first letter of a string.
|
||||
*
|
||||
* @param str the string to capitalize
|
||||
* @returns the capitalized string
|
||||
*/
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
21
projects/website/src/common/utils.ts
Normal file
21
projects/website/src/common/utils.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the url is valid
|
||||
*
|
||||
* @param url the url to validate
|
||||
* @returns true if the url is valid, false otherwise
|
||||
*/
|
||||
export function validateUrl(url: string) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
34
projects/website/src/common/website-utils.ts
Normal file
34
projects/website/src/common/website-utils.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
/**
|
||||
* Sets the player id cookie
|
||||
*
|
||||
* @param playerId the player id to set
|
||||
*/
|
||||
export function setPlayerIdCookie(playerId: string) {
|
||||
Cookies.set("playerId", playerId, { path: "/" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if we're in production
|
||||
*/
|
||||
export function isProduction() {
|
||||
return process.env.NODE_ENV === "production";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the build information
|
||||
*
|
||||
* @returns the build information
|
||||
*/
|
||||
export function getBuildInformation() {
|
||||
const buildId = process.env.NEXT_PUBLIC_BUILD_ID
|
||||
? isProduction()
|
||||
? process.env.NEXT_PUBLIC_BUILD_ID.slice(0, 7)
|
||||
: "dev"
|
||||
: "";
|
||||
const buildTime = process.env.NEXT_PUBLIC_BUILD_TIME;
|
||||
const buildTimeShort = process.env.NEXT_PUBLIC_BUILD_TIME_SHORT;
|
||||
|
||||
return { buildId, buildTime, buildTimeShort };
|
||||
}
|
16
projects/website/src/common/worker/worker.ts
Normal file
16
projects/website/src/common/worker/worker.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as Comlink from "comlink";
|
||||
import { scoresaberService } from "@/common/service/impl/scoresaber";
|
||||
|
||||
export interface WorkerApi {
|
||||
getPlayerExample: typeof getPlayerExample;
|
||||
}
|
||||
|
||||
const workerApi: WorkerApi = {
|
||||
getPlayerExample: getPlayerExample,
|
||||
};
|
||||
|
||||
async function getPlayerExample() {
|
||||
return await scoresaberService.lookupPlayer("76561198449412074");
|
||||
}
|
||||
|
||||
Comlink.expose(workerApi);
|
5
projects/website/src/common/worker/workers.ts
Normal file
5
projects/website/src/common/worker/workers.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as Comlink from "comlink";
|
||||
import { WorkerApi } from "@/common/worker/worker";
|
||||
|
||||
export const scoresaberReloadedWorker = () =>
|
||||
Comlink.wrap<WorkerApi>(new Worker(new URL("./worker.ts", import.meta.url)));
|
22
projects/website/src/common/youtube-utils.ts
Normal file
22
projects/website/src/common/youtube-utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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(" ", "+"));
|
||||
}
|
Reference in New Issue
Block a user