diff --git a/.gitea/kubernetes/backend/deployment.yaml b/.gitea/kubernetes/backend/deployment.yaml index 2478e05..bd1cf6c 100644 --- a/.gitea/kubernetes/backend/deployment.yaml +++ b/.gitea/kubernetes/backend/deployment.yaml @@ -25,4 +25,10 @@ spec: memory: 128Mi limits: cpu: 1000m # 1 vCPU - memory: 512Mi \ No newline at end of file + memory: 512Mi + env: + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: ssr-backend-secret + key: MONGO_URI \ No newline at end of file diff --git a/.gitea/kubernetes/backend/ingress.yaml b/.gitea/kubernetes/backend/ingress.yaml index a134817..2de160a 100644 --- a/.gitea/kubernetes/backend/ingress.yaml +++ b/.gitea/kubernetes/backend/ingress.yaml @@ -10,7 +10,7 @@ spec: entryPoints: - websecure routes: - - match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api-test`) + - match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api`) kind: Rule middlewares: - name: default-headers diff --git a/.gitea/kubernetes/backend/sealed-secrets.yaml b/.gitea/kubernetes/backend/sealed-secrets.yaml new file mode 100644 index 0000000..a2d1855 --- /dev/null +++ b/.gitea/kubernetes/backend/sealed-secrets.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: ssr-backend-secret + namespace: public-services +spec: + encryptedData: + MONGO_URI: AgBDDZhuphvpFZJZq31CA8OmiTr1J8p5Fy/rcr/zvm2nl64GnpbmVCuYgUiH6PwZPCUa9zcyUeZJO9/Xqe9PbJ8hA82j0Pb+Pcl1Rk3+B6jkDaEzcJKDmXS/zx8Q+JPWFOGVpRNy0HCKxm8azy88A9iyiKseIFsMWWrkJMkEObokRBCB4joD9Mh+aOsE2vaUkoE49ASxwVXU9MnL/34eksqGD6D5/BGpVZGftvY/x5eOuhULtK7z3tcd/orc//21AXUSAlVgWcekstEfZWQovk7Rwl67pgpHYf+KuegY4i+0ybge1qEngjvwt76yObTqfmhrdVQNfrV21FpTfoBeZS6ZoHdli6DBanPZgXJdKU2Ttr3C5EJ8c0Gir20J3wRs11SQ1gaKu6bxL4EH4kAtgdVoD5t6MSqvDzkfovAcJUHfXLA2HPhs1CEcu7Y6Kv/v+aGWSlo9jPVQg8JJ7IPF/+DDbF4JgEnwr34e7M5Z/CKVhwm7mK8Nr1yzgEhkucjZ6fcEVmt91fjx1usxDvtN+mllibc7HS2a/ObMDx3MtfHxXhTpt0wXyNyhXtnKNvKbICR5LGZfosF3viNfuRcEFTGvC2Ak9hlhVrznp0FRUiQJSNWsBQKZUKG5Yd5ckQwJUY8B/OLAjg0Keo26LIkciZ0jZD3JtK+bxU5LntdfSCwuO9+xHgaMgCxY+7plPQOgXL9BAOk9Zerc/6xQXJ7y4Q1PogrNaiPn6xCr368utFH2bA4zOAZCwrngnZtmT4pB6r6C/425JoZiQQw6qVQklpC7UhWpV1SbMvdGrlYgBW9TkT5rJPNIE3Bp3kA= + template: + metadata: + creationTimestamp: null + name: ssr-backend-secret + namespace: public-services + type: Opaque diff --git a/.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml b/.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml index b1b2677..19f113b 100644 --- a/.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml +++ b/.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml @@ -7,4 +7,4 @@ metadata: spec: stripPrefix: prefixes: - - "/api-test" + - "/api" diff --git a/.gitea/kubernetes/website/deployment.yaml b/.gitea/kubernetes/website/deployment.yaml index 7a4e15e..46f34de 100644 --- a/.gitea/kubernetes/website/deployment.yaml +++ b/.gitea/kubernetes/website/deployment.yaml @@ -27,28 +27,8 @@ spec: cpu: 1000m # 1 vCPU memory: 256Mi env: - - name: MONGO_URI - valueFrom: - secretKeyRef: - name: ssr-secret - key: MONGO_URI - name: NEXT_PUBLIC_SITE_URL valueFrom: secretKeyRef: name: ssr-secret key: NEXT_PUBLIC_SITE_URL - - name: NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY - valueFrom: - secretKeyRef: - name: ssr-secret - key: NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY - - name: TRIGGER_API_KEY - valueFrom: - secretKeyRef: - name: ssr-secret - key: TRIGGER_API_KEY - - name: TRIGGER_API_URL - valueFrom: - secretKeyRef: - name: ssr-secret - key: TRIGGER_API_URL diff --git a/.gitea/kubernetes/website/sealed-secrets.yaml b/.gitea/kubernetes/website/sealed-secrets.yaml index f2a0a7e..96b446c 100644 --- a/.gitea/kubernetes/website/sealed-secrets.yaml +++ b/.gitea/kubernetes/website/sealed-secrets.yaml @@ -7,11 +7,7 @@ metadata: namespace: public-services spec: encryptedData: - MONGO_URI: AgBDDZhuphvpFZJZq31CA8OmiTr1J8p5Fy/rcr/zvm2nl64GnpbmVCuYgUiH6PwZPCUa9zcyUeZJO9/Xqe9PbJ8hA82j0Pb+Pcl1Rk3+B6jkDaEzcJKDmXS/zx8Q+JPWFOGVpRNy0HCKxm8azy88A9iyiKseIFsMWWrkJMkEObokRBCB4joD9Mh+aOsE2vaUkoE49ASxwVXU9MnL/34eksqGD6D5/BGpVZGftvY/x5eOuhULtK7z3tcd/orc//21AXUSAlVgWcekstEfZWQovk7Rwl67pgpHYf+KuegY4i+0ybge1qEngjvwt76yObTqfmhrdVQNfrV21FpTfoBeZS6ZoHdli6DBanPZgXJdKU2Ttr3C5EJ8c0Gir20J3wRs11SQ1gaKu6bxL4EH4kAtgdVoD5t6MSqvDzkfovAcJUHfXLA2HPhs1CEcu7Y6Kv/v+aGWSlo9jPVQg8JJ7IPF/+DDbF4JgEnwr34e7M5Z/CKVhwm7mK8Nr1yzgEhkucjZ6fcEVmt91fjx1usxDvtN+mllibc7HS2a/ObMDx3MtfHxXhTpt0wXyNyhXtnKNvKbICR5LGZfosF3viNfuRcEFTGvC2Ak9hlhVrznp0FRUiQJSNWsBQKZUKG5Yd5ckQwJUY8B/OLAjg0Keo26LIkciZ0jZD3JtK+bxU5LntdfSCwuO9+xHgaMgCxY+7plPQOgXL9BAOk9Zerc/6xQXJ7y4Q1PogrNaiPn6xCr368utFH2bA4zOAZCwrngnZtmT4pB6r6C/425JoZiQQw6qVQklpC7UhWpV1SbMvdGrlYgBW9TkT5rJPNIE3Bp3kA= NEXT_PUBLIC_SITE_URL: AgCpMUZ2MFY8mHgQ3fizTzcBImnwFmWzccRCtMAThI0cAIOcDe15Drk2a5a4UjcYgl1F+JrHB3b3IPbflr1E4dNAANKRgiGW+gyI2S7J/oDpb+ANCv/0RJIlfQh9Pcb/E4noKVOoUfe4dg5asq1kQjOob4uOn6MfQXoC5WfgK8u8q0T5tEPcuGxXt2Q1OnyAAWm/0Z7JSLfgQN2sKaAbRbWqKfwfsc4LgjxY98m/+BkXN7x6R7BJmXXMd0cb5ctdgM1ZpU+gYhhwyO0xsxYWURcJb9EsrNZR6OY4DbwXw2tpoagFxA20u5J2ZUhUeVRg2x2R5AdkL7OBIT73Xbh3WxIYVAqGDhs90aRrmlCdr61eBLCLtytC33LJ/6Odq2Pa9DLaKqRlqRX/IWk7+cgHOKfSd8/k5R1roA3A96ShFby9RdXGudGLA2G4dvLtrruLCYVRfxMJB2k3UYtGZB21o+3SAV0jx/83eoYzoBGHM6K8ySCpL1uDCo8ATL2iYJcacgYZGKaGxBumzEjAMBqTLBSUl0Jhx3mr59p6mrYKFtbewa9rJUOkNniYvdCeokLyVntxUMx60Jtrtg05G3vSFaP34Gp6Oq6J0jSzvYi/A3/iSe+cNB1fpNJvJVLRFmJ6f7qyMMoSujIoql5SfIhx/tyUHueiOFQ5KXKTeNhbu6byakY1ZHa2o03+Mooca2ATwUnlNNi73sKluFKhnRysANIiVoRZLDQniLwV - NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY: AgCZwhhUNhSMwuR1pCP5qDY9fD99u78PFq89ej141pc/L/y/UCydLvftFKT62bXzIFhoq77dlU3yFx2FqbApdiDv3sDltZkIQh/afYwySPw3bXxoQoHcAix5qGhWrpDkPFDOi+sJkkPnnZC1OBncrqz8xAwfYAhwOscW9mjugRMJPynqSlnVHS1RdYm6z7eSJpZEMEHIT4tptPnzP+icRwbolgKL66JXFXvuS6SnTZ+ZOtub39L+wpWE9dQ83E5YqtWl3hci2G+rK9KBk89zuBM7Ho+MTpcdcaes64ApMqaUnFPelqJKSk6PK7mEX9DZhCUqNyCu897ktfHKulVZQ5Wy2+pVHXx9e1IBI7YqNph64CbX6N0V6ABfNlO2sS+zFG3dGuEGj/lI9hfSxqQauYOWXR7r8zM86WvNuxWuQFQbO4B1TDd8oofhZ+wwcUfJ0/pZIqyxcINB13opF107wa4MlfoCI6sgB4/adq/bbMP/JO10/GBiuJRhE63NhVJEZovJoRNV2+wBRNSVRfZpEQ9AXSACm1BtqOxhYhAmDnJt6ThF6VDWB2ZoDZfWul/kPUTUiOulGHmsRdn/bzTS8GjhY93G1/FpNmhNSOC8YbO3FDw8vXg2Vy6jpdKOhy08H9R/9UqbiHxnXPyBGyoizbnjP0sDx4jYYXtix03ZPFf6Dxz6iwwy5BbHpk9Ik+3l2iKI7IcxOOS9P8ljlsB0cCivpTax1iuDZ4hlJ7zm - TRIGGER_API_KEY: AgARH8DdSu8INQ2OW6I4s2W+HZqHGZHn0i54l02Ui48Oph9koB6pfTvAkYspQ6LI2zh/R/uiAeOHorybTMZ9X0EEwk5GxTuXBUn4f5Ifpd2QkoHeDVWP6MA951PVanfPuXLklwKJm2O70oFKIVE61v52yZbk0L3wAOiYdRTj0igrSEDkmmc9iHorGdbDCI3CkZHpOMMl37zdIwCvbpHaCnSBpKEuQ0PmvRtAw9ydM3FhVpTxNVh3KhTgvGBBYwrGXOZuKOayLGvQ16pYmTSPoN6DNRFSLjmE/BOjwKnYfZU0C0qkpGPlNLSUteuLLvHtzlS8IOSboOspreQJMVaSRpg+Qp1/cV0XGEhmU/CWVTYqkNx5QtfgaxWllrKrQxNW0WMDJmnQI83scsAiweSFUffsfiX8BCMjHkD2nvlXCz6vzUcJ7Zn0bDPoHcv/uG7efZbsJXLie1PxQiGwFYpuyr0b7+A+RVgx0G/WNwKJIUjFC7acI7jY4dGE04zKe1STYhMhoc1gjKGhXe0BG73LAX/O5/x6W4iYUyc4n0HL7gLwlbpfR3zLkvuiiAtzFeKGRr+SF24mj95pfw+MPFoKEi9htLdPgHxTYomfQ+1I8R7Iya0sHtyW2fI/1e5XzJOMHub/tYh5y9h0UqE5n7ByapRMyj0mOrKXXPUoT4btQDz0U6aNRX+MrlwMsuXYjSfUCuXmy30RKQImmT+9vaukIq1CX7WJ2LQ8fHaYACnp - TRIGGER_API_URL: AgAOwyGxQEScm5T3Hh1armqqcEcMEo0v5Mwf9JjEf3G+3svlDDPGHlyHdQolcC2YlkX7DhsenEp6rokh1grwyVoruyUc6OmRdRR70+PV5qMgSC3HY6lZ5f2gcGfA0uh9A5sm4qkOw4rliRddpJqKOqDz28zcrcu5RmusPxric+KF6Hcdy+ugqmq0KZl9VU2+D4z3QWkdokHk3WahdLneS4a3bHYC/NIpKyI5SveK6QAaQlU3NXrqKcof6VzDQG20bnCKGo+Y935LgzEIEmWKw2C9lwCV+/RUIjeaK2qzZpeMiZue9zgoq1dyNNjrar9B6zb+rSxcgnbqBolXUAVk1If3+egVNEaB9SjU22n+WoTA6HK1MOSwsaMtf1Tug/8nSQfFHdw1nZzBVtiVaFMtzmg0aKyrUpAYyz4XTn6xn9EhEKgcPSaWINf4zVcmceLOYenOP/y7S3cVx9KHBjUNGf/eDJVmXSiOzeguIJBfdEOla/lqv7Zx2/wvfHeEdurn5ENTkG2aQAekIvWiJ1HzPwrKKR6WcBpNTgjoDRMNxVoMcZ3QB9iJlp3AoLfJW72B2soTVeIikcNlT0Q0S91hiqvEcE+WuE5bDSttzhnb9nvEJXz6gC6AykCKH1VLIJJuiMI6R7V9z8fo7pFVXbQsM10VUph/9vxhib2XZ1c/25YMfj5vaI1+N7UiVDFlEfE2YJQMwd2vj+wTa3wHJz+2KXc/9rhBoaIpznN4LmKR6g== template: metadata: creationTimestamp: null diff --git a/.gitea/workflows/deploy-backend.yml b/.gitea/workflows/deploy-backend.yml index e62ffdb..14548b8 100644 --- a/.gitea/workflows/deploy-backend.yml +++ b/.gitea/workflows/deploy-backend.yml @@ -55,6 +55,7 @@ jobs: action: deploy namespace: public-services manifests: | + .gitea/kubernetes/backend/sealed-secrets.yaml .gitea/kubernetes/backend/deployment.yaml .gitea/kubernetes/backend/service.yaml .gitea/kubernetes/backend/strip-api-prefix-middleware.yaml diff --git a/.gitea/workflows/deploy-website.yml b/.gitea/workflows/deploy-website.yml index f068b71..54f7b35 100644 --- a/.gitea/workflows/deploy-website.yml +++ b/.gitea/workflows/deploy-website.yml @@ -1,15 +1,15 @@ name: "Deploy Website" -#on: -# workflow_dispatch: -# push: -# branches: -# - master -# paths: -# - projects/website/** -# - projects/common/** -# - .gitea/kubernetes/website/** -# - .gitea/workflows/deploy-website.yml +on: + workflow_dispatch: + push: + branches: + - master + paths: + - projects/website/** + - projects/common/** + - .gitea/kubernetes/website/** + - .gitea/workflows/deploy-website.yml jobs: deploy: diff --git a/bun.lockb b/bun.lockb index 82fce6f..ff1239a 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 08093bc..7e7e992 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,18 @@ "name": "scoresaber-reloaded", "version": "1.0.0", "workspaces": [ - "projects/backend", - "projects/website", - "projects/common" + "projects/*" ], "scripts": { - "dev": "bun run --filter '*' dev" + "dev:website": "bun --filter 'website' dev", + "dev:backend": "bun --filter 'backend' dev", + "dev:common": "bun --filter '@ssr/common' dev", + "dev": "concurrently \"bun run dev:common\" \"bun run dev:backend\"" }, "author": "fascinated7", - "license": "MIT" + "license": "MIT", + "dependencies": { + "concurrently": "^9.0.1", + "superjson": "^2.2.1" + } } diff --git a/projects/backend/.gitignore b/projects/backend/.gitignore index 87e5610..fb94ae7 100644 --- a/projects/backend/.gitignore +++ b/projects/backend/.gitignore @@ -39,4 +39,6 @@ yarn-error.log* **/*.tgz **/*.log package-lock.json -**/*.bun \ No newline at end of file +**/*.bun + +.env \ No newline at end of file diff --git a/projects/backend/package.json b/projects/backend/package.json index bd83df5..11414b1 100644 --- a/projects/backend/package.json +++ b/projects/backend/package.json @@ -2,21 +2,24 @@ "name": "backend", "version": "1.0.0", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", "dev": "bun run --watch src/index.ts", "start": "bun run src/index.ts" }, "dependencies": { "@bogeychan/elysia-etag": "^0.0.6", + "@dotenvx/dotenvx": "^1.16.1", "@elysiajs/cors": "^1.1.1", + "@elysiajs/cron": "^1.1.1", "@elysiajs/swagger": "^1.1.3", "@ssr/common": "workspace:common", "@tqman/nice-logger": "^1.0.1", + "@typegoose/typegoose": "^12.8.0", "elysia": "latest", "elysia-autoroutes": "^0.5.0", "elysia-decorators": "^1.0.2", "elysia-helmet": "^2.0.0", - "elysia-rate-limit": "^4.1.0" + "elysia-rate-limit": "^4.1.0", + "mongoose": "^8.7.0" }, "devDependencies": { "bun-types": "latest" diff --git a/projects/backend/src/common/config.ts b/projects/backend/src/common/config.ts new file mode 100644 index 0000000..806c026 --- /dev/null +++ b/projects/backend/src/common/config.ts @@ -0,0 +1,3 @@ +export const Config = { + mongoUri: process.env.MONGO_URI, +} \ No newline at end of file diff --git a/projects/backend/src/controller/player.controller.ts b/projects/backend/src/controller/player.controller.ts new file mode 100644 index 0000000..9e09b0b --- /dev/null +++ b/projects/backend/src/controller/player.controller.ts @@ -0,0 +1,54 @@ +import { Controller, Get } from "elysia-decorators"; +import { PlayerService } from "../service/player.service"; +import { t } from "elysia"; +import { PlayerHistory } from "@ssr/common/types/player/player-history"; +import { PlayerTrackedSince } from "@ssr/common/types/player/player-tracked-since"; + +@Controller("/player") +export default class PlayerController { + @Get("/history/:id", { + config: {}, + params: t.Object({ + id: t.String({ required: true }), + }), + query: t.Object({ + createIfMissing: t.Boolean({ default: false, required: false }), + }), + }) + public async getPlayer({ + params: { id }, + query: { createIfMissing }, + }: { + params: { id: string }; + query: { createIfMissing: boolean }; + }): Promise<{ statistics: Record }> { + const player = await PlayerService.getPlayer(id, createIfMissing); + return { statistics: player.getHistoryPreviousDays(50) }; + } + + @Get("/tracked/:id", { + config: {}, + params: t.Object({ + id: t.String({ required: true }), + }), + }) + public async getTrackedStatus({ + params: { id }, + query: { createIfMissing }, + }: { + params: { id: string }; + query: { createIfMissing: boolean }; + }): Promise { + try { + const player = await PlayerService.getPlayer(id, createIfMissing); + return { + tracked: true, + daysTracked: player.getDaysTracked(), + }; + } catch { + return { + tracked: false, + }; + } + } +} diff --git a/projects/backend/src/error/not-found-error.ts b/projects/backend/src/error/not-found-error.ts new file mode 100644 index 0000000..b9f3972 --- /dev/null +++ b/projects/backend/src/error/not-found-error.ts @@ -0,0 +1,10 @@ +import { HttpCode } from "../common/http-codes"; + +export class NotFoundError extends Error { + constructor( + public message: string = "not-found", + public status: number = HttpCode.NOT_FOUND.code + ) { + super(message); + } +} \ No newline at end of file diff --git a/projects/backend/src/error/rate-limit-error.ts b/projects/backend/src/error/rate-limit-error.ts index 50c944c..4b9c6aa 100644 --- a/projects/backend/src/error/rate-limit-error.ts +++ b/projects/backend/src/error/rate-limit-error.ts @@ -2,10 +2,9 @@ import { HttpCode } from "../common/http-codes"; export class RateLimitError extends Error { constructor( - public message: string = 'rate-limited', - public detail: string = '', + public message: string = "rate-limited", public status: number = HttpCode.TOO_MANY_REQUESTS.code ) { - super(message) + super(message); } } \ No newline at end of file diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index ee7362f..179caf4 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -2,14 +2,29 @@ import { Elysia } from "elysia"; import cors from "@elysiajs/cors"; import { decorators } from "elysia-decorators"; import { logger } from "@tqman/nice-logger"; -import { swagger } from '@elysiajs/swagger' -import { rateLimit } from 'elysia-rate-limit' +import { swagger } from "@elysiajs/swagger"; +import { rateLimit } from "elysia-rate-limit"; import { RateLimitError } from "./error/rate-limit-error"; -import { helmet } from 'elysia-helmet'; -import { etag } from '@bogeychan/elysia-etag' +import { helmet } from "elysia-helmet"; +import { etag } from "@bogeychan/elysia-etag"; import AppController from "./controller/app.controller"; +import * as dotenv from "@dotenvx/dotenvx"; +import mongoose from "mongoose"; +import { Config } from "./common/config"; +import { setLogLevel } from "@typegoose/typegoose"; +import PlayerController from "./controller/player.controller"; +import { PlayerService } from "./service/player.service"; -const app = new Elysia(); +// Load .env file +dotenv.config({ + logLevel: "success", + path: ".env", + override: true, +}); + +await mongoose.connect(Config.mongoUri!); // Connect to MongoDB +setLogLevel("DEBUG"); +export const app = new Elysia(); /** * Custom error handler @@ -50,33 +65,37 @@ app.use( /** * Rate limit (100 requests per minute) */ -app.use(rateLimit({ - scoping: "global", - duration: 60 * 1000, - max: 100, - skip: (request) => { - let [ _, path ] = request.url.split("/"); // Get the url parts - path === "" || path === undefined && (path = "/"); // If we're on /, the path is undefined, so we set it to / - return path === "/"; // ignore all requests to / - }, - errorResponse: new RateLimitError("Too many requests, please try again later"), -})) +app.use( + rateLimit({ + scoping: "global", + duration: 60 * 1000, + max: 100, + skip: request => { + let [_, path] = request.url.split("/"); // Get the url parts + path === "" || (path === undefined && (path = "/")); // If we're on /, the path is undefined, so we set it to / + return path === "/"; // ignore all requests to / + }, + errorResponse: new RateLimitError("Too many requests, please try again later"), + }) +); /** * Security settings */ -app.use(helmet({ - hsts: false, // Disable HSTS - contentSecurityPolicy: false, // Disable CSP - dnsPrefetchControl: true, // Enable DNS prefetch -})) +app.use( + helmet({ + hsts: false, // Disable HSTS + contentSecurityPolicy: false, // Disable CSP + dnsPrefetchControl: true, // Enable DNS prefetch + }) +); /** * Controllers */ app.use( decorators({ - controllers: [AppController], + controllers: [AppController, PlayerController], }) ); @@ -89,4 +108,9 @@ app.onStart(() => { console.log("Listening on port http://localhost:8080"); }); +/** + * Start cronjobs + */ +PlayerService.initCronjobs(); + app.listen(8080); diff --git a/projects/backend/src/model/player.ts b/projects/backend/src/model/player.ts new file mode 100644 index 0000000..57f8286 --- /dev/null +++ b/projects/backend/src/model/player.ts @@ -0,0 +1,108 @@ +import { getModelForClass, prop, ReturnModelType } from "@typegoose/typegoose"; +import { Document } from "mongoose"; +import { PlayerHistory } from "@ssr/common/types/player/player-history"; +import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-utils"; + +/** + * The model for a player. + */ +export class Player { + /** + * The id of the player. + */ + @prop() + public _id!: string; + + /** + * The player's statistic history. + */ + @prop() + private statisticHistory?: Record; + + @prop() + public lastTracked?: Date; + + /** + * Gets the player's statistic history. + */ + public getStatisticHistory(): Record { + if (this.statisticHistory === undefined) { + this.statisticHistory = {}; + } + return this.statisticHistory; + } + + /** + * Gets the player's history for a specific date. + * + * @param date the date to get the history for. + */ + public getHistoryByDate(date: Date): PlayerHistory { + if (this.statisticHistory === undefined) { + this.statisticHistory = {}; + } + return this.getStatisticHistory()[formatDateMinimal(getMidnightAlignedDate(date))] || {}; + } + + /** + * Gets the player's history for the previous X days. + * + * @param days the number of days to get the history for. + */ + public getHistoryPreviousDays(days: number): Record { + if (this.statisticHistory === undefined) { + this.statisticHistory = {}; + } + const history: Record = {}; + for (let i = 0; i < days; i++) { + const date = formatDateMinimal(getMidnightAlignedDate(getDaysAgoDate(i))); + const playerHistory = this.getStatisticHistory()[date]; + if (playerHistory === undefined || Object.keys(playerHistory).length === 0) { + continue; + } + history[date] = playerHistory; + } + return history; + } + + /** + * Sets the player's statistic history. + * + * @param date the date to set it for. + * @param history the history to set. + */ + public setStatisticHistory(date: Date, history: PlayerHistory) { + if (this.statisticHistory === undefined) { + this.statisticHistory = {}; + } + this.getStatisticHistory()[formatDateMinimal(getMidnightAlignedDate(date))] = history; + } + + /** + * Sorts the player's statistic history by + * date in descending order. (oldest to newest) + */ + public sortStatisticHistory() { + if (this.statisticHistory === undefined) { + this.statisticHistory = {}; + } + return Object.entries(this.getStatisticHistory()) + .sort((a, b) => Date.parse(b[0]) - Date.parse(a[0])) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + } + + /** + * Gets the number of days tracked. + * + * @returns the number of days tracked. + */ + public getDaysTracked(): number { + return Object.keys(this.getStatisticHistory()).length; + } +} + +// This type defines a Mongoose document based on Player. +export type PlayerDocument = Player & Document; + +// This type ensures that PlayerModel returns Mongoose documents (PlayerDocument) that have Mongoose methods (save, remove, etc.) +export const PlayerModel: ReturnModelType = getModelForClass(Player); diff --git a/projects/backend/src/service/player.service.ts b/projects/backend/src/service/player.service.ts new file mode 100644 index 0000000..fdcdbc8 --- /dev/null +++ b/projects/backend/src/service/player.service.ts @@ -0,0 +1,126 @@ +import { PlayerDocument, PlayerModel } from "../model/player"; +import { NotFoundError } from "../error/not-found-error"; +import { cron } from "@elysiajs/cron"; +import { app } from "../index"; +import { getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-utils"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; +import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; + +export class PlayerService { + /** + * Initialize the cron jobs + */ + public static initCronjobs() { + app.use( + cron({ + name: "player-statistics-tracker-cron", + pattern: "0 1 * * *", // Every day at 00:01 (midnight) + run: async () => { + const players: PlayerDocument[] = await PlayerModel.find({}); + for (const player of players) { + await PlayerService.trackScoreSaberPlayer(getMidnightAlignedDate(new Date()), player); + } + }, + }) + ); + } + + /** + * Get a player from the database. + * + * @param id the player to fetch + * @param create if true, create the player if it doesn't exist + * @returns the player + * @throws NotFoundError if the player is not found + */ + public static async getPlayer(id: string, create: boolean = false): Promise { + console.log(`Fetching player "${id}"...`); + let player: PlayerDocument | null = await PlayerModel.findById(id); + if (player === null && !create) { + console.log(`Player "${id}" not found.`); + throw new NotFoundError(`Player "${id}" not found`); + } + if (player === null) { + const playerToken = await scoresaberService.lookupPlayer(id); + if (playerToken === undefined) { + throw new NotFoundError(`Player "${id}" not found`); + } + + console.log(`Creating player "${id}"...`); + player = (await PlayerModel.create({ _id: id })) as any; + if (player === null) { + throw new NotFoundError(`Player "${id}" not found`); + } + await this.seedPlayerHistory(player, playerToken); + console.log(`Created player "${id}".`); + } else { + console.log(`Found player "${id}".`); + } + return player; + } + + /** + * Seeds the player's history using data from + * the ScoreSaber API. + * + * @param player the player to seed + * @param playerToken the SoreSaber player token + */ + public static async seedPlayerHistory(player: PlayerDocument, playerToken: ScoreSaberPlayerToken): Promise { + // Loop through rankHistory in reverse, from current day backwards + const playerRankHistory = playerToken.histories.split(",").map((value: string) => { + return parseInt(value); + }); + playerRankHistory.push(playerToken.rank); + + let daysAgo = 1; // Start from yesterday + for (let i = playerRankHistory.length - daysAgo - 1; i >= 0; i--) { + const rank = playerRankHistory[i]; + const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo)); + player.setStatisticHistory(date, { + rank: rank, + }); + daysAgo += 1; // Increment daysAgo for each earlier rank + } + await player.save(); + } + + /** + * Tracks a players statistics + * + * @param dateToday the date to track + * @param foundPlayer the player to track + */ + public static async trackScoreSaberPlayer(dateToday: Date, foundPlayer: PlayerDocument) { + const player = await scoresaberService.lookupPlayer(foundPlayer.id); + if (player == undefined) { + console.log(`Player "${foundPlayer.id}" not found on ScoreSaber`); + return; + } + if (player.inactive) { + console.log(`Player "${foundPlayer.id}" is inactive on ScoreSaber`); + return; + } + + // Seed the history with ScoreSaber data if no history exists + if (foundPlayer.getDaysTracked() === 0) { + await this.seedPlayerHistory(foundPlayer, player); + } + + // Update current day's statistics + let history = foundPlayer.getHistoryByDate(dateToday); + if (history == undefined) { + history = {}; // Initialize if history is not found + } + // Set the history data + history.pp = player.pp; + history.countryRank = player.countryRank; + history.rank = player.rank; + foundPlayer.setStatisticHistory(dateToday, history); + foundPlayer.sortStatisticHistory(); + foundPlayer.lastTracked = new Date(); + await foundPlayer.save(); + + console.log(`Tracked player "${foundPlayer.id}"!`); + } +} diff --git a/projects/backend/tsconfig.json b/projects/backend/tsconfig.json index 98efe04..0002c94 100644 --- a/projects/backend/tsconfig.json +++ b/projects/backend/tsconfig.json @@ -1,12 +1,14 @@ { "compilerOptions": { - "target": "ES2021", + "target": "ES2022", "module": "ES2022", - "moduleResolution": "node", + "moduleResolution": "Bundler", "types": ["bun-types"], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, } } diff --git a/projects/common/package.json b/projects/common/package.json index 93e043b..fb71653 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -3,12 +3,19 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "tsup src/index.ts --watch", - "build": "tsup src/index.ts" + "dev": "tsc --watch --preserveWatchOutput", + "build": "tsc" + }, + "exports": { + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js", + "require": "./dist/*.js", + "default": "./dist/*.js" + } }, "devDependencies": { "@types/node": "^22.7.4", - "tsup": "^8", "typescript": "^5" }, "dependencies": { diff --git a/projects/common/src/index.ts b/projects/common/src/index.ts deleted file mode 100644 index 11a5776..0000000 --- a/projects/common/src/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -export * from "src/utils/utils"; -export * from "src/utils/time-utils"; - -/** - * Player stuff - */ -export * from "src/types/player/player-history"; -export * from "src/types/player/player-tracked-since"; -export * from "src/types/player/player"; -export * from "src/types/player/impl/scoresaber-player"; -export * from "src/utils/player-utils"; - -/** - * Score stuff - */ -export * from "src/types/score/score"; -export * from "src/types/score/score-sort"; -export * from "src/types/score/modifier"; -export * from "src/types/score/impl/scoresaber-score"; - -/** - * Service stuff - */ -export * from "src/service/impl/beatsaver"; -export * from "src/service/impl/scoresaber"; - -/** - * Scoresaber Tokens - */ -export * from "src/types/token/scoresaber/score-saber-badge-token"; -export * from "src/types/token/scoresaber/score-saber-difficulty-token"; -export * from "src/types/token/scoresaber/score-saber-leaderboard-player-info-token"; -export * from "src/types/token/scoresaber/score-saber-leaderboard-scores-page-token"; -export * from "src/types/token/scoresaber/score-saber-leaderboard-token"; -export * from "src/types/token/scoresaber/score-saber-metadata-token"; -export * from "src/types/token/scoresaber/score-saber-player-score-token"; -export * from "src/types/token/scoresaber/score-saber-player-scores-page-token"; -export * from "src/types/token/scoresaber/score-saber-player-search-token"; -export * from "src/types/token/scoresaber/score-saber-player-token"; -export * from "src/types/token/scoresaber/score-saber-players-page-token"; -export * from "src/types/token/scoresaber/score-saber-score-token"; - -/** - * Beatsaver Tokens - */ -export * from "src/types/token/beatsaver/beat-saver-account-token"; -export * from "src/types/token/beatsaver/beat-saver-map-metadata-token"; -export * from "src/types/token/beatsaver/beat-saver-map-stats-token"; -export * from "src/types/token/beatsaver/beat-saver-map-token"; diff --git a/projects/common/src/service/impl/scoresaber.ts b/projects/common/src/service/impl/scoresaber.ts index 75ca24f..27c8295 100644 --- a/projects/common/src/service/impl/scoresaber.ts +++ b/projects/common/src/service/impl/scoresaber.ts @@ -1,7 +1,6 @@ import Service from "../service"; import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-token"; import ScoreSaberPlayerToken from "../../types/token/scoresaber/score-saber-player-token"; -import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "../../types/player/impl/scoresaber-player"; import { ScoreSaberPlayersPageToken } from "../../types/token/scoresaber/score-saber-players-page-token"; import { ScoreSort } from "../../types/score/score-sort"; import ScoreSaberPlayerScoresPageToken from "../../types/token/scoresaber/score-saber-player-scores-page-token"; @@ -55,19 +54,9 @@ class ScoreSaberService extends Service { * Looks up a player by their ID. * * @param playerId the ID of the player to look up - * @param apiUrl the url to the API for SSR * @returns the player that matches the ID, or undefined */ - async lookupPlayer( - playerId: string, - apiUrl: string - ): Promise< - | { - player: ScoreSaberPlayer; - rawPlayer: ScoreSaberPlayerToken; - } - | undefined - > { + async lookupPlayer(playerId: string): Promise { const before = performance.now(); this.log(`Looking up player "${playerId}"...`); const token = await this.fetch(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); @@ -75,10 +64,7 @@ class ScoreSaberService extends Service { return undefined; } this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`); - return { - player: await getScoreSaberPlayerFromToken(apiUrl, token), - rawPlayer: token, - }; + return token; } /** diff --git a/projects/common/src/types/player/impl/scoresaber-player.ts b/projects/common/src/types/player/impl/scoresaber-player.ts index dc3cca6..56c07b0 100644 --- a/projects/common/src/types/player/impl/scoresaber-player.ts +++ b/projects/common/src/types/player/impl/scoresaber-player.ts @@ -3,6 +3,7 @@ import ky from "ky"; import { PlayerHistory } from "../player-history"; import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token"; import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils"; +import { getPlayerIdCookie } from "website/src/common/website-utils"; /** * A ScoreSaber player. @@ -65,9 +66,15 @@ export default interface ScoreSaberPlayer extends Player { isBeingTracked?: boolean; } +/** + * Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}. + * + * @param token the player token + * @param apiUrl the api url for SSR + */ export async function getScoreSaberPlayerFromToken( - apiUrl: string, - token: ScoreSaberPlayerToken + token: ScoreSaberPlayerToken, + apiUrl: string ): Promise { const bio: ScoreSaberBio = { lines: token.bio?.split("\n") || [], @@ -86,10 +93,10 @@ export async function getScoreSaberPlayerFromToken( const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date())); let statisticHistory: { [key: string]: PlayerHistory } = {}; try { - const history = await ky + const { statistics: history } = await ky .get<{ - [key: string]: PlayerHistory; - }>(`${apiUrl}/api/player/history?id=${token.id}`) + statistics: { [key: string]: PlayerHistory }; + }>(`${apiUrl}/player/history/${token.id}${getPlayerIdCookie() == token.id ? "?createIfMissing=true" : ""}`) .json(); if (history === undefined || Object.entries(history).length === 0) { console.log("Player has no history, using fallback"); diff --git a/projects/common/src/types/player/player-tracked-since.ts b/projects/common/src/types/player/player-tracked-since.ts index 8b3756f..edb6a39 100644 --- a/projects/common/src/types/player/player-tracked-since.ts +++ b/projects/common/src/types/player/player-tracked-since.ts @@ -4,11 +4,6 @@ export interface PlayerTrackedSince { */ tracked: boolean; - /** - * The date the player was first tracked - */ - trackedSince?: string; - /** * The amount of days the player has been tracked */ diff --git a/projects/common/tsconfig.json b/projects/common/tsconfig.json index 30ac961..cf0405a 100644 --- a/projects/common/tsconfig.json +++ b/projects/common/tsconfig.json @@ -1,21 +1,18 @@ { "compilerOptions": { "module": "ES2022", - "moduleResolution": "Bundler", "target": "ES2022", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", + "declaration": true, + "moduleResolution": "node", "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false - } + "allowSyntheticDefaultImports": true, + "strict": true, + "baseUrl": "./", + "paths": { + "@ssr/*": ["dist/*"] // This is crucial for resolving the imports correctly + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/projects/common/tsup.config.ts b/projects/common/tsup.config.ts deleted file mode 100644 index dade4b1..0000000 --- a/projects/common/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/index.ts"], - splitting: false, - sourcemap: true, - clean: true, - dts: true, // Generates type declarations - format: ["esm"], // Ensures output is in ESM format -}); diff --git a/projects/website/.env-example b/projects/website/.env-example index f730995..b424e49 100644 --- a/projects/website/.env-example +++ b/projects/website/.env-example @@ -1,7 +1,2 @@ NEXT_PUBLIC_SITE_URL=http://localhost:3000 -NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY= - -TRIGGER_API_KEY= -TRIGGER_API_URL=https://trigger.example.com -MONGO_URI=mongodb://127.0.0.1:27017 -SENTRY_AUTH_TOKEN= +NEXT_PUBLIC_SITE_API=http://localhost:8080 \ No newline at end of file diff --git a/projects/website/Dockerfile b/projects/website/Dockerfile index 197be77..0ef35d0 100644 --- a/projects/website/Dockerfile +++ b/projects/website/Dockerfile @@ -1,25 +1,30 @@ -FROM node:20-alpine3.17 AS base +FROM oven/bun:1.1.30-alpine AS base -# Install pnpm -RUN npm install -g pnpm -ENV PNPM_HOME=/usr/local/bin +# Install dependencies +FROM base AS depends +WORKDIR /app +COPY . . +RUN bun install --frozen-lockfile +# Run the app FROM base AS runner WORKDIR /app -# Copy website package and lock files only -COPY package.json* pnpm-lock.yaml* pnpm-workspace.yaml* ./ -COPY website ./website - +# Set the environment +ENV NODE_ENV production ARG GIT_REV ENV GIT_REV=${GIT_REV} -RUN pnpm install --filter website -RUN pnpm run build:website +COPY --from=depends /app/node_modules ./node_modules +COPY --from=depends /app/package.json* /app/bun.lockb* ./ +COPY --from=depends /app/projects/website ./projects/website -# Expose the app port and start it +# Build the website +RUN bun run --filter website build + +# Expose the app port EXPOSE 3000 ENV HOSTNAME="0.0.0.0" ENV PORT=3000 -CMD ["pnpm", "start:website"] +CMD ["bun", "run", "--filter", "backend", "start"] diff --git a/projects/website/config.ts b/projects/website/config.ts index 5a2711a..b46cc87 100644 --- a/projects/website/config.ts +++ b/projects/website/config.ts @@ -1,3 +1,4 @@ export const config = { siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc", + siteApi: process.env.NEXT_PUBLIC_SITE_API || "https://ssr.fascinated.cc/api", }; diff --git a/projects/website/package.json b/projects/website/package.json index 024e3e1..e422d97 100644 --- a/projects/website/package.json +++ b/projects/website/package.json @@ -1,6 +1,6 @@ { "name": "website", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev --turbo", @@ -22,9 +22,6 @@ "@radix-ui/react-tooltip": "^1.1.2", "@sentry/nextjs": "8", "@tanstack/react-query": "^5.55.4", - "@trigger.dev/nextjs": "^3.0.8", - "@trigger.dev/react": "^3.0.8", - "@trigger.dev/sdk": "^3.0.8", "@uidotdev/usehooks": "^2.4.1", "chart.js": "^4.4.4", "class-variance-authority": "^0.7.0", @@ -36,7 +33,6 @@ "js-cookie": "^3.0.5", "ky": "^1.7.2", "lucide-react": "^0.447.0", - "mongoose": "^8.7.0", "next": "15.0.0-rc.0", "next-build-id": "^3.0.0", "next-themes": "^0.3.0", diff --git a/projects/website/src/app/(pages)/api/player/history/route.ts b/projects/website/src/app/(pages)/api/player/history/route.ts deleted file mode 100644 index b7395da..0000000 --- a/projects/website/src/app/(pages)/api/player/history/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { connectMongo } from "@/common/mongo"; -import { IPlayer, PlayerModel } from "@/common/schema/player-schema"; -import { seedPlayerHistory } from "@/common/player-utils"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; - -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 }); - } - const shouldCreatePlayer = playerIdCookie?.value === id; - - await connectMongo(); // Connect to Mongo - - // Fetch the player and return their statistic history - let foundPlayer: IPlayer | null = await PlayerModel.findById(id); - if (shouldCreatePlayer && foundPlayer == null) { - foundPlayer = await PlayerModel.create({ - _id: id, - trackedSince: new Date().toISOString(), - }); - const response = await scoresaberService.lookupPlayer(id, true); - if (response != undefined) { - const { player, rawPlayer } = response; - await seedPlayerHistory(foundPlayer!, player, rawPlayer); - } - } - if (foundPlayer == null) { - return NextResponse.json({ error: "Player not found" }, { status: 404 }); - } - - return NextResponse.json(foundPlayer.getHistoryPrevious(50)); -} diff --git a/projects/website/src/app/(pages)/api/player/isbeingtracked/route.ts b/projects/website/src/app/(pages)/api/player/isbeingtracked/route.ts deleted file mode 100644 index d80bdd3..0000000 --- a/projects/website/src/app/(pages)/api/player/isbeingtracked/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { connectMongo } from "@/common/mongo"; -import { IPlayer, PlayerModel } from "@/common/schema/player-schema"; -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 }); - } - await connectMongo(); // Connect to Mongo - - const foundPlayer: IPlayer | null = await PlayerModel.findById(id); - const response: PlayerTrackedSince = { - tracked: foundPlayer != null, - }; - if (foundPlayer != null) { - response["trackedSince"] = foundPlayer.trackedSince?.toUTCString(); - response["daysTracked"] = foundPlayer.getStatisticHistory().size; - } - return NextResponse.json(response); -} diff --git a/projects/website/src/app/(pages)/api/proxy/route.ts b/projects/website/src/app/(pages)/api/proxy/route.ts deleted file mode 100644 index edbec74..0000000 --- a/projects/website/src/app/(pages)/api/proxy/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { validateUrl } from "@/common/utils"; -import ky from "ky"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const url = request.nextUrl.searchParams.get("url"); - if (url == null) { - return NextResponse.json({ error: "Missing URL. ?url=" }, { status: 400 }); - } - - if (!validateUrl(url)) { - return NextResponse.json({ error: "Invalid URL" }, { status: 400 }); - } - - try { - const response = await ky.get(url, { - next: { - revalidate: 30, // 30 seconds - }, - }); - const { status, headers } = response; - if ( - !headers.has("content-type") || - (headers.has("content-type") && !headers.get("content-type")?.includes("application/json")) - ) { - return NextResponse.json({ - error: "We only support proxying JSON responses", - }); - } - - const body = await response.json(); - return NextResponse.json(body, { - status: status, - }); - } catch (err) { - console.error(`Error fetching data from ${url}:`, err); - return NextResponse.json( - { error: "Failed to proxy this request." }, - { - status: 500, - headers: { - "Access-Control-Allow-Origin": "*", - }, - } - ); - } -} diff --git a/projects/website/src/app/(pages)/api/trigger/route.ts b/projects/website/src/app/(pages)/api/trigger/route.ts deleted file mode 100644 index 6331ed9..0000000 --- a/projects/website/src/app/(pages)/api/trigger/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createAppRoute } from "@trigger.dev/nextjs"; -import { client } from "@/trigger"; - -import "@/jobs"; - -//this route is used to send and receive data with Trigger.dev -export const { POST, dynamic } = createAppRoute(client); - -//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration -//export const maxDuration = 60; diff --git a/projects/website/src/app/(pages)/player/[...slug]/page.tsx b/projects/website/src/app/(pages)/player/[...slug]/page.tsx index 3781990..b9d9ba4 100644 --- a/projects/website/src/app/(pages)/player/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/player/[...slug]/page.tsx @@ -1,14 +1,16 @@ import { formatNumberWithCommas, formatPp } from "@/common/number-utils"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; -import { ScoreSort } from "@/common/model/score/score-sort"; import PlayerData from "@/components/player/player-data"; import { format } from "@formkit/tempo"; import { Metadata, Viewport } from "next"; import { redirect } from "next/navigation"; import { Colors } from "@/common/colors"; -import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token"; import { getAverageColor } from "@/common/image-utils"; import { cache } from "react"; +import { ScoreSort } from "@ssr/common/types/score/score-sort"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; +import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token"; +import { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player"; +import { config } from "../../../../../config"; const UNKNOWN_PLAYER = { title: "ScoreSaber Reloaded - Unknown Player", @@ -38,7 +40,8 @@ const getPlayerData = cache(async ({ params }: Props, fetchScores: boolean = tru 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; + const playerToken = await scoresaberService.lookupPlayer(id); + const player = playerToken && (await getScoreSaberPlayerFromToken(playerToken, config.siteApi)); let scores: ScoreSaberPlayerScoresPageToken | undefined; if (fetchScores) { scores = await scoresaberService.lookupPlayerScores({ diff --git a/projects/website/src/common/database/types/beatsaver-map.ts b/projects/website/src/common/database/types/beatsaver-map.ts new file mode 100644 index 0000000..31a33e4 --- /dev/null +++ b/projects/website/src/common/database/types/beatsaver-map.ts @@ -0,0 +1,23 @@ +import { Entity } from "dexie"; +import Database from "../database"; +import { BeatSaverMapToken } from "@ssr/common/types/token/beatsaver/beat-saver-map-token"; + +/** + * A beat saver map. + */ +export default class BeatSaverMap extends Entity { + /** + * The hash of the map. + */ + hash!: string; + + /** + * The bsr code for the map. + */ + bsr!: string; + + /** + * The full data for the map. + */ + fullData!: BeatSaverMapToken; +} diff --git a/projects/website/src/common/mongo.ts b/projects/website/src/common/mongo.ts deleted file mode 100644 index 344a596..0000000 --- a/projects/website/src/common/mongo.ts +++ /dev/null @@ -1,12 +0,0 @@ -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); -} diff --git a/projects/website/src/common/player-utils.ts b/projects/website/src/common/player-utils.ts index fcfe265..2335694 100644 --- a/projects/website/src/common/player-utils.ts +++ b/projects/website/src/common/player-utils.ts @@ -1,4 +1,4 @@ -import { PlayerHistory } from "@/common/player/player-history"; +import { PlayerHistory } from "@ssr/common/types/player/player-history"; /** * Gets a value from an {@link PlayerHistory} diff --git a/projects/website/src/common/website-utils.ts b/projects/website/src/common/website-utils.ts index 54020b6..c2ffdf8 100644 --- a/projects/website/src/common/website-utils.ts +++ b/projects/website/src/common/website-utils.ts @@ -9,6 +9,15 @@ export function setPlayerIdCookie(playerId: string) { Cookies.set("playerId", playerId, { path: "/" }); } +/** + * Gets the player id cookie + * + * @returns the player id cookie + */ +export function getPlayerIdCookie() { + return Cookies.get("playerId"); +} + /** * Gets if we're in production */ diff --git a/projects/website/src/common/worker/worker.ts b/projects/website/src/common/worker/worker.ts index 2d8bd4f..05abb9f 100644 --- a/projects/website/src/common/worker/worker.ts +++ b/projects/website/src/common/worker/worker.ts @@ -1,5 +1,5 @@ import * as Comlink from "comlink"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; export interface WorkerApi { getPlayerExample: typeof getPlayerExample; diff --git a/projects/website/src/components/input/search-player.tsx b/projects/website/src/components/input/search-player.tsx index 8df7d76..34016f9 100644 --- a/projects/website/src/components/input/search-player.tsx +++ b/projects/website/src/components/input/search-player.tsx @@ -1,7 +1,5 @@ "use client"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; -import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token"; import { formatNumberWithCommas } from "@/common/number-utils"; import { zodResolver } from "@hookform/resolvers/zod"; import Link from "next/link"; @@ -13,6 +11,8 @@ import { Button } from "../ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel } from "../ui/form"; import { Input } from "../ui/input"; import { ScrollArea } from "../ui/scroll-area"; +import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; const formSchema = z.object({ username: z.string().min(3).max(50), diff --git a/projects/website/src/components/leaderboard/leaderboard-data.tsx b/projects/website/src/components/leaderboard/leaderboard-data.tsx index 8c8ed2f..921cd5c 100644 --- a/projects/website/src/components/leaderboard/leaderboard-data.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-data.tsx @@ -1,14 +1,13 @@ "use client"; -import ScoreSaberLeaderboardScoresPageToken from "@/common/model/token/scoresaber/score-saber-leaderboard-scores-page-token"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import LeaderboardScores from "@/components/leaderboard/leaderboard-scores"; import { LeaderboardInfo } from "@/components/leaderboard/leaderboard-info"; import { useQuery } from "@tanstack/react-query"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import BeatSaverMap from "@/common/database/types/beatsaver-map"; -import { beatsaverService } from "@/common/service/impl/beatsaver"; +import ScoreSaberLeaderboardScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-scores-page-token"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; type LeaderboardDataProps = { /** @@ -39,14 +38,15 @@ export function LeaderboardData({ initialPage, initialScores, initialLeaderboard staleTime: 30 * 1000, // Cache data for 30 seconds }); - const fetchBeatSaverData = useCallback(async () => { - const beatSaverMap = await beatsaverService.lookupMap(initialLeaderboard.songHash); - setBeatSaverMap(beatSaverMap); - }, [initialLeaderboard.songHash]); - - useEffect(() => { - fetchBeatSaverData(); - }, [fetchBeatSaverData]); + // todo: fix + // const fetchBeatSaverData = useCallback(async () => { + // const beatSaverMap = await beatsaverService.lookupMap(initialLeaderboard.songHash); + // setBeatSaverMap(beatSaverMap); + // }, [initialLeaderboard.songHash]); + // + // useEffect(() => { + // fetchBeatSaverData(); + // }, [fetchBeatSaverData]); /** * When the leaderboard changes, update the previous and current leaderboards. diff --git a/projects/website/src/components/leaderboard/leaderboard-info.tsx b/projects/website/src/components/leaderboard/leaderboard-info.tsx index 70c188a..2b679ae 100644 --- a/projects/website/src/components/leaderboard/leaderboard-info.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-info.tsx @@ -1,9 +1,9 @@ import Card from "@/components/card"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import Image from "next/image"; import { LeaderboardSongStarCount } from "@/components/leaderboard/leaderboard-song-star-count"; import ScoreButtons from "@/components/score/score-buttons"; import BeatSaverMap from "@/common/database/types/beatsaver-map"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; type LeaderboardInfoProps = { /** diff --git a/projects/website/src/components/leaderboard/leaderboard-player.tsx b/projects/website/src/components/leaderboard/leaderboard-player.tsx index 07816fe..61d4362 100644 --- a/projects/website/src/components/leaderboard/leaderboard-player.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-player.tsx @@ -1,7 +1,7 @@ -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; -import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; import Image from "next/image"; import Link from "next/link"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Props = { /** diff --git a/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx b/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx index a60e99c..70751c2 100644 --- a/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-score-stats.tsx @@ -1,11 +1,11 @@ -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; -import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; import { formatNumberWithCommas } from "@/common/number-utils"; import { XMarkIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; import { getScoreBadgeFromAccuracy } from "@/common/song-utils"; import Tooltip from "@/components/tooltip"; import { ScoreBadge, ScoreBadges } from "@/components/score/score-badge"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; const badges: ScoreBadge[] = [ { diff --git a/projects/website/src/components/leaderboard/leaderboard-score.tsx b/projects/website/src/components/leaderboard/leaderboard-score.tsx index 36aa4a3..bda3e31 100644 --- a/projects/website/src/components/leaderboard/leaderboard-score.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-score.tsx @@ -1,9 +1,9 @@ -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; -import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; import LeaderboardPlayer from "./leaderboard-player"; import LeaderboardScoreStats from "./leaderboard-score-stats"; import ScoreRankInfo from "@/components/score/score-rank-info"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; type Props = { /** diff --git a/projects/website/src/components/leaderboard/leaderboard-scores.tsx b/projects/website/src/components/leaderboard/leaderboard-scores.tsx index bbe21b7..b16df40 100644 --- a/projects/website/src/components/leaderboard/leaderboard-scores.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-scores.tsx @@ -1,8 +1,5 @@ "use client"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; -import ScoreSaberLeaderboardScoresPageToken from "@/common/model/token/scoresaber/score-saber-leaderboard-scores-page-token"; import useWindowDimensions from "@/hooks/use-window-dimensions"; import { useQuery } from "@tanstack/react-query"; import { motion, useAnimation } from "framer-motion"; @@ -11,10 +8,13 @@ import Card from "../card"; import Pagination from "../input/pagination"; import LeaderboardScore from "./leaderboard-score"; import { scoreAnimation } from "@/components/score/score-animation"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import { Button } from "@/components/ui/button"; import { clsx } from "clsx"; import { getDifficultyFromRawDifficulty } from "@/common/song-utils"; +import ScoreSaberLeaderboardScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-scores-page-token"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; type LeaderboardScoresProps = { /** 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 7c67564..c27b375 100644 --- a/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-song-star-count.tsx @@ -1,7 +1,7 @@ import { songDifficultyToColor } from "@/common/song-utils"; import { StarIcon } from "@heroicons/react/24/solid"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; type LeaderboardSongStarCountProps = { /** diff --git a/projects/website/src/components/player/chart/generic-player-chart.tsx b/projects/website/src/components/player/chart/generic-player-chart.tsx index 21476b0..f262b5e 100644 --- a/projects/website/src/components/player/chart/generic-player-chart.tsx +++ b/projects/website/src/components/player/chart/generic-player-chart.tsx @@ -1,10 +1,10 @@ "use client"; -import { parseDate } from "@/common/time-utils"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import React from "react"; import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart"; import { getValueFromHistory } from "@/common/player-utils"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import { parseDate } from "@ssr/common/utils/time-utils"; type Props = { /** diff --git a/projects/website/src/components/player/chart/player-accuracy-chart.tsx b/projects/website/src/components/player/chart/player-accuracy-chart.tsx index 88c88a2..1e032ec 100644 --- a/projects/website/src/components/player/chart/player-accuracy-chart.tsx +++ b/projects/website/src/components/player/chart/player-accuracy-chart.tsx @@ -1,9 +1,9 @@ "use client"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import React from "react"; import { DatasetConfig } from "@/components/chart/generic-chart"; import GenericPlayerChart from "@/components/player/chart/generic-player-chart"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Props = { player: ScoreSaberPlayer; diff --git a/projects/website/src/components/player/chart/player-charts.tsx b/projects/website/src/components/player/chart/player-charts.tsx index 4e9d575..2a4388f 100644 --- a/projects/website/src/components/player/chart/player-charts.tsx +++ b/projects/website/src/components/player/chart/player-charts.tsx @@ -1,12 +1,12 @@ "use client"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import PlayerRankingChart from "@/components/player/chart/player-ranking-chart"; import { FC, useState } from "react"; import Tooltip from "@/components/tooltip"; import PlayerAccuracyChart from "@/components/player/chart/player-accuracy-chart"; import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import { TrendingUpIcon } from "lucide-react"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type PlayerChartsProps = { /** diff --git a/projects/website/src/components/player/chart/player-ranking-chart.tsx b/projects/website/src/components/player/chart/player-ranking-chart.tsx index ccdf18d..c70733b 100644 --- a/projects/website/src/components/player/chart/player-ranking-chart.tsx +++ b/projects/website/src/components/player/chart/player-ranking-chart.tsx @@ -1,10 +1,10 @@ "use client"; import { formatNumberWithCommas } from "@/common/number-utils"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import React from "react"; import { DatasetConfig } from "@/components/chart/generic-chart"; import GenericPlayerChart from "@/components/player/chart/generic-player-chart"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Props = { player: ScoreSaberPlayer; diff --git a/projects/website/src/components/player/player-badges.tsx b/projects/website/src/components/player/player-badges.tsx index 9522308..7ebed33 100644 --- a/projects/website/src/components/player/player-badges.tsx +++ b/projects/website/src/components/player/player-badges.tsx @@ -1,6 +1,6 @@ -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import Image from "next/image"; import Tooltip from "@/components/tooltip"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Props = { player: ScoreSaberPlayer; diff --git a/projects/website/src/components/player/player-data.tsx b/projects/website/src/components/player/player-data.tsx index 27aff4f..9b6a0eb 100644 --- a/projects/website/src/components/player/player-data.tsx +++ b/projects/website/src/components/player/player-data.tsx @@ -1,19 +1,20 @@ "use client"; -import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; -import { ScoreSort } from "@/common/model/score/score-sort"; import { useQuery } from "@tanstack/react-query"; import Mini from "../ranking/mini"; import PlayerHeader from "./player-header"; import PlayerScores from "./player-scores"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import Card from "@/components/card"; import PlayerBadges from "@/components/player/player-badges"; import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsVisible } from "@/hooks/use-is-visible"; import { useRef } from "react"; import PlayerCharts from "@/components/player/chart/player-charts"; +import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player"; +import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token"; +import { ScoreSort } from "@ssr/common/types/score/score-sort"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; +import { config } from "../../../config"; type Props = { initialPlayerData: ScoreSaberPlayer; @@ -37,12 +38,18 @@ export default function PlayerData({ let player = initialPlayerData; const { data, isLoading, isError } = useQuery({ queryKey: ["player", player.id], - queryFn: () => scoresaberService.lookupPlayer(player.id), + queryFn: async (): Promise => { + const playerResponse = await scoresaberService.lookupPlayer(player.id); + if (playerResponse == undefined) { + return undefined; + } + return await getScoreSaberPlayerFromToken(playerResponse, config.siteApi); + }, staleTime: 1000 * 60 * 5, // Cache data for 5 minutes }); if (data && (!isLoading || !isError)) { - player = data.player; + player = data; } return ( diff --git a/projects/website/src/components/player/player-scores.tsx b/projects/website/src/components/player/player-scores.tsx index ddb76ae..3ad43bf 100644 --- a/projects/website/src/components/player/player-scores.tsx +++ b/projects/website/src/components/player/player-scores.tsx @@ -7,15 +7,15 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Card from "../card"; import Pagination from "../input/pagination"; import { Button } from "../ui/button"; -import { ScoreSort } from "@/common/model/score/score-sort"; -import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token"; import Score from "@/components/score/score"; -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"; import { useDebounce } from "@uidotdev/usehooks"; import { scoreAnimation } from "@/components/score/score-animation"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token"; +import { ScoreSort } from "@ssr/common/types/score/score-sort"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; type Props = { initialScoreData?: ScoreSaberPlayerScoresPageToken; diff --git a/projects/website/src/components/player/player-stats.tsx b/projects/website/src/components/player/player-stats.tsx index 553f778..0cafe7f 100644 --- a/projects/website/src/components/player/player-stats.tsx +++ b/projects/website/src/components/player/player-stats.tsx @@ -1,6 +1,6 @@ import { formatNumberWithCommas } from "@/common/number-utils"; import StatValue from "@/components/stat-value"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Badge = { name: string; diff --git a/projects/website/src/components/player/player-tracked-status.tsx b/projects/website/src/components/player/player-tracked-status.tsx index c21b16c..3621fa8 100644 --- a/projects/website/src/components/player/player-tracked-status.tsx +++ b/projects/website/src/components/player/player-tracked-status.tsx @@ -1,15 +1,13 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import ky from "ky"; import { config } from "../../../config"; import Tooltip from "@/components/tooltip"; import { InformationCircleIcon } from "@heroicons/react/16/solid"; -import { format } from "@formkit/tempo"; -import { PlayerTrackedSince } from "@/common/player/player-tracked-since"; -import { getDaysAgo } from "@/common/time-utils"; import { formatNumberWithCommas } from "@/common/number-utils"; +import { PlayerTrackedSince } from "@ssr/common/types/player/player-tracked-since"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; type Props = { player: ScoreSaberPlayer; @@ -18,32 +16,19 @@ type Props = { export default function PlayerTrackedStatus({ player }: Props) { const { data, isLoading, isError } = useQuery({ queryKey: ["playerIsBeingTracked", player.id], - queryFn: () => ky.get(`${config.siteUrl}/api/player/isbeingtracked?id=${player.id}`).json(), + queryFn: () => ky.get(`${config.siteApi}/player/tracked/${player.id}`).json(), }); if (isLoading || isError || !data?.tracked) { return undefined; } - const trackedSince = new Date(data.trackedSince!); - const daysAgo = getDaysAgo(trackedSince) + 1; - let daysAgoFormatted = `${formatNumberWithCommas(daysAgo)} day${daysAgo > 1 ? "s" : ""} ago`; - if (daysAgo === 1) { - daysAgoFormatted = "Today"; - } - if (daysAgo === 2) { - daysAgoFormatted = "Yesterday"; - } - return (

This player is having their statistics tracked!

-

- Tracked Since: {format(trackedSince)} ({daysAgoFormatted}) -

Days Tracked: {formatNumberWithCommas(data.daysTracked!)}

} diff --git a/projects/website/src/components/ranking/mini.tsx b/projects/website/src/components/ranking/mini.tsx index 0e6995e..3620aa5 100644 --- a/projects/website/src/components/ranking/mini.tsx +++ b/projects/website/src/components/ranking/mini.tsx @@ -1,5 +1,3 @@ -import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token"; -import { ScoreSaberPlayersPageToken } from "@/common/model/token/scoresaber/score-saber-players-page-token"; import { formatNumberWithCommas, formatPp } from "@/common/number-utils"; import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import { useQuery } from "@tanstack/react-query"; @@ -8,9 +6,11 @@ import { ReactElement } from "react"; import Card from "../card"; import CountryFlag from "../country-flag"; import { Avatar, AvatarImage } from "../ui/avatar"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; -import { scoresaberService } from "@/common/service/impl/scoresaber"; import { PlayerRankingSkeleton } from "@/components/ranking/player-ranking-skeleton"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import { ScoreSaberPlayersPageToken } from "@ssr/common/types/token/scoresaber/score-saber-players-page-token"; +import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; +import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; const PLAYER_NAME_MAX_LENGTH = 18; diff --git a/projects/website/src/components/ranking/player-ranking-skeleton.tsx b/projects/website/src/components/ranking/player-ranking-skeleton.tsx index 8a27c9c..be962dc 100644 --- a/projects/website/src/components/ranking/player-ranking-skeleton.tsx +++ b/projects/website/src/components/ranking/player-ranking-skeleton.tsx @@ -1,5 +1,5 @@ import Card from "@/components/card"; -import { Skeleton } from "@/app/components/ui/skeleton"; +import { Skeleton } from "@/components/ui/skeleton"; export function PlayerRankingSkeleton() { const skeletonArray = new Array(5).fill(0); diff --git a/projects/website/src/components/score/score-badge.tsx b/projects/website/src/components/score/score-badge.tsx index 222bec9..86ca9bf 100644 --- a/projects/website/src/components/score/score-badge.tsx +++ b/projects/website/src/components/score/score-badge.tsx @@ -1,6 +1,6 @@ -import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import StatValue from "@/components/stat-value"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; /** * A badge to display in the score stats. diff --git a/projects/website/src/components/score/score-buttons.tsx b/projects/website/src/components/score/score-buttons.tsx index e46abb9..1a214ee 100644 --- a/projects/website/src/components/score/score-buttons.tsx +++ b/projects/website/src/components/score/score-buttons.tsx @@ -1,6 +1,5 @@ "use client"; -import { copyToClipboard } from "../../../../common/src/utils/browser-utils"; import BeatSaverMap from "@/common/database/types/beatsaver-map"; import { songNameToYouTubeLink } from "@/common/youtube-utils"; import BeatSaverLogo from "@/components/logos/beatsaver-logo"; @@ -9,7 +8,8 @@ import { useToast } from "@/hooks/use-toast"; import { Dispatch, SetStateAction } from "react"; import LeaderboardButton from "./leaderboard-button"; import ScoreButton from "./score-button"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; +import { copyToClipboard } from "@/common/browser-utils"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; type Props = { leaderboard: ScoreSaberLeaderboardToken; diff --git a/projects/website/src/components/score/score-info.tsx b/projects/website/src/components/score/score-info.tsx index 1d45ffd..74c6b72 100644 --- a/projects/website/src/components/score/score-info.tsx +++ b/projects/website/src/components/score/score-info.tsx @@ -1,5 +1,4 @@ import BeatSaverMap from "@/common/database/types/beatsaver-map"; -import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token"; import { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils"; import FallbackLink from "@/components/fallback-link"; import Tooltip from "@/components/tooltip"; @@ -8,6 +7,7 @@ import clsx from "clsx"; import Image from "next/image"; import { songDifficultyToColor } from "@/common/song-utils"; import Link from "next/link"; +import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; type Props = { leaderboard: ScoreSaberLeaderboardToken; diff --git a/projects/website/src/components/score/score-rank-info.tsx b/projects/website/src/components/score/score-rank-info.tsx index 4ae20bc..ac8924d 100644 --- a/projects/website/src/components/score/score-rank-info.tsx +++ b/projects/website/src/components/score/score-rank-info.tsx @@ -1,9 +1,9 @@ -import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token"; import { formatNumberWithCommas } from "@/common/number-utils"; -import { timeAgo } from "@/common/time-utils"; import { format } from "@formkit/tempo"; import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import Tooltip from "../tooltip"; +import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; +import { timeAgo } from "@ssr/common/utils/time-utils"; type Props = { score: ScoreSaberScoreToken; diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index 50dc044..ec2ed9e 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -1,16 +1,15 @@ "use client"; import BeatSaverMap from "@/common/database/types/beatsaver-map"; -import ScoreSaberPlayerScoreToken from "@/common/model/token/scoresaber/score-saber-player-score-token"; -import { beatsaverService } from "@/common/service/impl/beatsaver"; import LeaderboardScores from "@/components/leaderboard/leaderboard-scores"; -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; import ScoreButtons from "./score-buttons"; import ScoreSongInfo from "./score-info"; import ScoreRankInfo from "./score-rank-info"; import ScoreStats from "./score-stats"; -import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import { motion } from "framer-motion"; +import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; +import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; type Props = { /** @@ -29,14 +28,15 @@ export default function Score({ player, playerScore }: Props) { const [beatSaverMap, setBeatSaverMap] = useState(); const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false); - const fetchBeatSaverData = useCallback(async () => { - const beatSaverMap = await beatsaverService.lookupMap(leaderboard.songHash); - setBeatSaverMap(beatSaverMap); - }, [leaderboard.songHash]); - - useEffect(() => { - fetchBeatSaverData(); - }, [fetchBeatSaverData]); + // todo: fix + // const fetchBeatSaverData = useCallback(async () => { + // const beatSaverMap = await beatsaverService.lookupMap(leaderboard.songHash); + // setBeatSaverMap(beatSaverMap); + // }, [leaderboard.songHash]); + // + // useEffect(() => { + // fetchBeatSaverData(); + // }, [fetchBeatSaverData]); const page = Math.floor(score.rank / 12) + 1; return ( diff --git a/projects/website/src/app/components/ui/skeleton.tsx b/projects/website/src/components/ui/skeleton.tsx similarity index 100% rename from projects/website/src/app/components/ui/skeleton.tsx rename to projects/website/src/components/ui/skeleton.tsx diff --git a/projects/website/src/jobs/index.ts b/projects/website/src/jobs/index.ts deleted file mode 100644 index 5c5264d..0000000 --- a/projects/website/src/jobs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// export all your job files here - -export * from "./track-player-statistics"; diff --git a/projects/website/src/jobs/track-player-statistics.ts b/projects/website/src/jobs/track-player-statistics.ts deleted file mode 100644 index b216dd7..0000000 --- a/projects/website/src/jobs/track-player-statistics.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { cronTrigger } from "@trigger.dev/sdk"; -import { client } from "@/trigger"; -import { connectMongo } from "@/common/mongo"; -import { getMidnightAlignedDate } from "@/common/time-utils"; -import { IPlayer, PlayerModel } from "@/common/schema/player-schema"; -import { trackScoreSaberPlayer } from "@/common/player-utils"; - -client.defineJob({ - id: "track-player-statistics", - name: "Tracks player statistics", - version: "0.0.1", - trigger: cronTrigger({ - // Run at 00:01 every day (midnight) - cron: "0 1 * * *", - }), - run: async (payload, io) => { - await io.logger.info("Connecting to Mongo"); - await connectMongo(); - - await io.logger.info("Finding players..."); - const players: IPlayer[] = await PlayerModel.find({}); - await io.logger.info(`Found ${players.length} player${players.length > 1 ? "s" : ""}.`); - - const dateToday = getMidnightAlignedDate(new Date()); - for (const foundPlayer of players) { - await io.runTask(`track-player-${foundPlayer.id}`, async () => { - await trackScoreSaberPlayer(dateToday, foundPlayer, io); - }); - } - }, -}); diff --git a/projects/website/src/trigger.ts b/projects/website/src/trigger.ts deleted file mode 100644 index a2cf138..0000000 --- a/projects/website/src/trigger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TriggerClient } from "@trigger.dev/sdk"; - -export const client = new TriggerClient({ - id: "scoresaber-reloaded-KB0Z", - apiKey: process.env.TRIGGER_API_KEY, - apiUrl: process.env.TRIGGER_API_URL, -});