Compare commits
40 Commits
d2e6849bca
...
main_old
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed77dd471 | ||
|
|
94ea44c66e | ||
|
|
7441d1ac58 | ||
|
|
7ff26229d9 | ||
|
|
b93ce38d48 | ||
|
|
ae934e77f4 | ||
|
|
cae5fa1c71 | ||
|
|
fc3f626313 | ||
|
|
1027b49258 | ||
|
|
5e20b94a1f | ||
|
|
f99d6efaf9 | ||
|
|
602e6e3473 | ||
|
|
285a33c488 | ||
|
|
cac42c9b29 | ||
|
|
498010e3e2 | ||
|
|
c170e1e1fe | ||
|
|
cde74a04d5 | ||
|
|
9a105b30c6 | ||
|
|
8efc0a7a4b | ||
|
|
d11e50d4ef | ||
|
|
4bc25fb0bc | ||
|
|
3be2284da9 | ||
|
|
a35c688a64 | ||
|
|
617a94262b | ||
|
|
3fe92be980 | ||
|
|
7d3c0a3fa4 | ||
|
|
a926dcde07 | ||
|
|
e20582897f | ||
|
|
cf09014d50 | ||
|
|
0169468114 | ||
|
|
9d92a8a510 | ||
|
|
51e3d5141c | ||
|
|
97c8c9e72b | ||
|
|
3ffa6b03c7 | ||
|
|
8dfff139e1 | ||
|
|
22be6682dd | ||
|
|
c74da09bde | ||
|
|
c5d69f1a19 | ||
|
|
2396464a19 | ||
|
|
cb83ea06fa |
16
.nitro/types/nitro-config.d.ts
vendored
Normal file
16
.nitro/types/nitro-config.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// Generated by nitro
|
||||
|
||||
// App Config
|
||||
import type { Defu } from 'defu'
|
||||
|
||||
|
||||
|
||||
type UserAppConfig = Defu<{}, []>
|
||||
|
||||
declare module "nitropack/types" {
|
||||
interface AppConfig extends UserAppConfig {}
|
||||
interface NitroRuntimeConfig {
|
||||
|
||||
}
|
||||
}
|
||||
export {}
|
||||
1
.nitro/types/nitro-imports.d.ts
vendored
Normal file
1
.nitro/types/nitro-imports.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
8
.nitro/types/nitro-routes.d.ts
vendored
Normal file
8
.nitro/types/nitro-routes.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Generated by nitro
|
||||
import type { Serialize, Simplify } from "nitropack/types";
|
||||
declare module "nitropack/types" {
|
||||
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
|
||||
interface InternalApi {
|
||||
}
|
||||
}
|
||||
export {}
|
||||
3
.nitro/types/nitro.d.ts
vendored
Normal file
3
.nitro/types/nitro.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference path="./nitro-routes.d.ts" />
|
||||
/// <reference path="./nitro-config.d.ts" />
|
||||
/// <reference path="./nitro-imports.d.ts" />
|
||||
323
brackets/11.json
Normal file
323
brackets/11.json
Normal file
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"winners": [
|
||||
[
|
||||
{
|
||||
"lid": 0,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 1
|
||||
},
|
||||
{
|
||||
"lid": 1,
|
||||
"round": 0,
|
||||
"order": 1,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 8,
|
||||
"away_seed": 9,
|
||||
"away_from_lid": null,
|
||||
"away_from_loser": null,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 2,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 4
|
||||
},
|
||||
{
|
||||
"lid": 3,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 5
|
||||
},
|
||||
{
|
||||
"lid": 4,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 2
|
||||
},
|
||||
{
|
||||
"lid": 5,
|
||||
"round": 0,
|
||||
"order": 2,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 7,
|
||||
"away_seed": 10,
|
||||
"away_from_lid": null,
|
||||
"away_from_loser": null,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 6,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 3
|
||||
},
|
||||
{
|
||||
"lid": 7,
|
||||
"round": 0,
|
||||
"order": 3,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 6,
|
||||
"away_seed": 11,
|
||||
"away_from_lid": null,
|
||||
"away_from_loser": null,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 8,
|
||||
"round": 1,
|
||||
"order": 5,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 1,
|
||||
"away_from_lid": 1,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 9,
|
||||
"round": 1,
|
||||
"order": 4,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 4,
|
||||
"away_seed": 5,
|
||||
"away_from_lid": null,
|
||||
"away_from_loser": null,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 10,
|
||||
"round": 1,
|
||||
"order": 6,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 2,
|
||||
"away_from_lid": 5,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 11,
|
||||
"round": 1,
|
||||
"order": 7,
|
||||
"bye": false,
|
||||
"home_from_lid": null,
|
||||
"home_from_loser": null,
|
||||
"home_seed": 3,
|
||||
"away_from_lid": 7,
|
||||
"away_from_loser": null,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 18,
|
||||
"round": 2,
|
||||
"order": 13,
|
||||
"bye": false,
|
||||
"home_from_lid": 8,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 9,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 19,
|
||||
"round": 2,
|
||||
"order": 14,
|
||||
"bye": false,
|
||||
"home_from_lid": 10,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 11,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 23,
|
||||
"round": 3,
|
||||
"order": 16,
|
||||
"bye": false,
|
||||
"home_from_lid": 18,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 19,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 25,
|
||||
"round": 4,
|
||||
"order": 18,
|
||||
"bye": false,
|
||||
"home_from_lid": 23,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 24,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 26,
|
||||
"round": 5,
|
||||
"order": 19,
|
||||
"bye": false,
|
||||
"home_from_lid": 25,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 25,
|
||||
"away_from_loser": true,
|
||||
"reset": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"losers": [
|
||||
[
|
||||
{
|
||||
"lid": 12,
|
||||
"round": 0,
|
||||
"order": null,
|
||||
"bye": true,
|
||||
"home_from_lid": 10,
|
||||
"home_from_loser": true
|
||||
},
|
||||
{
|
||||
"lid": 13,
|
||||
"round": 0,
|
||||
"order": 10,
|
||||
"bye": false,
|
||||
"home_from_lid": 11,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 1,
|
||||
"away_from_loser": true,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 14,
|
||||
"round": 0,
|
||||
"order": 8,
|
||||
"bye": false,
|
||||
"home_from_lid": 9,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 5,
|
||||
"away_from_loser": true,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 15,
|
||||
"round": 0,
|
||||
"order": 9,
|
||||
"bye": false,
|
||||
"home_from_lid": 8,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 7,
|
||||
"away_from_loser": true,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 16,
|
||||
"round": 1,
|
||||
"order": 9,
|
||||
"bye": false,
|
||||
"home_from_lid": 10,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 13,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 17,
|
||||
"round": 1,
|
||||
"order": 10,
|
||||
"bye": false,
|
||||
"home_from_lid": 14,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 15,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 20,
|
||||
"round": 2,
|
||||
"order": 13,
|
||||
"bye": false,
|
||||
"home_from_lid": 18,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 16,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
},
|
||||
{
|
||||
"lid": 21,
|
||||
"round": 2,
|
||||
"order": 14,
|
||||
"bye": false,
|
||||
"home_from_lid": 19,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 17,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 22,
|
||||
"round": 3,
|
||||
"order": 15,
|
||||
"bye": false,
|
||||
"home_from_lid": 20,
|
||||
"home_from_loser": false,
|
||||
"away_from_lid": 21,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"lid": 24,
|
||||
"round": 4,
|
||||
"order": 17,
|
||||
"bye": false,
|
||||
"home_from_lid": 23,
|
||||
"home_from_loser": true,
|
||||
"away_from_lid": 22,
|
||||
"away_from_loser": false,
|
||||
"reset": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -32,17 +32,17 @@ services:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis-cache
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
#redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis-cache
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# command: redis-server --appendonly yes
|
||||
# volumes:
|
||||
# - redis-data:/data
|
||||
# networks:
|
||||
# - app-network
|
||||
# restart: unless-stopped
|
||||
|
||||
supertokens:
|
||||
image: registry.supertokens.io/supertokens/supertokens-postgresql
|
||||
@@ -51,6 +51,7 @@ services:
|
||||
- postgres
|
||||
environment:
|
||||
POSTGRESQL_CONNECTION_URI: postgresql://supertokens:password@postgres:5432/supertokens
|
||||
ACCESS_TOKEN_VALIDITY: 360000
|
||||
ports:
|
||||
- "3567:3567"
|
||||
env_file:
|
||||
|
||||
16
package.json
16
package.json
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
"build": "vite build && tsc --noEmit",
|
||||
"start": "vite start"
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
@@ -18,13 +18,18 @@
|
||||
"@mantine/hooks": "^8.2.4",
|
||||
"@mantine/tiptap": "^8.2.4",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@svgmoji/noto": "^3.2.0",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@tanstack/react-query-devtools": "^5.66.0",
|
||||
"@tanstack/react-router": "^1.130.12",
|
||||
"@tanstack/react-router-devtools": "^1.130.13",
|
||||
"@tanstack/react-router-with-query": "^1.130.12",
|
||||
"@tanstack/react-start": "^1.130.15",
|
||||
"@tanstack/react-router": "1.130.12",
|
||||
"@tanstack/react-router-devtools": "1.130.13",
|
||||
"@tanstack/react-router-with-query": "1.130.12",
|
||||
"@tanstack/react-start": "1.130.15",
|
||||
"@tiptap/pm": "^3.4.3",
|
||||
"@tiptap/react": "^3.4.3",
|
||||
"@tiptap/starter-kit": "^3.4.3",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"dotenv": "^17.2.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"ioredis": "^5.7.0",
|
||||
@@ -33,6 +38,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-scan": "^0.4.3",
|
||||
"react-use-draggable-scroll": "^0.4.7",
|
||||
"recharts": "^3.1.2",
|
||||
"redaxios": "^0.5.1",
|
||||
|
||||
44
pb_migrations/1757211840_updated_matches.js
Normal file
44
pb_migrations/1757211840_updated_matches.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(18, new Field({
|
||||
"hidden": false,
|
||||
"id": "number1705071305",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "home_seed",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(19, new Field({
|
||||
"hidden": false,
|
||||
"id": "number3588777624",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "away_seed",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("number1705071305")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("number3588777624")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1757211934_updated_tournaments.js
Normal file
28
pb_migrations/1757211934_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_2541054544",
|
||||
"hidden": false,
|
||||
"id": "relation103159226",
|
||||
"maxSelect": 999,
|
||||
"minSelect": 0,
|
||||
"name": "matches",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation103159226")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
27
pb_migrations/1757263183_updated_matches.js
Normal file
27
pb_migrations/1757263183_updated_matches.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(20, new Field({
|
||||
"hidden": false,
|
||||
"id": "number3320769076",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "round",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("number3320769076")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1757386414_updated_players.js
Normal file
28
pb_migrations/1757386414_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1757386423_updated_matches.js
Normal file
28
pb_migrations/1757386423_updated_matches.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1757386431_updated_teams.js
Normal file
28
pb_migrations/1757386431_updated_teams.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1568971955")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1568971955")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1757386438_updated_tournaments.js
Normal file
28
pb_migrations/1757386438_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
31
pb_migrations/1757615830_updated_matches.js
Normal file
31
pb_migrations/1757615830_updated_matches.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(21, new Field({
|
||||
"hidden": false,
|
||||
"id": "select2063623452",
|
||||
"maxSelect": 1,
|
||||
"name": "status",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": [
|
||||
"tbd",
|
||||
"ready",
|
||||
"started",
|
||||
"ended"
|
||||
]
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("select2063623452")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
235
pb_migrations/1757710506_created_player_stats.js
Normal file
235
pb_migrations/1757710506_created_player_stats.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_976G",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "first_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_njLe",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "last_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_ZNMy",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_340646327",
|
||||
"hidden": false,
|
||||
"id": "relation869376999",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "tournament_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_nxTv",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number103159226",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "matches",
|
||||
"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": "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"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1062535948",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_won_by",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json4249694556",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_lost_by",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"id": "pbc_135889471",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "player_stats",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "\n SELECT\n (p.id || '_' || t.id || '_' || tour.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n p.first_name,\n p.last_name,\n t.id as team_id,\n t.name as team_name,\n tour.id as tournament_id,\n tour.name as tournament_name,\n COUNT(m.id) as matches,\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 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) as margin_of_victory,\n 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) as margin_of_loss,\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_won_by,\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_lost_by,\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 FROM players p\n JOIN teams t ON json_extract(t.players, '$[*]') LIKE '%' || p.id || '%'\n JOIN matches m ON (m.home = t.id OR m.away = t.id)\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended'\n GROUP BY p.id, t.id, tour.id",
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
194
pb_migrations/1757710693_updated_player_stats.js
Normal file
194
pb_migrations/1757710693_updated_player_stats.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"viewQuery": "SELECT\n (p.id || '_' || t.id || '_' || tour.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n t.id as team_id,\n t.name as team_name,\n tour.id as tournament_id,\n tour.name as tournament_name,\n COUNT(m.id) as matches,\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 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'\n GROUP BY p.id, t.id, tour.id"
|
||||
}, collection)
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_976G")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_njLe")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_ZNMy")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_nxTv")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("json3041953980")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("json1531431708")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("json1062535948")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("json4249694556")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_YqC8",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_jZTo",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"viewQuery": "\n SELECT\n (p.id || '_' || t.id || '_' || tour.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n p.first_name,\n p.last_name,\n t.id as team_id,\n t.name as team_name,\n tour.id as tournament_id,\n tour.name as tournament_name,\n COUNT(m.id) as matches,\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 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) as margin_of_victory,\n 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) as margin_of_loss,\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_won_by,\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_lost_by,\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 FROM players p\n JOIN teams t ON json_extract(t.players, '$[*]') LIKE '%' || p.id || '%'\n JOIN matches m ON (m.home = t.id OR m.away = t.id)\n JOIN tournaments tour ON m.tournament = tour.id\n WHERE m.status = 'ended'\n GROUP BY p.id, t.id, tour.id"
|
||||
}, collection)
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_976G",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "first_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_njLe",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "last_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_ZNMy",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(8, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_nxTv",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(12, new Field({
|
||||
"hidden": false,
|
||||
"id": "json3041953980",
|
||||
"maxSize": 1,
|
||||
"name": "margin_of_victory",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(13, new Field({
|
||||
"hidden": false,
|
||||
"id": "json1531431708",
|
||||
"maxSize": 1,
|
||||
"name": "margin_of_loss",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(14, new Field({
|
||||
"hidden": false,
|
||||
"id": "json1062535948",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_won_by",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(15, new Field({
|
||||
"hidden": false,
|
||||
"id": "json4249694556",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_lost_by",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}))
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_YqC8")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_jZTo")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
96
pb_migrations/1757710817_updated_player_stats.js
Normal file
96
pb_migrations/1757710817_updated_player_stats.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"name": "player_stats_per_tournament"
|
||||
}, collection)
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_YqC8")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_jZTo")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_XGbN",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_uud6",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"name": "player_stats"
|
||||
}, collection)
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_YqC8",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_jZTo",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_XGbN")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_uud6")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
180
pb_migrations/1757710882_created_player_stats.js
Normal file
180
pb_migrations/1757710882_created_player_stats.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_fQu1",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number103159226",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "matches",
|
||||
"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_1358894712",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "player_stats",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n (p.id || '_' || t.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n t.id as team_id,\n t.name as team_name,\n COUNT(m.id) as matches,\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 -- Additional calculated stats\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 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 calculations\n 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) as margin_of_victory,\n 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) 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'\n GROUP BY p.id, t.id",
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1358894712");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
57
pb_migrations/1757711182_updated_player_stats.js
Normal file
57
pb_migrations/1757711182_updated_player_stats.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1358894712")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"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 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'\n GROUP BY p.id"
|
||||
}, collection)
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation694999214")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_fQu1")
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1358894712")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"viewQuery": "SELECT\n (p.id || '_' || t.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n t.id as team_id,\n t.name as team_name,\n COUNT(m.id) as matches,\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 -- Additional calculated stats\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 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 calculations\n 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) as margin_of_victory,\n 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) 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'\n GROUP BY p.id, t.id"
|
||||
}, collection)
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_fQu1",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
211
pb_migrations/1757800000_created_team_stats.js
Normal file
211
pb_migrations/1757800000_created_team_stats.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_ZNMy",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number103159226",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "matches",
|
||||
"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": "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"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1062535948",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_won_by",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json4249694556",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_lost_by",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"id": "pbc_135889472",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "team_stats",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": `
|
||||
SELECT
|
||||
t.id as id,
|
||||
t.id as team_id,
|
||||
t.name as team_name,
|
||||
COUNT(m.id) as matches,
|
||||
SUM(CASE
|
||||
WHEN (m.home = t.id AND m.home_cups > m.away_cups) OR
|
||||
(m.away = t.id AND m.away_cups > m.home_cups)
|
||||
THEN 1 ELSE 0
|
||||
END) as wins,
|
||||
SUM(CASE
|
||||
WHEN (m.home = t.id AND m.home_cups < m.away_cups) OR
|
||||
(m.away = t.id AND m.away_cups < m.home_cups)
|
||||
THEN 1 ELSE 0
|
||||
END) as losses,
|
||||
AVG(CASE
|
||||
WHEN m.home = t.id AND m.home_cups > m.away_cups
|
||||
THEN m.home_cups - m.away_cups
|
||||
WHEN m.away = t.id AND m.away_cups > m.home_cups
|
||||
THEN m.away_cups - m.home_cups
|
||||
ELSE NULL
|
||||
END) as margin_of_victory,
|
||||
AVG(CASE
|
||||
WHEN m.home = t.id AND m.home_cups < m.away_cups
|
||||
THEN m.away_cups - m.home_cups
|
||||
WHEN m.away = t.id AND m.away_cups < m.home_cups
|
||||
THEN m.home_cups - m.away_cups
|
||||
ELSE NULL
|
||||
END) as margin_of_loss,
|
||||
SUM(CASE
|
||||
WHEN m.home = t.id THEN m.home_cups
|
||||
WHEN m.away = t.id THEN m.away_cups
|
||||
ELSE 0
|
||||
END) as total_cups_won_by,
|
||||
SUM(CASE
|
||||
WHEN m.home = t.id THEN m.away_cups
|
||||
WHEN m.away = t.id THEN m.home_cups
|
||||
ELSE 0
|
||||
END) as total_cups_lost_by,
|
||||
SUM(CASE
|
||||
WHEN m.home = t.id THEN m.home_cups
|
||||
WHEN m.away = t.id THEN m.away_cups
|
||||
ELSE 0
|
||||
END) as total_cups_made,
|
||||
SUM(CASE
|
||||
WHEN m.home = t.id THEN m.away_cups
|
||||
WHEN m.away = t.id THEN m.home_cups
|
||||
ELSE 0
|
||||
END) as total_cups_against
|
||||
FROM teams t
|
||||
JOIN matches m ON (m.home = t.id OR m.away = t.id)
|
||||
JOIN tournaments tour ON m.tournament = tour.id
|
||||
WHERE m.status = 'ended'
|
||||
GROUP BY t.id`,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889472");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
167
pb_migrations/1757910262_deleted_player_stats_per_tournament.js
Normal file
167
pb_migrations/1757910262_deleted_player_stats_per_tournament.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889471");
|
||||
|
||||
return app.delete(collection);
|
||||
}, (app) => {
|
||||
const collection = new Collection({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation2582050271",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "player_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json4231605813",
|
||||
"maxSize": 1,
|
||||
"name": "player_name",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_XGbN",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_340646327",
|
||||
"hidden": false,
|
||||
"id": "relation869376999",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "tournament_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_uud6",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number103159226",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "matches",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"id": "pbc_135889471",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "player_stats_per_tournament",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n (p.id || '_' || t.id || '_' || tour.id) as id,\n p.id as player_id,\n (p.first_name || ' ' || p.last_name) as player_name,\n t.id as team_id,\n t.name as team_name,\n tour.id as tournament_id,\n tour.name as tournament_name,\n COUNT(m.id) as matches,\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 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'\n GROUP BY p.id, t.id, tour.id",
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
})
|
||||
96
pb_migrations/1757950535_updated_team_stats.js
Normal file
96
pb_migrations/1757950535_updated_team_stats.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889472")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"name": "team_stats_per_tournament"
|
||||
}, collection)
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_2Mic")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_C8ev")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_QmWG",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_1o7N",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_135889472")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"name": "team_stats"
|
||||
}, collection)
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(2, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_2Mic",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_C8ev",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "tournament_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}))
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_QmWG")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("_clone_1o7N")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
157
pb_migrations/1757950649_created_team_stats.js
Normal file
157
pb_migrations/1757950649_created_team_stats.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
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_1568971955",
|
||||
"hidden": false,
|
||||
"id": "relation694999214",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "team_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "_clone_nYJn",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "team_name",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "number103159226",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "matches",
|
||||
"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": "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"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1062535948",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_won_by",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json4249694556",
|
||||
"maxSize": 1,
|
||||
"name": "total_cups_lost_by",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"id": "pbc_1582517110",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "team_stats",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "\n SELECT\n t.id as id,\n t.id as team_id,\n t.name as team_name,\n COUNT(m.id) as matches,\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 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) as margin_of_victory,\n 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) as margin_of_loss,\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_won_by,\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_lost_by,\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 FROM teams t\n JOIN matches m ON (m.home = t.id OR m.away = t.id)\n WHERE m.status = 'ended'\n GROUP BY t.id",
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1582517110");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
55
pb_migrations/1758042930_updated_teams.js
Normal file
55
pb_migrations/1758042930_updated_teams.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1568971955")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("number3356599746")
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(9, new Field({
|
||||
"hidden": false,
|
||||
"id": "number1329349942",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "song_end",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1568971955")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(8, new Field({
|
||||
"hidden": false,
|
||||
"id": "number3356599746",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "song_year",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(10, new Field({
|
||||
"hidden": false,
|
||||
"id": "number1329349942",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "song_duration",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
37
pb_migrations/1758049631_updated_player_stats.js
Normal file
37
pb_migrations/1758049631_updated_player_stats.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1358894712")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"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'\n GROUP BY p.id"
|
||||
}, collection)
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(4, new Field({
|
||||
"hidden": false,
|
||||
"id": "number3837590211",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "tournaments",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1358894712")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"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 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'\n GROUP BY p.id"
|
||||
}, collection)
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("number3837590211")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758054877_updated_tournaments.js
Normal file
28
pb_migrations/1758054877_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(11, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation1584152981",
|
||||
"maxSelect": 999,
|
||||
"minSelect": 0,
|
||||
"name": "free_agents",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation1584152981")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
84
pb_migrations/1758081731_created_reactions.js
Normal file
84
pb_migrations/1758081731_created_reactions.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/// <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": "text3058431538",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "emoji",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"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": "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_1549310251",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "reactions",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1549310251");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
pb_migrations/1758123221_updated_reactions.js
Normal file
28
pb_migrations/1758123221_updated_reactions.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1549310251")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_2541054544",
|
||||
"hidden": false,
|
||||
"id": "relation2052834565",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "match",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_1549310251")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2052834565")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
85
pb_migrations/1758379630_created_badges.js
Normal file
85
pb_migrations/1758379630_created_badges.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/// <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": "text1843675174",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "description",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_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);
|
||||
})
|
||||
28
pb_migrations/1758380013_updated_players.js
Normal file
28
pb_migrations/1758380013_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((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)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2029409178")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758385120_updated_players.js
Normal file
28
pb_migrations/1758385120_updated_players.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(6, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1340419796",
|
||||
"hidden": false,
|
||||
"id": "relation2813965191",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "featured_badge",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_3072146508")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation2813965191")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
84
pb_migrations/1758388728_created_free_agents.js
Normal file
84
pb_migrations/1758388728_created_free_agents.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/// <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_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation2551806565",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "player",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text1146066909",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "phone",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate2990389176",
|
||||
"name": "created",
|
||||
"onCreate": true,
|
||||
"onUpdate": false,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "autodate3332085495",
|
||||
"name": "updated",
|
||||
"onCreate": true,
|
||||
"onUpdate": true,
|
||||
"presentable": false,
|
||||
"system": false,
|
||||
"type": "autodate"
|
||||
}
|
||||
],
|
||||
"id": "pbc_2929550049",
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"name": "free_agents",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049");
|
||||
|
||||
return app.delete(collection);
|
||||
})
|
||||
28
pb_migrations/1758402128_updated_free_agents.js
Normal file
28
pb_migrations/1758402128_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(3, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_340646327",
|
||||
"hidden": false,
|
||||
"id": "relation3177167065",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "tournament",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation3177167065")
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758402424_updated_tournaments.js
Normal file
28
pb_migrations/1758402424_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// remove field
|
||||
collection.fields.removeById("relation1584152981")
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(11, new Field({
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_3072146508",
|
||||
"hidden": false,
|
||||
"id": "relation1584152981",
|
||||
"maxSelect": 999,
|
||||
"minSelect": 0,
|
||||
"name": "free_agents",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}))
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758575563_updated_free_agents.js
Normal file
28
pb_migrations/1758575563_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
28
pb_migrations/1758575597_updated_free_agents.js
Normal file
28
pb_migrations/1758575597_updated_free_agents.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"listRule": null,
|
||||
"updateRule": null,
|
||||
"viewRule": null
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("pbc_2929550049")
|
||||
|
||||
// update collection data
|
||||
unmarshal({
|
||||
"createRule": "",
|
||||
"deleteRule": "",
|
||||
"listRule": "",
|
||||
"updateRule": "",
|
||||
"viewRule": ""
|
||||
}, collection)
|
||||
|
||||
return app.save(collection)
|
||||
})
|
||||
@@ -16,6 +16,7 @@ import { Route as LogoutRouteImport } from './routes/logout'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as AuthedRouteImport } from './routes/_authed'
|
||||
import { Route as AuthedIndexRouteImport } from './routes/_authed/index'
|
||||
import { Route as AuthedStatsRouteImport } from './routes/_authed/stats'
|
||||
import { Route as AuthedSettingsRouteImport } from './routes/_authed/settings'
|
||||
import { Route as AuthedAdminRouteImport } from './routes/_authed/admin'
|
||||
import { Route as AuthedTournamentsIndexRouteImport } from './routes/_authed/tournaments/index'
|
||||
@@ -25,9 +26,18 @@ import { Route as AuthedTeamsTeamIdRouteImport } from './routes/_authed/teams.$t
|
||||
import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/profile.$playerId'
|
||||
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||
import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id'
|
||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
||||
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||
import { ServerRoute as ApiTeamsUploadLogoServerRouteImport } from './routes/api/teams/upload-logo'
|
||||
import { ServerRoute as ApiSpotifyTokenServerRouteImport } from './routes/api/spotify/token'
|
||||
import { ServerRoute as ApiSpotifySearchServerRouteImport } from './routes/api/spotify/search'
|
||||
import { ServerRoute as ApiSpotifyResumeServerRouteImport } from './routes/api/spotify/resume'
|
||||
import { ServerRoute as ApiSpotifyPlaybackServerRouteImport } from './routes/api/spotify/playback'
|
||||
import { ServerRoute as ApiSpotifyCaptureServerRouteImport } from './routes/api/spotify/capture'
|
||||
import { ServerRoute as ApiSpotifyCallbackServerRouteImport } from './routes/api/spotify/callback'
|
||||
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||
import { ServerRoute as ApiFilesCollectionRecordIdFileServerRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||
@@ -58,6 +68,11 @@ const AuthedIndexRoute = AuthedIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedStatsRoute = AuthedStatsRouteImport.update({
|
||||
id: '/stats',
|
||||
path: '/stats',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedSettingsRoute = AuthedSettingsRouteImport.update({
|
||||
id: '/settings',
|
||||
path: '/settings',
|
||||
@@ -105,10 +120,16 @@ const AuthedAdminTournamentsIndexRoute =
|
||||
path: '/tournaments/',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const AuthedAdminTournamentsIdRoute =
|
||||
AuthedAdminTournamentsIdRouteImport.update({
|
||||
id: '/tournaments/$id',
|
||||
path: '/tournaments/$id',
|
||||
const AuthedTournamentsIdBracketRoute =
|
||||
AuthedTournamentsIdBracketRouteImport.update({
|
||||
id: '/tournaments/$id/bracket',
|
||||
path: '/tournaments/$id/bracket',
|
||||
getParentRoute: () => AuthedRoute,
|
||||
} as any)
|
||||
const AuthedAdminTournamentsIdIndexRoute =
|
||||
AuthedAdminTournamentsIdIndexRouteImport.update({
|
||||
id: '/tournaments/$id/',
|
||||
path: '/tournaments/$id/',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const AuthedAdminTournamentsRunIdRoute =
|
||||
@@ -117,12 +138,56 @@ const AuthedAdminTournamentsRunIdRoute =
|
||||
path: '/tournaments/run/$id',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const AuthedAdminTournamentsIdTeamsRoute =
|
||||
AuthedAdminTournamentsIdTeamsRouteImport.update({
|
||||
id: '/tournaments/$id/teams',
|
||||
path: '/tournaments/$id/teams',
|
||||
getParentRoute: () => AuthedAdminRoute,
|
||||
} as any)
|
||||
const ApiTournamentsUploadLogoServerRoute =
|
||||
ApiTournamentsUploadLogoServerRouteImport.update({
|
||||
id: '/api/tournaments/upload-logo',
|
||||
path: '/api/tournaments/upload-logo',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiTeamsUploadLogoServerRoute =
|
||||
ApiTeamsUploadLogoServerRouteImport.update({
|
||||
id: '/api/teams/upload-logo',
|
||||
path: '/api/teams/upload-logo',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyTokenServerRoute = ApiSpotifyTokenServerRouteImport.update({
|
||||
id: '/api/spotify/token',
|
||||
path: '/api/spotify/token',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifySearchServerRoute = ApiSpotifySearchServerRouteImport.update({
|
||||
id: '/api/spotify/search',
|
||||
path: '/api/spotify/search',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyResumeServerRoute = ApiSpotifyResumeServerRouteImport.update({
|
||||
id: '/api/spotify/resume',
|
||||
path: '/api/spotify/resume',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyPlaybackServerRoute =
|
||||
ApiSpotifyPlaybackServerRouteImport.update({
|
||||
id: '/api/spotify/playback',
|
||||
path: '/api/spotify/playback',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyCaptureServerRoute = ApiSpotifyCaptureServerRouteImport.update({
|
||||
id: '/api/spotify/capture',
|
||||
path: '/api/spotify/capture',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyCallbackServerRoute =
|
||||
ApiSpotifyCallbackServerRouteImport.update({
|
||||
id: '/api/spotify/callback',
|
||||
path: '/api/spotify/callback',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiEventsSplatServerRoute = ApiEventsSplatServerRouteImport.update({
|
||||
id: '/api/events/$',
|
||||
path: '/api/events/$',
|
||||
@@ -146,6 +211,7 @@ export interface FileRoutesByFullPath {
|
||||
'/refresh-session': typeof RefreshSessionRoute
|
||||
'/admin': typeof AuthedAdminRouteWithChildren
|
||||
'/settings': typeof AuthedSettingsRoute
|
||||
'/stats': typeof AuthedStatsRoute
|
||||
'/': typeof AuthedIndexRoute
|
||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
@@ -153,15 +219,18 @@ export interface FileRoutesByFullPath {
|
||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||
'/admin/': typeof AuthedAdminIndexRoute
|
||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/login': typeof LoginRoute
|
||||
'/logout': typeof LogoutRoute
|
||||
'/refresh-session': typeof RefreshSessionRoute
|
||||
'/settings': typeof AuthedSettingsRoute
|
||||
'/stats': typeof AuthedStatsRoute
|
||||
'/': typeof AuthedIndexRoute
|
||||
'/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
@@ -169,9 +238,11 @@ export interface FileRoutesByTo {
|
||||
'/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||
'/admin': typeof AuthedAdminIndexRoute
|
||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -181,6 +252,7 @@ export interface FileRoutesById {
|
||||
'/refresh-session': typeof RefreshSessionRoute
|
||||
'/_authed/admin': typeof AuthedAdminRouteWithChildren
|
||||
'/_authed/settings': typeof AuthedSettingsRoute
|
||||
'/_authed/stats': typeof AuthedStatsRoute
|
||||
'/_authed/': typeof AuthedIndexRoute
|
||||
'/_authed/admin/preview': typeof AuthedAdminPreviewRoute
|
||||
'/_authed/profile/$playerId': typeof AuthedProfilePlayerIdRoute
|
||||
@@ -188,9 +260,11 @@ export interface FileRoutesById {
|
||||
'/_authed/tournaments/$tournamentId': typeof AuthedTournamentsTournamentIdRoute
|
||||
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||
'/_authed/admin/tournaments/$id/': typeof AuthedAdminTournamentsIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -200,6 +274,7 @@ export interface FileRouteTypes {
|
||||
| '/refresh-session'
|
||||
| '/admin'
|
||||
| '/settings'
|
||||
| '/stats'
|
||||
| '/'
|
||||
| '/admin/preview'
|
||||
| '/profile/$playerId'
|
||||
@@ -207,15 +282,18 @@ export interface FileRouteTypes {
|
||||
| '/tournaments/$tournamentId'
|
||||
| '/admin/'
|
||||
| '/tournaments'
|
||||
| '/admin/tournaments/$id'
|
||||
| '/tournaments/$id/bracket'
|
||||
| '/admin/tournaments'
|
||||
| '/admin/tournaments/$id/teams'
|
||||
| '/admin/tournaments/run/$id'
|
||||
| '/admin/tournaments/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/login'
|
||||
| '/logout'
|
||||
| '/refresh-session'
|
||||
| '/settings'
|
||||
| '/stats'
|
||||
| '/'
|
||||
| '/admin/preview'
|
||||
| '/profile/$playerId'
|
||||
@@ -223,9 +301,11 @@ export interface FileRouteTypes {
|
||||
| '/tournaments/$tournamentId'
|
||||
| '/admin'
|
||||
| '/tournaments'
|
||||
| '/admin/tournaments/$id'
|
||||
| '/tournaments/$id/bracket'
|
||||
| '/admin/tournaments'
|
||||
| '/admin/tournaments/$id/teams'
|
||||
| '/admin/tournaments/run/$id'
|
||||
| '/admin/tournaments/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_authed'
|
||||
@@ -234,6 +314,7 @@ export interface FileRouteTypes {
|
||||
| '/refresh-session'
|
||||
| '/_authed/admin'
|
||||
| '/_authed/settings'
|
||||
| '/_authed/stats'
|
||||
| '/_authed/'
|
||||
| '/_authed/admin/preview'
|
||||
| '/_authed/profile/$playerId'
|
||||
@@ -241,9 +322,11 @@ export interface FileRouteTypes {
|
||||
| '/_authed/tournaments/$tournamentId'
|
||||
| '/_authed/admin/'
|
||||
| '/_authed/tournaments/'
|
||||
| '/_authed/admin/tournaments/$id'
|
||||
| '/_authed/tournaments/$id/bracket'
|
||||
| '/_authed/admin/tournaments/'
|
||||
| '/_authed/admin/tournaments/$id/teams'
|
||||
| '/_authed/admin/tournaments/run/$id'
|
||||
| '/_authed/admin/tournaments/$id/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -255,12 +338,26 @@ export interface RootRouteChildren {
|
||||
export interface FileServerRoutesByFullPath {
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/search': typeof ApiSpotifySearchServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
export interface FileServerRoutesByTo {
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/search': typeof ApiSpotifySearchServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
@@ -268,6 +365,13 @@ export interface FileServerRoutesById {
|
||||
__root__: typeof rootServerRouteImport
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/search': typeof ApiSpotifySearchServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/teams/upload-logo': typeof ApiTeamsUploadLogoServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
@@ -276,18 +380,39 @@ export interface FileServerRouteTypes {
|
||||
fullPaths:
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/search'
|
||||
| '/api/spotify/token'
|
||||
| '/api/teams/upload-logo'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
fileServerRoutesByTo: FileServerRoutesByTo
|
||||
to:
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/search'
|
||||
| '/api/spotify/token'
|
||||
| '/api/teams/upload-logo'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/search'
|
||||
| '/api/spotify/token'
|
||||
| '/api/teams/upload-logo'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
fileServerRoutesById: FileServerRoutesById
|
||||
@@ -295,6 +420,13 @@ export interface FileServerRouteTypes {
|
||||
export interface RootServerRouteChildren {
|
||||
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
|
||||
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
|
||||
ApiSpotifyCallbackServerRoute: typeof ApiSpotifyCallbackServerRoute
|
||||
ApiSpotifyCaptureServerRoute: typeof ApiSpotifyCaptureServerRoute
|
||||
ApiSpotifyPlaybackServerRoute: typeof ApiSpotifyPlaybackServerRoute
|
||||
ApiSpotifyResumeServerRoute: typeof ApiSpotifyResumeServerRoute
|
||||
ApiSpotifySearchServerRoute: typeof ApiSpotifySearchServerRoute
|
||||
ApiSpotifyTokenServerRoute: typeof ApiSpotifyTokenServerRoute
|
||||
ApiTeamsUploadLogoServerRoute: typeof ApiTeamsUploadLogoServerRoute
|
||||
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
|
||||
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
}
|
||||
@@ -336,6 +468,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedIndexRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/stats': {
|
||||
id: '/_authed/stats'
|
||||
path: '/stats'
|
||||
fullPath: '/stats'
|
||||
preLoaderRoute: typeof AuthedStatsRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/settings': {
|
||||
id: '/_authed/settings'
|
||||
path: '/settings'
|
||||
@@ -399,11 +538,18 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedAdminTournamentsIndexRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
'/_authed/admin/tournaments/$id': {
|
||||
id: '/_authed/admin/tournaments/$id'
|
||||
'/_authed/tournaments/$id/bracket': {
|
||||
id: '/_authed/tournaments/$id/bracket'
|
||||
path: '/tournaments/$id/bracket'
|
||||
fullPath: '/tournaments/$id/bracket'
|
||||
preLoaderRoute: typeof AuthedTournamentsIdBracketRouteImport
|
||||
parentRoute: typeof AuthedRoute
|
||||
}
|
||||
'/_authed/admin/tournaments/$id/': {
|
||||
id: '/_authed/admin/tournaments/$id/'
|
||||
path: '/tournaments/$id'
|
||||
fullPath: '/admin/tournaments/$id'
|
||||
preLoaderRoute: typeof AuthedAdminTournamentsIdRouteImport
|
||||
preLoaderRoute: typeof AuthedAdminTournamentsIdIndexRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
'/_authed/admin/tournaments/run/$id': {
|
||||
@@ -413,6 +559,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedAdminTournamentsRunIdRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
'/_authed/admin/tournaments/$id/teams': {
|
||||
id: '/_authed/admin/tournaments/$id/teams'
|
||||
path: '/tournaments/$id/teams'
|
||||
fullPath: '/admin/tournaments/$id/teams'
|
||||
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
|
||||
parentRoute: typeof AuthedAdminRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
declare module '@tanstack/react-start/server' {
|
||||
@@ -424,6 +577,55 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiTournamentsUploadLogoServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/teams/upload-logo': {
|
||||
id: '/api/teams/upload-logo'
|
||||
path: '/api/teams/upload-logo'
|
||||
fullPath: '/api/teams/upload-logo'
|
||||
preLoaderRoute: typeof ApiTeamsUploadLogoServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/token': {
|
||||
id: '/api/spotify/token'
|
||||
path: '/api/spotify/token'
|
||||
fullPath: '/api/spotify/token'
|
||||
preLoaderRoute: typeof ApiSpotifyTokenServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/search': {
|
||||
id: '/api/spotify/search'
|
||||
path: '/api/spotify/search'
|
||||
fullPath: '/api/spotify/search'
|
||||
preLoaderRoute: typeof ApiSpotifySearchServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/resume': {
|
||||
id: '/api/spotify/resume'
|
||||
path: '/api/spotify/resume'
|
||||
fullPath: '/api/spotify/resume'
|
||||
preLoaderRoute: typeof ApiSpotifyResumeServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/playback': {
|
||||
id: '/api/spotify/playback'
|
||||
path: '/api/spotify/playback'
|
||||
fullPath: '/api/spotify/playback'
|
||||
preLoaderRoute: typeof ApiSpotifyPlaybackServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/capture': {
|
||||
id: '/api/spotify/capture'
|
||||
path: '/api/spotify/capture'
|
||||
fullPath: '/api/spotify/capture'
|
||||
preLoaderRoute: typeof ApiSpotifyCaptureServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/callback': {
|
||||
id: '/api/spotify/callback'
|
||||
path: '/api/spotify/callback'
|
||||
fullPath: '/api/spotify/callback'
|
||||
preLoaderRoute: typeof ApiSpotifyCallbackServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/events/$': {
|
||||
id: '/api/events/$'
|
||||
path: '/api/events/$'
|
||||
@@ -451,17 +653,19 @@ declare module '@tanstack/react-start/server' {
|
||||
interface AuthedAdminRouteChildren {
|
||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||
AuthedAdminTournamentsIdRoute: typeof AuthedAdminTournamentsIdRoute
|
||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
|
||||
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
||||
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
|
||||
}
|
||||
|
||||
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||
AuthedAdminTournamentsIdRoute: AuthedAdminTournamentsIdRoute,
|
||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
|
||||
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
||||
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,
|
||||
}
|
||||
|
||||
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
||||
@@ -471,21 +675,25 @@ const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
||||
interface AuthedRouteChildren {
|
||||
AuthedAdminRoute: typeof AuthedAdminRouteWithChildren
|
||||
AuthedSettingsRoute: typeof AuthedSettingsRoute
|
||||
AuthedStatsRoute: typeof AuthedStatsRoute
|
||||
AuthedIndexRoute: typeof AuthedIndexRoute
|
||||
AuthedProfilePlayerIdRoute: typeof AuthedProfilePlayerIdRoute
|
||||
AuthedTeamsTeamIdRoute: typeof AuthedTeamsTeamIdRoute
|
||||
AuthedTournamentsTournamentIdRoute: typeof AuthedTournamentsTournamentIdRoute
|
||||
AuthedTournamentsIndexRoute: typeof AuthedTournamentsIndexRoute
|
||||
AuthedTournamentsIdBracketRoute: typeof AuthedTournamentsIdBracketRoute
|
||||
}
|
||||
|
||||
const AuthedRouteChildren: AuthedRouteChildren = {
|
||||
AuthedAdminRoute: AuthedAdminRouteWithChildren,
|
||||
AuthedSettingsRoute: AuthedSettingsRoute,
|
||||
AuthedStatsRoute: AuthedStatsRoute,
|
||||
AuthedIndexRoute: AuthedIndexRoute,
|
||||
AuthedProfilePlayerIdRoute: AuthedProfilePlayerIdRoute,
|
||||
AuthedTeamsTeamIdRoute: AuthedTeamsTeamIdRoute,
|
||||
AuthedTournamentsTournamentIdRoute: AuthedTournamentsTournamentIdRoute,
|
||||
AuthedTournamentsIndexRoute: AuthedTournamentsIndexRoute,
|
||||
AuthedTournamentsIdBracketRoute: AuthedTournamentsIdBracketRoute,
|
||||
}
|
||||
|
||||
const AuthedRouteWithChildren =
|
||||
@@ -503,6 +711,13 @@ export const routeTree = rootRouteImport
|
||||
const rootServerRouteChildren: RootServerRouteChildren = {
|
||||
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
|
||||
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
|
||||
ApiSpotifyCallbackServerRoute: ApiSpotifyCallbackServerRoute,
|
||||
ApiSpotifyCaptureServerRoute: ApiSpotifyCaptureServerRoute,
|
||||
ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute,
|
||||
ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute,
|
||||
ApiSpotifySearchServerRoute: ApiSpotifySearchServerRoute,
|
||||
ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute,
|
||||
ApiTeamsUploadLogoServerRoute: ApiTeamsUploadLogoServerRoute,
|
||||
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
|
||||
ApiFilesCollectionRecordIdFileServerRoute:
|
||||
ApiFilesCollectionRecordIdFileServerRoute,
|
||||
|
||||
@@ -5,6 +5,10 @@ import { routeTree } from "./routeTree.gen";
|
||||
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
||||
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
|
||||
export function createRouter() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -32,7 +36,7 @@ export function createRouter() {
|
||||
defaultPreload: "intent",
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
scrollRestoration: true,
|
||||
defaultViewTransition: true,
|
||||
defaultViewTransition: false,
|
||||
}),
|
||||
queryClient
|
||||
);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import "@mantine/carousel/styles.css";
|
||||
import {
|
||||
HeadContent,
|
||||
Navigate,
|
||||
@@ -17,9 +14,12 @@ import Providers from "@/features/core/components/providers";
|
||||
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
|
||||
import { HeaderConfig } from "@/features/core/types/header-config";
|
||||
import { playerQueries } from "@/features/players/queries";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import FullScreenLoader from "@/components/full-screen-loader";
|
||||
import mantineCssUrl from '@mantine/core/styles.css?url'
|
||||
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
|
||||
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
|
||||
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
|
||||
|
||||
export const Route = createRootRouteWithContext<{
|
||||
queryClient: QueryClient;
|
||||
@@ -60,12 +60,18 @@ export const Route = createRootRouteWithContext<{
|
||||
},
|
||||
{ rel: "manifest", href: "/site.webmanifest" },
|
||||
{ rel: "icon", href: "/favicon.ico" },
|
||||
{ rel: 'stylesheet', href: mantineCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineDatesCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineTiptapCssUrl }
|
||||
],
|
||||
}),
|
||||
errorComponent: (props) => {
|
||||
return (
|
||||
<RootDocument>
|
||||
<DefaultCatchBoundary {...props} />
|
||||
<Providers>
|
||||
<DefaultCatchBoundary {...props} />
|
||||
</Providers>
|
||||
</RootDocument>
|
||||
);
|
||||
},
|
||||
@@ -128,7 +134,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
<div className="app">{children}</div>
|
||||
<Scripts />
|
||||
<ReactQueryDevtools />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { redirect, createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
import Layout from "@/features/core/components/layout";
|
||||
import { useServerEvents } from "@/hooks/use-server-events";
|
||||
import { Loader } from "@mantine/core";
|
||||
import { Flex, Loader } from "@mantine/core";
|
||||
|
||||
export const Route = createFileRoute("/_authed")({
|
||||
beforeLoad: ({ context }) => {
|
||||
@@ -26,7 +26,9 @@ export const Route = createFileRoute("/_authed")({
|
||||
},
|
||||
pendingComponent: () => (
|
||||
<Layout>
|
||||
<Loader size="xl" />
|
||||
<Flex w='100%' h="40dvh" justify="center" align="flex-end">
|
||||
<Loader size='xl' />
|
||||
</Flex>
|
||||
</Layout>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { useBracketPreview } from "@/features/bracket/queries";
|
||||
import BracketPreview from "@/features/admin/components/preview";
|
||||
import { NumberInput } from "@mantine/core";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/preview")({
|
||||
component: RouteComponent,
|
||||
@@ -10,10 +11,25 @@ export const Route = createFileRoute("/_authed/admin/preview")({
|
||||
title: "Bracket Preview",
|
||||
},
|
||||
withPadding: false,
|
||||
fullWidth: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const bracket = useBracketPreview(16);
|
||||
return <BracketView bracket={bracket.data} />;
|
||||
const [n, setN] = useState(16);
|
||||
return (
|
||||
<>
|
||||
<NumberInput
|
||||
min={9}
|
||||
max={27}
|
||||
label="Number of teams"
|
||||
value={n}
|
||||
onChange={(value) => setN(value as number)}
|
||||
w={150}
|
||||
mb={2}
|
||||
mx="md"
|
||||
/>
|
||||
<BracketPreview n={n} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import ManageTournament from "@/features/tournaments/components/manage-tournament";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id")({
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
32
src/app/routes/_authed/admin/tournaments/$id/teams.tsx
Normal file
32
src/app/routes/_authed/admin/tournaments/$id/teams.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import ManageTeams from "@/features/teams/components/manage-teams";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
||||
return {
|
||||
tournament,
|
||||
};
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: `${context.tournament.name} Teams`,
|
||||
},
|
||||
withPadding: false,
|
||||
}),
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { tournament } = Route.useRouteContext();
|
||||
return <ManageTeams tournament={tournament} />;
|
||||
}
|
||||
@@ -1,30 +1,90 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import {
|
||||
tournamentQueries,
|
||||
useTournament,
|
||||
} from "@/features/tournaments/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
||||
import { Container } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { SpotifyControlsBar } from "@/features/spotify/components";
|
||||
|
||||
export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
)
|
||||
if (!tournament) throw redirect({ to: '/admin/tournaments' })
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
||||
return {
|
||||
tournament,
|
||||
}
|
||||
};
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
fullWidth: true,
|
||||
withPadding: false,
|
||||
showSpotifyPanel: true,
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: `Run ${context.tournament.name}`,
|
||||
},
|
||||
}),
|
||||
component: RouteComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams()
|
||||
return <p>Run tournament</p>
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
|
||||
const bracket: BracketData = useMemo(() => {
|
||||
if (!tournament.matches || tournament.matches.length === 0) {
|
||||
return { winners: [], losers: [] };
|
||||
}
|
||||
|
||||
const winnersMap = new Map<number, Match[]>();
|
||||
const losersMap = new Map<number, Match[]>();
|
||||
|
||||
tournament.matches
|
||||
.sort((a, b) => a.lid - b.lid)
|
||||
.forEach((match) => {
|
||||
if (!match.is_losers_bracket) {
|
||||
if (!winnersMap.has(match.round)) {
|
||||
winnersMap.set(match.round, []);
|
||||
}
|
||||
winnersMap.get(match.round)!.push(match);
|
||||
} else {
|
||||
if (!losersMap.has(match.round)) {
|
||||
losersMap.set(match.round, []);
|
||||
}
|
||||
losersMap.get(match.round)!.push(match);
|
||||
}
|
||||
});
|
||||
|
||||
const winners = Array.from(winnersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
|
||||
const losers = Array.from(losersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
return { winners, losers };
|
||||
}, [tournament.matches]);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<SpotifyControlsBar />
|
||||
{tournament.matches?.length ? (
|
||||
<BracketView bracket={bracket} showControls />
|
||||
) : (
|
||||
<SeedTournament
|
||||
tournamentId={tournament.id}
|
||||
teams={tournament.teams || []}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { TrophyIcon } from "@phosphor-icons/react";
|
||||
import ListLink from "@/components/list-link";
|
||||
import { Box, Divider, Text } from "@mantine/core";
|
||||
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||
import { Suspense } from "react";
|
||||
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
||||
|
||||
export const Route = createFileRoute("/_authed/")({
|
||||
component: Home,
|
||||
loader: () => ({
|
||||
withPadding: false
|
||||
})
|
||||
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
||||
<Home />
|
||||
</Suspense>,
|
||||
beforeLoad: async ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
|
||||
|
||||
return { tournament }
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
withPadding: false,
|
||||
header: {
|
||||
title: context.tournament.name || "FLXN"
|
||||
}
|
||||
}),
|
||||
pendingComponent: () => <UpcomingTournamentSkeleton />
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<>
|
||||
<Box h='60vh' p="md">
|
||||
<Text m='16vh' fw={500}>Some Content Here</Text>
|
||||
</Box>
|
||||
const { data: tournament } = useCurrentTournament();
|
||||
if (!tournament.matches || tournament.matches.length === 0) {
|
||||
return <UpcomingTournament tournament={tournament} />;
|
||||
}
|
||||
|
||||
<Box>
|
||||
<Text pl='md'>Quick Links</Text>
|
||||
<Divider />
|
||||
<ListLink label="All Tournaments" to="/tournaments" Icon={TrophyIcon} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
return <StartedTournament tournament={tournament} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Profile from "@/features/players/components/profile";
|
||||
import { playerQueries } from "@/features/players/queries";
|
||||
import HeaderSkeleton from "@/features/players/components/profile/header-skeleton";
|
||||
import ProfileSkeleton from "@/features/players/components/profile/skeleton";
|
||||
import { playerKeys, playerQueries } from "@/features/players/queries";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
const searchSchema = z.object({
|
||||
@@ -12,10 +15,16 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||
validateSearch: searchSchema,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
const { queryClient } = context;
|
||||
await prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.details(params.playerId)
|
||||
);
|
||||
await Promise.all([
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.details(params.playerId)
|
||||
),
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.matches(params.playerId)
|
||||
),
|
||||
]);
|
||||
},
|
||||
loader: ({ params, context }) => ({
|
||||
header: {
|
||||
@@ -24,10 +33,13 @@ export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||
settingsLink:
|
||||
context?.auth.user.id === params.playerId ? "/settings" : undefined,
|
||||
},
|
||||
refresh: [playerQueries.details(params.playerId).queryKey],
|
||||
withPadding: false,
|
||||
refresh: [playerKeys.details(params.playerId), playerKeys.matches(params.playerId), playerKeys.stats(params.playerId)],
|
||||
}),
|
||||
component: () => {
|
||||
const { playerId } = Route.useParams();
|
||||
return <Profile id={playerId} />;
|
||||
return <Suspense fallback={<ProfileSkeleton />}>
|
||||
<Profile id={playerId} />
|
||||
</Suspense>;
|
||||
},
|
||||
});
|
||||
|
||||
28
src/app/routes/_authed/stats.tsx
Normal file
28
src/app/routes/_authed/stats.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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 PlayerStatsTableSkeleton from "@/features/players/components/player-stats-table-skeleton";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
|
||||
export const Route = createFileRoute("/_authed/stats")({
|
||||
component: Stats,
|
||||
beforeLoad: ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
prefetchServerQuery(queryClient, playerQueries.allStats());
|
||||
},
|
||||
loader: () => ({
|
||||
withPadding: false,
|
||||
fullWidth: true,
|
||||
header: {
|
||||
title: "Player Stats"
|
||||
},
|
||||
refresh: [playerQueries.allStats().queryKey],
|
||||
}),
|
||||
});
|
||||
|
||||
function Stats() {
|
||||
return <Suspense fallback={<PlayerStatsTableSkeleton />}>
|
||||
<PlayerStatsTable />
|
||||
</Suspense>;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import TeamProfile from "@/features/teams/components/team-profile";
|
||||
import { teamQueries } from "@/features/teams/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import ProfileSkeleton from "@/features/teams/components/team-profile/skeleton";
|
||||
import { teamKeys, teamQueries } from "@/features/teams/queries";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { redirect, createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
const searchSchema = z.object({
|
||||
@@ -20,10 +21,13 @@ export const Route = createFileRoute("/_authed/teams/$teamId")({
|
||||
collapsed: true,
|
||||
withBackButton: true,
|
||||
},
|
||||
refresh: [teamQueries.details(params.teamId).queryKey],
|
||||
refresh: [teamKeys.details(params.teamId), teamKeys.matches(params.teamId), teamKeys.stats(params.teamId)],
|
||||
withPadding: false
|
||||
}),
|
||||
component: () => {
|
||||
const { teamId } = Route.useParams();
|
||||
return <TeamProfile id={teamId} />;
|
||||
return <Suspense fallback={<ProfileSkeleton />}>
|
||||
<TeamProfile id={teamId} />
|
||||
</Suspense>;
|
||||
},
|
||||
});
|
||||
|
||||
82
src/app/routes/_authed/tournaments/$id.bracket.tsx
Normal file
82
src/app/routes/_authed/tournaments/$id.bracket.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import {
|
||||
tournamentQueries,
|
||||
useTournament,
|
||||
} from "@/features/tournaments/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
||||
import { Container } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { SpotifyControlsBar } from "@/features/spotify/components";
|
||||
|
||||
export const Route = createFileRoute("/_authed/tournaments/$id/bracket")({
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
const tournament = await ensureServerQueryData(
|
||||
queryClient,
|
||||
tournamentQueries.details(params.id)
|
||||
);
|
||||
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
||||
return {
|
||||
tournament,
|
||||
};
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
fullWidth: true,
|
||||
withPadding: false,
|
||||
showSpotifyPanel: true,
|
||||
header: {
|
||||
withBackButton: true,
|
||||
title: `${context.tournament.name}`,
|
||||
},
|
||||
}),
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
|
||||
const bracket: BracketData = useMemo(() => {
|
||||
if (!tournament.matches || tournament.matches.length === 0) {
|
||||
return { winners: [], losers: [] };
|
||||
}
|
||||
|
||||
const winnersMap = new Map<number, Match[]>();
|
||||
const losersMap = new Map<number, Match[]>();
|
||||
|
||||
tournament.matches
|
||||
.sort((a, b) => a.lid - b.lid)
|
||||
.forEach((match) => {
|
||||
if (!match.is_losers_bracket) {
|
||||
if (!winnersMap.has(match.round)) {
|
||||
winnersMap.set(match.round, []);
|
||||
}
|
||||
winnersMap.get(match.round)!.push(match);
|
||||
} else {
|
||||
if (!losersMap.has(match.round)) {
|
||||
losersMap.set(match.round, []);
|
||||
}
|
||||
losersMap.get(match.round)!.push(match);
|
||||
}
|
||||
});
|
||||
|
||||
const winners = Array.from(winnersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
|
||||
const losers = Array.from(losersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
return { winners, losers };
|
||||
}, [tournament.matches]);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
<BracketView bracket={bracket} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { tournamentQueries } from '@/features/tournaments/queries';
|
||||
import Profile from '@/features/tournaments/components/profile';
|
||||
import { z } from "zod";
|
||||
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch';
|
||||
import { Suspense } from 'react';
|
||||
import ProfileSkeleton from '@/features/tournaments/components/profile/skeleton';
|
||||
|
||||
const searchSchema = z.object({
|
||||
tab: z.string().optional(),
|
||||
@@ -10,9 +12,9 @@ const searchSchema = z.object({
|
||||
|
||||
export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||
validateSearch: searchSchema,
|
||||
beforeLoad: async ({ context, params }) => {
|
||||
beforeLoad: ({ context, params }) => {
|
||||
const { queryClient } = context;
|
||||
await prefetchServerQuery(queryClient, tournamentQueries.details(params.tournamentId))
|
||||
prefetchServerQuery(queryClient, tournamentQueries.details(params.tournamentId))
|
||||
},
|
||||
loader: ({ params, context }) => ({
|
||||
header: {
|
||||
@@ -20,7 +22,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||
withBackButton: true,
|
||||
settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined
|
||||
},
|
||||
refresh: tournamentQueries.details(params.tournamentId).queryKey,
|
||||
refresh: [tournamentQueries.details(params.tournamentId).queryKey],
|
||||
withPadding: false
|
||||
}),
|
||||
component: RouteComponent,
|
||||
@@ -28,5 +30,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||
|
||||
function RouteComponent() {
|
||||
const tournamentId = Route.useParams().tournamentId;
|
||||
return <Profile id={tournamentId} />
|
||||
return <Suspense fallback={<ProfileSkeleton />}>
|
||||
<Profile id={tournamentId} />
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import Page from '@/components/page'
|
||||
import { Stack } from '@mantine/core'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { TournamentCard } from '@/features/tournaments/components/tournament-card'
|
||||
import { tournamentQueries, useTournaments } from '@/features/tournaments/queries'
|
||||
import { useAuth } from '@/contexts/auth-context'
|
||||
import { useSheet } from '@/hooks/use-sheet'
|
||||
import Sheet from '@/components/sheet/sheet'
|
||||
import TournamentForm from '@/features/tournaments/components/tournament-form'
|
||||
import { PlusIcon } from '@phosphor-icons/react'
|
||||
import Button from '@/components/button'
|
||||
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch'
|
||||
import { Suspense } from 'react'
|
||||
import TournamentCardList from '@/features/tournaments/components/tournament-card-list'
|
||||
import { Skeleton, Stack } from '@mantine/core'
|
||||
|
||||
export const Route = createFileRoute('/_authed/tournaments/')({
|
||||
beforeLoad: async ({ context }) => {
|
||||
const { queryClient } = context;
|
||||
await prefetchServerQuery(queryClient, tournamentQueries.list())
|
||||
prefetchServerQuery(queryClient, tournamentQueries.list())
|
||||
},
|
||||
loader: () => ({
|
||||
header: {
|
||||
@@ -27,27 +21,11 @@ export const Route = createFileRoute('/_authed/tournaments/')({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { data: tournaments } = useTournaments();
|
||||
const { roles } = useAuth();
|
||||
const sheet = useSheet();
|
||||
|
||||
return <Page>
|
||||
<Stack>
|
||||
{
|
||||
roles?.includes("Admin") ? (
|
||||
<>
|
||||
<Button leftSection={<PlusIcon />} variant='subtle' onClick={sheet.open}>Create Tournament</Button>
|
||||
<Sheet {...sheet.props} title='Create Tournament'>
|
||||
<TournamentForm close={sheet.close} />
|
||||
</Sheet>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
tournaments?.map((tournament: any) => (
|
||||
<TournamentCard key={tournament.id} tournament={tournament} />
|
||||
))
|
||||
}
|
||||
</Stack>
|
||||
</Page>
|
||||
return <Suspense fallback={<Stack gap="md">
|
||||
{Array(10).fill(null).map((_, index) => (
|
||||
<Skeleton height="120px" w="100%" />
|
||||
))}
|
||||
</Stack>}>
|
||||
<TournamentCardList />
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial connection messages
|
||||
const connectMessage = `data: ${JSON.stringify({ type: "connected" })}\n\n`;
|
||||
controller.enqueue(new TextEncoder().encode(connectMessage));
|
||||
|
||||
// Listen for events and broadcast to all connections
|
||||
const handleEvent = (event: ServerEvent) => {
|
||||
logger.info('ServerEvents | Event received', event);
|
||||
const message = `data: ${JSON.stringify(event)}\n\n`;
|
||||
@@ -25,8 +23,9 @@ export const ServerRoute = createServerFileRoute("/api/events/$").middleware([su
|
||||
};
|
||||
|
||||
serverEvents.on("test", handleEvent);
|
||||
serverEvents.on("match", handleEvent);
|
||||
serverEvents.on("reaction", handleEvent);
|
||||
|
||||
// Keep alive ping every 30 seconds
|
||||
const pingInterval = setInterval(() => {
|
||||
try {
|
||||
const pingMessage = `data: ${JSON.stringify({ type: "ping" })}\n\n`;
|
||||
|
||||
@@ -5,7 +5,7 @@ export const ServerRoute = createServerFileRoute("/api/files/$collection/$record
|
||||
GET: async ({ params, request }) => {
|
||||
try {
|
||||
const { collection, recordId, file } = params;
|
||||
const pocketbaseUrl = process.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
const pocketbaseUrl = process.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
|
||||
const fileUrl = `${pocketbaseUrl}/api/files/${collection}/${recordId}/${file}`;
|
||||
|
||||
logger.info('File proxy', {
|
||||
|
||||
127
src/app/routes/api/spotify/callback.ts
Normal file
127
src/app/routes/api/spotify/callback.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyAuth } from '@/lib/spotify/auth'
|
||||
|
||||
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!
|
||||
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!
|
||||
const SPOTIFY_REDIRECT_URI = import.meta.env.VITE_SPOTIFY_REDIRECT_URI!
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/callback').methods({
|
||||
GET: async ({ request }: { request: Request }) => {
|
||||
const getReturnPath = (state: string | null): string => {
|
||||
if (!state) return '/';
|
||||
try {
|
||||
const decodedState = JSON.parse(atob(state));
|
||||
return decodedState.returnPath || '/';
|
||||
} catch {
|
||||
return '/';
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const url = new URL(request.url)
|
||||
const code = url.searchParams.get('code')
|
||||
const state = url.searchParams.get('state')
|
||||
const error = url.searchParams.get('error')
|
||||
|
||||
const returnPath = getReturnPath(state);
|
||||
|
||||
if (error) {
|
||||
console.error('Spotify OAuth error:', error)
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': returnPath + '?spotify_error=' + encodeURIComponent(error),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!code || !state) {
|
||||
console.error('Missing code or state:', { code: !!code, state: !!state })
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': returnPath + '?spotify_error=missing_code_or_state',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Token exchange attempt:', {
|
||||
client_id: SPOTIFY_CLIENT_ID,
|
||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||
has_code: !!code,
|
||||
has_state: !!state,
|
||||
})
|
||||
|
||||
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
console.error('Token exchange error:', {
|
||||
status: tokenResponse.status,
|
||||
statusText: tokenResponse.statusText,
|
||||
body: errorText,
|
||||
redirect_uri: SPOTIFY_REDIRECT_URI,
|
||||
})
|
||||
|
||||
const errorParam = encodeURIComponent(`${tokenResponse.status}: ${errorText}`)
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': `${returnPath}?spotify_error=token_exchange_failed&details=${errorParam}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json()
|
||||
|
||||
console.log('Token exchange successful:', {
|
||||
has_access_token: !!tokens.access_token,
|
||||
has_refresh_token: !!tokens.refresh_token,
|
||||
expires_in: tokens.expires_in,
|
||||
})
|
||||
|
||||
console.log('Decoded return path:', returnPath);
|
||||
|
||||
const response = new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': returnPath + '?spotify_auth=success',
|
||||
},
|
||||
})
|
||||
|
||||
const isSecure = process.env.NODE_ENV === 'production'
|
||||
const cookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${tokens.expires_in}`
|
||||
|
||||
response.headers.append('Set-Cookie', `spotify_access_token=${tokens.access_token}; ${cookieOptions}`)
|
||||
|
||||
if (tokens.refresh_token) {
|
||||
const refreshCookieOptions = `HttpOnly; Secure=${isSecure}; SameSite=Strict; Path=/; Max-Age=${60 * 60 * 24 * 30}` // 30 days
|
||||
response.headers.append('Set-Cookie', `spotify_refresh_token=${tokens.refresh_token}; ${refreshCookieOptions}`)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Spotify callback error:', error)
|
||||
const url = new URL(request.url);
|
||||
const state = url.searchParams.get('state');
|
||||
const returnPath = getReturnPath(state);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': returnPath + '?spotify_error=callback_failed',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
59
src/app/routes/api/spotify/capture.ts
Normal file
59
src/app/routes/api/spotify/capture.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/capture').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
// Get access token from cookies
|
||||
const cookies = request.headers.get('Cookie') || ''
|
||||
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||
|
||||
if (!accessTokenMatch) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
// Create a snapshot of the current playback state
|
||||
const snapshot = await spotifyClient.createPlaybackSnapshot()
|
||||
|
||||
if (!snapshot) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No active playback to capture' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ snapshot }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Spotify capture error:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to capture playback state'
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
202
src/app/routes/api/spotify/playback.ts
Normal file
202
src/app/routes/api/spotify/playback.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||
|
||||
function getAccessTokenFromCookies(request: Request): string | null {
|
||||
const cookieHeader = request.headers.get('cookie')
|
||||
if (!cookieHeader) return null
|
||||
|
||||
const cookies = Object.fromEntries(
|
||||
cookieHeader.split('; ').map(c => c.split('='))
|
||||
)
|
||||
|
||||
return cookies.spotify_access_token || null
|
||||
}
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/playback').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
const accessToken = getAccessTokenFromCookies(request)
|
||||
if (!accessToken) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { action, deviceId, volumePercent, trackId, positionMs } = body
|
||||
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
switch (action) {
|
||||
case 'play':
|
||||
await spotifyClient.play(deviceId)
|
||||
break
|
||||
case 'playTrack':
|
||||
if (!trackId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'trackId is required for playTrack action' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
await spotifyClient.playTrack(trackId, deviceId, positionMs)
|
||||
break
|
||||
case 'pause':
|
||||
await spotifyClient.pause()
|
||||
break
|
||||
case 'next':
|
||||
await spotifyClient.skipToNext()
|
||||
break
|
||||
case 'previous':
|
||||
await spotifyClient.skipToPrevious()
|
||||
break
|
||||
case 'volume':
|
||||
if (typeof volumePercent !== 'number') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'volumePercent must be a number' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
await spotifyClient.setVolume(volumePercent)
|
||||
break
|
||||
case 'transfer':
|
||||
if (!deviceId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'deviceId is required for transfer action' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
await spotifyClient.transferPlayback(deviceId)
|
||||
break
|
||||
default:
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Invalid action' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Playback control error:', error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('NO_ACTIVE_DEVICE')) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No active device found. Please select a device first.' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (error.message.includes('PREMIUM_REQUIRED')) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Spotify Premium is required for playback control.' }),
|
||||
{
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
console.error('Full error details:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Playback control failed', details: error instanceof Error ? error.message : 'Unknown error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
GET: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
const accessToken = getAccessTokenFromCookies(request)
|
||||
if (!accessToken) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
const type = url.searchParams.get('type')
|
||||
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
if (type === 'devices') {
|
||||
const devices = await spotifyClient.getDevices()
|
||||
return new Response(
|
||||
JSON.stringify({ devices }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} else if (type === 'state') {
|
||||
const playbackState = await spotifyClient.getPlaybackState()
|
||||
return new Response(
|
||||
JSON.stringify({ playbackState }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const [devices, playbackState] = await Promise.all([
|
||||
spotifyClient.getDevices(),
|
||||
spotifyClient.getPlaybackState(),
|
||||
])
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ devices, playbackState }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get playback data error:', error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to get playback data' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
72
src/app/routes/api/spotify/resume.ts
Normal file
72
src/app/routes/api/spotify/resume.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/resume').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
// Get access token from cookies
|
||||
const cookies = request.headers.get('Cookie') || ''
|
||||
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||
|
||||
if (!accessTokenMatch) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
// Parse the request body to get the snapshot
|
||||
const body = await request.json()
|
||||
const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot }
|
||||
|
||||
if (!snapshot) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No snapshot provided' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Restore the playback state from the snapshot
|
||||
await spotifyClient.restorePlaybackSnapshot(snapshot)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Spotify resume error:', error)
|
||||
|
||||
let errorMessage = 'Failed to resume playback state'
|
||||
|
||||
// Handle common Spotify Premium requirement error
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Premium') || error.message.includes('403')) {
|
||||
errorMessage = 'Spotify Premium required for playback control'
|
||||
} else {
|
||||
errorMessage = error.message
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
81
src/app/routes/api/spotify/search.ts
Normal file
81
src/app/routes/api/spotify/search.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
|
||||
// Function to get Client Credentials access token
|
||||
async function getClientCredentialsToken(): Promise<string> {
|
||||
const clientId = process.env.VITE_SPOTIFY_CLIENT_ID
|
||||
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('Missing Spotify client credentials')
|
||||
}
|
||||
|
||||
const response = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get Spotify access token')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.access_token
|
||||
}
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/search').methods({
|
||||
GET: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
const url = new URL(request.url)
|
||||
const query = url.searchParams.get('q')
|
||||
|
||||
if (!query) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Query parameter q is required' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Get client credentials access token
|
||||
const accessToken = await getClientCredentialsToken()
|
||||
|
||||
// Search using Spotify API directly
|
||||
const searchUrl = `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=20`
|
||||
|
||||
const searchResponse = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!searchResponse.ok) {
|
||||
throw new Error('Spotify search request failed')
|
||||
}
|
||||
|
||||
const searchResult = await searchResponse.json()
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ tracks: searchResult.tracks.items }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Search failed', details: error instanceof Error ? error.message : 'Unknown error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
125
src/app/routes/api/spotify/token.ts
Normal file
125
src/app/routes/api/spotify/token.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
|
||||
const SPOTIFY_CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID!
|
||||
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET!
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/token').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { refresh_token } = body
|
||||
|
||||
if (!refresh_token) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'refresh_token is required' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const tokenResponse = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.json()
|
||||
console.error('Token refresh error:', error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to refresh token', details: error }),
|
||||
{
|
||||
status: tokenResponse.status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json()
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: tokens.access_token,
|
||||
expires_in: tokens.expires_in,
|
||||
scope: tokens.scope,
|
||||
token_type: tokens.token_type,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Token refresh endpoint error:', error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// GET endpoint to retrieve current tokens from cookies
|
||||
GET: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
const cookieHeader = request.headers.get('cookie')
|
||||
if (!cookieHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No cookies found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const cookies = Object.fromEntries(
|
||||
cookieHeader.split('; ').map((c: string) => c.split('='))
|
||||
)
|
||||
|
||||
const accessToken = cookies.spotify_access_token
|
||||
const refreshToken = cookies.spotify_refresh_token
|
||||
|
||||
if (!accessToken && !refreshToken) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No Spotify tokens found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: accessToken || null,
|
||||
refresh_token: refreshToken || null,
|
||||
has_tokens: !!(accessToken || refreshToken),
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Get tokens endpoint error:', error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
117
src/app/routes/api/teams/upload-logo.ts
Normal file
117
src/app/routes/api/teams/upload-logo.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||
import { superTokensRequestMiddleware } from '@/utils/supertokens';
|
||||
import { pbAdmin } from '@/lib/pocketbase/client';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { z } from 'zod';
|
||||
|
||||
const uploadSchema = z.object({
|
||||
teamId: z.string().min(1, 'Team ID is required'),
|
||||
});
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/teams/upload-logo')
|
||||
.middleware([superTokensRequestMiddleware])
|
||||
.methods({
|
||||
POST: async ({ request, context }) => {
|
||||
try {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
|
||||
if (!userId) return new Response('Unauthenticated', { status: 401 });
|
||||
|
||||
const formData = await request.formData();
|
||||
const teamId = formData.get('teamId') as string;
|
||||
const logoFile = formData.get('logo') as File;
|
||||
|
||||
const validationResult = uploadSchema.safeParse({ teamId });
|
||||
if (!validationResult.success) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid input',
|
||||
details: validationResult.error.issues
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
if (!logoFile || logoFile.size === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Logo file is required'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
|
||||
if (!allowedTypes.includes(logoFile.type)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid file type. Only JPEG, PNG and GIF are allowed.'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (logoFile.size > maxSize) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File too large. Maximum size is 10MB.'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const team = await pbAdmin.getTeam(teamId);
|
||||
if (!team) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Team not found'
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const user = await pbAdmin.getPlayerByAuthId(context.userAuthId)
|
||||
if (!team.players.map(p => p.id).includes(user!.id) && !isAdmin)
|
||||
return new Response('Unauthorized', { status: 403 });
|
||||
|
||||
logger.info('Uploading team logo', {
|
||||
teamId,
|
||||
fileName: logoFile.name,
|
||||
fileSize: logoFile.size,
|
||||
userId
|
||||
});
|
||||
|
||||
const pbFormData = new FormData();
|
||||
pbFormData.append('logo', logoFile);
|
||||
|
||||
const updatedTeam= await pbAdmin.updateTeam(teamId, pbFormData as any);
|
||||
|
||||
logger.info('Team logo uploaded successfully', {
|
||||
teamId,
|
||||
logo: updatedTeam.logo
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
team: updatedTeam,
|
||||
message: 'Logo uploaded successfully'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('Error uploading team logo:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to upload logo',
|
||||
message: error.message || 'Unknown error occurred'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -18,10 +18,21 @@ function RouteComponent() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const redirect = urlParams.get('redirect')
|
||||
|
||||
if (redirect) {
|
||||
const isServerFunction = redirect && (
|
||||
redirect.startsWith('_serverFn') ||
|
||||
redirect.startsWith('api/') ||
|
||||
redirect.includes('_serverFn')
|
||||
);
|
||||
|
||||
if (redirect && !isServerFunction) {
|
||||
window.location.href = decodeURIComponent(redirect)
|
||||
} else {
|
||||
window.location.href = '/'
|
||||
const referrer = document.referrer;
|
||||
const referrerUrl = referrer && !referrer.includes('/_serverFn') && !referrer.includes('/api/')
|
||||
? referrer
|
||||
: '/';
|
||||
|
||||
window.location.href = referrerUrl;
|
||||
}
|
||||
} else {
|
||||
window.location.href = '/login'
|
||||
|
||||
@@ -7,23 +7,22 @@ import {
|
||||
redirect,
|
||||
} from '@tanstack/react-router'
|
||||
import type { ErrorComponentProps } from '@tanstack/react-router'
|
||||
import {
|
||||
Box,
|
||||
import {
|
||||
Box,
|
||||
Button as MantineButton,
|
||||
Text,
|
||||
Title,
|
||||
Stack,
|
||||
Group,
|
||||
Alert,
|
||||
Text,
|
||||
Stack,
|
||||
Group,
|
||||
Collapse,
|
||||
Code,
|
||||
ThemeIcon
|
||||
Container,
|
||||
Center
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { useEffect } from 'react'
|
||||
import toast from '@/lib/sonner'
|
||||
import { logger } from '@/lib/logger'
|
||||
import { ExclamationMarkIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||
import { XCircleIcon, WarningIcon } from '@phosphor-icons/react'
|
||||
import Button from './button'
|
||||
|
||||
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
@@ -50,112 +49,90 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
|
||||
if (errorMessage.toLowerCase().includes('unauthorized')) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '50vh',
|
||||
padding: 'var(--mantine-spacing-xl)',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="lg">
|
||||
<ThemeIcon color="red" size={80} radius="xl">
|
||||
<XCircleIcon size={48} />
|
||||
</ThemeIcon>
|
||||
<Title order={2} ta="center">Access Denied</Title>
|
||||
<Text size="lg" c="dimmed" ta="center">
|
||||
You don't have permission to access this.
|
||||
</Text>
|
||||
<Group>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
<MantineButton
|
||||
component={Link}
|
||||
to="/"
|
||||
variant="filled"
|
||||
>
|
||||
Home
|
||||
</MantineButton>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Container size="sm" py="xl">
|
||||
<Center>
|
||||
<Stack align="center" gap="md">
|
||||
<XCircleIcon size={64} color="var(--mantine-color-red-6)" />
|
||||
<Text size="xl" fw={600}>Access Denied</Text>
|
||||
<Text c="dimmed" ta="center">
|
||||
You don't have permission to access this page.
|
||||
</Text>
|
||||
<Group gap="sm" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
<MantineButton
|
||||
component={Link}
|
||||
to="/"
|
||||
variant="filled"
|
||||
>
|
||||
Home
|
||||
</MantineButton>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '50vh',
|
||||
padding: 'var(--mantine-spacing-xl)',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="lg" maw={600}>
|
||||
<ThemeIcon color="red" size={80} radius="xl">
|
||||
<ExclamationMarkIcon size={48} />
|
||||
</ThemeIcon>
|
||||
|
||||
<Title order={2} ta="center">Something went wrong</Title>
|
||||
|
||||
<Text size="lg" c="dimmed" ta="center">
|
||||
There was an unexpected error. Please try again later.
|
||||
</Text>
|
||||
<Container size="sm" py="xl">
|
||||
<Center>
|
||||
<Stack align="center" gap="md" w="100%">
|
||||
<WarningIcon size={64} color="var(--mantine-color-red-6)" />
|
||||
|
||||
<Alert
|
||||
variant="light"
|
||||
color="red"
|
||||
title="Error Details"
|
||||
w="100%"
|
||||
>
|
||||
<Text mb="sm">{errorMessage}</Text>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
onClick={toggleDetails}
|
||||
>
|
||||
{detailsOpened ? 'Hide' : 'Show'} stack trace
|
||||
</Button>
|
||||
<Collapse in={detailsOpened}>
|
||||
<Code block mt="md" p="md">
|
||||
{errorStack}
|
||||
</Code>
|
||||
</Collapse>
|
||||
</Alert>
|
||||
<Text size="xl" fw={600}>Something went wrong</Text>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => router.invalidate()}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
{isRoot ? (
|
||||
<MantineButton
|
||||
component={Link}
|
||||
to="/"
|
||||
variant="filled"
|
||||
>
|
||||
Home
|
||||
</MantineButton>
|
||||
) : (
|
||||
<Text c="dimmed" ta="center">
|
||||
An error occurred while loading this page.
|
||||
</Text>
|
||||
|
||||
<Box w="100%" mt="md">
|
||||
<Text size="sm" c="dimmed" mb="xs">Error: {errorMessage}</Text>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => window.history.back()}
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
onClick={toggleDetails}
|
||||
fullWidth
|
||||
>
|
||||
Go Back
|
||||
{detailsOpened ? 'Hide' : 'Show'} details
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Collapse in={detailsOpened}>
|
||||
<Code block mt="sm" p="sm" style={{ fontSize: '11px' }}>
|
||||
{errorStack}
|
||||
</Code>
|
||||
</Collapse>
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="lg">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => router.invalidate()}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
{isRoot ? (
|
||||
<MantineButton
|
||||
component={Link}
|
||||
to="/"
|
||||
variant="filled"
|
||||
>
|
||||
Home
|
||||
</MantineButton>
|
||||
) : (
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const Avatar = ({
|
||||
color="initials"
|
||||
size={size}
|
||||
radius={radius}
|
||||
w="fit-content"
|
||||
w={size}
|
||||
styles={{
|
||||
image: {
|
||||
objectFit: "contain",
|
||||
|
||||
57
src/components/countdown.tsx
Normal file
57
src/components/countdown.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import useNow from '@/hooks/use-now';
|
||||
import { Text, Group } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface CountdownProps {
|
||||
date: Date;
|
||||
label?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface TimeLeft {
|
||||
days: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
function calculateTimeLeft(targetDate: Date, currentTime = new Date()): TimeLeft {
|
||||
const difference = targetDate.getTime() - currentTime.getTime();
|
||||
|
||||
if (difference <= 0) {
|
||||
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
|
||||
minutes: Math.floor((difference / 1000 / 60) % 60),
|
||||
seconds: Math.floor((difference / 1000) % 60)
|
||||
};
|
||||
}
|
||||
|
||||
export function Countdown({ date, label, color }: CountdownProps) {
|
||||
const now = useNow();
|
||||
const timeLeft = useMemo(() => calculateTimeLeft(date, now), [date, now]);
|
||||
|
||||
const formatTime = () => {
|
||||
const pad = (num: number) => num.toString().padStart(2, '0');
|
||||
|
||||
if (timeLeft.days > 0) {
|
||||
return `${timeLeft.days}d ${pad(timeLeft.hours)}:${pad(timeLeft.minutes)}:${pad(timeLeft.seconds)}`;
|
||||
} else {
|
||||
return `${pad(timeLeft.hours)}:${pad(timeLeft.minutes)}:${pad(timeLeft.seconds)}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group gap="xs">
|
||||
{label && <Text size='sm' fw={500}>{label}:</Text>}
|
||||
<Text size='sm' fw={600} c={color} ff="monospace">
|
||||
{formatTime()}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default Countdown;
|
||||
@@ -6,14 +6,16 @@ interface ListLinkProps {
|
||||
label: string;
|
||||
to: string;
|
||||
Icon?: Icon;
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ListLink = ({ label, to, Icon }: ListLinkProps) => {
|
||||
const ListLink = ({ label, to, Icon, disabled=false }: ListLinkProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavLink
|
||||
disabled={disabled}
|
||||
w="100%"
|
||||
p="md"
|
||||
component={"button"}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Container, ContainerProps } from "@mantine/core";
|
||||
import { Container, ContainerProps, Box } from "@mantine/core";
|
||||
import useRouterConfig from "@/features/core/hooks/use-router-config";
|
||||
import BackButton from "@/features/core/components/back-button";
|
||||
import SettingsButton from "@/features/core/components/settings-button";
|
||||
|
||||
interface PageProps extends ContainerProps, React.PropsWithChildren {
|
||||
noPadding?: boolean;
|
||||
@@ -16,8 +18,15 @@ const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => {
|
||||
m={0}
|
||||
maw={fullWidth ? '100%' : 600}
|
||||
mx="auto"
|
||||
pos="relative"
|
||||
{...props}
|
||||
>
|
||||
{header.collapsed && header.withBackButton && (
|
||||
<BackButton top={4} />
|
||||
)}
|
||||
{header.collapsed && header.settingsLink && (
|
||||
<SettingsButton to={header.settingsLink} />
|
||||
)}
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
41
src/components/rich-text-editor.tsx
Normal file
41
src/components/rich-text-editor.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { RichTextEditor as MantineRichTextEditor } from '@mantine/tiptap';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function RichTextEditor({
|
||||
value,
|
||||
onChange,
|
||||
}: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: value,
|
||||
immediatelyRender: false,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<MantineRichTextEditor editor={editor}>
|
||||
<MantineRichTextEditor.Toolbar>
|
||||
<MantineRichTextEditor.ControlsGroup>
|
||||
<MantineRichTextEditor.Bold />
|
||||
<MantineRichTextEditor.Italic />
|
||||
</MantineRichTextEditor.ControlsGroup>
|
||||
<MantineRichTextEditor.ControlsGroup>
|
||||
<MantineRichTextEditor.Blockquote />
|
||||
<MantineRichTextEditor.Hr />
|
||||
<MantineRichTextEditor.BulletList />
|
||||
<MantineRichTextEditor.OrderedList />
|
||||
</MantineRichTextEditor.ControlsGroup>
|
||||
</MantineRichTextEditor.Toolbar>
|
||||
|
||||
<MantineRichTextEditor.Content h="45vh" />
|
||||
</MantineRichTextEditor>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Container, useComputedColorScheme } from "@mantine/core";
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import { Box, Container, Flex, Loader, useComputedColorScheme } from "@mantine/core";
|
||||
import { PropsWithChildren, Suspense, useEffect, useRef } from "react";
|
||||
import { Drawer as VaulDrawer } from "vaul";
|
||||
import { useMantineColorScheme } from "@mantine/core";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface DrawerProps extends PropsWithChildren {
|
||||
@@ -17,6 +16,7 @@ const Drawer: React.FC<DrawerProps> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
const colorScheme = useComputedColorScheme("light");
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const appElement = document.querySelector(".app") as HTMLElement;
|
||||
@@ -59,11 +59,31 @@ const Drawer: React.FC<DrawerProps> = ({
|
||||
};
|
||||
}, [opened, colorScheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened || !contentRef.current) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (contentRef.current) {
|
||||
const drawerContent = contentRef.current.closest('[data-vaul-drawer-wrapper]');
|
||||
if (drawerContent) {
|
||||
(drawerContent as HTMLElement).style.height = 'auto';
|
||||
(drawerContent as HTMLElement).offsetHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(contentRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [opened, children]);
|
||||
|
||||
return (
|
||||
<VaulDrawer.Root open={opened} onOpenChange={onChange}>
|
||||
<VaulDrawer.Portal>
|
||||
<VaulDrawer.Overlay className={styles.drawerOverlay} />
|
||||
<VaulDrawer.Content className={styles.drawerContent}>
|
||||
<VaulDrawer.Content className={styles.drawerContent} aria-describedby="drawer" ref={contentRef}>
|
||||
<Container flex={1} p="md">
|
||||
<Box
|
||||
mb="sm"
|
||||
@@ -74,9 +94,15 @@ const Drawer: React.FC<DrawerProps> = ({
|
||||
mr="auto"
|
||||
style={{ borderRadius: "9999px" }}
|
||||
/>
|
||||
<Container mah="fit-content" mx="auto" maw="28rem" px={0}>
|
||||
<Container mx="auto" maw="28rem" px={0}>
|
||||
<VaulDrawer.Title>{title}</VaulDrawer.Title>
|
||||
{children}
|
||||
<Suspense fallback={
|
||||
<Flex justify='center' align='center' w='100%' h={400}>
|
||||
<Loader size='lg' />
|
||||
</Flex>
|
||||
}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</Container>
|
||||
</Container>
|
||||
</VaulDrawer.Content>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Modal as MantineModal, Title } from "@mantine/core";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Flex, Loader, Modal as MantineModal, Title } from "@mantine/core";
|
||||
import { PropsWithChildren, Suspense } from "react";
|
||||
|
||||
interface ModalProps extends PropsWithChildren {
|
||||
title?: string;
|
||||
@@ -13,7 +13,15 @@ const Modal: React.FC<ModalProps> = ({ title, children, opened, onClose }) => (
|
||||
onClose={onClose}
|
||||
title={<Title order={3}>{title}</Title>}
|
||||
>
|
||||
{children}
|
||||
<Suspense
|
||||
fallback={
|
||||
<Flex justify="center" align="center" w="100%" h={400}>
|
||||
<Loader size="lg" />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Suspense>
|
||||
</MantineModal>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PropsWithChildren, useCallback } from "react";
|
||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||
import Drawer from "./drawer";
|
||||
import Modal from "./modal";
|
||||
import { Box, ScrollArea } from "@mantine/core";
|
||||
import { ScrollArea } from "@mantine/core";
|
||||
|
||||
interface SheetProps extends PropsWithChildren {
|
||||
title?: string;
|
||||
@@ -29,7 +29,7 @@ const Sheet: React.FC<SheetProps> = ({ title, children, opened, onChange }) => {
|
||||
scrollbars="y"
|
||||
type="scroll"
|
||||
>
|
||||
<Box mah="70vh">{children}</Box>
|
||||
{children}
|
||||
</ScrollArea>
|
||||
</SheetComponent>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Text, UnstyledButton, Flex, Stack } from "@mantine/core";
|
||||
import { CaretRightIcon } from "@phosphor-icons/react";
|
||||
import { ComponentType, useContext } from "react";
|
||||
import React, { ComponentType, useContext } from "react";
|
||||
import { SlidePanelContext } from "./slide-panel-context";
|
||||
|
||||
interface SlidePanelFieldProps {
|
||||
@@ -11,7 +11,7 @@ interface SlidePanelFieldProps {
|
||||
title: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
formatValue?: (value: any) => string;
|
||||
formatValue?: (value: any) => string | React.ReactNode;
|
||||
componentProps?: Record<string, any>;
|
||||
withAsterisk?: boolean;
|
||||
error?: string;
|
||||
|
||||
@@ -17,6 +17,7 @@ interface SlidePanelProps {
|
||||
onCancel?: () => void;
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
maxHeight?: string;
|
||||
formProps?: Record<string, any>;
|
||||
loading?: boolean;
|
||||
@@ -28,6 +29,7 @@ const SlidePanel = ({
|
||||
onCancel,
|
||||
submitText = "Submit",
|
||||
cancelText = "Cancel",
|
||||
cancelColor = "red",
|
||||
maxHeight = "70vh",
|
||||
formProps = {},
|
||||
loading = false,
|
||||
@@ -114,7 +116,7 @@ const SlidePanel = ({
|
||||
{onCancel && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
color={cancelColor}
|
||||
fullWidth
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
@@ -165,14 +167,11 @@ const SlidePanel = ({
|
||||
bg="var(--mantine-color-dimmed)"
|
||||
my="xs"
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<panelConfig.Component
|
||||
value={tempValue}
|
||||
onChange={setTempValue}
|
||||
{...(panelConfig.componentProps || {})}
|
||||
/>
|
||||
</Box>
|
||||
<panelConfig.Component
|
||||
value={tempValue}
|
||||
onChange={setTempValue}
|
||||
{...(panelConfig.componentProps || {})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
margin-top: 24px;
|
||||
height: fit-content;
|
||||
height: auto !important;
|
||||
min-height: fit-content;
|
||||
max-height: 100dvh;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
outline: none;
|
||||
transition: height 0.2s ease-out;
|
||||
}
|
||||
|
||||
161
src/components/stats-overview.tsx
Normal file
161
src/components/stats-overview.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
ThemeIcon,
|
||||
Skeleton,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
CrownIcon,
|
||||
XIcon,
|
||||
FireIcon,
|
||||
ShieldIcon,
|
||||
ChartLineUpIcon,
|
||||
ShieldCheckIcon,
|
||||
BoxingGloveIcon,
|
||||
Icon,
|
||||
ArrowUpIcon,
|
||||
ArrowDownIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { BaseStats } from "@/types/stats";
|
||||
|
||||
interface StatsOverviewProps {
|
||||
statsData: BaseStats | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const StatItem = ({
|
||||
label,
|
||||
value,
|
||||
suffix = "",
|
||||
Icon,
|
||||
}: {
|
||||
label: string;
|
||||
value: number | null;
|
||||
suffix?: string;
|
||||
Icon?: Icon;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Group justify="space-between" align="center" py="md" px="sm">
|
||||
<Group gap="sm" align="center">
|
||||
{Icon && (
|
||||
<ThemeIcon size="md" variant="light" radius="sm" color="gray">
|
||||
<Icon size={16} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
<Text size="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
{value !== null ? (
|
||||
<Text size="sm" fw={700} c="dimmed">
|
||||
{`${value}${suffix}`}
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton width={20} height={20} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsOverview = ({ statsData, isLoading = false }: StatsOverviewProps) => {
|
||||
if (!statsData && !isLoading) {
|
||||
return (
|
||||
<Box p="sm" h="auto" mih={200}>
|
||||
<Text ta="center" size="sm" fw={600} c="dimmed">
|
||||
No stats available yet
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!statsData) return null;
|
||||
|
||||
const overallStats = {
|
||||
matches: statsData.matches,
|
||||
wins: statsData.wins,
|
||||
losses: statsData.losses,
|
||||
total_cups_made: statsData.total_cups_made,
|
||||
total_cups_against: statsData.total_cups_against,
|
||||
};
|
||||
|
||||
const avgCupsPerMatch =
|
||||
overallStats.matches > 0
|
||||
? parseFloat((overallStats.total_cups_made / overallStats.matches).toFixed(1))
|
||||
: 0;
|
||||
|
||||
const avgCupsAgainstPerMatch =
|
||||
overallStats.matches > 0
|
||||
? parseFloat((overallStats.total_cups_against / overallStats.matches).toFixed(1))
|
||||
: 0;
|
||||
|
||||
const avgMarginOfVictory = statsData.margin_of_victory ? parseFloat(statsData.margin_of_victory.toFixed(1)) : 0;
|
||||
const avgMarginOfLoss = statsData.margin_of_loss ? parseFloat(statsData.margin_of_loss.toFixed(1)) : 0;
|
||||
|
||||
const allStats = [
|
||||
{ label: "Matches Played", value: overallStats.matches, Icon: BoxingGloveIcon },
|
||||
{ label: "Wins", value: overallStats.wins, Icon: CrownIcon },
|
||||
{ label: "Losses", value: overallStats.losses, Icon: XIcon },
|
||||
{ label: "Cups Made", value: overallStats.total_cups_made, Icon: FireIcon },
|
||||
{ label: "Cups Against", value: overallStats.total_cups_against, Icon: ShieldIcon },
|
||||
{ label: "Avg Cups Per Game", value: avgCupsPerMatch > 0 ? avgCupsPerMatch : null, Icon: ChartLineUpIcon },
|
||||
{ label: "Avg Cups Against", value: avgCupsAgainstPerMatch > 0 ? avgCupsAgainstPerMatch : null, Icon: ShieldCheckIcon },
|
||||
{ label: "Avg Win Margin", value: avgMarginOfVictory > 0 ? avgMarginOfVictory : null, Icon: ArrowUpIcon },
|
||||
{ label: "Avg Loss Margin", value: avgMarginOfLoss > 0 ? avgMarginOfLoss : null, Icon: ArrowDownIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={0}>
|
||||
{allStats.map((stat, index) => (
|
||||
<Box key={stat.label}>
|
||||
<StatItem
|
||||
label={stat.label}
|
||||
value={stat.value}
|
||||
Icon={stat.Icon}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{index < allStats.length - 1 && <Divider />}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatsSkeleton = () => {
|
||||
const skeletonStats = [
|
||||
{ label: "Matches Played", Icon: BoxingGloveIcon },
|
||||
{ label: "Wins", Icon: CrownIcon },
|
||||
{ label: "Losses", Icon: XIcon },
|
||||
{ label: "Cups Made", Icon: FireIcon },
|
||||
{ label: "Cups Against", Icon: ShieldIcon },
|
||||
{ label: "Avg Cups Per Game", Icon: ChartLineUpIcon },
|
||||
{ label: "Avg Cups Against", Icon: ShieldCheckIcon },
|
||||
{ label: "Avg Win Margin", Icon: ArrowUpIcon },
|
||||
{ label: "Avg Loss Margin", Icon: ArrowDownIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={0}>
|
||||
{skeletonStats.map((stat, index) => (
|
||||
<Box key={stat.label}>
|
||||
<StatItem
|
||||
label={stat.label}
|
||||
value={null}
|
||||
Icon={stat.Icon}
|
||||
isLoading={true}
|
||||
/>
|
||||
{index < skeletonStats.length - 1 && <Divider />}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsOverview;
|
||||
@@ -86,7 +86,7 @@ function SwipeableTabs({
|
||||
}
|
||||
}, [search?.tab]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeight = useCallback(() => {
|
||||
const activeSlideRef = slideRefs.current[activeTab];
|
||||
if (activeSlideRef) {
|
||||
const height = activeSlideRef.scrollHeight;
|
||||
@@ -94,6 +94,33 @@ function SwipeableTabs({
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
updateHeight();
|
||||
}, [activeTab, updateHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(updateHeight, 0);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [updateHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeSlideRef = slideRefs.current[activeTab];
|
||||
if (!activeSlideRef) return;
|
||||
|
||||
let timeoutId: any;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(updateHeight, 16);
|
||||
});
|
||||
|
||||
resizeObserver.observe(activeSlideRef);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [activeTab, updateHeight]);
|
||||
|
||||
const setControlRef = useCallback(
|
||||
(index: number) => (node: HTMLSpanElement | null) => {
|
||||
controlsRefs.current[index] = node;
|
||||
@@ -109,13 +136,14 @@ function SwipeableTabs({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box style={{ touchAction: "pan-y" }}>
|
||||
<Box
|
||||
ref={setRootRef}
|
||||
pos="sticky"
|
||||
top={0}
|
||||
style={{
|
||||
display: "flex",
|
||||
paddingInline: "var(--mantine-spacing-md)",
|
||||
marginBottom: "var(--mantine-spacing-md)",
|
||||
zIndex: 100,
|
||||
backgroundColor: "var(--mantine-color-body)",
|
||||
@@ -137,7 +165,7 @@ function SwipeableTabs({
|
||||
onClick={() => changeTab(index)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-md)",
|
||||
padding: "var(--mantine-spacing-sm) var(--mantine-spacing-xs)",
|
||||
textAlign: "center",
|
||||
color:
|
||||
activeTab === index
|
||||
@@ -155,7 +183,7 @@ function SwipeableTabs({
|
||||
component="span"
|
||||
style={{
|
||||
display: "inline-block",
|
||||
paddingInline: "1rem",
|
||||
paddingInline: "0.5rem",
|
||||
paddingBottom: "0.25rem",
|
||||
}}
|
||||
ref={setControlRef(index)}
|
||||
@@ -176,6 +204,7 @@ function SwipeableTabs({
|
||||
overflow: "hidden",
|
||||
height: carouselHeight === "auto" ? "auto" : `${carouselHeight}px`,
|
||||
transition: "height 300ms ease",
|
||||
touchAction: "pan-y",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { createContext, PropsWithChildren, useCallback, useContext, useMemo } from "react";
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { MantineColor, MantineColorScheme } from "@mantine/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Player } from "@/features/players/types";
|
||||
import { playerKeys, playerQueries, useMe } from "@/features/players/queries";
|
||||
|
||||
|
||||
interface AuthData {
|
||||
user: Player | undefined;
|
||||
metadata: { accentColor: MantineColor; colorScheme: MantineColorScheme };
|
||||
roles: string[];
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export const defaultAuthData: AuthData = {
|
||||
user: undefined,
|
||||
metadata: { accentColor: 'blue', colorScheme: 'auto' },
|
||||
metadata: { accentColor: "blue", colorScheme: "dark" },
|
||||
roles: [],
|
||||
}
|
||||
phone: ""
|
||||
};
|
||||
|
||||
export interface AuthContextType extends AuthData {
|
||||
set: ({ user, metadata, roles }: Partial<AuthContextType>) => void;
|
||||
@@ -27,39 +34,46 @@ const AuthContext = createContext<AuthContextType>({
|
||||
});
|
||||
|
||||
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { data } = useMe();
|
||||
|
||||
const set = useCallback((updates: Partial<AuthData>) => {
|
||||
queryClient.setQueryData(playerKeys.auth, (oldData: AuthData | undefined) => {
|
||||
const currentData = oldData || defaultAuthData;
|
||||
return {
|
||||
...currentData,
|
||||
...updates,
|
||||
metadata: updates.metadata
|
||||
? { ...currentData.metadata, ...updates.metadata }
|
||||
: currentData.metadata
|
||||
};
|
||||
});
|
||||
}, [queryClient]);
|
||||
|
||||
return (
|
||||
<AuthContext
|
||||
value={{
|
||||
user: data?.user || defaultAuthData.user,
|
||||
metadata: data?.metadata || defaultAuthData.metadata,
|
||||
roles: data?.roles || defaultAuthData.roles,
|
||||
set
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext>
|
||||
)
|
||||
const set = useCallback(
|
||||
(updates: Partial<AuthData>) => {
|
||||
queryClient.setQueryData(
|
||||
playerKeys.auth,
|
||||
(oldData: AuthData | undefined) => {
|
||||
const currentData = oldData || defaultAuthData;
|
||||
return {
|
||||
...currentData,
|
||||
...updates,
|
||||
metadata: updates.metadata
|
||||
? { ...currentData.metadata, ...updates.metadata }
|
||||
: currentData.metadata,
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
[queryClient]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
user: data?.user || defaultAuthData.user,
|
||||
metadata: data?.metadata || defaultAuthData.metadata,
|
||||
roles: data?.roles || defaultAuthData.roles,
|
||||
phone: data?.phone || "",
|
||||
set,
|
||||
}),
|
||||
[data, defaultAuthData]
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
470
src/contexts/spotify-context.tsx
Normal file
470
src/contexts/spotify-context.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import { createContext, useCallback, useEffect, useState, PropsWithChildren } from 'react';
|
||||
import { SpotifyAuth } from '@/lib/spotify/auth';
|
||||
import { useAuth } from './auth-context';
|
||||
import type {
|
||||
SpotifyContextType,
|
||||
SpotifyAuthState,
|
||||
SpotifyDevice,
|
||||
SpotifyPlaybackState,
|
||||
SpotifyPlaybackSnapshot,
|
||||
SpotifyTrack,
|
||||
} from '@/lib/spotify/types';
|
||||
|
||||
const defaultSpotifyState: SpotifyAuthState = {
|
||||
isAuthenticated: false,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
expiresAt: null,
|
||||
scopes: [],
|
||||
};
|
||||
|
||||
export const SpotifyContext = createContext<SpotifyContextType | null>(null);
|
||||
|
||||
export const SpotifyProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { roles } = useAuth();
|
||||
const isAdmin = roles?.includes('Admin') || false;
|
||||
|
||||
const [authState, setAuthState] = useState<SpotifyAuthState>(defaultSpotifyState);
|
||||
|
||||
const [currentTrack, setCurrentTrack] = useState<SpotifyTrack | null>(null);
|
||||
const [playbackState, setPlaybackState] = useState<SpotifyPlaybackState | null>(null);
|
||||
|
||||
const [devices, setDevices] = useState<SpotifyDevice[]>([]);
|
||||
const [activeDevice, setActiveDeviceState] = useState<SpotifyDevice | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [capturedState, setCapturedState] = useState<SpotifyPlaybackSnapshot | null>(null);
|
||||
const [isCaptureLoading, setIsCaptureLoading] = useState(false);
|
||||
const [isResumeLoading, setIsResumeLoading] = useState(false);
|
||||
|
||||
const spotifyAuth = new SpotifyAuth(
|
||||
import.meta.env.VITE_SPOTIFY_CLIENT_ID!,
|
||||
import.meta.env.VITE_SPOTIFY_REDIRECT_URI!
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin) {
|
||||
checkExistingAuth();
|
||||
handleOAuthCallback();
|
||||
}
|
||||
}, [isAdmin]);
|
||||
|
||||
const checkExistingAuth = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/spotify/token', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.access_token) {
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: Date.now() + (3600 * 1000),
|
||||
scopes: [],
|
||||
});
|
||||
|
||||
await Promise.all([getDevices(), refreshPlaybackState()]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check existing auth:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthCallback = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const spotifyAuth = urlParams.get('spotify_auth');
|
||||
const error = urlParams.get('spotify_error');
|
||||
const details = urlParams.get('details');
|
||||
|
||||
if (spotifyAuth === 'success') {
|
||||
checkExistingAuth();
|
||||
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete('spotify_auth');
|
||||
newUrl.searchParams.delete('state');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
|
||||
if (error) {
|
||||
let errorMessage = `Authentication failed: ${error}`;
|
||||
if (details) {
|
||||
errorMessage += ` - ${decodeURIComponent(details)}`;
|
||||
}
|
||||
setError(errorMessage);
|
||||
|
||||
console.error('Spotify OAuth Error:', { error, details });
|
||||
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete('spotify_error');
|
||||
newUrl.searchParams.delete('details');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const login = useCallback(() => {
|
||||
if (!isAdmin) return;
|
||||
spotifyAuth.startAuthFlow(window.location.pathname);
|
||||
}, [isAdmin, spotifyAuth]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setAuthState(defaultSpotifyState);
|
||||
setCurrentTrack(null);
|
||||
setPlaybackState(null);
|
||||
setDevices([]);
|
||||
setActiveDeviceState(null);
|
||||
setError(null);
|
||||
|
||||
document.cookie = 'spotify_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
document.cookie = 'spotify_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
}, []);
|
||||
|
||||
const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => {
|
||||
const response = await fetch(`/api/spotify/${endpoint}`, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = 'Request failed';
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch {
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
console.warn(`Non-JSON response from ${endpoint}:`, contentType);
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse JSON response from ${endpoint}:`, error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const play = useCallback(async (deviceId?: string) => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'play', deviceId }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 500);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||
setError(error.message);
|
||||
}
|
||||
console.warn('Playback action completed with warning:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const playTrack = useCallback(async (trackId: string, deviceId?: string, positionMs?: number) => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'playTrack', trackId, deviceId, positionMs }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 500);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||
setError(error.message);
|
||||
}
|
||||
console.warn('Track playback action completed with warning:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const pause = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'pause' }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 500);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||
setError(error.message);
|
||||
}
|
||||
console.warn('Playback action completed with warning:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const skipNext = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'next' }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 500);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||
setError(error.message);
|
||||
}
|
||||
console.warn('Playback action completed with warning:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const skipPrevious = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'previous' }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 500);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !error.message.includes('JSON')) {
|
||||
setError(error.message);
|
||||
}
|
||||
console.warn('Playback action completed with warning:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const setVolume = useCallback(async (volumePercent: number) => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'volume', volumePercent }),
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to set volume');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const getDevices = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await makeSpotifyRequest('playback?type=devices');
|
||||
setDevices(data.devices || []);
|
||||
|
||||
const active = data.devices?.find((d: SpotifyDevice) => d.is_active);
|
||||
if (active) {
|
||||
setActiveDeviceState(active);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to get devices');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const setActiveDevice = useCallback(async (deviceId: string) => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('playback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ action: 'transfer', deviceId }),
|
||||
});
|
||||
|
||||
const device = devices.find(d => d.id === deviceId);
|
||||
if (device) {
|
||||
setActiveDeviceState(device);
|
||||
}
|
||||
|
||||
setTimeout(getDevices, 1000);
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to set active device');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated, devices]);
|
||||
|
||||
const refreshPlaybackState = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
try {
|
||||
const data = await makeSpotifyRequest('playback?type=state');
|
||||
const state = data.playbackState;
|
||||
|
||||
setPlaybackState(state);
|
||||
setCurrentTrack(state?.item || null);
|
||||
|
||||
if (state?.device) {
|
||||
setActiveDeviceState(state.device);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh playback state:', error);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
const interval = setInterval(refreshPlaybackState, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [authState.isAuthenticated, refreshPlaybackState]);
|
||||
|
||||
const capturePlaybackState = useCallback(async () => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
|
||||
setIsCaptureLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await makeSpotifyRequest('capture', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (response.snapshot) {
|
||||
setCapturedState(response.snapshot);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to capture playback state');
|
||||
} finally {
|
||||
setIsCaptureLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated]);
|
||||
|
||||
const resumePlaybackState = useCallback(async () => {
|
||||
if (!authState.isAuthenticated || !capturedState) return;
|
||||
|
||||
setIsResumeLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await makeSpotifyRequest('resume', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ snapshot: capturedState }),
|
||||
});
|
||||
|
||||
setTimeout(refreshPlaybackState, 1000);
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to resume playback state');
|
||||
} finally {
|
||||
setIsResumeLoading(false);
|
||||
}
|
||||
}, [authState.isAuthenticated, capturedState, refreshPlaybackState]);
|
||||
|
||||
const clearCapturedState = useCallback(() => {
|
||||
setCapturedState(null);
|
||||
}, []);
|
||||
|
||||
const searchTracks = useCallback(async (query: string): Promise<SpotifyTrack[]> => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/spotify/search?q=${encodeURIComponent(query)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.tracks || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to search tracks:', error);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const contextValue: SpotifyContextType = {
|
||||
...authState,
|
||||
currentTrack,
|
||||
playbackState,
|
||||
devices,
|
||||
activeDevice,
|
||||
isLoading,
|
||||
error,
|
||||
capturedState,
|
||||
isCaptureLoading,
|
||||
isResumeLoading,
|
||||
login,
|
||||
logout,
|
||||
play,
|
||||
playTrack,
|
||||
pause,
|
||||
skipNext,
|
||||
skipPrevious,
|
||||
setVolume,
|
||||
getDevices,
|
||||
setActiveDevice,
|
||||
refreshPlaybackState,
|
||||
capturePlaybackState,
|
||||
resumePlaybackState,
|
||||
clearCapturedState,
|
||||
searchTracks,
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpotifyContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SpotifyContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Flex, Text } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { MatchCard } from './match-card';
|
||||
import { Match } from '../types';
|
||||
|
||||
interface BracketRoundProps {
|
||||
matches: Match[];
|
||||
roundIndex: number;
|
||||
getParentMatchOrder: (parentLid: number) => number | string;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
}
|
||||
|
||||
export const BracketRound: React.FC<BracketRoundProps> = ({
|
||||
matches,
|
||||
roundIndex,
|
||||
getParentMatchOrder,
|
||||
onAnnounce,
|
||||
}) => {
|
||||
const isBye = (type: string) => type?.toLowerCase() === 'bye';
|
||||
|
||||
return (
|
||||
<Flex direction="column" key={roundIndex} gap={24} justify="space-around">
|
||||
{matches.map((match, matchIndex) => {
|
||||
if (!match) return null;
|
||||
if (isBye(match.type)) return <></>; // for spacing
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="row"
|
||||
key={matchIndex}
|
||||
align="center"
|
||||
justify="end"
|
||||
gap={8}
|
||||
>
|
||||
<Text c="dimmed" fw="bolder">
|
||||
{match.order}
|
||||
</Text>
|
||||
<MatchCard
|
||||
match={match}
|
||||
getParentMatchOrder={getParentMatchOrder}
|
||||
onAnnounce={onAnnounce}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import React, { useCallback } from 'react';
|
||||
import { BracketMaps } from '../utils/bracket-maps';
|
||||
import { BracketRound } from './bracket-round';
|
||||
import { Match } from '../types';
|
||||
|
||||
interface BracketViewProps {
|
||||
bracket: Match[][];
|
||||
bracketMaps: BracketMaps;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
}
|
||||
|
||||
const BracketView: React.FC<BracketViewProps> = ({
|
||||
bracket,
|
||||
bracketMaps,
|
||||
onAnnounce,
|
||||
}) => {
|
||||
|
||||
const getParentMatchOrder = useCallback((parentLid: number): number | string => {
|
||||
const parentMatch = bracketMaps.matchByLid.get(parentLid);
|
||||
if (
|
||||
parentMatch &&
|
||||
parentMatch.order !== null &&
|
||||
parentMatch.order !== undefined
|
||||
) {
|
||||
return parentMatch.order;
|
||||
}
|
||||
return `Match ${parentLid}`;
|
||||
}, [bracketMaps]);
|
||||
|
||||
return (
|
||||
<Flex direction="row" gap={24} justify="left" pos="relative" p="xl">
|
||||
{bracket.map((round, roundIndex) => (
|
||||
<BracketRound
|
||||
key={roundIndex}
|
||||
matches={round}
|
||||
roundIndex={roundIndex}
|
||||
getParentMatchOrder={getParentMatchOrder}
|
||||
onAnnounce={onAnnounce}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BracketView;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ScrollArea, Text } from "@mantine/core";
|
||||
import BracketView from "./bracket-view";
|
||||
import useAppShellHeight from "@/hooks/use-appshell-height";
|
||||
import { BracketMaps } from "../utils/bracket-maps";
|
||||
import { Match } from "@/features/matches/types";
|
||||
|
||||
interface BracketProps {
|
||||
winners: Match[][];
|
||||
losers?: Match[][];
|
||||
bracketMaps: BracketMaps | null;
|
||||
}
|
||||
|
||||
const Bracket: React.FC<BracketProps> = ({ winners, losers, bracketMaps }) => {
|
||||
const height = useAppShellHeight();
|
||||
|
||||
if (!bracketMaps) return <p>Bracket not available.</p>;
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
h={`calc(${height} - 4rem)`}
|
||||
className="bracket-container"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, var(--mantine-color-default-border) 1px, transparent 1px)`,
|
||||
backgroundSize: "16px 16px",
|
||||
backgroundPosition: "0 0, 8px 8px",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Winners Bracket
|
||||
</Text>
|
||||
<BracketView bracket={winners} bracketMaps={bracketMaps} />
|
||||
</div>
|
||||
{losers && (
|
||||
<div>
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Losers Bracket
|
||||
</Text>
|
||||
<BracketView bracket={losers} bracketMaps={bracketMaps} />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bracket;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ActionIcon, Card, Text } from '@mantine/core';
|
||||
import { PlayIcon } from '@phosphor-icons/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { MatchSlot } from './match-slot';
|
||||
import { Match } from '../types';
|
||||
|
||||
interface MatchCardProps {
|
||||
match: Match;
|
||||
getParentMatchOrder: (parentLid: number) => number | string;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
}
|
||||
|
||||
export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
match,
|
||||
getParentMatchOrder,
|
||||
onAnnounce
|
||||
}) => {
|
||||
|
||||
const showAnnounce = useMemo(() =>
|
||||
onAnnounce && match.home.team && match.away.team,
|
||||
[onAnnounce, match.home.team, match.away.team]);
|
||||
|
||||
const handleAnnounce = useCallback(() =>
|
||||
onAnnounce?.(match.home.team, match.away.team), [match.home.team, match.away.team]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
pos="relative"
|
||||
w={200}
|
||||
style={{ overflow: 'visible' }}
|
||||
data-match-lid={match.lid}
|
||||
>
|
||||
<Card.Section withBorder p={0}>
|
||||
<MatchSlot slot={match.home} getParentMatchOrder={getParentMatchOrder} />
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section p={0} mb={-16}>
|
||||
<MatchSlot slot={match.away} getParentMatchOrder={getParentMatchOrder} />
|
||||
</Card.Section>
|
||||
|
||||
{match.reset && (
|
||||
<Text
|
||||
pos="absolute"
|
||||
top={-20}
|
||||
left={8}
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw="bold"
|
||||
>
|
||||
* If necessary
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showAnnounce && (
|
||||
<ActionIcon
|
||||
pos="absolute"
|
||||
variant="filled"
|
||||
color="green"
|
||||
top={-20}
|
||||
right={-12}
|
||||
onClick={handleAnnounce}
|
||||
bd="none"
|
||||
style={{ boxShadow: 'none' }}
|
||||
size="xs"
|
||||
>
|
||||
<PlayIcon size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { SeedBadge } from "./seed-badge";
|
||||
|
||||
interface MatchSlotProps {
|
||||
slot: any;
|
||||
getParentMatchOrder: (parentLid: number) => number | string;
|
||||
}
|
||||
|
||||
export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
slot,
|
||||
getParentMatchOrder,
|
||||
}) => {
|
||||
const renderSlotContent = () => {
|
||||
if (slot?.seed) {
|
||||
return slot.team ? (
|
||||
<Text size="xs">{slot.team.name}</Text>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
Team {slot.seed}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (slot?.parent_lid !== null && slot?.parent_lid !== undefined) {
|
||||
return (
|
||||
<Text c="dimmed" size="xs">
|
||||
{slot.loser ? "Loser" : "Winner"} of Match{" "}
|
||||
{getParentMatchOrder(slot.parent_lid)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (slot) {
|
||||
return (
|
||||
<Text c="dimmed" size="xs" fs="italic">
|
||||
TBD
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex align="stretch">
|
||||
{slot?.seed && <SeedBadge seed={slot.seed} />}
|
||||
<div style={{ flex: 1, padding: "4px 8px" }}>{renderSlotContent()}</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import {
|
||||
Text,
|
||||
Container,
|
||||
Flex,
|
||||
NumberInput,
|
||||
Group,
|
||||
Loader,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { bracketQueries, useBracketPreview } from "../queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createBracketMaps, BracketMaps } from "../utils/bracket-maps";
|
||||
import { BracketData, Match } from "../types";
|
||||
import Bracket from "./bracket";
|
||||
import "./styles.module.css";
|
||||
|
||||
interface PreviewTeam {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const PreviewBracket: React.FC = () => {
|
||||
const [teamCount, setTeamCount] = useState(20);
|
||||
const { data, isLoading, error } = useBracketPreview(teamCount);
|
||||
|
||||
const [teams, setTeams] = useState<PreviewTeam[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTeams(
|
||||
Array.from({ length: teamCount }, (_, i) => ({
|
||||
id: `team-${i + 1}`,
|
||||
name: `Team ${i + 1}`,
|
||||
}))
|
||||
);
|
||||
}, [teamCount]);
|
||||
|
||||
const [seededWinnersBracket, setSeededWinnersBracket] = useState<Match[][]>(
|
||||
[]
|
||||
);
|
||||
const [seededLosersBracket, setSeededLosersBracket] = useState<Match[][]>([]);
|
||||
const [bracketMaps, setBracketMaps] = useState<BracketMaps | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || teams.length === 0) return;
|
||||
|
||||
const maps = createBracketMaps(data);
|
||||
setBracketMaps(maps);
|
||||
|
||||
const mapBracket = (bracket: Match[][]) => {
|
||||
return bracket.map((round) =>
|
||||
round.map((match) => {
|
||||
const mappedMatch = { ...match };
|
||||
|
||||
if (match.home?.seed && match.home.seed > 0) {
|
||||
const teamIndex = match.home.seed - 1;
|
||||
if (teams[teamIndex]) {
|
||||
mappedMatch.home = {
|
||||
...match.home,
|
||||
team: teams[teamIndex],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (match.away?.seed && match.away.seed > 0) {
|
||||
const teamIndex = match.away.seed - 1;
|
||||
if (teams[teamIndex]) {
|
||||
mappedMatch.away = {
|
||||
...match.away,
|
||||
team: teams[teamIndex],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return mappedMatch;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const bracketData = data as BracketData;
|
||||
setSeededWinnersBracket(mapBracket(bracketData.winners));
|
||||
setSeededLosersBracket(mapBracket(bracketData.losers));
|
||||
}, [teams, data]);
|
||||
|
||||
if (error) return <p>Error loading bracket</p>;
|
||||
|
||||
return (
|
||||
<Container p={0} w="100%" style={{ userSelect: "none" }}>
|
||||
<Flex w="100%" justify="space-between" align="center" h="3rem">
|
||||
<Group gap="sm" mx="auto">
|
||||
<Text size="sm" c="dimmed">
|
||||
Teams:
|
||||
</Text>
|
||||
<NumberInput
|
||||
value={teamCount}
|
||||
onChange={(value) => setTeamCount(Number(value) || 12)}
|
||||
min={12}
|
||||
max={20}
|
||||
size="sm"
|
||||
w={80}
|
||||
allowDecimal={false}
|
||||
clampBehavior="strict"
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Flex w="100%" gap={24}>
|
||||
{isLoading ? (
|
||||
<Flex justify="center" align="center" h="20vh" w="100%">
|
||||
<Loader size="xl" />
|
||||
</Flex>
|
||||
) : (
|
||||
<Bracket
|
||||
winners={seededWinnersBracket}
|
||||
losers={seededLosersBracket}
|
||||
bracketMaps={bracketMaps}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
|
||||
interface SeedBadgeProps {
|
||||
seed: number;
|
||||
}
|
||||
|
||||
export const SeedBadge: React.FC<SeedBadgeProps> = ({ seed }) => {
|
||||
return (
|
||||
<Text
|
||||
size="xs"
|
||||
fw="bold"
|
||||
py="4"
|
||||
bg="var(--mantine-color-default-hover)"
|
||||
style={{
|
||||
width: "32px",
|
||||
textAlign: "center",
|
||||
color: "var(--mantine-color-text)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderTopLeftRadius: "var(--mantine-radius-default)",
|
||||
borderBottomLeftRadius: "var(--mantine-radius-default)",
|
||||
}}
|
||||
>
|
||||
{seed}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Flex, Text, Select, Card } from "@mantine/core";
|
||||
|
||||
interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SeedListProps {
|
||||
teams: Team[];
|
||||
onSeedChange: (currentIndex: number, newIndex: number) => void;
|
||||
}
|
||||
|
||||
export function SeedList({ teams, onSeedChange }: SeedListProps) {
|
||||
const seedOptions = teams.map((_, index) => ({
|
||||
value: index.toString(),
|
||||
label: `Seed ${index + 1}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap={8}>
|
||||
{teams.map((team, index) => (
|
||||
<Card key={team.id} withBorder p="xs">
|
||||
<Flex align="center" gap="xs" justify="space-between">
|
||||
<Flex align="center" gap="xs">
|
||||
<Select
|
||||
value={index.toString()}
|
||||
data={seedOptions}
|
||||
onChange={(value) => {
|
||||
if (value !== null) {
|
||||
const newIndex = parseInt(value);
|
||||
if (newIndex !== index) {
|
||||
onSeedChange(index, newIndex);
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
w={100}
|
||||
/>
|
||||
<Text size="sm" fw={500}>
|
||||
{team.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.bracket-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bracket-container {
|
||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { previewBracket } from "./server";
|
||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { BracketData } from "./types";
|
||||
|
||||
const bracketKeys = {
|
||||
preview: (teams: number) => ["bracket", "preview", teams] as const,
|
||||
};
|
||||
|
||||
export const bracketQueries = {
|
||||
preview: (teams: number) => ({
|
||||
queryKey: bracketKeys.preview(teams),
|
||||
queryFn: () => previewBracket({ data: teams }),
|
||||
}),
|
||||
};
|
||||
|
||||
export const useBracketPreview = (teams: number) =>
|
||||
useServerSuspenseQuery<BracketData>(bracketQueries.preview(teams));
|
||||
@@ -1,21 +0,0 @@
|
||||
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { z } from "zod";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import brackets from "./utils";
|
||||
import { BracketData } from "./types";
|
||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||
|
||||
const logger = new Logger("Bracket Generation");
|
||||
|
||||
export const previewBracket = createServerFn()
|
||||
.validator(z.number())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: teams }) =>
|
||||
toServerResult(async () => {
|
||||
logger.info("Generating bracket", teams);
|
||||
if (!Object.keys(brackets).includes(teams.toString()))
|
||||
throw Error("Bracket not available");
|
||||
return brackets[teams as keyof typeof brackets] as BracketData;
|
||||
})
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
/*export interface Slot {
|
||||
seed?: number;
|
||||
team?: any;
|
||||
}
|
||||
|
||||
export interface Match {
|
||||
lid: number;
|
||||
round: number;
|
||||
order: number | null;
|
||||
type: string;
|
||||
home: Slot;
|
||||
away: Slot;
|
||||
reset?: boolean;
|
||||
}*/
|
||||
import { Match } from "../matches/types";
|
||||
|
||||
export interface BracketData {
|
||||
winners: Match[][];
|
||||
losers: Match[][];
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { BracketData, Match } from "../types";
|
||||
|
||||
export interface BracketMaps {
|
||||
matchByLid: Map<number, Match>;
|
||||
matchByOrder: Map<number, Match>;
|
||||
allMatches: Match[];
|
||||
}
|
||||
|
||||
export function createBracketMaps(bracketData: BracketData): BracketMaps {
|
||||
const matchByLid = new Map<number, Match>();
|
||||
const matchByOrder = new Map<number, Match>();
|
||||
const allMatches: Match[] = [];
|
||||
|
||||
[...bracketData.winners, ...bracketData.losers].forEach((round) => {
|
||||
round.forEach((match) => {
|
||||
matchByLid.set(match.lid, match);
|
||||
|
||||
if (match.order !== null && match.order !== undefined) {
|
||||
matchByOrder.set(match.order, match);
|
||||
}
|
||||
|
||||
allMatches.push(match);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
matchByLid,
|
||||
matchByOrder,
|
||||
allMatches,
|
||||
};
|
||||
}
|
||||
|
||||
export function getMatchByLid(
|
||||
maps: BracketMaps,
|
||||
lid: number
|
||||
): Match | undefined {
|
||||
return maps.matchByLid.get(lid);
|
||||
}
|
||||
|
||||
export function getMatchByOrder(
|
||||
maps: BracketMaps,
|
||||
order: number
|
||||
): Match | undefined {
|
||||
return maps.matchByOrder.get(order);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Imports saved json dumps of bracket generation from a python script that I didn't prioritize converting to TS
|
||||
*/
|
||||
import b12 from "../../../../scripts/brackets/12.json";
|
||||
import b13 from "../../../../scripts/brackets/13.json";
|
||||
import b14 from "../../../../scripts/brackets/14.json";
|
||||
import b15 from "../../../../scripts/brackets/15.json";
|
||||
import b16 from "../../../../scripts/brackets/16.json";
|
||||
import b17 from "../../../../scripts/brackets/17.json";
|
||||
import b18 from "../../../../scripts/brackets/18.json";
|
||||
import b19 from "../../../../scripts/brackets/19.json";
|
||||
import b20 from "../../../../scripts/brackets/20.json";
|
||||
|
||||
export default {
|
||||
12: b12,
|
||||
13: b13,
|
||||
14: b14,
|
||||
15: b15,
|
||||
16: b16,
|
||||
17: b17,
|
||||
18: b18,
|
||||
19: b19,
|
||||
20: b20,
|
||||
};
|
||||
@@ -19,7 +19,7 @@ const AdminPage = () => {
|
||||
label="Open Pocketbase"
|
||||
Icon={DatabaseIcon}
|
||||
onClick={() =>
|
||||
window.location.replace(import.meta.env.VITE_POCKETBASE_URL! + "/_/")
|
||||
window.location.replace(process.env.POCKETBASE_URL! + "/_/")
|
||||
}
|
||||
/>
|
||||
<ListLink
|
||||
|
||||
14
src/features/admin/components/preview.tsx
Normal file
14
src/features/admin/components/preview.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useBracketPreview } from "../../bracket/queries";
|
||||
import BracketView from "../../bracket/components/bracket-view";
|
||||
|
||||
interface BracketPreviewProps {
|
||||
n: number;
|
||||
}
|
||||
|
||||
const BracketPreview: React.FC<BracketPreviewProps> = ({ n }) => {
|
||||
const { data: bracket } = useBracketPreview(n);
|
||||
|
||||
return <BracketView bracket={bracket} />
|
||||
};
|
||||
|
||||
export default BracketPreview;
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Text, ScrollArea } from "@mantine/core";
|
||||
import { MatchCard } from "./match-card";
|
||||
import { BracketData } from "../types";
|
||||
import { Bracket } from "./bracket";
|
||||
import useAppShellHeight from "@/hooks/use-appshell-height";
|
||||
import { Match } from "@/features/matches/types";
|
||||
|
||||
interface BracketViewProps {
|
||||
bracket: BracketData;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
showControls?: boolean
|
||||
}
|
||||
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => {
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls }) => {
|
||||
const height = useAppShellHeight();
|
||||
const orders = useMemo(() => {
|
||||
const map: Record<number, number> = {};
|
||||
@@ -20,7 +20,7 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => {
|
||||
}, [bracket.winners, bracket.losers]);
|
||||
|
||||
return <ScrollArea
|
||||
h={`calc(${height} - 4rem)`}
|
||||
h={`calc(${height})`}
|
||||
className="bracket-container"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, var(--mantine-color-default-border) 1px, transparent 1px)`,
|
||||
@@ -32,14 +32,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => {
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Winners Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.winners} orders={orders} onAnnounce={onAnnounce} />
|
||||
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} />
|
||||
</div>
|
||||
{bracket.losers && (
|
||||
<div>
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Losers Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.losers} orders={orders} onAnnounce={onAnnounce} />
|
||||
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -5,16 +5,16 @@ import { MatchCard } from "./match-card";
|
||||
interface BracketProps {
|
||||
rounds: Match[][];
|
||||
orders: Record<number, number>;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const Bracket: React.FC<BracketProps> = ({
|
||||
rounds,
|
||||
orders,
|
||||
onAnnounce,
|
||||
showControls,
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction="row" gap={24} justify="left" p="xl">
|
||||
<Flex direction="row" gap={24} justify="left">
|
||||
{rounds.map((round, roundIndex) => (
|
||||
<Flex
|
||||
key={roundIndex}
|
||||
@@ -23,20 +23,21 @@ export const Bracket: React.FC<BracketProps> = ({
|
||||
pos="relative"
|
||||
gap={24}
|
||||
justify="space-around"
|
||||
p={24}
|
||||
>
|
||||
{round
|
||||
.filter((match) => !match.bye)
|
||||
.map((match) => {
|
||||
return (
|
||||
<div key={match.lid}>
|
||||
<MatchCard
|
||||
match={match}
|
||||
orders={orders}
|
||||
onAnnounce={onAnnounce}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{round.map((match) =>
|
||||
match.bye ? (
|
||||
<div key={match.lid}></div>
|
||||
) : (
|
||||
<div key={match.lid}>
|
||||
<MatchCard
|
||||
match={match}
|
||||
orders={orders}
|
||||
showControls={showControls}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
import { ActionIcon, Card, Flex, Text } from "@mantine/core";
|
||||
import { PlayIcon } from "@phosphor-icons/react";
|
||||
import { ActionIcon, Card, Flex, Text, Indicator } from "@mantine/core";
|
||||
import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { MatchSlot } from "./match-slot";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { Team } from "@/features/teams/types";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { MatchForm } from "./match-form";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { endMatch, startMatch } from "@/features/matches/server";
|
||||
import { tournamentKeys } from "@/features/tournaments/queries";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useSpotifyPlayback } from "@/lib/spotify/hooks";
|
||||
|
||||
interface MatchCardProps {
|
||||
match: Match;
|
||||
orders: Record<number, number>;
|
||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
match,
|
||||
orders,
|
||||
onAnnounce,
|
||||
showControls,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const editSheet = useSheet();
|
||||
const { playTrack, pause } = useSpotifyPlayback();
|
||||
const homeSlot = useMemo(
|
||||
() => ({
|
||||
from: orders[match.home_from_lid],
|
||||
from_loser: match.home_from_loser,
|
||||
team: match.home,
|
||||
seed: match.home_seed,
|
||||
cups: match.status === "ended" ? match.home_cups : undefined,
|
||||
isWinner:
|
||||
match.status === "ended" &&
|
||||
match.home_cups !== undefined &&
|
||||
match.away_cups !== undefined &&
|
||||
match.home_cups > match.away_cups,
|
||||
}),
|
||||
[match]
|
||||
);
|
||||
@@ -30,69 +48,259 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
from_loser: match.away_from_loser,
|
||||
team: match.away,
|
||||
seed: match.away_seed,
|
||||
cups: match.status === "ended" ? match.away_cups : undefined,
|
||||
isWinner:
|
||||
match.status === "ended" &&
|
||||
match.away_cups !== undefined &&
|
||||
match.home_cups !== undefined &&
|
||||
match.away_cups > match.home_cups,
|
||||
}),
|
||||
[match]
|
||||
);
|
||||
|
||||
const showAnnounce = useMemo(
|
||||
() => onAnnounce && match.home && match.away,
|
||||
[onAnnounce, match.home, match.away]
|
||||
const showToolbar = useMemo(
|
||||
() => match.status === "ready" && showControls,
|
||||
[match.status, showControls]
|
||||
);
|
||||
|
||||
const handleAnnounce = useCallback(
|
||||
() => onAnnounce?.(match.home, match.away),
|
||||
[match.home, match.away]
|
||||
const showEditButton = useMemo(
|
||||
() => showControls && match.status === "started",
|
||||
[showControls, match.status]
|
||||
);
|
||||
|
||||
const hasWalkoutData = showControls && match.home && match.away && 'song_id' in match.home && 'song_id' in match.away;
|
||||
|
||||
const start = useServerMutation({
|
||||
mutationFn: startMatch,
|
||||
successMessage: "Match started!",
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tournamentKeys.details(match.tournament.id),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const end = useServerMutation({
|
||||
mutationFn: endMatch,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tournamentKeys.details(match.tournament.id),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
async (data: {
|
||||
home_cups: number;
|
||||
away_cups: number;
|
||||
ot_count: number;
|
||||
}) => {
|
||||
end.mutate({
|
||||
data: {
|
||||
...data,
|
||||
matchId: match.id,
|
||||
},
|
||||
});
|
||||
editSheet.close();
|
||||
},
|
||||
[match.id, editSheet]
|
||||
);
|
||||
|
||||
const speak = useCallback((text: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!("speechSynthesis" in window)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
|
||||
const preferredVoice =
|
||||
voices.find(
|
||||
(voice) =>
|
||||
voice.lang.startsWith("en") && voice.name.includes("Daniel")
|
||||
) ||
|
||||
voices.find((voice) => voice.lang.startsWith("en") && voice.default);
|
||||
|
||||
if (preferredVoice) {
|
||||
utterance.voice = preferredVoice;
|
||||
}
|
||||
|
||||
utterance.rate = 0.9;
|
||||
utterance.volume = 0.8;
|
||||
utterance.pitch = 1.0;
|
||||
|
||||
utterance.onend = () => resolve();
|
||||
utterance.onerror = () => resolve();
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const playTeamWalkout = useCallback((team: Team): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const songDuration = (team.song_end - team.song_start) * 1000;
|
||||
|
||||
playTrack(team.song_id, undefined, team.song_start * 1000);
|
||||
|
||||
setTimeout(async () => {
|
||||
await pause();
|
||||
resolve();
|
||||
}, songDuration);
|
||||
});
|
||||
}, [playTrack, pause]);
|
||||
|
||||
const handleSpeakerClick = useCallback(async () => {
|
||||
if (!hasWalkoutData || !match.home?.name || !match.away?.name) {
|
||||
await speak(`${match.home?.name || "Home"} vs. ${match.away?.name || "Away"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const homeTeam = match.home as Team;
|
||||
const awayTeam = match.away as Team;
|
||||
|
||||
await playTeamWalkout(homeTeam);
|
||||
await speak(homeTeam.name);
|
||||
await speak("versus");
|
||||
await playTeamWalkout(awayTeam);
|
||||
await speak(awayTeam.name);
|
||||
await speak("have fun, good luck!");
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Walkout sequence error:', error);
|
||||
await speak(`${match.home.name} vs. ${match.away.name}`);
|
||||
}
|
||||
}, [hasWalkoutData, match.home, match.away, speak, playTeamWalkout]);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
start.mutate({
|
||||
data: match.id,
|
||||
});
|
||||
|
||||
// Play walkout sequence after starting the match
|
||||
if (hasWalkoutData && match.home?.name && match.away?.name) {
|
||||
try {
|
||||
const homeTeam = match.home as Team;
|
||||
const awayTeam = match.away as Team;
|
||||
|
||||
await playTeamWalkout(homeTeam);
|
||||
await speak(homeTeam.name);
|
||||
await speak("versus");
|
||||
await playTeamWalkout(awayTeam);
|
||||
await speak(awayTeam.name);
|
||||
await speak("have fun, good luck!");
|
||||
} catch (error) {
|
||||
console.warn('Auto-walkout sequence error:', error);
|
||||
}
|
||||
}
|
||||
}, [match, start, hasWalkoutData, playTeamWalkout, speak]);
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" justify="end" gap={8}>
|
||||
<Text c="dimmed" fw="bolder">
|
||||
{match.order}
|
||||
</Text>
|
||||
<Card
|
||||
withBorder
|
||||
pos="relative"
|
||||
w={200}
|
||||
style={{ overflow: "visible" }}
|
||||
data-match-lid={match.lid}
|
||||
>
|
||||
<Card.Section withBorder p={0}>
|
||||
<MatchSlot {...homeSlot} />
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section p={0} mb={-16}>
|
||||
<MatchSlot {...awaySlot} />
|
||||
</Card.Section>
|
||||
|
||||
{match.reset && (
|
||||
<Text
|
||||
pos="absolute"
|
||||
top={-20}
|
||||
left={8}
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw="bold"
|
||||
<Flex align="stretch">
|
||||
<Indicator
|
||||
inline
|
||||
processing={match.status === "started"}
|
||||
color="red"
|
||||
size={12}
|
||||
disabled={match.status !== "started" || showEditButton}
|
||||
>
|
||||
<Card
|
||||
w={showToolbar || showEditButton ? 200 : 220}
|
||||
withBorder
|
||||
pos="relative"
|
||||
style={{ overflow: "visible" }}
|
||||
data-match-lid={match.lid}
|
||||
>
|
||||
* If necessary
|
||||
</Text>
|
||||
<Card.Section withBorder p={0}>
|
||||
<MatchSlot {...homeSlot} />
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section p={0} mb={-16}>
|
||||
<MatchSlot {...awaySlot} />
|
||||
</Card.Section>
|
||||
|
||||
{match.reset && (
|
||||
<Text
|
||||
pos="absolute"
|
||||
top={-20}
|
||||
left={8}
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw="bold"
|
||||
>
|
||||
* If necessary
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{showControls && match.status !== "tbd" && (
|
||||
<ActionIcon
|
||||
pos="absolute"
|
||||
bottom={-2}
|
||||
left={-26}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={handleSpeakerClick}
|
||||
>
|
||||
<SpeakerHighIcon size={12} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Card>
|
||||
</Indicator>
|
||||
|
||||
{showToolbar && (
|
||||
<Flex direction="column" justify="center" align="center">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
onClick={handleStart}
|
||||
size="sm"
|
||||
h="100%"
|
||||
radius="sm"
|
||||
ml={-4}
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<PlayIcon size={14} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{showAnnounce && (
|
||||
<ActionIcon
|
||||
pos="absolute"
|
||||
variant="filled"
|
||||
color="green"
|
||||
top={-20}
|
||||
right={-12}
|
||||
onClick={handleAnnounce}
|
||||
bd="none"
|
||||
style={{ boxShadow: "none" }}
|
||||
size="xs"
|
||||
>
|
||||
<PlayIcon size={12} />
|
||||
</ActionIcon>
|
||||
|
||||
{showEditButton && (
|
||||
<Flex direction="column" justify="center" align="center">
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
onClick={editSheet.open}
|
||||
size="sm"
|
||||
h="100%"
|
||||
radius="sm"
|
||||
ml={-4}
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
|
||||
<Sheet title="Edit Match" {...editSheet.props}>
|
||||
<MatchForm
|
||||
match={match}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={editSheet.close}
|
||||
/>
|
||||
</Sheet>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
156
src/features/bracket/components/match-form.tsx
Normal file
156
src/features/bracket/components/match-form.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Button, TextInput, Stack, Group, Text, Flex, Divider, NumberInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Match } from "@/features/matches/types";
|
||||
|
||||
interface MatchFormProps {
|
||||
match: Match;
|
||||
onSubmit: (data: {
|
||||
home_cups: number;
|
||||
away_cups: number;
|
||||
ot_count: number;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const MatchForm: React.FC<MatchFormProps> = ({
|
||||
match,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
home_cups: match.home_cups || 10,
|
||||
away_cups: match.away_cups || 10,
|
||||
ot_count: match.ot_count || 0,
|
||||
},
|
||||
validate: {
|
||||
home_cups: (value, values) => {
|
||||
if (value === null || value === undefined) return "Home cups is required";
|
||||
if (values.ot_count > 0) return null;
|
||||
|
||||
const homeCups = Number(value);
|
||||
const awayCups = Number(values.away_cups);
|
||||
|
||||
if (homeCups !== 10 && awayCups !== 10) {
|
||||
return "At least one team must have 10 cups";
|
||||
}
|
||||
|
||||
// Both teams can't have 10 cups
|
||||
if (homeCups === 10 && awayCups === 10) {
|
||||
return "Both teams cannot have 10 cups";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
away_cups: (value, values) => {
|
||||
if (value === null || value === undefined) return "Away cups is required";
|
||||
if (values.ot_count > 0) return null;
|
||||
|
||||
const awayCups = Number(value);
|
||||
const homeCups = Number(values.home_cups);
|
||||
|
||||
if (homeCups !== 10 && awayCups !== 10) {
|
||||
return "At least one team must have 10 cups";
|
||||
}
|
||||
|
||||
if (homeCups === 10 && awayCups === 10) {
|
||||
return "Both teams cannot have 10 cups";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
ot_count: (value) =>
|
||||
value === null || value === undefined
|
||||
? "Overtime count is required"
|
||||
: null,
|
||||
},
|
||||
transformValues: (values) => ({
|
||||
home_cups: Number(values.home_cups),
|
||||
away_cups: Number(values.away_cups),
|
||||
ot_count: Number(values.ot_count),
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = form.onSubmit(() => {
|
||||
const transformedValues = form.getTransformedValues();
|
||||
onSubmit(transformedValues);
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<Flex mx='auto' direction='column' gap='md' miw={250}>
|
||||
|
||||
<Group gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} size="sm">
|
||||
{match.home?.name} Cups
|
||||
</Text>
|
||||
{
|
||||
match.home?.players.map(p => (<Text size='xs' c='dimmed'>
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>))
|
||||
}
|
||||
</Stack>
|
||||
<NumberInput
|
||||
ml='auto'
|
||||
min={0}
|
||||
w={70}
|
||||
ta="center"
|
||||
key={form.key("home_cups")}
|
||||
{...form.getInputProps("home_cups")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} size="sm">
|
||||
{match.away?.name} Cups
|
||||
</Text>
|
||||
{
|
||||
match.away?.players.map(p => (<Text size='xs' c='dimmed'>
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>))
|
||||
}
|
||||
</Stack>
|
||||
<NumberInput
|
||||
ml='auto'
|
||||
ta="center"
|
||||
w={70}
|
||||
min={0}
|
||||
key={form.key("away_cups")}
|
||||
{...form.getInputProps("away_cups")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">
|
||||
OT Count
|
||||
</Text>
|
||||
<TextInput
|
||||
ml='auto'
|
||||
ta="center"
|
||||
w={50}
|
||||
type="number"
|
||||
min={0}
|
||||
key={form.key("ot_count")}
|
||||
{...form.getInputProps("ot_count")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
</Flex>
|
||||
|
||||
<Stack mt="md">
|
||||
<Button type="submit">Update Match</Button>
|
||||
<Button variant="subtle" color="red" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Flex, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { CrownIcon } from "@phosphor-icons/react";
|
||||
import { SeedBadge } from "./seed-badge";
|
||||
import { TeamInfo } from "@/features/teams/types";
|
||||
|
||||
@@ -8,6 +9,8 @@ interface MatchSlotProps {
|
||||
from_loser?: boolean;
|
||||
team?: TeamInfo;
|
||||
seed?: number;
|
||||
cups?: number;
|
||||
isWinner?: boolean;
|
||||
}
|
||||
|
||||
export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
@@ -15,21 +18,51 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
from_loser,
|
||||
team,
|
||||
seed,
|
||||
cups,
|
||||
isWinner
|
||||
}) => (
|
||||
<Flex align="stretch">
|
||||
{seed && <SeedBadge seed={seed} />}
|
||||
<Flex p="4px 8px">
|
||||
{team ? (
|
||||
<Text size="xs">{team.name}</Text>
|
||||
) : from ? (
|
||||
<Text c="dimmed" size="xs">
|
||||
{from_loser ? "Loser" : "Winner"} of Match {from}
|
||||
</Text>
|
||||
) : (
|
||||
<Text c="dimmed" size="xs">
|
||||
TBD
|
||||
</Text>
|
||||
)}
|
||||
{(seed && seed > 0) ? <SeedBadge seed={seed} /> : undefined}
|
||||
<Flex p="4px 8px" w='100%' align="center">
|
||||
<Flex align="center" gap={4} flex={1}>
|
||||
{team ? (
|
||||
<>
|
||||
<Text
|
||||
size={team.name.length > 12 ? (team.name.length > 18 ? '10px' : '11px') : 'xs'}
|
||||
truncate
|
||||
style={{ minWidth: 0, flex: 1 }}
|
||||
>
|
||||
{team.name}
|
||||
</Text>
|
||||
{isWinner && (
|
||||
<CrownIcon
|
||||
size={14}
|
||||
weight="fill"
|
||||
style={{
|
||||
color: 'gold',
|
||||
marginLeft: '2px',
|
||||
marginTop: '-1px',
|
||||
filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.3))',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : from ? (
|
||||
<Text c="dimmed" size="xs" truncate style={{ minWidth: 0, flex: 1 }}>
|
||||
{from_loser ? "Loser" : "Winner"} of Match {from}
|
||||
</Text>
|
||||
) : (
|
||||
<Text c="dimmed" size="xs" truncate style={{ minWidth: 0, flex: 1 }}>
|
||||
TBD
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
{
|
||||
cups !== undefined ? (
|
||||
<Text ta='center' w={15} fw="800" ml={4} size="xs">{cups}</Text>
|
||||
) : undefined
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Imports saved json dumps of bracket generation from a python script that I didn't prioritize converting to TS
|
||||
*/
|
||||
import b10 from "../../../../brackets/10.json";
|
||||
import b11 from "../../../../brackets/11.json";
|
||||
import b12 from "../../../../brackets/12.json";
|
||||
import b13 from "../../../../brackets/13.json";
|
||||
import b14 from "../../../../brackets/14.json";
|
||||
@@ -12,6 +14,8 @@ import b19 from "../../../../brackets/19.json";
|
||||
import b20 from "../../../../brackets/20.json";
|
||||
|
||||
export default {
|
||||
10: b10,
|
||||
11: b11,
|
||||
12: b12,
|
||||
13: b13,
|
||||
14: b14,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user