This commit is contained in:
parent
d2df95381c
commit
1792648e8d
@ -31,6 +31,12 @@ const nextConfig = {
|
|||||||
port: "",
|
port: "",
|
||||||
pathname: "/**",
|
pathname: "/**",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "eu.cdn.beatsaver.com",
|
||||||
|
port: "",
|
||||||
|
pathname: "/**",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
278
package-lock.json
generated
278
package-lock.json
generated
@ -10,10 +10,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boiseitguru/cookie-cutter": "^0.2.1",
|
"@boiseitguru/cookie-cutter": "^0.2.1",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@sentry/nextjs": "^7.74.1",
|
"@sentry/nextjs": "^7.74.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
@ -35,6 +38,7 @@
|
|||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"websocket": "^1.0.34",
|
||||||
"zustand": "^4.4.3"
|
"zustand": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -43,6 +47,7 @@
|
|||||||
"@types/node-fetch-cache": "^3.0.3",
|
"@types/node-fetch-cache": "^3.0.3",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/websocket": "^1.0.8",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
@ -695,6 +700,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popover": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz",
|
||||||
@ -834,6 +862,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.1",
|
||||||
|
"@radix-ui/react-use-size": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-separator": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz",
|
||||||
@ -908,6 +999,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.1",
|
||||||
|
"@radix-ui/react-use-size": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
||||||
@ -1483,6 +1603,15 @@
|
|||||||
"integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==",
|
"integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/websocket": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-wvkOpWApbuxVfHhSQ1XrjVN4363vsfLJwEo4AboIZk0g1vJA5nmLp8GXUHuIdf4/Fe7+/V0Efe2HvWiLqHtlqw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "6.8.0",
|
"version": "6.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
|
||||||
@ -2076,6 +2205,18 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bufferutil": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@ -2372,6 +2513,15 @@
|
|||||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
|
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.50",
|
||||||
|
"type": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@ -2695,6 +2845,39 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.62",
|
||||||
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
|
||||||
|
"integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"ext": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||||
@ -3148,6 +3331,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ext/node_modules/type": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -4072,6 +4268,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-typedarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
|
||||||
|
},
|
||||||
"node_modules/is-weakmap": {
|
"node_modules/is-weakmap": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
|
||||||
@ -4597,6 +4798,11 @@
|
|||||||
"react-dom": "*"
|
"react-dom": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||||
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
"version": "3.51.0",
|
"version": "3.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
|
||||||
@ -4661,6 +4867,16 @@
|
|||||||
"webidl-conversions": "^3.0.0"
|
"webidl-conversions": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz",
|
||||||
|
"integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.13",
|
"version": "2.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||||
@ -6310,6 +6526,11 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@ -6399,6 +6620,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray-to-buffer": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-typedarray": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
||||||
@ -6537,6 +6766,18 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utf-8-validate": {
|
||||||
|
"version": "5.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
|
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -6594,6 +6835,35 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/websocket": {
|
||||||
|
"version": "1.0.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
|
||||||
|
"integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"debug": "^2.2.0",
|
||||||
|
"es5-ext": "^0.10.50",
|
||||||
|
"typedarray-to-buffer": "^3.1.5",
|
||||||
|
"utf-8-validate": "^5.0.2",
|
||||||
|
"yaeti": "^0.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -6710,6 +6980,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaeti": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
@ -11,10 +11,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boiseitguru/cookie-cutter": "^0.2.1",
|
"@boiseitguru/cookie-cutter": "^0.2.1",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@sentry/nextjs": "^7.74.1",
|
"@sentry/nextjs": "^7.74.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
@ -36,6 +39,7 @@
|
|||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"websocket": "^1.0.34",
|
||||||
"zustand": "^4.4.3"
|
"zustand": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -44,6 +48,7 @@
|
|||||||
"@types/node-fetch-cache": "^3.0.3",
|
"@types/node-fetch-cache": "^3.0.3",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/websocket": "^1.0.8",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
|
@ -3,7 +3,6 @@ import { ssrSettings } from "@/ssrSettings";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Image from "next/image";
|
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
@ -49,16 +48,7 @@ export default function RootLayout({
|
|||||||
src="https://analytics.fascinated.cc/js/script.js"
|
src="https://analytics.fascinated.cc/js/script.js"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<body className={clsx(font.className, "bg-black text-primary")}>
|
<body className={clsx(font.className, "text-primary")}>
|
||||||
<div className="fixed left-0 top-0 z-0 h-full w-full blur-sm">
|
|
||||||
<Image
|
|
||||||
className="object-fill object-center"
|
|
||||||
alt="Background image"
|
|
||||||
src={"/assets/background.webp"}
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AppProvider>{children}</AppProvider>
|
<AppProvider>{children}</AppProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
115
src/app/overlay/builder/page.tsx
Normal file
115
src/app/overlay/builder/page.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Container from "@/components/Container";
|
||||||
|
import { Input } from "@/components/input/Input";
|
||||||
|
import { RadioInput } from "@/components/input/RadioInput";
|
||||||
|
import { SwitchInput } from "@/components/input/SwitchInput";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useOverlaySettingsStore } from "@/store/overlaySettingsStore";
|
||||||
|
import useStore from "@/utils/useStore";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the overlay with the current settings
|
||||||
|
*
|
||||||
|
* @param settings the settings to pass to the overlay
|
||||||
|
*/
|
||||||
|
function goToOverlay(settings: any) {
|
||||||
|
window.open(`/overlay?data=${JSON.stringify(settings)}`, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Analytics() {
|
||||||
|
const settingsStore = useStore(useOverlaySettingsStore, (store) => store);
|
||||||
|
if (!settingsStore) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Container>
|
||||||
|
<Card className="mt-2">
|
||||||
|
<CardTitle className="p-3">
|
||||||
|
<h1>Overlay Builder</h1>
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-2">
|
||||||
|
Confused on how to use this? Check out the{" "}
|
||||||
|
<span className="transform-gpu text-pp-blue transition-all hover:opacity-80">
|
||||||
|
<Link href={"https://www.youtube.com/watch?v=IjctLf1nX8w"}>
|
||||||
|
tutorial
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="ip-address"
|
||||||
|
label="IP Address"
|
||||||
|
defaultValue={settingsStore.ipAddress}
|
||||||
|
onChange={(e) => {
|
||||||
|
settingsStore.setIpAddress(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="account-id"
|
||||||
|
label="Account ID"
|
||||||
|
defaultValue={settingsStore.accountId}
|
||||||
|
onChange={(e) => {
|
||||||
|
settingsStore.setAccountId(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioInput
|
||||||
|
id="platform"
|
||||||
|
label="Platform"
|
||||||
|
defaultValue={settingsStore.platform}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: "scoresaber",
|
||||||
|
value: "ScoreSaber",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: "beatleader",
|
||||||
|
// value: "BeatLeader",
|
||||||
|
// },
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
settingsStore.setPlatform(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<Label>Settings</Label>
|
||||||
|
<SwitchInput
|
||||||
|
id="show-player-stats"
|
||||||
|
label="Show Player Stats"
|
||||||
|
defaultChecked={settingsStore.settings.showPlayerStats}
|
||||||
|
onChange={(value) => {
|
||||||
|
settingsStore.setShowPlayerStats(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SwitchInput
|
||||||
|
id="show-song-info"
|
||||||
|
label="Show Song Info"
|
||||||
|
defaultChecked={settingsStore.settings.showSongInfo}
|
||||||
|
onChange={(value) => {
|
||||||
|
settingsStore.setShowSongInfo(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-3"
|
||||||
|
onClick={() => {
|
||||||
|
goToOverlay(settingsStore);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open Overlay
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
119
src/app/overlay/page.tsx
Normal file
119
src/app/overlay/page.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Container from "@/components/Container";
|
||||||
|
import Spinner from "@/components/Spinner";
|
||||||
|
import PlayerStats from "@/components/overlay/PlayerStats";
|
||||||
|
import ScoreStats from "@/components/overlay/ScoreStats";
|
||||||
|
import SongInfo from "@/components/overlay/SongInfo";
|
||||||
|
import { Card, CardDescription, CardTitle } from "@/components/ui/card";
|
||||||
|
import { HttpSiraStatus } from "@/overlay/httpSiraStatus";
|
||||||
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
|
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||||
|
import { Component } from "react";
|
||||||
|
|
||||||
|
const UPDATE_INTERVAL = 1000 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
interface OverlayProps {}
|
||||||
|
|
||||||
|
interface OverlayState {
|
||||||
|
mounted: boolean;
|
||||||
|
player: ScoresaberPlayer | undefined;
|
||||||
|
settings: any | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Overlay extends Component<OverlayProps, OverlayState> {
|
||||||
|
constructor(props: OverlayProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
mounted: false,
|
||||||
|
player: undefined,
|
||||||
|
settings: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayer = async (playerId: string) => {
|
||||||
|
console.log(`Updating player stats for ${playerId}`);
|
||||||
|
const player = await ScoreSaberAPI.fetchPlayerData(playerId);
|
||||||
|
if (!player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ player });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.state.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ mounted: true });
|
||||||
|
if (!this.state.mounted) {
|
||||||
|
HttpSiraStatus.connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const searchParams = url.searchParams;
|
||||||
|
const data = searchParams.get("data");
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settings = JSON.parse(data);
|
||||||
|
this.setState({ settings: settings });
|
||||||
|
|
||||||
|
this.updatePlayer(settings.accountId);
|
||||||
|
setInterval(() => {
|
||||||
|
this.updatePlayer(settings.accountId);
|
||||||
|
}, UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { player } = this.state;
|
||||||
|
|
||||||
|
if (!this.state.mounted || !player) {
|
||||||
|
return (
|
||||||
|
<main className="flex items-center p-3">
|
||||||
|
<Spinner />
|
||||||
|
<p className="text-xl">Loading player data</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.settings) {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Container>
|
||||||
|
<Card className="mt-2 p-3">
|
||||||
|
<CardTitle>Overlay</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
<p>
|
||||||
|
This page is meant to be used as an overlay for streaming.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To generate an overlay, go to the builder{" "}
|
||||||
|
<a
|
||||||
|
className="transform-gpu text-pp-blue transition-all hover:opacity-80"
|
||||||
|
href="/overlay/builder"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</CardDescription>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<div>
|
||||||
|
<PlayerStats player={player} />
|
||||||
|
<ScoreStats />
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0">
|
||||||
|
<SongInfo />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
import { TooltipProvider } from "./ui/Tooltip";
|
import { TooltipProvider } from "./ui/Tooltip";
|
||||||
import { ThemeProvider } from "./ui/theme-provider";
|
import { ThemeProvider } from "./ui/theme-provider";
|
||||||
|
|
||||||
@ -52,7 +53,15 @@ export default class AppProvider extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
||||||
<TooltipProvider>{props.children}</TooltipProvider>
|
<TooltipProvider>
|
||||||
|
<ToastContainer
|
||||||
|
className="z-50"
|
||||||
|
position="top-right"
|
||||||
|
theme="dark"
|
||||||
|
pauseOnFocusLoss={false}
|
||||||
|
/>
|
||||||
|
{props.children}
|
||||||
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { ToastContainer } from "react-toastify";
|
import Image from "next/image";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
|
|
||||||
export default function Container({ children }: { children: React.ReactNode }) {
|
export default function Container({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastContainer
|
<div className="fixed left-0 top-0 z-0 h-full w-full blur-sm">
|
||||||
className="z-50"
|
<Image
|
||||||
position="top-right"
|
className="object-fill object-center"
|
||||||
theme="dark"
|
alt="Background image"
|
||||||
pauseOnFocusLoss={false}
|
src={"/assets/background.webp"}
|
||||||
|
fill
|
||||||
/>
|
/>
|
||||||
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
</div>
|
||||||
|
<div className="z-[9999] m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="w-full flex-1">{children}</div>
|
<div className="w-full flex-1">{children}</div>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { GlobeAltIcon } from "@heroicons/react/24/outline";
|
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
@ -129,6 +129,12 @@ export default function Navbar() {
|
|||||||
icon={<GlobeAltIcon height={23} width={23} />}
|
icon={<GlobeAltIcon height={23} width={23} />}
|
||||||
href="/ranking/global/1"
|
href="/ranking/global/1"
|
||||||
/>
|
/>
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View the overlay builder"
|
||||||
|
text="Overlay"
|
||||||
|
icon={<TvIcon height={23} width={23} />}
|
||||||
|
href="/overlay/builder"
|
||||||
|
/>
|
||||||
<NavbarButton
|
<NavbarButton
|
||||||
ariaLabel="View analytics for Scoresaber"
|
ariaLabel="View analytics for Scoresaber"
|
||||||
text="Analytics"
|
text="Analytics"
|
||||||
|
24
src/components/input/Input.tsx
Normal file
24
src/components/input/Input.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Input as Inputtt } from "../ui/input";
|
||||||
|
import { Label } from "../ui/label";
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Input({ label, id, defaultValue, onChange }: InputProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label htmlFor={id}>{label}</Label>
|
||||||
|
<Inputtt
|
||||||
|
id={id}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange && onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
45
src/components/input/RadioInput.tsx
Normal file
45
src/components/input/RadioInput.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
||||||
|
|
||||||
|
type RadioProps = {
|
||||||
|
id: string;
|
||||||
|
defaultValue: string;
|
||||||
|
label?: string;
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
}[];
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RadioInput({
|
||||||
|
id,
|
||||||
|
defaultValue,
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
onChange,
|
||||||
|
}: RadioProps) {
|
||||||
|
return (
|
||||||
|
<div className="mt-2">
|
||||||
|
{id && label && <Label htmlFor={id}>{label}</Label>}
|
||||||
|
<RadioGroup
|
||||||
|
id={id}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
className="mt-2"
|
||||||
|
onValueChange={(value) => onChange && onChange(value)}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value={item.id} id={item.id}>
|
||||||
|
{item.value}
|
||||||
|
</RadioGroupItem>
|
||||||
|
<Label htmlFor={item.id}>{item.value}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
src/components/input/SwitchInput.tsx
Normal file
27
src/components/input/SwitchInput.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Label } from "../ui/label";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
|
||||||
|
type SwitchProps = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange?: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SwitchInput({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
defaultChecked,
|
||||||
|
onChange,
|
||||||
|
}: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<div className="mt-2 flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id={id}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onCheckedChange={(value) => onChange && onChange(value)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={id}>{label}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
34
src/components/overlay/PlayerStats.tsx
Normal file
34
src/components/overlay/PlayerStats.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
|
import { formatNumber } from "@/utils/numberUtils";
|
||||||
|
import { GlobeAltIcon } from "@heroicons/react/20/solid";
|
||||||
|
import Image from "next/image";
|
||||||
|
import CountyFlag from "../CountryFlag";
|
||||||
|
|
||||||
|
type PlayerStatsProps = {
|
||||||
|
player: ScoresaberPlayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlayerStats({ player }: PlayerStatsProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 p-2">
|
||||||
|
<Image
|
||||||
|
alt="Player profile picture"
|
||||||
|
className="rounded-md"
|
||||||
|
src={player.profilePicture}
|
||||||
|
width={180}
|
||||||
|
height={180}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GlobeAltIcon width={25} height={25} />
|
||||||
|
<p className="text-3xl">#{formatNumber(player.rank)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CountyFlag className="w-[25px]" countryCode={player.country} />
|
||||||
|
<p className="text-3xl">#{formatNumber(player.countryRank)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
20
src/components/overlay/ScoreStats.tsx
Normal file
20
src/components/overlay/ScoreStats.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useOverlayDataStore } from "@/store/overlayDataStore";
|
||||||
|
import { formatNumber } from "@/utils/numberUtils";
|
||||||
|
import useStore from "@/utils/useStore";
|
||||||
|
|
||||||
|
export default function ScoreStats() {
|
||||||
|
const dataStore = useStore(useOverlayDataStore, (store) => store);
|
||||||
|
if (!dataStore) return null;
|
||||||
|
const { scoreStats } = dataStore;
|
||||||
|
if (!scoreStats) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col p-2">
|
||||||
|
<p className="text-2xl">{formatNumber(scoreStats.score)}</p>
|
||||||
|
<p className="text-2xl">Combo: {formatNumber(scoreStats.combo)}</p>
|
||||||
|
<p className="text-2xl">
|
||||||
|
{scoreStats.rank} {scoreStats.accuracy.toFixed(2)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
src/components/overlay/SongInfo.tsx
Normal file
46
src/components/overlay/SongInfo.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useOverlayDataStore } from "@/store/overlayDataStore";
|
||||||
|
import { songDifficultyToColor } from "@/utils/songUtils";
|
||||||
|
import useStore from "@/utils/useStore";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function SongInfo() {
|
||||||
|
const dataStore = useStore(useOverlayDataStore, (store) => store);
|
||||||
|
if (!dataStore) return null;
|
||||||
|
const { paused, songInfo } = dataStore;
|
||||||
|
if (!songInfo) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex transform-gpu gap-2 p-2 transition-all",
|
||||||
|
paused ? "grayscale" : "grayscale-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="rounded-md"
|
||||||
|
alt="Song Image"
|
||||||
|
src={songInfo.art}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col justify-between pb-2 pt-1">
|
||||||
|
<div>
|
||||||
|
<p className="text-xl font-bold">{songInfo.songName}</p>
|
||||||
|
<p className="text-md">{songInfo.songMapper}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-2">
|
||||||
|
<p
|
||||||
|
className="text-md rounded-md p-[3px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: songDifficultyToColor(songInfo.difficulty),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{songInfo.difficulty}
|
||||||
|
</p>
|
||||||
|
<p className="text-md">!bsr {songInfo.bsr}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/utils/utils"
|
import { cn } from "@/utils/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@ -10,12 +10,12 @@ const Card = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Card.displayName = "Card"
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
|||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
|
|||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
));
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
|
|||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
};
|
||||||
|
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/utils"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
44
src/components/ui/radio-group.tsx
Normal file
44
src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
import { Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/utils"
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
127
src/overlay/httpSiraStatus.ts
Normal file
127
src/overlay/httpSiraStatus.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useOverlayDataStore } from "@/store/overlayDataStore";
|
||||||
|
import { useOverlaySettingsStore } from "@/store/overlaySettingsStore";
|
||||||
|
import { BeatsaverAPI } from "@/utils/beatsaver/api";
|
||||||
|
import { w3cwebsocket as WebSocketClient } from "websocket";
|
||||||
|
|
||||||
|
const settingsStore = useOverlaySettingsStore;
|
||||||
|
const overlayDataStore = useOverlayDataStore;
|
||||||
|
let isConnected = false;
|
||||||
|
|
||||||
|
async function loadIntoSong(data: any) {
|
||||||
|
const { beatmap, performance } = data;
|
||||||
|
const hash = beatmap.songHash;
|
||||||
|
const beatsaverMapData = await BeatsaverAPI.fetchMapByHash(hash);
|
||||||
|
if (beatsaverMapData == undefined) return;
|
||||||
|
const coverURL =
|
||||||
|
beatsaverMapData.versions[beatsaverMapData.versions.length - 1].coverURL;
|
||||||
|
|
||||||
|
overlayDataStore.setState({
|
||||||
|
scoreStats: {
|
||||||
|
accuracy: performance.relativeScore * 100,
|
||||||
|
score: performance.rawScore,
|
||||||
|
combo: performance.combo,
|
||||||
|
rank: performance.rank,
|
||||||
|
},
|
||||||
|
songInfo: {
|
||||||
|
art: coverURL,
|
||||||
|
bsr: beatsaverMapData.id,
|
||||||
|
difficulty: beatmap.difficulty,
|
||||||
|
songMapper: beatsaverMapData.metadata.levelAuthorName,
|
||||||
|
songName: beatsaverMapData.metadata.songName,
|
||||||
|
songSubName: beatsaverMapData.metadata.songSubName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handlers = {
|
||||||
|
[key: string]: (data: any) => void;
|
||||||
|
};
|
||||||
|
const handlers: Handlers = {
|
||||||
|
hello: async (data: any) => {
|
||||||
|
if (!data.beatmap || !data.performance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadIntoSong(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
songStart: (data: any) => {
|
||||||
|
loadIntoSong(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
scoreChanged: (data: any) => {
|
||||||
|
const { performance } = data;
|
||||||
|
if (performance == undefined) return;
|
||||||
|
|
||||||
|
overlayDataStore.setState({
|
||||||
|
scoreStats: {
|
||||||
|
accuracy: performance.relativeScore * 100,
|
||||||
|
score: performance.rawScore,
|
||||||
|
combo: performance.combo,
|
||||||
|
rank: performance.rank,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Left the song
|
||||||
|
finished: (data: any) => {
|
||||||
|
overlayDataStore.setState({
|
||||||
|
scoreStats: undefined,
|
||||||
|
songInfo: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
menu: (data: any) => {
|
||||||
|
overlayDataStore.setState({
|
||||||
|
scoreStats: undefined,
|
||||||
|
songInfo: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// pause & resume
|
||||||
|
pause: (data: any) => {
|
||||||
|
overlayDataStore.setState({
|
||||||
|
paused: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resume: (data: any) => {
|
||||||
|
overlayDataStore.setState({
|
||||||
|
paused: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
if (isConnected) return;
|
||||||
|
const client = new WebSocketClient(
|
||||||
|
`ws://${settingsStore.getState().ipAddress}:6557/socket`,
|
||||||
|
);
|
||||||
|
isConnected = true;
|
||||||
|
client.onopen = () => {
|
||||||
|
console.log("WebSocket Connected to HttpSiraStatus");
|
||||||
|
};
|
||||||
|
client.onerror = (error) => {
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
client.onclose = () => {
|
||||||
|
isConnected = false;
|
||||||
|
console.log(
|
||||||
|
"Lost connection to HttpSiraStatus, reconnecting in 5 seconds...",
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
connectWebSocket();
|
||||||
|
}, 5000); // 5 seconds
|
||||||
|
};
|
||||||
|
client.onmessage = (message) => {
|
||||||
|
const data = JSON.parse(message.data.toString());
|
||||||
|
const handler = handlers[data.event];
|
||||||
|
//console.log(data.event);
|
||||||
|
if (handler == undefined) {
|
||||||
|
console.error(`No handler for ${data.event}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler(data.status);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HttpSiraStatus = {
|
||||||
|
connectWebSocket,
|
||||||
|
};
|
21
src/schemas/beatsaver/BeatsaverMap.ts
Normal file
21
src/schemas/beatsaver/BeatsaverMap.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { BeatsaverMapMetadata } from "./BeatsaverMapMetadata";
|
||||||
|
import { BeatsaverMapStats } from "./BeatsaverMapStats";
|
||||||
|
import { BeatsaverMapVersion } from "./BeatsaverMapVersion";
|
||||||
|
import { BeatsaverUploader } from "./BeatsaverUploader";
|
||||||
|
|
||||||
|
export type BeatsaverMap = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
uploader: BeatsaverUploader;
|
||||||
|
metadata: BeatsaverMapMetadata;
|
||||||
|
stats: BeatsaverMapStats;
|
||||||
|
uploaded: string;
|
||||||
|
automapper: boolean;
|
||||||
|
ranked: boolean;
|
||||||
|
qualified: boolean;
|
||||||
|
versions: BeatsaverMapVersion[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastPublishedAt: string;
|
||||||
|
};
|
22
src/schemas/beatsaver/BeatsaverMapDifficulty.ts
Normal file
22
src/schemas/beatsaver/BeatsaverMapDifficulty.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { BeatsaverMapSummary } from "./BeatsaverMapSummary";
|
||||||
|
|
||||||
|
export type BeatsaverMapDifficulty = {
|
||||||
|
njs: number;
|
||||||
|
offset: number;
|
||||||
|
notes: number;
|
||||||
|
bombs: number;
|
||||||
|
obstacles: number;
|
||||||
|
nps: number;
|
||||||
|
length: number;
|
||||||
|
characteristic: string;
|
||||||
|
difficulty: string;
|
||||||
|
events: number;
|
||||||
|
chroma: boolean;
|
||||||
|
me: boolean;
|
||||||
|
ne: boolean;
|
||||||
|
cinema: boolean;
|
||||||
|
seconds: number;
|
||||||
|
paritySummary: BeatsaverMapSummary;
|
||||||
|
maxScore: number;
|
||||||
|
label: string;
|
||||||
|
};
|
8
src/schemas/beatsaver/BeatsaverMapMetadata.ts
Normal file
8
src/schemas/beatsaver/BeatsaverMapMetadata.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export type BeatsaverMapMetadata = {
|
||||||
|
bpm: number;
|
||||||
|
duration: number;
|
||||||
|
songName: string;
|
||||||
|
songSubName: string;
|
||||||
|
songAuthorName: string;
|
||||||
|
levelAuthorName: string;
|
||||||
|
};
|
7
src/schemas/beatsaver/BeatsaverMapStats.ts
Normal file
7
src/schemas/beatsaver/BeatsaverMapStats.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type BeatsaverMapStats = {
|
||||||
|
plays: number;
|
||||||
|
downloads: number;
|
||||||
|
upvotes: number;
|
||||||
|
downvotes: number;
|
||||||
|
score: number;
|
||||||
|
};
|
5
src/schemas/beatsaver/BeatsaverMapSummary.ts
Normal file
5
src/schemas/beatsaver/BeatsaverMapSummary.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type BeatsaverMapSummary = {
|
||||||
|
errors: number;
|
||||||
|
warns: number;
|
||||||
|
resets: number;
|
||||||
|
};
|
12
src/schemas/beatsaver/BeatsaverMapVersion.ts
Normal file
12
src/schemas/beatsaver/BeatsaverMapVersion.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { BeatsaverMapDifficulty } from "./BeatsaverMapDifficulty";
|
||||||
|
|
||||||
|
export type BeatsaverMapVersion = {
|
||||||
|
hash: string;
|
||||||
|
state: string;
|
||||||
|
createdAt: string;
|
||||||
|
sageScore: number;
|
||||||
|
diffs: BeatsaverMapDifficulty[];
|
||||||
|
downloadURL: string;
|
||||||
|
coverURL: string;
|
||||||
|
previewURL: string;
|
||||||
|
};
|
10
src/schemas/beatsaver/BeatsaverUploader.ts
Normal file
10
src/schemas/beatsaver/BeatsaverUploader.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type BeatsaverUploader = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
hash: string;
|
||||||
|
avatar: string;
|
||||||
|
type: string;
|
||||||
|
admin: boolean;
|
||||||
|
curator: boolean;
|
||||||
|
playlistUrl: string;
|
||||||
|
};
|
42
src/store/overlayDataStore.ts
Normal file
42
src/store/overlayDataStore.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface OverlayDataStore {
|
||||||
|
paused: boolean;
|
||||||
|
scoreStats:
|
||||||
|
| {
|
||||||
|
accuracy: number;
|
||||||
|
score: number;
|
||||||
|
rank: string;
|
||||||
|
combo: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
songInfo:
|
||||||
|
| {
|
||||||
|
art: string;
|
||||||
|
songName: string;
|
||||||
|
songSubName: string;
|
||||||
|
songMapper: string;
|
||||||
|
difficulty: string;
|
||||||
|
bsr: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOverlayDataStore = create<OverlayDataStore>()((set, get) => ({
|
||||||
|
paused: false,
|
||||||
|
songInfo: undefined,
|
||||||
|
scoreStats: undefined,
|
||||||
|
|
||||||
|
setScoreStats(scoreStats: OverlayDataStore["scoreStats"]) {
|
||||||
|
set({
|
||||||
|
scoreStats,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setSongInfo(songInfo: OverlayDataStore["songInfo"]) {
|
||||||
|
set({
|
||||||
|
songInfo,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
71
src/store/overlaySettingsStore.ts
Normal file
71
src/store/overlaySettingsStore.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
import { IDBStorage } from "./IndexedDBStorage";
|
||||||
|
|
||||||
|
interface OverlaySettingsStore {
|
||||||
|
ipAddress: string;
|
||||||
|
accountId: string;
|
||||||
|
platform: string;
|
||||||
|
settings: {
|
||||||
|
showPlayerStats: boolean;
|
||||||
|
showSongInfo: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
setIpAddress: (ipAddress: string) => void;
|
||||||
|
setAccountId: (accountId: string) => void;
|
||||||
|
setPlatform: (platform: string) => void;
|
||||||
|
setShowPlayerStats: (showPlayerStats: boolean) => void;
|
||||||
|
setShowSongInfo: (showSongInfo: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOverlaySettingsStore = create<OverlaySettingsStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
ipAddress: "localhost",
|
||||||
|
accountId: "",
|
||||||
|
platform: "scoresaber",
|
||||||
|
settings: {
|
||||||
|
showPlayerStats: true,
|
||||||
|
showSongInfo: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
setIpAddress(ipAddress: string) {
|
||||||
|
set({
|
||||||
|
ipAddress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setAccountId(accountId: string) {
|
||||||
|
set({
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setPlatform(platform: string) {
|
||||||
|
set({
|
||||||
|
platform,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setShowPlayerStats(showPlayerStats: boolean) {
|
||||||
|
set({
|
||||||
|
settings: {
|
||||||
|
...get().settings,
|
||||||
|
showPlayerStats,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setShowSongInfo(showSongInfo: boolean) {
|
||||||
|
set({
|
||||||
|
settings: {
|
||||||
|
...get().settings,
|
||||||
|
showSongInfo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "overlaySettings",
|
||||||
|
storage: createJSONStorage(() => IDBStorage),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
37
src/utils/beatsaver/api.ts
Normal file
37
src/utils/beatsaver/api.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BeatsaverMap } from "@/schemas/beatsaver/BeatsaverMap";
|
||||||
|
import { ssrSettings } from "@/ssrSettings";
|
||||||
|
import { FetchQueue } from "../fetchWithQueue";
|
||||||
|
import { formatString } from "../string";
|
||||||
|
|
||||||
|
// Create a fetch instance with a cache
|
||||||
|
export const BeatsaverFetchQueue = new FetchQueue();
|
||||||
|
|
||||||
|
// Api endpoints
|
||||||
|
const BS_API_URL = ssrSettings.proxy + "/https://api.beatsaver.com";
|
||||||
|
export const BS_GET_MAP_BY_HASH_URL = BS_API_URL + "/maps/hash/{}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the map info for the provided hash
|
||||||
|
*
|
||||||
|
* @param hash the hash of the map
|
||||||
|
* @returns the map info
|
||||||
|
*/
|
||||||
|
async function fetchMapByHash(
|
||||||
|
hash: string,
|
||||||
|
): Promise<BeatsaverMap | undefined | null> {
|
||||||
|
const response = await BeatsaverFetchQueue.fetch(
|
||||||
|
formatString(BS_GET_MAP_BY_HASH_URL, true, hash),
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
// Check if there was an error fetching the user data
|
||||||
|
if (json.error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json as BeatsaverMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BeatsaverAPI = {
|
||||||
|
fetchMapByHash,
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user