Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4396d9527 | ||
|
|
352fadadb5 |
@@ -31,8 +31,8 @@
|
|||||||
"facehash": "^0.0.7",
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.3.0",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.2.7",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"redaxios": "^0.5.1",
|
"redaxios": "^0.5.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"twilio": "^5.8.0",
|
"twilio": "^5.8.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
"web-vitals": "^6.2.0",
|
|
||||||
"zod": "^4.0.15",
|
"zod": "^4.0.15",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -49,8 +48,8 @@
|
|||||||
"@lingui/cli": "^6.5.0",
|
"@lingui/cli": "^6.5.0",
|
||||||
"@lingui/vite-plugin": "^6.5.0",
|
"@lingui/vite-plugin": "^6.5.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
@@ -655,9 +654,9 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="],
|
"@types/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||||
|
|
||||||
@@ -1329,9 +1328,9 @@
|
|||||||
|
|
||||||
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
|
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
|
||||||
|
|
||||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||||
|
|
||||||
"react-imask": ["react-imask@7.6.1", "", { "dependencies": { "imask": "^7.6.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=0.14.0" } }, "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA=="],
|
"react-imask": ["react-imask@7.6.1", "", { "dependencies": { "imask": "^7.6.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=0.14.0" } }, "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA=="],
|
||||||
|
|
||||||
@@ -1397,7 +1396,7 @@
|
|||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
|
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
|
||||||
|
|
||||||
@@ -1591,8 +1590,6 @@
|
|||||||
|
|
||||||
"web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="],
|
"web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="],
|
||||||
|
|
||||||
"web-vitals": ["web-vitals@6.2.0", "", {}, "sha512-i2jhLkq6eB38jgJX5OIjEkyzQKgsG7FR56v7GphJNjzKAySgbRbwAfsFC0dDfHROWmCycf912SJpU2QoYIcMVg=="],
|
|
||||||
|
|
||||||
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||||
|
|
||||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ spec:
|
|||||||
value: "3000"
|
value: "3000"
|
||||||
- name: NODE_ENV
|
- name: NODE_ENV
|
||||||
value: "production"
|
value: "production"
|
||||||
- name: TELEMETRY_EXCLUDE_PHONES
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: flxn-secrets
|
|
||||||
key: telemetry_exclude_phones
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
- name: TWILIO_ACCOUNT_SID
|
- name: TWILIO_ACCOUNT_SID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
|
|||||||
+4
-5
@@ -43,8 +43,8 @@
|
|||||||
"facehash": "^0.0.7",
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.3.0",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.2.7",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"redaxios": "^0.5.1",
|
"redaxios": "^0.5.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
"twilio": "^5.8.0",
|
"twilio": "^5.8.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
"web-vitals": "^6.2.0",
|
|
||||||
"zod": "^4.0.15"
|
"zod": "^4.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -61,8 +60,8 @@
|
|||||||
"@lingui/cli": "^6.5.0",
|
"@lingui/cli": "^6.5.0",
|
||||||
"@lingui/vite-plugin": "^6.5.0",
|
"@lingui/vite-plugin": "^6.5.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
/// <reference path="../pb_data/types.d.ts" />
|
|
||||||
migrate((app) => {
|
|
||||||
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
|
||||||
|
|
||||||
unmarshal({
|
|
||||||
"indexes": [
|
|
||||||
"CREATE INDEX `idx_activities_created` ON `activities` (`created`)",
|
|
||||||
"CREATE INDEX `idx_activities_name_created` ON `activities` (`name`, `created`)",
|
|
||||||
"CREATE INDEX `idx_activities_player_created` ON `activities` (`player`, `created`)",
|
|
||||||
"CREATE INDEX `idx_activities_success_created` ON `activities` (`success`, `created`)"
|
|
||||||
]
|
|
||||||
}, collection)
|
|
||||||
|
|
||||||
return app.save(collection)
|
|
||||||
}, (app) => {
|
|
||||||
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
|
||||||
|
|
||||||
unmarshal({
|
|
||||||
"indexes": []
|
|
||||||
}, collection)
|
|
||||||
|
|
||||||
return app.save(collection)
|
|
||||||
})
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
/// <reference path="../pb_data/types.d.ts" />
|
|
||||||
migrate((app) => {
|
|
||||||
const collection = new Collection({
|
|
||||||
"createRule": null,
|
|
||||||
"deleteRule": null,
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "[a-z0-9]{15}",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text3208210256",
|
|
||||||
"max": 15,
|
|
||||||
"min": 15,
|
|
||||||
"name": "id",
|
|
||||||
"pattern": "^[a-z0-9]+$",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": true,
|
|
||||||
"required": true,
|
|
||||||
"system": true,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ce_name",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "name",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cascadeDelete": false,
|
|
||||||
"collectionId": "pbc_3072146508",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "relation_ce_player",
|
|
||||||
"maxSelect": 1,
|
|
||||||
"minSelect": 0,
|
|
||||||
"name": "player",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "relation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ce_session_id",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "session_id",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ce_path",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "path",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ce_route_id",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "route_id",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "number_ce_value",
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"name": "value",
|
|
||||||
"onlyInt": false,
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "json_ce_props",
|
|
||||||
"maxSize": 0,
|
|
||||||
"name": "props",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ce_user_agent",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "user_agent",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate2990389176",
|
|
||||||
"name": "created",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": false,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate3332085495",
|
|
||||||
"name": "updated",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": true,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "pbc_client_events",
|
|
||||||
"indexes": [
|
|
||||||
"CREATE INDEX `idx_client_events_created` ON `client_events` (`created`)",
|
|
||||||
"CREATE INDEX `idx_client_events_name_created` ON `client_events` (`name`, `created`)",
|
|
||||||
"CREATE INDEX `idx_client_events_player_created` ON `client_events` (`player`, `created`)",
|
|
||||||
"CREATE INDEX `idx_client_events_session_id` ON `client_events` (`session_id`)"
|
|
||||||
],
|
|
||||||
"listRule": null,
|
|
||||||
"name": "client_events",
|
|
||||||
"system": false,
|
|
||||||
"type": "base",
|
|
||||||
"updateRule": null,
|
|
||||||
"viewRule": null
|
|
||||||
});
|
|
||||||
|
|
||||||
return app.save(collection);
|
|
||||||
}, (app) => {
|
|
||||||
const collection = app.findCollectionByNameOrId("pbc_client_events");
|
|
||||||
|
|
||||||
return app.delete(collection);
|
|
||||||
})
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
/// <reference path="../pb_data/types.d.ts" />
|
|
||||||
migrate((app) => {
|
|
||||||
const collection = new Collection({
|
|
||||||
"createRule": null,
|
|
||||||
"deleteRule": null,
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "[a-z0-9]{15}",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text3208210256",
|
|
||||||
"max": 15,
|
|
||||||
"min": 15,
|
|
||||||
"name": "id",
|
|
||||||
"pattern": "^[a-z0-9]+$",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": true,
|
|
||||||
"required": true,
|
|
||||||
"system": true,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_message",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "message",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_stack",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "stack",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "select_cerr_source",
|
|
||||||
"maxSelect": 1,
|
|
||||||
"name": "source",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "select",
|
|
||||||
"values": [
|
|
||||||
"window",
|
|
||||||
"unhandledrejection",
|
|
||||||
"error-boundary",
|
|
||||||
"sw"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_path",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "path",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_route_id",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "route_id",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_group_hash",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "group_hash",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "bool_cerr_resolved",
|
|
||||||
"name": "resolved",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "bool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cascadeDelete": false,
|
|
||||||
"collectionId": "pbc_3072146508",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "relation_cerr_player",
|
|
||||||
"maxSelect": 1,
|
|
||||||
"minSelect": 0,
|
|
||||||
"name": "player",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "relation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_session_id",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "session_id",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_cerr_user_agent",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "user_agent",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "json_cerr_props",
|
|
||||||
"maxSize": 0,
|
|
||||||
"name": "props",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate2990389176",
|
|
||||||
"name": "created",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": false,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate3332085495",
|
|
||||||
"name": "updated",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": true,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "pbc_client_errors",
|
|
||||||
"indexes": [
|
|
||||||
"CREATE INDEX `idx_client_errors_group_created` ON `client_errors` (`group_hash`, `created`)",
|
|
||||||
"CREATE INDEX `idx_client_errors_created` ON `client_errors` (`created`)",
|
|
||||||
"CREATE INDEX `idx_client_errors_resolved_created` ON `client_errors` (`resolved`, `created`)"
|
|
||||||
],
|
|
||||||
"listRule": null,
|
|
||||||
"name": "client_errors",
|
|
||||||
"system": false,
|
|
||||||
"type": "base",
|
|
||||||
"updateRule": null,
|
|
||||||
"viewRule": null
|
|
||||||
});
|
|
||||||
|
|
||||||
return app.save(collection);
|
|
||||||
}, (app) => {
|
|
||||||
const collection = app.findCollectionByNameOrId("pbc_client_errors");
|
|
||||||
|
|
||||||
return app.delete(collection);
|
|
||||||
})
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
/// <reference path="../pb_data/types.d.ts" />
|
|
||||||
migrate((app) => {
|
|
||||||
const rollups = new Collection({
|
|
||||||
"createRule": null,
|
|
||||||
"deleteRule": null,
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "[a-z0-9]{15}",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text3208210256",
|
|
||||||
"max": 15,
|
|
||||||
"min": 15,
|
|
||||||
"name": "id",
|
|
||||||
"pattern": "^[a-z0-9]+$",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": true,
|
|
||||||
"required": true,
|
|
||||||
"system": true,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_tr_date",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "date",
|
|
||||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_tr_metric",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "metric",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_tr_dim",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "dim",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "number_tr_value",
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"name": "value",
|
|
||||||
"onlyInt": false,
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "json_tr_meta",
|
|
||||||
"maxSize": 0,
|
|
||||||
"name": "meta",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate2990389176",
|
|
||||||
"name": "created",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": false,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate3332085495",
|
|
||||||
"name": "updated",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": true,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "pbc_telemetry_rollups",
|
|
||||||
"indexes": [
|
|
||||||
"CREATE UNIQUE INDEX `idx_rollups_date_metric_dim` ON `telemetry_rollups` (`date`, `metric`, `dim`)",
|
|
||||||
"CREATE INDEX `idx_rollups_metric_date` ON `telemetry_rollups` (`metric`, `date`)"
|
|
||||||
],
|
|
||||||
"listRule": null,
|
|
||||||
"name": "telemetry_rollups",
|
|
||||||
"system": false,
|
|
||||||
"type": "base",
|
|
||||||
"updateRule": null,
|
|
||||||
"viewRule": null
|
|
||||||
});
|
|
||||||
|
|
||||||
app.save(rollups);
|
|
||||||
|
|
||||||
const alerts = new Collection({
|
|
||||||
"createRule": null,
|
|
||||||
"deleteRule": null,
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "[a-z0-9]{15}",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text3208210256",
|
|
||||||
"max": 15,
|
|
||||||
"min": 15,
|
|
||||||
"name": "id",
|
|
||||||
"pattern": "^[a-z0-9]+$",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": true,
|
|
||||||
"required": true,
|
|
||||||
"system": true,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ta_kind",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "kind",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": true,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ta_dim",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "dim",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"autogeneratePattern": "",
|
|
||||||
"hidden": false,
|
|
||||||
"id": "text_ta_message",
|
|
||||||
"max": 0,
|
|
||||||
"min": 0,
|
|
||||||
"name": "message",
|
|
||||||
"pattern": "",
|
|
||||||
"presentable": false,
|
|
||||||
"primaryKey": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "number_ta_value",
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"name": "value",
|
|
||||||
"onlyInt": false,
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "json_ta_meta",
|
|
||||||
"maxSize": 0,
|
|
||||||
"name": "meta",
|
|
||||||
"presentable": false,
|
|
||||||
"required": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate2990389176",
|
|
||||||
"name": "created",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": false,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": false,
|
|
||||||
"id": "autodate3332085495",
|
|
||||||
"name": "updated",
|
|
||||||
"onCreate": true,
|
|
||||||
"onUpdate": true,
|
|
||||||
"presentable": false,
|
|
||||||
"system": false,
|
|
||||||
"type": "autodate"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"id": "pbc_telemetry_alerts",
|
|
||||||
"indexes": [
|
|
||||||
"CREATE INDEX `idx_alerts_kind_dim_created` ON `telemetry_alerts` (`kind`, `dim`, `created`)"
|
|
||||||
],
|
|
||||||
"listRule": null,
|
|
||||||
"name": "telemetry_alerts",
|
|
||||||
"system": false,
|
|
||||||
"type": "base",
|
|
||||||
"updateRule": null,
|
|
||||||
"viewRule": null
|
|
||||||
});
|
|
||||||
|
|
||||||
return app.save(alerts);
|
|
||||||
}, (app) => {
|
|
||||||
app.delete(app.findCollectionByNameOrId("pbc_telemetry_alerts"));
|
|
||||||
return app.delete(app.findCollectionByNameOrId("pbc_telemetry_rollups"));
|
|
||||||
})
|
|
||||||
@@ -50,12 +50,6 @@ textarea,
|
|||||||
transform: scale(0.90) translateY(-4px);
|
transform: scale(0.90) translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* themes paint their full-page texture through this element */
|
|
||||||
.flxn-theme-overlay {
|
|
||||||
position: fixed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.app,
|
.app,
|
||||||
[data-drawer-level] {
|
[data-drawer-level] {
|
||||||
@@ -13,6 +13,7 @@ const { count, size, warnings } = await generateSW({
|
|||||||
'icon-192x192.png',
|
'icon-192x192.png',
|
||||||
'icon-512x512.png',
|
'icon-512x512.png',
|
||||||
'site.webmanifest',
|
'site.webmanifest',
|
||||||
|
'styles.css',
|
||||||
'push-handlers.js',
|
'push-handlers.js',
|
||||||
],
|
],
|
||||||
// Hand-written Web Push handlers (public/push-handlers.js) are pulled into
|
// Hand-written Web Push handlers (public/push-handlers.js) are pulled into
|
||||||
|
|||||||
+8
-17
@@ -1,5 +1,5 @@
|
|||||||
// Grant or revoke a role for a user by phone number (US +1 assumed).
|
// Grant or revoke the "Admin" role for a user by phone number (US +1 assumed).
|
||||||
// Usage: bun run scripts/make-admin.ts <phone> [--role Admin|Developer] [--revoke]
|
// Usage: bun run scripts/make-admin.ts <phone> [--revoke]
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import SuperTokens from "supertokens-node";
|
import SuperTokens from "supertokens-node";
|
||||||
import Session from "supertokens-node/recipe/session";
|
import Session from "supertokens-node/recipe/session";
|
||||||
@@ -26,19 +26,10 @@ SuperTokens.init({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const VALID_ROLES = ["Admin", "Developer"] as const;
|
|
||||||
|
|
||||||
const raw = process.argv[2];
|
const raw = process.argv[2];
|
||||||
const revoke = process.argv.includes("--revoke");
|
const revoke = process.argv.includes("--revoke");
|
||||||
const roleFlagIndex = process.argv.indexOf("--role");
|
|
||||||
const role = roleFlagIndex === -1 ? "Admin" : process.argv[roleFlagIndex + 1];
|
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
console.error("Usage: bun run scripts/make-admin.ts <phone> [--role Admin|Developer] [--revoke]");
|
console.error("Usage: bun run scripts/make-admin.ts <phone> [--revoke]");
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
|
|
||||||
console.error(`Invalid role "${role}". Valid roles: ${VALID_ROLES.join(", ")}`);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +60,15 @@ if (!user) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserRoles.createNewRoleOrAddPermissions(role, []);
|
await UserRoles.createNewRoleOrAddPermissions("Admin", []);
|
||||||
|
|
||||||
if (revoke) {
|
if (revoke) {
|
||||||
const res = await UserRoles.removeUserRole("public", user.id, role);
|
const res = await UserRoles.removeUserRole("public", user.id, "Admin");
|
||||||
console.log(`Removed ${role} from ${matched} (user ${user.id}):`, res.status);
|
console.log(`Removed Admin from ${matched} (user ${user.id}):`, res.status);
|
||||||
} else {
|
} else {
|
||||||
const res = await UserRoles.addRoleToUser("public", user.id, role);
|
const res = await UserRoles.addRoleToUser("public", user.id, "Admin");
|
||||||
console.log(
|
console.log(
|
||||||
`Granted ${role} to ${matched} (user ${user.id}):`,
|
`Granted Admin to ${matched} (user ${user.id}):`,
|
||||||
res.status === "OK"
|
res.status === "OK"
|
||||||
? res.didUserAlreadyHaveRole
|
? res.didUserAlreadyHaveRole
|
||||||
? "already had it"
|
? "already had it"
|
||||||
|
|||||||
+1
-206
@@ -22,7 +22,6 @@ import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
|
|||||||
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
|
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
|
||||||
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
|
import { Route as AuthedAdminIndexRouteImport } from './routes/_authed/admin/index'
|
||||||
import { Route as ApiTournamentsUploadLogoRouteImport } from './routes/api/tournaments/upload-logo'
|
import { Route as ApiTournamentsUploadLogoRouteImport } from './routes/api/tournaments/upload-logo'
|
||||||
import { Route as ApiTelemetryBeaconRouteImport } from './routes/api/telemetry/beacon'
|
|
||||||
import { Route as ApiTeamsUploadLogoRouteImport } from './routes/api/teams/upload-logo'
|
import { Route as ApiTeamsUploadLogoRouteImport } from './routes/api/teams/upload-logo'
|
||||||
import { Route as ApiSpotifyTokenRouteImport } from './routes/api/spotify/token'
|
import { Route as ApiSpotifyTokenRouteImport } from './routes/api/spotify/token'
|
||||||
import { Route as ApiSpotifySearchRouteImport } from './routes/api/spotify/search'
|
import { Route as ApiSpotifySearchRouteImport } from './routes/api/spotify/search'
|
||||||
@@ -38,27 +37,19 @@ import { Route as ApiAuthSplatRouteImport } from './routes/api/auth.$'
|
|||||||
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
|
import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_authed/tournaments/$tournamentId'
|
||||||
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$teamId'
|
||||||
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
||||||
import { Route as AuthedAdminTelemetryRouteImport } from './routes/_authed/admin/telemetry'
|
|
||||||
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||||
import { Route as AuthedAdminBadgesRouteImport } from './routes/_authed/admin/badges'
|
import { Route as AuthedAdminBadgesRouteImport } from './routes/_authed/admin/badges'
|
||||||
import { Route as AuthedAdminActivitiesRouteImport } from './routes/_authed/admin/activities'
|
import { Route as AuthedAdminActivitiesRouteImport } from './routes/_authed/admin/activities'
|
||||||
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||||
import { Route as AuthedAdminTelemetryIndexRouteImport } from './routes/_authed/admin/telemetry/index'
|
|
||||||
import { Route as AuthedTournamentsIdPredictionsRouteImport } from './routes/_authed/tournaments/$id.predictions'
|
import { Route as AuthedTournamentsIdPredictionsRouteImport } from './routes/_authed/tournaments/$id.predictions'
|
||||||
import { Route as AuthedTournamentsIdGroupsRouteImport } from './routes/_authed/tournaments/$id.groups'
|
import { Route as AuthedTournamentsIdGroupsRouteImport } from './routes/_authed/tournaments/$id.groups'
|
||||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||||
import { Route as AuthedAdminTelemetryUsageRouteImport } from './routes/_authed/admin/telemetry/usage'
|
|
||||||
import { Route as AuthedAdminTelemetrySessionsRouteImport } from './routes/_authed/admin/telemetry/sessions'
|
|
||||||
import { Route as AuthedAdminTelemetryPlayersRouteImport } from './routes/_authed/admin/telemetry/players'
|
|
||||||
import { Route as AuthedAdminTelemetryFunctionsRouteImport } from './routes/_authed/admin/telemetry/functions'
|
|
||||||
import { Route as AuthedAdminTelemetryErrorsRouteImport } from './routes/_authed/admin/telemetry/errors'
|
|
||||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||||
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||||
import { Route as AuthedTournamentsIdPredictionsMakeRouteImport } from './routes/_authed/tournaments/$id.predictions_.make'
|
import { Route as AuthedTournamentsIdPredictionsMakeRouteImport } from './routes/_authed/tournaments/$id.predictions_.make'
|
||||||
import { Route as AuthedTournamentsIdPredictionsPlayerIdRouteImport } from './routes/_authed/tournaments/$id.predictions_.$playerId'
|
import { Route as AuthedTournamentsIdPredictionsPlayerIdRouteImport } from './routes/_authed/tournaments/$id.predictions_.$playerId'
|
||||||
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||||
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
||||||
import { Route as AuthedAdminTournamentsIdEnrollmentRouteImport } from './routes/_authed/admin/tournaments/$id/enrollment'
|
|
||||||
import { Route as AuthedAdminTournamentsIdAssignPartnersRouteImport } from './routes/_authed/admin/tournaments/$id/assign-partners'
|
import { Route as AuthedAdminTournamentsIdAssignPartnersRouteImport } from './routes/_authed/admin/tournaments/$id/assign-partners'
|
||||||
|
|
||||||
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
||||||
@@ -126,11 +117,6 @@ const ApiTournamentsUploadLogoRoute =
|
|||||||
path: '/api/tournaments/upload-logo',
|
path: '/api/tournaments/upload-logo',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const ApiTelemetryBeaconRoute = ApiTelemetryBeaconRouteImport.update({
|
|
||||||
id: '/api/telemetry/beacon',
|
|
||||||
path: '/api/telemetry/beacon',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiTeamsUploadLogoRoute = ApiTeamsUploadLogoRouteImport.update({
|
const ApiTeamsUploadLogoRoute = ApiTeamsUploadLogoRouteImport.update({
|
||||||
id: '/api/teams/upload-logo',
|
id: '/api/teams/upload-logo',
|
||||||
path: '/api/teams/upload-logo',
|
path: '/api/teams/upload-logo',
|
||||||
@@ -207,11 +193,6 @@ const AuthedProfilePlayerIdRoute = AuthedProfilePlayerIdRouteImport.update({
|
|||||||
path: '/profile/$playerId',
|
path: '/profile/$playerId',
|
||||||
getParentRoute: () => AuthedRoute,
|
getParentRoute: () => AuthedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthedAdminTelemetryRoute = AuthedAdminTelemetryRouteImport.update({
|
|
||||||
id: '/telemetry',
|
|
||||||
path: '/telemetry',
|
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
|
const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
|
||||||
id: '/preview',
|
id: '/preview',
|
||||||
path: '/preview',
|
path: '/preview',
|
||||||
@@ -233,12 +214,6 @@ const AuthedAdminTournamentsIndexRoute =
|
|||||||
path: '/tournaments/',
|
path: '/tournaments/',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthedAdminTelemetryIndexRoute =
|
|
||||||
AuthedAdminTelemetryIndexRouteImport.update({
|
|
||||||
id: '/',
|
|
||||||
path: '/',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedTournamentsIdPredictionsRoute =
|
const AuthedTournamentsIdPredictionsRoute =
|
||||||
AuthedTournamentsIdPredictionsRouteImport.update({
|
AuthedTournamentsIdPredictionsRouteImport.update({
|
||||||
id: '/tournaments/$id/predictions',
|
id: '/tournaments/$id/predictions',
|
||||||
@@ -257,36 +232,6 @@ const AuthedTournamentsIdBracketRoute =
|
|||||||
path: '/tournaments/$id/bracket',
|
path: '/tournaments/$id/bracket',
|
||||||
getParentRoute: () => AuthedRoute,
|
getParentRoute: () => AuthedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthedAdminTelemetryUsageRoute =
|
|
||||||
AuthedAdminTelemetryUsageRouteImport.update({
|
|
||||||
id: '/usage',
|
|
||||||
path: '/usage',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTelemetrySessionsRoute =
|
|
||||||
AuthedAdminTelemetrySessionsRouteImport.update({
|
|
||||||
id: '/sessions',
|
|
||||||
path: '/sessions',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTelemetryPlayersRoute =
|
|
||||||
AuthedAdminTelemetryPlayersRouteImport.update({
|
|
||||||
id: '/players',
|
|
||||||
path: '/players',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTelemetryFunctionsRoute =
|
|
||||||
AuthedAdminTelemetryFunctionsRouteImport.update({
|
|
||||||
id: '/functions',
|
|
||||||
path: '/functions',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTelemetryErrorsRoute =
|
|
||||||
AuthedAdminTelemetryErrorsRouteImport.update({
|
|
||||||
id: '/errors',
|
|
||||||
path: '/errors',
|
|
||||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTournamentsIdIndexRoute =
|
const AuthedAdminTournamentsIdIndexRoute =
|
||||||
AuthedAdminTournamentsIdIndexRouteImport.update({
|
AuthedAdminTournamentsIdIndexRouteImport.update({
|
||||||
id: '/tournaments/$id/',
|
id: '/tournaments/$id/',
|
||||||
@@ -323,12 +268,6 @@ const AuthedAdminTournamentsIdTeamsRoute =
|
|||||||
path: '/tournaments/$id/teams',
|
path: '/tournaments/$id/teams',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthedAdminTournamentsIdEnrollmentRoute =
|
|
||||||
AuthedAdminTournamentsIdEnrollmentRouteImport.update({
|
|
||||||
id: '/tournaments/$id/enrollment',
|
|
||||||
path: '/tournaments/$id/enrollment',
|
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthedAdminTournamentsIdAssignPartnersRoute =
|
const AuthedAdminTournamentsIdAssignPartnersRoute =
|
||||||
AuthedAdminTournamentsIdAssignPartnersRouteImport.update({
|
AuthedAdminTournamentsIdAssignPartnersRouteImport.update({
|
||||||
id: '/tournaments/$id/assign-partners',
|
id: '/tournaments/$id/assign-partners',
|
||||||
@@ -349,7 +288,6 @@ export interface FileRoutesByFullPath {
|
|||||||
'/admin/activities': typeof AuthedAdminActivitiesRoute
|
'/admin/activities': typeof AuthedAdminActivitiesRoute
|
||||||
'/admin/badges': typeof AuthedAdminBadgesRoute
|
'/admin/badges': typeof AuthedAdminBadgesRoute
|
||||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||||
'/admin/telemetry': typeof AuthedAdminTelemetryRouteWithChildren
|
|
||||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
@@ -365,22 +303,14 @@ export interface FileRoutesByFullPath {
|
|||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
||||||
'/api/telemetry/beacon': typeof ApiTelemetryBeaconRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
||||||
'/admin/': typeof AuthedAdminIndexRoute
|
'/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
|
||||||
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
|
||||||
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
|
||||||
'/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
|
||||||
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||||
'/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
'/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
||||||
'/admin/telemetry/': typeof AuthedAdminTelemetryIndexRoute
|
|
||||||
'/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/admin/tournaments/$id/enrollment': typeof AuthedAdminTournamentsIdEnrollmentRoute
|
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/tournaments/$id/predictions/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
'/tournaments/$id/predictions/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
||||||
@@ -415,22 +345,14 @@ export interface FileRoutesByTo {
|
|||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
||||||
'/api/telemetry/beacon': typeof ApiTelemetryBeaconRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
||||||
'/admin': typeof AuthedAdminIndexRoute
|
'/admin': typeof AuthedAdminIndexRoute
|
||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
|
||||||
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
|
||||||
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
|
||||||
'/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
|
||||||
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||||
'/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
'/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
||||||
'/admin/telemetry': typeof AuthedAdminTelemetryIndexRoute
|
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/admin/tournaments/$id/enrollment': typeof AuthedAdminTournamentsIdEnrollmentRoute
|
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/tournaments/$id/predictions/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
'/tournaments/$id/predictions/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
||||||
@@ -453,7 +375,6 @@ export interface FileRoutesById {
|
|||||||
'/_authed/admin/activities': typeof AuthedAdminActivitiesRoute
|
'/_authed/admin/activities': typeof AuthedAdminActivitiesRoute
|
||||||
'/_authed/admin/badges': typeof AuthedAdminBadgesRoute
|
'/_authed/admin/badges': typeof AuthedAdminBadgesRoute
|
||||||
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
||||||
'/_authed/admin/telemetry': typeof AuthedAdminTelemetryRouteWithChildren
|
|
||||||
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||||
@@ -469,22 +390,14 @@ export interface FileRoutesById {
|
|||||||
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
'/api/spotify/search': typeof ApiSpotifySearchRoute
|
||||||
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
'/api/spotify/token': typeof ApiSpotifyTokenRoute
|
||||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoRoute
|
||||||
'/api/telemetry/beacon': typeof ApiTelemetryBeaconRoute
|
|
||||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoRoute
|
||||||
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
||||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/_authed/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
|
||||||
'/_authed/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
|
||||||
'/_authed/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
|
||||||
'/_authed/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
|
||||||
'/_authed/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
|
||||||
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/_authed/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
'/_authed/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||||
'/_authed/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
'/_authed/tournaments/$id/predictions': typeof AuthedTournamentsIdPredictionsRoute
|
||||||
'/_authed/admin/telemetry/': typeof AuthedAdminTelemetryIndexRoute
|
|
||||||
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
'/_authed/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
'/_authed/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/_authed/admin/tournaments/$id/enrollment': typeof AuthedAdminTournamentsIdEnrollmentRoute
|
|
||||||
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/_authed/tournaments/$id/predictions_/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
'/_authed/tournaments/$id/predictions_/$playerId': typeof AuthedTournamentsIdPredictionsPlayerIdRoute
|
||||||
@@ -507,7 +420,6 @@ export interface FileRouteTypes {
|
|||||||
| '/admin/activities'
|
| '/admin/activities'
|
||||||
| '/admin/badges'
|
| '/admin/badges'
|
||||||
| '/admin/preview'
|
| '/admin/preview'
|
||||||
| '/admin/telemetry'
|
|
||||||
| '/profile/$playerId'
|
| '/profile/$playerId'
|
||||||
| '/teams/$teamId'
|
| '/teams/$teamId'
|
||||||
| '/tournaments/$tournamentId'
|
| '/tournaments/$tournamentId'
|
||||||
@@ -523,22 +435,14 @@ export interface FileRouteTypes {
|
|||||||
| '/api/spotify/search'
|
| '/api/spotify/search'
|
||||||
| '/api/spotify/token'
|
| '/api/spotify/token'
|
||||||
| '/api/teams/upload-logo'
|
| '/api/teams/upload-logo'
|
||||||
| '/api/telemetry/beacon'
|
|
||||||
| '/api/tournaments/upload-logo'
|
| '/api/tournaments/upload-logo'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/tournaments/'
|
| '/tournaments/'
|
||||||
| '/admin/telemetry/errors'
|
|
||||||
| '/admin/telemetry/functions'
|
|
||||||
| '/admin/telemetry/players'
|
|
||||||
| '/admin/telemetry/sessions'
|
|
||||||
| '/admin/telemetry/usage'
|
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/tournaments/$id/groups'
|
| '/tournaments/$id/groups'
|
||||||
| '/tournaments/$id/predictions'
|
| '/tournaments/$id/predictions'
|
||||||
| '/admin/telemetry/'
|
|
||||||
| '/admin/tournaments/'
|
| '/admin/tournaments/'
|
||||||
| '/admin/tournaments/$id/assign-partners'
|
| '/admin/tournaments/$id/assign-partners'
|
||||||
| '/admin/tournaments/$id/enrollment'
|
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/tournaments/$id/predictions/$playerId'
|
| '/tournaments/$id/predictions/$playerId'
|
||||||
@@ -573,22 +477,14 @@ export interface FileRouteTypes {
|
|||||||
| '/api/spotify/search'
|
| '/api/spotify/search'
|
||||||
| '/api/spotify/token'
|
| '/api/spotify/token'
|
||||||
| '/api/teams/upload-logo'
|
| '/api/teams/upload-logo'
|
||||||
| '/api/telemetry/beacon'
|
|
||||||
| '/api/tournaments/upload-logo'
|
| '/api/tournaments/upload-logo'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/admin/telemetry/errors'
|
|
||||||
| '/admin/telemetry/functions'
|
|
||||||
| '/admin/telemetry/players'
|
|
||||||
| '/admin/telemetry/sessions'
|
|
||||||
| '/admin/telemetry/usage'
|
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/tournaments/$id/groups'
|
| '/tournaments/$id/groups'
|
||||||
| '/tournaments/$id/predictions'
|
| '/tournaments/$id/predictions'
|
||||||
| '/admin/telemetry'
|
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
| '/admin/tournaments/$id/assign-partners'
|
| '/admin/tournaments/$id/assign-partners'
|
||||||
| '/admin/tournaments/$id/enrollment'
|
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/tournaments/$id/predictions/$playerId'
|
| '/tournaments/$id/predictions/$playerId'
|
||||||
@@ -610,7 +506,6 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/admin/activities'
|
| '/_authed/admin/activities'
|
||||||
| '/_authed/admin/badges'
|
| '/_authed/admin/badges'
|
||||||
| '/_authed/admin/preview'
|
| '/_authed/admin/preview'
|
||||||
| '/_authed/admin/telemetry'
|
|
||||||
| '/_authed/profile/$playerId'
|
| '/_authed/profile/$playerId'
|
||||||
| '/_authed/teams/$teamId'
|
| '/_authed/teams/$teamId'
|
||||||
| '/_authed/tournaments/$tournamentId'
|
| '/_authed/tournaments/$tournamentId'
|
||||||
@@ -626,22 +521,14 @@ export interface FileRouteTypes {
|
|||||||
| '/api/spotify/search'
|
| '/api/spotify/search'
|
||||||
| '/api/spotify/token'
|
| '/api/spotify/token'
|
||||||
| '/api/teams/upload-logo'
|
| '/api/teams/upload-logo'
|
||||||
| '/api/telemetry/beacon'
|
|
||||||
| '/api/tournaments/upload-logo'
|
| '/api/tournaments/upload-logo'
|
||||||
| '/_authed/admin/'
|
| '/_authed/admin/'
|
||||||
| '/_authed/tournaments/'
|
| '/_authed/tournaments/'
|
||||||
| '/_authed/admin/telemetry/errors'
|
|
||||||
| '/_authed/admin/telemetry/functions'
|
|
||||||
| '/_authed/admin/telemetry/players'
|
|
||||||
| '/_authed/admin/telemetry/sessions'
|
|
||||||
| '/_authed/admin/telemetry/usage'
|
|
||||||
| '/_authed/tournaments/$id/bracket'
|
| '/_authed/tournaments/$id/bracket'
|
||||||
| '/_authed/tournaments/$id/groups'
|
| '/_authed/tournaments/$id/groups'
|
||||||
| '/_authed/tournaments/$id/predictions'
|
| '/_authed/tournaments/$id/predictions'
|
||||||
| '/_authed/admin/telemetry/'
|
|
||||||
| '/_authed/admin/tournaments/'
|
| '/_authed/admin/tournaments/'
|
||||||
| '/_authed/admin/tournaments/$id/assign-partners'
|
| '/_authed/admin/tournaments/$id/assign-partners'
|
||||||
| '/_authed/admin/tournaments/$id/enrollment'
|
|
||||||
| '/_authed/admin/tournaments/$id/teams'
|
| '/_authed/admin/tournaments/$id/teams'
|
||||||
| '/_authed/admin/tournaments/run/$id'
|
| '/_authed/admin/tournaments/run/$id'
|
||||||
| '/_authed/tournaments/$id/predictions_/$playerId'
|
| '/_authed/tournaments/$id/predictions_/$playerId'
|
||||||
@@ -668,7 +555,6 @@ export interface RootRouteChildren {
|
|||||||
ApiSpotifySearchRoute: typeof ApiSpotifySearchRoute
|
ApiSpotifySearchRoute: typeof ApiSpotifySearchRoute
|
||||||
ApiSpotifyTokenRoute: typeof ApiSpotifyTokenRoute
|
ApiSpotifyTokenRoute: typeof ApiSpotifyTokenRoute
|
||||||
ApiTeamsUploadLogoRoute: typeof ApiTeamsUploadLogoRoute
|
ApiTeamsUploadLogoRoute: typeof ApiTeamsUploadLogoRoute
|
||||||
ApiTelemetryBeaconRoute: typeof ApiTelemetryBeaconRoute
|
|
||||||
ApiTournamentsUploadLogoRoute: typeof ApiTournamentsUploadLogoRoute
|
ApiTournamentsUploadLogoRoute: typeof ApiTournamentsUploadLogoRoute
|
||||||
ApiFilesCollectionRecordIdFileRoute: typeof ApiFilesCollectionRecordIdFileRoute
|
ApiFilesCollectionRecordIdFileRoute: typeof ApiFilesCollectionRecordIdFileRoute
|
||||||
}
|
}
|
||||||
@@ -766,13 +652,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ApiTournamentsUploadLogoRouteImport
|
preLoaderRoute: typeof ApiTournamentsUploadLogoRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/api/telemetry/beacon': {
|
|
||||||
id: '/api/telemetry/beacon'
|
|
||||||
path: '/api/telemetry/beacon'
|
|
||||||
fullPath: '/api/telemetry/beacon'
|
|
||||||
preLoaderRoute: typeof ApiTelemetryBeaconRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/teams/upload-logo': {
|
'/api/teams/upload-logo': {
|
||||||
id: '/api/teams/upload-logo'
|
id: '/api/teams/upload-logo'
|
||||||
path: '/api/teams/upload-logo'
|
path: '/api/teams/upload-logo'
|
||||||
@@ -878,13 +757,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedProfilePlayerIdRouteImport
|
preLoaderRoute: typeof AuthedProfilePlayerIdRouteImport
|
||||||
parentRoute: typeof AuthedRoute
|
parentRoute: typeof AuthedRoute
|
||||||
}
|
}
|
||||||
'/_authed/admin/telemetry': {
|
|
||||||
id: '/_authed/admin/telemetry'
|
|
||||||
path: '/telemetry'
|
|
||||||
fullPath: '/admin/telemetry'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/preview': {
|
'/_authed/admin/preview': {
|
||||||
id: '/_authed/admin/preview'
|
id: '/_authed/admin/preview'
|
||||||
path: '/preview'
|
path: '/preview'
|
||||||
@@ -913,13 +785,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport
|
preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
'/_authed/admin/telemetry/': {
|
|
||||||
id: '/_authed/admin/telemetry/'
|
|
||||||
path: '/'
|
|
||||||
fullPath: '/admin/telemetry/'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryIndexRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/tournaments/$id/predictions': {
|
'/_authed/tournaments/$id/predictions': {
|
||||||
id: '/_authed/tournaments/$id/predictions'
|
id: '/_authed/tournaments/$id/predictions'
|
||||||
path: '/tournaments/$id/predictions'
|
path: '/tournaments/$id/predictions'
|
||||||
@@ -941,41 +806,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedTournamentsIdBracketRouteImport
|
preLoaderRoute: typeof AuthedTournamentsIdBracketRouteImport
|
||||||
parentRoute: typeof AuthedRoute
|
parentRoute: typeof AuthedRoute
|
||||||
}
|
}
|
||||||
'/_authed/admin/telemetry/usage': {
|
|
||||||
id: '/_authed/admin/telemetry/usage'
|
|
||||||
path: '/usage'
|
|
||||||
fullPath: '/admin/telemetry/usage'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryUsageRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/telemetry/sessions': {
|
|
||||||
id: '/_authed/admin/telemetry/sessions'
|
|
||||||
path: '/sessions'
|
|
||||||
fullPath: '/admin/telemetry/sessions'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetrySessionsRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/telemetry/players': {
|
|
||||||
id: '/_authed/admin/telemetry/players'
|
|
||||||
path: '/players'
|
|
||||||
fullPath: '/admin/telemetry/players'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryPlayersRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/telemetry/functions': {
|
|
||||||
id: '/_authed/admin/telemetry/functions'
|
|
||||||
path: '/functions'
|
|
||||||
fullPath: '/admin/telemetry/functions'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryFunctionsRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/telemetry/errors': {
|
|
||||||
id: '/_authed/admin/telemetry/errors'
|
|
||||||
path: '/errors'
|
|
||||||
fullPath: '/admin/telemetry/errors'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTelemetryErrorsRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/tournaments/$id/': {
|
'/_authed/admin/tournaments/$id/': {
|
||||||
id: '/_authed/admin/tournaments/$id/'
|
id: '/_authed/admin/tournaments/$id/'
|
||||||
path: '/tournaments/$id'
|
path: '/tournaments/$id'
|
||||||
@@ -1018,13 +848,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
|
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
'/_authed/admin/tournaments/$id/enrollment': {
|
|
||||||
id: '/_authed/admin/tournaments/$id/enrollment'
|
|
||||||
path: '/tournaments/$id/enrollment'
|
|
||||||
fullPath: '/admin/tournaments/$id/enrollment'
|
|
||||||
preLoaderRoute: typeof AuthedAdminTournamentsIdEnrollmentRouteImport
|
|
||||||
parentRoute: typeof AuthedAdminRoute
|
|
||||||
}
|
|
||||||
'/_authed/admin/tournaments/$id/assign-partners': {
|
'/_authed/admin/tournaments/$id/assign-partners': {
|
||||||
id: '/_authed/admin/tournaments/$id/assign-partners'
|
id: '/_authed/admin/tournaments/$id/assign-partners'
|
||||||
path: '/tournaments/$id/assign-partners'
|
path: '/tournaments/$id/assign-partners'
|
||||||
@@ -1035,36 +858,13 @@ declare module '@tanstack/react-router' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthedAdminTelemetryRouteChildren {
|
|
||||||
AuthedAdminTelemetryErrorsRoute: typeof AuthedAdminTelemetryErrorsRoute
|
|
||||||
AuthedAdminTelemetryFunctionsRoute: typeof AuthedAdminTelemetryFunctionsRoute
|
|
||||||
AuthedAdminTelemetryPlayersRoute: typeof AuthedAdminTelemetryPlayersRoute
|
|
||||||
AuthedAdminTelemetrySessionsRoute: typeof AuthedAdminTelemetrySessionsRoute
|
|
||||||
AuthedAdminTelemetryUsageRoute: typeof AuthedAdminTelemetryUsageRoute
|
|
||||||
AuthedAdminTelemetryIndexRoute: typeof AuthedAdminTelemetryIndexRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthedAdminTelemetryRouteChildren: AuthedAdminTelemetryRouteChildren = {
|
|
||||||
AuthedAdminTelemetryErrorsRoute: AuthedAdminTelemetryErrorsRoute,
|
|
||||||
AuthedAdminTelemetryFunctionsRoute: AuthedAdminTelemetryFunctionsRoute,
|
|
||||||
AuthedAdminTelemetryPlayersRoute: AuthedAdminTelemetryPlayersRoute,
|
|
||||||
AuthedAdminTelemetrySessionsRoute: AuthedAdminTelemetrySessionsRoute,
|
|
||||||
AuthedAdminTelemetryUsageRoute: AuthedAdminTelemetryUsageRoute,
|
|
||||||
AuthedAdminTelemetryIndexRoute: AuthedAdminTelemetryIndexRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthedAdminTelemetryRouteWithChildren =
|
|
||||||
AuthedAdminTelemetryRoute._addFileChildren(AuthedAdminTelemetryRouteChildren)
|
|
||||||
|
|
||||||
interface AuthedAdminRouteChildren {
|
interface AuthedAdminRouteChildren {
|
||||||
AuthedAdminActivitiesRoute: typeof AuthedAdminActivitiesRoute
|
AuthedAdminActivitiesRoute: typeof AuthedAdminActivitiesRoute
|
||||||
AuthedAdminBadgesRoute: typeof AuthedAdminBadgesRoute
|
AuthedAdminBadgesRoute: typeof AuthedAdminBadgesRoute
|
||||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||||
AuthedAdminTelemetryRoute: typeof AuthedAdminTelemetryRouteWithChildren
|
|
||||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||||
AuthedAdminTournamentsIdAssignPartnersRoute: typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
AuthedAdminTournamentsIdAssignPartnersRoute: typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
AuthedAdminTournamentsIdEnrollmentRoute: typeof AuthedAdminTournamentsIdEnrollmentRoute
|
|
||||||
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
|
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
||||||
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
|
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
|
||||||
@@ -1074,13 +874,10 @@ const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
|||||||
AuthedAdminActivitiesRoute: AuthedAdminActivitiesRoute,
|
AuthedAdminActivitiesRoute: AuthedAdminActivitiesRoute,
|
||||||
AuthedAdminBadgesRoute: AuthedAdminBadgesRoute,
|
AuthedAdminBadgesRoute: AuthedAdminBadgesRoute,
|
||||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||||
AuthedAdminTelemetryRoute: AuthedAdminTelemetryRouteWithChildren,
|
|
||||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||||
AuthedAdminTournamentsIdAssignPartnersRoute:
|
AuthedAdminTournamentsIdAssignPartnersRoute:
|
||||||
AuthedAdminTournamentsIdAssignPartnersRoute,
|
AuthedAdminTournamentsIdAssignPartnersRoute,
|
||||||
AuthedAdminTournamentsIdEnrollmentRoute:
|
|
||||||
AuthedAdminTournamentsIdEnrollmentRoute,
|
|
||||||
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
|
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
|
||||||
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
||||||
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,
|
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,
|
||||||
@@ -1147,7 +944,6 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ApiSpotifySearchRoute: ApiSpotifySearchRoute,
|
ApiSpotifySearchRoute: ApiSpotifySearchRoute,
|
||||||
ApiSpotifyTokenRoute: ApiSpotifyTokenRoute,
|
ApiSpotifyTokenRoute: ApiSpotifyTokenRoute,
|
||||||
ApiTeamsUploadLogoRoute: ApiTeamsUploadLogoRoute,
|
ApiTeamsUploadLogoRoute: ApiTeamsUploadLogoRoute,
|
||||||
ApiTelemetryBeaconRoute: ApiTelemetryBeaconRoute,
|
|
||||||
ApiTournamentsUploadLogoRoute: ApiTournamentsUploadLogoRoute,
|
ApiTournamentsUploadLogoRoute: ApiTournamentsUploadLogoRoute,
|
||||||
ApiFilesCollectionRecordIdFileRoute: ApiFilesCollectionRecordIdFileRoute,
|
ApiFilesCollectionRecordIdFileRoute: ApiFilesCollectionRecordIdFileRoute,
|
||||||
}
|
}
|
||||||
@@ -1156,11 +952,10 @@ export const routeTree = rootRouteImport
|
|||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
import type { getRouter } from './router.tsx'
|
import type { getRouter } from './router.tsx'
|
||||||
import type { startInstance } from './start.ts'
|
import type { createStart } from '@tanstack/react-start'
|
||||||
declare module '@tanstack/react-start' {
|
declare module '@tanstack/react-start' {
|
||||||
interface Register {
|
interface Register {
|
||||||
ssr: true
|
ssr: true
|
||||||
router: Awaited<ReturnType<typeof getRouter>>
|
router: Awaited<ReturnType<typeof getRouter>>
|
||||||
config: Awaited<ReturnType<typeof startInstance.getOptions>>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|||||||
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
||||||
import RoutePending from "../components/route-pending";
|
|
||||||
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
||||||
|
|
||||||
export function getRouter() {
|
export function getRouter() {
|
||||||
@@ -31,9 +30,6 @@ export function getRouter() {
|
|||||||
},
|
},
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
defaultPreloadStaleTime: 60_000,
|
defaultPreloadStaleTime: 60_000,
|
||||||
defaultPendingMs: 100,
|
|
||||||
defaultPendingMinMs: 0,
|
|
||||||
defaultPendingComponent: RoutePending,
|
|
||||||
defaultErrorComponent: DefaultCatchBoundary,
|
defaultErrorComponent: DefaultCatchBoundary,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import {
|
|||||||
Scripts,
|
Scripts,
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
isRedirect,
|
isRedirect,
|
||||||
useRouter,
|
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DefaultCatchBoundary } from "@/components/DefaultCatchBoundary";
|
import { DefaultCatchBoundary } from "@/components/DefaultCatchBoundary";
|
||||||
import { type QueryClient } from "@tanstack/react-query";
|
import { type QueryClient } from "@tanstack/react-query";
|
||||||
import { ensureSuperTokensFrontend } from "@/lib/supertokens/client";
|
import { ensureSuperTokensFrontend } from "@/lib/supertokens/client";
|
||||||
import { initTelemetry, track } from "@/lib/telemetry/client";
|
|
||||||
import { AuthContextType } from "@/contexts/auth-context";
|
import { AuthContextType } from "@/contexts/auth-context";
|
||||||
import Providers from "@/features/core/components/providers";
|
import Providers from "@/features/core/components/providers";
|
||||||
import { SessionMonitor } from "@/components/session-monitor";
|
import { SessionMonitor } from "@/components/session-monitor";
|
||||||
@@ -31,7 +29,6 @@ import { getRootMeta } from "@/lib/i18n/meta";
|
|||||||
import FullScreenLoader from "@/components/full-screen-loader";
|
import FullScreenLoader from "@/components/full-screen-loader";
|
||||||
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
||||||
import mantineCssUrl from '@mantine/core/styles.css?url'
|
import mantineCssUrl from '@mantine/core/styles.css?url'
|
||||||
import appStylesUrl from '../styles.css?url'
|
|
||||||
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
|
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
|
||||||
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
|
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
|
||||||
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
|
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
|
||||||
@@ -102,7 +99,6 @@ export const Route = createRootRouteWithContext<{
|
|||||||
},
|
},
|
||||||
{ rel: "manifest", href: "/site.webmanifest" },
|
{ rel: "manifest", href: "/site.webmanifest" },
|
||||||
{ rel: "icon", href: "/favicon.ico" },
|
{ rel: "icon", href: "/favicon.ico" },
|
||||||
{ rel: 'stylesheet', href: appStylesUrl },
|
|
||||||
{ rel: 'stylesheet', href: mantineCssUrl },
|
{ rel: 'stylesheet', href: mantineCssUrl },
|
||||||
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
||||||
{ rel: 'stylesheet', href: mantineDatesCssUrl },
|
{ rel: 'stylesheet', href: mantineDatesCssUrl },
|
||||||
@@ -179,27 +175,12 @@ export const Route = createRootRouteWithContext<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
ensureSuperTokensFrontend();
|
ensureSuperTokensFrontend();
|
||||||
initTelemetry(router);
|
|
||||||
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
||||||
// first install claims the page too; only reload on real updates
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||||
const hadController = !!navigator.serviceWorker.controller;
|
|
||||||
let reloading = false;
|
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
||||||
if (!hadController || reloading) return;
|
|
||||||
reloading = true;
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
navigator.serviceWorker.register('/sw.js').then((registration) => {
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') registration.update().catch(() => {});
|
|
||||||
});
|
|
||||||
}).catch(() => track('sw_register_failed'));
|
|
||||||
}
|
}
|
||||||
}, [router]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootDocument>
|
<RootDocument>
|
||||||
@@ -212,6 +193,7 @@ function RootComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: analytics -> process.env data-website-id
|
||||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
@@ -245,6 +227,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
'try{var d=document.documentElement,t=localStorage.getItem("flxn-theme");if(t&&t!=="default"){d.setAttribute("data-theme",t);}var s=localStorage.getItem("flxn-theme-scheme");if(s){d.setAttribute("data-mantine-color-scheme",s);}var f=JSON.parse(localStorage.getItem("flxn-theme-fonts")||"[]");for(var i=0;i<f.length;i++){var l=document.createElement("link");l.rel="stylesheet";l.href=f[i];l.setAttribute("data-flxn-theme-font",t||"default");document.head.appendChild(l);}}catch(e){}',
|
'try{var d=document.documentElement,t=localStorage.getItem("flxn-theme");if(t&&t!=="default"){d.setAttribute("data-theme",t);}var s=localStorage.getItem("flxn-theme-scheme");if(s){d.setAttribute("data-mantine-color-scheme",s);}var f=JSON.parse(localStorage.getItem("flxn-theme-fonts")||"[]");for(var i=0;i<f.length;i++){var l=document.createElement("link");l.rel="stylesheet";l.href=f[i];l.setAttribute("data-flxn-theme-font",t||"default");document.head.appendChild(l);}}catch(e){}',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
style={{
|
style={{
|
||||||
@@ -256,7 +239,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="app">{children}</div>
|
<div className="app">{children}</div>
|
||||||
<div className="flxn-theme-overlay" aria-hidden="true" />
|
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Outlet, redirect, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, redirect, createFileRoute } from "@tanstack/react-router";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import type { HeaderConfig } from "@/features/core/types/header-config";
|
import type { HeaderConfig } from "@/features/core/types/header-config";
|
||||||
import { isAdminRole } from "@/features/core/utils/roles";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin")({
|
export const Route = createFileRoute("/_authed/admin")({
|
||||||
component: Outlet,
|
component: Outlet,
|
||||||
beforeLoad: ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
if (!isAdminRole(context.auth?.roles)) {
|
if (!context.auth?.roles?.includes("Admin")) {
|
||||||
throw redirect({ to: "/" });
|
throw redirect({ to: "/" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,75 @@
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
|
import { ActivitiesTable, activityQueries } from "@/features/activities";
|
||||||
|
import { PlayersActivityTable, playerQueries } from "@/features/players";
|
||||||
|
import { Box, Divider, Group, Skeleton, Stack, Tabs } from "@mantine/core";
|
||||||
|
import { Suspense, useState } from "react";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
import { msg } from "@lingui/core/macro";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/activities")({
|
export const Route = createFileRoute("/_authed/admin/activities")({
|
||||||
beforeLoad: () => {
|
component: Stats,
|
||||||
throw redirect({ to: "/admin/telemetry/functions" });
|
beforeLoad: ({ context }) => {
|
||||||
|
const queryClient = context.queryClient;
|
||||||
|
prefetchServerQuery(queryClient, activityQueries.search());
|
||||||
|
prefetchServerQuery(queryClient, playerQueries.activity());
|
||||||
},
|
},
|
||||||
|
loader: () => ({
|
||||||
|
withPadding: false,
|
||||||
|
fullWidth: true,
|
||||||
|
header: {
|
||||||
|
title: msg`Activities`,
|
||||||
|
withBackButton: true,
|
||||||
|
},
|
||||||
|
refresh: [activityQueries.search().queryKey, playerQueries.activity().queryKey],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function ActivityRowsSkeleton({ withSearch = false }: { withSearch?: boolean }) {
|
||||||
|
return (
|
||||||
|
<Stack gap={0}>
|
||||||
|
{withSearch && (
|
||||||
|
<Box px="md" pb="sm">
|
||||||
|
<Skeleton height={42} radius="sm" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
|
<div key={`activity-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
|
||||||
|
<Group p="md" wrap="nowrap" w="100%" justify="space-between">
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Skeleton height={14} width={160} radius="sm" />
|
||||||
|
<Skeleton height={10} width={100} radius="sm" />
|
||||||
|
</Stack>
|
||||||
|
<Skeleton height={10} width={60} radius="sm" />
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stats() {
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>("server-functions");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.List mb='md'>
|
||||||
|
<Tabs.Tab value="server-functions"><Trans>Server Functions</Trans></Tabs.Tab>
|
||||||
|
<Tabs.Tab value="player-activity"><Trans>Player Activity</Trans></Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="server-functions">
|
||||||
|
<Suspense fallback={<ActivityRowsSkeleton withSearch />}>
|
||||||
|
<ActivitiesTable />
|
||||||
|
</Suspense>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="player-activity">
|
||||||
|
<Suspense fallback={<ActivityRowsSkeleton />}>
|
||||||
|
<PlayersActivityTable />
|
||||||
|
</Suspense>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { AdminPage } from "@/features/admin";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import { AdminDashboard } from "@/features/admin";
|
|
||||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/")({
|
export const Route = createFileRoute("/_authed/admin/")({
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
prefetchServerQuery(context.queryClient, tournamentQueries.current());
|
|
||||||
},
|
|
||||||
loader: () => ({
|
loader: () => ({
|
||||||
header: {
|
header: {
|
||||||
withBackButton: true,
|
withBackButton: true,
|
||||||
@@ -15,5 +10,5 @@ export const Route = createFileRoute("/_authed/admin/")({
|
|||||||
},
|
},
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
}),
|
}),
|
||||||
component: () => <AdminDashboard />,
|
component: () => <AdminPage />,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Outlet, createFileRoute, redirect, useNavigate, useRouterState } from "@tanstack/react-router";
|
|
||||||
import { Tabs } from "@mantine/core";
|
|
||||||
import { Trans } from "@lingui/react/macro";
|
|
||||||
import { msg } from "@lingui/core/macro";
|
|
||||||
import { isDeveloperRole } from "@/features/core/utils/roles";
|
|
||||||
|
|
||||||
const TAB_ROUTES = {
|
|
||||||
usage: "/admin/telemetry/usage",
|
|
||||||
players: "/admin/telemetry/players",
|
|
||||||
functions: "/admin/telemetry/functions",
|
|
||||||
sessions: "/admin/telemetry/sessions",
|
|
||||||
errors: "/admin/telemetry/errors",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type TabValue = keyof typeof TAB_ROUTES;
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry")({
|
|
||||||
component: TelemetryLayout,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
if (!isDeveloperRole(context.auth?.roles)) {
|
|
||||||
throw redirect({ to: "/admin" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
header: {
|
|
||||||
title: msg`Telemetry`,
|
|
||||||
withBackButton: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function TelemetryLayout() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
||||||
const activeTab =
|
|
||||||
(Object.keys(TAB_ROUTES) as TabValue[]).find((tab) =>
|
|
||||||
pathname.endsWith(`/${tab}`)
|
|
||||||
) ?? "usage";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (value && value in TAB_ROUTES) {
|
|
||||||
navigate({ to: TAB_ROUTES[value as TabValue] });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.List mb="md" style={{ flexWrap: "nowrap", overflowX: "auto", overflowY: "hidden" }}>
|
|
||||||
<Tabs.Tab value="usage">
|
|
||||||
<Trans>Usage</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="players">
|
|
||||||
<Trans>Players</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="functions">
|
|
||||||
<Trans>Functions</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="sessions">
|
|
||||||
<Trans>Sessions</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="errors">
|
|
||||||
<Trans>Errors</Trans>
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
|
|
||||||
<Outlet />
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import { telemetryQueries, lastNDays, ErrorBrowser } from "@/features/telemetry";
|
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
|
||||||
|
|
||||||
const errorGroupParams = () => ({ ...lastNDays(30), includeResolved: false });
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/errors")({
|
|
||||||
component: ErrorsTab,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
prefetchServerQuery(context.queryClient, telemetryQueries.errorGroups(errorGroupParams()));
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
withPadding: false,
|
|
||||||
fullWidth: true,
|
|
||||||
refresh: [telemetryQueries.errorGroups(errorGroupParams()).queryKey],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function ErrorsTab() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<ListSkeleton rows={6} withSubtitle />}>
|
|
||||||
<ErrorBrowser />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import { ActivitiesTable, activityQueries } from "@/features/activities";
|
|
||||||
import { playerQueries } from "@/features/players";
|
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/functions")({
|
|
||||||
component: FunctionsTab,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
const queryClient = context.queryClient;
|
|
||||||
prefetchServerQuery(queryClient, activityQueries.search());
|
|
||||||
prefetchServerQuery(queryClient, playerQueries.list());
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
withPadding: false,
|
|
||||||
fullWidth: true,
|
|
||||||
refresh: [activityQueries.search().queryKey],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function FunctionsTab() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<ListSkeleton rows={8} withSearch withSubtitle withTrailing="text" />}>
|
|
||||||
<ActivitiesTable />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/")({
|
|
||||||
beforeLoad: () => {
|
|
||||||
throw redirect({ to: "/admin/telemetry/usage" });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import { PlayersActivityTable, playerQueries } from "@/features/players";
|
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/players")({
|
|
||||||
component: PlayersTab,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
prefetchServerQuery(context.queryClient, playerQueries.activity());
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
withPadding: false,
|
|
||||||
fullWidth: true,
|
|
||||||
refresh: [playerQueries.activity().queryKey],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function PlayersTab() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<ListSkeleton rows={8} withSubtitle withTrailing="text" />}>
|
|
||||||
<PlayersActivityTable />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import { telemetryQueries, SessionsList, defaultSessionParams } from "@/features/telemetry";
|
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/sessions")({
|
|
||||||
component: SessionsTab,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
prefetchServerQuery(context.queryClient, telemetryQueries.sessions(defaultSessionParams));
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
withPadding: false,
|
|
||||||
fullWidth: true,
|
|
||||||
refresh: [telemetryQueries.sessions(defaultSessionParams).queryKey],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function SessionsTab() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<ListSkeleton rows={8} withSubtitle withTrailing="text" />}>
|
|
||||||
<SessionsList />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import { telemetryQueries, lastNDays, USAGE_METRICS, UsageScreen } from "@/features/telemetry";
|
|
||||||
|
|
||||||
const defaultUsageParams = () => ({ metrics: USAGE_METRICS, ...lastNDays(7) });
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/telemetry/usage")({
|
|
||||||
component: UsageTab,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
prefetchServerQuery(context.queryClient, telemetryQueries.rollups(defaultUsageParams()));
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
withPadding: false,
|
|
||||||
fullWidth: true,
|
|
||||||
refresh: [telemetryQueries.rollups(defaultUsageParams()).queryKey],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function UsageTab() {
|
|
||||||
return <UsageScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||||
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
|
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { StatTileSkeleton } from "@/components/skeletons";
|
|
||||||
import { Stack, Text, Button, Alert, LoadingOverlay, Group, Skeleton } from "@mantine/core";
|
import { Stack, Text, Button, Alert, LoadingOverlay, Group, Skeleton } from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
|
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
|
||||||
@@ -38,7 +37,10 @@ function AssignPartnersPending() {
|
|||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<StatTileSkeleton align="row" />
|
<Group gap="xs" align="baseline">
|
||||||
|
<Skeleton height={28} width={36} radius="sm" />
|
||||||
|
<Skeleton height={14} width={110} radius="sm" />
|
||||||
|
</Group>
|
||||||
<Skeleton height={36} w="100%" radius="sm" style={{ opacity: 0.7 }} />
|
<Skeleton height={36} w="100%" radius="sm" style={{ opacity: 0.7 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
||||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
|
||||||
import Enrollment from "@/features/tournaments/components/enrollment";
|
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
|
||||||
import { ListSkeleton, StatTileSkeleton } from "@/components/skeletons";
|
|
||||||
import { Skeleton, Stack } from "@mantine/core";
|
|
||||||
import { msg } from "@lingui/core/macro";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/enrollment")({
|
|
||||||
beforeLoad: async ({ context, params }) => {
|
|
||||||
const { queryClient } = context;
|
|
||||||
const tournament = await ensureServerQueryData(
|
|
||||||
queryClient,
|
|
||||||
tournamentQueries.details(params.id)
|
|
||||||
);
|
|
||||||
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
|
||||||
if (tournament.regional) {
|
|
||||||
await ensureServerQueryData(
|
|
||||||
queryClient,
|
|
||||||
tournamentQueries.free_agents(params.id)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await ensureServerQueryData(
|
|
||||||
queryClient,
|
|
||||||
tournamentQueries.unenrolled(params.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { tournament };
|
|
||||||
},
|
|
||||||
loader: () => ({
|
|
||||||
header: {
|
|
||||||
withBackButton: true,
|
|
||||||
title: msg`Enrollment`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
component: RouteComponent,
|
|
||||||
pendingComponent: EnrollmentPending,
|
|
||||||
});
|
|
||||||
|
|
||||||
function EnrollmentPending() {
|
|
||||||
return (
|
|
||||||
<Stack gap="lg">
|
|
||||||
<StatTileSkeleton align="row" />
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Skeleton height={14} width={80} radius="sm" />
|
|
||||||
<Skeleton height={42} radius="sm" />
|
|
||||||
</Stack>
|
|
||||||
<ListSkeleton rows={6} withAvatar avatarSize={32} withTrailing padding="xs" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const { tournament } = Route.useRouteContext();
|
|
||||||
return <Enrollment tournament={tournament} />;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
|||||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||||
import ManageTournament from "@/features/tournaments/components/manage-tournament";
|
import ManageTournament from "@/features/tournaments/components/manage-tournament";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
||||||
@@ -31,14 +31,18 @@ export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
|||||||
|
|
||||||
function ManageTournamentPending() {
|
function ManageTournamentPending() {
|
||||||
return (
|
return (
|
||||||
<ListSkeleton
|
<Stack gap={0}>
|
||||||
rows={5}
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
withAvatar
|
<div key={`manage-tournament-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.14, 0.4) }}>
|
||||||
avatarSize={20}
|
<Group p="md" wrap="nowrap" w="100%">
|
||||||
withTrailing
|
<Skeleton height={20} width={20} radius="sm" />
|
||||||
fadeStep={0.14}
|
<Skeleton height={16} width="45%" radius="sm" />
|
||||||
fadeFloor={0.4}
|
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
||||||
/>
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
|||||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||||
import ManageTeams from "@/features/teams/components/manage-teams";
|
import ManageTeams from "@/features/teams/components/manage-teams";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
import { Box, Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||||
import { Box, Skeleton, Stack } from "@mantine/core";
|
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
|
export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
|
||||||
@@ -39,7 +38,21 @@ function ManageTeamsPending() {
|
|||||||
<Box px="md">
|
<Box px="md">
|
||||||
<Skeleton height={12} width={90} radius="sm" />
|
<Skeleton height={12} width={90} radius="sm" />
|
||||||
</Box>
|
</Box>
|
||||||
<ListSkeleton rows={8} withAvatar avatarSize={40} withTrailing="double" padding="xs" />
|
<Stack gap={0}>
|
||||||
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
|
<div key={`manage-teams-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
|
||||||
|
<Group p="xs" wrap="nowrap" w="100%">
|
||||||
|
<Skeleton height={40} width={40} radius="sm" />
|
||||||
|
<Skeleton height={16} width="45%" radius="sm" />
|
||||||
|
<Stack ml="auto" gap={6} align="flex-end">
|
||||||
|
<Skeleton height={10} width={90} radius="sm" />
|
||||||
|
<Skeleton height={10} width={70} radius="sm" />
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import ManageTournaments from "@/features/admin/components/manage-tournaments";
|
import ManageTournaments from "@/features/admin/components/manage-tournaments";
|
||||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
import { ListSkeleton } from "@/components/skeletons";
|
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
@@ -22,13 +22,25 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({
|
|||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function TournamentListSkeleton() {
|
||||||
|
return (
|
||||||
|
<Stack gap={0}>
|
||||||
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<div key={`manage-tournaments-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}>
|
||||||
|
<Group p="md" wrap="nowrap" w="100%">
|
||||||
|
<Skeleton height={16} width="55%" radius="sm" />
|
||||||
|
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense fallback={<TournamentListSkeleton />}>
|
||||||
fallback={
|
|
||||||
<ListSkeleton rows={6} titleWidth="55%" withTrailing fadeStep={0.12} fadeFloor={0.4} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ManageTournaments />
|
<ManageTournaments />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
|||||||
import SetupGroupStage from "@/features/tournaments/components/setup-group-stage";
|
import SetupGroupStage from "@/features/tournaments/components/setup-group-stage";
|
||||||
import GroupStageView from "@/features/tournaments/components/group-stage-view";
|
import GroupStageView from "@/features/tournaments/components/group-stage-view";
|
||||||
import { Container, Stack, Divider, Title, Box, Card, Group, Skeleton, SimpleGrid } from "@mantine/core";
|
import { Container, Stack, Divider, Title, Box, Card, Group, Skeleton, SimpleGrid } from "@mantine/core";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { BracketData } from "@/features/bracket/types";
|
||||||
|
import { Match } from "@/features/matches/types";
|
||||||
import BracketView from "@/features/bracket/components/bracket-view";
|
import BracketView from "@/features/bracket/components/bracket-view";
|
||||||
import { SpotifyControlsBar } from "@/features/spotify/components";
|
import { SpotifyControlsBar } from "@/features/spotify/components";
|
||||||
import { useIsAdmin } from "@/features/core/hooks/use-is-admin";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useTournamentRunState } from "@/features/tournaments/hooks/use-tournament-run-state";
|
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
|
|
||||||
@@ -81,10 +83,67 @@ function RunTournamentPending() {
|
|||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const { data: tournament } = useTournament(id);
|
const { data: tournament } = useTournament(id);
|
||||||
const isAdmin = useIsAdmin();
|
const { roles } = useAuth();
|
||||||
|
const isAdmin = roles?.includes('Admin') || false;
|
||||||
|
|
||||||
const { hasGroupStage, hasKnockout, knockoutBracketPopulated, nextUpMatchId, bracket } =
|
const hasGroupStage = useMemo(() => {
|
||||||
useTournamentRunState(tournament.matches);
|
return tournament.matches?.some((match) => match.round === -1) || false;
|
||||||
|
}, [tournament.matches]);
|
||||||
|
|
||||||
|
const hasKnockout = useMemo(() => {
|
||||||
|
return tournament.matches?.some((match) => match.round !== -1) || false;
|
||||||
|
}, [tournament.matches]);
|
||||||
|
|
||||||
|
const knockoutBracketPopulated = useMemo(() => {
|
||||||
|
return tournament.matches?.some((match) =>
|
||||||
|
match.round === 0 && match.lid >= 0 && (match.home || match.away)
|
||||||
|
) || false;
|
||||||
|
}, [tournament.matches]);
|
||||||
|
|
||||||
|
const nextUpMatchId = useMemo(() => {
|
||||||
|
const ready = (tournament.matches ?? [])
|
||||||
|
.filter(
|
||||||
|
(match) =>
|
||||||
|
match.status === "ready" && match.home && match.away && !match.bye
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.order - b.order);
|
||||||
|
return ready[0]?.id;
|
||||||
|
}, [tournament.matches]);
|
||||||
|
|
||||||
|
const bracket: BracketData = useMemo(() => {
|
||||||
|
if (!tournament.matches || tournament.matches.length === 0) {
|
||||||
|
return { winners: [], losers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const winnersMap = new Map<number, Match[]>();
|
||||||
|
const losersMap = new Map<number, Match[]>();
|
||||||
|
|
||||||
|
tournament.matches
|
||||||
|
.filter((match) => match.round !== -1)
|
||||||
|
.sort((a, b) => a.lid - b.lid)
|
||||||
|
.forEach((match) => {
|
||||||
|
if (!match.is_losers_bracket) {
|
||||||
|
if (!winnersMap.has(match.round)) {
|
||||||
|
winnersMap.set(match.round, []);
|
||||||
|
}
|
||||||
|
winnersMap.get(match.round)!.push(match);
|
||||||
|
} else {
|
||||||
|
if (!losersMap.has(match.round)) {
|
||||||
|
losersMap.set(match.round, []);
|
||||||
|
}
|
||||||
|
losersMap.get(match.round)!.push(match);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const winners = Array.from(winnersMap.entries())
|
||||||
|
.sort(([a], [b]) => a - b)
|
||||||
|
.map(([, matches]) => matches);
|
||||||
|
|
||||||
|
const losers = Array.from(losersMap.entries())
|
||||||
|
.sort(([a], [b]) => a - b)
|
||||||
|
.map(([, matches]) => matches);
|
||||||
|
return { winners, losers };
|
||||||
|
}, [tournament.matches]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="md" px={0}>
|
<Container size="md" px={0}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
||||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||||
import { getServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
|
||||||
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
||||||
@@ -11,13 +10,16 @@ export const Route = createFileRoute("/_authed/")({
|
|||||||
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
||||||
<Home />
|
<Home />
|
||||||
</Suspense>,
|
</Suspense>,
|
||||||
beforeLoad: ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
prefetchServerQuery(context.queryClient, tournamentQueries.current());
|
const queryClient = context.queryClient;
|
||||||
|
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
|
||||||
|
|
||||||
|
return { tournament }
|
||||||
},
|
},
|
||||||
loader: ({ context }) => ({
|
loader: ({ context }) => ({
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
header: {
|
header: {
|
||||||
title: getServerQueryData(context.queryClient, tournamentQueries.current())?.name || "FLXN"
|
title: context.tournament.name || "FLXN"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
pendingComponent: () => <UpcomingTournamentSkeleton />
|
pendingComponent: () => <UpcomingTournamentSkeleton />
|
||||||
|
|||||||
@@ -14,12 +14,26 @@ const searchSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
beforeLoad: ({ params, context }) => {
|
beforeLoad: async ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
prefetchServerQuery(queryClient, playerQueries.details(params.playerId));
|
await Promise.all([
|
||||||
prefetchServerQuery(queryClient, playerQueries.matches(params.playerId));
|
prefetchServerQuery(
|
||||||
prefetchServerQuery(queryClient, playerQueries.stats(params.playerId));
|
queryClient,
|
||||||
prefetchServerQuery(queryClient, badgeQueries.playerBadges(params.playerId));
|
playerQueries.details(params.playerId)
|
||||||
|
),
|
||||||
|
prefetchServerQuery(
|
||||||
|
queryClient,
|
||||||
|
playerQueries.matches(params.playerId)
|
||||||
|
),
|
||||||
|
prefetchServerQuery(
|
||||||
|
queryClient,
|
||||||
|
playerQueries.stats(params.playerId)
|
||||||
|
),
|
||||||
|
prefetchServerQuery(
|
||||||
|
queryClient,
|
||||||
|
badgeQueries.playerBadges(params.playerId)
|
||||||
|
),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
loader: ({ params, context }) => ({
|
loader: ({ params, context }) => ({
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { playerQueries } from "@/features/players/queries";
|
import { playerQueries } from "@/features/players/queries";
|
||||||
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
import PlayerStatsTable from "@/features/players/components/player-stats-table";
|
||||||
import { Suspense, useState, useDeferredValue, useEffect, useRef } from "react";
|
import { Suspense, useState, useDeferredValue, useEffect } from "react";
|
||||||
import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
||||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
import LeagueHeadToHead from "@/features/players/components/league-head-to-head";
|
import LeagueHeadToHead from "@/features/players/components/league-head-to-head";
|
||||||
@@ -9,7 +9,6 @@ import { Box, Loader, Tabs, Button, Group, Container, Stack } from "@mantine/cor
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authed/stats")({
|
export const Route = createFileRoute("/_authed/stats")({
|
||||||
component: Stats,
|
component: Stats,
|
||||||
@@ -33,14 +32,6 @@ function Stats() {
|
|||||||
const isStale = viewType !== deferredViewType;
|
const isStale = viewType !== deferredViewType;
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const viewTypeRef = useRef(viewType);
|
|
||||||
const changeViewType = (next: typeof viewType) => {
|
|
||||||
if (viewTypeRef.current === next) return;
|
|
||||||
viewTypeRef.current = next;
|
|
||||||
track("stats_filtered", { value: next });
|
|
||||||
setViewType(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = window.setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
prefetchServerQuery(queryClient, playerQueries.allStats('mainline'));
|
prefetchServerQuery(queryClient, playerQueries.allStats('mainline'));
|
||||||
@@ -63,21 +54,21 @@ function Stats() {
|
|||||||
<Button
|
<Button
|
||||||
variant={viewType === 'all' ? 'filled' : 'light'}
|
variant={viewType === 'all' ? 'filled' : 'light'}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
onClick={() => changeViewType('all')}
|
onClick={() => setViewType('all')}
|
||||||
>
|
>
|
||||||
<Trans>All</Trans>
|
<Trans>All</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewType === 'mainline' ? 'filled' : 'light'}
|
variant={viewType === 'mainline' ? 'filled' : 'light'}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
onClick={() => changeViewType('mainline')}
|
onClick={() => setViewType('mainline')}
|
||||||
>
|
>
|
||||||
<Trans>Mainline</Trans>
|
<Trans>Mainline</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewType === 'regional' ? 'filled' : 'light'}
|
variant={viewType === 'regional' ? 'filled' : 'light'}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
onClick={() => changeViewType('regional')}
|
onClick={() => setViewType('regional')}
|
||||||
>
|
>
|
||||||
<Trans>Regional</Trans>
|
<Trans>Regional</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ const searchSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
beforeLoad: ({ params, context }) => {
|
beforeLoad: async ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
await prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
||||||
},
|
},
|
||||||
loader: ({ params }) => ({
|
loader: ({ params }) => ({
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { tournamentQueries } from '@/features/tournaments/queries';
|
import { tournamentQueries } from '@/features/tournaments/queries';
|
||||||
import Profile from '@/features/tournaments/components/profile';
|
import Profile from '@/features/tournaments/components/profile';
|
||||||
import { isAdminRole } from '@/features/core/utils/roles';
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch';
|
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
@@ -21,7 +20,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
|||||||
header: {
|
header: {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
withBackButton: true,
|
withBackButton: true,
|
||||||
settingsLink: isAdminRole(context.auth.roles) ? `/admin/tournaments/${params.tournamentId}` : undefined
|
settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined
|
||||||
},
|
},
|
||||||
refresh: [tournamentQueries.details(params.tournamentId).queryKey],
|
refresh: [tournamentQueries.details(params.tournamentId).queryKey],
|
||||||
withPadding: false
|
withPadding: false
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ ensureSuperTokensBackend();
|
|||||||
// forwards all supertokens api methods to our API
|
// forwards all supertokens api methods to our API
|
||||||
const superTokensHandler = handleAuthAPIRequest();
|
const superTokensHandler = handleAuthAPIRequest();
|
||||||
const handleRequest = async ({ request }: {request: Request}) => {
|
const handleRequest = async ({ request }: {request: Request}) => {
|
||||||
|
console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
|
||||||
|
console.log("Handling auth request:", request.method, request.url);
|
||||||
return superTokensHandler(request);
|
return superTokensHandler(request);
|
||||||
};
|
};
|
||||||
export const Route = createFileRoute('/api/auth/$')({
|
export const Route = createFileRoute('/api/auth/$')({
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import { serverEvents, EVENT_TYPES, type ServerEvent } from "@/lib/events/emitter";
|
||||||
serverEvents,
|
|
||||||
EVENT_TYPES,
|
|
||||||
incrementConnections,
|
|
||||||
decrementConnections,
|
|
||||||
type ServerEvent,
|
|
||||||
} from "@/lib/events/emitter";
|
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
|
|
||||||
|
let activeConnections = 0;
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/events/$")({
|
export const Route = createFileRoute("/api/events/$")({
|
||||||
@@ -16,9 +11,9 @@ export const Route = createFileRoute("/api/events/$")({
|
|||||||
middleware: [superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
GET: ({ request }) => {
|
GET: ({ request }) => {
|
||||||
const active = incrementConnections();
|
activeConnections++;
|
||||||
const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||||
logger.info(`ServerEvents | New connection ${connectionId}. Active: ${active}`);
|
logger.info(`ServerEvents | New connection ${connectionId}. Active: ${activeConnections}`);
|
||||||
|
|
||||||
let cleanedUp = false;
|
let cleanedUp = false;
|
||||||
let cleanup = () => {};
|
let cleanup = () => {};
|
||||||
@@ -39,20 +34,19 @@ export const Route = createFileRoute("/api/events/$")({
|
|||||||
serverEvents.on(type, handleEvent);
|
serverEvents.on(type, handleEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Under the shortest idle timeout observed on proxies/PWA paths.
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
send({ type: "ping", timestamp: Date.now() });
|
send({ type: "ping", timestamp: Date.now() });
|
||||||
}, 8000);
|
}, 15000);
|
||||||
|
|
||||||
cleanup = () => {
|
cleanup = () => {
|
||||||
if (cleanedUp) return;
|
if (cleanedUp) return;
|
||||||
cleanedUp = true;
|
cleanedUp = true;
|
||||||
const remaining = decrementConnections();
|
activeConnections--;
|
||||||
for (const type of EVENT_TYPES) {
|
for (const type of EVENT_TYPES) {
|
||||||
serverEvents.off(type, handleEvent);
|
serverEvents.off(type, handleEvent);
|
||||||
}
|
}
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
logger.info(`ServerEvents | Connection ${connectionId} cleanup completed. Active: ${remaining}`);
|
logger.info(`ServerEvents | Connection ${connectionId} cleanup completed. Active: ${activeConnections}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.signal?.addEventListener("abort", cleanup);
|
request.signal?.addEventListener("abort", cleanup);
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { ensureTelemetryScheduler } from "@/lib/telemetry/scheduler.server";
|
|
||||||
import { getHealthSnapshot } from "@/lib/telemetry/health.server";
|
|
||||||
import { ensureSuperTokensBackend } from "@/lib/supertokens/server";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/health")({
|
export const Route = createFileRoute("/api/health")({
|
||||||
server: {
|
server: {
|
||||||
handlers: {
|
handlers: {
|
||||||
GET: async () => {
|
GET: () => {
|
||||||
ensureSuperTokensBackend();
|
|
||||||
ensureTelemetryScheduler();
|
|
||||||
const snapshot = await getHealthSnapshot();
|
|
||||||
|
|
||||||
// Always 200: a dependency outage must not fail the k8s probes.
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
status: snapshot.status,
|
status: "ok",
|
||||||
checks: snapshot.checks,
|
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ const json = (body: unknown, status = 200) =>
|
|||||||
|
|
||||||
export const Route = createFileRoute("/api/push/subscribe")({
|
export const Route = createFileRoute("/api/push/subscribe")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [apiLoggingMiddleware, superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
POST: async ({ request, context }) => {
|
POST: async ({ request, context }) => {
|
||||||
const player = (context as any).player;
|
const player = (context as any).player;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
|
||||||
import { sendPushToPlayer } from "@/lib/push";
|
import { sendPushToPlayer } from "@/lib/push";
|
||||||
import { isPushConfigured } from "@/lib/config";
|
import { isPushConfigured } from "@/lib/config";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
@@ -14,7 +13,7 @@ const json = (body: unknown, status = 200) =>
|
|||||||
|
|
||||||
export const Route = createFileRoute("/api/push/test")({
|
export const Route = createFileRoute("/api/push/test")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [apiLoggingMiddleware, superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
POST: async ({ context }) => {
|
POST: async ({ context }) => {
|
||||||
const player = (context as any).player;
|
const player = (context as any).player;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ const json = (body: unknown, status = 200) =>
|
|||||||
|
|
||||||
export const Route = createFileRoute("/api/push/unsubscribe")({
|
export const Route = createFileRoute("/api/push/unsubscribe")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [apiLoggingMiddleware, superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
POST: async ({ request, context }) => {
|
POST: async ({ request, context }) => {
|
||||||
const player = (context as any).player;
|
const player = (context as any).player;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
import { isAdminRole } from "@/features/core/utils/roles";
|
|
||||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { localizedFor } from "@/lib/i18n/server-messages";
|
import { localizedFor } from "@/lib/i18n/server-messages";
|
||||||
@@ -14,13 +12,13 @@ const uploadSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/api/teams/upload-logo")({
|
export const Route = createFileRoute("/api/teams/upload-logo")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [apiLoggingMiddleware, superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
POST: async ({ request, context }) => {
|
POST: async ({ request, context }) => {
|
||||||
const i18n = localizedFor(context.metadata);
|
const i18n = localizedFor(context.metadata);
|
||||||
try {
|
try {
|
||||||
const userId = context.userAuthId;
|
const userId = context.userAuthId;
|
||||||
const isAdmin = isAdminRole(context.roles);
|
const isAdmin = context.roles.includes("Admin");
|
||||||
|
|
||||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { superTokensOptionalRequestMiddleware } from "@/utils/supertokens";
|
|
||||||
import { beaconPayloadSchema, MAX_BEACON_BYTES, type BeaconEvent } from "@/lib/telemetry/beacon-schema";
|
|
||||||
import { errorGroupHash } from "@/lib/telemetry/error-hash";
|
|
||||||
import { redactArguments } from "@/lib/telemetry/redact-args";
|
|
||||||
import { Logger } from "@/lib/logger";
|
|
||||||
import type { Player } from "@/features/players/types";
|
|
||||||
|
|
||||||
const logger = new Logger("Telemetry");
|
|
||||||
|
|
||||||
const BUCKET_CAPACITY = 120;
|
|
||||||
const REFILL_PER_MS = BUCKET_CAPACITY / 60_000;
|
|
||||||
const buckets = new Map<string, { tokens: number; updatedAt: number }>();
|
|
||||||
|
|
||||||
const takeTokens = (key: string, count: number): boolean => {
|
|
||||||
const now = Date.now();
|
|
||||||
const bucket = buckets.get(key) ?? { tokens: BUCKET_CAPACITY, updatedAt: now };
|
|
||||||
bucket.tokens = Math.min(BUCKET_CAPACITY, bucket.tokens + (now - bucket.updatedAt) * REFILL_PER_MS);
|
|
||||||
bucket.updatedAt = now;
|
|
||||||
if (bucket.tokens < count) {
|
|
||||||
buckets.set(key, bucket);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bucket.tokens -= count;
|
|
||||||
buckets.set(key, bucket);
|
|
||||||
|
|
||||||
if (buckets.size > 2000) {
|
|
||||||
for (const [k, v] of buckets) {
|
|
||||||
if (now - v.updatedAt > 5 * 60_000) buckets.delete(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientIp = (request: Request): string =>
|
|
||||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
||||||
request.headers.get("x-real-ip") ||
|
|
||||||
"unknown";
|
|
||||||
|
|
||||||
const writeEvents = async (
|
|
||||||
events: BeaconEvent[],
|
|
||||||
base: { sessionId: string; playerId?: string; userAgent?: string }
|
|
||||||
) => {
|
|
||||||
const { isExcludedPlayerId } = await import("@/lib/telemetry/exclusions.server");
|
|
||||||
if (base.playerId && (await isExcludedPlayerId(base.playerId))) {
|
|
||||||
// Excluded players still report errors; their usage stays out.
|
|
||||||
events = events.filter((event) => event.kind === "error");
|
|
||||||
if (events.length === 0) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pbAdmin } = await import("@/lib/pocketbase/client");
|
|
||||||
await pbAdmin.authPromise;
|
|
||||||
|
|
||||||
for (const event of events) {
|
|
||||||
try {
|
|
||||||
if (event.kind === "error") {
|
|
||||||
await pbAdmin.createClientError({
|
|
||||||
message: event.message,
|
|
||||||
stack: event.stack,
|
|
||||||
source: event.source,
|
|
||||||
path: event.path,
|
|
||||||
route_id: event.routeId,
|
|
||||||
group_hash: errorGroupHash(event),
|
|
||||||
resolved: false,
|
|
||||||
player: base.playerId,
|
|
||||||
session_id: base.sessionId,
|
|
||||||
user_agent: base.userAgent,
|
|
||||||
props: redactArguments(event.props),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await pbAdmin.createClientEvent({
|
|
||||||
name: event.kind === "vital" ? `vital.${event.metric}` : event.name,
|
|
||||||
player: base.playerId,
|
|
||||||
session_id: base.sessionId,
|
|
||||||
path: event.path,
|
|
||||||
route_id: event.routeId,
|
|
||||||
value: event.value,
|
|
||||||
props:
|
|
||||||
event.kind === "vital"
|
|
||||||
? event.rating
|
|
||||||
? { rating: event.rating }
|
|
||||||
: undefined
|
|
||||||
: redactArguments(event.props),
|
|
||||||
user_agent: base.userAgent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to persist beacon event", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/telemetry/beacon")({
|
|
||||||
server: {
|
|
||||||
middleware: [superTokensOptionalRequestMiddleware],
|
|
||||||
handlers: {
|
|
||||||
POST: async ({ request, context }) => {
|
|
||||||
const player = (context as unknown as { player?: Player | null }).player ?? undefined;
|
|
||||||
|
|
||||||
const raw = await request.text();
|
|
||||||
if (raw.length > MAX_BEACON_BYTES) {
|
|
||||||
return new Response(null, { status: 413 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return new Response(null, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = beaconPayloadSchema.safeParse(parsed);
|
|
||||||
if (!result.success) {
|
|
||||||
return new Response(null, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = player ? `p:${player.id}` : `ip:${clientIp(request)}`;
|
|
||||||
if (!takeTokens(key, result.data.events.length)) {
|
|
||||||
return new Response(null, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeEvents(result.data.events, {
|
|
||||||
sessionId: result.data.sessionId,
|
|
||||||
playerId: player?.id,
|
|
||||||
userAgent: request.headers.get("user-agent") ?? undefined,
|
|
||||||
}).catch((error) => logger.error("Beacon write batch failed", error));
|
|
||||||
|
|
||||||
return new Response(null, { status: 204 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { msg } from "@lingui/core/macro";
|
import { msg } from "@lingui/core/macro";
|
||||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||||
import { isAdminRole } from "@/features/core/utils/roles";
|
|
||||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { localizedFor } from "@/lib/i18n/server-messages";
|
import { localizedFor } from "@/lib/i18n/server-messages";
|
||||||
@@ -14,13 +12,13 @@ const uploadSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/api/tournaments/upload-logo")({
|
export const Route = createFileRoute("/api/tournaments/upload-logo")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [apiLoggingMiddleware, superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
POST: async ({ request, context }) => {
|
POST: async ({ request, context }) => {
|
||||||
const i18n = localizedFor(context.metadata);
|
const i18n = localizedFor(context.metadata);
|
||||||
try {
|
try {
|
||||||
const userId = context.userAuthId;
|
const userId = context.userAuthId;
|
||||||
const isAdmin = isAdminRole(context.roles);
|
const isAdmin = context.roles.includes("Admin");
|
||||||
|
|
||||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
||||||
if (!isAdmin) return new Response("Unauthorized", { status: 403 });
|
if (!isAdmin) return new Response("Unauthorized", { status: 403 });
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { createCsrfMiddleware, createStart } from "@tanstack/react-start";
|
|
||||||
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
|
||||||
|
|
||||||
const csrfMiddleware = createCsrfMiddleware({
|
|
||||||
filter: (ctx) => ctx.handlerType === "serverFn",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const startInstance = createStart(() => ({
|
|
||||||
requestMiddleware: [csrfMiddleware],
|
|
||||||
functionMiddleware: [serverFnLoggingMiddleware],
|
|
||||||
}));
|
|
||||||
@@ -22,7 +22,6 @@ import { useDisclosure } from '@mantine/hooks'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import toast from '@/lib/sonner'
|
import toast from '@/lib/sonner'
|
||||||
import { logger } from '@/lib/logger'
|
import { logger } from '@/lib/logger'
|
||||||
import { captureBoundaryError } from '@/lib/telemetry/client'
|
|
||||||
import { XCircleIcon, WarningIcon } from '@phosphor-icons/react'
|
import { XCircleIcon, WarningIcon } from '@phosphor-icons/react'
|
||||||
import { Trans, useLingui } from '@lingui/react/macro'
|
import { Trans, useLingui } from '@lingui/react/macro'
|
||||||
import Button from './button'
|
import Button from './button'
|
||||||
@@ -42,7 +41,6 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.error('DefaultCatchBoundary | ', error)
|
logger.error('DefaultCatchBoundary | ', error)
|
||||||
captureBoundaryError(error)
|
|
||||||
|
|
||||||
if (errorMessage.toLowerCase().includes('unauthenticated')) {
|
if (errorMessage.toLowerCase().includes('unauthenticated')) {
|
||||||
toast.error(t`You've been logged out`)
|
toast.error(t`You've been logged out`)
|
||||||
|
|||||||
@@ -53,14 +53,12 @@ export function Countdown({ date, label, color }: CountdownProps) {
|
|||||||
return (
|
return (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{label && <Text size='sm' fw={500}>{label}:</Text>}
|
{label && <Text size='sm' fw={500}>{label}:</Text>}
|
||||||
{/* Server and client render different seconds; the first client tick corrects it. */}
|
|
||||||
<Text
|
<Text
|
||||||
size='sm'
|
size='sm'
|
||||||
fw={600}
|
fw={600}
|
||||||
c={isFinalStretch ? 'red' : color}
|
c={isFinalStretch ? 'red' : color}
|
||||||
ff="monospace"
|
ff="monospace"
|
||||||
className={classes.time}
|
className={classes.time}
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
>
|
||||||
{prefix}
|
{prefix}
|
||||||
<span
|
<span
|
||||||
@@ -68,7 +66,6 @@ export function Countdown({ date, label, color }: CountdownProps) {
|
|||||||
className={`${classes.seconds} ${
|
className={`${classes.seconds} ${
|
||||||
isFinalStretch ? classes.finalTick : classes.tick
|
isFinalStretch ? classes.finalTick : classes.tick
|
||||||
}`}
|
}`}
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
>
|
||||||
{pad(timeLeft.seconds)}
|
{pad(timeLeft.seconds)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Box } from "@mantine/core";
|
|
||||||
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
const HeadToHeadIcon = ({ size = 14 }: { size?: number }) => (
|
|
||||||
<Box
|
|
||||||
component="span"
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
display: "inline-block",
|
|
||||||
width: size * (27.5 / 14),
|
|
||||||
height: size * (16 / 14),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FootballHelmetIcon
|
|
||||||
size={size}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
transform: "rotate(25deg)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FootballHelmetIcon
|
|
||||||
size={size}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
transform: "scaleX(-1) rotate(25deg)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default HeadToHeadIcon;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { track } from '@/lib/telemetry/client'
|
|
||||||
import { Box, Paper, Group, Text, ActionIcon } from '@mantine/core'
|
import { Box, Paper, Group, Text, ActionIcon } from '@mantine/core'
|
||||||
import { DownloadIcon, XIcon } from '@phosphor-icons/react'
|
import { DownloadIcon, XIcon } from '@phosphor-icons/react'
|
||||||
import { Trans, useLingui } from '@lingui/react/macro'
|
import { Trans, useLingui } from '@lingui/react/macro'
|
||||||
@@ -27,17 +26,13 @@ export function IOSInstallPrompt() {
|
|||||||
|
|
||||||
if ((isIOS || isAndroid) && !isInStandaloneMode && !hasBeenDismissed) {
|
if ((isIOS || isAndroid) && !isInStandaloneMode && !hasBeenDismissed) {
|
||||||
setPlatform(isIOS ? 'ios' : 'android')
|
setPlatform(isIOS ? 'ios' : 'android')
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => setShow(true), 3000)
|
||||||
setShow(true)
|
|
||||||
track('pwa_ios_prompt_shown')
|
|
||||||
}, 3000)
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDismiss = () => {
|
const handleDismiss = () => {
|
||||||
localStorage.setItem('pwa-install-prompt-dismissed', 'true')
|
localStorage.setItem('pwa-install-prompt-dismissed', 'true')
|
||||||
track('pwa_ios_prompt_dismissed')
|
|
||||||
setShow(false)
|
setShow(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Text, UnstyledButton } from "@mantine/core";
|
|
||||||
import { Icon } from "@phosphor-icons/react";
|
|
||||||
import classes from "./nav-tile.module.css";
|
|
||||||
|
|
||||||
interface NavTileProps {
|
|
||||||
label: string;
|
|
||||||
Icon: Icon;
|
|
||||||
onClick?: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A single square-ish nav tile with icon-over-label and press feedback. */
|
|
||||||
export const NavTile = ({ label, Icon, onClick, disabled }: NavTileProps) => (
|
|
||||||
<UnstyledButton
|
|
||||||
className={`${classes.tile} flxn-tile`}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<span className={`${classes.icon} flxn-tile-icon`}>
|
|
||||||
<Icon size={22} weight="bold" />
|
|
||||||
</span>
|
|
||||||
<Text size="xs" fw={600} lineClamp={2} lh={1.2}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</UnstyledButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default NavTile;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
.tile {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 14px 8px;
|
|
||||||
border-radius: var(--mantine-radius-md);
|
|
||||||
border: 1px solid var(--mantine-color-default-border);
|
|
||||||
background-color: var(--mantine-color-body);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--mantine-primary-color-filled);
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.tile:hover:not(:disabled) {
|
|
||||||
background-color: var(--mantine-color-default-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile:active:not(:disabled) {
|
|
||||||
background-color: var(--mantine-color-default-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.tile {
|
|
||||||
transition: background-color 120ms ease-out, transform 120ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile:active:not(:disabled) {
|
|
||||||
transform: scale(0.97);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Skeleton, Stack } from "@mantine/core";
|
|
||||||
|
|
||||||
const RoutePending = () => (
|
|
||||||
<Stack gap="md" p="md" w="100%">
|
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<Skeleton
|
|
||||||
key={`route-pending-${index}`}
|
|
||||||
height={96}
|
|
||||||
w="100%"
|
|
||||||
radius="md"
|
|
||||||
style={{ opacity: Math.max(1 - index * 0.15, 0.3) }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default RoutePending;
|
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||||
import Drawer from "./drawer";
|
import Drawer from "./drawer";
|
||||||
import Modal from "./modal";
|
import Modal from "./modal";
|
||||||
import { Box, Flex, Loader } from "@mantine/core";
|
import { ScrollArea, Flex, Loader } from "@mantine/core";
|
||||||
|
|
||||||
interface SheetProps extends PropsWithChildren {
|
interface SheetProps extends PropsWithChildren {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -60,19 +60,14 @@ const Sheet: React.FC<SheetProps> = ({ title, children, opened, onChange }) => {
|
|||||||
<Loader size='lg' />
|
<Loader size='lg' />
|
||||||
</Flex>
|
</Flex>
|
||||||
}>
|
}>
|
||||||
{/* Native overflow, not ScrollArea: vaul's drag-vs-scroll detection
|
<ScrollArea.Autosize
|
||||||
only recognizes natively scrollable elements. */}
|
style={{ flex: 1, maxHeight: '75dvh' }}
|
||||||
<Box
|
scrollbarSize={8}
|
||||||
style={{
|
scrollbars="y"
|
||||||
flex: 1,
|
type="scroll"
|
||||||
maxHeight: "75dvh",
|
|
||||||
overflowY: "auto",
|
|
||||||
overscrollBehavior: "contain",
|
|
||||||
WebkitOverflowScrolling: "touch",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</ScrollArea.Autosize>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</SheetComponent>
|
</SheetComponent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Box, Group, Paper, Skeleton } from "@mantine/core";
|
|
||||||
import type { MantineRadius } from "@mantine/core";
|
|
||||||
|
|
||||||
export interface CardSkeletonProps {
|
|
||||||
withAvatar?: boolean;
|
|
||||||
avatarSize?: number;
|
|
||||||
withBadge?: boolean;
|
|
||||||
radius?: MantineRadius;
|
|
||||||
opacity?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CardSkeleton = ({
|
|
||||||
withAvatar = true,
|
|
||||||
avatarSize = 32,
|
|
||||||
withBadge = true,
|
|
||||||
radius = "md",
|
|
||||||
opacity = 1,
|
|
||||||
}: CardSkeletonProps) => {
|
|
||||||
return (
|
|
||||||
<Paper p="sm" withBorder radius={radius} style={{ opacity }}>
|
|
||||||
<Group gap="sm" align="center" wrap="nowrap">
|
|
||||||
{withAvatar && (
|
|
||||||
<Skeleton height={avatarSize} width={avatarSize} radius="sm" />
|
|
||||||
)}
|
|
||||||
<Box style={{ flex: 1 }}>
|
|
||||||
<Skeleton height={16} width="70%" mb={4} />
|
|
||||||
<Skeleton height={12} width="40%" />
|
|
||||||
</Box>
|
|
||||||
{withBadge && <Skeleton height={20} width={60} radius="xl" />}
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CardSkeleton;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./list-skeleton";
|
|
||||||
export * from "./card-skeleton";
|
|
||||||
export * from "./stat-tile-skeleton";
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { Box, Divider, Group, Skeleton, Stack } from "@mantine/core";
|
|
||||||
import type { MantineSpacing } from "@mantine/core";
|
|
||||||
|
|
||||||
export interface ListSkeletonProps {
|
|
||||||
rows?: number;
|
|
||||||
withSearch?: boolean;
|
|
||||||
withAvatar?: boolean;
|
|
||||||
avatarSize?: number;
|
|
||||||
withSubtitle?: boolean;
|
|
||||||
withTrailing?: boolean | "double" | "text";
|
|
||||||
titleWidth?: string | number;
|
|
||||||
padding?: MantineSpacing;
|
|
||||||
fadeStep?: number;
|
|
||||||
fadeFloor?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ListSkeleton = ({
|
|
||||||
rows = 6,
|
|
||||||
withSearch = false,
|
|
||||||
withAvatar = false,
|
|
||||||
avatarSize = 40,
|
|
||||||
withSubtitle = false,
|
|
||||||
withTrailing = false,
|
|
||||||
titleWidth = "45%",
|
|
||||||
padding = "md",
|
|
||||||
fadeStep = 0.1,
|
|
||||||
fadeFloor = 0.35,
|
|
||||||
}: ListSkeletonProps) => {
|
|
||||||
return (
|
|
||||||
<Stack gap={0}>
|
|
||||||
{withSearch && (
|
|
||||||
<Box px={padding} pb="sm">
|
|
||||||
<Skeleton height={42} radius="sm" />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{Array.from({ length: rows }).map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{ opacity: Math.max(1 - index * fadeStep, fadeFloor) }}
|
|
||||||
>
|
|
||||||
<Group
|
|
||||||
p={padding}
|
|
||||||
wrap="nowrap"
|
|
||||||
w="100%"
|
|
||||||
justify={withTrailing === "text" ? "space-between" : undefined}
|
|
||||||
>
|
|
||||||
{withAvatar && (
|
|
||||||
<Skeleton height={avatarSize} width={avatarSize} radius="sm" />
|
|
||||||
)}
|
|
||||||
{withSubtitle ? (
|
|
||||||
<Stack gap={6}>
|
|
||||||
<Skeleton height={14} width={160} radius="sm" />
|
|
||||||
<Skeleton height={10} width={100} radius="sm" />
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Skeleton height={16} width={titleWidth} radius="sm" />
|
|
||||||
)}
|
|
||||||
{withTrailing === "double" ? (
|
|
||||||
<Stack ml="auto" gap={6} align="flex-end">
|
|
||||||
<Skeleton height={10} width={90} radius="sm" />
|
|
||||||
<Skeleton height={10} width={70} radius="sm" />
|
|
||||||
</Stack>
|
|
||||||
) : withTrailing === "text" ? (
|
|
||||||
<Skeleton height={10} width={60} radius="sm" />
|
|
||||||
) : withTrailing ? (
|
|
||||||
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
|
||||||
) : null}
|
|
||||||
</Group>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListSkeleton;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Card, Group, Skeleton, Stack } from "@mantine/core";
|
|
||||||
|
|
||||||
export interface StatTileSkeletonProps {
|
|
||||||
withCard?: boolean;
|
|
||||||
align?: "row" | "column";
|
|
||||||
numberWidth?: number;
|
|
||||||
numberHeight?: number;
|
|
||||||
labelWidth?: number;
|
|
||||||
labelHeight?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StatTileSkeleton = ({
|
|
||||||
withCard = false,
|
|
||||||
align = "column",
|
|
||||||
numberWidth = align === "row" ? 36 : 48,
|
|
||||||
numberHeight = 28,
|
|
||||||
labelWidth = align === "row" ? 110 : 80,
|
|
||||||
labelHeight = align === "row" ? 14 : 12,
|
|
||||||
}: StatTileSkeletonProps) => {
|
|
||||||
const content =
|
|
||||||
align === "row" ? (
|
|
||||||
<Group gap="xs" align="baseline">
|
|
||||||
<Skeleton height={numberHeight} width={numberWidth} radius="sm" />
|
|
||||||
<Skeleton height={labelHeight} width={labelWidth} radius="sm" />
|
|
||||||
</Group>
|
|
||||||
) : (
|
|
||||||
<Stack gap={4} align="center">
|
|
||||||
<Skeleton height={numberHeight} width={numberWidth} radius="sm" />
|
|
||||||
<Skeleton height={labelHeight} width={labelWidth} radius="sm" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!withCard) return content;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card withBorder radius="md" p="md">
|
|
||||||
{content}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StatTileSkeleton;
|
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
startTransition,
|
useMemo,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -48,9 +48,6 @@ function SwipeableTabs({
|
|||||||
}, [search?.tab, tabs, defaultTab]);
|
}, [search?.tab, tabs, defaultTab]);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
|
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
|
||||||
const [visitedTabs, setVisitedTabs] = useState<ReadonlySet<number>>(
|
|
||||||
() => new Set([getActiveTabFromUrl()])
|
|
||||||
);
|
|
||||||
const [stuck, setStuck] = useState(false);
|
const [stuck, setStuck] = useState(false);
|
||||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||||
@@ -63,9 +60,6 @@ function SwipeableTabs({
|
|||||||
if (index === activeTab || index < 0 || index >= tabs.length) return;
|
if (index === activeTab || index < 0 || index >= tabs.length) return;
|
||||||
|
|
||||||
setActiveTab(index);
|
setActiveTab(index);
|
||||||
startTransition(() => {
|
|
||||||
setVisitedTabs((prev) => (prev.has(index) ? prev : new Set(prev).add(index)));
|
|
||||||
});
|
|
||||||
embla?.scrollTo(index);
|
embla?.scrollTo(index);
|
||||||
onTabChange?.(index, tabs[index]);
|
onTabChange?.(index, tabs[index]);
|
||||||
|
|
||||||
@@ -231,7 +225,7 @@ function SwipeableTabs({
|
|||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<Carousel.Slide key={`${tab.label}-content-${index}`}>
|
<Carousel.Slide key={`${tab.label}-content-${index}`}>
|
||||||
<Box ref={setSlideRef(index)} style={{ height: "auto", width: "100%", maxWidth: "100vw", overflow: "hidden" }}>
|
<Box ref={setSlideRef(index)} style={{ height: "auto", width: "100%", maxWidth: "100vw", overflow: "hidden" }}>
|
||||||
{(visitedTabs.has(index) || index === activeTab) ? tab.content : null}
|
{tab.content}
|
||||||
</Box>
|
</Box>
|
||||||
</Carousel.Slide>
|
</Carousel.Slide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ interface AuthData {
|
|||||||
theme?: string;
|
theme?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
seenThemesHint?: boolean;
|
seenThemesHint?: boolean;
|
||||||
telemetryAlerts?: boolean;
|
|
||||||
};
|
};
|
||||||
roles: string[];
|
roles: string[];
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createContext, useCallback, useEffect, useMemo, useState, PropsWithChildren } from 'react';
|
import { createContext, useCallback, useEffect, useMemo, useState, PropsWithChildren } from 'react';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { SpotifyAuth } from '@/lib/spotify/auth';
|
import { SpotifyAuth } from '@/lib/spotify/auth';
|
||||||
import { useIsAdmin } from '@/features/core/hooks/use-is-admin';
|
import { useAuth } from './auth-context';
|
||||||
import { useConfig } from '@/hooks/use-config';
|
import { useConfig } from '@/hooks/use-config';
|
||||||
import type {
|
import type {
|
||||||
SpotifyContextType,
|
SpotifyContextType,
|
||||||
@@ -40,7 +40,8 @@ const deepEqual = (a: unknown, b: unknown): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const isAdmin = useIsAdmin();
|
const { roles } = useAuth();
|
||||||
|
const isAdmin = roles?.includes('Admin') || false;
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
|||||||
@@ -1,173 +1,137 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, memo } from "react";
|
||||||
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
||||||
import { Alert, Code, Container, Group, Stack, Text } from "@mantine/core";
|
import {
|
||||||
import { CheckIcon, XIcon } from "@phosphor-icons/react";
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
UnstyledButton,
|
||||||
|
Select,
|
||||||
|
Pagination,
|
||||||
|
Code,
|
||||||
|
Alert,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
CaretUpIcon,
|
||||||
|
CaretDownIcon,
|
||||||
|
CheckIcon,
|
||||||
|
XIcon,
|
||||||
|
ChecksIcon,
|
||||||
|
PulseIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
import { Activity, ActivitySearchParams } from "../types";
|
import { Activity, ActivitySearchParams } from "../types";
|
||||||
import { useActivities } from "../queries";
|
import { useActivities } from "../queries";
|
||||||
import { usePlayers } from "@/features/players/queries";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import {
|
|
||||||
DetailSheet,
|
|
||||||
PaginatedList,
|
|
||||||
SearchFilterBar,
|
|
||||||
TelemetryListItem,
|
|
||||||
} from "@/features/telemetry/components/shared";
|
|
||||||
|
|
||||||
const SLOW_THRESHOLD_MS = 1000;
|
interface ActivityListItemProps {
|
||||||
|
activity: Activity;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const ActivitiesTable = () => {
|
const ActivityListItem = memo(({ activity, onClick }: ActivityListItemProps) => {
|
||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const [page, setPage] = useState(1);
|
const playerName = typeof activity.player === "object" && activity.player
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [successFilter, setSuccessFilter] = useState<string | null>(null);
|
|
||||||
const [playerFilter, setPlayerFilter] = useState<string | null>(null);
|
|
||||||
const [slowOnly, setSlowOnly] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState("-created");
|
|
||||||
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
|
|
||||||
const { isOpen: detailsOpened, open: openDetails, close: closeDetails } = useSheet();
|
|
||||||
|
|
||||||
const { data: players } = usePlayers();
|
|
||||||
|
|
||||||
const searchParams: ActivitySearchParams = useMemo(
|
|
||||||
() => ({
|
|
||||||
page,
|
|
||||||
perPage: 100,
|
|
||||||
name: search || undefined,
|
|
||||||
success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined,
|
|
||||||
player: playerFilter || undefined,
|
|
||||||
sortBy,
|
|
||||||
}),
|
|
||||||
[page, search, successFilter, playerFilter, sortBy]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: result } = useActivities(searchParams);
|
|
||||||
|
|
||||||
const items = useMemo(
|
|
||||||
() => (slowOnly ? result.items.filter((a) => a.duration > SLOW_THRESHOLD_MS) : result.items),
|
|
||||||
[result.items, slowOnly]
|
|
||||||
);
|
|
||||||
|
|
||||||
const playerName = (activity: Activity) =>
|
|
||||||
typeof activity.player === "object" && activity.player
|
|
||||||
? `${activity.player.first_name} ${activity.player.last_name}`
|
? `${activity.player.first_name} ${activity.player.last_name}`
|
||||||
: t`System`;
|
: t`System`;
|
||||||
|
|
||||||
const handleSort = (field: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
setSortBy((prev) => {
|
const date = new Date(dateStr);
|
||||||
if (prev === field) return `-${field}`;
|
return date.toLocaleString(i18n.locale);
|
||||||
if (prev === `-${field}`) return field;
|
|
||||||
return `-${field}`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleActivityClick = (activity: Activity) => {
|
|
||||||
setSelectedActivity(activity);
|
|
||||||
openDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseDetails = () => {
|
|
||||||
setSelectedActivity(null);
|
|
||||||
closeDetails();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="100%" px={0}>
|
<UnstyledButton
|
||||||
<Stack gap="xs">
|
w="100%"
|
||||||
<SearchFilterBar
|
p="md"
|
||||||
searchValue={search}
|
onClick={onClick}
|
||||||
onSearchChange={(value) => {
|
style={{
|
||||||
setSearch(value);
|
borderRadius: 0,
|
||||||
setPage(1);
|
transition: "background-color 0.15s ease",
|
||||||
}}
|
}}
|
||||||
searchPlaceholder={t`serverFn name`}
|
styles={{
|
||||||
filters={[
|
root: {
|
||||||
{
|
'&:hover': {
|
||||||
placeholder: t`Status`,
|
backgroundColor: 'var(--mantine-color-gray-0)',
|
||||||
value: successFilter,
|
|
||||||
onChange: (value) => {
|
|
||||||
setSuccessFilter(value);
|
|
||||||
setPage(1);
|
|
||||||
},
|
},
|
||||||
data: [
|
|
||||||
{ value: "all", label: t`All` },
|
|
||||||
{ value: "success", label: t`Success` },
|
|
||||||
{ value: "failure", label: t`Failure` },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
}}
|
||||||
placeholder: t`Player`,
|
>
|
||||||
value: playerFilter,
|
<Group justify="space-between" align="flex-start" w="100%">
|
||||||
onChange: (value) => {
|
<Stack gap={4} flex={1}>
|
||||||
setPlayerFilter(value);
|
<Group gap="xs">
|
||||||
setPage(1);
|
<Text size="sm" fw={600}>
|
||||||
},
|
{activity.name}
|
||||||
data: players.map((player) => ({
|
</Text>
|
||||||
value: player.id,
|
{activity.success ? (
|
||||||
label: `${player.first_name} ${player.last_name}`,
|
|
||||||
})),
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
chips={[
|
|
||||||
{
|
|
||||||
label: t`Slow >1s (this page)`,
|
|
||||||
checked: slowOnly,
|
|
||||||
onChange: setSlowOnly,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
sortFields={[
|
|
||||||
{ field: "created", label: <Trans>Date</Trans> },
|
|
||||||
{ field: "duration", label: <Trans>Duration</Trans> },
|
|
||||||
]}
|
|
||||||
sortBy={sortBy}
|
|
||||||
onSort={handleSort}
|
|
||||||
summary={
|
|
||||||
<Plural value={result.totalItems} one="# total activity" other="# total activities" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PaginatedList
|
|
||||||
items={items}
|
|
||||||
keyOf={(activity) => activity.id}
|
|
||||||
empty={t`No Activities Found`}
|
|
||||||
page={page}
|
|
||||||
totalPages={result.totalPages}
|
|
||||||
onPageChange={setPage}
|
|
||||||
renderItem={(activity) => (
|
|
||||||
<TelemetryListItem
|
|
||||||
title={activity.name}
|
|
||||||
icon={
|
|
||||||
activity.success ? (
|
|
||||||
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
||||||
) : (
|
) : (
|
||||||
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
||||||
)
|
|
||||||
}
|
|
||||||
meta={[
|
|
||||||
playerName(activity),
|
|
||||||
<Trans>{activity.duration}ms</Trans>,
|
|
||||||
new Date(activity.created).toLocaleString(i18n.locale),
|
|
||||||
]}
|
|
||||||
error={activity.error}
|
|
||||||
onClick={() => handleActivityClick(activity)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</Group>
|
||||||
|
<Group gap="md">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{playerName}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
<Trans>{activity.duration}ms</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatDate(activity.created)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{activity.error && (
|
||||||
|
<Text size="xs" c="red" lineClamp={1}>
|
||||||
|
{activity.error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ActivityDetailsSheetProps {
|
||||||
|
activity: Activity | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivityDetailsSheet = memo(({ activity, isOpen, onClose }: ActivityDetailsSheetProps) => {
|
||||||
|
const { t, i18n } = useLingui();
|
||||||
|
if (!activity) return null;
|
||||||
|
|
||||||
|
const playerName = typeof activity.player === "object" && activity.player
|
||||||
|
? `${activity.player.first_name} ${activity.player.last_name}`
|
||||||
|
: t`System`;
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleString(i18n.locale);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet title={t`Activity Details`} opened={isOpen} onChange={onClose}>
|
||||||
|
<Stack gap="md" p="md">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
|
<Trans>Function Name</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">{activity.name}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<DetailSheet
|
<Stack gap="xs">
|
||||||
title={t`Activity Details`}
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
opened={detailsOpened}
|
<Trans>Status</Trans>
|
||||||
onClose={handleCloseDetails}
|
</Text>
|
||||||
fields={
|
|
||||||
selectedActivity
|
|
||||||
? [
|
|
||||||
{ label: <Trans>Function Name</Trans>, value: selectedActivity.name },
|
|
||||||
{
|
|
||||||
label: <Trans>Status</Trans>,
|
|
||||||
value: (
|
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
{selectedActivity.success ? (
|
{activity.success ? (
|
||||||
<>
|
<>
|
||||||
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
||||||
<Text size="sm" c="green">
|
<Text size="sm" c="green">
|
||||||
@@ -183,53 +147,247 @@ export const ActivitiesTable = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
),
|
</Stack>
|
||||||
},
|
|
||||||
{ label: <Trans>Player</Trans>, value: playerName(selectedActivity) },
|
<Stack gap="xs">
|
||||||
{
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
label: <Trans>Duration</Trans>,
|
<Trans>Player</Trans>
|
||||||
value: (
|
</Text>
|
||||||
<Text size="sm">
|
<Text size="sm">{playerName}</Text>
|
||||||
<Trans>{selectedActivity.duration}ms</Trans>
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
|
<Trans>Duration</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text size="sm"><Trans>{activity.duration}ms</Trans></Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
|
<Trans>Created</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">{formatDate(activity.created)}</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{activity.user_agent && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
|
<Trans>User Agent</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" style={{ wordBreak: "break-word" }}>
|
||||||
|
{activity.user_agent}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activity.error && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
|
<Trans>Error Message</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <Trans>Created</Trans>,
|
|
||||||
value: new Date(selectedActivity.created).toLocaleString(i18n.locale),
|
|
||||||
},
|
|
||||||
...(selectedActivity.user_agent
|
|
||||||
? [{ label: <Trans>User Agent</Trans>, value: selectedActivity.user_agent }]
|
|
||||||
: []),
|
|
||||||
...(selectedActivity.error
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: <Trans>Error Message</Trans>,
|
|
||||||
value: (
|
|
||||||
<Alert color="red" variant="light">
|
<Alert color="red" variant="light">
|
||||||
<Text size="sm" style={{ wordBreak: "break-word" }}>
|
<Text size="sm" style={{ wordBreak: "break-word" }}>
|
||||||
{selectedActivity.error}
|
{activity.error}
|
||||||
</Text>
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
),
|
</Stack>
|
||||||
},
|
)}
|
||||||
]
|
|
||||||
: []),
|
{activity.arguments && (
|
||||||
...(selectedActivity.arguments
|
<Stack gap="xs">
|
||||||
? [
|
<Text size="xs" fw={700} c="dimmed">
|
||||||
{
|
<Trans>Arguments</Trans>
|
||||||
label: <Trans>Arguments</Trans>,
|
</Text>
|
||||||
value: (
|
|
||||||
<Code block style={{ fontSize: "11px" }}>
|
<Code block style={{ fontSize: "11px" }}>
|
||||||
{JSON.stringify(selectedActivity.arguments, null, 2)}
|
{JSON.stringify(activity.arguments, null, 2)}
|
||||||
</Code>
|
</Code>
|
||||||
),
|
</Stack>
|
||||||
},
|
)}
|
||||||
]
|
</Stack>
|
||||||
: []),
|
</Sheet>
|
||||||
]
|
);
|
||||||
: []
|
});
|
||||||
}
|
|
||||||
|
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
|
||||||
|
const { data: result } = useActivities(searchParams);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack gap={0}>
|
||||||
|
{result.items.map((activity: Activity, index: number) => (
|
||||||
|
<Box key={activity.id}>
|
||||||
|
<ActivityListItem
|
||||||
|
activity={activity}
|
||||||
|
onClick={() => onActivityClick(activity)}
|
||||||
|
/>
|
||||||
|
{index < result.items.length - 1 && <Divider />}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{result.items.length === 0 && (
|
||||||
|
<Stack align="center" gap="md" py="xl">
|
||||||
|
<ThemeIcon size="xl" variant="light" radius="md">
|
||||||
|
<PulseIcon size={32} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Title order={3} c="dimmed">
|
||||||
|
<Trans>No Activities Found</Trans>
|
||||||
|
</Title>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result.totalPages > 1 && (
|
||||||
|
<Group justify="center" py="md">
|
||||||
|
<Pagination
|
||||||
|
total={result.totalPages}
|
||||||
|
value={page}
|
||||||
|
onChange={setPage}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivitiesTable = () => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [successFilter, setSuccessFilter] = useState<string | null>(null);
|
||||||
|
const [sortBy, setSortBy] = useState("-created");
|
||||||
|
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen: detailsOpened,
|
||||||
|
open: openDetails,
|
||||||
|
close: closeDetails,
|
||||||
|
} = useSheet();
|
||||||
|
|
||||||
|
const searchParams: ActivitySearchParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
page,
|
||||||
|
perPage: 100,
|
||||||
|
name: search || undefined,
|
||||||
|
success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined,
|
||||||
|
sortBy,
|
||||||
|
}),
|
||||||
|
[page, search, successFilter, sortBy]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: result } = useActivities(searchParams);
|
||||||
|
|
||||||
|
const handleSort = (field: string) => {
|
||||||
|
setSortBy((prev) => {
|
||||||
|
if (prev === field) return `-${field}`;
|
||||||
|
if (prev === `-${field}`) return field;
|
||||||
|
return `-${field}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortIcon = (field: string) => {
|
||||||
|
if (sortBy === field) return <CaretUpIcon size={14} />;
|
||||||
|
if (sortBy === `-${field}`) return <CaretDownIcon size={14} />;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivityClick = (activity: Activity) => {
|
||||||
|
setSelectedActivity(activity);
|
||||||
|
openDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDetails = () => {
|
||||||
|
setSelectedActivity(null);
|
||||||
|
closeDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="100%" px={0}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Stack gap="xs" px="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder={t`serverFn name`}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.currentTarget.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
leftSection={<MagnifyingGlassIcon size={16} />}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group>
|
||||||
|
<Select
|
||||||
|
placeholder={t`Status`}
|
||||||
|
value={successFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSuccessFilter(value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{ value: "all", label: t`All` },
|
||||||
|
{ value: "success", label: t`Success` },
|
||||||
|
{ value: "failure", label: t`Failure` },
|
||||||
|
]}
|
||||||
|
clearable
|
||||||
|
size="sm"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Group px="md" justify="space-between" align="center">
|
||||||
|
<Text size="10px" lh={0} c="dimmed">
|
||||||
|
<Plural value={result.totalItems} one="# total activity" other="# total activities" />
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
<Trans>Sort:</Trans>
|
||||||
|
</Text>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => handleSort("created")}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={sortBy.includes("created") ? 600 : 400}
|
||||||
|
c={sortBy.includes("created") ? "var(--mantine-color-text)" : "dimmed"}
|
||||||
|
>
|
||||||
|
<Trans>Date</Trans>
|
||||||
|
</Text>
|
||||||
|
{getSortIcon("created")}
|
||||||
|
</UnstyledButton>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => handleSort("duration")}
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={sortBy.includes("duration") ? 600 : 400}
|
||||||
|
c={sortBy.includes("duration") ? "var(--mantine-color-text)" : "dimmed"}
|
||||||
|
>
|
||||||
|
<Trans>Duration</Trans>
|
||||||
|
</Text>
|
||||||
|
{getSortIcon("duration")}
|
||||||
|
</UnstyledButton>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ActivitiesResults
|
||||||
|
searchParams={searchParams}
|
||||||
|
page={page}
|
||||||
|
setPage={setPage}
|
||||||
|
onActivityClick={handleActivityClick}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<ActivityDetailsSheet
|
||||||
|
activity={selectedActivity}
|
||||||
|
isOpen={detailsOpened}
|
||||||
|
onClose={handleCloseDetails}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { superTokensDeveloperFunctionMiddleware } from "@/utils/supertokens";
|
import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -17,7 +17,7 @@ const activitySearchParamsSchema = z.object({
|
|||||||
|
|
||||||
export const searchActivities = createServerFn()
|
export const searchActivities = createServerFn()
|
||||||
.validator(activitySearchParamsSchema)
|
.validator(activitySearchParamsSchema)
|
||||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult<ActivityListResult>(async () => {
|
toServerResult<ActivityListResult>(async () => {
|
||||||
const result = await pbAdmin.searchActivities(data);
|
const result = await pbAdmin.searchActivities(data);
|
||||||
|
|||||||
+17
-13
@@ -1,22 +1,21 @@
|
|||||||
import { List, Text } from "@mantine/core";
|
import { List, Text } from "@mantine/core";
|
||||||
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
||||||
|
import ListLink from "@/components/list-link";
|
||||||
import {
|
import {
|
||||||
|
DatabaseIcon,
|
||||||
TreeStructureIcon,
|
TreeStructureIcon,
|
||||||
TrophyIcon,
|
TrophyIcon,
|
||||||
MedalIcon,
|
MedalIcon,
|
||||||
CrownIcon,
|
CrownIcon,
|
||||||
PulseIcon,
|
ListIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import ListLink from "@/components/list-link";
|
|
||||||
import ListButton from "@/components/list-button";
|
import ListButton from "@/components/list-button";
|
||||||
import { useIsDeveloper } from "@/features/core/hooks/use-is-developer";
|
|
||||||
import { getBadgeMigrationStatus, migrateBadgeProgress } from "@/features/badges/server";
|
import { getBadgeMigrationStatus, migrateBadgeProgress } from "@/features/badges/server";
|
||||||
import { useServerQuery } from "@/lib/tanstack-query/hooks";
|
import { useServerQuery } from "@/lib/tanstack-query/hooks";
|
||||||
import toast from "@/lib/sonner";
|
import toast from "@/lib/sonner";
|
||||||
|
|
||||||
export const ToolsList = () => {
|
const AdminPage = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const isDeveloper = useIsDeveloper();
|
|
||||||
|
|
||||||
const { data: migrationStatus, refetch: refetchMigrationStatus } = useServerQuery({
|
const { data: migrationStatus, refetch: refetchMigrationStatus } = useServerQuery({
|
||||||
queryKey: ["admin", "badge-migration-status"],
|
queryKey: ["admin", "badge-migration-status"],
|
||||||
@@ -52,13 +51,6 @@ export const ToolsList = () => {
|
|||||||
Icon={TrophyIcon}
|
Icon={TrophyIcon}
|
||||||
to="/admin/tournaments"
|
to="/admin/tournaments"
|
||||||
/>
|
/>
|
||||||
{isDeveloper && (
|
|
||||||
<ListLink
|
|
||||||
label={t`Telemetry`}
|
|
||||||
Icon={PulseIcon}
|
|
||||||
to="/admin/telemetry/usage"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ListLink
|
<ListLink
|
||||||
label={t`Award Badges`}
|
label={t`Award Badges`}
|
||||||
Icon={CrownIcon}
|
Icon={CrownIcon}
|
||||||
@@ -94,6 +86,18 @@ export const ToolsList = () => {
|
|||||||
<Trans>Badge recalculation failed. Check the server logs.</Trans>
|
<Trans>Badge recalculation failed. Check the server logs.</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
<ListLink
|
||||||
|
label={t`Activities`}
|
||||||
|
Icon={ListIcon}
|
||||||
|
to="/admin/activities"
|
||||||
|
/>
|
||||||
|
<ListButton
|
||||||
|
label={t`Open Pocketbase`}
|
||||||
|
Icon={DatabaseIcon}
|
||||||
|
onClick={() =>
|
||||||
|
window.location.replace(process.env.POCKETBASE_URL! + "/_/")
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ListLink
|
<ListLink
|
||||||
label={t`Bracket Preview`}
|
label={t`Bracket Preview`}
|
||||||
Icon={TreeStructureIcon}
|
Icon={TreeStructureIcon}
|
||||||
@@ -103,4 +107,4 @@ export const ToolsList = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToolsList;
|
export default AdminPage;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Box, Card, Text, Select, Button, Group, Stack, Badge, Divider } from "@mantine/core";
|
import { Box, Card, Text, Select, Button, Group, Stack, Badge, Divider } from "@mantine/core";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import useAwardManualBadge from "@/features/badges/hooks/use-award-manual-badge";
|
import { awardManualBadge } from "@/features/badges/server";
|
||||||
import { useAllBadges } from "@/features/badges/queries";
|
import { useAllBadges } from "@/features/badges/queries";
|
||||||
import toast from "@/lib/sonner";
|
import toast from "@/lib/sonner";
|
||||||
import { usePlayers } from "@/features/players/queries";
|
import { usePlayers } from "@/features/players/queries";
|
||||||
@@ -13,18 +13,22 @@ const AwardBadges = () => {
|
|||||||
|
|
||||||
const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
|
const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
|
||||||
const [selectedBadgeId, setSelectedBadgeId] = useState<string | null>(null);
|
const [selectedBadgeId, setSelectedBadgeId] = useState<string | null>(null);
|
||||||
|
const [isAwarding, setIsAwarding] = useState(false);
|
||||||
const awardBadge = useAwardManualBadge();
|
|
||||||
|
|
||||||
const manualBadges = allBadges.filter((badge) => badge.type === "manual");
|
const manualBadges = allBadges.filter((badge) => badge.type === "manual");
|
||||||
|
|
||||||
const handleAwardBadge = () => {
|
const handleAwardBadge = async () => {
|
||||||
if (!selectedPlayerId || !selectedBadgeId) return;
|
if (!selectedPlayerId || !selectedBadgeId) return;
|
||||||
|
|
||||||
awardBadge.mutate(
|
setIsAwarding(true);
|
||||||
{ playerId: selectedPlayerId, badgeId: selectedBadgeId },
|
try {
|
||||||
{
|
await awardManualBadge({
|
||||||
onSuccess: () => {
|
data: {
|
||||||
|
playerId: selectedPlayerId,
|
||||||
|
badgeId: selectedBadgeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const selectedPlayer = players.find((p) => p.id === selectedPlayerId);
|
const selectedPlayer = players.find((p) => p.id === selectedPlayerId);
|
||||||
const playerName = selectedPlayer
|
const playerName = selectedPlayer
|
||||||
? `${selectedPlayer.first_name} ${selectedPlayer.last_name}`
|
? `${selectedPlayer.first_name} ${selectedPlayer.last_name}`
|
||||||
@@ -33,9 +37,11 @@ const AwardBadges = () => {
|
|||||||
toast.success(t`Badge awarded to ${playerName}`);
|
toast.success(t`Badge awarded to ${playerName}`);
|
||||||
|
|
||||||
setSelectedPlayerId(null);
|
setSelectedPlayerId(null);
|
||||||
},
|
} catch (error) {
|
||||||
|
toast.error(t`Failed to award badge`);
|
||||||
|
} finally {
|
||||||
|
setIsAwarding(false);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const playerOptions = players.map((player) => ({
|
const playerOptions = players.map((player) => ({
|
||||||
@@ -92,7 +98,7 @@ const AwardBadges = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleAwardBadge}
|
onClick={handleAwardBadge}
|
||||||
disabled={!selectedPlayerId}
|
disabled={!selectedPlayerId}
|
||||||
loading={awardBadge.isPending}
|
loading={isAwarding}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<Trans>Award Badge</Trans>
|
<Trans>Award Badge</Trans>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { SimpleGrid, Skeleton } from "@mantine/core";
|
|
||||||
import QuietBoundary from "./quiet-boundary";
|
|
||||||
import SectionBox from "./section-box";
|
|
||||||
import TournamentSection from "./tournament-section";
|
|
||||||
import ToolsList from "./tools-list";
|
|
||||||
|
|
||||||
const TournamentSkeleton = () => (
|
|
||||||
<SectionBox>
|
|
||||||
<Skeleton height={24} width={180} radius="sm" />
|
|
||||||
<SimpleGrid cols={3} spacing="sm" mt="sm">
|
|
||||||
<Skeleton height={72} radius="md" />
|
|
||||||
<Skeleton height={72} radius="md" />
|
|
||||||
<Skeleton height={72} radius="md" />
|
|
||||||
</SimpleGrid>
|
|
||||||
</SectionBox>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const AdminDashboard = () => (
|
|
||||||
<>
|
|
||||||
<QuietBoundary>
|
|
||||||
<Suspense fallback={<TournamentSkeleton />}>
|
|
||||||
<TournamentSection />
|
|
||||||
</Suspense>
|
|
||||||
</QuietBoundary>
|
|
||||||
<ToolsList />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default AdminDashboard;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Component, ReactNode } from "react";
|
|
||||||
import { Text } from "@mantine/core";
|
|
||||||
import { Trans } from "@lingui/react/macro";
|
|
||||||
|
|
||||||
interface QuietBoundaryProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuietBoundaryState {
|
|
||||||
hasError: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QuietBoundary extends Component<QuietBoundaryProps, QuietBoundaryState> {
|
|
||||||
state: QuietBoundaryState = { hasError: false };
|
|
||||||
|
|
||||||
static getDerivedStateFromError(): QuietBoundaryState {
|
|
||||||
return { hasError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
<Trans>Unavailable right now</Trans>
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuietBoundary;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { Box } from "@mantine/core";
|
|
||||||
|
|
||||||
export const SectionBox = ({ children }: { children: ReactNode }) => (
|
|
||||||
<Box
|
|
||||||
px="md"
|
|
||||||
py="sm"
|
|
||||||
style={{ borderBottom: "1px solid var(--mantine-color-default-border)" }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default SectionBox;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { SimpleGrid, Text, Title } from "@mantine/core";
|
|
||||||
import { useLingui } from "@lingui/react/macro";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { GearIcon, PlayIcon, UsersThreeIcon } from "@phosphor-icons/react";
|
|
||||||
import { useCurrentTournament } from "@/features/tournaments/queries";
|
|
||||||
import { NavTile } from "@/components/nav-tile";
|
|
||||||
import { Countdown } from "@/components/countdown";
|
|
||||||
import SectionBox from "./section-box";
|
|
||||||
|
|
||||||
export const TournamentSection = () => {
|
|
||||||
const { t } = useLingui();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { data: tournament } = useCurrentTournament();
|
|
||||||
|
|
||||||
if (!tournament) return null;
|
|
||||||
|
|
||||||
const upcoming = !!tournament.start_time && Date.parse(tournament.start_time) > Date.now();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionBox>
|
|
||||||
<Title order={3}>{tournament.name}</Title>
|
|
||||||
{upcoming && (
|
|
||||||
<Text size="sm" c="dimmed" component="div">
|
|
||||||
<Countdown date={new Date(tournament.start_time)} label={t`Starts in`} />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<SimpleGrid cols={3} spacing="sm" mt="sm">
|
|
||||||
<NavTile
|
|
||||||
label={t`Run`}
|
|
||||||
Icon={PlayIcon}
|
|
||||||
onClick={() => navigate({ to: `/admin/tournaments/run/${tournament.id}` })}
|
|
||||||
/>
|
|
||||||
<NavTile
|
|
||||||
label={t`Enrollment`}
|
|
||||||
Icon={UsersThreeIcon}
|
|
||||||
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}/enrollment` })}
|
|
||||||
/>
|
|
||||||
<NavTile
|
|
||||||
label={t`Manage`}
|
|
||||||
Icon={GearIcon}
|
|
||||||
onClick={() => navigate({ to: `/admin/tournaments/${tournament.id}` })}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
</SectionBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TournamentSection;
|
|
||||||
@@ -2,4 +2,4 @@ import { Logger } from "@/lib/logger";
|
|||||||
|
|
||||||
export const logger = new Logger("Admin");
|
export const logger = new Logger("Admin");
|
||||||
|
|
||||||
export { default as AdminDashboard } from "./components/dashboard";
|
export { default as AdminPage } from "./components/admin-page";
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const BadgeIcon = ({ badge, filled, size = 48 }: BadgeIconProps & { size?
|
|||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import PlayerList from '@/features/players/components/player-list';
|
|||||||
import { useAuth } from '@/contexts/auth-context';
|
import { useAuth } from '@/contexts/auth-context';
|
||||||
import { Player } from '@/features/players/types';
|
import { Player } from '@/features/players/types';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { track } from '@/lib/telemetry/client';
|
|
||||||
|
|
||||||
const BadgeStatsTable = () => {
|
const BadgeStatsTable = () => {
|
||||||
const { data: allBadges } = useAllBadges();
|
const { data: allBadges } = useAllBadges();
|
||||||
@@ -142,13 +141,7 @@ const BadgeStatRow: React.FC<BadgeStatRowProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={badge.id}>
|
<Box key={badge.id}>
|
||||||
<UnstyledButton
|
<UnstyledButton onClick={badgeSheet.open} w='100%'>
|
||||||
onClick={() => {
|
|
||||||
track("badge_viewed", { value: badge.key });
|
|
||||||
badgeSheet.open();
|
|
||||||
}}
|
|
||||||
w='100%'
|
|
||||||
>
|
|
||||||
<Grid p={'xs'} align='center'>
|
<Grid p={'xs'} align='center'>
|
||||||
<Grid.Col span={2} style={{ display: 'flex', alignItems: 'center' }}>
|
<Grid.Col span={2} style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<BadgeIcon badge={badge} filled={true} />
|
<BadgeIcon badge={badge} filled={true} />
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
|
||||||
import { awardManualBadge } from "@/features/badges/server";
|
|
||||||
import { badgeKeys } from "@/features/badges/queries";
|
|
||||||
|
|
||||||
interface AwardManualBadgeInput {
|
|
||||||
playerId: string;
|
|
||||||
badgeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useAwardManualBadge() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useServerMutation({
|
|
||||||
mutationFn: (data: AwardManualBadgeInput) => awardManualBadge({ data }),
|
|
||||||
onSuccess: (_data, { playerId }) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: badgeKeys.playerBadges(playerId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: badgeKeys.allEarnedBadges() });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||||
|
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -14,7 +15,7 @@ export const getPlayerBadges = createServerFn()
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const migrateBadgeProgress = createServerFn()
|
export const migrateBadgeProgress = createServerFn()
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
toServerResult(async () => startBadgeMigration())
|
toServerResult(async () => startBadgeMigration())
|
||||||
);
|
);
|
||||||
@@ -40,7 +41,7 @@ export const awardManualBadge = createServerFn()
|
|||||||
playerId: z.string(),
|
playerId: z.string(),
|
||||||
badgeId: z.string(),
|
badgeId: z.string(),
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const result = await pbAdmin.awardManualBadge(data.playerId, data.badgeId);
|
const result = await pbAdmin.awardManualBadge(data.playerId, data.badgeId);
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!showControls && <MatchReport match={match} compact source="match_card" />}
|
{!showControls && <MatchReport match={match} compact />}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { Match } from "@/features/matches/types";
|
import { Match } from "@/features/matches/types";
|
||||||
import TeamAvatar from "@/components/team-avatar";
|
import TeamAvatar from "@/components/team-avatar";
|
||||||
@@ -24,7 +24,6 @@ import { MatchReport } from "./match-report";
|
|||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
|
const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
|||||||
ended={ended}
|
ended={ended}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MatchReport match={match} source="match_dock" />
|
<MatchReport match={match} />
|
||||||
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="sm">
|
<Group justify="space-between" wrap="nowrap" gap="sm">
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||||
@@ -167,7 +166,7 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
|||||||
</Center>
|
</Center>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<EmojiBar matchId={match.id} source="match_dock" />
|
<EmojiBar matchId={match.id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Box>
|
</Box>
|
||||||
{!hasPrivate && (
|
{!hasPrivate && (
|
||||||
@@ -175,20 +174,33 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={h2hSheet.open}
|
||||||
track("h2h_viewed", {
|
|
||||||
source: "match_dock",
|
|
||||||
context: [match?.home?.name, match?.away?.name]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" v "),
|
|
||||||
});
|
|
||||||
h2hSheet.open();
|
|
||||||
}}
|
|
||||||
aria-label={t`View head-to-head`}
|
aria-label={t`View head-to-head`}
|
||||||
w={40}
|
w={40}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<HeadToHeadIcon />
|
<Group
|
||||||
|
style={{ position: "relative", width: 27.5, height: 16 }}
|
||||||
|
>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: "rotate(25deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: "scaleX(-1) rotate(25deg)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import Sheet from "@/components/sheet/sheet";
|
|||||||
import { MatchForm } from "./match-form";
|
import { MatchForm } from "./match-form";
|
||||||
import toast from "@/lib/sonner";
|
import toast from "@/lib/sonner";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
const teamId = (t: Match["home"]): string | undefined =>
|
const teamId = (t: Match["home"]): string | undefined =>
|
||||||
!t ? undefined : typeof t === "string" ? t : t.id;
|
!t ? undefined : typeof t === "string" ? t : t.id;
|
||||||
@@ -27,10 +26,9 @@ const teamName = (t: Match["home"]): string | undefined =>
|
|||||||
interface MatchReportProps {
|
interface MatchReportProps {
|
||||||
match: Match;
|
match: Match;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
source?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchReport: React.FC<MatchReportProps> = ({ match, compact, source }) => {
|
export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const formSheet = useSheet();
|
const formSheet = useSheet();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -131,9 +129,8 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact, source
|
|||||||
|
|
||||||
const openForm = useCallback(() => {
|
const openForm = useCallback(() => {
|
||||||
setAcknowledgedReport(pendingSignature);
|
setAcknowledgedReport(pendingSignature);
|
||||||
track("score_report_opened", source ? { source } : undefined);
|
|
||||||
formSheet.open();
|
formSheet.open();
|
||||||
}, [pendingSignature, formSheet.open, source]);
|
}, [pendingSignature, formSheet.open]);
|
||||||
|
|
||||||
const dismissTakeover = useCallback(() => {
|
const dismissTakeover = useCallback(() => {
|
||||||
setAcknowledgedReport(pendingSignature);
|
setAcknowledgedReport(pendingSignature);
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { AppShell } from '@mantine/core';
|
import { AppShell, Box } from '@mantine/core';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { useLocation } from '@tanstack/react-router';
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import Navbar from './navbar';
|
import Navbar from './navbar';
|
||||||
import Pullable from './pullable';
|
import Pullable from './pullable';
|
||||||
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
|
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
|
||||||
import useRouterConfig from '../hooks/use-router-config';
|
import useRouterConfig from '../hooks/use-router-config';
|
||||||
import Page from '@/components/page';
|
import Page from '@/components/page';
|
||||||
|
import './route-transition.css';
|
||||||
|
|
||||||
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { header, withPadding, fullWidth } = useRouterConfig();
|
const { header, withPadding, fullWidth } = useRouterConfig();
|
||||||
const viewport = useVisualViewportSize();
|
const viewport = useVisualViewportSize();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
id='app-shell'
|
id='app-shell'
|
||||||
layout='alt'
|
layout='alt'
|
||||||
// 0 = header collapse is instant; Mantine's default animates main's padding
|
|
||||||
transitionDuration={0}
|
|
||||||
header={{
|
header={{
|
||||||
// Old installs froze black-translucent at add-to-home-screen time, so
|
// Old installs froze black-translucent at add-to-home-screen time, so
|
||||||
// the webview can extend under the status bar; inset is 0 elsewhere.
|
// the webview can extend under the status bar; inset is 0 elsewhere.
|
||||||
@@ -47,14 +48,16 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
pos='relative'
|
pos='relative'
|
||||||
h='100%'
|
h='100%'
|
||||||
mah='100%'
|
mah='100%'
|
||||||
pb={{ base: 'calc(73px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
pb={{ base: 'calc(65px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
||||||
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
|
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
|
||||||
maw='100dvw'
|
maw='100dvw'
|
||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Pullable>
|
<Pullable>
|
||||||
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||||
|
<Box key={location.pathname} className="flxn-route-enter">
|
||||||
{children}
|
{children}
|
||||||
|
</Box>
|
||||||
</Page>
|
</Page>
|
||||||
</Pullable>
|
</Pullable>
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useLayoutEffect, useRef } from "react";
|
|
||||||
import { useRouterState } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
const TRAVEL_MS = 333;
|
|
||||||
|
|
||||||
export const useNavIndicator = (isMobile: boolean) => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const indicatorRef = useRef<HTMLSpanElement>(null);
|
|
||||||
const shapeRef = useRef<HTMLSpanElement>(null);
|
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
const indicator = indicatorRef.current;
|
|
||||||
const shape = shapeRef.current;
|
|
||||||
if (!container || !indicator || !shape) return;
|
|
||||||
|
|
||||||
let last: number | null = null;
|
|
||||||
|
|
||||||
const place = () => {
|
|
||||||
const active = container.querySelector<HTMLElement>("[data-active]");
|
|
||||||
if (!active) {
|
|
||||||
delete indicator.dataset.placed;
|
|
||||||
last = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = active.offsetLeft;
|
|
||||||
const y = active.offsetTop;
|
|
||||||
const travelled = isMobile ? x : y;
|
|
||||||
const first = indicator.dataset.placed !== "1";
|
|
||||||
|
|
||||||
if (first) indicator.style.transition = "none";
|
|
||||||
indicator.style.setProperty("--nav-x", `${x}px`);
|
|
||||||
indicator.style.setProperty("--nav-y", `${y}px`);
|
|
||||||
indicator.style.setProperty("--nav-w", `${active.offsetWidth}px`);
|
|
||||||
indicator.style.setProperty("--nav-h", `${active.offsetHeight}px`);
|
|
||||||
indicator.dataset.placed = "1";
|
|
||||||
|
|
||||||
if (first) {
|
|
||||||
indicator.getBoundingClientRect();
|
|
||||||
indicator.style.transition = "";
|
|
||||||
} else if (
|
|
||||||
last !== null &&
|
|
||||||
Math.abs(travelled - last) > 1 &&
|
|
||||||
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
||||||
) {
|
|
||||||
const axis = isMobile ? "scaleX" : "scaleY";
|
|
||||||
shape.animate(
|
|
||||||
[
|
|
||||||
{ transform: `${axis}(1)`, offset: 0 },
|
|
||||||
{ transform: `${axis}(0.88)`, offset: 0.4 },
|
|
||||||
{ transform: `${axis}(1)`, offset: 1 },
|
|
||||||
],
|
|
||||||
{ duration: TRAVEL_MS, easing: "ease-in-out" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
last = travelled;
|
|
||||||
};
|
|
||||||
|
|
||||||
place();
|
|
||||||
const observer = new ResizeObserver(place);
|
|
||||||
observer.observe(container);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [pathname, isMobile]);
|
|
||||||
|
|
||||||
return { containerRef, indicatorRef, shapeRef };
|
|
||||||
};
|
|
||||||
@@ -32,19 +32,30 @@ export const NavLink = ({
|
|||||||
<Box
|
<Box
|
||||||
component={Link}
|
component={Link}
|
||||||
to={href}
|
to={href}
|
||||||
className={`flxn-nav-link ${styles.navLinkBox}`}
|
className={styles.navLinkBox}
|
||||||
p={{ base: 0, sm: 8 }}
|
p={{ base: 0, sm: 8 }}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
data-active={isActive || undefined}
|
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={{ base: "column", md: "row" }}
|
direction={{ base: "column", md: "row" }}
|
||||||
align="center"
|
align="center"
|
||||||
gap={{ base: 0, md: "xs" }}
|
gap={{ base: 0, md: "xs" }}
|
||||||
>
|
>
|
||||||
<Icon weight={isActive ? "fill" : "regular"} size={28} />
|
<Icon
|
||||||
<Text visibleFrom="md" ta="center" size="md" fw={isActive ? 800 : 500}>
|
weight={isActive ? "fill" : "regular"}
|
||||||
|
size={28}
|
||||||
|
style={{
|
||||||
|
color: isActive ? "var(--mantine-primary-color-filled)" : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
visibleFrom="md"
|
||||||
|
ta="center"
|
||||||
|
size="md"
|
||||||
|
fw={isActive ? 800 : 500}
|
||||||
|
c={isActive ? "var(--mantine-primary-color-filled)" : undefined}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navLinkBox svg,
|
||||||
|
.navLinkBox p {
|
||||||
|
transition: color 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
.navLinkBox {
|
.navLinkBox {
|
||||||
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
|
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import { useAuth } from "@/contexts/auth-context";
|
|||||||
import { useLinks } from "../hooks/use-links";
|
import { useLinks } from "../hooks/use-links";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useIsPWA } from "@/hooks/use-is-pwa";
|
import { useIsPWA } from "@/hooks/use-is-pwa";
|
||||||
import { useNavIndicator } from "./nav-indicator/use-nav-indicator";
|
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const { user, roles } = useAuth()
|
const { user, roles } = useAuth()
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isPWA = useIsPWA();
|
const isPWA = useIsPWA();
|
||||||
const { containerRef, indicatorRef, shapeRef } = useNavIndicator(isMobile);
|
|
||||||
|
|
||||||
const links = useLinks(user?.id, roles);
|
const links = useLinks(user?.id, roles);
|
||||||
|
|
||||||
@@ -19,16 +17,9 @@ const Navbar = () => {
|
|||||||
? 'max(env(safe-area-inset-bottom, 0px), 0.5rem)'
|
? 'max(env(safe-area-inset-bottom, 0px), 0.5rem)'
|
||||||
: 'env(safe-area-inset-bottom, 0px)';
|
: 'env(safe-area-inset-bottom, 0px)';
|
||||||
|
|
||||||
const indicator = (
|
|
||||||
<span ref={indicatorRef} className="flxn-nav-indicator" aria-hidden="true">
|
|
||||||
<span ref={shapeRef} className="flxn-nav-indicator-shape" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMobile) return (
|
if (isMobile) return (
|
||||||
<Paper className="flxn-nav" component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={bottomOffset} style={{ zIndex: 10 }}>
|
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={bottomOffset} style={{ zIndex: 10 }}>
|
||||||
<Group ref={containerRef} pos='relative' gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
|
<Group gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
|
||||||
{indicator}
|
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<NavLink key={link.href} {...link} />
|
<NavLink key={link.href} {...link} />
|
||||||
))}
|
))}
|
||||||
@@ -36,10 +27,9 @@ const Navbar = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
|
|
||||||
return <AppShell.Navbar className="flxn-nav" p="xs" role='navigation'>
|
return <AppShell.Navbar p="xs" role='navigation'>
|
||||||
<AppShell.Section grow component={ScrollArea}>
|
<AppShell.Section grow component={ScrollArea}>
|
||||||
<Stack ref={containerRef} pos='relative' gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
|
<Stack gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
|
||||||
{indicator}
|
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<NavLink key={link.href} {...link} />
|
<NavLink key={link.href} {...link} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.flxn-route-enter {
|
||||||
|
animation: flxn-route-fade-in 180ms ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flxn-route-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { useAuth } from "@/contexts/auth-context";
|
|
||||||
import { isAdminRole } from "../utils/roles";
|
|
||||||
|
|
||||||
export const useIsAdmin = (): boolean => {
|
|
||||||
const { roles } = useAuth();
|
|
||||||
return isAdminRole(roles);
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { useAuth } from "@/contexts/auth-context";
|
|
||||||
import { isDeveloperRole } from "../utils/roles";
|
|
||||||
|
|
||||||
export const useIsDeveloper = (): boolean => {
|
|
||||||
const { roles } = useAuth();
|
|
||||||
return isDeveloperRole(roles);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { HouseIcon, RankingIcon, ShieldIcon, TrophyIcon, UserCircleIcon } from "@phosphor-icons/react";
|
import { HouseIcon, RankingIcon, ShieldIcon, TrophyIcon, UserCircleIcon } from "@phosphor-icons/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLingui } from "@lingui/react/macro";
|
import { useLingui } from "@lingui/react/macro";
|
||||||
import { isAdminRole } from "../utils/roles";
|
|
||||||
|
|
||||||
export const useLinks = (userId: string | undefined, roles: string[]) => {
|
export const useLinks = (userId: string | undefined, roles: string[]) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -32,7 +31,7 @@ export const useLinks = (userId: string | undefined, roles: string[]) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (isAdminRole(roles)) {
|
if (roles.includes('Admin')) {
|
||||||
links.push({
|
links.push({
|
||||||
label: t`Admin`,
|
label: t`Admin`,
|
||||||
href: '/admin',
|
href: '/admin',
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export const ADMIN_ROLE = "Admin";
|
|
||||||
export const DEVELOPER_ROLE = "Developer";
|
|
||||||
|
|
||||||
export const isDeveloperRole = (roles?: string[] | null): boolean =>
|
|
||||||
!!roles?.includes(DEVELOPER_ROLE);
|
|
||||||
|
|
||||||
export const isAdminRole = (roles?: string[] | null): boolean =>
|
|
||||||
!!roles?.includes(ADMIN_ROLE) || isDeveloperRole(roles);
|
|
||||||
@@ -21,7 +21,7 @@ const useConsumeCode = (onWrongCode: () => void) => {
|
|||||||
} else {
|
} else {
|
||||||
queryClient.setQueryData(playerKeys.auth, response.data);
|
queryClient.setQueryData(playerKeys.auth, response.data);
|
||||||
toast.success(t`Successfully logged in. Welcome back!`);
|
toast.success(t`Successfully logged in. Welcome back!`);
|
||||||
window.location.href = '/';
|
navigate({ to: '/' })
|
||||||
}
|
}
|
||||||
} else if (data.status === 'INCORRECT_USER_INPUT_CODE_ERROR') {
|
} else if (data.status === 'INCORRECT_USER_INPUT_CODE_ERROR') {
|
||||||
onWrongCode();
|
onWrongCode();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const useCreateUser = () => {
|
|||||||
...old,
|
...old,
|
||||||
user: data
|
user: data
|
||||||
}));
|
}));
|
||||||
window.location.href = '/';
|
navigate({ to: '/' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
|
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
|
||||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { Match } from "../types";
|
import { Match } from "../types";
|
||||||
import TeamAvatar from "@/components/team-avatar";
|
import TeamAvatar from "@/components/team-avatar";
|
||||||
@@ -10,7 +10,6 @@ import Sheet from "@/components/sheet/sheet";
|
|||||||
import TeamHeadToHeadSheet from "./team-head-to-head-sheet";
|
import TeamHeadToHeadSheet from "./team-head-to-head-sheet";
|
||||||
import AnimatedScore from "./animated-score";
|
import AnimatedScore from "./animated-score";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
interface MatchCardProps {
|
interface MatchCardProps {
|
||||||
match: Match;
|
match: Match;
|
||||||
@@ -42,10 +41,6 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
|||||||
|
|
||||||
const handleH2HClick = (e: React.MouseEvent) => {
|
const handleH2HClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
track("h2h_viewed", {
|
|
||||||
source: "match_card",
|
|
||||||
context: [match.home?.name, match.away?.name].filter(Boolean).join(" v "),
|
|
||||||
});
|
|
||||||
h2hSheet.open();
|
h2hSheet.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +89,26 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
|||||||
aria-label={t`View head-to-head`}
|
aria-label={t`View head-to-head`}
|
||||||
w={40}
|
w={40}
|
||||||
>
|
>
|
||||||
<HeadToHeadIcon />
|
<Group style={{ position: 'relative', width: 27.5, height: 16 }}>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: 'rotate(25deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: 'scaleX(-1) rotate(25deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -204,7 +218,7 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<EmojiBar matchId={match.id} source="match_card" />
|
<EmojiBar matchId={match.id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MantineSpacing, Stack, Text } from "@mantine/core";
|
import { Stack, Text } from "@mantine/core";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Match } from "../types";
|
import { Match } from "../types";
|
||||||
import MatchCard from "./match-card";
|
import MatchCard from "./match-card";
|
||||||
@@ -7,10 +7,9 @@ import { Trans } from "@lingui/react/macro";
|
|||||||
interface MatchListProps {
|
interface MatchListProps {
|
||||||
matches: Match[];
|
matches: Match[];
|
||||||
hideH2H?: boolean;
|
hideH2H?: boolean;
|
||||||
p?: MantineSpacing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MatchList = ({ matches, hideH2H = false, p = "md" }: MatchListProps) => {
|
const MatchList = ({ matches, hideH2H = false }: MatchListProps) => {
|
||||||
const filteredMatches = useMemo(
|
const filteredMatches = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...(matches ?? [])]
|
[...(matches ?? [])]
|
||||||
@@ -28,7 +27,7 @@ const MatchList = ({ matches, hideH2H = false, p = "md" }: MatchListProps) => {
|
|||||||
const isRegional = filteredMatches[0]?.tournament?.regional;
|
const isRegional = filteredMatches[0]?.tournament?.regional;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p={p} gap="sm">
|
<Stack p="md" gap="sm">
|
||||||
{isRegional && (
|
{isRegional && (
|
||||||
<Text size="xs" c="dimmed" ta="center" px="md">
|
<Text size="xs" c="dimmed" ta="center" px="md">
|
||||||
<Trans>Matches for regionals are unordered</Trans>
|
<Trans>Matches for regionals are unordered</Trans>
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ const TeamHeadToHeadContent = ({ team1, team2, isOpen = true }: TeamHeadToHeadSh
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600} px="md">
|
||||||
<Trans>Match History (<Plural value={totalMatches} one="# match" other="# matches" />)</Trans>
|
<Trans>Match History (<Plural value={totalMatches} one="# match" other="# matches" />)</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<MatchList matches={matches} hideH2H p={0} />
|
<MatchList matches={matches} hideH2H />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Match, MatchInput } from "@/features/matches/types";
|
|||||||
import { emitServerEvent } from "@/lib/events/emitter";
|
import { emitServerEvent } from "@/lib/events/emitter";
|
||||||
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
|
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||||
import { PlayerInfo } from "../players/types";
|
import { PlayerInfo } from "../players/types";
|
||||||
|
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||||
|
|
||||||
const orderedTeamsSchema = z.object({
|
const orderedTeamsSchema = z.object({
|
||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
@@ -17,7 +18,7 @@ const orderedTeamsSchema = z.object({
|
|||||||
|
|
||||||
export const generateTournamentBracket = createServerFn()
|
export const generateTournamentBracket = createServerFn()
|
||||||
.validator(orderedTeamsSchema)
|
.validator(orderedTeamsSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
|
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
logger.info("Generating tournament bracket", {
|
logger.info("Generating tournament bracket", {
|
||||||
@@ -140,7 +141,7 @@ export const generateTournamentBracket = createServerFn()
|
|||||||
|
|
||||||
export const startMatch = createServerFn()
|
export const startMatch = createServerFn()
|
||||||
.validator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
logger.info("Starting match", data);
|
logger.info("Starting match", data);
|
||||||
@@ -169,7 +170,7 @@ export const populateKnockoutBracket = createServerFn()
|
|||||||
.validator(z.object({
|
.validator(z.object({
|
||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { tournamentId } }) =>
|
.handler(async ({ data: { tournamentId } }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const tournament = await pbAdmin.getTournament(tournamentId);
|
const tournament = await pbAdmin.getTournament(tournamentId);
|
||||||
@@ -800,7 +801,7 @@ const endMatchSchema = z.object({
|
|||||||
});
|
});
|
||||||
export const endMatch = createServerFn()
|
export const endMatch = createServerFn()
|
||||||
.validator(endMatchSchema)
|
.validator(endMatchSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
|
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count } }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
logger.info("Ending match", matchId);
|
logger.info("Ending match", matchId);
|
||||||
@@ -826,7 +827,7 @@ const reportScoreSchema = z.object({
|
|||||||
|
|
||||||
export const reportMatchScore = createServerFn()
|
export const reportMatchScore = createServerFn()
|
||||||
.validator(reportScoreSchema)
|
.validator(reportScoreSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count }, context }) =>
|
.handler(async ({ data: { matchId, home_cups, away_cups, ot_count }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||||
@@ -889,7 +890,7 @@ const matchIdSchema = z.object({ matchId: z.string() });
|
|||||||
|
|
||||||
export const confirmMatchScore = createServerFn()
|
export const confirmMatchScore = createServerFn()
|
||||||
.validator(matchIdSchema)
|
.validator(matchIdSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { matchId }, context }) =>
|
.handler(async ({ data: { matchId }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||||
@@ -928,7 +929,7 @@ export const confirmMatchScore = createServerFn()
|
|||||||
|
|
||||||
export const clearMatchReport = createServerFn()
|
export const clearMatchReport = createServerFn()
|
||||||
.validator(matchIdSchema)
|
.validator(matchIdSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { matchId }, context }) =>
|
.handler(async ({ data: { matchId }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
const player = await pbAdmin.getPlayerByAuthId(context.userAuthId);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { matchInputSchema } from "./types";
|
|
||||||
|
|
||||||
describe("matchInputSchema.partial()", () => {
|
|
||||||
it("does not inject keys the caller did not send", () => {
|
|
||||||
const parsed = matchInputSchema.partial().parse({ home_cups: 7 });
|
|
||||||
expect(parsed).toEqual({ home_cups: 7 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -42,20 +42,20 @@ export interface Match {
|
|||||||
export const matchInputSchema = z.object({
|
export const matchInputSchema = z.object({
|
||||||
order: z.number().int().min(1).optional(),
|
order: z.number().int().min(1).optional(),
|
||||||
lid: z.number().int().min(1),
|
lid: z.number().int().min(1),
|
||||||
reset: z.boolean().optional(),
|
reset: z.boolean().optional().default(false),
|
||||||
round: z.number().int().min(1),
|
round: z.number().int().min(1),
|
||||||
home_cups: z.number().int().min(0).optional(),
|
home_cups: z.number().int().min(0).optional().default(0),
|
||||||
away_cups: z.number().int().min(0).optional(),
|
away_cups: z.number().int().min(0).optional().default(0),
|
||||||
ot_count: z.number().int().min(0).optional(),
|
ot_count: z.number().int().min(0).optional().default(0),
|
||||||
start_time: z.iso.datetime().optional(),
|
start_time: z.iso.datetime().optional(),
|
||||||
end_time: z.iso.datetime().optional(),
|
end_time: z.iso.datetime().optional(),
|
||||||
bye: z.boolean().optional(),
|
bye: z.boolean().optional().default(false),
|
||||||
home_from_lid: z.number().int().min(1).optional(),
|
home_from_lid: z.number().int().min(1).optional(),
|
||||||
away_from_lid: z.number().int().min(1).optional(),
|
away_from_lid: z.number().int().min(1).optional(),
|
||||||
home_from_loser: z.boolean().optional(),
|
home_from_loser: z.boolean().optional().default(false),
|
||||||
away_from_loser: z.boolean().optional(),
|
away_from_loser: z.boolean().optional().default(false),
|
||||||
is_losers_bracket: z.boolean().optional(),
|
is_losers_bracket: z.boolean().optional().default(false),
|
||||||
status: z.enum(["tbd", "ready", "started", "ended"]).optional(),
|
status: z.enum(["tbd", "ready", "started", "ended"]).optional().default("tbd"),
|
||||||
tournament: z.string().min(1),
|
tournament: z.string().min(1),
|
||||||
home: z.string().min(1).nullable().optional(),
|
home: z.string().min(1).nullable().optional(),
|
||||||
away: z.string().min(1).nullable().optional(),
|
away: z.string().min(1).nullable().optional(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Stack, Text, TextInput, Box, Paper, Group, Divider, ActionIcon, Button } from "@mantine/core";
|
import { Stack, Text, TextInput, Box, Paper, Group, Divider, ActionIcon, Button } from "@mantine/core";
|
||||||
import { useState, useMemo, useRef } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { MagnifyingGlassIcon, XIcon, ArrowRightIcon, PlusIcon } from "@phosphor-icons/react";
|
import { MagnifyingGlassIcon, XIcon, ArrowRightIcon, PlusIcon } from "@phosphor-icons/react";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { useAllPlayerStats } from "../queries";
|
import { useAllPlayerStats } from "../queries";
|
||||||
@@ -8,7 +8,6 @@ import { useSheet } from "@/hooks/use-sheet";
|
|||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
|
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
|
||||||
import PlayerAvatar from "@/components/player-avatar";
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
const LeagueHeadToHead = () => {
|
const LeagueHeadToHead = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -16,7 +15,6 @@ const LeagueHeadToHead = () => {
|
|||||||
const [player1Id, setPlayer1Id] = useState<string | null>(null);
|
const [player1Id, setPlayer1Id] = useState<string | null>(null);
|
||||||
const [player2Id, setPlayer2Id] = useState<string | null>(null);
|
const [player2Id, setPlayer2Id] = useState<string | null>(null);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const searchedRef = useRef(false);
|
|
||||||
const { data: allPlayerStats } = useAllPlayerStats();
|
const { data: allPlayerStats } = useAllPlayerStats();
|
||||||
const h2hSheet = useSheet();
|
const h2hSheet = useSheet();
|
||||||
|
|
||||||
@@ -53,12 +51,6 @@ const LeagueHeadToHead = () => {
|
|||||||
setPlayer1Id(playerId);
|
setPlayer1Id(playerId);
|
||||||
} else {
|
} else {
|
||||||
setPlayer2Id(playerId);
|
setPlayer2Id(playerId);
|
||||||
const opponent =
|
|
||||||
allPlayerStats?.find((p) => p.player_id === playerId)?.player_name || "";
|
|
||||||
track("h2h_viewed", {
|
|
||||||
source: "stats",
|
|
||||||
context: [player1Name, opponent].filter(Boolean).join(" v "),
|
|
||||||
});
|
|
||||||
h2hSheet.open();
|
h2hSheet.open();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -136,14 +128,7 @@ const LeagueHeadToHead = () => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t`Search players`}
|
placeholder={t`Search players`}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
const value = e.currentTarget.value;
|
|
||||||
if (value && !searchedRef.current) {
|
|
||||||
searchedRef.current = true;
|
|
||||||
track("player_searched");
|
|
||||||
}
|
|
||||||
setSearch(value);
|
|
||||||
}}
|
|
||||||
leftSection={<MagnifyingGlassIcon size={16} />}
|
leftSection={<MagnifyingGlassIcon size={16} />}
|
||||||
size="md"
|
size="md"
|
||||||
px="md"
|
px="md"
|
||||||
|
|||||||
@@ -260,10 +260,10 @@ const PlayerHeadToHeadContent = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600} px="md">
|
||||||
<Trans>Match History ({totalMatches})</Trans>
|
<Trans>Match History ({totalMatches})</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<MatchList matches={matches} hideH2H p={0} />
|
<MatchList matches={matches} hideH2H />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { useNavigate } from "@tanstack/react-router";
|
|||||||
import { useAllPlayerStats } from "../queries";
|
import { useAllPlayerStats } from "../queries";
|
||||||
import { PlayerListItemSkeleton } from "./player-stats-table-skeleton";
|
import { PlayerListItemSkeleton } from "./player-stats-table-skeleton";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
type SortKey = keyof PlayerStats | "mmr";
|
type SortKey = keyof PlayerStats | "mmr";
|
||||||
type SortDirection = "asc" | "desc";
|
type SortDirection = "asc" | "desc";
|
||||||
@@ -178,7 +177,6 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const deferredSearch = useDeferredValue(search);
|
const deferredSearch = useDeferredValue(search);
|
||||||
const searchedRef = useRef(false);
|
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||||
key: "mmr" as SortKey,
|
key: "mmr" as SortKey,
|
||||||
direction: "desc",
|
direction: "desc",
|
||||||
@@ -293,10 +291,10 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const handleSort = (key: SortKey) => {
|
const handleSort = (key: SortKey) => {
|
||||||
const direction =
|
setSortConfig((prev) => ({
|
||||||
sortConfig.key === key && sortConfig.direction === "desc" ? "asc" : "desc";
|
key,
|
||||||
track("stats_sorted", { value: key, context: direction });
|
direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc",
|
||||||
setSortConfig({ key, direction });
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSortIcon = (key: SortKey) => {
|
const getSortIcon = (key: SortKey) => {
|
||||||
@@ -330,14 +328,7 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t`Search players`}
|
placeholder={t`Search players`}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
const value = e.currentTarget.value;
|
|
||||||
if (value && !searchedRef.current) {
|
|
||||||
searchedRef.current = true;
|
|
||||||
track("player_searched");
|
|
||||||
}
|
|
||||||
setSearch(value);
|
|
||||||
}}
|
|
||||||
leftSection={<MagnifyingGlassIcon size={16} />}
|
leftSection={<MagnifyingGlassIcon size={16} />}
|
||||||
size="md"
|
size="md"
|
||||||
px="md"
|
px="md"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import Sheet from "@/components/sheet/sheet";
|
import Sheet from "@/components/sheet/sheet";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { Flex, Title, ActionIcon, Stack, Button } from "@mantine/core";
|
import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
|
||||||
import { PencilIcon } from "@phosphor-icons/react";
|
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
||||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLingui } from "@lingui/react/macro";
|
import { useLingui } from "@lingui/react/macro";
|
||||||
import NameUpdateForm from "./name-form";
|
import NameUpdateForm from "./name-form";
|
||||||
@@ -10,7 +9,6 @@ import PlayerAvatar from "@/components/player-avatar";
|
|||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import { Player } from "../../types";
|
import { Player } from "../../types";
|
||||||
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
|
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
player: Player;
|
player: Player;
|
||||||
@@ -55,17 +53,30 @@ const Header = ({ player }: HeaderProps) => {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={() => {
|
onClick={h2hSheet.open}
|
||||||
track("h2h_viewed", {
|
|
||||||
source: "profile",
|
|
||||||
context: [player.first_name, player.last_name].filter(Boolean).join(" "),
|
|
||||||
});
|
|
||||||
h2hSheet.open();
|
|
||||||
}}
|
|
||||||
w={40}
|
w={40}
|
||||||
display={!owner ? 'block' : 'none'}
|
display={!owner ? 'block' : 'none'}
|
||||||
>
|
>
|
||||||
<HeadToHeadIcon />
|
<Box style={{ position: 'relative', width: 27.5, height: 16 }}>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: 'rotate(25deg)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FootballHelmetIcon
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
transform: 'scaleX(-1) rotate(25deg)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Stack, Text, Divider, Group, Button } from "@mantine/core";
|
import { Box, Stack, Text, Divider, Group, Button } from "@mantine/core";
|
||||||
import { Suspense, useState, useDeferredValue, useRef } from "react";
|
import { Suspense, useState, useDeferredValue } from "react";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
@@ -10,7 +10,6 @@ import StatsOverview, { StatsSkeleton } from "@/components/stats-overview";
|
|||||||
import MatchList from "@/features/matches/components/match-list";
|
import MatchList from "@/features/matches/components/match-list";
|
||||||
import BadgeShowcase from "@/features/badges/components/badge-showcase";
|
import BadgeShowcase from "@/features/badges/components/badge-showcase";
|
||||||
import BadgeShowcaseSkeleton from "@/features/badges/components/badge-showcase-skeleton";
|
import BadgeShowcaseSkeleton from "@/features/badges/components/badge-showcase-skeleton";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,22 +20,14 @@ const StatsWithFilter = ({ id }: { id: string }) => {
|
|||||||
const deferredViewType = useDeferredValue(viewType);
|
const deferredViewType = useDeferredValue(viewType);
|
||||||
const isStale = viewType !== deferredViewType;
|
const isStale = viewType !== deferredViewType;
|
||||||
|
|
||||||
const viewTypeRef = useRef(viewType);
|
|
||||||
const changeViewType = (next: typeof viewType) => {
|
|
||||||
if (viewTypeRef.current === next) return;
|
|
||||||
viewTypeRef.current = next;
|
|
||||||
track("stats_filtered", { value: next, source: "profile" });
|
|
||||||
setViewType(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group gap="xs" px="md" justify="space-between" align="center">
|
<Group gap="xs" px="md" justify="space-between" align="center">
|
||||||
<Text size="md" fw={700}><Trans>Statistics</Trans></Text>
|
<Text size="md" fw={700}><Trans>Statistics</Trans></Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Button variant={viewType === 'all' ? 'filled' : 'light'} size="compact-xs" onClick={() => changeViewType('all')}><Trans>All</Trans></Button>
|
<Button variant={viewType === 'all' ? 'filled' : 'light'} size="compact-xs" onClick={() => setViewType('all')}><Trans>All</Trans></Button>
|
||||||
<Button variant={viewType === 'mainline' ? 'filled' : 'light'} size="compact-xs" onClick={() => changeViewType('mainline')}><Trans>Mainline</Trans></Button>
|
<Button variant={viewType === 'mainline' ? 'filled' : 'light'} size="compact-xs" onClick={() => setViewType('mainline')}><Trans>Mainline</Trans></Button>
|
||||||
<Button variant={viewType === 'regional' ? 'filled' : 'light'} size="compact-xs" onClick={() => changeViewType('regional')}><Trans>Regional</Trans></Button>
|
<Button variant={viewType === 'regional' ? 'filled' : 'light'} size="compact-xs" onClick={() => setViewType('regional')}><Trans>Regional</Trans></Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 150ms' }}>
|
<Box style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 150ms' }}>
|
||||||
@@ -99,12 +90,7 @@ const Profile = ({ id }: ProfileProps) => {
|
|||||||
<>
|
<>
|
||||||
<Header player={player} />
|
<Header player={player} />
|
||||||
<Box mt="lg">
|
<Box mt="lg">
|
||||||
<SwipeableTabs
|
<SwipeableTabs tabs={tabs} />
|
||||||
tabs={tabs}
|
|
||||||
onTabChange={(_, tab) => {
|
|
||||||
if (tab.value === "matches") track("match_history_viewed", { source: "profile" });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|||||||
import { Logger } from "@/lib/logger";
|
import { Logger } from "@/lib/logger";
|
||||||
import { getRequest } from "@tanstack/react-start/server";
|
import { getRequest } from "@tanstack/react-start/server";
|
||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
|
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||||
|
|
||||||
const logger = new Logger('Players');
|
const logger = new Logger('Players');
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export const getPlayer = createServerFn()
|
|||||||
|
|
||||||
export const updatePlayer = createServerFn()
|
export const updatePlayer = createServerFn()
|
||||||
.validator(playerUpdateSchema)
|
.validator(playerUpdateSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userAuthId = context.userAuthId;
|
const userAuthId = context.userAuthId;
|
||||||
@@ -83,7 +84,7 @@ export const updatePlayer = createServerFn()
|
|||||||
|
|
||||||
export const createPlayer = createServerFn()
|
export const createPlayer = createServerFn()
|
||||||
.validator(playerInputSchema)
|
.validator(playerInputSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userAuthId = context.userAuthId;
|
const userAuthId = context.userAuthId;
|
||||||
@@ -116,7 +117,7 @@ export const createPlayer = createServerFn()
|
|||||||
|
|
||||||
export const associatePlayer = createServerFn()
|
export const associatePlayer = createServerFn()
|
||||||
.validator(z.string())
|
.validator(z.string())
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ context, data }) =>
|
.handler(async ({ context, data }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userAuthId = context.userAuthId;
|
const userAuthId = context.userAuthId;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
|
import { CaretLeftIcon, CaretRightIcon, InfoIcon } from "@phosphor-icons/react";
|
||||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
|
||||||
import WizardOrbIcon from "@/components/wizard-orb-icon";
|
import WizardOrbIcon from "@/components/wizard-orb-icon";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
@@ -27,7 +26,6 @@ import {
|
|||||||
import { PredictionBracket } from "./prediction-bracket";
|
import { PredictionBracket } from "./prediction-bracket";
|
||||||
import { MatchupSheet } from "./matchup-sheet";
|
import { MatchupSheet } from "./matchup-sheet";
|
||||||
import { WinnerSelector } from "./winner-selector";
|
import { WinnerSelector } from "./winner-selector";
|
||||||
import { track } from "@/lib/telemetry/client";
|
|
||||||
|
|
||||||
interface PredictionEditorProps {
|
interface PredictionEditorProps {
|
||||||
tournament: Tournament;
|
tournament: Tournament;
|
||||||
@@ -217,19 +215,11 @@ export const PredictionEditor: React.FC<PredictionEditorProps> = ({
|
|||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => {
|
onClick={matchupSheet.open}
|
||||||
if (activeResolved?.home && activeResolved?.away) {
|
|
||||||
track("h2h_viewed", {
|
|
||||||
source: "predictions",
|
|
||||||
context: `${activeResolved.home.name} v ${activeResolved.away.name}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
matchupSheet.open();
|
|
||||||
}}
|
|
||||||
disabled={!activeResolved?.home && !activeResolved?.away}
|
disabled={!activeResolved?.home && !activeResolved?.away}
|
||||||
aria-label={t`Matchup details`}
|
aria-label={t`Matchup details`}
|
||||||
>
|
>
|
||||||
<HeadToHeadIcon />
|
<InfoIcon size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" align="center" wrap="nowrap">
|
<Group gap="sm" align="center" wrap="nowrap">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user