badges
This commit is contained in:
106
pb_migrations/1759285520_deleted_player_badge_progress.js
Normal file
106
pb_migrations/1759285520_deleted_player_badge_progress.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_badge_progress");
|
||||||
|
|
||||||
|
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": "relation_player",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 1,
|
||||||
|
"name": "player_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "pbc_1340419796",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation_badge",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 1,
|
||||||
|
"name": "badge_id",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number_current",
|
||||||
|
"max": null,
|
||||||
|
"min": 0,
|
||||||
|
"name": "current_progress",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool_earned",
|
||||||
|
"name": "is_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_badge_progress",
|
||||||
|
"indexes": [
|
||||||
|
"CREATE UNIQUE INDEX `idx_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);
|
||||||
|
})
|
||||||
28
pb_migrations/1759285544_updated_players.js
Normal file
28
pb_migrations/1759285544_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("relation2029409178")
|
||||||
|
|
||||||
|
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"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
28
pb_migrations/1759285564_updated_players.js
Normal file
28
pb_migrations/1759285564_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||||
|
|
||||||
|
// 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": "relation2813965191",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "featured_badge",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
159
pb_migrations/1759285574_deleted_badges.js
Normal file
159
pb_migrations/1759285574_deleted_badges.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/// <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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text_icon_key",
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"name": "icon_key",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json1578432567",
|
||||||
|
"maxSize": 2000000,
|
||||||
|
"name": "criteria",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number_sort",
|
||||||
|
"max": null,
|
||||||
|
"min": 0,
|
||||||
|
"name": "sort_order",
|
||||||
|
"onlyInt": true,
|
||||||
|
"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);
|
||||||
|
})
|
||||||
133
pb_migrations/1759285803_created_badges.js
Normal file
133
pb_migrations/1759285803_created_badges.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/// <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": "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/1759285923_created_badge_progress.js
Normal file
104
pb_migrations/1759285923_created_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_3342597247",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "badge_progress",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3342597247");
|
||||||
|
|
||||||
|
return app.delete(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)
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
<CaretRightIcon style={{ marginLeft: "auto" }} size={20} />
|
{loading ? (
|
||||||
|
<Loader size="sm" style={{ marginLeft: "auto" }} />
|
||||||
|
) : (
|
||||||
|
<CaretRightIcon style={{ marginLeft: "auto" }} size={20} />
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ import {
|
|||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
TreeStructureIcon,
|
TreeStructureIcon,
|
||||||
TrophyIcon,
|
TrophyIcon,
|
||||||
|
MedalIcon,
|
||||||
} 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 +28,12 @@ const AdminPage = () => {
|
|||||||
Icon={TrophyIcon}
|
Icon={TrophyIcon}
|
||||||
to="/admin/tournaments"
|
to="/admin/tournaments"
|
||||||
/>
|
/>
|
||||||
|
<ListButton
|
||||||
|
label="Migrate Badge Progress"
|
||||||
|
Icon={MedalIcon}
|
||||||
|
onClick={handleMigrateBadges}
|
||||||
|
loading={isMigrating}
|
||||||
|
/>
|
||||||
<ListButton
|
<ListButton
|
||||||
label="Open Pocketbase"
|
label="Open Pocketbase"
|
||||||
Icon={DatabaseIcon}
|
Icon={DatabaseIcon}
|
||||||
|
|||||||
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, Card, Skeleton, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
const BadgeShowcaseSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<Box mb="lg">
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p={0}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
background: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||||
|
borderBottom: '1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} tt="uppercase" c="dimmed" style={{ letterSpacing: '0.5px' }}>
|
||||||
|
Badges
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))',
|
||||||
|
gap: 'var(--mantine-spacing-sm)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={i}
|
||||||
|
height={70}
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BadgeShowcaseSkeleton;
|
||||||
189
src/features/badges/components/badge-showcase.tsx
Normal file
189
src/features/badges/components/badge-showcase.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { Box, Text, Tooltip, Card } from "@mantine/core";
|
||||||
|
import { usePlayerBadges, useAllBadges } from "../queries";
|
||||||
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
import { Badge, BadgeProgress } from "../types";
|
||||||
|
import { useMemo } from "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">
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p={0}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
background: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||||
|
borderBottom: '1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" fw={600} tt="uppercase" c="dimmed" style={{ letterSpacing: '0.5px' }}>
|
||||||
|
Badges
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
mah={120}
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))',
|
||||||
|
gap: 'var(--mantine-spacing-sm)',
|
||||||
|
overflow: 'scroll',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badgesToDisplay.map((display) => (
|
||||||
|
<Tooltip
|
||||||
|
key={display.badge.id}
|
||||||
|
label={
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" fw={600} mb={4}>
|
||||||
|
{display.badge.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" mb={4}>
|
||||||
|
{display.badge.description}
|
||||||
|
</Text>
|
||||||
|
{isCurrentUser && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Progress: {display.progressText}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
multiline
|
||||||
|
w={220}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
padding="sm"
|
||||||
|
radius="md"
|
||||||
|
shadow={display.earned ? "xs" : undefined}
|
||||||
|
style={(theme) => ({
|
||||||
|
opacity: display.earned ? 1 : 0.35,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
minHeight: 70,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderStyle: display.earned ? 'solid' : 'dashed',
|
||||||
|
':hover': {
|
||||||
|
transform: display.earned ? 'translateY(-2px)' : 'none',
|
||||||
|
boxShadow: display.earned ? theme.shadows.sm : undefined,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
ta="center"
|
||||||
|
fw={display.earned ? 600 : 500}
|
||||||
|
c={display.earned ? undefined : "dimmed"}
|
||||||
|
style={{ lineHeight: 1.3 }}
|
||||||
|
>
|
||||||
|
{display.badge.name}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</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,24 @@
|
|||||||
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())
|
||||||
|
);
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Box } from "@mantine/core";
|
import { Box } 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,14 @@ const Profile = ({ id }: ProfileProps) => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <StatsOverview statsData={stats} isLoading={statsLoading} />,
|
content: (
|
||||||
|
<>
|
||||||
|
<Suspense fallback={<BadgeShowcaseSkeleton />}>
|
||||||
|
<BadgeShowcase playerId={id} />
|
||||||
|
</Suspense>
|
||||||
|
<StatsOverview statsData={stats} isLoading={statsLoading} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matches",
|
label: "Matches",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 { createActivitiesService } from "./services/activities";
|
||||||
|
import { createBadgesService } from "./services/badges";
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class PocketBaseAdminClient {
|
|||||||
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, createActivitiesService(this.pb));
|
||||||
|
Object.assign(this, createBadgesService(this.pb));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +59,8 @@ interface AdminClient
|
|||||||
ReturnType<typeof createTournamentsService>,
|
ReturnType<typeof createTournamentsService>,
|
||||||
ReturnType<typeof createMatchesService>,
|
ReturnType<typeof createMatchesService>,
|
||||||
ReturnType<typeof createReactionsService>,
|
ReturnType<typeof createReactionsService>,
|
||||||
ReturnType<typeof createActivitiesService> {
|
ReturnType<typeof createActivitiesService>,
|
||||||
|
ReturnType<typeof createBadgesService> {
|
||||||
authPromise: Promise<void>;
|
authPromise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
407
src/lib/pocketbase/services/badges.ts
Normal file
407
src/lib/pocketbase/services/badges.ts
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
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 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),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user