diff --git a/Teams-2.xlsx b/Teams-2.xlsx new file mode 100644 index 0000000..9012367 Binary files /dev/null and b/Teams-2.xlsx differ diff --git a/bun.lock b/bun.lock index 1d51468..3bd1fcb 100644 --- a/bun.lock +++ b/bun.lock @@ -46,6 +46,7 @@ "supertokens-web-js": "^0.15.0", "twilio": "^5.8.0", "vaul": "^1.1.2", + "xlsx": "^0.18.5", "zod": "^4.0.15", "zustand": "^5.0.7", }, @@ -530,6 +531,8 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="], @@ -582,6 +585,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001745", "", {}, "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ=="], + "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], + "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], @@ -592,6 +597,8 @@ "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -604,6 +611,8 @@ "core-js-pure": ["core-js-pure@3.45.1", "", {}, "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ=="], + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], @@ -732,6 +741,8 @@ "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], + "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], "framer-motion": ["framer-motion@12.23.22", "", { "dependencies": { "motion-dom": "^12.23.21", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA=="], @@ -1118,6 +1129,8 @@ "srvx": ["srvx@0.8.7", "", { "dependencies": { "cookie-es": "^2.0.0" }, "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-g3+15LlwVOGL2QpoTPZlvRjg+9a5Tx/69CatXjFP6txvhIaW2FmGyzJfb8yft5wyfGddvJmP/Yx+e/uNDMRSLQ=="], + "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], "sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="], @@ -1212,8 +1225,14 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], + + "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + "xmlbuilder": ["xmlbuilder@13.0.2", "", {}, "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="], "xmlbuilder2": ["xmlbuilder2@3.1.1", "", { "dependencies": { "@oozcitak/dom": "1.15.10", "@oozcitak/infra": "1.0.8", "@oozcitak/util": "8.3.8", "js-yaml": "3.14.1" } }, "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw=="], diff --git a/package.json b/package.json index 8452087..9469827 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "supertokens-web-js": "^0.15.0", "twilio": "^5.8.0", "vaul": "^1.1.2", + "xlsx": "^0.18.5", "zod": "^4.0.15", "zustand": "^5.0.7" }, diff --git a/pb_migrations/1760556705_updated_teams.js b/pb_migrations/1760556705_updated_teams.js new file mode 100644 index 0000000..fb8db15 --- /dev/null +++ b/pb_migrations/1760556705_updated_teams.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1568971955") + + // add field + collection.fields.addAt(14, new Field({ + "hidden": false, + "id": "bool3523658193", + "name": "private", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1568971955") + + // remove field + collection.fields.removeById("bool3523658193") + + return app.save(collection) +}) diff --git a/pb_migrations/1760556851_updated_tournaments.js b/pb_migrations/1760556851_updated_tournaments.js new file mode 100644 index 0000000..70b1438 --- /dev/null +++ b/pb_migrations/1760556851_updated_tournaments.js @@ -0,0 +1,24 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // add field + collection.fields.addAt(12, new Field({ + "hidden": false, + "id": "bool3403970290", + "name": "regional", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // remove field + collection.fields.removeById("bool3403970290") + + return app.save(collection) +}) diff --git a/pb_migrations/1760556905_updated_tournaments.js b/pb_migrations/1760556905_updated_tournaments.js new file mode 100644 index 0000000..a3456c7 --- /dev/null +++ b/pb_migrations/1760556905_updated_tournaments.js @@ -0,0 +1,31 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // add field + collection.fields.addAt(13, new Field({ + "hidden": false, + "id": "select3736761055", + "maxSelect": 1, + "name": "format", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "single_elim", + "double_elim", + "groups", + "swiss" + ] + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // remove field + collection.fields.removeById("select3736761055") + + return app.save(collection) +}) diff --git a/pb_migrations/1760559911_created_player_regional_stats.js b/pb_migrations/1760559911_created_player_regional_stats.js new file mode 100644 index 0000000..ad1d465 --- /dev/null +++ b/pb_migrations/1760559911_created_player_regional_stats.js @@ -0,0 +1,165 @@ +/// +migrate((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" + }, + { + "hidden": false, + "id": "json4231605813", + "maxSize": 1, + "name": "player_name", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "number103159226", + "max": null, + "min": null, + "name": "matches", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3837590211", + "max": null, + "min": null, + "name": "tournaments", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json2732118329", + "maxSize": 1, + "name": "wins", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json724428801", + "maxSize": 1, + "name": "losses", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3154249934", + "maxSize": 1, + "name": "total_cups_made", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3227208027", + "maxSize": 1, + "name": "total_cups_against", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json2379943496", + "maxSize": 1, + "name": "win_percentage", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3165107022", + "maxSize": 1, + "name": "avg_cups_per_match", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3041953980", + "maxSize": 1, + "name": "margin_of_victory", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json1531431708", + "maxSize": 1, + "name": "margin_of_loss", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "id": "pbc_4086490894", + "indexes": [], + "listRule": null, + "name": "player_regional_stats", + "system": false, + "type": "view", + "updateRule": null, + "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended' AND\n tour.regional = true\n GROUP BY p.id", + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_4086490894"); + + return app.delete(collection); +}) diff --git a/pb_migrations/1760559954_created_player_mainline_stats.js b/pb_migrations/1760559954_created_player_mainline_stats.js new file mode 100644 index 0000000..5c67082 --- /dev/null +++ b/pb_migrations/1760559954_created_player_mainline_stats.js @@ -0,0 +1,165 @@ +/// +migrate((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" + }, + { + "hidden": false, + "id": "json4231605813", + "maxSize": 1, + "name": "player_name", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "number103159226", + "max": null, + "min": null, + "name": "matches", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "number3837590211", + "max": null, + "min": null, + "name": "tournaments", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json2732118329", + "maxSize": 1, + "name": "wins", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json724428801", + "maxSize": 1, + "name": "losses", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3154249934", + "maxSize": 1, + "name": "total_cups_made", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3227208027", + "maxSize": 1, + "name": "total_cups_against", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json2379943496", + "maxSize": 1, + "name": "win_percentage", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3165107022", + "maxSize": 1, + "name": "avg_cups_per_match", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json3041953980", + "maxSize": 1, + "name": "margin_of_victory", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "json1531431708", + "maxSize": 1, + "name": "margin_of_loss", + "presentable": false, + "required": false, + "system": false, + "type": "json" + } + ], + "id": "pbc_15286826", + "indexes": [], + "listRule": null, + "name": "player_mainline_stats", + "system": false, + "type": "view", + "updateRule": null, + "viewQuery": "SELECT\n p.id as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n COUNT(m.id) as matches,\n COUNT(DISTINCT m.tournament) as tournaments,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) as wins,\n SUM(CASE\n WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR\n (m.away = t.id AND m.away_cups < m.home_cups)\n THEN 1 ELSE 0\n END) as losses,\n SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) as total_cups_made,\n SUM(CASE\n WHEN m.home = t.id THEN m.away_cups\n WHEN m.away = t.id THEN m.home_cups\n ELSE 0\n END) as total_cups_against,\n -- Win percentage\n ROUND((CAST(SUM(CASE\n WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR\n (m.away = t.id AND m.away_cups > m.home_cups)\n THEN 1 ELSE 0\n END) AS REAL) / COUNT(m.id)) * 100, 2) as win_percentage,\n -- Average cups per match\n ROUND(CAST(SUM(CASE\n WHEN m.home = t.id THEN m.home_cups\n WHEN m.away = t.id THEN m.away_cups\n ELSE 0\n END) AS REAL) / COUNT(m.id), 2) as avg_cups_per_match,\n -- Margin of Victory\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups > m.away_cups\n THEN m.home_cups - m.away_cups\n WHEN m.away = t.id AND m.away_cups > m.home_cups\n THEN m.away_cups - m.home_cups\n ELSE NULL\n END), 2) as margin_of_victory,\n -- Margin of Loss\n ROUND(AVG(CASE\n WHEN m.home = t.id AND m.home_cups < m.away_cups\n THEN m.away_cups - m.home_cups\n WHEN m.away = t.id AND m.away_cups < m.home_cups\n THEN m.home_cups - m.away_cups\n ELSE NULL\n END), 2) as margin_of_loss\n FROM players p, teams t, matches m, tournaments tour\n WHERE\n t.players LIKE '%\"' || p.id || '\"%' AND\n (m.home = t.id OR m.away = t.id) AND\n m.tournament = tour.id AND\n m.status = 'ended' AND\n (tour.regional = false OR tour.regional IS NULL)\n GROUP BY p.id", + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_15286826"); + + return app.delete(collection); +}) diff --git a/pb_migrations/1760585178_updated_tournaments.js b/pb_migrations/1760585178_updated_tournaments.js new file mode 100644 index 0000000..df372ba --- /dev/null +++ b/pb_migrations/1760585178_updated_tournaments.js @@ -0,0 +1,48 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // update field + collection.fields.addAt(13, new Field({ + "hidden": false, + "id": "select3736761055", + "maxSelect": 1, + "name": "format", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "single_elim", + "double_elim", + "groups", + "swiss", + "swiss_bracket", + "round_robin" + ] + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_340646327") + + // update field + collection.fields.addAt(13, new Field({ + "hidden": false, + "id": "select3736761055", + "maxSelect": 1, + "name": "format", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "single_elim", + "double_elim", + "groups", + "swiss" + ] + })) + + return app.save(collection) +}) diff --git a/src/app/routes/_authed/stats.tsx b/src/app/routes/_authed/stats.tsx index 65f385b..030bf57 100644 --- a/src/app/routes/_authed/stats.tsx +++ b/src/app/routes/_authed/stats.tsx @@ -1,17 +1,19 @@ import { createFileRoute } from "@tanstack/react-router"; import { playerQueries } from "@/features/players/queries"; import PlayerStatsTable from "@/features/players/components/player-stats-table"; -import { Suspense } from "react"; +import { Suspense, useState, useDeferredValue } from "react"; import PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton"; import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch"; import LeagueHeadToHead from "@/features/players/components/league-head-to-head"; -import { Box, Loader, Tabs } from "@mantine/core"; +import { Box, Loader, Tabs, Button, Group, Container, Stack } from "@mantine/core"; export const Route = createFileRoute("/_authed/stats")({ component: Stats, beforeLoad: ({ context }) => { const queryClient = context.queryClient; - prefetchServerQuery(queryClient, playerQueries.allStats()); + prefetchServerQuery(queryClient, playerQueries.allStats('all')); + prefetchServerQuery(queryClient, playerQueries.allStats('mainline')); + prefetchServerQuery(queryClient, playerQueries.allStats('regional')); }, loader: () => ({ withPadding: false, @@ -24,6 +26,10 @@ export const Route = createFileRoute("/_authed/stats")({ }); function Stats() { + const [viewType, setViewType] = useState<'all' | 'mainline' | 'regional'>('all'); + const deferredViewType = useDeferredValue(viewType); + const isStale = viewType !== deferredViewType; + return ( @@ -32,9 +38,38 @@ function Stats() { - }> - - + + + + + + + + + }> + + + + + diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx index eb41d1a..827942c 100644 --- a/src/features/matches/components/match-card.tsx +++ b/src/features/matches/components/match-card.tsx @@ -20,6 +20,7 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => { const isHomeWin = match.home_cups > match.away_cups; const isAwayWin = match.away_cups > match.home_cups; const isStarted = match.status === "started"; + const hasPrivate = match.home?.private || match.away?.private; const handleHomeTeamClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -65,13 +66,17 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => { {match.tournament.name} - - - - Round {match.round + 1} - {match.is_losers_bracket && " (Losers)"} - + {!match.tournament.regional && ( + <> + - + + Round {match.round + 1} + {match.is_losers_bracket && " (Losers)"} + + + )} - {match.home && match.away && !hideH2H && ( + {match.home && match.away && !hideH2H && !hasPrivate && ( { return undefined; } + const isRegional = filteredMatches[0]?.tournament?.regional; + return ( + {isRegional && ( + + Matches for regionals are unordered + + )} {filteredMatches.map((match, index) => (
{ ); }; -const PlayerStatsTableSkeleton = () => { +interface PlayerStatsTableSkeletonProps { + hideFilters?: boolean; +} + +const PlayerStatsTableSkeleton = ({ hideFilters = false }: PlayerStatsTableSkeletonProps) => { return ( diff --git a/src/features/players/components/player-stats-table.tsx b/src/features/players/components/player-stats-table.tsx index e602c08..f2b9fba 100644 --- a/src/features/players/components/player-stats-table.tsx +++ b/src/features/players/components/player-stats-table.tsx @@ -142,8 +142,12 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU ); }); -const PlayerStatsTable = () => { - const { data: playerStats } = useAllPlayerStats(); +interface PlayerStatsTableProps { + viewType?: 'all' | 'mainline' | 'regional'; +} + +const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => { + const { data: playerStats } = useAllPlayerStats(viewType); const navigate = useNavigate(); const [search, setSearch] = useState(""); const [sortConfig, setSortConfig] = useState({ @@ -292,21 +296,19 @@ const PlayerStatsTable = () => { if (playerStats.length === 0) { return ( - - - - - - - No Stats Available - - - + + + + + + No Stats Available + + ); } return ( - + Showing {filteredAndSortedStats.length} of {playerStats.length} players diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx index 33c397d..ff524bb 100644 --- a/src/features/players/components/profile/index.tsx +++ b/src/features/players/components/profile/index.tsx @@ -1,10 +1,10 @@ -import { Box, Stack, Text, Divider } from "@mantine/core"; -import { Suspense } from "react"; +import { Box, Stack, Text, Divider, Group, Button } from "@mantine/core"; +import { Suspense, useState, useDeferredValue } from "react"; import Header from "./header"; import SwipeableTabs from "@/components/swipeable-tabs"; import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries"; import TeamList from "@/features/teams/components/team-list"; -import StatsOverview from "@/components/stats-overview"; +import StatsOverview, { StatsSkeleton } from "@/components/stats-overview"; 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"; @@ -13,10 +13,38 @@ interface ProfileProps { id: string; } +const StatsWithFilter = ({ id }: { id: string }) => { + const [viewType, setViewType] = useState<'all' | 'mainline' | 'regional'>('all'); + const deferredViewType = useDeferredValue(viewType); + const isStale = viewType !== deferredViewType; + + return ( + + + Statistics + + + + + + + + }> + + + + + ); +}; + +const StatsContent = ({ id, viewType }: { id: string; viewType: 'all' | 'mainline' | 'regional' }) => { + const { data: stats, isLoading: statsLoading } = usePlayerStats(id, viewType); + return ; +}; + const Profile = ({ id }: ProfileProps) => { const { data: player } = usePlayer(id); const { data: matches } = usePlayerMatches(id); - const { data: stats, isLoading: statsLoading } = usePlayerStats(id); const tabs = [ { @@ -29,10 +57,7 @@ const Profile = ({ id }: ProfileProps) => { - - Statistics - - + , }, { diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts index 3a871b4..eb61288 100644 --- a/src/features/players/queries.ts +++ b/src/features/players/queries.ts @@ -7,8 +7,8 @@ export const playerKeys = { details: (id: string) => ['players', 'details', id], unassociated: ['players','unassociated'], unenrolled: (tournamentId: string) => ['players', 'unenrolled', tournamentId], - stats: (id: string) => ['players', 'stats', id], - allStats: ['players', 'stats', 'all'], + stats: (id: string, viewType?: 'all' | 'mainline' | 'regional') => ['players', 'stats', id, viewType ?? 'all'], + allStats: (viewType?: 'all' | 'mainline' | 'regional') => ['players', 'stats', 'all', viewType ?? 'all'], matches: (id: string) => ['players', 'matches', id], activity: ['players', 'activity'], }; @@ -34,13 +34,13 @@ export const playerQueries = { queryKey: playerKeys.unenrolled(tournamentId), queryFn: async () => await getUnenrolledPlayers({ data: tournamentId }) }), - stats: (id: string) => ({ - queryKey: playerKeys.stats(id), - queryFn: async () => await getPlayerStats({ data: id }) + stats: (id: string, viewType?: 'all' | 'mainline' | 'regional') => ({ + queryKey: playerKeys.stats(id, viewType), + queryFn: async () => await getPlayerStats({ data: { playerId: id, viewType } }) }), - allStats: () => ({ - queryKey: playerKeys.allStats, - queryFn: async () => await getAllPlayerStats() + allStats: (viewType?: 'all' | 'mainline' | 'regional') => ({ + queryKey: playerKeys.allStats(viewType), + queryFn: async () => await getAllPlayerStats({ data: viewType }) }), matches: (id: string) => ({ queryKey: playerKeys.matches(id), @@ -84,11 +84,11 @@ export const usePlayers = () => export const useUnassociatedPlayers = () => useServerSuspenseQuery(playerQueries.unassociated()); -export const usePlayerStats = (id: string) => - useServerSuspenseQuery(playerQueries.stats(id)); +export const usePlayerStats = (id: string, viewType?: 'all' | 'mainline' | 'regional') => + useServerSuspenseQuery(playerQueries.stats(id, viewType)); -export const useAllPlayerStats = () => - useServerSuspenseQuery(playerQueries.allStats()); +export const useAllPlayerStats = (viewType?: 'all' | 'mainline' | 'regional') => + useServerSuspenseQuery(playerQueries.allStats(viewType)); export const usePlayerMatches = (id: string) => useServerSuspenseQuery(playerQueries.matches(id)); diff --git a/src/features/players/server.ts b/src/features/players/server.ts index 399dba3..56874e1 100644 --- a/src/features/players/server.ts +++ b/src/features/players/server.ts @@ -136,16 +136,20 @@ export const getUnassociatedPlayers = createServerFn() ); export const getPlayerStats = createServerFn() - .inputValidator(z.string()) + .inputValidator(z.object({ + playerId: z.string(), + viewType: z.enum(['all', 'mainline', 'regional']).optional() + })) .middleware([superTokensFunctionMiddleware]) .handler(async ({ data }) => - toServerResult(async () => await pbAdmin.getPlayerStats(data)) + toServerResult(async () => await pbAdmin.getPlayerStats(data.playerId, data.viewType)) ); export const getAllPlayerStats = createServerFn() + .inputValidator(z.enum(['all', 'mainline', 'regional']).optional()) .middleware([superTokensFunctionMiddleware]) - .handler(async () => - toServerResult(async () => await pbAdmin.getAllPlayerStats()) + .handler(async ({ data }) => + toServerResult(async () => await pbAdmin.getAllPlayerStats(data)) ); export const getPlayerMatches = createServerFn() diff --git a/src/features/teams/components/team-list.tsx b/src/features/teams/components/team-list.tsx index 0812a84..2c006e0 100644 --- a/src/features/teams/components/team-list.tsx +++ b/src/features/teams/components/team-list.tsx @@ -55,10 +55,10 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => { const navigate = useNavigate(); const handleClick = useCallback( - (teamId: string) => { + (teamId: string, priv: boolean) => { if (onTeamClick) { onTeamClick(teamId); - } else { + } else if (!priv) { navigate({ to: `/teams/${teamId}` }); } }, @@ -100,7 +100,7 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => { /> } style={{ cursor: "pointer" }} - onClick={() => handleClick(team.id)} + onClick={() => handleClick(team.id, team.private)} styles={{ itemWrapper: { width: "100%" }, itemLabel: { width: "100%" }, diff --git a/src/features/teams/types.ts b/src/features/teams/types.ts index c034a53..4b7cafa 100644 --- a/src/features/teams/types.ts +++ b/src/features/teams/types.ts @@ -19,6 +19,7 @@ export interface Team { updated: string; players: PlayerInfo[]; tournaments: TournamentInfo[]; + private: boolean; } export interface TeamInfo { @@ -28,6 +29,7 @@ export interface TeamInfo { accent_color: string; logo?: string; players: PlayerInfo[]; + private: boolean; } export const teamInputSchema = z diff --git a/src/features/tournaments/components/tournament-card.tsx b/src/features/tournaments/components/tournament-card.tsx index 2aebe0d..88c864d 100644 --- a/src/features/tournaments/components/tournament-card.tsx +++ b/src/features/tournaments/components/tournament-card.tsx @@ -58,7 +58,7 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => { {tournament.name} - {(tournament.first_place || tournament.second_place || tournament.third_place) && ( + {((tournament.first_place || tournament.second_place || tournament.third_place) && !tournament.regional) && ( {tournament.first_place && ( { return ( - + {tournament.regional && ( + }> + Regional tournaments are a work in progress. Some features might not work as expected. + + )} + {!tournament.regional && } { const criteria = badge.criteria; - const stats = await pb.collection("player_stats").getFirstListItem( + const stats = await pb.collection("player_mainline_stats").getFirstListItem( `player_id = "${playerId}"` ).catch(() => null); @@ -103,8 +103,8 @@ export function createBadgesService(pb: PocketBase) { 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', + filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && ot_count > 0 && (tournament.regional = false || tournament.regional = null)`, + expand: 'tournament,home,away,home.players,away.players', }); if (criteria.overtime_matches !== undefined) { @@ -131,8 +131,8 @@ export function createBadgesService(pb: PocketBase) { 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', + filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`, + expand: 'tournament,home,away,home.players,away.players', }); const bigWins = matches.filter(m => { @@ -159,7 +159,7 @@ export function createBadgesService(pb: PocketBase) { const criteria = badge.criteria; const matches = await pb.collection("matches").getFullList({ - filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended"`, + filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`, expand: 'tournament,home,away,home.players,away.players', }); @@ -209,8 +209,8 @@ export function createBadgesService(pb: PocketBase) { for (const tournamentId of tournamentIds) { const tournamentMatches = await pb.collection("matches").getFullList({ - filter: `tournament = "${tournamentId}" && status = "ended"`, - expand: 'home,away,home.players,away.players', + filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`, + expand: 'tournament,home,away,home.players,away.players', }); const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket); @@ -241,8 +241,8 @@ export function createBadgesService(pb: PocketBase) { for (const tournamentId of tournamentIds) { const tournamentMatches = await pb.collection("matches").getFullList({ - filter: `tournament = "${tournamentId}" && status = "ended"`, - expand: 'home,away,home.players,away.players', + filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`, + expand: 'tournament,home,away,home.players,away.players', }); if (criteria.placement === 2) { @@ -293,6 +293,7 @@ export function createBadgesService(pb: PocketBase) { if (criteria.tournament_record !== undefined) { const tournaments = await pb.collection("tournaments").getFullList({ + filter: 'regional = false || regional = null', sort: 'start_time', }); @@ -344,6 +345,7 @@ export function createBadgesService(pb: PocketBase) { if (criteria.consecutive_wins !== undefined) { const tournaments = await pb.collection("tournaments").getFullList({ + filter: 'regional = false || regional = null', sort: 'start_time', }); diff --git a/src/lib/pocketbase/services/matches.ts b/src/lib/pocketbase/services/matches.ts index 8ce3943..9bcb325 100644 --- a/src/lib/pocketbase/services/matches.ts +++ b/src/lib/pocketbase/services/matches.ts @@ -32,7 +32,7 @@ export function createMatchesService(pb: PocketBase) { }, async createMatch(data: MatchInput): Promise { - logger.info("PocketBase | Creating match", data); + // logger.info("PocketBase | Creating match", data); const result = await pb.collection("matches").create(data); return result; }, @@ -92,23 +92,40 @@ export function createMatchesService(pb: PocketBase) { return []; } - const filterConditions: string[] = []; - player1TeamIds.forEach(team1Id => { - player2TeamIds.forEach(team2Id => { - filterConditions.push(`(home="${team1Id}" && away="${team2Id}")`); - filterConditions.push(`(home="${team2Id}" && away="${team1Id}")`); + const allTeamIds = [...new Set([...player1TeamIds, ...player2TeamIds])]; + const batchSize = 10; + const allMatches: any[] = []; + + for (let i = 0; i < allTeamIds.length; i += batchSize) { + const batch = allTeamIds.slice(i, i + batchSize); + const teamFilters = batch.map(id => `home="${id}" || away="${id}"`).join(' || '); + + const results = await pb.collection("matches").getFullList({ + filter: teamFilters, + expand: "tournament, home, away, home.players, away.players", + sort: "-created", }); - }); - const filter = filterConditions.join(" || "); + allMatches.push(...results); + } - const results = await pb.collection("matches").getFullList({ - filter, - expand: "tournament, home, away, home.players, away.players", - sort: "-created", - }); + const uniqueMatches = Array.from( + new Map(allMatches.map(m => [m.id, m])).values() + ); - return results.map(match => transformMatch(match)); + return uniqueMatches + .filter(match => { + const homeTeamId = typeof match.home === 'string' ? match.home : match.home?.id; + const awayTeamId = typeof match.away === 'string' ? match.away : match.away?.id; + + const player1InHome = player1TeamIds.includes(homeTeamId); + const player1InAway = player1TeamIds.includes(awayTeamId); + const player2InHome = player2TeamIds.includes(homeTeamId); + const player2InAway = player2TeamIds.includes(awayTeamId); + + return (player1InHome && player2InAway) || (player1InAway && player2InHome); + }) + .map(match => transformMatch(match)); }, async getMatchesBetweenTeams(team1Id: string, team2Id: string): Promise { diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts index 89a720d..2d4c638 100644 --- a/src/lib/pocketbase/services/players.ts +++ b/src/lib/pocketbase/services/players.ts @@ -8,7 +8,6 @@ import type { import type { Match } from "@/features/matches/types"; import { transformPlayer, transformPlayerInfo, transformMatch } from "@/lib/pocketbase/util/transform-types"; import PocketBase from "pocketbase"; -import { DataFetchOptions } from "./base"; export function createPlayersService(pb: PocketBase) { return { @@ -65,9 +64,15 @@ export function createPlayersService(pb: PocketBase) { return result.map(transformPlayer); }, - async getPlayerStats(playerId: string): Promise { + async getPlayerStats(playerId: string, viewType: 'all' | 'mainline' | 'regional' = 'all'): Promise { try { - const result = await pb.collection("player_stats").getFirstListItem( + const collectionMap = { + all: 'player_stats', + mainline: 'player_mainline_stats', + regional: 'player_regional_stats', + }; + + const result = await pb.collection(collectionMap[viewType]).getFirstListItem( `player_id = "${playerId}"` ); return result; @@ -90,8 +95,14 @@ export function createPlayersService(pb: PocketBase) { } }, - async getAllPlayerStats(): Promise { - const result = await pb.collection("player_stats").getFullList({ + async getAllPlayerStats(viewType: 'all' | 'mainline' | 'regional' = 'all'): Promise { + const collectionMap = { + all: 'player_stats', + mainline: 'player_mainline_stats', + regional: 'player_regional_stats', + }; + + const result = await pb.collection(collectionMap[viewType]).getFullList({ sort: "-win_percentage,-total_cups_made", }); return result; diff --git a/src/lib/pocketbase/services/tournaments.ts b/src/lib/pocketbase/services/tournaments.ts index fe483dd..592a0e3 100644 --- a/src/lib/pocketbase/services/tournaments.ts +++ b/src/lib/pocketbase/services/tournaments.ts @@ -34,7 +34,7 @@ export function createTournamentsService(pb: PocketBase) { .getFirstListItem('', { expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players", - sort: "-created", + sort: "-start_time", } ); @@ -52,7 +52,7 @@ export function createTournamentsService(pb: PocketBase) { .collection("tournaments") .getFullList({ expand: "teams,teams.players,matches", - sort: "-created", + sort: "-start_time", }); const tournamentsWithStats = await Promise.all(result.map(async (tournament) => { diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts index 03c583d..900de8f 100644 --- a/src/lib/pocketbase/util/transform-types.ts +++ b/src/lib/pocketbase/util/transform-types.ts @@ -1,4 +1,3 @@ -import { Reaction } from "@/features/matches/server"; import { Match } from "@/features/matches/types"; import { Player, PlayerInfo } from "@/features/players/types"; import { Team, TeamInfo } from "@/features/teams/types"; @@ -25,7 +24,8 @@ export function transformTeamInfo(record: any): TeamInfo { primary_color: record.primary_color, accent_color: record.accent_color, players, - logo: record.logo + logo: record.logo, + private: record.private || false, }; } @@ -107,6 +107,7 @@ export const transformTournamentInfo = (record: any): TournamentInfo => { end_time: record.end_time, logo: record.logo, glitch_logo: record.glitch_logo, + regional: record.regional || false, first_place, second_place, third_place, @@ -116,6 +117,7 @@ export const transformTournamentInfo = (record: any): TournamentInfo => { export function transformPlayer(record: any): Player { const teams = record.expand?.teams + ?.filter((team: any) => !team.private) ?.sort((a: any, b: any) => new Date(a.created) < new Date(b.created) ? -1 : 0 ) @@ -135,13 +137,6 @@ export function transformPlayer(record: any): Player { export function transformFreeAgent(record: any) { const player = record.expand?.player ? transformPlayerInfo(record.expand.player) : undefined; - const tournaments = - record.expand?.tournaments - ?.sort((a: any, b: any) => - new Date(a.created!) < new Date(b.created!) ? -1 : 0 - ) - ?.map(transformTournamentInfo) ?? []; - return { id: record.id as string, phone: record.phone as string, @@ -180,6 +175,7 @@ export function transformTeam(record: any): Team { updated: record.updated, players, tournaments, + private: record.private || false, }; } @@ -264,6 +260,7 @@ export function transformTournament(record: any, isAdmin: boolean = false): Tour end_time: record.end_time, created: record.created, updated: record.updated, + regional: record.regional || false, teams, matches, first_place, diff --git a/test.js b/test.js new file mode 100644 index 0000000..739a63c --- /dev/null +++ b/test.js @@ -0,0 +1,269 @@ +import PocketBase from "pocketbase"; +import * as xlsx from "xlsx"; +import { nanoid } from "nanoid"; + +import { createTeamsService } from "./src/lib/pocketbase/services/teams.ts"; +import { createPlayersService } from "./src/lib/pocketbase/services/players.ts"; +import { createMatchesService } from "./src/lib/pocketbase/services/matches.ts"; +import { createTournamentsService } from "./src/lib/pocketbase/services/tournaments.ts"; + +const POCKETBASE_URL = "http://127.0.0.1:8090"; +const EXCEL_FILE_PATH = "./Teams-2.xlsx"; + +const ADMIN_EMAIL = "kyle.yohler@gmail.com"; +const ADMIN_PASSWORD = "xj44aqz9CWrNNM0o"; + +// --- Helpers --- +async function createPlayerIfMissing(playersService, nameColumn, idColumn) { + const playerId = idColumn?.trim(); + if (playerId) return playerId; + + let firstName, lastName; + if (!nameColumn || !nameColumn.trim()) { + firstName = `Player_${nanoid(4)}`; + lastName = "(Regional)"; + } else { + const parts = nameColumn.trim().split(" "); + firstName = parts[0]; + lastName = parts[1] || "(Regional)"; + } + + const newPlayer = await playersService.createPlayer({ first_name: firstName, last_name: lastName }); + return newPlayer.id; +} + +async function handleTeamsSheet(rows, teamsService, playersService, pb, tournamentIdMap = {}) { + console.log(`šŸ“„ Importing ${rows.length} teams...`); + const teamIdMap = {}; // spreadsheet ID -> PocketBase ID + + for (const [i, row] of rows.entries()) { + try { + const spreadsheetTeamId = row["ID"]?.toString().trim(); + if (!spreadsheetTeamId) { + console.warn(`āš ļø [${i + 1}] Team row missing spreadsheet ID, skipping.`); + continue; + } + + const p1Id = await createPlayerIfMissing(playersService, row["P1 Name"], row["P1 ID"]); + const p2Id = await createPlayerIfMissing(playersService, row["P2 Name"], row["P2 ID"]); + + let name = row["Name"]?.trim(); + if (!name) { + const p1First = row["P1 Name"]?.split(" ")[0] || "Player1"; + const p2First = row["P2 Name"]?.split(" ")[0] || "Player2"; + name = `${p1First} and ${p2First}`; + console.warn(`āš ļø [${i + 1}] No team name found. Using generated name: ${name}`); + } + + const existing = await pb.collection("teams").getFullList({ + filter: `name = "${name}"`, + fields: "id", + }); + + if (existing.length > 0) { + console.log(`ā„¹ļø [${i + 1}] Team "${name}" already exists, skipping.`); + teamIdMap[spreadsheetTeamId] = existing[0].id; + continue; + } + + // If there's a tournament for this team, get its PB ID + const tournamentSpreadsheetId = row["Tournament ID"]?.toString().trim(); + const tournamentId = tournamentSpreadsheetId ? tournamentIdMap[tournamentSpreadsheetId] : undefined; + + const teamInput = { + name, + primary_color: row.primary_color || "", + accent_color: row.accent_color || "", + logo: row.logo || "", + players: [p1Id, p2Id], + tournament: tournamentId, // single tournament relation, + private: true + }; + + const team = await teamsService.createTeam(teamInput); + teamIdMap[spreadsheetTeamId] = team.id; + + console.log(`āœ… [${i + 1}] Created team: ${team.name} with players: ${[p1Id, p2Id].join(", ")}`); + + // Add the team to the tournament's "teams" relation + if (tournamentId) { + await pb.collection("tournaments").update(tournamentId, { + "teams+": [team.id], + }); + console.log(`āœ… Added team "${team.name}" to tournament ${tournamentId}`); + } + } catch (err) { + console.error(`āŒ [${i + 1}] Failed to create team: ${err.message}`); + } + } + + return teamIdMap; +} + + +async function handleTournamentSheet(rows, tournamentsService, teamIdMap, pb) { + console.log(`šŸ“„ Importing ${rows.length} tournaments...`); + const tournamentIdMap = {}; + const validFormats = ["double_elim", "single_elim", "groups", "swiss", "swiss_bracket"]; + + for (const [i, row] of rows.entries()) { + try { + const spreadsheetId = row["ID"]?.toString().trim(); + if (!spreadsheetId) { + console.warn(`āš ļø [${i + 1}] Tournament missing spreadsheet ID, skipping.`); + continue; + } + + if (!row["Name"]) { + console.warn(`āš ļø [${i + 1}] Tournament name missing, skipping.`); + continue; + } + + const format = validFormats.includes(row["Format"]) ? row["Format"] : "double_elim"; + + // Convert start_time to ISO datetime string + let startTime = null; + if (row["Start Time"]) { + try { + startTime = new Date(row["Start Time"]).toISOString(); + } catch (e) { + console.warn(`āš ļø [${i + 1}] Invalid start time format, using null`); + } + } + + const tournamentInput = { + name: row["Name"], + start_time: startTime, + format, + regional: true, + teams: Object.values(teamIdMap), // Add all created teams + }; + + const tournament = await tournamentsService.createTournament(tournamentInput); + tournamentIdMap[spreadsheetId] = tournament.id; + + console.log(`āœ… [${i + 1}] Created tournament: ${tournament.name} with ${Object.values(teamIdMap).length} teams`); + } catch (err) { + console.error(`āŒ [${i + 1}] Failed to create tournament: ${err.message}`); + } + } + + return tournamentIdMap; +} + + +async function handleMatchesSheet(rows, matchesService, teamIdMap, tournamentIdMap, pb) { + console.log(`šŸ“„ Importing ${rows.length} matches...`); + + const tournamentMatchesMap = {}; + + for (const [i, row] of rows.entries()) { + try { + const homeId = teamIdMap[row["Home ID"]]; + const awayId = teamIdMap[row["Away ID"]]; + const tournamentId = tournamentIdMap[row["Tournament ID"]]; + + if (!homeId || !awayId || !tournamentId) { + console.warn(`āš ļø [${i + 1}] Could not find mapping for Home, Away, or Tournament, skipping.`); + continue; + } + + // --- Ensure the teams are linked to the tournament --- + for (const teamId of [homeId, awayId]) { + const team = await pb.collection("teams").getOne(teamId, { fields: "tournaments" }); + const tournaments = team.tournaments || []; + if (!tournaments.includes(tournamentId)) { + // Add tournament to team + await pb.collection("teams").update(teamId, { "tournaments+": [tournamentId] }); + // Add team to tournament + await pb.collection("tournaments").update(tournamentId, { "teams+": [teamId] }); + console.log(`āœ… Linked team ${team.name} to tournament ${tournamentId}`); + } + } + + // --- Create match --- + const data = { + tournament: tournamentId, + home: homeId, + away: awayId, + home_cups: Number(row["Home cups"] || 0), + away_cups: Number(row["Away cups"] || 0), + status: "ended", + lid: i+1 + }; + + const match = await matchesService.createMatch(data); + console.log(`āœ… [${i + 1}] Created match ID: ${match.id}`); + + if (!tournamentMatchesMap[tournamentId]) tournamentMatchesMap[tournamentId] = []; + tournamentMatchesMap[tournamentId].push(match.id); + } catch (err) { + console.error(`āŒ [${i + 1}] Failed to create match: ${err.message}`); + } + } + + // Update each tournament with the created match IDs + for (const [tournamentId, matchIds] of Object.entries(tournamentMatchesMap)) { + try { + await pb.collection("tournaments").update(tournamentId, { "matches+": matchIds }); + console.log(`āœ… Updated tournament ${tournamentId} with ${matchIds.length} matches`); + } catch (err) { + console.error(`āŒ Failed to update tournament ${tournamentId} with matches: ${err.message}`); + } + } +} + + +// --- Main Import --- +export async function importExcel() { + const pb = new PocketBase(POCKETBASE_URL); + await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); + + const teamsService = createTeamsService(pb); + const playersService = createPlayersService(pb); + const tournamentsService = createTournamentsService(pb); + const matchesService = createMatchesService(pb); + + const workbook = xlsx.readFile(EXCEL_FILE_PATH); + + let teamIdMap = {}; + let tournamentIdMap = {}; + + // Process sheets in correct order: Tournaments -> Teams -> Matches + const sheetOrder = ["tournament", "tournaments", "teams", "matches"]; + const processedSheets = new Set(); + + for (const sheetNamePattern of sheetOrder) { + for (const sheetName of workbook.SheetNames) { + if (processedSheets.has(sheetName)) continue; + if (sheetName.toLowerCase() !== sheetNamePattern) continue; + + const worksheet = workbook.Sheets[sheetName]; + const rows = xlsx.utils.sheet_to_json(worksheet); + + console.log(`\nšŸ“˜ Processing sheet: ${sheetName}`); + + switch (sheetName.toLowerCase()) { + case "teams": + teamIdMap = await handleTeamsSheet(rows, teamsService, playersService, pb, tournamentIdMap); + break; + case "tournament": + case "tournaments": + tournamentIdMap = await handleTournamentSheet(rows, tournamentsService, teamIdMap, pb); + break; + case "matches": + await handleMatchesSheet(rows, matchesService, teamIdMap, tournamentIdMap, pb); + break; + default: + console.log(`āš ļø No handler found for sheet '${sheetName}', skipping.`); + } + + processedSheets.add(sheetName); + } + } + + console.log("\nšŸŽ‰ All sheets imported successfully!"); +} + +// --- Run --- +importExcel().catch(console.error); \ No newline at end of file