diff --git a/.env-example b/.env-example index ecdc481..f730995 100644 --- a/.env-example +++ b/.env-example @@ -2,5 +2,6 @@ NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY= TRIGGER_API_KEY= -TRIGGER_API_URL=https://trigger.fascinated.cc +TRIGGER_API_URL=https://trigger.example.com MONGO_URI=mongodb://127.0.0.1:27017 +SENTRY_AUTH_TOKEN= diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..dca3e4e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "semi": true, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "avoid", + "jsxSingleQuote": false, + "jsxBracketSameLine": false +} diff --git a/package.json b/package.json index 7f4ff0e..5573cbc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@trigger.dev/nextjs": "^3.0.8", "@trigger.dev/react": "^3.0.8", "@trigger.dev/sdk": "^3.0.8", + "@uidotdev/usehooks": "^2.4.1", "canvas": "3.0.0-rc2", "chart.js": "^4.4.4", "class-variance-authority": "^0.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a70c8b2..7f81884 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@trigger.dev/sdk': specifier: ^3.0.8 version: 3.0.9 + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@19.0.0-rc-3edc000d-20240926(react@19.0.0-rc-3edc000d-20240926))(react@19.0.0-rc-3edc000d-20240926) canvas: specifier: 3.0.0-rc2 version: 3.0.0-rc2 @@ -1440,6 +1443,13 @@ packages: resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} engines: {node: ^16.0.0 || >=18.0.0} + '@uidotdev/usehooks@2.4.1': + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -5106,6 +5116,11 @@ snapshots: '@typescript-eslint/types': 7.2.0 eslint-visitor-keys: 3.4.3 + '@uidotdev/usehooks@2.4.1(react-dom@19.0.0-rc-3edc000d-20240926(react@19.0.0-rc-3edc000d-20240926))(react@19.0.0-rc-3edc000d-20240926)': + dependencies: + react: 19.0.0-rc-3edc000d-20240926 + react-dom: 19.0.0-rc-3edc000d-20240926(react@19.0.0-rc-3edc000d-20240926) + '@ungap/structured-clone@1.2.0': {} '@webassemblyjs/ast@1.12.1': diff --git a/src/app/(pages)/api/player/history/route.ts b/src/app/(pages)/api/player/history/route.ts index 186e1f0..b7395da 100644 --- a/src/app/(pages)/api/player/history/route.ts +++ b/src/app/(pages)/api/player/history/route.ts @@ -8,10 +8,7 @@ export async function GET(request: NextRequest) { const playerIdCookie = request.cookies.get("playerId"); const id = request.nextUrl.searchParams.get("id"); if (id == null) { - return NextResponse.json( - { error: "Unknown player. Missing: ?id=" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Unknown player. Missing: ?id=" }, { status: 400 }); } const shouldCreatePlayer = playerIdCookie?.value === id; diff --git a/src/app/(pages)/api/player/isbeingtracked/route.ts b/src/app/(pages)/api/player/isbeingtracked/route.ts index 66efc41..d80bdd3 100644 --- a/src/app/(pages)/api/player/isbeingtracked/route.ts +++ b/src/app/(pages)/api/player/isbeingtracked/route.ts @@ -6,10 +6,7 @@ import { PlayerTrackedSince } from "@/common/player/player-tracked-since"; export async function GET(request: NextRequest) { const id = request.nextUrl.searchParams.get("id"); if (id == null) { - return NextResponse.json( - { error: "Unknown player. Missing: ?id=" }, - { status: 400 }, - ); + return NextResponse.json({ error: "Unknown player. Missing: ?id=" }, { status: 400 }); } await connectMongo(); // Connect to Mongo diff --git a/src/app/(pages)/api/proxy/route.ts b/src/app/(pages)/api/proxy/route.ts index eb3bb86..edbec74 100644 --- a/src/app/(pages)/api/proxy/route.ts +++ b/src/app/(pages)/api/proxy/route.ts @@ -21,8 +21,7 @@ export async function GET(request: NextRequest) { const { status, headers } = response; if ( !headers.has("content-type") || - (headers.has("content-type") && - !headers.get("content-type")?.includes("application/json")) + (headers.has("content-type") && !headers.get("content-type")?.includes("application/json")) ) { return NextResponse.json({ error: "We only support proxying JSON responses", @@ -42,7 +41,7 @@ export async function GET(request: NextRequest) { headers: { "Access-Control-Allow-Origin": "*", }, - }, + } ); } } diff --git a/src/app/(pages)/player/[...slug]/page.tsx b/src/app/(pages)/player/[...slug]/page.tsx index bce0973..cd657cb 100644 --- a/src/app/(pages)/player/[...slug]/page.tsx +++ b/src/app/(pages)/player/[...slug]/page.tsx @@ -31,34 +31,32 @@ type Props = { * @param fetchScores whether to fetch the scores * @returns the player data and scores */ -const getPlayerData = cache( - async ({ params }: Props, fetchScores: boolean = true) => { - const { slug } = await params; - const id = slug[0]; // The players id - const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method - const page = parseInt(slug[2]) || 1; // The page number - const search = (slug[3] as string) || ""; // The search query +const getPlayerData = cache(async ({ params }: Props, fetchScores: boolean = true) => { + const { slug } = await params; + const id = slug[0]; // The players id + const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method + const page = parseInt(slug[2]) || 1; // The page number + const search = (slug[3] as string) || ""; // The search query - const player = (await scoresaberService.lookupPlayer(id, false))?.player; - let scores: ScoreSaberPlayerScoresPageToken | undefined; - if (fetchScores) { - scores = await scoresaberService.lookupPlayerScores({ - playerId: id, - sort, - page, - search, - }); - } + const player = (await scoresaberService.lookupPlayer(id, false))?.player; + let scores: ScoreSaberPlayerScoresPageToken | undefined; + if (fetchScores) { + scores = await scoresaberService.lookupPlayerScores({ + playerId: id, + sort, + page, + search, + }); + } - return { - sort: sort, - page: page, - search: search, - player: player, - scores: scores, - }; - }, -); + return { + sort: sort, + page: page, + search: search, + player: player, + scores: scores, + }; +}); export async function generateMetadata(props: Props): Promise { const { player } = await getPlayerData(props, false); @@ -123,13 +121,7 @@ export default async function Search(props: Props) { return (
- +
); } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 9388e06..362f8e8 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -4,11 +4,7 @@ import * as Sentry from "@sentry/nextjs"; import NextError from "next/error"; import { useEffect } from "react"; -export default function GlobalError({ - error, -}: { - error: Error & { digest?: string }; -}) { +export default function GlobalError({ error }: { error: Error & { digest?: string } }) { useEffect(() => { Sentry.captureException(error); }, [error]); diff --git a/src/app/globals.css b/src/app/globals.css index 3545eb7..9ed8165 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,82 +3,82 @@ @tailwind utilities; :root { - --background: #ffffff; - --foreground: #171717; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { - .text-balance { - text-wrap: balance; - } + .text-balance { + text-wrap: balance; + } } @layer base { - :root { - --background: 0 0% 100%; - --foreground: 20 14.3% 4.1%; - --card: 0 0% 100%; - --card-foreground: 20 14.3% 4.1%; - --popover: 0 0% 100%; - --popover-foreground: 20 14.3% 4.1%; - --primary: 24 9.8% 10%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --muted: 60 4.8% 95.9%; - --muted-foreground: 25 5.3% 44.7%; - --accent: 60 4.8% 95.9%; - --accent-foreground: 24 9.8% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 90%; - --input: 20 5.9% 90%; - --ring: 20 14.3% 4.1%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } - .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 60 9.1% 97.8%; - --primary-foreground: 24 9.8% 10%; - --secondary: 12 6.5% 9.5%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 45.1%; - --ring: 24 5.7% 82.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + --secondary: 12 6.5% 9.5%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 45.1%; + --ring: 24 5.7% 82.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2027be5..77d9ff6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import "./globals.css"; import Footer from "@/components/footer"; import { PreloadResources } from "@/components/preload-resources"; import { QueryProvider } from "@/components/providers/query-provider"; @@ -10,13 +11,12 @@ import localFont from "next/font/local"; import BackgroundImage from "../components/background-image"; import DatabaseLoader from "../components/loaders/database-loader"; import NavBar from "../components/navbar/navbar"; - -import "./globals.css"; import { Colors } from "@/common/colors"; +import OfflineNetwork from "@/components/offline-network"; const siteFont = localFont({ src: "./fonts/JetBrainsMono.ttf", - weight: "100 400", + weight: "100 300", }); export const metadata: Metadata = { @@ -48,14 +48,12 @@ export const metadata: Metadata = { "Stream enhancement, Professional overlay, Easy to use overlay builder.", openGraph: { title: "Scoresaber Reloaded", - description: - "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", + description: "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", url: "https://ssr.fascinated.cc", locale: "en_US", type: "website", }, - description: - "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", + description: "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", }; export const viewport: Viewport = { @@ -75,24 +73,21 @@ export default function RootLayout({ - - - -
- -
- {children} -
-
-
-
-
-
+ + + + +
+ +
+ {children} +
+
+
+
+
+
+
diff --git a/src/common/database/types/beatsaver-map.ts b/src/common/database/types/beatsaver-map.ts index 492df24..d58eaa5 100644 --- a/src/common/database/types/beatsaver-map.ts +++ b/src/common/database/types/beatsaver-map.ts @@ -1,5 +1,5 @@ -import {BeatSaverMapToken as BSMap} from "@/common/model/token/beatsaver/beat-saver-map-token"; -import {Entity} from "dexie"; +import { BeatSaverMapToken as BSMap } from "@/common/model/token/beatsaver/beat-saver-map-token"; +import { Entity } from "dexie"; import Database from "../database"; /** diff --git a/src/common/database/types/settings.ts b/src/common/database/types/settings.ts index be700f1..51f47fd 100644 --- a/src/common/database/types/settings.ts +++ b/src/common/database/types/settings.ts @@ -1,4 +1,4 @@ -import {Entity} from "dexie"; +import { Entity } from "dexie"; import Database from "../database"; /** diff --git a/src/common/image-utils.ts b/src/common/image-utils.ts index 717b00b..fe37a86 100644 --- a/src/common/image-utils.ts +++ b/src/common/image-utils.ts @@ -1,8 +1,8 @@ -import {createCanvas, loadImage} from "canvas"; -import {config} from "../../config"; +import { createCanvas, loadImage } from "canvas"; +import { config } from "../../config"; import ky from "ky"; -import {extractColors} from "extract-colors"; -import {cache} from "react"; +import { extractColors } from "extract-colors"; +import { cache } from "react"; /** * Proxies all non-localhost images to make them load faster. @@ -46,9 +46,7 @@ export const getAverageColor = cache(async (src: string) => { // Use your extractColors function to calculate the average color const color = await extractColors({ data, width, height }); - console.log( - `Found average color of "${src}" in ${(performance.now() - before).toFixed(0)}ms`, - ); + console.log(`Found average color of "${src}" in ${(performance.now() - before).toFixed(0)}ms`); return color[2]; } catch (error) { console.error("Error while getting average color:", error); diff --git a/src/common/model/player/impl/scoresaber-player.ts b/src/common/model/player/impl/scoresaber-player.ts index 4a268d6..9122d33 100644 --- a/src/common/model/player/impl/scoresaber-player.ts +++ b/src/common/model/player/impl/scoresaber-player.ts @@ -3,11 +3,7 @@ import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-p import { PlayerHistory } from "@/common/player/player-history"; import { config } from "../../../../../config"; import ky from "ky"; -import { - formatDateMinimal, - getDaysAgoDate, - getMidnightAlignedDate, -} from "@/common/time-utils"; +import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "@/common/time-utils"; /** * A ScoreSaber player. @@ -64,16 +60,14 @@ export default interface ScoreSaberPlayer extends Player { inactive: boolean; } -export async function getScoreSaberPlayerFromToken( - token: ScoreSaberPlayerToken, -): Promise { +export async function getScoreSaberPlayerFromToken(token: ScoreSaberPlayerToken): Promise { const bio: ScoreSaberBio = { lines: token.bio?.split("\n") || [], linesStripped: token.bio?.replace(/<[^>]+>/g, "")?.split("\n") || [], }; const role = token.role == null ? undefined : (token.role as ScoreSaberRole); const badges: ScoreSaberBadge[] = - token.badges?.map((badge) => { + token.badges?.map(badge => { return { url: badge.image, description: badge.description, @@ -103,7 +97,7 @@ export async function getScoreSaberPlayerFromToken( statisticHistory = history; } catch (error) { // Fallback to ScoreSaber History if the player has no history - const playerRankHistory = token.histories.split(",").map((value) => { + const playerRankHistory = token.histories.split(",").map(value => { return parseInt(value); }); playerRankHistory.push(token.rank); @@ -124,9 +118,7 @@ export async function getScoreSaberPlayerFromToken( .sort((a, b) => Date.parse(b[0]) - Date.parse(a[0])) .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); - const yesterdayDate = formatDateMinimal( - getMidnightAlignedDate(getDaysAgoDate(1)), - ); + const yesterdayDate = formatDateMinimal(getMidnightAlignedDate(getDaysAgoDate(1))); const todayStats = statisticHistory[todayDate]; const yesterdayStats = statisticHistory[yesterdayDate]; const hasChange = !!(todayStats && yesterdayStats); diff --git a/src/common/model/player/player.ts b/src/common/model/player/player.ts index 36a2994..0c13ac0 100644 --- a/src/common/model/player/player.ts +++ b/src/common/model/player/player.ts @@ -43,7 +43,7 @@ export default class Player { country: string, rank: number, countryRank: number, - joinedDate: Date, + joinedDate: Date ) { this.id = id; this.name = name; diff --git a/src/common/model/score/impl/scoresaber-score.ts b/src/common/model/score/impl/scoresaber-score.ts index d293b35..c0504a8 100644 --- a/src/common/model/score/impl/scoresaber-score.ts +++ b/src/common/model/score/impl/scoresaber-score.ts @@ -12,19 +12,9 @@ export default class ScoreSaberScore extends Score { misses: number, badCuts: number, fullCombo: boolean, - timestamp: Date, + timestamp: Date ) { - super( - score, - weight, - rank, - worth, - modifiers, - misses, - badCuts, - fullCombo, - timestamp, - ); + super(score, weight, rank, worth, modifiers, misses, badCuts, fullCombo, timestamp); } /** @@ -33,7 +23,7 @@ export default class ScoreSaberScore extends Score { * @param token the token to convert */ public static fromToken(token: ScoreSaberScoreToken): ScoreSaberScore { - const modifiers: Modifier[] = token.modifiers.split(",").map((mod) => { + const modifiers: Modifier[] = token.modifiers.split(",").map(mod => { mod = mod.toUpperCase(); const modifier = Modifier[mod as keyof typeof Modifier]; if (modifier === undefined) { @@ -51,7 +41,7 @@ export default class ScoreSaberScore extends Score { token.missedNotes, token.badCuts, token.fullCombo, - new Date(token.timeSet), + new Date(token.timeSet) ); } } diff --git a/src/common/model/score/score.ts b/src/common/model/score/score.ts index 8c04764..77c787a 100644 --- a/src/common/model/score/score.ts +++ b/src/common/model/score/score.ts @@ -65,7 +65,7 @@ export default class Score { misses: number, badCuts: number, fullCombo: boolean, - timestamp: Date, + timestamp: Date ) { this._score = score; this._weight = weight; diff --git a/src/common/player-utils.ts b/src/common/player-utils.ts index 6f7dd06..34024f2 100644 --- a/src/common/player-utils.ts +++ b/src/common/player-utils.ts @@ -16,7 +16,7 @@ const INACTIVE_CHECK_AGAIN_TIME = 3 * 24 * 60 * 60 * 1000; // 3 days */ export function sortPlayerHistory(history: Map) { return Array.from(history.entries()).sort( - (a, b) => Date.parse(b[0]) - Date.parse(a[0]), // Sort in descending order + (a, b) => Date.parse(b[0]) - Date.parse(a[0]) // Sort in descending order ); } @@ -31,10 +31,10 @@ export function sortPlayerHistory(history: Map) { export async function seedPlayerHistory( foundPlayer: IPlayer, player: ScoreSaberPlayer, - rawPlayer: ScoreSaberPlayerToken, + rawPlayer: ScoreSaberPlayerToken ): Promise> { // Loop through rankHistory in reverse, from current day backwards - const playerRankHistory = rawPlayer.histories.split(",").map((value) => { + const playerRankHistory = rawPlayer.histories.split(",").map(value => { return parseInt(value); }); playerRankHistory.push(player.rank); @@ -63,34 +63,23 @@ export async function seedPlayerHistory( * @param dateToday the date to use * @param foundPlayer the player to track */ -export async function trackScoreSaberPlayer( - dateToday: Date, - foundPlayer: IPlayer, - io?: IO, -) { +export async function trackScoreSaberPlayer(dateToday: Date, foundPlayer: IPlayer, io?: IO) { io && (await io.logger.info(`Updating statistics for ${foundPlayer.id}...`)); // Check if the player is inactive and if we check their inactive status again if ( foundPlayer.rawPlayer && foundPlayer.rawPlayer.inactive && - Date.now() - foundPlayer.getLastTracked().getTime() > - INACTIVE_CHECK_AGAIN_TIME + Date.now() - foundPlayer.getLastTracked().getTime() > INACTIVE_CHECK_AGAIN_TIME ) { - io && - (await io.logger.warn( - `Player ${foundPlayer.id} is inactive, skipping...`, - )); + io && (await io.logger.warn(`Player ${foundPlayer.id} is inactive, skipping...`)); return; } // Lookup player data from the ScoreSaber service const response = await scoresaberService.lookupPlayer(foundPlayer.id, true); if (response == undefined) { - io && - (await io.logger.warn( - `Player ${foundPlayer.id} not found on ScoreSaber`, - )); + io && (await io.logger.warn(`Player ${foundPlayer.id} not found on ScoreSaber`)); return; } const { player, rawPlayer } = response; diff --git a/src/common/schema/player-schema.ts b/src/common/schema/player-schema.ts index bd149c8..0210e79 100644 --- a/src/common/schema/player-schema.ts +++ b/src/common/schema/player-schema.ts @@ -1,10 +1,6 @@ import mongoose, { Document, Schema } from "mongoose"; import { PlayerHistory } from "@/common/player/player-history"; -import { - formatDateMinimal, - getDaysAgo, - getMidnightAlignedDate, -} from "@/common/time-utils"; +import { formatDateMinimal, getDaysAgo, getMidnightAlignedDate } from "@/common/time-utils"; import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import { sortPlayerHistory } from "@/common/player-utils"; @@ -95,11 +91,7 @@ PlayerSchema.methods.getLastTracked = function (): Date { }; PlayerSchema.methods.getHistoryByDate = function (date: Date): PlayerHistory { - return ( - this.statisticHistory.get( - formatDateMinimal(getMidnightAlignedDate(date)), - ) || {} - ); + return this.statisticHistory.get(formatDateMinimal(getMidnightAlignedDate(date))) || {}; }; PlayerSchema.methods.getHistoryPrevious = function (amount: number): { @@ -118,33 +110,21 @@ PlayerSchema.methods.getHistoryPrevious = function (amount: number): { return toReturn; }; -PlayerSchema.methods.getStatisticHistory = function (): Map< - Date, - PlayerHistory -> { +PlayerSchema.methods.getStatisticHistory = function (): Map { if (!this.statisticHistory) { this.statisticHistory = new Map(); } return this.statisticHistory; }; -PlayerSchema.methods.setStatisticHistory = function ( - date: Date, - data: PlayerHistory, -): void { +PlayerSchema.methods.setStatisticHistory = function (date: Date, data: PlayerHistory): void { if (!this.statisticHistory) { this.statisticHistory = new Map(); } - return this.statisticHistory.set( - formatDateMinimal(getMidnightAlignedDate(date)), - data, - ); + return this.statisticHistory.set(formatDateMinimal(getMidnightAlignedDate(date)), data); }; -PlayerSchema.methods.sortStatisticHistory = function (): Map< - Date, - PlayerHistory -> { +PlayerSchema.methods.sortStatisticHistory = function (): Map { if (!this.statisticHistory) { this.statisticHistory = new Map(); } @@ -152,18 +132,14 @@ PlayerSchema.methods.sortStatisticHistory = function (): Map< // Sort the player's history this.statisticHistory = new Map( Array.from(this.statisticHistory.entries() as [string, PlayerHistory][]) - .sort( - (a: [string, PlayerHistory], b: [string, PlayerHistory]) => - Date.parse(b[0]) - Date.parse(a[0]), - ) + .sort((a: [string, PlayerHistory], b: [string, PlayerHistory]) => Date.parse(b[0]) - Date.parse(a[0])) // Convert the date strings back to Date objects for the resulting Map - .map(([date, history]) => [formatDateMinimal(new Date(date)), history]), + .map(([date, history]) => [formatDateMinimal(new Date(date)), history]) ); return this.statisticHistory; }; // Mongoose Model for Player -const PlayerModel = - mongoose.models.Player || mongoose.model("Player", PlayerSchema); +const PlayerModel = mongoose.models.Player || mongoose.model("Player", PlayerSchema); export { PlayerModel }; diff --git a/src/common/service/impl/beatsaver.ts b/src/common/service/impl/beatsaver.ts index b98fb82..4890f71 100644 --- a/src/common/service/impl/beatsaver.ts +++ b/src/common/service/impl/beatsaver.ts @@ -18,26 +18,18 @@ class BeatSaverService extends Service { * @param useProxy whether to use the proxy or not * @returns the map that match the query, or undefined if no map were found */ - async lookupMap( - query: string, - useProxy = true, - ): Promise { + async lookupMap(query: string, useProxy = true): Promise { const before = performance.now(); this.log(`Looking up map "${query}"...`); let map = await db.beatSaverMaps.get(query); // The map is cached if (map != undefined) { - this.log( - `Found cached map "${query}" in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found cached map "${query}" in ${(performance.now() - before).toFixed(0)}ms`); return map; } - const response = await this.fetch( - useProxy, - LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query), - ); + const response = await this.fetch(useProxy, LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query)); // Map not found if (response == undefined) { return undefined; @@ -55,9 +47,7 @@ class BeatSaverService extends Service { fullData: response, }); map = await db.beatSaverMaps.get(query); - this.log( - `Found map "${query}" in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found map "${query}" in ${(performance.now() - before).toFixed(0)}ms`); return map; } } diff --git a/src/common/service/impl/scoresaber.ts b/src/common/service/impl/scoresaber.ts index 92ba85f..b40e68d 100644 --- a/src/common/service/impl/scoresaber.ts +++ b/src/common/service/impl/scoresaber.ts @@ -5,9 +5,7 @@ import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-p import { ScoreSaberPlayersPageToken } from "@/common/model/token/scoresaber/score-saber-players-page-token"; import { ScoreSort } from "../../model/score/score-sort"; import Service from "../service"; -import ScoreSaberPlayer, { - getScoreSaberPlayerFromToken, -} from "@/common/model/player/impl/scoresaber-player"; +import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@/common/model/player/impl/scoresaber-player"; const API_BASE = "https://scoresaber.com/api"; const SEARCH_PLAYERS_ENDPOINT = `${API_BASE}/players?search=:query`; @@ -29,15 +27,12 @@ class ScoreSaberService extends Service { * @param useProxy whether to use the proxy or not * @returns the players that match the query, or undefined if no players were found */ - async searchPlayers( - query: string, - useProxy = true, - ): Promise { + async searchPlayers(query: string, useProxy = true): Promise { const before = performance.now(); this.log(`Searching for players matching "${query}"...`); const results = await this.fetch( useProxy, - SEARCH_PLAYERS_ENDPOINT.replace(":query", query), + SEARCH_PLAYERS_ENDPOINT.replace(":query", query) ); if (results === undefined) { return undefined; @@ -46,9 +41,7 @@ class ScoreSaberService extends Service { return undefined; } results.players.sort((a, b) => a.rank - b.rank); - this.log( - `Found ${results.players.length} players in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found ${results.players.length} players in ${(performance.now() - before).toFixed(0)}ms`); return results; } @@ -61,7 +54,7 @@ class ScoreSaberService extends Service { */ async lookupPlayer( playerId: string, - useProxy = true, + useProxy = true ): Promise< | { player: ScoreSaberPlayer; @@ -71,16 +64,11 @@ class ScoreSaberService extends Service { > { const before = performance.now(); this.log(`Looking up player "${playerId}"...`); - const token = await this.fetch( - useProxy, - LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId), - ); + const token = await this.fetch(useProxy, LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); if (token === undefined) { return undefined; } - this.log( - `Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`); return { player: await getScoreSaberPlayerFromToken(token), rawPlayer: token, @@ -94,22 +82,17 @@ class ScoreSaberService extends Service { * @param useProxy whether to use the proxy or not * @returns the players on the page, or undefined */ - async lookupPlayers( - page: number, - useProxy = true, - ): Promise { + async lookupPlayers(page: number, useProxy = true): Promise { const before = performance.now(); this.log(`Looking up players on page "${page}"...`); const response = await this.fetch( useProxy, - LOOKUP_PLAYERS_ENDPOINT.replace(":page", page.toString()), + LOOKUP_PLAYERS_ENDPOINT.replace(":page", page.toString()) ); if (response === undefined) { return undefined; } - this.log( - `Found ${response.players.length} players in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found ${response.players.length} players in ${(performance.now() - before).toFixed(0)}ms`); return response; } @@ -124,25 +107,18 @@ class ScoreSaberService extends Service { async lookupPlayersByCountry( page: number, country: string, - useProxy = true, + useProxy = true ): Promise { const before = performance.now(); - this.log( - `Looking up players on page "${page}" for country "${country}"...`, - ); + this.log(`Looking up players on page "${page}" for country "${country}"...`); const response = await this.fetch( useProxy, - LOOKUP_PLAYERS_BY_COUNTRY_ENDPOINT.replace( - ":page", - page.toString(), - ).replace(":country", country), + LOOKUP_PLAYERS_BY_COUNTRY_ENDPOINT.replace(":page", page.toString()).replace(":country", country) ); if (response === undefined) { return undefined; } - this.log( - `Found ${response.players.length} players in ${(performance.now() - before).toFixed(0)}ms`, - ); + this.log(`Found ${response.players.length} players in ${(performance.now() - before).toFixed(0)}ms`); return response; } @@ -171,24 +147,20 @@ class ScoreSaberService extends Service { }): Promise { const before = performance.now(); this.log( - `Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${ - search ? `, search "${search}"` : "" - }...`, + `Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${search ? `, search "${search}"` : ""}...` ); const response = await this.fetch( useProxy, LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId) .replace(":limit", 8 + "") .replace(":sort", sort) - .replace(":page", page + "") + (search ? `&search=${search}` : ""), + .replace(":page", page + "") + (search ? `&search=${search}` : "") ); if (response === undefined) { return undefined; } this.log( - `Found ${response.playerScores.length} scores for player "${playerId}" in ${( - performance.now() - before - ).toFixed(0)}ms`, + `Found ${response.playerScores.length} scores for player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms` ); return response; } @@ -205,26 +177,19 @@ class ScoreSaberService extends Service { async lookupLeaderboardScores( leaderboardId: string, page: number, - useProxy = true, + useProxy = true ): Promise { const before = performance.now(); - this.log( - `Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`, - ); + this.log(`Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`); const response = await this.fetch( useProxy, - LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace( - ":page", - page.toString(), - ), + LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace(":page", page.toString()) ); if (response === undefined) { return undefined; } this.log( - `Found ${response.scores.length} scores for leaderboard "${leaderboardId}" in ${( - performance.now() - before - ).toFixed(0)}ms`, + `Found ${response.scores.length} scores for leaderboard "${leaderboardId}" in ${(performance.now() - before).toFixed(0)}ms` ); return response; } diff --git a/src/common/service/service.ts b/src/common/service/service.ts index 88d0e0f..b71601a 100644 --- a/src/common/service/service.ts +++ b/src/common/service/service.ts @@ -17,9 +17,7 @@ export default class Service { * @param data the data to log */ public log(data: unknown) { - console.log( - `[${isRunningAsWorker() ? "Worker - " : ""}${this.name}]: ${data}`, - ); + console.log(`[${isRunningAsWorker() ? "Worker - " : ""}${this.name}]: ${data}`); } /** @@ -41,10 +39,7 @@ export default class Service { * @param url the url to fetch * @returns the fetched data */ - public async fetch( - useProxy: boolean, - url: string, - ): Promise { + public async fetch(useProxy: boolean, url: string): Promise { try { return await ky .get(this.buildRequestUrl(useProxy, url), { diff --git a/src/common/time-utils.ts b/src/common/time-utils.ts index f46fd0a..0a5f929 100644 --- a/src/common/time-utils.ts +++ b/src/common/time-utils.ts @@ -20,10 +20,7 @@ export function timeAgo(input: Date | number) { for (const key in ranges) { if (ranges[key] < Math.abs(secondsElapsed)) { const delta = secondsElapsed / ranges[key]; - return formatter.format( - Math.round(delta), - key as Intl.RelativeTimeFormatUnit, - ); + return formatter.format(Math.round(delta), key as Intl.RelativeTimeFormatUnit); } } } @@ -48,9 +45,7 @@ export function formatDateMinimal(date: Date) { * @param date the date */ export function getMidnightAlignedDate(date: Date) { - return new Date( - Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), - ); + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); } /** diff --git a/src/common/youtube-utils.ts b/src/common/youtube-utils.ts index 4e1d1f2..3c5203c 100644 --- a/src/common/youtube-utils.ts +++ b/src/common/youtube-utils.ts @@ -6,11 +6,7 @@ * @param author the author of the song * @returns the YouTube link for the song */ -export function songNameToYouTubeLink( - name: string, - songSubName: string, - author: string, -) { +export function songNameToYouTubeLink(name: string, songSubName: string, author: string) { const baseUrl = "https://www.youtube.com/results?search_query="; let query = ""; if (name) { diff --git a/src/components/background-image.tsx b/src/components/background-image.tsx index e0b41c0..644dc70 100644 --- a/src/components/background-image.tsx +++ b/src/components/background-image.tsx @@ -2,17 +2,14 @@ import { useLiveQuery } from "dexie-react-hooks"; import { config } from "../../config"; -import { getImageUrl } from "../common/image-utils"; +import { getImageUrl } from "@/common/image-utils"; import useDatabase from "../hooks/use-database"; export default function BackgroundImage() { const database = useDatabase(); const settings = useLiveQuery(() => database.getSettings()); - if ( - settings?.backgroundImage == undefined || - settings?.backgroundImage == "" - ) { + if (settings == undefined || settings?.backgroundImage == undefined || settings?.backgroundImage == "") { return null; // Don't render anything if the background image is not set } diff --git a/src/components/card.tsx b/src/components/card.tsx index d20060b..39bfb79 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -6,14 +6,5 @@ type Props = { }; export default function Card({ children, className }: Props) { - return ( -
- {children} -
- ); + return
{children}
; } diff --git a/src/components/chart/customized-axis-tick.tsx b/src/components/chart/customized-axis-tick.tsx index afaa488..5c27a17 100644 --- a/src/components/chart/customized-axis-tick.tsx +++ b/src/components/chart/customized-axis-tick.tsx @@ -12,14 +12,7 @@ export const CustomizedAxisTick = ({ }) => { return ( - + {payload.value} diff --git a/src/components/input/pagination.tsx b/src/components/input/pagination.tsx index 5e563dc..f03f400 100644 --- a/src/components/input/pagination.tsx +++ b/src/components/input/pagination.tsx @@ -23,10 +23,7 @@ type PaginationItemWrapperProps = { children: React.ReactNode; }; -function PaginationItemWrapper({ - isLoadingPage, - children, -}: PaginationItemWrapperProps) { +function PaginationItemWrapper({ isLoadingPage, children }: PaginationItemWrapperProps) { return ( void; }; -export default function Pagination({ - mobilePagination, - page, - totalPages, - loadingPage, - onPageChange, -}: Props) { +export default function Pagination({ mobilePagination, page, totalPages, loadingPage, onPageChange }: Props) { totalPages = Math.round(totalPages); const isLoading = loadingPage !== undefined; const [currentPage, setCurrentPage] = useState(page); @@ -81,12 +72,7 @@ export default function Pagination({ }, [page]); const handlePageChange = (newPage: number) => { - if ( - newPage < 1 || - newPage > totalPages || - newPage == currentPage || - isLoading - ) { + if (newPage < 1 || newPage > totalPages || newPage == currentPage || isLoading) { return; } @@ -109,20 +95,15 @@ export default function Pagination({ pageNumbers.push( <> - handlePageChange(1)}> - 1 - + handlePageChange(1)}>1 {/* Only show ellipsis if more than 2 pages from the start */} {startPage > 2 && ( - + )} - , + ); } @@ -130,17 +111,10 @@ export default function Pagination({ for (let i = startPage; i <= endPage; i++) { pageNumbers.push( - handlePageChange(i)} - > - {loadingPage === i ? ( - - ) : ( - i - )} + handlePageChange(i)}> + {loadingPage === i ? : i} - , + ); } @@ -152,31 +126,22 @@ export default function Pagination({ {/* Previous button for mobile and desktop */} - handlePageChange(currentPage - 1)} - /> + handlePageChange(currentPage - 1)} /> {renderPageNumbers()} {/* For desktop, show ellipsis and link to the last page */} - {!mobilePagination && - currentPage < totalPages && - totalPages - currentPage > 2 && ( - <> - - - - - handlePageChange(totalPages)}> - {totalPages} - - - - )} + {!mobilePagination && currentPage < totalPages && totalPages - currentPage > 2 && ( + <> + + + + + handlePageChange(totalPages)}>{totalPages} + + + )} {/* Next button for mobile and desktop */} diff --git a/src/components/input/search-player.tsx b/src/components/input/search-player.tsx index c976343..8df7d76 100644 --- a/src/components/input/search-player.tsx +++ b/src/components/input/search-player.tsx @@ -40,10 +40,7 @@ export default function SearchPlayer() {
{/* Search */}
- + Username - + )} @@ -73,7 +66,7 @@ export default function SearchPlayer() { {results !== undefined && (
- {results?.map((player) => { + {results?.map(player => { return (

{player.name}

-

- #{formatNumberWithCommas(player.rank)} -

+

#{formatNumberWithCommas(player.rank)}

); diff --git a/src/components/leaderboard/leaderboard-score-stats.tsx b/src/components/leaderboard/leaderboard-score-stats.tsx index b45367d..565dd81 100644 --- a/src/components/leaderboard/leaderboard-score-stats.tsx +++ b/src/components/leaderboard/leaderboard-score-stats.tsx @@ -9,7 +9,7 @@ type Badge = { name: string; create: ( score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, + leaderboard: ScoreSaberLeaderboardToken ) => string | React.ReactNode | undefined; }; @@ -26,10 +26,7 @@ const badges: Badge[] = [ }, { name: "Accuracy", - create: ( - score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, - ) => { + create: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => { const acc = (score.baseScore / leaderboard.maxScore) * 100; return `${acc.toFixed(2)}%`; }, @@ -41,16 +38,8 @@ const badges: Badge[] = [ return ( <> -

- {fullCombo ? ( - FC - ) : ( - formatNumberWithCommas(score.missedNotes) - )} -

- +

{fullCombo ? FC : formatNumberWithCommas(score.missedNotes)}

+ ); }, diff --git a/src/components/leaderboard/leaderboard-scores.tsx b/src/components/leaderboard/leaderboard-scores.tsx index e7787df..5a1d73c 100644 --- a/src/components/leaderboard/leaderboard-scores.tsx +++ b/src/components/leaderboard/leaderboard-scores.tsx @@ -19,9 +19,7 @@ export default function LeaderboardScores({ leaderboard }: Props) { const { width } = useWindowDimensions(); const [currentPage, setCurrentPage] = useState(1); - const [currentScores, setCurrentScores] = useState< - ScoreSaberLeaderboardScoresPageToken | undefined - >(); + const [currentScores, setCurrentScores] = useState(); const { data: scores, @@ -30,11 +28,7 @@ export default function LeaderboardScores({ leaderboard }: Props) { refetch, } = useQuery({ queryKey: ["playerScores", leaderboard.id, currentPage], - queryFn: () => - scoresaberService.lookupLeaderboardScores( - leaderboard.id + "", - currentPage, - ), + queryFn: () => scoresaberService.lookupLeaderboardScores(leaderboard.id + "", currentPage), staleTime: 30 * 1000, // Cache data for 30 seconds }); @@ -53,35 +47,23 @@ export default function LeaderboardScores({ leaderboard }: Props) { } return ( - +
{isError &&

Oopsies! Something went wrong.

} - {currentScores.scores.length === 0 && ( -

No scores found. Invalid Page?

- )} + {currentScores.scores.length === 0 &&

No scores found. Invalid Page?

}
{currentScores.scores.map((playerScore, index) => ( - + ))}
diff --git a/src/components/loaders/database-loader.tsx b/src/components/loaders/database-loader.tsx index 5a65387..a47b2e1 100644 --- a/src/components/loaders/database-loader.tsx +++ b/src/components/loaders/database-loader.tsx @@ -3,6 +3,7 @@ import { createContext, useEffect, useState } from "react"; import Database, { db } from "../../common/database/database"; import FullscreenLoader from "./fullscreen-loader"; +import { useToast } from "@/hooks/use-toast"; /** * The context for the database. This is used to access the database from within the app. @@ -14,21 +15,25 @@ type Props = { }; export default function DatabaseLoader({ children }: Props) { + const { toast } = useToast(); const [database, setDatabase] = useState(undefined); useEffect(() => { const before = performance.now(); setDatabase(db); console.log(`Loaded database in ${performance.now() - before}ms`); + + db.on("ready", err => { + toast({ + title: "Database loaded", + description: "The database was loaded successfully.", + }); + }); }, []); return ( - {database == undefined ? ( - - ) : ( - children - )} + {database == undefined ? : children} ); } diff --git a/src/components/logos/beatsaver-logo.tsx b/src/components/logos/beatsaver-logo.tsx index fe57b5a..7c8dfef 100644 --- a/src/components/logos/beatsaver-logo.tsx +++ b/src/components/logos/beatsaver-logo.tsx @@ -3,10 +3,7 @@ type BeatSaverLogoProps = { className?: string; }; -export default function BeatSaverLogo({ - size = 32, - className, -}: BeatSaverLogoProps) { +export default function BeatSaverLogo({ size = 32, className }: BeatSaverLogoProps) { return ( - + diff --git a/src/components/logos/scoresaber-logo.tsx b/src/components/logos/scoresaber-logo.tsx index 2fad0d5..d5f6802 100644 --- a/src/components/logos/scoresaber-logo.tsx +++ b/src/components/logos/scoresaber-logo.tsx @@ -2,12 +2,6 @@ import Image from "next/image"; export default function ScoreSaberLogo() { return ( - + ); } diff --git a/src/components/logos/youtube-logo.tsx b/src/components/logos/youtube-logo.tsx index 384b76f..0576734 100644 --- a/src/components/logos/youtube-logo.tsx +++ b/src/components/logos/youtube-logo.tsx @@ -3,10 +3,7 @@ type YouTubeLogoProps = { className?: string; }; -export default function YouTubeLogo({ - size = 32, - className, -}: YouTubeLogoProps) { +export default function YouTubeLogo({ size = 32, className }: YouTubeLogoProps) { return ( + + ) : ( + children + ); +} diff --git a/src/components/player/player-badges.tsx b/src/components/player/player-badges.tsx index 75d36b2..9522308 100644 --- a/src/components/player/player-badges.tsx +++ b/src/components/player/player-badges.tsx @@ -11,22 +11,9 @@ export default function PlayerBadges({ player }: Props) {
{player.badges?.map((badge, index) => { return ( - - {badge.description} -

- } - > + {badge.description}

}>
- {badge.description} + {badge.description}
); diff --git a/src/components/player/player-header.tsx b/src/components/player/player-header.tsx index 23097b8..8f76bcc 100644 --- a/src/components/player/player-header.tsx +++ b/src/components/player/player-header.tsx @@ -1,13 +1,13 @@ -import {formatNumberWithCommas, formatPp} from "@/common/number-utils"; -import {GlobeAmericasIcon} from "@heroicons/react/24/solid"; +import { formatNumberWithCommas, formatPp } from "@/common/number-utils"; +import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import Card from "../card"; import CountryFlag from "../country-flag"; -import {Avatar, AvatarImage} from "../ui/avatar"; +import { Avatar, AvatarImage } from "../ui/avatar"; import ClaimProfile from "./claim-profile"; import PlayerStats from "./player-stats"; import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import Tooltip from "@/components/tooltip"; -import {ReactElement} from "react"; +import { ReactElement } from "react"; import PlayerTrackedStatus from "@/components/player/player-tracked-status"; /** @@ -17,18 +17,12 @@ import PlayerTrackedStatus from "@/components/player/player-tracked-status"; * @param tooltip the tooltip to display * @param format the function to format the value */ -const renderChange = ( - change: number, - tooltip: ReactElement, - format?: (value: number) => string, -) => { +const renderChange = (change: number, tooltip: ReactElement, format?: (value: number) => string) => { format = format ?? formatNumberWithCommas; return ( -

0 ? "text-green-400" : "text-red-400"}`} - > +

0 ? "text-green-400" : "text-red-400"}`}> {change > 0 ? "+" : ""} {format(change)}

@@ -49,11 +43,7 @@ const playerData = [ return (

#{formatNumberWithCommas(player.rank)}

- {rankChange != 0 && - renderChange( - rankChange, -

The change in your rank compared to yesterday

, - )} + {rankChange != 0 && renderChange(rankChange,

The change in your rank compared to yesterday

)}
); }, @@ -70,11 +60,7 @@ const playerData = [ return (

#{formatNumberWithCommas(player.countryRank)}

- {rankChange != 0 && - renderChange( - rankChange, -

The change in your rank compared to yesterday

, - )} + {rankChange != 0 && renderChange(rankChange,

The change in your rank compared to yesterday

)}
); }, @@ -89,13 +75,9 @@ const playerData = [

{formatPp(player.pp)}pp

{ppChange != 0 && - renderChange( - ppChange, -

The change in your pp compared to yesterday

, - (number) => { - return `${formatPp(number)}pp`; - }, - )} + renderChange(ppChange,

The change in your pp compared to yesterday

, number => { + return `${formatPp(number)}pp`; + })}
); }, @@ -111,10 +93,7 @@ export default function PlayerHeader({ player }: Props) {
- +
@@ -124,20 +103,13 @@ export default function PlayerHeader({ player }: Props) {
- {player.inactive && ( -

Inactive Account

- )} - {player.banned && ( -

Banned Account

- )} + {player.inactive &&

Inactive Account

} + {player.banned &&

Banned Account

}
{playerData.map((subName, index) => { // Check if the player is inactive or banned and if the data should be shown - if ( - !subName.showWhenInactiveOrBanned && - (player.inactive || player.banned) - ) { + if (!subName.showWhenInactiveOrBanned && (player.inactive || player.banned)) { return null; } diff --git a/src/components/player/player-rank-chart.tsx b/src/components/player/player-rank-chart.tsx index 020fc5c..448ed03 100644 --- a/src/components/player/player-rank-chart.tsx +++ b/src/components/player/player-rank-chart.tsx @@ -2,30 +2,13 @@ "use client"; import { formatNumberWithCommas } from "@/common/number-utils"; -import { - CategoryScale, - Chart, - Legend, - LinearScale, - LineElement, - PointElement, - Title, - Tooltip, -} from "chart.js"; +import { CategoryScale, Chart, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from "chart.js"; import { Line } from "react-chartjs-2"; import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import { getDaysAgo, parseDate } from "@/common/time-utils"; import { useIsMobile } from "@/hooks/use-is-mobile"; -Chart.register( - LinearScale, - CategoryScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, -); +Chart.register(LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend); type AxisPosition = "left" | "right"; @@ -71,7 +54,7 @@ const generateAxis = ( reverse: boolean, display: boolean, position: AxisPosition, - displayName: string, + displayName: string ): Axis => ({ id, position, @@ -99,12 +82,7 @@ const generateAxis = ( * @param borderColor the border color of the dataset * @param yAxisID the ID of the y-axis */ -const generateDataset = ( - label: string, - data: (number | null)[], - borderColor: string, - yAxisID: string, -): Dataset => ({ +const generateDataset = (label: string, data: (number | null)[], borderColor: string, yAxisID: string): Dataset => ({ label, data, borderColor, @@ -155,8 +133,7 @@ const datasetConfig: DatasetConfig[] = [ displayName: "Country Rank", position: "left", }, - labelFormatter: (value: number) => - `Country Rank #${formatNumberWithCommas(value)}`, + labelFormatter: (value: number) => `Country Rank #${formatNumberWithCommas(value)}`, }, { title: "PP", @@ -181,10 +158,7 @@ type Props = { export default function PlayerRankChart({ player }: Props) { const isMobile = useIsMobile(); - if ( - !player.statisticHistory || - Object.keys(player.statisticHistory).length === 0 - ) { + if (!player.statisticHistory || Object.keys(player.statisticHistory).length === 0) { return (

Unable to load player rank chart, missing data...

@@ -200,7 +174,7 @@ export default function PlayerRankChart({ player }: Props) { }; const statisticEntries = Object.entries(player.statisticHistory).sort( - ([a], [b]) => parseDate(a).getTime() - parseDate(b).getTime(), + ([a], [b]) => parseDate(a).getTime() - parseDate(b).getTime() ); let previousDate: Date | null = null; @@ -211,18 +185,11 @@ export default function PlayerRankChart({ player }: Props) { // Insert nulls for missing days if (previousDate) { - const diffDays = Math.floor( - (currentDate.getTime() - previousDate.getTime()) / - (1000 * 60 * 60 * 24), - ); + const diffDays = Math.floor((currentDate.getTime() - previousDate.getTime()) / (1000 * 60 * 60 * 24)); for (let i = 1; i < diffDays; i++) { - labels.push( - `${getDaysAgo( - new Date(currentDate.getTime() - i * 24 * 60 * 60 * 1000), - )} days ago`, - ); - datasetConfig.forEach((config) => { + labels.push(`${getDaysAgo(new Date(currentDate.getTime() - i * 24 * 60 * 60 * 1000))} days ago`); + datasetConfig.forEach(config => { histories[config.field].push(null); }); } @@ -232,10 +199,8 @@ export default function PlayerRankChart({ player }: Props) { labels.push(daysAgo === 0 ? "Today" : `${daysAgo} days ago`); // stupid typescript crying wahh wahh wahh - https://youtu.be/hBEKgHDzm_s?si=ekOdMMdb-lFnA1Yz&t=11 - datasetConfig.forEach((config) => { - (histories as any)[config.field].push( - (history as any)[config.field] ?? null, - ); + datasetConfig.forEach(config => { + (histories as any)[config.field].push((history as any)[config.field] ?? null); }); previousDate = currentDate; @@ -252,23 +217,16 @@ export default function PlayerRankChart({ player }: Props) { }; const datasets: Dataset[] = datasetConfig - .map((config) => { - if (histories[config.field].some((value) => value !== null)) { + .map(config => { + if (histories[config.field].some(value => value !== null)) { axes[config.axisId] = generateAxis( config.axisId, config.axisConfig.reverse, - isMobile && config.axisConfig.hideOnMobile - ? false - : config.axisConfig.display, + isMobile && config.axisConfig.hideOnMobile ? false : config.axisConfig.display, config.axisConfig.position, - config.axisConfig.displayName, - ); - return generateDataset( - config.title, - histories[config.field], - config.color, - config.axisId, + config.axisConfig.displayName ); + return generateDataset(config.title, histories[config.field], config.color, config.axisId); } return null; }) @@ -298,9 +256,7 @@ export default function PlayerRankChart({ player }: Props) { callbacks: { label(context: any) { const value = Number(context.parsed.y); - const config = datasetConfig.find( - (cfg) => cfg.title === context.dataset.label, - ); + const config = datasetConfig.find(cfg => cfg.title === context.dataset.label); return config?.labelFormatter(value) ?? ""; }, }, diff --git a/src/components/player/player-scores.tsx b/src/components/player/player-scores.tsx index ea59267..d613d25 100644 --- a/src/components/player/player-scores.tsx +++ b/src/components/player/player-scores.tsx @@ -14,8 +14,7 @@ import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import { scoresaberService } from "@/common/service/impl/scoresaber"; import { Input } from "@/components/ui/input"; import { clsx } from "clsx"; - -const INPUT_DEBOUNCE_DELAY = 250; // milliseconds +import { useDebounce } from "@uidotdev/usehooks"; type Props = { initialScoreData?: ScoreSaberPlayerScoresPageToken; @@ -49,40 +48,19 @@ const scoreAnimation: Variants = { visible: { opacity: 1, x: 0, transition: { staggerChildren: 0.03 } }, }; -export default function PlayerScores({ - initialScoreData, - initialSearch, - player, - sort, - page, -}: Props) { +export default function PlayerScores({ initialScoreData, initialSearch, player, sort, page }: Props) { const { width } = useWindowDimensions(); const controls = useAnimation(); const [pageState, setPageState] = useState({ page, sort }); const [previousPage, setPreviousPage] = useState(page); - const [currentScores, setCurrentScores] = useState< - ScoreSaberPlayerScoresPageToken | undefined - >(initialScoreData); - const [searchState, setSearchState] = useState({ - query: initialSearch || "", - }); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState( - initialSearch || "", - ); + const [currentScores, setCurrentScores] = useState(initialScoreData); + const [searchTerm, setSearchTerm] = useState(initialSearch || ""); + const debouncedSearchTerm = useDebounce(searchTerm, 250); const isSearchActive = debouncedSearchTerm.length >= 3; const [shouldFetch, setShouldFetch] = useState(false); // New state to control fetching - // Debounce the search query - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedSearchTerm(searchState.query); - }, INPUT_DEBOUNCE_DELAY); - - return () => clearTimeout(handler); - }, [searchState.query]); - const { data: scores, isError, @@ -98,15 +76,11 @@ export default function PlayerScores({ }); }, staleTime: 30 * 1000, // 30 seconds - enabled: - shouldFetch && - (debouncedSearchTerm.length >= 3 || debouncedSearchTerm.length === 0), // Only enable if we set shouldFetch to true + enabled: shouldFetch && (debouncedSearchTerm.length >= 3 || debouncedSearchTerm.length === 0), // Only enable if we set shouldFetch to true }); const handleScoreLoad = useCallback(async () => { - await controls.start( - previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft", - ); + await controls.start(previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft"); setCurrentScores(scores); await controls.start("visible"); }, [scores, controls, previousPage, pageState.page]); @@ -124,15 +98,11 @@ export default function PlayerScores({ useEffect(() => { const newUrl = `/player/${player.id}/${pageState.sort}/${pageState.page}${isSearchActive ? `?search=${debouncedSearchTerm}` : ""}`; - window.history.replaceState( - { ...window.history.state, as: newUrl, url: newUrl }, - "", - newUrl, - ); + window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl); }, [pageState, debouncedSearchTerm, player.id, isSearchActive]); const handleSearchChange = (query: string) => { - setSearchState({ query }); + setSearchTerm(query); if (query.length >= 3) { setShouldFetch(true); // Set to true to trigger fetch } else { @@ -141,23 +111,18 @@ export default function PlayerScores({ }; const clearSearch = () => { - setSearchState({ query: "" }); - setDebouncedSearchTerm(""); // Clear the debounced term + setSearchTerm(""); }; - const invalidSearch = - searchState.query.length >= 1 && searchState.query.length < 3; - + const invalidSearch = searchTerm.length >= 1 && searchTerm.length < 3; return (
- {scoreSort.map((sortOption) => ( + {scoreSort.map(sortOption => (
diff --git a/src/components/score/score-button.tsx b/src/components/score/score-button.tsx index fea6123..9f68cd4 100644 --- a/src/components/score/score-button.tsx +++ b/src/components/score/score-button.tsx @@ -1,8 +1,4 @@ -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; type Props = { /** diff --git a/src/components/score/score-buttons.tsx b/src/components/score/score-buttons.tsx index 80e98e0..f904de4 100644 --- a/src/components/score/score-buttons.tsx +++ b/src/components/score/score-buttons.tsx @@ -53,10 +53,7 @@ export default function ScoreButtons({ {/* Open map in BeatSaver */} { - window.open( - `https://beatsaver.com/maps/${beatSaverMap.bsr}`, - "_blank", - ); + window.open(`https://beatsaver.com/maps/${beatSaverMap.bsr}`, "_blank"); }} tooltip={

Click to open the map

} > @@ -69,12 +66,8 @@ export default function ScoreButtons({ { window.open( - songNameToYouTubeLink( - leaderboard.songName, - leaderboard.songSubName, - leaderboard.songAuthorName, - ), - "_blank", + songNameToYouTubeLink(leaderboard.songName, leaderboard.songSubName, leaderboard.songAuthorName), + "_blank" ); }} tooltip={

Click to open the song in YouTube

} diff --git a/src/components/score/score-info.tsx b/src/components/score/score-info.tsx index 9fafc2a..5c200b9 100644 --- a/src/components/score/score-info.tsx +++ b/src/components/score/score-info.tsx @@ -14,13 +14,9 @@ type Props = { }; export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) { - const diff = getDifficultyFromScoreSaberDifficulty( - leaderboard.difficulty.difficulty, - ); + const diff = getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty); const mappersProfile = - beatSaverMap != undefined - ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` - : undefined; + beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` : undefined; return (
@@ -72,13 +68,7 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {

{leaderboard.songAuthorName}

-

+

{leaderboard.levelAuthorName}

diff --git a/src/components/score/score-rank-info.tsx b/src/components/score/score-rank-info.tsx index a582424..b0bd6cb 100644 --- a/src/components/score/score-rank-info.tsx +++ b/src/components/score/score-rank-info.tsx @@ -26,9 +26,7 @@ export default function ScoreRankInfo({ score }: Props) {

} > -

- {timeAgo(new Date(score.timeSet))} -

+

{timeAgo(new Date(score.timeSet))}

); diff --git a/src/components/score/score-stats.tsx b/src/components/score/score-stats.tsx index a589d50..5b77583 100644 --- a/src/components/score/score-stats.tsx +++ b/src/components/score/score-stats.tsx @@ -9,13 +9,10 @@ import Tooltip from "@/components/tooltip"; type Badge = { name: string; - color?: ( - score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, - ) => string | undefined; + color?: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => string | undefined; create: ( score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, + leaderboard: ScoreSaberLeaderboardToken ) => string | React.ReactNode | undefined; }; @@ -35,17 +32,11 @@ const badges: Badge[] = [ }, { name: "Accuracy", - color: ( - score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, - ) => { + color: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => { const acc = (score.baseScore / leaderboard.maxScore) * 100; return getScoreBadgeFromAccuracy(acc).color; }, - create: ( - score: ScoreSaberScoreToken, - leaderboard: ScoreSaberLeaderboardToken, - ) => { + create: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => { const acc = (score.baseScore / leaderboard.maxScore) * 100; const scoreBadge = getScoreBadgeFromAccuracy(acc); let accDetails = `Accuracy ${scoreBadge.name != "-" ? scoreBadge.name : ""}`; @@ -93,16 +84,8 @@ const badges: Badge[] = [ return ( <> -

- {fullCombo ? ( - FC - ) : ( - formatNumberWithCommas(score.missedNotes) - )} -

- +

{fullCombo ? FC : formatNumberWithCommas(score.missedNotes)}

+ ); }, diff --git a/src/components/stat-value.tsx b/src/components/stat-value.tsx index dc980c9..66db5c2 100644 --- a/src/components/stat-value.tsx +++ b/src/components/stat-value.tsx @@ -22,7 +22,7 @@ export default function StatValue({ name, color, value }: Props) {
)} -
- {typeof value === "string" ?

{value}

: value} -
+
{typeof value === "string" ?

{value}

: value}
); } diff --git a/src/components/tooltip.tsx b/src/components/tooltip.tsx index 2df65ae..b7b8201 100644 --- a/src/components/tooltip.tsx +++ b/src/components/tooltip.tsx @@ -1,8 +1,4 @@ -import { - Tooltip as ShadCnTooltip, - TooltipContent, - TooltipTrigger, -} from "./ui/tooltip"; +import { Tooltip as ShadCnTooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; type Props = { /** diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index bd2c9f0..924040c 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -11,10 +11,7 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -24,11 +21,7 @@ const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; @@ -38,10 +31,7 @@ const AvatarFallback = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 0026f98..c4129f8 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -9,14 +9,10 @@ const buttonVariants = cva( { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, @@ -31,7 +27,7 @@ const buttonVariants = cva( variant: "default", size: "default", }, - }, + } ); export interface ButtonProps @@ -43,14 +39,8 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; - return ( - - ); - }, + return ; + } ); Button.displayName = "Button"; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 0505d9e..257a7c9 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -2,82 +2,40 @@ import * as React from "react"; import { cn } from "@/common/utils"; -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
+const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
)); Card.displayName = "Card"; -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); CardHeader.displayName = "CardHeader"; -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); CardTitle.displayName = "CardTitle"; -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); CardDescription.displayName = "CardDescription"; -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>
+); CardContent.displayName = "CardContent"; -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) =>
+); CardFooter.displayName = "CardFooter"; -export { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -}; +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index bcb263a..54dd180 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -3,14 +3,7 @@ import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; -import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, -} from "react-hook-form"; +import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form"; import { cn } from "@/common/utils"; import { Label } from "@/components/ui/label"; @@ -24,9 +17,7 @@ type FormFieldContextValue< name: TName; }; -const FormFieldContext = React.createContext( - {} as FormFieldContextValue, -); +const FormFieldContext = React.createContext({} as FormFieldContextValue); const FormField = < TFieldValues extends FieldValues = FieldValues, @@ -68,22 +59,19 @@ type FormItemContextValue = { id: string; }; -const FormItemContext = React.createContext( - {} as FormItemContextValue, +const FormItemContext = React.createContext({} as FormItemContextValue); + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); + } ); - -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId(); - - return ( - -
- - ); -}); FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< @@ -92,88 +80,59 @@ const FormLabel = React.forwardRef< >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField(); - return ( -