LETS GO BABY
This commit is contained in:
parent
e0fca1168a
commit
e87d73bbdf
@ -25,4 +25,10 @@ spec:
|
|||||||
memory: 128Mi
|
memory: 128Mi
|
||||||
limits:
|
limits:
|
||||||
cpu: 1000m # 1 vCPU
|
cpu: 1000m # 1 vCPU
|
||||||
memory: 512Mi
|
memory: 512Mi
|
||||||
|
env:
|
||||||
|
- name: MONGO_URI
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ssr-backend-secret
|
||||||
|
key: MONGO_URI
|
@ -10,7 +10,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api-test`)
|
- match: Host(`ssr.fascinated.cc`) && PathPrefix(`/api`)
|
||||||
kind: Rule
|
kind: Rule
|
||||||
middlewares:
|
middlewares:
|
||||||
- name: default-headers
|
- name: default-headers
|
||||||
|
16
.gitea/kubernetes/backend/sealed-secrets.yaml
Normal file
16
.gitea/kubernetes/backend/sealed-secrets.yaml
Normal file
@ -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
|
@ -7,4 +7,4 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
stripPrefix:
|
stripPrefix:
|
||||||
prefixes:
|
prefixes:
|
||||||
- "/api-test"
|
- "/api"
|
||||||
|
@ -27,28 +27,8 @@ spec:
|
|||||||
cpu: 1000m # 1 vCPU
|
cpu: 1000m # 1 vCPU
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
env:
|
env:
|
||||||
- name: MONGO_URI
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: ssr-secret
|
|
||||||
key: MONGO_URI
|
|
||||||
- name: NEXT_PUBLIC_SITE_URL
|
- name: NEXT_PUBLIC_SITE_URL
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: ssr-secret
|
name: ssr-secret
|
||||||
key: NEXT_PUBLIC_SITE_URL
|
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
|
|
||||||
|
@ -7,11 +7,7 @@ metadata:
|
|||||||
namespace: public-services
|
namespace: public-services
|
||||||
spec:
|
spec:
|
||||||
encryptedData:
|
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_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:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
|
@ -55,6 +55,7 @@ jobs:
|
|||||||
action: deploy
|
action: deploy
|
||||||
namespace: public-services
|
namespace: public-services
|
||||||
manifests: |
|
manifests: |
|
||||||
|
.gitea/kubernetes/backend/sealed-secrets.yaml
|
||||||
.gitea/kubernetes/backend/deployment.yaml
|
.gitea/kubernetes/backend/deployment.yaml
|
||||||
.gitea/kubernetes/backend/service.yaml
|
.gitea/kubernetes/backend/service.yaml
|
||||||
.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml
|
.gitea/kubernetes/backend/strip-api-prefix-middleware.yaml
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
name: "Deploy Website"
|
name: "Deploy Website"
|
||||||
|
|
||||||
#on:
|
on:
|
||||||
# workflow_dispatch:
|
workflow_dispatch:
|
||||||
# push:
|
push:
|
||||||
# branches:
|
branches:
|
||||||
# - master
|
- master
|
||||||
# paths:
|
paths:
|
||||||
# - projects/website/**
|
- projects/website/**
|
||||||
# - projects/common/**
|
- projects/common/**
|
||||||
# - .gitea/kubernetes/website/**
|
- .gitea/kubernetes/website/**
|
||||||
# - .gitea/workflows/deploy-website.yml
|
- .gitea/workflows/deploy-website.yml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
15
package.json
15
package.json
@ -2,13 +2,18 @@
|
|||||||
"name": "scoresaber-reloaded",
|
"name": "scoresaber-reloaded",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"projects/backend",
|
"projects/*"
|
||||||
"projects/website",
|
|
||||||
"projects/common"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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",
|
"author": "fascinated7",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"superjson": "^2.2.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
4
projects/backend/.gitignore
vendored
4
projects/backend/.gitignore
vendored
@ -39,4 +39,6 @@ yarn-error.log*
|
|||||||
**/*.tgz
|
**/*.tgz
|
||||||
**/*.log
|
**/*.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
**/*.bun
|
**/*.bun
|
||||||
|
|
||||||
|
.env
|
@ -2,21 +2,24 @@
|
|||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
"start": "bun run src/index.ts"
|
"start": "bun run src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bogeychan/elysia-etag": "^0.0.6",
|
"@bogeychan/elysia-etag": "^0.0.6",
|
||||||
|
"@dotenvx/dotenvx": "^1.16.1",
|
||||||
"@elysiajs/cors": "^1.1.1",
|
"@elysiajs/cors": "^1.1.1",
|
||||||
|
"@elysiajs/cron": "^1.1.1",
|
||||||
"@elysiajs/swagger": "^1.1.3",
|
"@elysiajs/swagger": "^1.1.3",
|
||||||
"@ssr/common": "workspace:common",
|
"@ssr/common": "workspace:common",
|
||||||
"@tqman/nice-logger": "^1.0.1",
|
"@tqman/nice-logger": "^1.0.1",
|
||||||
|
"@typegoose/typegoose": "^12.8.0",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"elysia-autoroutes": "^0.5.0",
|
"elysia-autoroutes": "^0.5.0",
|
||||||
"elysia-decorators": "^1.0.2",
|
"elysia-decorators": "^1.0.2",
|
||||||
"elysia-helmet": "^2.0.0",
|
"elysia-helmet": "^2.0.0",
|
||||||
"elysia-rate-limit": "^4.1.0"
|
"elysia-rate-limit": "^4.1.0",
|
||||||
|
"mongoose": "^8.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
|
3
projects/backend/src/common/config.ts
Normal file
3
projects/backend/src/common/config.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const Config = {
|
||||||
|
mongoUri: process.env.MONGO_URI,
|
||||||
|
}
|
54
projects/backend/src/controller/player.controller.ts
Normal file
54
projects/backend/src/controller/player.controller.ts
Normal file
@ -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<string, PlayerHistory> }> {
|
||||||
|
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<PlayerTrackedSince> {
|
||||||
|
try {
|
||||||
|
const player = await PlayerService.getPlayer(id, createIfMissing);
|
||||||
|
return {
|
||||||
|
tracked: true,
|
||||||
|
daysTracked: player.getDaysTracked(),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
tracked: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
projects/backend/src/error/not-found-error.ts
Normal file
10
projects/backend/src/error/not-found-error.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -2,10 +2,9 @@ import { HttpCode } from "../common/http-codes";
|
|||||||
|
|
||||||
export class RateLimitError extends Error {
|
export class RateLimitError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public message: string = 'rate-limited',
|
public message: string = "rate-limited",
|
||||||
public detail: string = '',
|
|
||||||
public status: number = HttpCode.TOO_MANY_REQUESTS.code
|
public status: number = HttpCode.TOO_MANY_REQUESTS.code
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,14 +2,29 @@ import { Elysia } from "elysia";
|
|||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import { decorators } from "elysia-decorators";
|
import { decorators } from "elysia-decorators";
|
||||||
import { logger } from "@tqman/nice-logger";
|
import { logger } from "@tqman/nice-logger";
|
||||||
import { swagger } from '@elysiajs/swagger'
|
import { swagger } from "@elysiajs/swagger";
|
||||||
import { rateLimit } from 'elysia-rate-limit'
|
import { rateLimit } from "elysia-rate-limit";
|
||||||
import { RateLimitError } from "./error/rate-limit-error";
|
import { RateLimitError } from "./error/rate-limit-error";
|
||||||
import { helmet } from 'elysia-helmet';
|
import { helmet } from "elysia-helmet";
|
||||||
import { etag } from '@bogeychan/elysia-etag'
|
import { etag } from "@bogeychan/elysia-etag";
|
||||||
import AppController from "./controller/app.controller";
|
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
|
* Custom error handler
|
||||||
@ -50,33 +65,37 @@ app.use(
|
|||||||
/**
|
/**
|
||||||
* Rate limit (100 requests per minute)
|
* Rate limit (100 requests per minute)
|
||||||
*/
|
*/
|
||||||
app.use(rateLimit({
|
app.use(
|
||||||
scoping: "global",
|
rateLimit({
|
||||||
duration: 60 * 1000,
|
scoping: "global",
|
||||||
max: 100,
|
duration: 60 * 1000,
|
||||||
skip: (request) => {
|
max: 100,
|
||||||
let [ _, path ] = request.url.split("/"); // Get the url parts
|
skip: request => {
|
||||||
path === "" || path === undefined && (path = "/"); // If we're on /, the path is undefined, so we set it to /
|
let [_, path] = request.url.split("/"); // Get the url parts
|
||||||
return path === "/"; // ignore all requests to /
|
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"),
|
},
|
||||||
}))
|
errorResponse: new RateLimitError("Too many requests, please try again later"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security settings
|
* Security settings
|
||||||
*/
|
*/
|
||||||
app.use(helmet({
|
app.use(
|
||||||
hsts: false, // Disable HSTS
|
helmet({
|
||||||
contentSecurityPolicy: false, // Disable CSP
|
hsts: false, // Disable HSTS
|
||||||
dnsPrefetchControl: true, // Enable DNS prefetch
|
contentSecurityPolicy: false, // Disable CSP
|
||||||
}))
|
dnsPrefetchControl: true, // Enable DNS prefetch
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controllers
|
* Controllers
|
||||||
*/
|
*/
|
||||||
app.use(
|
app.use(
|
||||||
decorators({
|
decorators({
|
||||||
controllers: [AppController],
|
controllers: [AppController, PlayerController],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -89,4 +108,9 @@ app.onStart(() => {
|
|||||||
console.log("Listening on port http://localhost:8080");
|
console.log("Listening on port http://localhost:8080");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start cronjobs
|
||||||
|
*/
|
||||||
|
PlayerService.initCronjobs();
|
||||||
|
|
||||||
app.listen(8080);
|
app.listen(8080);
|
||||||
|
108
projects/backend/src/model/player.ts
Normal file
108
projects/backend/src/model/player.ts
Normal file
@ -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<string, PlayerHistory>;
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public lastTracked?: Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the player's statistic history.
|
||||||
|
*/
|
||||||
|
public getStatisticHistory(): Record<string, PlayerHistory> {
|
||||||
|
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<string, PlayerHistory> {
|
||||||
|
if (this.statisticHistory === undefined) {
|
||||||
|
this.statisticHistory = {};
|
||||||
|
}
|
||||||
|
const history: Record<string, PlayerHistory> = {};
|
||||||
|
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<typeof Player> = getModelForClass(Player);
|
126
projects/backend/src/service/player.service.ts
Normal file
126
projects/backend/src/service/player.service.ts
Normal file
@ -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<PlayerDocument> {
|
||||||
|
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<void> {
|
||||||
|
// 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}"!`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2021",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "Bundler",
|
||||||
"types": ["bun-types"],
|
"types": ["bun-types"],
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,19 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsup src/index.ts --watch",
|
"dev": "tsc --watch --preserveWatchOutput",
|
||||||
"build": "tsup src/index.ts"
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./*": {
|
||||||
|
"types": "./dist/*.d.ts",
|
||||||
|
"import": "./dist/*.js",
|
||||||
|
"require": "./dist/*.js",
|
||||||
|
"default": "./dist/*.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.7.4",
|
"@types/node": "^22.7.4",
|
||||||
"tsup": "^8",
|
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -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";
|
|
@ -1,7 +1,6 @@
|
|||||||
import Service from "../service";
|
import Service from "../service";
|
||||||
import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-token";
|
import { ScoreSaberPlayerSearchToken } from "../../types/token/scoresaber/score-saber-player-search-token";
|
||||||
import ScoreSaberPlayerToken from "../../types/token/scoresaber/score-saber-player-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 { ScoreSaberPlayersPageToken } from "../../types/token/scoresaber/score-saber-players-page-token";
|
||||||
import { ScoreSort } from "../../types/score/score-sort";
|
import { ScoreSort } from "../../types/score/score-sort";
|
||||||
import ScoreSaberPlayerScoresPageToken from "../../types/token/scoresaber/score-saber-player-scores-page-token";
|
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.
|
* Looks up a player by their ID.
|
||||||
*
|
*
|
||||||
* @param playerId the ID of the player to look up
|
* @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
|
* @returns the player that matches the ID, or undefined
|
||||||
*/
|
*/
|
||||||
async lookupPlayer(
|
async lookupPlayer(playerId: string): Promise<ScoreSaberPlayerToken | undefined> {
|
||||||
playerId: string,
|
|
||||||
apiUrl: string
|
|
||||||
): Promise<
|
|
||||||
| {
|
|
||||||
player: ScoreSaberPlayer;
|
|
||||||
rawPlayer: ScoreSaberPlayerToken;
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
> {
|
|
||||||
const before = performance.now();
|
const before = performance.now();
|
||||||
this.log(`Looking up player "${playerId}"...`);
|
this.log(`Looking up player "${playerId}"...`);
|
||||||
const token = await this.fetch<ScoreSaberPlayerToken>(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId));
|
const token = await this.fetch<ScoreSaberPlayerToken>(LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId));
|
||||||
@ -75,10 +64,7 @@ class ScoreSaberService extends Service {
|
|||||||
return 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 {
|
return token;
|
||||||
player: await getScoreSaberPlayerFromToken(apiUrl, token),
|
|
||||||
rawPlayer: token,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,7 @@ import ky from "ky";
|
|||||||
import { PlayerHistory } from "../player-history";
|
import { PlayerHistory } from "../player-history";
|
||||||
import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token";
|
import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token";
|
||||||
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils";
|
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils";
|
||||||
|
import { getPlayerIdCookie } from "website/src/common/website-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ScoreSaber player.
|
* A ScoreSaber player.
|
||||||
@ -65,9 +66,15 @@ export default interface ScoreSaberPlayer extends Player {
|
|||||||
isBeingTracked?: boolean;
|
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(
|
export async function getScoreSaberPlayerFromToken(
|
||||||
apiUrl: string,
|
token: ScoreSaberPlayerToken,
|
||||||
token: ScoreSaberPlayerToken
|
apiUrl: string
|
||||||
): Promise<ScoreSaberPlayer> {
|
): Promise<ScoreSaberPlayer> {
|
||||||
const bio: ScoreSaberBio = {
|
const bio: ScoreSaberBio = {
|
||||||
lines: token.bio?.split("\n") || [],
|
lines: token.bio?.split("\n") || [],
|
||||||
@ -86,10 +93,10 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
|
const todayDate = formatDateMinimal(getMidnightAlignedDate(new Date()));
|
||||||
let statisticHistory: { [key: string]: PlayerHistory } = {};
|
let statisticHistory: { [key: string]: PlayerHistory } = {};
|
||||||
try {
|
try {
|
||||||
const history = await ky
|
const { statistics: history } = await ky
|
||||||
.get<{
|
.get<{
|
||||||
[key: string]: PlayerHistory;
|
statistics: { [key: string]: PlayerHistory };
|
||||||
}>(`${apiUrl}/api/player/history?id=${token.id}`)
|
}>(`${apiUrl}/player/history/${token.id}${getPlayerIdCookie() == token.id ? "?createIfMissing=true" : ""}`)
|
||||||
.json();
|
.json();
|
||||||
if (history === undefined || Object.entries(history).length === 0) {
|
if (history === undefined || Object.entries(history).length === 0) {
|
||||||
console.log("Player has no history, using fallback");
|
console.log("Player has no history, using fallback");
|
||||||
|
@ -4,11 +4,6 @@ export interface PlayerTrackedSince {
|
|||||||
*/
|
*/
|
||||||
tracked: boolean;
|
tracked: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* The date the player was first tracked
|
|
||||||
*/
|
|
||||||
trackedSince?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of days the player has been tracked
|
* The amount of days the player has been tracked
|
||||||
*/
|
*/
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"declaration": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"declaration": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"allowSyntheticDefaultImports": true,
|
||||||
"noImplicitAny": false,
|
"strict": true,
|
||||||
"strictBindCallApply": false,
|
"baseUrl": "./",
|
||||||
"forceConsistentCasingInFileNames": false,
|
"paths": {
|
||||||
"noFallthroughCasesInSwitch": false
|
"@ssr/*": ["dist/*"] // This is crucial for resolving the imports correctly
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
});
|
|
@ -1,7 +1,2 @@
|
|||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=
|
NEXT_PUBLIC_SITE_API=http://localhost:8080
|
||||||
|
|
||||||
TRIGGER_API_KEY=
|
|
||||||
TRIGGER_API_URL=https://trigger.example.com
|
|
||||||
MONGO_URI=mongodb://127.0.0.1:27017
|
|
||||||
SENTRY_AUTH_TOKEN=
|
|
@ -1,25 +1,30 @@
|
|||||||
FROM node:20-alpine3.17 AS base
|
FROM oven/bun:1.1.30-alpine AS base
|
||||||
|
|
||||||
# Install pnpm
|
# Install dependencies
|
||||||
RUN npm install -g pnpm
|
FROM base AS depends
|
||||||
ENV PNPM_HOME=/usr/local/bin
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Run the app
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy website package and lock files only
|
# Set the environment
|
||||||
COPY package.json* pnpm-lock.yaml* pnpm-workspace.yaml* ./
|
ENV NODE_ENV production
|
||||||
COPY website ./website
|
|
||||||
|
|
||||||
ARG GIT_REV
|
ARG GIT_REV
|
||||||
ENV GIT_REV=${GIT_REV}
|
ENV GIT_REV=${GIT_REV}
|
||||||
|
|
||||||
RUN pnpm install --filter website
|
COPY --from=depends /app/node_modules ./node_modules
|
||||||
RUN pnpm run build:website
|
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
|
EXPOSE 3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|
||||||
CMD ["pnpm", "start:website"]
|
CMD ["bun", "run", "--filter", "backend", "start"]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const config = {
|
export const config = {
|
||||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc",
|
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc",
|
||||||
|
siteApi: process.env.NEXT_PUBLIC_SITE_API || "https://ssr.fascinated.cc/api",
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
@ -22,9 +22,6 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@sentry/nextjs": "8",
|
"@sentry/nextjs": "8",
|
||||||
"@tanstack/react-query": "^5.55.4",
|
"@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",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@ -36,7 +33,6 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
"lucide-react": "^0.447.0",
|
"lucide-react": "^0.447.0",
|
||||||
"mongoose": "^8.7.0",
|
|
||||||
"next": "15.0.0-rc.0",
|
"next": "15.0.0-rc.0",
|
||||||
"next-build-id": "^3.0.0",
|
"next-build-id": "^3.0.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
@ -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));
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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": "*",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
@ -1,14 +1,16 @@
|
|||||||
import { formatNumberWithCommas, formatPp } from "@/common/number-utils";
|
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 PlayerData from "@/components/player/player-data";
|
||||||
import { format } from "@formkit/tempo";
|
import { format } from "@formkit/tempo";
|
||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Colors } from "@/common/colors";
|
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 { getAverageColor } from "@/common/image-utils";
|
||||||
import { cache } from "react";
|
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 = {
|
const UNKNOWN_PLAYER = {
|
||||||
title: "ScoreSaber Reloaded - 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 page = parseInt(slug[2]) || 1; // The page number
|
||||||
const search = (slug[3] as string) || ""; // The search query
|
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;
|
let scores: ScoreSaberPlayerScoresPageToken | undefined;
|
||||||
if (fetchScores) {
|
if (fetchScores) {
|
||||||
scores = await scoresaberService.lookupPlayerScores({
|
scores = await scoresaberService.lookupPlayerScores({
|
||||||
|
23
projects/website/src/common/database/types/beatsaver-map.ts
Normal file
23
projects/website/src/common/database/types/beatsaver-map.ts
Normal file
@ -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<Database> {
|
||||||
|
/**
|
||||||
|
* The hash of the map.
|
||||||
|
*/
|
||||||
|
hash!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bsr code for the map.
|
||||||
|
*/
|
||||||
|
bsr!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full data for the map.
|
||||||
|
*/
|
||||||
|
fullData!: BeatSaverMapToken;
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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}
|
* Gets a value from an {@link PlayerHistory}
|
||||||
|
@ -9,6 +9,15 @@ export function setPlayerIdCookie(playerId: string) {
|
|||||||
Cookies.set("playerId", playerId, { path: "/" });
|
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
|
* Gets if we're in production
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as Comlink from "comlink";
|
import * as Comlink from "comlink";
|
||||||
import { scoresaberService } from "@/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
|
|
||||||
export interface WorkerApi {
|
export interface WorkerApi {
|
||||||
getPlayerExample: typeof getPlayerExample;
|
getPlayerExample: typeof getPlayerExample;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"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 { formatNumberWithCommas } from "@/common/number-utils";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -13,6 +11,8 @@ import { Button } from "../ui/button";
|
|||||||
import { Form, FormControl, FormField, FormItem, FormLabel } from "../ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel } from "../ui/form";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
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({
|
const formSchema = z.object({
|
||||||
username: z.string().min(3).max(50),
|
username: z.string().min(3).max(50),
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
"use client";
|
"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 LeaderboardScores from "@/components/leaderboard/leaderboard-scores";
|
||||||
import { LeaderboardInfo } from "@/components/leaderboard/leaderboard-info";
|
import { LeaderboardInfo } from "@/components/leaderboard/leaderboard-info";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { scoresaberService } from "@/common/service/impl/scoresaber";
|
import { useEffect, useState } from "react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
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 = {
|
type LeaderboardDataProps = {
|
||||||
/**
|
/**
|
||||||
@ -39,14 +38,15 @@ export function LeaderboardData({ initialPage, initialScores, initialLeaderboard
|
|||||||
staleTime: 30 * 1000, // Cache data for 30 seconds
|
staleTime: 30 * 1000, // Cache data for 30 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchBeatSaverData = useCallback(async () => {
|
// todo: fix
|
||||||
const beatSaverMap = await beatsaverService.lookupMap(initialLeaderboard.songHash);
|
// const fetchBeatSaverData = useCallback(async () => {
|
||||||
setBeatSaverMap(beatSaverMap);
|
// const beatSaverMap = await beatsaverService.lookupMap(initialLeaderboard.songHash);
|
||||||
}, [initialLeaderboard.songHash]);
|
// setBeatSaverMap(beatSaverMap);
|
||||||
|
// }, [initialLeaderboard.songHash]);
|
||||||
useEffect(() => {
|
//
|
||||||
fetchBeatSaverData();
|
// useEffect(() => {
|
||||||
}, [fetchBeatSaverData]);
|
// fetchBeatSaverData();
|
||||||
|
// }, [fetchBeatSaverData]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the leaderboard changes, update the previous and current leaderboards.
|
* When the leaderboard changes, update the previous and current leaderboards.
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-saber-leaderboard-token";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { LeaderboardSongStarCount } from "@/components/leaderboard/leaderboard-song-star-count";
|
import { LeaderboardSongStarCount } from "@/components/leaderboard/leaderboard-song-star-count";
|
||||||
import ScoreButtons from "@/components/score/score-buttons";
|
import ScoreButtons from "@/components/score/score-buttons";
|
||||||
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
||||||
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
|
|
||||||
type LeaderboardInfoProps = {
|
type LeaderboardInfoProps = {
|
||||||
/**
|
/**
|
||||||
|
@ -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 Image from "next/image";
|
||||||
import Link from "next/link";
|
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 = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
|
@ -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 { formatNumberWithCommas } from "@/common/number-utils";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { getScoreBadgeFromAccuracy } from "@/common/song-utils";
|
import { getScoreBadgeFromAccuracy } from "@/common/song-utils";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import { ScoreBadge, ScoreBadges } from "@/components/score/score-badge";
|
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[] = [
|
const badges: ScoreBadge[] = [
|
||||||
{
|
{
|
||||||
|
@ -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 LeaderboardPlayer from "./leaderboard-player";
|
||||||
import LeaderboardScoreStats from "./leaderboard-score-stats";
|
import LeaderboardScoreStats from "./leaderboard-score-stats";
|
||||||
import ScoreRankInfo from "@/components/score/score-rank-info";
|
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 = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
"use client";
|
"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 useWindowDimensions from "@/hooks/use-window-dimensions";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion, useAnimation } from "framer-motion";
|
import { motion, useAnimation } from "framer-motion";
|
||||||
@ -11,10 +8,13 @@ import Card from "../card";
|
|||||||
import Pagination from "../input/pagination";
|
import Pagination from "../input/pagination";
|
||||||
import LeaderboardScore from "./leaderboard-score";
|
import LeaderboardScore from "./leaderboard-score";
|
||||||
import { scoreAnimation } from "@/components/score/score-animation";
|
import { scoreAnimation } from "@/components/score/score-animation";
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { getDifficultyFromRawDifficulty } from "@/common/song-utils";
|
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 = {
|
type LeaderboardScoresProps = {
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { songDifficultyToColor } from "@/common/song-utils";
|
import { songDifficultyToColor } from "@/common/song-utils";
|
||||||
import { StarIcon } from "@heroicons/react/24/solid";
|
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 { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils";
|
||||||
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
|
|
||||||
type LeaderboardSongStarCountProps = {
|
type LeaderboardSongStarCountProps = {
|
||||||
/**
|
/**
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { parseDate } from "@/common/time-utils";
|
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
|
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
import { getValueFromHistory } from "@/common/player-utils";
|
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 = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DatasetConfig } from "@/components/chart/generic-chart";
|
import { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
||||||
|
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import PlayerRankingChart from "@/components/player/chart/player-ranking-chart";
|
import PlayerRankingChart from "@/components/player/chart/player-ranking-chart";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import PlayerAccuracyChart from "@/components/player/chart/player-accuracy-chart";
|
import PlayerAccuracyChart from "@/components/player/chart/player-accuracy-chart";
|
||||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
import { TrendingUpIcon } from "lucide-react";
|
import { TrendingUpIcon } from "lucide-react";
|
||||||
|
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
|
||||||
|
|
||||||
type PlayerChartsProps = {
|
type PlayerChartsProps = {
|
||||||
/**
|
/**
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
import { formatNumberWithCommas } from "@/common/number-utils";
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DatasetConfig } from "@/components/chart/generic-chart";
|
import { DatasetConfig } from "@/components/chart/generic-chart";
|
||||||
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
import GenericPlayerChart from "@/components/player/chart/generic-player-chart";
|
||||||
|
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
|
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
"use client";
|
"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 { useQuery } from "@tanstack/react-query";
|
||||||
import Mini from "../ranking/mini";
|
import Mini from "../ranking/mini";
|
||||||
import PlayerHeader from "./player-header";
|
import PlayerHeader from "./player-header";
|
||||||
import PlayerScores from "./player-scores";
|
import PlayerScores from "./player-scores";
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
import PlayerBadges from "@/components/player/player-badges";
|
import PlayerBadges from "@/components/player/player-badges";
|
||||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||||
import { useIsVisible } from "@/hooks/use-is-visible";
|
import { useIsVisible } from "@/hooks/use-is-visible";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import PlayerCharts from "@/components/player/chart/player-charts";
|
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 = {
|
type Props = {
|
||||||
initialPlayerData: ScoreSaberPlayer;
|
initialPlayerData: ScoreSaberPlayer;
|
||||||
@ -37,12 +38,18 @@ export default function PlayerData({
|
|||||||
let player = initialPlayerData;
|
let player = initialPlayerData;
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["player", player.id],
|
queryKey: ["player", player.id],
|
||||||
queryFn: () => scoresaberService.lookupPlayer(player.id),
|
queryFn: async (): Promise<ScoreSaberPlayer | undefined> => {
|
||||||
|
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
|
staleTime: 1000 * 60 * 5, // Cache data for 5 minutes
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data && (!isLoading || !isError)) {
|
if (data && (!isLoading || !isError)) {
|
||||||
player = data.player;
|
player = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,15 +7,15 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import Card from "../card";
|
import Card from "../card";
|
||||||
import Pagination from "../input/pagination";
|
import Pagination from "../input/pagination";
|
||||||
import { Button } from "../ui/button";
|
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 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 { Input } from "@/components/ui/input";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { useDebounce } from "@uidotdev/usehooks";
|
import { useDebounce } from "@uidotdev/usehooks";
|
||||||
import { scoreAnimation } from "@/components/score/score-animation";
|
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 = {
|
type Props = {
|
||||||
initialScoreData?: ScoreSaberPlayerScoresPageToken;
|
initialScoreData?: ScoreSaberPlayerScoresPageToken;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
import { formatNumberWithCommas } from "@/common/number-utils";
|
||||||
import StatValue from "@/components/stat-value";
|
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 = {
|
type Badge = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
import { config } from "../../../config";
|
import { config } from "../../../config";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
import { InformationCircleIcon } from "@heroicons/react/16/solid";
|
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 { 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 = {
|
type Props = {
|
||||||
player: ScoreSaberPlayer;
|
player: ScoreSaberPlayer;
|
||||||
@ -18,32 +16,19 @@ type Props = {
|
|||||||
export default function PlayerTrackedStatus({ player }: Props) {
|
export default function PlayerTrackedStatus({ player }: Props) {
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["playerIsBeingTracked", player.id],
|
queryKey: ["playerIsBeingTracked", player.id],
|
||||||
queryFn: () => ky.get<PlayerTrackedSince>(`${config.siteUrl}/api/player/isbeingtracked?id=${player.id}`).json(),
|
queryFn: () => ky.get<PlayerTrackedSince>(`${config.siteApi}/player/tracked/${player.id}`).json(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || isError || !data?.tracked) {
|
if (isLoading || isError || !data?.tracked) {
|
||||||
return undefined;
|
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 (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
display={
|
display={
|
||||||
<div className="flex flex-col justify-center items-center">
|
<div className="flex flex-col justify-center items-center">
|
||||||
<p>This player is having their statistics tracked!</p>
|
<p>This player is having their statistics tracked!</p>
|
||||||
<p>
|
|
||||||
Tracked Since: {format(trackedSince)} ({daysAgoFormatted})
|
|
||||||
</p>
|
|
||||||
<p>Days Tracked: {formatNumberWithCommas(data.daysTracked!)}</p>
|
<p>Days Tracked: {formatNumberWithCommas(data.daysTracked!)}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -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 { formatNumberWithCommas, formatPp } from "@/common/number-utils";
|
||||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@ -8,9 +6,11 @@ import { ReactElement } from "react";
|
|||||||
import Card from "../card";
|
import Card from "../card";
|
||||||
import CountryFlag from "../country-flag";
|
import CountryFlag from "../country-flag";
|
||||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
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 { 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;
|
const PLAYER_NAME_MAX_LENGTH = 18;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Card from "@/components/card";
|
import Card from "@/components/card";
|
||||||
import { Skeleton } from "@/app/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
export function PlayerRankingSkeleton() {
|
export function PlayerRankingSkeleton() {
|
||||||
const skeletonArray = new Array(5).fill(0);
|
const skeletonArray = new Array(5).fill(0);
|
||||||
|
@ -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 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.
|
* A badge to display in the score stats.
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { copyToClipboard } from "../../../../common/src/utils/browser-utils";
|
|
||||||
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
||||||
import { songNameToYouTubeLink } from "@/common/youtube-utils";
|
import { songNameToYouTubeLink } from "@/common/youtube-utils";
|
||||||
import BeatSaverLogo from "@/components/logos/beatsaver-logo";
|
import BeatSaverLogo from "@/components/logos/beatsaver-logo";
|
||||||
@ -9,7 +8,8 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import LeaderboardButton from "./leaderboard-button";
|
import LeaderboardButton from "./leaderboard-button";
|
||||||
import ScoreButton from "./score-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 = {
|
type Props = {
|
||||||
leaderboard: ScoreSaberLeaderboardToken;
|
leaderboard: ScoreSaberLeaderboardToken;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
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 { getDifficultyFromScoreSaberDifficulty } from "@/common/scoresaber-utils";
|
||||||
import FallbackLink from "@/components/fallback-link";
|
import FallbackLink from "@/components/fallback-link";
|
||||||
import Tooltip from "@/components/tooltip";
|
import Tooltip from "@/components/tooltip";
|
||||||
@ -8,6 +7,7 @@ import clsx from "clsx";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { songDifficultyToColor } from "@/common/song-utils";
|
import { songDifficultyToColor } from "@/common/song-utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
leaderboard: ScoreSaberLeaderboardToken;
|
leaderboard: ScoreSaberLeaderboardToken;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import ScoreSaberScoreToken from "@/common/model/token/scoresaber/score-saber-score-token";
|
|
||||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
import { formatNumberWithCommas } from "@/common/number-utils";
|
||||||
import { timeAgo } from "@/common/time-utils";
|
|
||||||
import { format } from "@formkit/tempo";
|
import { format } from "@formkit/tempo";
|
||||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
import Tooltip from "../tooltip";
|
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 = {
|
type Props = {
|
||||||
score: ScoreSaberScoreToken;
|
score: ScoreSaberScoreToken;
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BeatSaverMap from "@/common/database/types/beatsaver-map";
|
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 LeaderboardScores from "@/components/leaderboard/leaderboard-scores";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import ScoreButtons from "./score-buttons";
|
import ScoreButtons from "./score-buttons";
|
||||||
import ScoreSongInfo from "./score-info";
|
import ScoreSongInfo from "./score-info";
|
||||||
import ScoreRankInfo from "./score-rank-info";
|
import ScoreRankInfo from "./score-rank-info";
|
||||||
import ScoreStats from "./score-stats";
|
import ScoreStats from "./score-stats";
|
||||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
|
||||||
import { motion } from "framer-motion";
|
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 = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
@ -29,14 +28,15 @@ export default function Score({ player, playerScore }: Props) {
|
|||||||
const [beatSaverMap, setBeatSaverMap] = useState<BeatSaverMap | undefined>();
|
const [beatSaverMap, setBeatSaverMap] = useState<BeatSaverMap | undefined>();
|
||||||
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
|
const [isLeaderboardExpanded, setIsLeaderboardExpanded] = useState(false);
|
||||||
|
|
||||||
const fetchBeatSaverData = useCallback(async () => {
|
// todo: fix
|
||||||
const beatSaverMap = await beatsaverService.lookupMap(leaderboard.songHash);
|
// const fetchBeatSaverData = useCallback(async () => {
|
||||||
setBeatSaverMap(beatSaverMap);
|
// const beatSaverMap = await beatsaverService.lookupMap(leaderboard.songHash);
|
||||||
}, [leaderboard.songHash]);
|
// setBeatSaverMap(beatSaverMap);
|
||||||
|
// }, [leaderboard.songHash]);
|
||||||
useEffect(() => {
|
//
|
||||||
fetchBeatSaverData();
|
// useEffect(() => {
|
||||||
}, [fetchBeatSaverData]);
|
// fetchBeatSaverData();
|
||||||
|
// }, [fetchBeatSaverData]);
|
||||||
|
|
||||||
const page = Math.floor(score.rank / 12) + 1;
|
const page = Math.floor(score.rank / 12) + 1;
|
||||||
return (
|
return (
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
// export all your job files here
|
|
||||||
|
|
||||||
export * from "./track-player-statistics";
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
@ -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,
|
|
||||||
});
|
|
Reference in New Issue
Block a user