Compare commits
25 Commits
upgrade
...
2dfb7c63d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dfb7c63d3 | ||
|
|
03b2b54c1f | ||
|
|
0910f11228 | ||
|
|
a376f98fe7 | ||
|
|
1f4f66f8c5 | ||
|
|
5729dab35f | ||
|
|
c05fd5dc6d | ||
|
|
b9a42b4743 | ||
|
|
74e28cc2ac | ||
|
|
adf304b1e0 | ||
|
|
d18cdce15f | ||
|
|
aa87a9da5b | ||
|
|
6224404aa9 | ||
|
|
654041b6b6 | ||
|
|
ce29c41bf3 | ||
|
|
63ea515a31 | ||
|
|
8b1bbe213d | ||
|
|
ed538b7373 | ||
|
|
03e3bbcbc0 | ||
|
|
baf75eddba | ||
|
|
5094933302 | ||
|
|
9564b46d45 | ||
|
|
ece5094f13 | ||
|
|
cfe1ee7171 | ||
|
|
3a41609a91 |
108
pb_migrations/1759244692_created_activities.js
Normal file
108
pb_migrations/1759244692_created_activities.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/// <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": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json4225120046",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "arguments",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_3072146508",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2551806565",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "player",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3293145029",
|
||||||
|
"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_1262591861",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "activities",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1262591861");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
})
|
||||||
27
pb_migrations/1759245857_updated_activities.js
Normal file
27
pb_migrations/1759245857_updated_activities.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(5, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number2254405824",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "duration",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("number2254405824")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
43
pb_migrations/1759246171_updated_activities.js
Normal file
43
pb_migrations/1759246171_updated_activities.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(6, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool1862328242",
|
||||||
|
"name": "success",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(7, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1574812785",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "error",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1262591861")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("bool1862328242")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text1574812785")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
27
pb_migrations/1759340868_updated_badges.js
Normal file
27
pb_migrations/1759340868_updated_badges.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1340419796")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(7, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4113142680",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "order",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1340419796")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("number4113142680")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
46
pb_migrations/1759344923_updated_players.js
Normal file
46
pb_migrations/1759344923_updated_players.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("relation2029409178")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("relation2813965191")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(5, new Field({
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2029409178",
|
||||||
|
"maxSelect": 999,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "badges",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(6, new Field({
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2813965191",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "featured_badge",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
187
pb_migrations/1759344931_deleted_player_badges_view.js
Normal file
187
pb_migrations/1759344931_deleted_player_badges_view.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_5062686152");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = new Collection({
|
||||||
|
"createRule": null,
|
||||||
|
"deleteRule": null,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_3072146508",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2582050271",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "player_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation4154639100",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "badge_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_GhrR",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "badge_name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_DEaW",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "badge_description",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_MHmw",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "badge_type",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"tournament_participation",
|
||||||
|
"tournament_placement",
|
||||||
|
"performance",
|
||||||
|
"overtime",
|
||||||
|
"match_milestone"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_11YE",
|
||||||
|
"max": 50,
|
||||||
|
"min": 0,
|
||||||
|
"name": "badge_icon",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_qAJu",
|
||||||
|
"max": 50,
|
||||||
|
"min": 0,
|
||||||
|
"name": "badge_color",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_giOf",
|
||||||
|
"name": "is_progressive",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json3212413036",
|
||||||
|
"maxSize": 1,
|
||||||
|
"name": "current_progress",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json4171899439",
|
||||||
|
"maxSize": 1,
|
||||||
|
"name": "target_progress",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json3435813110",
|
||||||
|
"maxSize": 1,
|
||||||
|
"name": "is_earned",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "_clone_Q7lC",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "earned_at",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_5062686152",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "player_badges_view",
|
||||||
|
"system": false,
|
||||||
|
"type": "view",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewQuery": "\n SELECT\n (p.id || '_' || b.id) as id,\n p.id as player_id,\n b.id as badge_id,\n b.name as badge_name,\n b.description as badge_description,\n b.type as badge_type,\n b.icon as badge_icon,\n b.color as badge_color,\n b.is_progressive,\n COALESCE(pbp.current_progress, 0) as current_progress,\n COALESCE(pbp.target_progress, b.progress_target, 1) as target_progress,\n COALESCE(pbp.is_earned, false) as is_earned,\n pbp.earned_at\n FROM players p\n CROSS JOIN badges b\n LEFT JOIN player_badge_progress pbp ON pbp.player_id = p.id AND pbp.badge_id = b.id\n ",
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
})
|
||||||
129
pb_migrations/1759344938_deleted_player_badge_progress.js
Normal file
129
pb_migrations/1759344938_deleted_player_badge_progress.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_4251874343");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
}, (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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "pbc_3072146508",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2847519201",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 1,
|
||||||
|
"name": "player_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation3948571039",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 1,
|
||||||
|
"name": "badge_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1847293057",
|
||||||
|
"max": null,
|
||||||
|
"min": 0,
|
||||||
|
"name": "current_progress",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number2948571040",
|
||||||
|
"max": null,
|
||||||
|
"min": 1,
|
||||||
|
"name": "target_progress",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool3049672141",
|
||||||
|
"name": "is_earned",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date1150773242",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "earned_at",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_4251874343",
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX `idx_unique_player_badge` ON `player_badge_progress` (`player_id`, `badge_id`)"
|
||||||
|
],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "player_badge_progress",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
})
|
||||||
173
pb_migrations/1759344944_deleted_badges.js
Normal file
173
pb_migrations/1759344944_deleted_badges.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1340419796");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
}, (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": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1843675174",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "description",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "select4029814376",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "type",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"tournament_participation",
|
||||||
|
"tournament_placement",
|
||||||
|
"performance",
|
||||||
|
"overtime",
|
||||||
|
"match_milestone"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json1578432567",
|
||||||
|
"maxSize": 2000000,
|
||||||
|
"name": "criteria",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3928475610",
|
||||||
|
"max": 50,
|
||||||
|
"min": 0,
|
||||||
|
"name": "icon",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1847293056",
|
||||||
|
"max": 50,
|
||||||
|
"min": 0,
|
||||||
|
"name": "color",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4113142680",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "order",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2847519203",
|
||||||
|
"name": "is_progressive",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number2948571038",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "progress_target",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_1340419796",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "badges",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
})
|
||||||
145
pb_migrations/1759345060_created_badges.js
Normal file
145
pb_migrations/1759345060_created_badges.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/// <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": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2324736937",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "key",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1843675174",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "description",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json3055524737",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "criteria",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "select2363381545",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "type",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"manual",
|
||||||
|
"match",
|
||||||
|
"tournament"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool3646955747",
|
||||||
|
"name": "progressive",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4113142680",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "order",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_1340419796",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "badges",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1340419796");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
})
|
||||||
104
pb_migrations/1759345122_created_player_badge_progress.js
Normal file
104
pb_migrations/1759345122_created_player_badge_progress.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/// <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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation4277159965",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "badge",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_3072146508",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2551806565",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "player",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number570552902",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "progress",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2625885481",
|
||||||
|
"name": "earned",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_1063824264",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "player_badge_progress",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1063824264");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
})
|
||||||
20
pb_migrations/1759345318_updated_player_badge_progress.js
Normal file
20
pb_migrations/1759345318_updated_player_badge_progress.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1063824264")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"name": "badge_progress"
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_1063824264")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"name": "player_badge_progress"
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -330,6 +330,8 @@ async function startServer() {
|
|||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: PORT,
|
port: PORT,
|
||||||
|
|
||||||
|
idleTimeout: 255,
|
||||||
|
|
||||||
routes: {
|
routes: {
|
||||||
// Serve static assets (preloaded or on-demand)
|
// Serve static assets (preloaded or on-demand)
|
||||||
...routes,
|
...routes,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { Route as AuthedTournamentsTournamentIdRouteImport } from './routes/_aut
|
|||||||
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 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 AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||||
@@ -161,6 +162,11 @@ const AuthedAdminPreviewRoute = AuthedAdminPreviewRouteImport.update({
|
|||||||
path: '/preview',
|
path: '/preview',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedAdminBadgesRoute = AuthedAdminBadgesRouteImport.update({
|
||||||
|
id: '/badges',
|
||||||
|
path: '/badges',
|
||||||
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
|
} as any)
|
||||||
const AuthedAdminTournamentsIndexRoute =
|
const AuthedAdminTournamentsIndexRoute =
|
||||||
AuthedAdminTournamentsIndexRouteImport.update({
|
AuthedAdminTournamentsIndexRouteImport.update({
|
||||||
id: '/tournaments/',
|
id: '/tournaments/',
|
||||||
@@ -206,6 +212,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/settings': typeof AuthedSettingsRoute
|
'/settings': typeof AuthedSettingsRoute
|
||||||
'/stats': typeof AuthedStatsRoute
|
'/stats': typeof AuthedStatsRoute
|
||||||
'/': typeof AuthedIndexRoute
|
'/': typeof AuthedIndexRoute
|
||||||
|
'/admin/badges': typeof AuthedAdminBadgesRoute
|
||||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
@@ -236,6 +243,7 @@ export interface FileRoutesByTo {
|
|||||||
'/settings': typeof AuthedSettingsRoute
|
'/settings': typeof AuthedSettingsRoute
|
||||||
'/stats': typeof AuthedStatsRoute
|
'/stats': typeof AuthedStatsRoute
|
||||||
'/': typeof AuthedIndexRoute
|
'/': typeof AuthedIndexRoute
|
||||||
|
'/admin/badges': typeof AuthedAdminBadgesRoute
|
||||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
@@ -269,6 +277,7 @@ export interface FileRoutesById {
|
|||||||
'/_authed/settings': typeof AuthedSettingsRoute
|
'/_authed/settings': typeof AuthedSettingsRoute
|
||||||
'/_authed/stats': typeof AuthedStatsRoute
|
'/_authed/stats': typeof AuthedStatsRoute
|
||||||
'/_authed/': typeof AuthedIndexRoute
|
'/_authed/': typeof AuthedIndexRoute
|
||||||
|
'/_authed/admin/badges': typeof AuthedAdminBadgesRoute
|
||||||
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
||||||
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||||
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
'/_authed/teams/$teamId': typeof AuthedTeamsTeamIdRoute
|
||||||
@@ -302,6 +311,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings'
|
| '/settings'
|
||||||
| '/stats'
|
| '/stats'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/admin/badges'
|
||||||
| '/admin/preview'
|
| '/admin/preview'
|
||||||
| '/profile/$playerId'
|
| '/profile/$playerId'
|
||||||
| '/teams/$teamId'
|
| '/teams/$teamId'
|
||||||
@@ -332,6 +342,7 @@ export interface FileRouteTypes {
|
|||||||
| '/settings'
|
| '/settings'
|
||||||
| '/stats'
|
| '/stats'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/admin/badges'
|
||||||
| '/admin/preview'
|
| '/admin/preview'
|
||||||
| '/profile/$playerId'
|
| '/profile/$playerId'
|
||||||
| '/teams/$teamId'
|
| '/teams/$teamId'
|
||||||
@@ -364,6 +375,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/settings'
|
| '/_authed/settings'
|
||||||
| '/_authed/stats'
|
| '/_authed/stats'
|
||||||
| '/_authed/'
|
| '/_authed/'
|
||||||
|
| '/_authed/admin/badges'
|
||||||
| '/_authed/admin/preview'
|
| '/_authed/admin/preview'
|
||||||
| '/_authed/profile/$playerId'
|
| '/_authed/profile/$playerId'
|
||||||
| '/_authed/teams/$teamId'
|
| '/_authed/teams/$teamId'
|
||||||
@@ -576,6 +588,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
|
preLoaderRoute: typeof AuthedAdminPreviewRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
|
'/_authed/admin/badges': {
|
||||||
|
id: '/_authed/admin/badges'
|
||||||
|
path: '/badges'
|
||||||
|
fullPath: '/admin/badges'
|
||||||
|
preLoaderRoute: typeof AuthedAdminBadgesRouteImport
|
||||||
|
parentRoute: typeof AuthedAdminRoute
|
||||||
|
}
|
||||||
'/_authed/admin/tournaments/': {
|
'/_authed/admin/tournaments/': {
|
||||||
id: '/_authed/admin/tournaments/'
|
id: '/_authed/admin/tournaments/'
|
||||||
path: '/tournaments'
|
path: '/tournaments'
|
||||||
@@ -622,6 +641,7 @@ declare module '@tanstack/react-router' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AuthedAdminRouteChildren {
|
interface AuthedAdminRouteChildren {
|
||||||
|
AuthedAdminBadgesRoute: typeof AuthedAdminBadgesRoute
|
||||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||||
@@ -631,6 +651,7 @@ interface AuthedAdminRouteChildren {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
||||||
|
AuthedAdminBadgesRoute: AuthedAdminBadgesRoute,
|
||||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const Route = createRootRouteWithContext<{
|
|||||||
{
|
{
|
||||||
name: "viewport",
|
name: "viewport",
|
||||||
content:
|
content:
|
||||||
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=overlays-content",
|
"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, interactive-widget=resizes-content",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
@@ -122,8 +122,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
{...mantineHtmlProps}
|
{...mantineHtmlProps}
|
||||||
style={{
|
style={{
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
overflowY: "hidden",
|
height: "100%",
|
||||||
position: "fixed",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -135,9 +134,10 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
<body
|
<body
|
||||||
style={{
|
style={{
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
overflowY: "hidden",
|
height: "100%",
|
||||||
position: "fixed",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="app">{children}</div>
|
<div className="app">{children}</div>
|
||||||
|
|||||||
10
src/app/routes/_authed/admin/badges.tsx
Normal file
10
src/app/routes/_authed/admin/badges.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import AwardBadges from "@/features/admin/components/award-badges";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authed/admin/badges")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <AwardBadges />;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { badgeKeys, badgeQueries } from "@/features/badges/queries";
|
||||||
import Profile from "@/features/players/components/profile";
|
import Profile from "@/features/players/components/profile";
|
||||||
import HeaderSkeleton from "@/features/players/components/profile/header-skeleton";
|
import HeaderSkeleton from "@/features/players/components/profile/header-skeleton";
|
||||||
import ProfileSkeleton from "@/features/players/components/profile/skeleton";
|
import ProfileSkeleton from "@/features/players/components/profile/skeleton";
|
||||||
@@ -24,6 +25,14 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
|
|||||||
queryClient,
|
queryClient,
|
||||||
playerQueries.matches(params.playerId)
|
playerQueries.matches(params.playerId)
|
||||||
),
|
),
|
||||||
|
prefetchServerQuery(
|
||||||
|
queryClient,
|
||||||
|
playerQueries.stats(params.playerId)
|
||||||
|
),
|
||||||
|
prefetchServerQuery(
|
||||||
|
queryClient,
|
||||||
|
badgeQueries.playerBadges(params.playerId)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
loader: ({ params, context }) => ({
|
loader: ({ params, context }) => ({
|
||||||
@@ -34,7 +43,7 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
|
|||||||
context?.auth.user.id === params.playerId ? "/settings" : undefined,
|
context?.auth.user.id === params.playerId ? "/settings" : undefined,
|
||||||
},
|
},
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
refresh: [playerKeys.details(params.playerId), playerKeys.matches(params.playerId), playerKeys.stats(params.playerId)],
|
refresh: [playerKeys.details(params.playerId), playerKeys.matches(params.playerId), playerKeys.stats(params.playerId), badgeKeys.playerBadges(params.playerId)],
|
||||||
}),
|
}),
|
||||||
component: () => {
|
component: () => {
|
||||||
const { playerId } = Route.useParams();
|
const { playerId } = Route.useParams();
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import { serverEvents, 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;
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/events/$")({
|
export const Route = createFileRoute("/api/events/$")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [superTokensRequestMiddleware],
|
middleware: [superTokensRequestMiddleware],
|
||||||
handlers: {
|
handlers: {
|
||||||
GET: ({ request, context }) => {
|
GET: ({ request }) => {
|
||||||
|
activeConnections++;
|
||||||
|
const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
||||||
|
logger.info(`ServerEvents | New connection ${connectionId}. Active: ${activeConnections}`);
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
||||||
@@ -17,6 +22,10 @@ export const Route = createFileRoute("/api/events/$")({
|
|||||||
logger.info("ServerEvents | Event received", event);
|
logger.info("ServerEvents | Event received", event);
|
||||||
const message = `data: ${JSON.stringify(event)}\n\n`;
|
const message = `data: ${JSON.stringify(event)}\n\n`;
|
||||||
try {
|
try {
|
||||||
|
if (!controller.desiredSize || controller.desiredSize <= 0) {
|
||||||
|
logger.warn("ServerEvents | Stream closed, skipping event");
|
||||||
|
return;
|
||||||
|
}
|
||||||
controller.enqueue(new TextEncoder().encode(message));
|
controller.enqueue(new TextEncoder().encode(message));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("ServerEvents | Error sending SSE message", error);
|
logger.error("ServerEvents | Error sending SSE message", error);
|
||||||
@@ -29,16 +38,34 @@ export const Route = createFileRoute("/api/events/$")({
|
|||||||
|
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
if (!controller.desiredSize || controller.desiredSize <= 0) {
|
||||||
|
clearInterval(pingInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pingMessage = `data: ${JSON.stringify({ type: "ping", timestamp: Date.now() })}\n\n`;
|
||||||
controller.enqueue(new TextEncoder().encode(pingMessage));
|
controller.enqueue(new TextEncoder().encode(pingMessage));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error("ServerEvents | Ping interval error", e);
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 15000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const heartbeatMessage = `data: ${JSON.stringify({ type: "heartbeat", timestamp: Date.now() })}\n\n`;
|
||||||
|
controller.enqueue(new TextEncoder().encode(heartbeatMessage));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("ServerEvents | Heartbeat error", e);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
activeConnections--;
|
||||||
serverEvents.off("test", handleEvent);
|
serverEvents.off("test", handleEvent);
|
||||||
|
serverEvents.off("match", handleEvent);
|
||||||
|
serverEvents.off("reaction", handleEvent);
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
|
logger.info(`ServerEvents | Connection ${connectionId} cleanup completed. Active: ${activeConnections}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.signal?.addEventListener("abort", cleanup);
|
request.signal?.addEventListener("abort", cleanup);
|
||||||
@@ -49,10 +76,14 @@ export const Route = createFileRoute("/api/events/$")({
|
|||||||
return new Response(stream, {
|
return new Response(stream, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/event-stream",
|
"Content-Type": "text/event-stream",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
Connection: "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Access-Control-Allow-Headers": "Cache-Control",
|
"Access-Control-Allow-Headers": "Cache-Control",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
"X-Proxy-Buffering": "no",
|
||||||
|
"Proxy-Buffering": "off",
|
||||||
|
"Transfer-Encoding": "chunked",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -107,10 +107,9 @@ export const Route = createFileRoute("/api/teams/upload-logo")({
|
|||||||
const pbFormData = new FormData();
|
const pbFormData = new FormData();
|
||||||
pbFormData.append("logo", logoFile);
|
pbFormData.append("logo", logoFile);
|
||||||
|
|
||||||
const updatedTeam = await pbAdmin.updateTeam(
|
await pbAdmin.updateTeam(teamId, pbFormData as any);
|
||||||
teamId,
|
const updatedTeam = await pbAdmin.getTeam(teamId);
|
||||||
pbFormData as any
|
if (!updatedTeam) throw new Error("Failed to fetch updated team");
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("Team logo uploaded successfully", {
|
logger.info("Team logo uploaded successfully", {
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
import { Divider, Group, Text, UnstyledButton } from "@mantine/core";
|
import { Divider, Group, Loader, Text, UnstyledButton } from "@mantine/core";
|
||||||
import { CaretRightIcon, Icon } from "@phosphor-icons/react";
|
import { CaretRightIcon, Icon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface ListButtonProps {
|
interface ListButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
Icon: Icon;
|
Icon: Icon;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListButton = ({ label, onClick, Icon }: ListButtonProps) => {
|
const ListButton = ({ label, onClick, Icon, loading }: ListButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UnstyledButton w="100%" p="md" component={"button"} onClick={onClick}>
|
<UnstyledButton
|
||||||
|
w="100%"
|
||||||
|
p="md"
|
||||||
|
component={"button"}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
<Group>
|
<Group>
|
||||||
<Icon weight="bold" size={20} />
|
<Icon weight="bold" size={20} />
|
||||||
<Text fw={500} size="md">
|
<Text fw={500} size="md">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
{loading ? (
|
||||||
|
<Loader size="sm" style={{ marginLeft: "auto" }} />
|
||||||
|
) : (
|
||||||
<CaretRightIcon style={{ marginLeft: "auto" }} size={20} />
|
<CaretRightIcon style={{ marginLeft: "auto" }} size={20} />
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -4,10 +4,24 @@ import {
|
|||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
TreeStructureIcon,
|
TreeStructureIcon,
|
||||||
TrophyIcon,
|
TrophyIcon,
|
||||||
|
MedalIcon,
|
||||||
|
CrownIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import ListButton from "@/components/list-button";
|
import ListButton from "@/components/list-button";
|
||||||
|
import { migrateBadgeProgress } from "@/features/badges/server";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const AdminPage = () => {
|
const AdminPage = () => {
|
||||||
|
const [isMigrating, setIsMigrating] = useState(false);
|
||||||
|
|
||||||
|
const handleMigrateBadges = async () => {
|
||||||
|
if (isMigrating) return;
|
||||||
|
|
||||||
|
setIsMigrating(true);
|
||||||
|
await migrateBadgeProgress();
|
||||||
|
setIsMigrating(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List p="0">
|
<List p="0">
|
||||||
<ListLink
|
<ListLink
|
||||||
@@ -15,6 +29,17 @@ const AdminPage = () => {
|
|||||||
Icon={TrophyIcon}
|
Icon={TrophyIcon}
|
||||||
to="/admin/tournaments"
|
to="/admin/tournaments"
|
||||||
/>
|
/>
|
||||||
|
<ListLink
|
||||||
|
label="Award Badges"
|
||||||
|
Icon={CrownIcon}
|
||||||
|
to="/admin/badges"
|
||||||
|
/>
|
||||||
|
<ListButton
|
||||||
|
label="Migrate Badge Progress"
|
||||||
|
Icon={MedalIcon}
|
||||||
|
onClick={handleMigrateBadges}
|
||||||
|
loading={isMigrating}
|
||||||
|
/>
|
||||||
<ListButton
|
<ListButton
|
||||||
label="Open Pocketbase"
|
label="Open Pocketbase"
|
||||||
Icon={DatabaseIcon}
|
Icon={DatabaseIcon}
|
||||||
|
|||||||
99
src/features/admin/components/award-badges.tsx
Normal file
99
src/features/admin/components/award-badges.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Box, Card, Text, Select, Button, Group, Stack } from "@mantine/core";
|
||||||
|
import { awardManualBadge } from "@/features/badges/server";
|
||||||
|
import { useAllBadges } from "@/features/badges/queries";
|
||||||
|
import toast from "@/lib/sonner";
|
||||||
|
import { usePlayers } from "@/features/players/queries";
|
||||||
|
|
||||||
|
const AwardBadges = () => {
|
||||||
|
const { data: players } = usePlayers();
|
||||||
|
const { data: allBadges } = useAllBadges();
|
||||||
|
|
||||||
|
const [selectedPlayerId, setSelectedPlayerId] = useState<string | null>(null);
|
||||||
|
const [selectedBadgeId, setSelectedBadgeId] = useState<string | null>(null);
|
||||||
|
const [isAwarding, setIsAwarding] = useState(false);
|
||||||
|
|
||||||
|
const manualBadges = allBadges.filter((badge) => badge.type === "manual");
|
||||||
|
|
||||||
|
const handleAwardBadge = async () => {
|
||||||
|
if (!selectedPlayerId || !selectedBadgeId) return;
|
||||||
|
|
||||||
|
setIsAwarding(true);
|
||||||
|
try {
|
||||||
|
await awardManualBadge({
|
||||||
|
data: {
|
||||||
|
playerId: selectedPlayerId,
|
||||||
|
badgeId: selectedBadgeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Badge awarded successfully");
|
||||||
|
|
||||||
|
setSelectedPlayerId(null);
|
||||||
|
setSelectedBadgeId(null);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to award badge");
|
||||||
|
} finally {
|
||||||
|
setIsAwarding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playerOptions = players.map((player) => ({
|
||||||
|
value: player.id,
|
||||||
|
label: `${player.first_name} ${player.last_name}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const badgeOptions = manualBadges.map((badge) => ({
|
||||||
|
value: badge.id,
|
||||||
|
label: badge.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p="md">
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Box>
|
||||||
|
<Text size="lg" fw={600} mb="xs">
|
||||||
|
Award Manual Badge
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Select a player and a manual badge to award
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Player"
|
||||||
|
placeholder="Select a player"
|
||||||
|
data={playerOptions}
|
||||||
|
value={selectedPlayerId}
|
||||||
|
onChange={setSelectedPlayerId}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Badge"
|
||||||
|
placeholder="Select a badge"
|
||||||
|
data={badgeOptions}
|
||||||
|
value={selectedBadgeId}
|
||||||
|
onChange={setSelectedBadgeId}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleAwardBadge}
|
||||||
|
disabled={!selectedPlayerId || !selectedBadgeId}
|
||||||
|
loading={isAwarding}
|
||||||
|
>
|
||||||
|
Award Badge
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AwardBadges;
|
||||||
47
src/features/badges/components/badge-showcase-skeleton.tsx
Normal file
47
src/features/badges/components/badge-showcase-skeleton.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Box, Skeleton, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
const BadgeShowcaseSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<Box mb="lg">
|
||||||
|
<Box
|
||||||
|
px="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: '220px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(85px, 1fr))',
|
||||||
|
gap: 'var(--mantine-spacing-md)',
|
||||||
|
paddingBottom: 'var(--mantine-spacing-sm)',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
aspectRatio: '1',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
height="100%"
|
||||||
|
radius="12px"
|
||||||
|
style={{
|
||||||
|
aspectRatio: '1',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BadgeShowcaseSkeleton;
|
||||||
269
src/features/badges/components/badge-showcase.tsx
Normal file
269
src/features/badges/components/badge-showcase.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import { Box, Text, Popover, Progress } from "@mantine/core";
|
||||||
|
import { usePlayerBadges, useAllBadges } from "../queries";
|
||||||
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
import { Badge, BadgeProgress } from "../types";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { MedalIcon, LockKeyIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
interface BadgeShowcaseProps {
|
||||||
|
playerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BadgeDisplay {
|
||||||
|
badge: Badge;
|
||||||
|
progress?: BadgeProgress;
|
||||||
|
earned: boolean;
|
||||||
|
progressText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BadgeShowcase = ({ playerId }: BadgeShowcaseProps) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { data: badgeProgress } = usePlayerBadges(playerId);
|
||||||
|
const { data: allBadges } = useAllBadges();
|
||||||
|
|
||||||
|
const isCurrentUser = user?.id === playerId;
|
||||||
|
|
||||||
|
const badgesToDisplay = useMemo(() => {
|
||||||
|
const displays: BadgeDisplay[] = [];
|
||||||
|
|
||||||
|
if (isCurrentUser) {
|
||||||
|
for (const badge of allBadges) {
|
||||||
|
const progress = badgeProgress.find(bp => bp.badge.id === badge.id);
|
||||||
|
const earned = progress?.earned || false;
|
||||||
|
|
||||||
|
if (badge.type === 'manual' && !earned) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progressText = "";
|
||||||
|
if (progress) {
|
||||||
|
const target = getTargetProgress(badge);
|
||||||
|
progressText = `${progress.progress} / ${target}`;
|
||||||
|
} else {
|
||||||
|
const target = getTargetProgress(badge);
|
||||||
|
progressText = `0 / ${target}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
displays.push({
|
||||||
|
badge,
|
||||||
|
progress,
|
||||||
|
earned,
|
||||||
|
progressText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displays.sort((a, b) => {
|
||||||
|
if (a.earned && !b.earned) return -1;
|
||||||
|
if (!a.earned && b.earned) return 1;
|
||||||
|
return a.badge.order - b.badge.order;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const earnedProgress = badgeProgress.filter(bp => bp.earned);
|
||||||
|
for (const progress of earnedProgress) {
|
||||||
|
const badge: Badge = {
|
||||||
|
...progress.badge,
|
||||||
|
criteria: {},
|
||||||
|
created: progress.created,
|
||||||
|
updated: progress.updated,
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = getTargetProgress(badge);
|
||||||
|
displays.push({
|
||||||
|
badge,
|
||||||
|
progress,
|
||||||
|
earned: true,
|
||||||
|
progressText: `${progress.progress} / ${target}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
displays.sort((a, b) => a.badge.order - b.badge.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return displays;
|
||||||
|
}, [allBadges, badgeProgress, isCurrentUser]);
|
||||||
|
|
||||||
|
if (badgesToDisplay.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb="lg">
|
||||||
|
<Box
|
||||||
|
px="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: '220px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(85px, 1fr))',
|
||||||
|
gap: 'var(--mantine-spacing-md)',
|
||||||
|
paddingBottom: 'var(--mantine-spacing-sm)',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badgesToDisplay.map((display) => {
|
||||||
|
const isStackableBadge = ['winner_badge', 'silver_medal_badge', 'bronze_medal_badge'].includes(display.badge.key);
|
||||||
|
const stackCount = display.earned && isStackableBadge
|
||||||
|
? (display.progress?.progress || 0)
|
||||||
|
: 1;
|
||||||
|
const showStack = stackCount > 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover key={display.badge.id} width={220} position="top" withArrow shadow="md">
|
||||||
|
<Popover.Target>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showStack && (
|
||||||
|
<>
|
||||||
|
{[...Array(Math.min(stackCount - 1, 2))].map((_, i) => (
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
aspectRatio: '1',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '2px solid var(--mantine-primary-color-5)',
|
||||||
|
position: 'absolute',
|
||||||
|
top: `${(i + 1) * 3}px`,
|
||||||
|
left: `${(i + 1) * 3}px`,
|
||||||
|
right: `-${(i + 1) * 3}px`,
|
||||||
|
bottom: `-${(i + 1) * 3}px`,
|
||||||
|
opacity: 0.4 - (i * 0.15),
|
||||||
|
zIndex: -(i + 1),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
aspectRatio: '1',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-7))',
|
||||||
|
border: display.earned
|
||||||
|
? '2px solid var(--mantine-primary-color-6)'
|
||||||
|
: '2px dashed var(--mantine-primary-color-4)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
padding: 'var(--mantine-spacing-xs)',
|
||||||
|
position: 'relative',
|
||||||
|
boxShadow: display.earned
|
||||||
|
? '0 0 0 1px color-mix(in srgb, var(--mantine-primary-color-6) 20%, transparent)'
|
||||||
|
: 'none',
|
||||||
|
opacity: display.earned ? 1 : 0.4,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{display.earned ? (
|
||||||
|
<MedalIcon
|
||||||
|
size={32}
|
||||||
|
weight="fill"
|
||||||
|
color="var(--mantine-primary-color-6)"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LockKeyIcon
|
||||||
|
size={28}
|
||||||
|
weight="regular"
|
||||||
|
color="var(--mantine-color-dimmed)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showStack && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '4px',
|
||||||
|
right: '4px',
|
||||||
|
color: 'var(--mantine-primary-color-6)',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x{stackCount}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
fw={display.earned ? 600 : 500}
|
||||||
|
ta="center"
|
||||||
|
c={display.earned ? undefined : 'dimmed'}
|
||||||
|
style={{
|
||||||
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{display.badge.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" fw={600} mb="xs">
|
||||||
|
{display.badge.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" mb={isCurrentUser ? "sm" : undefined}>
|
||||||
|
{display.badge.description}
|
||||||
|
</Text>
|
||||||
|
{isCurrentUser && (
|
||||||
|
<Box>
|
||||||
|
<Box mb="xs" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text size="xs" fw={500} c="dimmed">
|
||||||
|
Progress
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" fw={600} c="dimmed">
|
||||||
|
{display.progressText}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Progress
|
||||||
|
value={(display.progress?.progress || 0) / getTargetProgress(display.badge) * 100}
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
color={display.earned ? "green" : undefined}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTargetProgress(badge: Badge): number {
|
||||||
|
const criteria = badge.criteria;
|
||||||
|
return (
|
||||||
|
criteria.matches_played ||
|
||||||
|
criteria.tournament_wins ||
|
||||||
|
criteria.tournaments_attended ||
|
||||||
|
criteria.overtime_matches ||
|
||||||
|
criteria.overtime_wins ||
|
||||||
|
criteria.consecutive_wins ||
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BadgeShowcase;
|
||||||
24
src/features/badges/queries.ts
Normal file
24
src/features/badges/queries.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||||
|
import { getPlayerBadges, getAllBadges } from "./server";
|
||||||
|
|
||||||
|
export const badgeKeys = {
|
||||||
|
playerBadges: (playerId: string) => ['badges', 'player', playerId],
|
||||||
|
allBadges: () => ['badges', 'all'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const badgeQueries = {
|
||||||
|
playerBadges: (playerId: string) => ({
|
||||||
|
queryKey: badgeKeys.playerBadges(playerId),
|
||||||
|
queryFn: async () => await getPlayerBadges({ data: playerId })
|
||||||
|
}),
|
||||||
|
allBadges: () => ({
|
||||||
|
queryKey: badgeKeys.allBadges(),
|
||||||
|
queryFn: async () => await getAllBadges()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePlayerBadges = (playerId: string) =>
|
||||||
|
useServerSuspenseQuery(badgeQueries.playerBadges(playerId));
|
||||||
|
|
||||||
|
export const useAllBadges = () =>
|
||||||
|
useServerSuspenseQuery(badgeQueries.allBadges());
|
||||||
@@ -1,4 +1,34 @@
|
|||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getPlayerBadges = createServerFn()
|
||||||
|
.inputValidator(z.string())
|
||||||
|
.middleware([superTokensFunctionMiddleware])
|
||||||
|
.handler(async ({ data: playerId }) =>
|
||||||
|
toServerResult(() => pbAdmin.getPlayerBadgeProgress(playerId))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const migrateBadgeProgress = createServerFn()
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
|
.handler(async () =>
|
||||||
|
toServerResult(() => pbAdmin.migrateBadgeProgress())
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getAllBadges = createServerFn()
|
||||||
|
.middleware([superTokensFunctionMiddleware])
|
||||||
|
.handler(async () =>
|
||||||
|
toServerResult(() => pbAdmin.listBadges())
|
||||||
|
);
|
||||||
|
|
||||||
|
export const awardManualBadge = createServerFn()
|
||||||
|
.inputValidator(z.object({
|
||||||
|
playerId: z.string(),
|
||||||
|
badgeId: z.string(),
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
toServerResult(() => pbAdmin.awardManualBadge(data.playerId, data.badgeId))
|
||||||
|
);
|
||||||
|
|||||||
25
src/features/badges/types.ts
Normal file
25
src/features/badges/types.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export interface BadgeInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description: string;
|
||||||
|
type: "manual" | "match" | "tournament";
|
||||||
|
progressive: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Badge extends BadgeInfo {
|
||||||
|
criteria: Record<string, any>;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BadgeProgress {
|
||||||
|
id: string;
|
||||||
|
badge: BadgeInfo;
|
||||||
|
player: string;
|
||||||
|
progress: number;
|
||||||
|
earned: boolean;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
@@ -31,7 +31,11 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
pos='relative'
|
pos='relative'
|
||||||
h='100dvh'
|
h='100dvh'
|
||||||
mah='100dvh'
|
mah='100dvh'
|
||||||
// style={{ top: viewport.top }} //, transition: 'top 0.1s ease-in-out' }}
|
style={{
|
||||||
|
height: `${viewport.height}px`,
|
||||||
|
minHeight: '100dvh',
|
||||||
|
// top: viewport.top
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Header {...header} />
|
<Header {...header} />
|
||||||
<AppShell.Main
|
<AppShell.Main
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const Navbar = () => {
|
|||||||
// boxShadow: `5px 5px ${boxShadowColor}`, borderColor
|
// boxShadow: `5px 5px ${boxShadowColor}`, borderColor
|
||||||
|
|
||||||
if (isMobile) return (
|
if (isMobile) return (
|
||||||
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1.5rem)' pos='fixed' m='0.75rem' bottom='0' 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='0' style={{ zIndex: 10 }}>
|
||||||
<Group 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 }}>
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<NavLink key={link.href} {...link} />
|
<NavLink key={link.href} {...link} />
|
||||||
|
|||||||
@@ -19,11 +19,17 @@ const useVisualViewportSize = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!windowExists) return;
|
if (!windowExists) return;
|
||||||
|
|
||||||
|
setSize();
|
||||||
|
|
||||||
window.visualViewport?.addEventListener('resize', setSize, eventListerOptions);
|
window.visualViewport?.addEventListener('resize', setSize, eventListerOptions);
|
||||||
|
window.visualViewport?.addEventListener('scroll', setSize, eventListerOptions);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.visualViewport?.removeEventListener('resize', setSize);
|
window.visualViewport?.removeEventListener('resize', setSize);
|
||||||
|
window.visualViewport?.removeEventListener('scroll', setSize);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [setSize]);
|
||||||
|
|
||||||
return windowSize;
|
return windowSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { MatchInput } from "@/features/matches/types";
|
|||||||
import { serverEvents } from "@/lib/events/emitter";
|
import { serverEvents } 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()
|
||||||
.inputValidator(orderedTeamsSchema)
|
.inputValidator(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", {
|
||||||
@@ -138,7 +139,7 @@ export const generateTournamentBracket = createServerFn()
|
|||||||
|
|
||||||
export const startMatch = createServerFn()
|
export const startMatch = createServerFn()
|
||||||
.inputValidator(z.string())
|
.inputValidator(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);
|
||||||
@@ -171,7 +172,7 @@ const endMatchSchema = z.object({
|
|||||||
});
|
});
|
||||||
export const endMatch = createServerFn()
|
export const endMatch = createServerFn()
|
||||||
.inputValidator(endMatchSchema)
|
.inputValidator(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);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Box } from "@mantine/core";
|
import { Box, Stack, Text, Divider } from "@mantine/core";
|
||||||
|
import { Suspense } from "react";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
|
||||||
import TeamList from "@/features/teams/components/team-list";
|
import TeamList from "@/features/teams/components/team-list";
|
||||||
import StatsOverview from "@/components/stats-overview";
|
import StatsOverview 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 BadgeShowcaseSkeleton from "@/features/badges/components/badge-showcase-skeleton";
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,7 +21,19 @@ const Profile = ({ id }: ProfileProps) => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <StatsOverview statsData={stats} isLoading={statsLoading} />,
|
content: <>
|
||||||
|
<Stack px="md">
|
||||||
|
<Text size="md" fw={700}>Badges</Text>
|
||||||
|
<Suspense fallback={<BadgeShowcaseSkeleton />}>
|
||||||
|
<BadgeShowcase playerId={id} />
|
||||||
|
</Suspense>
|
||||||
|
</Stack>
|
||||||
|
<Divider my="md" />
|
||||||
|
<Stack>
|
||||||
|
<Text px="md" size="md" fw={700}>Statistics</Text>
|
||||||
|
<StatsOverview statsData={stats} isLoading={statsLoading} />
|
||||||
|
</Stack>
|
||||||
|
</>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matches",
|
label: "Matches",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|||||||
import { logger } from ".";
|
import { logger } from ".";
|
||||||
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";
|
||||||
|
|
||||||
export const fetchMe = createServerFn()
|
export const fetchMe = createServerFn()
|
||||||
.handler(async () =>
|
.handler(async () =>
|
||||||
@@ -46,7 +47,7 @@ export const getPlayer = createServerFn()
|
|||||||
|
|
||||||
export const updatePlayer = createServerFn()
|
export const updatePlayer = createServerFn()
|
||||||
.inputValidator(playerUpdateSchema)
|
.inputValidator(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;
|
||||||
@@ -98,7 +99,7 @@ export const createPlayer = createServerFn()
|
|||||||
|
|
||||||
export const associatePlayer = createServerFn()
|
export const associatePlayer = createServerFn()
|
||||||
.inputValidator(z.string())
|
.inputValidator(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;
|
||||||
|
|||||||
@@ -106,11 +106,7 @@ const TeamForm = ({
|
|||||||
|
|
||||||
mutation(teamData, {
|
mutation(teamData, {
|
||||||
onSuccess: async (team: any) => {
|
onSuccess: async (team: any) => {
|
||||||
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
close();
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: teamKeys.details(team.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (logo && team) {
|
if (logo && team) {
|
||||||
try {
|
try {
|
||||||
let processedLogo = logo;
|
let processedLogo = logo;
|
||||||
@@ -152,7 +148,12 @@ const TeamForm = ({
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log("Logo upload result:", result);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: teamKeys.details(team.id),
|
||||||
|
});
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
tournamentKeys.details(result.team!.id),
|
tournamentKeys.details(result.team!.id),
|
||||||
result.team
|
result.team
|
||||||
@@ -164,12 +165,16 @@ const TeamForm = ({
|
|||||||
toast.error(logoErrorMessage);
|
toast.error(logoErrorMessage);
|
||||||
logger.error("Team logo upload error", error);
|
logger.error("Team logo upload error", error);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: teamKeys.details(team.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (team && team.id) {
|
if (team && team.id) {
|
||||||
onSubmit?.(team.id)
|
onSubmit?.(team.id)
|
||||||
}
|
}
|
||||||
close();
|
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
toast.error(`${errorMessage}: ${error.message}`);
|
toast.error(`${errorMessage}: ${error.message}`);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
|||||||
import { teamInputSchema, teamUpdateSchema } from "./types";
|
import { teamInputSchema, teamUpdateSchema } from "./types";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { Match } from "../matches/types";
|
import { Match } from "../matches/types";
|
||||||
|
import { serverFnLoggingMiddleware } from "@/utils/activities";
|
||||||
|
|
||||||
|
|
||||||
export const listTeamInfos = createServerFn()
|
export const listTeamInfos = createServerFn()
|
||||||
@@ -30,7 +31,7 @@ export const getTeamInfo = createServerFn()
|
|||||||
|
|
||||||
export const createTeam = createServerFn()
|
export const createTeam = createServerFn()
|
||||||
.inputValidator(teamInputSchema)
|
.inputValidator(teamInputSchema)
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data, context }) =>
|
.handler(async ({ data, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userId = context.userAuthId;
|
const userId = context.userAuthId;
|
||||||
@@ -50,7 +51,7 @@ export const updateTeam = createServerFn()
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
updates: teamUpdateSchema
|
updates: teamUpdateSchema
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { id, updates }, context }) =>
|
.handler(async ({ data: { id, updates }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userId = context.userAuthId;
|
const userId = context.userAuthId;
|
||||||
@@ -61,10 +62,10 @@ export const updateTeam = createServerFn()
|
|||||||
throw new Error("Team not found");
|
throw new Error("Team not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPlayerOnTeam = team.players.some(player => player.id === userId);
|
//const isPlayerOnTeam = team.players.some(player => player.id === userId);
|
||||||
if (!isAdmin && !isPlayerOnTeam) {
|
//if (!isAdmin && !isPlayerOnTeam) {
|
||||||
throw new Error("You can only update teams that you are a member of");
|
// throw new Error("You can only update teams that you are a member of");
|
||||||
}
|
// }
|
||||||
|
|
||||||
logger.info("Updating team", { teamId: id, userId, isAdmin });
|
logger.info("Updating team", { teamId: id, userId, isAdmin });
|
||||||
return pbAdmin.updateTeam(id, updates);
|
return pbAdmin.updateTeam(id, updates);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { tournamentInputSchema } from "@/features/tournaments/types";
|
|||||||
import { logger } from ".";
|
import { logger } from ".";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
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";
|
||||||
|
|
||||||
export const listTournaments = createServerFn()
|
export const listTournaments = createServerFn()
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware])
|
||||||
@@ -14,7 +15,7 @@ export const listTournaments = createServerFn()
|
|||||||
|
|
||||||
export const createTournament = createServerFn()
|
export const createTournament = createServerFn()
|
||||||
.inputValidator(tournamentInputSchema)
|
.inputValidator(tournamentInputSchema)
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(() => pbAdmin.createTournament(data))
|
toServerResult(() => pbAdmin.createTournament(data))
|
||||||
);
|
);
|
||||||
@@ -24,7 +25,7 @@ export const updateTournament = createServerFn()
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
updates: tournamentInputSchema.partial()
|
updates: tournamentInputSchema.partial()
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data }) =>
|
.handler(async ({ data }) =>
|
||||||
toServerResult(() => pbAdmin.updateTournament(data.id, data.updates))
|
toServerResult(() => pbAdmin.updateTournament(data.id, data.updates))
|
||||||
);
|
);
|
||||||
@@ -48,7 +49,7 @@ export const enrollTeam = createServerFn()
|
|||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
teamId: z.string()
|
teamId: z.string()
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { tournamentId, teamId }, context }) =>
|
.handler(async ({ data: { tournamentId, teamId }, context }) =>
|
||||||
toServerResult(async () => {
|
toServerResult(async () => {
|
||||||
const userId = context.userAuthId;
|
const userId = context.userAuthId;
|
||||||
@@ -57,11 +58,11 @@ export const enrollTeam = createServerFn()
|
|||||||
const team = await pbAdmin.getTeam(teamId);
|
const team = await pbAdmin.getTeam(teamId);
|
||||||
if (!team) { throw new Error('Team not found'); }
|
if (!team) { throw new Error('Team not found'); }
|
||||||
|
|
||||||
const isPlayerOnTeam = team.players?.some(player => player.id === userId);
|
//const isPlayerOnTeam = team.players?.some(player => player.id === userId);
|
||||||
|
|
||||||
if (!isPlayerOnTeam && !isAdmin) {
|
//if (!isPlayerOnTeam && !isAdmin) {
|
||||||
throw new Error('You do not have permission to enroll this team');
|
// throw new Error('You do not have permission to enroll this team');
|
||||||
}
|
//}
|
||||||
|
|
||||||
logger.info('Enrolling team in tournament', { tournamentId, teamId, userId });
|
logger.info('Enrolling team in tournament', { tournamentId, teamId, userId });
|
||||||
const tournament = await pbAdmin.enrollTeam(tournamentId, teamId);
|
const tournament = await pbAdmin.enrollTeam(tournamentId, teamId);
|
||||||
@@ -74,7 +75,7 @@ export const unenrollTeam = createServerFn()
|
|||||||
tournamentId: z.string(),
|
tournamentId: z.string(),
|
||||||
teamId: z.string()
|
teamId: z.string()
|
||||||
}))
|
}))
|
||||||
.middleware([superTokensAdminFunctionMiddleware])
|
.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
.handler(async ({ data: { tournamentId, teamId }, context }) =>
|
.handler(async ({ data: { tournamentId, teamId }, context }) =>
|
||||||
toServerResult(() => pbAdmin.unenrollTeam(tournamentId, teamId))
|
toServerResult(() => pbAdmin.unenrollTeam(tournamentId, teamId))
|
||||||
);
|
);
|
||||||
@@ -95,7 +96,7 @@ export const getFreeAgents = createServerFn()
|
|||||||
|
|
||||||
export const enrollFreeAgent = createServerFn()
|
export const enrollFreeAgent = createServerFn()
|
||||||
.inputValidator(z.object({ phone: z.string(), tournamentId: z.string() }))
|
.inputValidator(z.object({ phone: z.string(), tournamentId: 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;
|
||||||
@@ -109,7 +110,7 @@ export const enrollFreeAgent = createServerFn()
|
|||||||
|
|
||||||
export const unenrollFreeAgent = createServerFn()
|
export const unenrollFreeAgent = createServerFn()
|
||||||
.inputValidator(z.object({ tournamentId: z.string() }))
|
.inputValidator(z.object({ tournamentId: 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;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const eventHandlers: Record<string, EventHandler> = {
|
|||||||
logger.info("New Connection");
|
logger.info("New Connection");
|
||||||
},
|
},
|
||||||
"ping": () => {},
|
"ping": () => {},
|
||||||
|
"heartbeat": () => {},
|
||||||
"match": (event, queryClient) => {
|
"match": (event, queryClient) => {
|
||||||
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
|
queryClient.invalidateQueries(tournamentQueries.details(event.tournamentId))
|
||||||
queryClient.invalidateQueries(tournamentQueries.current())
|
queryClient.invalidateQueries(tournamentQueries.current())
|
||||||
@@ -73,15 +74,15 @@ export function useServerEvents() {
|
|||||||
logger.error("SSE connection error", error);
|
logger.error("SSE connection error", error);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
|
|
||||||
if (shouldConnectRef.current && retryCountRef.current < 5) {
|
if (shouldConnectRef.current && retryCountRef.current < 10) {
|
||||||
retryCountRef.current += 1;
|
retryCountRef.current += 1;
|
||||||
const delay = Math.min(
|
const delay = Math.min(
|
||||||
1000 * Math.pow(2, retryCountRef.current - 1),
|
1000 * Math.pow(1.5, retryCountRef.current - 1),
|
||||||
30000
|
15000
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`SSE reconnection attempt ${retryCountRef.current}/5 in ${delay}ms`
|
`SSE reconnection attempt ${retryCountRef.current}/10 in ${delay}ms`
|
||||||
);
|
);
|
||||||
|
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
@@ -89,7 +90,7 @@ export function useServerEvents() {
|
|||||||
connectEventSource();
|
connectEventSource();
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
} else if (retryCountRef.current >= 5) {
|
} else if (retryCountRef.current >= 10) {
|
||||||
logger.error("SSE max reconnection attempts reached");
|
logger.error("SSE max reconnection attempts reached");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,23 @@ import { EventEmitter } from "events";
|
|||||||
|
|
||||||
export const serverEvents = new EventEmitter();
|
export const serverEvents = new EventEmitter();
|
||||||
|
|
||||||
|
serverEvents.setMaxListeners(50);
|
||||||
|
|
||||||
|
// Debug logging for listener count
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
setInterval(() => {
|
||||||
|
const listenerCounts = {
|
||||||
|
test: serverEvents.listenerCount('test'),
|
||||||
|
match: serverEvents.listenerCount('match'),
|
||||||
|
reaction: serverEvents.listenerCount('reaction'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (listenerCounts.test > 0 || listenerCounts.match > 0 || listenerCounts.reaction > 0) {
|
||||||
|
console.log('ServerEvents listener count:', listenerCounts);
|
||||||
|
}
|
||||||
|
}, 30000); // Log every 30 seconds in development
|
||||||
|
}
|
||||||
|
|
||||||
export type TestEvent = {
|
export type TestEvent = {
|
||||||
type: "test";
|
type: "test";
|
||||||
playerId: string;
|
playerId: string;
|
||||||
|
|||||||
@@ -4,17 +4,29 @@ import { createTournamentsService } from "./services/tournaments";
|
|||||||
import { createTeamsService } from "./services/teams";
|
import { createTeamsService } from "./services/teams";
|
||||||
import { createMatchesService } from "./services/matches";
|
import { createMatchesService } from "./services/matches";
|
||||||
import { createReactionsService } from "./services/reactions";
|
import { createReactionsService } from "./services/reactions";
|
||||||
|
import { createActivitiesService } from "./services/activities";
|
||||||
|
import { createBadgesService } from "./services/badges";
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
class PocketBaseAdminClient {
|
class PocketBaseAdminClient {
|
||||||
private pb: PocketBase;
|
private pb: PocketBase;
|
||||||
public authPromise: Promise<void>;
|
public authPromise: Promise<void>;
|
||||||
|
private refreshInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
this.pb = new PocketBase(process.env.POCKETBASE_URL);
|
||||||
|
|
||||||
this.pb.beforeSend = (url, options) => {
|
this.pb.beforeSend = async (url, options) => {
|
||||||
|
if (this.pb.authStore.isValid && this.isTokenExpiringSoon()) {
|
||||||
|
try {
|
||||||
|
await this.refreshAuth();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh admin token, re-authenticating:', error);
|
||||||
|
await this.authenticate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
options.cache = "no-store";
|
options.cache = "no-store";
|
||||||
options.headers = {
|
options.headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
@@ -35,16 +47,74 @@ class PocketBaseAdminClient {
|
|||||||
Object.assign(this, createTournamentsService(this.pb));
|
Object.assign(this, createTournamentsService(this.pb));
|
||||||
Object.assign(this, createMatchesService(this.pb));
|
Object.assign(this, createMatchesService(this.pb));
|
||||||
Object.assign(this, createReactionsService(this.pb));
|
Object.assign(this, createReactionsService(this.pb));
|
||||||
|
Object.assign(this, createActivitiesService(this.pb));
|
||||||
|
Object.assign(this, createBadgesService(this.pb));
|
||||||
|
|
||||||
|
this.startTokenRefresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async authenticate() {
|
private async authenticate() {
|
||||||
|
try {
|
||||||
await this.pb
|
await this.pb
|
||||||
.collection("_superusers")
|
.collection("_superusers")
|
||||||
.authWithPassword(
|
.authWithPassword(
|
||||||
process.env.POCKETBASE_ADMIN_EMAIL!,
|
process.env.POCKETBASE_ADMIN_EMAIL!,
|
||||||
process.env.POCKETBASE_ADMIN_PASSWORD!
|
process.env.POCKETBASE_ADMIN_PASSWORD!
|
||||||
);
|
);
|
||||||
|
console.log('PocketBase admin authenticated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to authenticate PocketBase admin:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshAuth() {
|
||||||
|
try {
|
||||||
|
await this.pb.collection("_superusers").authRefresh();
|
||||||
|
console.log('PocketBase admin token refreshed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh PocketBase admin token:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isTokenExpiringSoon(): boolean {
|
||||||
|
if (!this.pb.authStore.token) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(this.pb.authStore.token.split('.')[1]));
|
||||||
|
const expiresAt = payload.exp * 1000;
|
||||||
|
const now = Date.now();
|
||||||
|
const fiveMinutes = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
return expiresAt - now < fiveMinutes;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTokenRefresh() {
|
||||||
|
this.refreshInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.refreshAuth();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Periodic token refresh failed, re-authenticating:', error);
|
||||||
|
try {
|
||||||
|
await this.authenticate();
|
||||||
|
} catch (authError) {
|
||||||
|
console.error('Re-authentication failed:', authError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10 * 60 * 1000);
|
||||||
|
|
||||||
|
if (typeof process !== 'undefined') {
|
||||||
|
process.on('beforeExit', () => {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +124,9 @@ interface AdminClient
|
|||||||
ReturnType<typeof createTeamsService>,
|
ReturnType<typeof createTeamsService>,
|
||||||
ReturnType<typeof createTournamentsService>,
|
ReturnType<typeof createTournamentsService>,
|
||||||
ReturnType<typeof createMatchesService>,
|
ReturnType<typeof createMatchesService>,
|
||||||
ReturnType<typeof createReactionsService> {
|
ReturnType<typeof createReactionsService>,
|
||||||
|
ReturnType<typeof createActivitiesService>,
|
||||||
|
ReturnType<typeof createBadgesService> {
|
||||||
authPromise: Promise<void>;
|
authPromise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
src/lib/pocketbase/services/activities.ts
Normal file
56
src/lib/pocketbase/services/activities.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import PocketBase from "pocketbase";
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
player?: string;
|
||||||
|
duration: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
arguments?: any;
|
||||||
|
user_agent?: string;
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityInput {
|
||||||
|
name: string;
|
||||||
|
player?: string;
|
||||||
|
duration: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
arguments?: any;
|
||||||
|
user_agent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createActivitiesService(pb: PocketBase) {
|
||||||
|
return {
|
||||||
|
async createActivity(data: ActivityInput): Promise<Activity> {
|
||||||
|
const result = await pb.collection("activities").create<Activity>(data);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getRecentActivities(limit: number = 100): Promise<Activity[]> {
|
||||||
|
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
return result.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getActivitiesByUser(userId: string, limit: number = 50): Promise<Activity[]> {
|
||||||
|
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
||||||
|
filter: `user_id = "${userId}"`,
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
return result.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getActivitiesByFunction(functionName: string, limit: number = 50): Promise<Activity[]> {
|
||||||
|
const result = await pb.collection("activities").getList<Activity>(1, limit, {
|
||||||
|
filter: `function_name = "${functionName}"`,
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
return result.items;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
433
src/lib/pocketbase/services/badges.ts
Normal file
433
src/lib/pocketbase/services/badges.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import PocketBase from "pocketbase";
|
||||||
|
import { Badge, BadgeProgress } from "@/features/badges/types";
|
||||||
|
import { transformBadge, transformBadgeProgress } from "@/lib/pocketbase/util/transform-types";
|
||||||
|
|
||||||
|
export interface PlayerStats {
|
||||||
|
player_id: string;
|
||||||
|
matches: number;
|
||||||
|
wins: number;
|
||||||
|
losses: number;
|
||||||
|
total_cups_made: number;
|
||||||
|
total_cups_against: number;
|
||||||
|
margin_of_victory: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBadgesService(pb: PocketBase) {
|
||||||
|
return {
|
||||||
|
async getBadge(id: string): Promise<Badge> {
|
||||||
|
const result = await pb.collection("badges").getOne(id);
|
||||||
|
return transformBadge(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listBadges(): Promise<Badge[]> {
|
||||||
|
const results = await pb.collection("badges").getFullList({
|
||||||
|
sort: 'name',
|
||||||
|
});
|
||||||
|
return results.map(transformBadge);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getBadgeProgress(id: string): Promise<BadgeProgress> {
|
||||||
|
const result = await pb.collection("badge_progress").getOne(id, {
|
||||||
|
expand: 'badge,player',
|
||||||
|
});
|
||||||
|
return transformBadgeProgress(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPlayerBadgeProgress(playerId: string): Promise<BadgeProgress[]> {
|
||||||
|
const results = await pb.collection("badge_progress").getFullList({
|
||||||
|
filter: `player = "${playerId}"`,
|
||||||
|
expand: 'badge',
|
||||||
|
});
|
||||||
|
return results.map(transformBadgeProgress);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createBadgeProgress(data: {
|
||||||
|
badge: string;
|
||||||
|
player: string;
|
||||||
|
progress: number;
|
||||||
|
earned: boolean;
|
||||||
|
}): Promise<BadgeProgress> {
|
||||||
|
return await pb.collection("badge_progress").create<BadgeProgress>(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateBadgeProgress(id: string, data: {
|
||||||
|
progress?: number;
|
||||||
|
earned?: boolean;
|
||||||
|
}): Promise<BadgeProgress> {
|
||||||
|
return await pb.collection("badge_progress").update<BadgeProgress>(id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteBadgeProgress(id: string): Promise<boolean> {
|
||||||
|
await pb.collection("badge_progress").delete(id);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearAllBadgeProgress(): Promise<number> {
|
||||||
|
const existingProgress = await pb.collection("badge_progress").getFullList();
|
||||||
|
for (const progress of existingProgress) {
|
||||||
|
await pb.collection("badge_progress").delete(progress.id);
|
||||||
|
}
|
||||||
|
return existingProgress.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
async calculateBadgeProgress(playerId: string, badge: Badge): Promise<number> {
|
||||||
|
if (badge.type === "manual") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (badge.type === "match") {
|
||||||
|
return await this.calculateMatchBadgeProgress(playerId, badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (badge.type === "tournament") {
|
||||||
|
return await this.calculateTournamentBadgeProgress(playerId, badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
async calculateMatchBadgeProgress(playerId: string, badge: Badge): Promise<number> {
|
||||||
|
const criteria = badge.criteria;
|
||||||
|
|
||||||
|
const stats = await pb.collection("player_stats").getFirstListItem<PlayerStats>(
|
||||||
|
`player_id = "${playerId}"`
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
if (!stats) return 0;
|
||||||
|
|
||||||
|
if (criteria.matches_played !== undefined) {
|
||||||
|
return stats.matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.overtime_matches !== undefined || criteria.overtime_wins !== undefined) {
|
||||||
|
const matches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && ot_count > 0`,
|
||||||
|
expand: 'home,away,home.players,away.players',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (criteria.overtime_matches !== undefined) {
|
||||||
|
return matches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.overtime_wins !== undefined) {
|
||||||
|
const overtimeWins = matches.filter(m => {
|
||||||
|
const isHome = m.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
m.expand?.home?.players?.includes(playerId);
|
||||||
|
const isAway = m.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
m.expand?.away?.players?.includes(playerId);
|
||||||
|
|
||||||
|
if (isHome) {
|
||||||
|
return m.home_cups > m.away_cups;
|
||||||
|
} else if (isAway) {
|
||||||
|
return m.away_cups > m.home_cups;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return overtimeWins.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.margin_of_victory !== undefined) {
|
||||||
|
const matches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended"`,
|
||||||
|
expand: 'home,away,home.players,away.players',
|
||||||
|
});
|
||||||
|
|
||||||
|
const bigWins = matches.filter(m => {
|
||||||
|
const isHome = m.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
m.expand?.home?.players?.includes(playerId);
|
||||||
|
const isAway = m.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
m.expand?.away?.players?.includes(playerId);
|
||||||
|
|
||||||
|
if (isHome && m.home_cups > m.away_cups) {
|
||||||
|
return (m.home_cups - m.away_cups) >= criteria.margin_of_victory;
|
||||||
|
} else if (isAway && m.away_cups > m.home_cups) {
|
||||||
|
return (m.away_cups - m.home_cups) >= criteria.margin_of_victory;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return bigWins.length > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
async calculateTournamentBadgeProgress(playerId: string, badge: Badge): Promise<number> {
|
||||||
|
const criteria = badge.criteria;
|
||||||
|
|
||||||
|
const matches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended"`,
|
||||||
|
expand: 'tournament,home,away,home.players,away.players',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tournamentIds = new Set(matches.map(m => m.tournament));
|
||||||
|
const tournamentsAttended = tournamentIds.size;
|
||||||
|
|
||||||
|
if (criteria.tournaments_attended !== undefined) {
|
||||||
|
return tournamentsAttended;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.tournament_wins !== undefined) {
|
||||||
|
if (tournamentIds.size === 0) return 0;
|
||||||
|
|
||||||
|
let tournamentWins = 0;
|
||||||
|
|
||||||
|
for (const tournamentId of tournamentIds) {
|
||||||
|
const tournamentMatches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `tournament = "${tournamentId}" && status = "ended"`,
|
||||||
|
expand: 'home,away,home.players,away.players',
|
||||||
|
});
|
||||||
|
|
||||||
|
const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket);
|
||||||
|
const finalsMatch = winnersMatches.reduce((highest: any, current: any) =>
|
||||||
|
(!highest || current.lid > highest.lid) ? current : highest, null);
|
||||||
|
|
||||||
|
if (finalsMatch && finalsMatch.status === 'ended') {
|
||||||
|
const finalsWinnerId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.home : finalsMatch.away;
|
||||||
|
|
||||||
|
const winningTeam = finalsMatch.expand?.[finalsWinnerId === finalsMatch.home ? 'home' : 'away'];
|
||||||
|
const winningPlayers = winningTeam?.expand?.players || winningTeam?.players || [];
|
||||||
|
|
||||||
|
const playerWon = winningPlayers.some((p: any) =>
|
||||||
|
(typeof p === 'string' ? p : p.id) === playerId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerWon) {
|
||||||
|
tournamentWins++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tournamentWins;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.placement !== undefined && typeof criteria.placement === 'number') {
|
||||||
|
let placementCount = 0;
|
||||||
|
|
||||||
|
for (const tournamentId of tournamentIds) {
|
||||||
|
const tournamentMatches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `tournament = "${tournamentId}" && status = "ended"`,
|
||||||
|
expand: 'home,away,home.players,away.players',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (criteria.placement === 2) {
|
||||||
|
const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket);
|
||||||
|
const finalsMatch = winnersMatches.reduce((highest: any, current: any) =>
|
||||||
|
(!highest || current.lid > highest.lid) ? current : highest, null);
|
||||||
|
|
||||||
|
if (finalsMatch && finalsMatch.status === 'ended') {
|
||||||
|
const finalsLoserId = (finalsMatch.home_cups > finalsMatch.away_cups) ? finalsMatch.away : finalsMatch.home;
|
||||||
|
|
||||||
|
const losingTeam = finalsMatch.expand?.[finalsLoserId === finalsMatch.home ? 'home' : 'away'];
|
||||||
|
const losingPlayers = losingTeam?.expand?.players || losingTeam?.players || [];
|
||||||
|
|
||||||
|
const playerLost = losingPlayers.some((p: any) =>
|
||||||
|
(typeof p === 'string' ? p : p.id) === playerId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerLost) {
|
||||||
|
placementCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.placement === 3) {
|
||||||
|
const losersMatches = tournamentMatches.filter(m => m.is_losers_bracket);
|
||||||
|
const losersFinale = losersMatches.reduce((highest: any, current: any) =>
|
||||||
|
(!highest || current.lid > highest.lid) ? current : highest, null);
|
||||||
|
|
||||||
|
if (losersFinale && losersFinale.status === 'ended') {
|
||||||
|
const losersFinaleLoserId = (losersFinale.home_cups > losersFinale.away_cups) ? losersFinale.away : losersFinale.home;
|
||||||
|
|
||||||
|
const losingTeam = losersFinale.expand?.[losersFinaleLoserId === losersFinale.home ? 'home' : 'away'];
|
||||||
|
const losingPlayers = losingTeam?.expand?.players || losingTeam?.players || [];
|
||||||
|
|
||||||
|
const playerLost = losingPlayers.some((p: any) =>
|
||||||
|
(typeof p === 'string' ? p : p.id) === playerId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playerLost) {
|
||||||
|
placementCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return placementCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.tournament_record !== undefined) {
|
||||||
|
const tournaments = await pb.collection("tournaments").getFullList({
|
||||||
|
sort: 'start_time',
|
||||||
|
});
|
||||||
|
|
||||||
|
let timesWent02 = 0;
|
||||||
|
|
||||||
|
for (const tournamentId of tournamentIds) {
|
||||||
|
const tournament = tournaments.find(t => t.id === tournamentId);
|
||||||
|
if (!tournament) continue;
|
||||||
|
|
||||||
|
const tournamentMatches = matches.filter(m => m.tournament === tournamentId);
|
||||||
|
|
||||||
|
let wins = 0;
|
||||||
|
let losses = 0;
|
||||||
|
|
||||||
|
for (const match of tournamentMatches) {
|
||||||
|
const isHome = match.expand?.home?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
match.expand?.home?.players?.includes(playerId);
|
||||||
|
const isAway = match.expand?.away?.expand?.players?.some((p: any) => p.id === playerId) ||
|
||||||
|
match.expand?.away?.players?.includes(playerId);
|
||||||
|
|
||||||
|
if (isHome && match.home_cups > match.away_cups) {
|
||||||
|
wins++;
|
||||||
|
} else if (isAway && match.away_cups > match.home_cups) {
|
||||||
|
wins++;
|
||||||
|
} else {
|
||||||
|
losses++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = `${wins}-${losses}`;
|
||||||
|
|
||||||
|
if (record === criteria.tournament_record) {
|
||||||
|
if (criteria.won_previous !== undefined && criteria.won_previous === true) {
|
||||||
|
const currentIndex = tournaments.findIndex(t => t.id === tournamentId);
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
const previousTournament = tournaments[currentIndex - 1];
|
||||||
|
if (previousTournament.winner_id === playerId) {
|
||||||
|
timesWent02++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timesWent02++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timesWent02 > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (criteria.consecutive_wins !== undefined) {
|
||||||
|
const tournaments = await pb.collection("tournaments").getFullList({
|
||||||
|
sort: 'start_time',
|
||||||
|
});
|
||||||
|
|
||||||
|
let consecutiveWins = 0;
|
||||||
|
let maxConsecutiveWins = 0;
|
||||||
|
|
||||||
|
for (const tournament of tournaments) {
|
||||||
|
if (!tournamentIds.has(tournament.id)) continue;
|
||||||
|
|
||||||
|
if (tournament.winner_id === playerId) {
|
||||||
|
consecutiveWins++;
|
||||||
|
maxConsecutiveWins = Math.max(maxConsecutiveWins, consecutiveWins);
|
||||||
|
} else {
|
||||||
|
consecutiveWins = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxConsecutiveWins >= criteria.consecutive_wins ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTargetProgress(badge: Badge): number {
|
||||||
|
if (badge.type === "manual") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const criteria = badge.criteria;
|
||||||
|
|
||||||
|
return (
|
||||||
|
criteria.matches_played ||
|
||||||
|
criteria.tournament_wins ||
|
||||||
|
criteria.tournaments_attended ||
|
||||||
|
criteria.overtime_matches ||
|
||||||
|
criteria.overtime_wins ||
|
||||||
|
criteria.consecutive_wins ||
|
||||||
|
1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async awardManualBadge(playerId: string, badgeId: string): Promise<BadgeProgress> {
|
||||||
|
// Get or create badge progress record
|
||||||
|
const existingProgress = await pb.collection("badge_progress").getFirstListItem(
|
||||||
|
`player = "${playerId}" && badge = "${badgeId}"`,
|
||||||
|
{ expand: 'badge' }
|
||||||
|
).catch(() => null);
|
||||||
|
|
||||||
|
if (existingProgress) {
|
||||||
|
// Update existing progress to mark as earned
|
||||||
|
const updated = await pb.collection("badge_progress").update(existingProgress.id, {
|
||||||
|
progress: 1,
|
||||||
|
earned: true,
|
||||||
|
}, { expand: 'badge' });
|
||||||
|
return transformBadgeProgress(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new progress record
|
||||||
|
const created = await pb.collection("badge_progress").create({
|
||||||
|
badge: badgeId,
|
||||||
|
player: playerId,
|
||||||
|
progress: 1,
|
||||||
|
earned: true,
|
||||||
|
}, { expand: 'badge' });
|
||||||
|
return transformBadgeProgress(created);
|
||||||
|
},
|
||||||
|
|
||||||
|
async migrateBadgeProgress(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
playersProcessed: number;
|
||||||
|
progressRecordsCreated: number;
|
||||||
|
totalBadgesEarned: number;
|
||||||
|
averageBadgesPerPlayer: string;
|
||||||
|
}> {
|
||||||
|
await this.clearAllBadgeProgress();
|
||||||
|
|
||||||
|
const badges = await this.listBadges();
|
||||||
|
|
||||||
|
const playerStats = await pb.collection("player_stats").getFullList<PlayerStats>();
|
||||||
|
const uniquePlayers = new Set(playerStats.map(s => s.player_id));
|
||||||
|
|
||||||
|
let totalProgressRecords = 0;
|
||||||
|
let totalBadgesEarned = 0;
|
||||||
|
|
||||||
|
for (const playerId of uniquePlayers) {
|
||||||
|
for (const badge of badges) {
|
||||||
|
try {
|
||||||
|
const progress = await this.calculateBadgeProgress(playerId, badge);
|
||||||
|
const target = this.getTargetProgress(badge);
|
||||||
|
const earned = progress >= target;
|
||||||
|
|
||||||
|
if (progress > 0 || earned) {
|
||||||
|
await this.createBadgeProgress({
|
||||||
|
badge: badge.id,
|
||||||
|
player: playerId,
|
||||||
|
progress: progress,
|
||||||
|
earned: earned,
|
||||||
|
});
|
||||||
|
|
||||||
|
totalProgressRecords++;
|
||||||
|
|
||||||
|
if (earned) {
|
||||||
|
totalBadgesEarned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error processing badge "${badge.name}" for player ${playerId}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
playersProcessed: uniquePlayers.size,
|
||||||
|
progressRecordsCreated: totalProgressRecords,
|
||||||
|
totalBadgesEarned: totalBadgesEarned,
|
||||||
|
averageBadgesPerPlayer: (totalBadgesEarned / uniquePlayers.size).toFixed(2),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -72,9 +72,17 @@ export function createTeamsService(pb: PocketBase) {
|
|||||||
|
|
||||||
const result = await pb.collection("teams").update(id, data);
|
const result = await pb.collection("teams").update(id, data);
|
||||||
|
|
||||||
return transformTeam(await pb.collection("teams").getOne(result.id, {
|
if (data instanceof FormData && data.has('logo')) {
|
||||||
expand: "players, tournaments"
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
const updated = await pb.collection("teams").getOne(result.id, {
|
||||||
|
expand: "players, tournaments",
|
||||||
|
// @ts-ignore - Add cache busting
|
||||||
|
$cancelKey: Date.now().toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformTeam(updated);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("PocketBase | Error updating team", error);
|
logger.error("PocketBase | Error updating team", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Match } from "@/features/matches/types";
|
|||||||
import { Player, PlayerInfo } from "@/features/players/types";
|
import { Player, PlayerInfo } from "@/features/players/types";
|
||||||
import { Team, TeamInfo } from "@/features/teams/types";
|
import { Team, TeamInfo } from "@/features/teams/types";
|
||||||
import { Tournament, TournamentInfo } from "@/features/tournaments/types";
|
import { Tournament, TournamentInfo } from "@/features/tournaments/types";
|
||||||
|
import { Badge, BadgeInfo, BadgeProgress } from "@/features/badges/types";
|
||||||
|
|
||||||
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
|
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
|
||||||
// this file transforms raw pocketbase results to our types
|
// this file transforms raw pocketbase results to our types
|
||||||
@@ -278,3 +279,36 @@ export function transformReaction(record: any) {
|
|||||||
match: record.match
|
match: record.match
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformBadgeInfo(record: any): BadgeInfo {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
key: record.key,
|
||||||
|
description: record.description,
|
||||||
|
type: record.type,
|
||||||
|
progressive: record.progressive,
|
||||||
|
order: record.order ?? 999,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformBadge(record: any): Badge {
|
||||||
|
return {
|
||||||
|
...transformBadgeInfo(record),
|
||||||
|
criteria: record.criteria,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformBadgeProgress(record: any): BadgeProgress {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
badge: record.expand?.badge ? transformBadgeInfo(record.expand.badge) : record.badge,
|
||||||
|
player: record.player,
|
||||||
|
progress: record.progress,
|
||||||
|
earned: record.earned,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import UserRoles from "supertokens-node/recipe/userroles";
|
|||||||
import { appInfo } from "./config";
|
import { appInfo } from "./config";
|
||||||
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
|
import PasswordlessDevelopmentMode from "./recipes/passwordless-development-mode";
|
||||||
import { logger } from "./";
|
import { logger } from "./";
|
||||||
import passwordlessTwilioVerify from "./recipes/passwordless-twilio-verify";
|
import PasswordlessTwilioVerify from "./recipes/passwordless-twilio-verify";
|
||||||
|
|
||||||
export const backendConfig = (): TypeInput => {
|
export const backendConfig = (): TypeInput => {
|
||||||
return {
|
return {
|
||||||
@@ -17,7 +17,8 @@ export const backendConfig = (): TypeInput => {
|
|||||||
},
|
},
|
||||||
appInfo,
|
appInfo,
|
||||||
recipeList: [
|
recipeList: [
|
||||||
passwordlessTwilioVerify.init(),
|
//PasswordlessTwilioVerify.init(),
|
||||||
|
PasswordlessDevelopmentMode.init(),
|
||||||
Session.init({
|
Session.init({
|
||||||
cookieSameSite: "lax",
|
cookieSameSite: "lax",
|
||||||
cookieSecure: import.meta.env.NODE_ENV === "production",
|
cookieSecure: import.meta.env.NODE_ENV === "production",
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import twilio, { type Twilio } from "twilio";
|
import twilio, { type Twilio } from "twilio";
|
||||||
|
|
||||||
const accountSid = process.env.TWILIO_ACCOUNT_SID!;
|
|
||||||
const authToken = process.env.TWILIO_AUTH_TOKEN!;
|
|
||||||
const serviceSid = process.env.TWILIO_SERVICE_SID!;
|
|
||||||
|
|
||||||
let client: Twilio;
|
let client: Twilio;
|
||||||
|
|
||||||
|
function getEnvVars() {
|
||||||
|
const accountSid = process.env.TWILIO_ACCOUNT_SID;
|
||||||
|
const authToken = process.env.TWILIO_AUTH_TOKEN;
|
||||||
|
const serviceSid = process.env.TWILIO_SERVICE_SID;
|
||||||
|
|
||||||
|
if (!accountSid || !authToken || !serviceSid) {
|
||||||
|
throw new Error(`Missing env vars. accountSid: ${!!accountSid}, authToken: ${!!authToken}, serviceSid: ${!!serviceSid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accountSid, authToken, serviceSid };
|
||||||
|
}
|
||||||
|
|
||||||
function getTwilioClient() {
|
function getTwilioClient() {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
const { accountSid, authToken } = getEnvVars();
|
||||||
client = twilio(accountSid, authToken);
|
client = twilio(accountSid, authToken);
|
||||||
}
|
}
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function sendVerifyCode(phoneNumber: string, code: string) {
|
export async function sendVerifyCode(phoneNumber: string, code: string) {
|
||||||
|
const { serviceSid } = getEnvVars();
|
||||||
|
|
||||||
const twilioClient = getTwilioClient();
|
const twilioClient = getTwilioClient();
|
||||||
|
|
||||||
const verification = await twilioClient!.verify.v2
|
const verification = await twilioClient!.verify.v2
|
||||||
@@ -32,6 +44,7 @@ export async function sendVerifyCode(phoneNumber: string, code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateVerify(sid: string) {
|
export async function updateVerify(sid: string) {
|
||||||
|
const { serviceSid } = getEnvVars();
|
||||||
const twilioClient = getTwilioClient();
|
const twilioClient = getTwilioClient();
|
||||||
|
|
||||||
const verification = await twilioClient!.verify.v2
|
const verification = await twilioClient!.verify.v2
|
||||||
|
|||||||
54
src/utils/activities.ts
Normal file
54
src/utils/activities.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
|
import { createMiddleware } from "@tanstack/react-start";
|
||||||
|
import { getRequest } from "@tanstack/react-start/server";
|
||||||
|
|
||||||
|
export const serverFnLoggingMiddleware = createMiddleware({
|
||||||
|
type: "function",
|
||||||
|
}).server(async ({ next, data, functionId, context }) => {
|
||||||
|
const request = getRequest();
|
||||||
|
|
||||||
|
const serverFnName = functionId.split('--')[1]?.split('_')[0] || 'unknown';
|
||||||
|
const userId = (context as any)?.metadata?.player_id || 'unknown';
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await next();
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pbAdmin.authPromise;
|
||||||
|
await pbAdmin.createActivity({
|
||||||
|
name: serverFnName,
|
||||||
|
player: userId !== 'unknown' ? userId : undefined,
|
||||||
|
duration,
|
||||||
|
success: true,
|
||||||
|
arguments: data,
|
||||||
|
user_agent: request.headers.get('user-agent') || undefined,
|
||||||
|
});
|
||||||
|
} catch (activityError) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pbAdmin.authPromise;
|
||||||
|
await pbAdmin.createActivity({
|
||||||
|
name: serverFnName,
|
||||||
|
player: userId !== 'unknown' ? userId : undefined,
|
||||||
|
duration,
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
arguments: data,
|
||||||
|
user_agent: request.headers.get('user-agent') || undefined,
|
||||||
|
});
|
||||||
|
} catch (activityError) {
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user