refactored a LOT of code, added some touches

This commit is contained in:
anonymousratwastaken 2025-12-19 15:29:38 +00:00
parent c1bdb0d182
commit cdadb96545
22 changed files with 1851 additions and 522 deletions

View file

@ -5,11 +5,14 @@
"name": "web", "name": "web",
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@base-ui/react": "^1.0.0",
"@workos-inc/node": "^7.77.0",
"@xboxreplay/xboxlive-api": "^3.4.3",
"@yudiel/react-qr-scanner": "^2.5.0", "@yudiel/react-qr-scanner": "^2.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"motion": "^12.23.26",
"next": "16.0.10", "next": "16.0.10",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@ -17,6 +20,7 @@
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1", "react-dom": "19.2.1",
"shadcn": "^3.6.1", "shadcn": "^3.6.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
}, },
@ -266,6 +270,12 @@
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
"@peculiar/webcrypto": ["@peculiar/webcrypto@1.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.5", "tslib": "^2.6.2", "webcrypto-core": "^1.8.0" } }, "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@ -428,22 +438,58 @@
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
"@types/cookie": ["@types/cookie@0.5.4", "", {}, "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA=="],
"@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="],
"@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="], "@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="],
"@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/keygrip": ["@types/keygrip@1.0.6", "", {}, "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ=="],
"@types/koa": ["@types/koa@2.15.0", "", { "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" } }, "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g=="],
"@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="],
@ -504,6 +550,12 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@workos-inc/node": ["@workos-inc/node@7.77.0", "", { "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", "leb": "^1.0.0", "qs": "6.14.0" } }, "sha512-6LGBAqih8kkzhHqmxueT9/xX93AJQxQhKekyNs0mqgWsrnqOPDiag1WIFzgxbQTvr564MqtEW502Tatfp1+x0Q=="],
"@xboxreplay/errors": ["@xboxreplay/errors@0.1.0", "", {}, "sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g=="],
"@xboxreplay/xboxlive-api": ["@xboxreplay/xboxlive-api@3.4.3", "", { "dependencies": { "@xboxreplay/errors": "^0.1.0", "axios": "^0.21.1" } }, "sha512-+iSmNCN4RHXZXBc48e0tJB+tvUs6XEwEWcXTcECrIttgDVN9fsFxroo+6ET+K38FobEM+5xUOfCVNqQoWQthxQ=="],
"@yudiel/react-qr-scanner": ["@yudiel/react-qr-scanner@2.5.0", "", { "dependencies": { "barcode-detector": "3.0.8", "webrtc-adapter": "9.0.3" }, "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "sha512-wVWvm0z5kGGc3tiMcxyr2aji3C6aU5K3Q7R08MOdyFDFkvMbFMF1Hz5P5WFYW+zuuqxDPBcRZPCvmVs6jBheTQ=="], "@yudiel/react-qr-scanner": ["@yudiel/react-qr-scanner@2.5.0", "", { "dependencies": { "barcode-detector": "3.0.8", "webrtc-adapter": "9.0.3" }, "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "sha512-wVWvm0z5kGGc3tiMcxyr2aji3C6aU5K3Q7R08MOdyFDFkvMbFMF1Hz5P5WFYW+zuuqxDPBcRZPCvmVs6jBheTQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@ -546,6 +598,8 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
@ -556,12 +610,16 @@
"axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
"axios": ["axios@0.21.4", "", { "dependencies": { "follow-redirects": "^1.14.0" } }, "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"barcode-detector": ["barcode-detector@3.0.8", "", { "dependencies": { "zxing-wasm": "2.2.4" } }, "sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A=="], "barcode-detector": ["barcode-detector@3.0.8", "", { "dependencies": { "zxing-wasm": "2.2.4" } }, "sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg=="],
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
@ -572,6 +630,8 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@ -620,7 +680,7 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@ -794,12 +854,16 @@
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], "fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
@ -878,6 +942,8 @@
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@ -892,6 +958,10 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"iron-session": ["iron-session@6.3.1", "", { "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@types/cookie": "^0.5.1", "@types/express": "^4.17.13", "@types/koa": "^2.13.5", "@types/node": "^17.0.41", "cookie": "^0.5.0", "iron-webcrypto": "^0.2.5" }, "peerDependencies": { "express": ">=4", "koa": ">=2", "next": ">=10" }, "optionalPeers": ["express", "koa", "next"] }, "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A=="],
"iron-webcrypto": ["iron-webcrypto@0.2.8", "", { "dependencies": { "buffer": "^6" } }, "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
@ -980,7 +1050,7 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@ -1012,6 +1082,8 @@
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
"leb": ["leb@1.0.0", "", {}, "sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
@ -1078,6 +1150,12 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"motion": ["motion@12.23.26", "", { "dependencies": { "framer-motion": "^12.23.26", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ=="],
"motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msw": ["msw@2.12.4", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg=="], "msw": ["msw@2.12.4", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg=="],
@ -1192,6 +1270,10 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
@ -1296,6 +1378,8 @@
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@ -1424,6 +1508,8 @@
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="],
"webrtc-adapter": ["webrtc-adapter@9.0.3", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ=="], "webrtc-adapter": ["webrtc-adapter@9.0.3", "", { "dependencies": { "sdp": "^3.2.0" } }, "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@ -1480,6 +1566,8 @@
"@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "@modelcontextprotocol/sdk/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
"@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
@ -1524,6 +1612,8 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"iron-session/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@ -1532,6 +1622,8 @@
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"msw/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"msw/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "msw/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],

View file

@ -10,11 +10,14 @@
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@base-ui/react": "^1.0.0",
"@workos-inc/node": "^7.77.0",
"@xboxreplay/xboxlive-api": "^3.4.3",
"@yudiel/react-qr-scanner": "^2.5.0", "@yudiel/react-qr-scanner": "^2.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"motion": "^12.23.26",
"next": "16.0.10", "next": "16.0.10",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@ -22,6 +25,7 @@
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1", "react-dom": "19.2.1",
"shadcn": "^3.6.1", "shadcn": "^3.6.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"
}, },

View file

@ -0,0 +1,695 @@
"use client";
import { useState, useTransition, useEffect, use } from "react";
import { Asterisk, CheckCircle2, Camera, CameraOff, PartyPopper, ExternalLink, ArrowLeft, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Scanner } from "@yudiel/react-qr-scanner";
import { toast } from "sonner";
import { motion } from "motion/react";
import { Item, ItemContent, ItemTitle, ItemMedia, ItemActions } from "@/components/ui/item";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { cfg } from "@/lib/cfg";
import { getAvatarProfile } from "@/lib/avatar";
interface PageProps {
params: Promise<{
method: string;
step: string;
}>;
}
export default function AccessPage({ params }: PageProps) {
const router = useRouter();
const resolvedParams = use(params);
const method = resolvedParams.method; // 'code' or 'qr'
const step = resolvedParams.step; // 'verify', 'details', 'success'
// Access Code State
const [length, setLength] = useState(6);
const [code, setCode] = useState("");
const [verified, setVerified] = useState(false);
const [whitelistSuccess, setWhitelistSuccess] = useState(false);
const [status, setStatus] = useState<{ ok: boolean; message?: string }>({ ok: false });
const [isPending, startTransition] = useTransition();
// Details State
const [preference, setPreference] = useState<"java" | "bedrock" | "both">("java");
const [javaName, setJavaName] = useState("");
const [bedrockName, setBedrockName] = useState("");
// QR State
const [showScanner, setShowScanner] = useState(false);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scannerError, setScannerError] = useState<string | null>(null);
// Success State
const [copied, setCopied] = useState(false);
const [profileData, setProfileData] = useState<{ avatarUrl: string; gamertag: string } | null>(null);
// Loading state to prevent premature redirects
const [isLoading, setIsLoading] = useState(true);
// Handle redirects based on verification state
useEffect(() => {
if (!isLoading) {
if (step === 'details' && !verified) {
router.push(`/access/${method}/verify`);
} else if (step === 'success' && !whitelistSuccess) {
router.push(`/access/${method}/verify`);
}
}
}, [step, verified, whitelistSuccess, method, router, isLoading]);
useEffect(() => {
if (method === 'qr' && step === 'verify' && hasPermission === null) {
const requestCameraPermission = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(track => track.stop()); // Stop immediately after getting permission
setHasPermission(true);
} catch (error) {
console.error('Camera permission denied:', error);
setHasPermission(false);
setScannerError("Camera permission denied. Please enable camera access to scan QR codes.");
}
};
requestCameraPermission();
}
}, [method, step, hasPermission]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
const savedVerified = localStorage.getItem(`${prefix}verified`);
const savedWhitelistSuccess = localStorage.getItem(`${prefix}whitelist-success`);
const savedCode = localStorage.getItem(`${prefix}auth-code`);
const savedJavaName = localStorage.getItem(`${prefix}java-name`);
const savedBedrockName = localStorage.getItem(`${prefix}bedrock-name`);
const savedPreference = localStorage.getItem(`${prefix}preference`);
if (savedVerified === 'true') {
setVerified(true);
}
if (savedWhitelistSuccess === 'true') {
setWhitelistSuccess(true);
}
if (savedCode) {
setCode(savedCode);
}
if (savedJavaName) {
setJavaName(savedJavaName);
}
if (savedBedrockName) {
setBedrockName(savedBedrockName);
}
if (savedPreference) {
setPreference(savedPreference as "java" | "bedrock" | "both");
}
// Fetch profile data for success step
if (step === 'success') {
fetchProfileData();
}
// Set loading to false after state is loaded
setIsLoading(false);
}, [method, step]);
// Save state to localStorage
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}verified`, verified.toString());
}, [verified, method]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}whitelist-success`, whitelistSuccess.toString());
}, [whitelistSuccess, method]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}auth-code`, code);
}, [code, method]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}java-name`, javaName);
}, [javaName, method]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}bedrock-name`, bedrockName);
}, [bedrockName, method]);
useEffect(() => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.setItem(`${prefix}preference`, preference);
}, [preference, method]);
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch("/api/access-code-config");
const config = await response.json();
setLength(config.length || 6);
} catch (error) {
console.error("Failed to fetch access code config:", error);
}
};
fetchConfig();
}, []);
const fetchProfileData = async () => {
try {
const res = await fetch(
`/api/xbox/profile/get?gamertag=${encodeURIComponent(cfg.server.bedrockFriend)}`
);
const data = await res.json();
setProfileData(data);
} catch (e) {
console.error("Failed to fetch profile", e);
}
};
const needsJavaName = preference === "java" || preference === "both";
const needsBedrockName = preference === "bedrock" || preference === "both";
const handleVerifyCode = async () => {
startTransition(async () => {
try {
const response = await fetch("/api/access-code", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
});
const result = await response.json();
if (!result.success) {
setVerified(false);
setStatus({ ok: false, message: result.message });
return;
}
setVerified(true);
setStatus({
ok: true,
message: "Access code verified!",
});
// Navigate to details step
router.push(`/access/${method}/details`);
} catch {
setVerified(false);
setStatus({
ok: false,
message: "We couldn't reach the server. Double-check your connection and try again.",
});
}
});
};
const handleQRScan = (detectedCodes: any[]) => {
if (detectedCodes.length > 0) {
const result = detectedCodes[0].rawValue;
const sanitizedResult = result.replace(/[^0-9A-Za-z]/g, "");
startTransition(async () => {
try {
const response = await fetch("/api/qr-verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: sanitizedResult,
}),
});
const verifyResult = await response.json();
if (verifyResult.success) {
setVerified(true);
setStatus({
ok: true,
message: verifyResult.message || "QR code verified successfully!",
});
// Navigate to details step
router.push(`/access/${method}/details`);
} else {
setVerified(false);
setStatus({
ok: false,
message: verifyResult.message || "QR code verification failed",
});
}
} catch {
setVerified(false);
setStatus({
ok: false,
message: "We couldn't reach the server. Try again soon.",
});
}
});
}
};
const handleWhitelistSubmit = async () => {
if (
(needsJavaName && !javaName.trim()) ||
(needsBedrockName && !bedrockName.trim())
) {
setStatus({
ok: false,
message: "Please fill out every username we need to whitelist.",
});
return;
}
startTransition(async () => {
try {
const response = await fetch("/api/whitelist", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
javaName: needsJavaName ? javaName.trim() : undefined,
bedrockName: needsBedrockName ? bedrockName.trim() : undefined,
preference,
accessCode: method === 'qr' ? "qr_verified" : code,
}),
});
const result = await response.json();
if (result.success) {
setStatus({
ok: true,
message: result.results.join(" ") || "Successfully added to whitelist!",
});
setWhitelistSuccess(true);
// Navigate to success step
router.push(`/access/${method}/success`);
} else {
setStatus({
ok: false,
message: result.errors.join(" ") || "Failed to add to whitelist.",
});
}
} catch {
setStatus({
ok: false,
message: "We couldn't reach the server. Double-check your connection and try again.",
});
}
});
};
const handleScannerError = (error: any) => {
console.error("QR Scanner error:", error);
setScannerError("Unable to access camera. Please check permissions.");
setHasPermission(false);
};
const handleCopyIp = async () => {
const serverIp = cfg.server.javaIp;
if (serverIp) {
await navigator.clipboard.writeText(serverIp);
toast.success("Copied to clipboard");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
// Get server information based on user's preference
const getServerInfo = () => {
switch (preference) {
case 'java':
return {
address: cfg.server.javaIp,
label: 'Java Server Address',
showProfile: false
};
case 'bedrock':
return {
address: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''),
label: 'Bedrock Server Address',
showProfile: true
};
case 'both':
return {
javaAddress: cfg.server.javaIp,
bedrockAddress: cfg.server.bedrockIp + (cfg.server.bedrockPort ? `:${cfg.server.bedrockPort}` : ''),
label: 'Server Addresses',
showProfile: true
};
default:
return {
address: cfg.server.javaIp,
label: 'Server Address',
showProfile: false
};
}
};
const serverInfo = getServerInfo();
const handleReset = () => {
const prefix = method === 'qr' ? 'frg-qr-' : 'frg-';
localStorage.removeItem(`${prefix}verified`);
localStorage.removeItem(`${prefix}whitelist-success`);
localStorage.removeItem(`${prefix}auth-code`);
localStorage.removeItem(`${prefix}java-name`);
localStorage.removeItem(`${prefix}bedrock-name`);
localStorage.removeItem(`${prefix}preference`);
setVerified(false);
setWhitelistSuccess(false);
setCode("");
setJavaName("");
setBedrockName("");
setStatus({ ok: false });
router.push(`/access/${method}/verify`);
};
// Render different steps
if (step === 'verify') {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-6">
{/* Header */}
<div className="text-center space-y-2">
<Link href="/" className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to home
</Link>
<h1 className="text-2xl font-semibold">
{method === 'code' ? 'Access Code Verification' : 'QR Code Verification'}
</h1>
<p className="text-muted-foreground">
{method === 'code'
? `Use the ${length}-character invite your friend gave you so we can safely whitelist you.`
: 'Scan a QR code to verify your access.'
}
</p>
</div>
{/* Verification Content */}
<div className="space-y-4">
{method === 'code' ? (
<div className="space-y-2">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Access code
</Label>
<InputOTP
maxLength={length}
className="w-full font-mono"
value={code}
onChange={setCode}
disabled={verified}
>
<InputOTPGroup className="flex h-16 w-full">
{Array.from({ length }).map((_, i) => (
<InputOTPSlot
key={i}
index={i}
className="flex-1 h-16 font-mono text-3xl"
/>
))}
</InputOTPGroup>
</InputOTP>
</div>
) : (
<div className="w-full max-w-sm mx-auto space-y-4">
{scannerError ? (
<div className="text-center space-y-4">
<CameraOff className="w-12 h-12 text-muted-foreground mx-auto" />
<p className="text-sm text-muted-foreground">{scannerError}</p>
<Button onClick={() => router.push('/')} variant="outline">
Back to home
</Button>
</div>
) : (
<>
<div className="relative w-full h-64 rounded-md overflow-hidden">
<Scanner
onScan={handleQRScan}
onError={handleScannerError}
styles={{
container: {
width: "100%",
height: "100%",
borderRadius: "0.5rem",
overflow: "hidden",
},
video: {
width: "100%",
height: "100%",
objectFit: "cover",
},
}}
/>
{hasPermission === null && (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 rounded-md">
<div className="text-center space-y-2">
<Camera className="w-8 h-8 text-muted-foreground mx-auto animate-pulse" />
<p className="text-sm text-muted-foreground">
Requesting camera permission...
</p>
</div>
</div>
)}
</div>
</>
)}
</div>
)}
{/* Status */}
<div className="text-center">
{status.message && (
<p className={status.ok ? "text-emerald-500" : "text-destructive"}>
{status.ok && <CheckCircle2 className="inline size-4 mr-1" />}
{status.message}
</p>
)}
</div>
{/* Actions */}
{method === 'code' && (
<Button
onClick={handleVerifyCode}
disabled={isPending || code.length < length}
className="w-full"
>
{isPending ? "Checking..." : "Verify access code"}
</Button>
)}
</div>
</div>
</div>
);
}
if (step === 'details') {
if (!verified) {
return null; // Redirect handled by useEffect
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-6">
{/* Header */}
<div className="text-center space-y-2">
<Link href={`/access/${method}/verify`} className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to verification
</Link>
<h1 className="text-2xl font-semibold">Server Details</h1>
<p className="text-muted-foreground">
Tell us which Minecraft edition you play and your username(s).
</p>
</div>
{/* Form */}
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Which edition?
</Label>
<ToggleGroup
type="single"
value={preference}
onValueChange={(value) =>
value && setPreference(value as typeof preference)
}
className="w-full"
>
<ToggleGroupItem value="java" className="flex-1">
Java
</ToggleGroupItem>
<ToggleGroupItem value="bedrock" className="flex-1">
Bedrock
</ToggleGroupItem>
<ToggleGroupItem value="both" className="flex-1">
Both
</ToggleGroupItem>
</ToggleGroup>
</div>
{needsJavaName && (
<div className="space-y-2">
<Label htmlFor="java-username">Java username</Label>
<Input
id="java-username"
placeholder="Your Java username"
value={javaName}
className="font-mono"
onChange={(event) => setJavaName(event.target.value)}
/>
</div>
)}
{needsBedrockName && (
<div className="space-y-2">
<Label htmlFor="bedrock-username">Bedrock Gamertag</Label>
<Input
id="bedrock-username"
placeholder="Your Xbox name"
value={bedrockName}
className="font-mono"
onChange={(event) => setBedrockName(event.target.value)}
/>
</div>
)}
{/* Status */}
<div className="text-center">
{status.message && (
<p className={status.ok ? "text-emerald-500" : "text-destructive"}>
{status.ok && <CheckCircle2 className="inline size-4 mr-1" />}
{status.message}
</p>
)}
</div>
{/* Actions */}
<Button
onClick={handleWhitelistSubmit}
disabled={isPending || (needsJavaName && !javaName.trim()) || (needsBedrockName && !bedrockName.trim())}
className="w-full"
>
{isPending ? "Submitting..." : "Continue"}
</Button>
</div>
</div>
</div>
);
}
if (step === 'success') {
if (!whitelistSuccess) {
return null; // Redirect handled by useEffect
}
const serverIp = cfg.server.javaIp;
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-6">
{/* Header */}
<div className="text-center space-y-4">
<PartyPopper className="w-16 h-16 text-primary mx-auto" />
<h1 className="text-2xl font-semibold">You're whitelisted!</h1>
<p className="text-muted-foreground">
Welcome to FRG Network! You can now join the server.
As soon as you join, come back and click "Claim my reward" to get a gift on behalf of the team here.
</p>
</div>
{/* Server IP */}
<div className="space-y-2 w-full justify-start">
<p className="text-sm text-muted-foreground">Server Address</p>
<div className="flex w-full items-center">
<Input
value={serverIp || "Loading..."}
readOnly
className="font-mono w-full"
/>
<Button
className="border-l-0 border-input"
variant="outline"
onClick={handleCopyIp}
disabled={!serverIp}
>
<motion.div
className="flex flex-row items-center pl-1 justify-center w-16 gap-1.5"
key={copied ? "check" : "copy"}
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.6, opacity: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
{copied ? <><CheckCircle2 className="text-muted-foreground w-4 h-4" /> <span className="text-muted-foreground">Copied</span></> : <>
<Copy className="w-4 h-4" /> Copy
</>}
</motion.div>
</Button>
</div>
</div>
{/* Profile */}
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">Add on Bedrock</p>
<Item variant="outline">
<ItemMedia>
<Avatar size="lg">
<AvatarImage src={profileData?.avatarUrl || ""} />
<AvatarFallback>?</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>{cfg.server.bedrockFriend}</ItemTitle>
</ItemContent>
<ItemActions>
<Button asChild>
<Link href={`https://www.xbox.com/en-GB/play/user/${profileData?.gamertag}`} target="_blank">
<ExternalLink className="w-4 h-4 mr-2" />
View
</Link>
</Button>
</ItemActions>
</Item>
</div>
{/* Actions */}
<div className="space-y-3">
<Button asChild className="w-full">
<Link href="/survey" target="_blank" rel="noopener noreferrer">
<ExternalLink className="w-4 h-4 mr-2" />
Claim my reward
</Link>
</Button>
<Button variant="outline" onClick={handleReset} className="w-full">
Start over
</Button>
</div>
</div>
</div>
);
}
// Invalid step - redirect handled by useEffect
return null;
}

View file

@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
const clientId = process.env.WORKOS_CLIENT_ID
const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`
export async function POST(request: NextRequest) {
try {
const { code } = await request.json()
if (!code) {
return NextResponse.json(
{ error: 'Authorization code is required' },
{ status: 400 }
)
}
if (!clientId) {
return NextResponse.json(
{ error: 'WorkOS client ID not configured' },
{ status: 500 }
)
}
// Exchange the authorization code for an access token
const { user } = await workos.userManagement.authenticateWithCode({
code,
clientId,
})
// Here you would typically:
// 1. Create or update the user in your database
// 2. Create a session for the user
// 3. Set secure HTTP-only cookies with the tokens
// For now, we'll return the user info and a redirect URL
// In a real implementation, you'd want to handle sessions properly
return NextResponse.json({
success: true,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
profilePictureUrl: user.profilePictureUrl,
},
redirect: '/', // or to a dashboard page
})
} catch (error) {
console.error('Auth callback error:', error)
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Authentication failed'
},
{ status: 500 }
)
}
}

View file

@ -31,7 +31,7 @@ export async function POST(request: Request) {
// Handle Java whitelist // Handle Java whitelist
if ((preference === "java" || preference === "both") && javaName?.trim()) { if ((preference === "java" || preference === "both") && javaName?.trim()) {
const result = await servertap.addToWhitelist(javaName.trim(), undefined, false); const result = await servertap.addToWhitelist(javaName.trim());
if (result.success) { if (result.success) {
results.push(`Successfully whitelisted Java player: ${javaName}`); results.push(`Successfully whitelisted Java player: ${javaName}`);
} else { } else {
@ -42,7 +42,7 @@ export async function POST(request: Request) {
// Handle Bedrock whitelist (for now, we'll use the same endpoint but could be different) // Handle Bedrock whitelist (for now, we'll use the same endpoint but could be different)
if ((preference === "bedrock" || preference === "both") && bedrockName?.trim()) { if ((preference === "bedrock" || preference === "both") && bedrockName?.trim()) {
const bedrockNameWithDot = `.${bedrockName.trim()}`; const bedrockNameWithDot = `.${bedrockName.trim()}`;
const result = await servertap.addToWhitelist(bedrockNameWithDot, undefined, true); const result = await servertap.addToWhitelist(bedrockNameWithDot);
if (result.success) { if (result.success) {
results.push(`Successfully whitelisted Bedrock player: ${bedrockNameWithDot}`); results.push(`Successfully whitelisted Bedrock player: ${bedrockNameWithDot}`);
} else { } else {

View file

@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { getAvatarProfile } from "@/lib/avatar";
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const gamertag = searchParams.get("gamertag");
if (!gamertag) {
return NextResponse.json({ error: "no gamertag" }, { status: 400 });
}
// Use the simple avatar service
const result = await getAvatarProfile(gamertag);
return NextResponse.json(result);
} catch (error) {
console.error("Failed to fetch Xbox profile:", error);
// Return fallback data on error
const { searchParams } = new URL(req.url);
const gamertag = searchParams.get("gamertag") || "Unknown";
return NextResponse.json(
{
gamertag,
avatarUrl: "",
error: "Failed to fetch profile data"
},
{ status: 500 }
);
}
}

View file

@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { getXboxProfileWithAvatar } from "@/lib/xbox";
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const gamertag = searchParams.get("gamertag");
if (!gamertag) {
return NextResponse.json(
{ error: "Gamertag parameter is required" },
{ status: 400 }
);
}
// Use the modified function that reads from environment variables
const result = await getXboxProfileWithAvatar(gamertag);
return NextResponse.json(result);
} catch (error) {
console.error("Failed to fetch Xbox profile:", error);
// Return fallback data on error
const { searchParams } = new URL(req.url);
const gamertag = searchParams.get("gamertag") || "Unknown";
return NextResponse.json(
{
gamertag,
avatarUrl: "",
error: "Failed to fetch profile data"
},
{ status: 500 }
);
}
}

View file

@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import XboxTokenManager from "@/lib/xbox-auth";
export async function GET() {
try {
const tokenManager = XboxTokenManager.getInstance();
const status = tokenManager.getTokenStatus();
return NextResponse.json({
status,
message: status.valid ? "Tokens are valid" : "Tokens need refresh"
});
} catch (error) {
console.error("Failed to get token status:", error);
return NextResponse.json(
{ error: "Failed to get token status" },
{ status: 500 }
);
}
}
export async function POST() {
try {
const tokenManager = XboxTokenManager.getInstance();
const tokens = await tokenManager.forceRefresh();
return NextResponse.json({
success: true,
message: "Tokens refreshed successfully",
expiresAt: tokens.expiresAt
});
} catch (error) {
console.error("Failed to refresh tokens:", error);
return NextResponse.json(
{
error: "Failed to refresh tokens",
message: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);
}
}

View file

@ -0,0 +1,139 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Loader2 } from 'lucide-react'
export default function AuthCallback() {
const router = useRouter()
const searchParams = useSearchParams()
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
const [error, setError] = useState<string>('')
useEffect(() => {
const handleCallback = async () => {
try {
const code = searchParams.get('code')
const error = searchParams.get('error')
const errorDescription = searchParams.get('error_description')
if (error) {
setError(errorDescription || error)
setStatus('error')
return
}
if (!code) {
setError('No authorization code received')
setStatus('error')
return
}
// Exchange the authorization code for tokens
const response = await fetch('/api/auth/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Authentication failed')
}
const data = await response.json()
// Store the session/token as needed
if (data.redirect) {
window.location.href = data.redirect
} else {
// Default redirect to dashboard or home
router.push('/')
}
setStatus('success')
} catch (err) {
setError(err instanceof Error ? err.message : 'Authentication failed')
setStatus('error')
}
}
handleCallback()
}, [searchParams, router])
if (status === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
<p className="text-muted-foreground">Completing authentication...</p>
</div>
</div>
)
}
if (status === 'error') {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-4 max-w-md mx-auto p-6">
<div className="rounded-full bg-destructive/10 p-3 w-fit mx-auto">
<svg
className="h-6 w-6 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-foreground">Authentication Failed</h2>
<p className="text-muted-foreground">{error}</p>
<div className="space-y-2">
<button
onClick={() => router.push('/')}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full"
>
Go to Homepage
</button>
<button
onClick={() => window.location.reload()}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-full"
>
Try Again
</button>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center space-y-4">
<div className="rounded-full bg-green-100 p-3 w-fit mx-auto">
<svg
className="h-6 w-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<p className="text-muted-foreground">Authentication successful! Redirecting...</p>
</div>
</div>
)
}

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import { Toaster } from "@/components/ui/sonner"
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Google_Sans_Code, Mona_Sans } from "next/font/google"; import { Google_Sans_Code, Mona_Sans } from "next/font/google";
@ -36,7 +37,7 @@ export default function RootLayout({
> >
{children} {children}
</ThemeProvider> </ThemeProvider>
<Toaster position="bottom-center" />
</body> </body>
</html> </html>
); );

View file

@ -1,9 +1,9 @@
import { ComponentExample } from "@/components/component-example"; import { ComponentExample } from "@/components/component-example";
import { AccessCodeDialog, QRDialog } from "@/components/dialogs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Asterisk, QrCode } from "lucide-react"; import { Asterisk, QrCode } from "lucide-react";
import { DotGothic16 } from "next/font/google"; import { DotGothic16 } from "next/font/google";
import { cfg } from "@/lib/cfg"; import { cfg } from "@/lib/cfg";
import Link from "next/link";
const dots = DotGothic16({ const dots = DotGothic16({
weight: "400", weight: "400",
@ -21,14 +21,18 @@ export default function Page() {
</h1> </h1>
<p>A private Minecraft server for me and my friends.</p> <p>A private Minecraft server for me and my friends.</p>
{cfg.accessCode.enabled && ( {cfg.accessCode.enabled && (
<AccessCodeDialog /> <Button asChild>
<Link href="/access/code/verify">
<Asterisk /> I have an access code
</Link>
</Button>
)} )}
{cfg.qr.enabled && ( {cfg.qr.enabled && (
<QRDialog> <Button variant="outline" asChild>
<Button variant={"outline"}> <Link href="/access/qr/verify">
<QrCode /> I have a QR code <QrCode /> I have a QR code
</Link>
</Button> </Button>
</QRDialog>
)} )}
</div> </div>
</main> </main>

137
src/app/survey/page.tsx Normal file
View file

@ -0,0 +1,137 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowLeft, ExternalLink, Gift } from "lucide-react";
import Link from "next/link";
export default function SurveyPage() {
const [minecraftUsername, setMinecraftUsername] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [rewardQueued, setRewardQueued] = useState(false);
const handleRewardOnly = async (e: React.FormEvent) => {
e.preventDefault();
if (!minecraftUsername.trim()) return;
setIsSubmitting(true);
try {
// Call the reward endpoint to give diamonds
const response = await fetch("/reward", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
player: minecraftUsername,
}),
});
if (response.ok) {
setRewardQueued(true);
} else {
console.error("Failed to queue reward");
}
} catch (error) {
console.error("Error queuing reward:", error);
} finally {
setIsSubmitting(false);
}
};
const TALLY_FORM_URL = "https://tally.so/r/nP5KdL"; // Replace with your actual Tally form URL
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background">
<Card className="max-w-2xl w-full">
<CardHeader>
<div className="flex items-center gap-2 mb-2">
<Button variant="ghost" size="sm" asChild>
<Link href="/">
<ArrowLeft className="w-4 h-4" />
</Link>
</Button>
</div>
<CardTitle className="text-2xl">FRG Network Survey</CardTitle>
<CardDescription>
Help us improve your server experience! Complete this survey to receive 2 diamonds as a thank you.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Quick reward section */}
<div className="bg-muted/50 p-4 rounded-lg space-y-4">
<div className="flex items-center gap-2">
<Gift className="w-5 h-5 text-emerald-500" />
<h3 className="font-semibold">Get Your Diamonds</h3>
</div>
<p className="text-sm text-muted-foreground">
Enter your Minecraft username to queue 2 diamonds for when you join the server:
</p>
{!rewardQueued ? (
<form onSubmit={handleRewardOnly} className="flex gap-2">
<Input
placeholder="Your Minecraft username"
value={minecraftUsername}
onChange={(e) => setMinecraftUsername(e.target.value)}
className="flex-1 font-mono"
required
/>
<Button
type="submit"
disabled={isSubmitting || !minecraftUsername.trim()}
>
{isSubmitting ? "Queuing..." : "Queue Diamonds"}
</Button>
</form>
) : (
<div className="flex items-center gap-2 text-emerald-600">
<Gift className="w-4 h-4" />
<span className="text-sm font-medium">2 diamonds queued for {minecraftUsername}!</span>
</div>
)}
</div>
{/* Survey section */}
<div className="space-y-4">
<div>
<h3 className="font-semibold mb-2">Take the Full Survey</h3>
<p className="text-sm text-muted-foreground mb-4">
Help us improve the server by sharing your thoughts and preferences.
</p>
</div>
<Button asChild className="w-full" size="lg">
<a
href={TALLY_FORM_URL}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="w-4 h-4 mr-2" />
Open Survey in Tally
</a>
</Button>
<p className="text-xs text-muted-foreground text-center">
Opens in a new window • Takes about 2-3 minutes
</p>
</div>
{/* Navigation */}
<div className="pt-4 border-t">
<Button variant="outline" asChild className="w-full">
<Link href="/">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Home
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View file

@ -1,422 +0,0 @@
"use client";
import { useState, useTransition, useEffect } from "react";
import { Asterisk, CheckCircle2, Camera, CameraOff } from "lucide-react";
import { Button } from "./ui/button";
import {
Dialog,
DialogDescription,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Google_Sans_Code } from "next/font/google";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Scanner } from "@yudiel/react-qr-scanner";
import { googleSansCode } from "@/app/layout";
export function AccessCodeDialog() {
const [length, setLength] = useState(6);
const [enabled, setEnabled] = useState(false);
const [code, setCode] = useState("");
const [preference, setPreference] = useState<"java" | "bedrock" | "both">(
"java"
);
const [javaName, setJavaName] = useState("");
const [bedrockName, setBedrockName] = useState("");
const [status, setStatus] = useState<{ ok: boolean; message?: string }>({
ok: false,
});
const [isPending, startTransition] = useTransition();
const [verified, setVerified] = useState(false);
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch("/api/access-code-config");
const config = await response.json();
setLength(config.length || 6);
setEnabled(config.enabled);
} catch (error) {
console.error("Failed to fetch access code config:", error);
}
};
fetchConfig();
}, []);
const needsJavaName = preference === "java" || preference === "both";
const needsBedrockName = preference === "bedrock" || preference === "both";
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus({ ok: false, message: undefined });
if (verified) {
// Handle whitelist submission
if (
(needsJavaName && !javaName.trim()) ||
(needsBedrockName && !bedrockName.trim())
) {
setStatus({
ok: false,
message: "Please fill out every username we need to whitelist.",
});
return;
}
startTransition(async () => {
try {
const response = await fetch("/api/whitelist", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
javaName: needsJavaName ? javaName.trim() : undefined,
bedrockName: needsBedrockName ? bedrockName.trim() : undefined,
preference,
accessCode: code,
}),
});
const result = await response.json();
if (result.success) {
setStatus({
ok: true,
message: result.results.join(" ") || "Successfully added to whitelist!",
});
} else {
setStatus({
ok: false,
message: result.errors.join(" ") || "Failed to add to whitelist.",
});
}
} catch {
setStatus({
ok: false,
message: "We couldn't reach the server. Double-check your connection and try again.",
});
}
});
return;
}
// Handle access code verification
startTransition(async () => {
try {
const response = await fetch("/api/access-code", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
}),
});
const result = await response.json();
if (!result.success) {
setVerified(false);
setStatus({ ok: false, message: result.message });
return;
}
setVerified(true);
setStatus({
ok: true,
message:
"Access code verified!",
});
} catch {
setVerified(false);
setStatus({
ok: false,
message:
"We couldn't reach the server. Double-check your connection and try again.",
});
}
});
};
return (
<Dialog>
<DialogTrigger asChild>
<Button>
<Asterisk /> I have an access code
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Authenticate via access code</DialogTitle>
<DialogDescription>
Use the {length}-character invite your friend gave you so we can
safely whitelist you.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<fieldset className="space-y-2">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Access code
</Label>
<InputOTP
maxLength={length}
className={`w-full font-mono`}
value={code}
onChange={setCode}
disabled={verified}
>
<InputOTPGroup className="flex h-16 w-full">
{Array.from({ length }).map((_, i) => (
<InputOTPSlot
key={i}
index={i}
className="flex-1 h-16 font-mono text-3xl"
/>
))}
</InputOTPGroup>
</InputOTP>
</fieldset>
{verified && (
<>
<fieldset className="space-y-2">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Which edition?
</Label>
<ToggleGroup
type="single"
value={preference}
onValueChange={(value) =>
value && setPreference(value as typeof preference)
}
className="w-full"
>
<ToggleGroupItem value="java" className="flex-1">
Java
</ToggleGroupItem>
<ToggleGroupItem value="bedrock" className="flex-1">
Bedrock
</ToggleGroupItem>
<ToggleGroupItem value="both" className="flex-1">
Both
</ToggleGroupItem>
</ToggleGroup>
</fieldset>
{needsJavaName && (
<fieldset className="space-y-2">
<Label htmlFor="java-username">Java username</Label>
<Input
id="java-username"
placeholder="Your Java username"
value={javaName}
className="font-mono"
onChange={(event) => setJavaName(event.target.value)}
/>
</fieldset>
)}
{needsBedrockName && (
<fieldset className="space-y-2">
<Label htmlFor="bedrock-username">Bedrock Gamertag</Label>
<Input
id="bedrock-username"
placeholder="Your Xbox name"
value={bedrockName}
className="font-mono"
onChange={(event) => setBedrockName(event.target.value)}
/>
</fieldset>
)}
</>
)}
<DialogFooter className="gap-2">
<div className="flex-1 text-xs text-muted-foreground">
{status.message && (
<p className={status.ok ? "text-emerald-500" : "text-destructive"}>
{status.ok && <CheckCircle2 className="inline size-4 mr-1" />}
{status.message}
</p>
)}
</div>
<Button
type="submit"
disabled={isPending || (!verified && code.length < length)}
>
{isPending
? "Checking..."
: verified
? "Continue"
: "Verify access code"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
export function QRDialog({ children }: { children: React.ReactNode }) {
const [status, setStatus] = useState<{ ok: boolean; message?: string }>({
ok: false,
});
const [isPending, startTransition] = useTransition();
const [showScanner, setShowScanner] = useState(false);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scannerError, setScannerError] = useState<string | null>(null);
const handleQRScan = (detectedCodes: any[]) => {
if (detectedCodes.length > 0) {
const result = detectedCodes[0].rawValue;
const sanitizedResult = result.replace(/[^0-9A-Za-z]/g, "");
startTransition(async () => {
try {
const response = await fetch("/api/qr-verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: sanitizedResult,
}),
});
const verifyResult = await response.json();
if (verifyResult.success) {
setStatus({
ok: true,
message: verifyResult.message || "QR code verified successfully!",
});
} else {
setStatus({
ok: false,
message: verifyResult.message || "QR code verification failed",
});
}
} catch {
setStatus({
ok: false,
message: "We couldn't reach the server. Try again soon.",
});
}
});
}
};
const handleScannerError = (error: any) => {
console.error("QR Scanner error:", error);
setScannerError("Unable to access camera. Please check permissions.");
setHasPermission(false);
};
const handlePermission = (permission: boolean) => {
setHasPermission(permission);
if (!permission) {
setScannerError("Camera permission denied. Please enable camera access to scan QR codes.");
} else {
setScannerError(null);
}
};
return (
<Dialog>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Scan QR Code</DialogTitle>
<DialogDescription>
Scan a QR code to verify your access.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center space-y-4">
{scannerError ? (
<div className="text-center space-y-4">
<CameraOff className="w-12 h-12 text-muted-foreground mx-auto" />
<p className="text-sm text-muted-foreground">{scannerError}</p>
<Button onClick={() => setShowScanner(false)} variant="outline">
Close
</Button>
</div>
) : (
<>
<div className="w-full max-w-sm">
{hasPermission === false ? (
<div className="text-center space-y-4">
<CameraOff className="w-12 h-12 text-muted-foreground mx-auto" />
<p className="text-sm text-muted-foreground">
Camera permission is required to scan QR codes.
</p>
<Button onClick={() => setShowScanner(false)} variant="outline">
Close
</Button>
</div>
) : (
<div className="relative w-full h-64 rounded-md overflow-hidden">
<Scanner
onScan={handleQRScan}
onError={handleScannerError}
styles={{
container: {
width: "100%",
height: "100%",
borderRadius: "0.5rem",
overflow: "hidden",
},
video: {
width: "100%",
height: "100%",
objectFit: "cover",
},
}}
/>
{hasPermission === null && (
<div className="absolute inset-0 flex items-center justify-center bg-muted/50 rounded-md">
<div className="text-center space-y-2">
<Camera className="w-8 h-8 text-muted-foreground mx-auto animate-pulse" />
<p className="text-sm text-muted-foreground">
Requesting camera permission...
</p>
</div>
</div>
)}
</div>
)}
</div>
<div className="text-center space-y-4">
<p className="text-sm text-muted-foreground">
Position the QR code within the frame to scan
</p>
{status.message && (
<p className={status.ok ? "text-emerald-500" : "text-destructive"}>
{status.ok && <CheckCircle2 className="inline size-4 mr-1" />}
{status.message}
</p>
)}
<Button onClick={() => setShowScanner(false)} variant="outline">
Close
</Button>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"rounded-full aspect-square size-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn("bg-muted text-muted-foreground size-8 rounded-full text-xs/relaxed group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}

View file

@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", "focus-visible:border-ring cursor-pointer focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
{ {
variants: { variants: {
variant: { variant: {

195
src/components/ui/item.tsx Normal file
View file

@ -0,0 +1,195 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn(
"gap-4 has-[[data-size=sm]]:gap-2.5 has-[[data-size=xs]]:gap-2 group/item-group flex w-full flex-col",
className
)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-2", className)}
{...props}
/>
)
}
const itemVariants = cva(
"[a]:hover:bg-muted rounded-md border text-xs/relaxed w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors",
{
variants: {
variant: {
default: "border-transparent",
outline: "border-border",
muted: "bg-muted/50 border-transparent",
},
size: {
default: "gap-2.5 px-3 py-2.5",
sm: "gap-2.5 px-3 py-2.5",
xs: "gap-2.5 px-2.5 py-2 [[data-slot=dropdown-menu-content]_&]:p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none",
{
variants: {
variant: {
default: "bg-transparent",
icon: "[&_svg:not([class*='size-'])]:size-4",
image: "size-8 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"gap-1 group-data-[size=xs]/item:gap-0.5 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"gap-2 text-xs/relaxed leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground text-left text-xs/relaxed [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("gap-2 flex items-center", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"gap-2 flex basis-full items-center justify-between",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"gap-2 flex basis-full items-center justify-between",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View file

@ -0,0 +1,49 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

64
src/lib/avatar.ts Normal file
View file

@ -0,0 +1,64 @@
// Simple avatar service using environment variables
// No Xbox API complexity required!
export interface AvatarProfile {
gamertag: string;
avatarUrl: string;
}
export async function getAvatarProfile(gamertag: string): Promise<AvatarProfile> {
// Check if there's a custom avatar URL in environment variables
const customAvatarUrl = process.env.AVATAR_URL;
if (customAvatarUrl) {
console.log('Using custom avatar URL from environment');
return {
gamertag,
avatarUrl: customAvatarUrl
};
}
// Fallback to playerdb.co (no authentication required)
try {
const gamertagFormats = [
gamertag.replace(/\s+/g, ''), // No spaces: "FRGServers"
gamertag.replace(/\s+/g, '_'), // Underscore: "FRG_Servers"
gamertag.replace(/\s+/g, '%20'), // URL encoded: "FRG%20Servers"
gamertag // Original: "FRG Servers"
];
for (const format of gamertagFormats) {
try {
console.log(`Trying playerdb.co with gamertag: "${format}"`);
const res = await fetch(
`https://playerdb.co/api/player/xbox/${encodeURIComponent(format)}`
);
if (res.ok) {
const data = await res.json();
console.log('playerdb.co success with format:', format);
const avatarUrl = data.data?.player?.avatar ||
data.data?.player?.head?.avatar ||
"";
return {
gamertag: data.data?.player?.username || gamertag,
avatarUrl
};
}
} catch (e) {
console.log(`playerdb.co error with format "${format}":`, (e as Error).message);
}
}
} catch (error) {
console.error("Avatar lookup failed:", error);
}
// Final fallback - no avatar
return {
gamertag,
avatarUrl: ""
};
}

View file

@ -2,7 +2,6 @@ const sanitizeCode = (value?: string | null) =>
value?.replace(/[^0-9A-Za-z]/g, "") ?? ""; value?.replace(/[^0-9A-Za-z]/g, "") ?? "";
const accessCodeValue = process.env.SERVER_ACCESS_CODE ?? ""; const accessCodeValue = process.env.SERVER_ACCESS_CODE ?? "";
export const cfg = { export const cfg = {
qr: { qr: {
enabled: process.env.SERVER_QR_CODE_ENABLED === "true", enabled: process.env.SERVER_QR_CODE_ENABLED === "true",
@ -13,14 +12,18 @@ export const cfg = {
value: accessCodeValue, value: accessCodeValue,
length: sanitizeCode(accessCodeValue).length || 6, length: sanitizeCode(accessCodeValue).length || 6,
}, },
minecraftApi: {
url: process.env.MINECRAFT_API_URL ?? "http://localhost:6767",
key: process.env.MINECRAFT_API_KEY ?? "",
},
serverTap: { serverTap: {
url: process.env.SERVERTAP_BASE_URL, url: process.env.SERVERTAP_BASE_URL,
key: process.env.SERVERTAP_API_KEY, key: process.env.SERVERTAP_API_KEY,
}, },
server: { server: {
javaIp: process.env.SERVER_JAVA_IP ?? "", javaIp: process.env.NEXT_PUBLIC_SERVER_JAVA_IP ?? "",
bedrockIp: process.env.SERVER_BEDROCK_IP ?? "", bedrockIp: process.env.NEXT_PUBLIC_SERVER_BEDROCK_IP ?? "",
bedrockPort: process.env.SERVER_BEDROCK_PORT ?? "", bedrockPort: process.env.NEXT_PUBLIC_SERVER_BEDROCK_PORT ?? "",
bedrockFriend: process.env.SERVER_BEDROCK_FRIEND ?? "", bedrockFriend: process.env.NEXT_PUBLIC_SERVER_BEDROCK_FRIEND ?? "",
}, },
}; };

93
src/lib/minecraft-api.ts Normal file
View file

@ -0,0 +1,93 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_MINECRAFT_API_URL || 'http://localhost:6767';
const API_KEY = process.env.NEXT_PUBLIC_MINECRAFT_API_KEY;
interface ApiResponse<T = any> {
success: boolean;
message?: string;
error?: string;
details?: string;
data?: T;
}
class MinecraftApiClient {
private async request<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${API_BASE_URL}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
};
if (API_KEY) {
headers.Authorization = `Bearer ${API_KEY}`;
}
try {
const response = await fetch(url, {
...options,
headers,
});
const data = await response.json();
if (!response.ok) {
return {
success: false,
error: data.error || 'Request failed',
details: data.details,
};
}
return {
success: true,
data,
message: data.message,
};
} catch (error) {
return {
success: false,
error: 'Network error',
details: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async rewardPlayer(playerName: string): Promise<ApiResponse> {
return this.request('/reward', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async addToWhitelist(playerName: string): Promise<ApiResponse> {
return this.request('/api/whitelist/add', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async removeFromWhitelist(playerName: string): Promise<ApiResponse> {
return this.request('/api/whitelist/remove', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async getWhitelist(): Promise<ApiResponse<{ whitelist: string }>> {
return this.request('/api/whitelist/list', {
method: 'GET',
});
}
async executeCommand(command: string): Promise<ApiResponse<{ response: string }>> {
return this.request('/api/command', {
method: 'POST',
body: JSON.stringify({ command }),
});
}
}
export const minecraftApi = new MinecraftApiClient();
export type { ApiResponse };

View file

@ -1,71 +1,57 @@
import { cfg } from "./cfg"; import { cfg } from "./cfg";
type ServerTapPlayer = { type MinecraftPlayer = {
uuid: string; name: string;
displayName: string;
address: string;
port: number;
exhaustion: number;
exp: number;
whitelisted: boolean;
banned: boolean;
op: boolean;
}; };
type ServerTapResponse<T> = { type MinecraftResponse<T> = {
success: boolean; success: boolean;
data?: T; data?: T;
error?: string; error?: string;
message?: string;
details?: string;
}; };
class ServerTapClient { class MinecraftClient {
private baseUrl: string; private baseUrl: string;
private apiKey: string; private apiKey: string;
constructor() { constructor() {
this.baseUrl = cfg.serverTap.url || ""; this.baseUrl = cfg.minecraftApi.url || "http://localhost:6767";
this.apiKey = cfg.serverTap.key || ""; this.apiKey = cfg.minecraftApi.key || "";
} }
private async makeRequest<T>( private async makeRequest<T>(
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {}
): Promise<ServerTapResponse<T>> { ): Promise<MinecraftResponse<T>> {
if (!this.baseUrl || !this.apiKey) {
return {
success: false,
error: "ServerTap is not configured",
};
}
try { try {
const url = `${this.baseUrl}${endpoint}`; const url = `${this.baseUrl}${endpoint}`;
console.log(`ServerTap API Request: ${options.method || 'GET'} ${url}`); const headers: Record<string, string> = {
console.log(`Headers:`, {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", ...(options.headers as Record<string, string> || {}),
key: this.apiKey ? "***" : "MISSING", };
...options.headers,
}); if (this.apiKey) {
headers.Authorization = `Bearer ${this.apiKey}`;
}
console.log(`Minecraft API Request: ${options.method || 'GET'} ${url}`);
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers,
"Content-Type": "application/json",
"Accept": "application/json",
key: this.apiKey,
...options.headers,
},
...options, ...options,
}); });
console.log(`ServerTap API Response: ${response.status} ${response.statusText}`); console.log(`Minecraft API Response: ${response.status} ${response.statusText}`);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorData = await response.json().catch(() => ({}));
console.error(`ServerTap API Error Response: ${errorText}`); console.error(`Minecraft API Error Response:`, errorData);
return { return {
success: false, success: false,
error: `HTTP ${response.status}: ${response.statusText} - ${errorText}`, error: errorData.error || `HTTP ${response.status}: ${response.statusText}`,
details: errorData.details,
}; };
} }
@ -73,9 +59,10 @@ class ServerTapClient {
return { return {
success: true, success: true,
data, data,
message: data.message,
}; };
} catch (error) { } catch (error) {
console.error(`ServerTap API Error:`, error); console.error(`Minecraft API Error:`, error);
console.error(`Base URL: ${this.baseUrl}`); console.error(`Base URL: ${this.baseUrl}`);
console.error(`API Key configured: ${!!this.apiKey}`); console.error(`API Key configured: ${!!this.apiKey}`);
return { return {
@ -85,66 +72,43 @@ class ServerTapClient {
} }
} }
async addToWhitelist(playerName: string, playerUuid?: string, isBedrock?: boolean): Promise<ServerTapResponse<void>> { async addToWhitelist(playerName: string): Promise<MinecraftResponse<void>> {
let uuid = playerUuid; return this.makeRequest("/api/whitelist/add", {
// For Bedrock players, convert XUID to Java UUID format if no UUID provided
if (isBedrock && !uuid) {
// Remove dot prefix and convert XUID to Java UUID format
const xuid = playerName.replace('.', '');
uuid = `00000000-0000-0000-0000-${xuid.padStart(12, '0')}`;
} else if (!uuid) {
// For Java players without UUID, leave empty as shown in curl example
uuid = '';
}
const body = new URLSearchParams({
uuid: uuid,
name: playerName,
});
return this.makeRequest(`/v1/server/whitelist`, {
method: "POST", method: "POST",
headers: { body: JSON.stringify({ player: playerName }),
"Content-Type": "application/x-www-form-urlencoded",
},
body: body.toString(),
}); });
} }
async removeFromWhitelist(playerName: string): Promise<ServerTapResponse<void>> { async removeFromWhitelist(playerName: string): Promise<MinecraftResponse<void>> {
return this.makeRequest(`/v1/whitelist/remove/${encodeURIComponent(playerName)}`, { return this.makeRequest("/api/whitelist/remove", {
method: "POST", method: "POST",
body: JSON.stringify({ player: playerName }),
}); });
} }
async getWhitelist(): Promise<ServerTapResponse<ServerTapPlayer[]>> { async getWhitelist(): Promise<MinecraftResponse<{ whitelist: string }>> {
return this.makeRequest("/v1/whitelist"); return this.makeRequest("/api/whitelist/list", {
method: "GET",
});
} }
async addOp(playerName: string): Promise<ServerTapResponse<void>> { async rewardPlayer(playerName: string): Promise<MinecraftResponse<void>> {
return this.makeRequest(`/v1/ops/add/${encodeURIComponent(playerName)}`, { return this.makeRequest("/reward", {
method: "POST", method: "POST",
body: JSON.stringify({ player: playerName }),
}); });
} }
async removeOp(playerName: string): Promise<ServerTapResponse<void>> { async executeCommand(command: string): Promise<MinecraftResponse<{ response: string }>> {
return this.makeRequest(`/v1/ops/remove/${encodeURIComponent(playerName)}`, { return this.makeRequest("/api/command", {
method: "POST", method: "POST",
body: JSON.stringify({ command }),
}); });
} }
async getOps(): Promise<ServerTapResponse<ServerTapPlayer[]>> {
return this.makeRequest("/v1/ops");
} }
async getPlayers(): Promise<ServerTapResponse<ServerTapPlayer[]>> { export const minecraft = new MinecraftClient();
return this.makeRequest("/v1/players");
}
async getServerInfo(): Promise<ServerTapResponse<any>> { // Legacy exports for backward compatibility
return this.makeRequest("/v1/server"); export const servertap = minecraft;
} export type { MinecraftPlayer as ServerTapPlayer, MinecraftResponse as ServerTapResponse };
}
export const servertap = new ServerTapClient();

28
src/lib/xbox-server.ts Normal file
View file

@ -0,0 +1,28 @@
import { getXboxProfileWithAvatar } from "./xbox";
/**
* Server-side helper to fetch Xbox profile data using environment variables
* This function can only be called on the server side
*/
export async function getXboxProfileServer(gamertag: string): Promise<{
gamertag: string;
avatarUrl: string;
error?: string;
}> {
try {
// Use the modified function that reads from environment variables
const result = await getXboxProfileWithAvatar(gamertag);
return result;
} catch (error) {
console.error("Failed to fetch Xbox profile:", error);
// Return fallback data on error
return {
gamertag,
avatarUrl: "",
error: "Failed to fetch profile data"
};
}
}