This commit is contained in:
parent
d2df95381c
commit
1792648e8d
@ -31,6 +31,12 @@ const nextConfig = {
|
||||
port: "",
|
||||
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": {
|
||||
"@boiseitguru/cookie-cutter": "^0.2.1",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@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-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.74.1",
|
||||
"bluebird": "^3.7.2",
|
||||
@ -35,6 +38,7 @@
|
||||
"sharp": "^0.32.6",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"websocket": "^1.0.34",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -43,6 +47,7 @@
|
||||
"@types/node-fetch-cache": "^3.0.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/websocket": "^1.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
|
||||
@ -2076,6 +2205,18 @@
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -2372,6 +2513,15 @@
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
|
||||
"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": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@ -2695,6 +2845,39 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@ -3148,6 +3331,19 @@
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
|
||||
@ -4597,6 +4798,11 @@
|
||||
"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": {
|
||||
"version": "3.51.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
|
||||
@ -4661,6 +4867,16 @@
|
||||
"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": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
@ -6310,6 +6526,11 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -6399,6 +6620,14 @@
|
||||
"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": {
|
||||
"version": "5.2.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -6594,6 +6835,35 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
@ -11,10 +11,13 @@
|
||||
"dependencies": {
|
||||
"@boiseitguru/cookie-cutter": "^0.2.1",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@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-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.74.1",
|
||||
"bluebird": "^3.7.2",
|
||||
@ -36,6 +39,7 @@
|
||||
"sharp": "^0.32.6",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"websocket": "^1.0.34",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -44,6 +48,7 @@
|
||||
"@types/node-fetch-cache": "^3.0.3",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/websocket": "^1.0.8",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8",
|
||||
|
@ -3,7 +3,6 @@ import { ssrSettings } from "@/ssrSettings";
|
||||
import clsx from "clsx";
|
||||
import { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import Image from "next/image";
|
||||
import Script from "next/script";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import "./globals.css";
|
||||
@ -49,16 +48,7 @@ export default function RootLayout({
|
||||
src="https://analytics.fascinated.cc/js/script.js"
|
||||
/>
|
||||
|
||||
<body className={clsx(font.className, "bg-black 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>
|
||||
|
||||
<body className={clsx(font.className, "text-primary")}>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</body>
|
||||
</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 { useSettingsStore } from "@/store/settingsStore";
|
||||
import React from "react";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { TooltipProvider } from "./ui/Tooltip";
|
||||
import { ThemeProvider } from "./ui/theme-provider";
|
||||
|
||||
@ -52,7 +53,15 @@ export default class AppProvider extends React.Component {
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Image from "next/image";
|
||||
import Footer from "./Footer";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function Container({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<ToastContainer
|
||||
className="z-50"
|
||||
position="top-right"
|
||||
theme="dark"
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||
<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>
|
||||
<div className="z-[9999] m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||
<Navbar />
|
||||
<div className="w-full flex-1">{children}</div>
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
ServerIcon,
|
||||
UserIcon,
|
||||
} 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 Avatar from "./Avatar";
|
||||
import { Button } from "./ui/button";
|
||||
@ -129,6 +129,12 @@ export default function Navbar() {
|
||||
icon={<GlobeAltIcon height={23} width={23} />}
|
||||
href="/ranking/global/1"
|
||||
/>
|
||||
<NavbarButton
|
||||
ariaLabel="View the overlay builder"
|
||||
text="Overlay"
|
||||
icon={<TvIcon height={23} width={23} />}
|
||||
href="/overlay/builder"
|
||||
/>
|
||||
<NavbarButton
|
||||
ariaLabel="View analytics for Scoresaber"
|
||||
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<
|
||||
HTMLDivElement,
|
||||
@ -10,12 +10,12 @@ const Card = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...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…
x
Reference in New Issue
Block a user