refactored a LOT of code, added some touches
This commit is contained in:
parent
c1bdb0d182
commit
cdadb96545
22 changed files with 1851 additions and 522 deletions
96
bun.lock
96
bun.lock
|
|
@ -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=="],
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
695
src/app/access/[method]/[step]/page.tsx
Normal file
695
src/app/access/[method]/[step]/page.tsx
Normal 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;
|
||||||
|
}
|
||||||
61
src/app/api/auth/callback/route.ts
Normal file
61
src/app/api/auth/callback/route.ts
Normal 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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
34
src/app/api/xbox/profile/get/route.ts
Normal file
34
src/app/api/xbox/profile/get/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/app/api/xbox/profile/route.ts
Normal file
37
src/app/api/xbox/profile/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/app/api/xbox/tokens/route.ts
Normal file
42
src/app/api/xbox/tokens/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/app/auth/callback/page.tsx
Normal file
139
src/app/auth/callback/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
</Button>
|
</Link>
|
||||||
</QRDialog>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
137
src/app/survey/page.tsx
Normal file
137
src/app/survey/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
109
src/components/ui/avatar.tsx
Normal file
109
src/components/ui/avatar.tsx
Normal 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,
|
||||||
|
}
|
||||||
|
|
@ -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
195
src/components/ui/item.tsx
Normal 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,
|
||||||
|
}
|
||||||
49
src/components/ui/sonner.tsx
Normal file
49
src/components/ui/sonner.tsx
Normal 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
64
src/lib/avatar.ts
Normal 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: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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
93
src/lib/minecraft-api.ts
Normal 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 };
|
||||||
|
|
@ -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[]>> {
|
|
||||||
return this.makeRequest("/v1/players");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerInfo(): Promise<ServerTapResponse<any>> {
|
|
||||||
return this.makeRequest("/v1/server");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const servertap = new ServerTapClient();
|
export const minecraft = new MinecraftClient();
|
||||||
|
|
||||||
|
// Legacy exports for backward compatibility
|
||||||
|
export const servertap = minecraft;
|
||||||
|
export type { MinecraftPlayer as ServerTapPlayer, MinecraftResponse as ServerTapResponse };
|
||||||
|
|
|
||||||
28
src/lib/xbox-server.ts
Normal file
28
src/lib/xbox-server.ts
Normal 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue