regionals enrollments
This commit is contained in:
109
pb_migrations/1771294794_created_groups.js
Normal file
109
pb_migrations/1771294794_created_groups.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/// <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_340646327",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation3177167065",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "tournament",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4113142680",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "order",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1568971955",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2529305176",
|
||||||
|
"maxSelect": 999,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "teams",
|
||||||
|
"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_3346940990",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "groups",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3346940990");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
})
|
||||||
52
pb_migrations/1771294861_updated_tournaments.js
Normal file
52
pb_migrations/1771294861_updated_tournaments.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("select3736761055")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(13, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3736761055",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "format",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(13, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "select3736761055",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "format",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"single_elim",
|
||||||
|
"double_elim",
|
||||||
|
"groups",
|
||||||
|
"swiss",
|
||||||
|
"swiss_bracket",
|
||||||
|
"round_robin"
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text3736761055")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
25
pb_migrations/1771294883_updated_tournaments.js
Normal file
25
pb_migrations/1771294883_updated_tournaments.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(14, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json118290348",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "group_config",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("json118290348")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
29
pb_migrations/1771294898_updated_tournaments.js
Normal file
29
pb_migrations/1771294898_updated_tournaments.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(15, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text2982008523",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "phase",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text2982008523")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
47
pb_migrations/1771295070_updated_matches.js
Normal file
47
pb_migrations/1771295070_updated_matches.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(22, new Field({
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_3346940990",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation1841317061",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "group",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(23, new Field({
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3987859035",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "match_type",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("relation1841317061")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("text3987859035")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -43,6 +43,7 @@ import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_aut
|
|||||||
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||||
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||||
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
import { Route as AuthedAdminTournamentsIdTeamsRouteImport } from './routes/_authed/admin/tournaments/$id/teams'
|
||||||
|
import { Route as AuthedAdminTournamentsIdAssignPartnersRouteImport } from './routes/_authed/admin/tournaments/$id/assign-partners'
|
||||||
|
|
||||||
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
const RefreshSessionRoute = RefreshSessionRouteImport.update({
|
||||||
id: '/refresh-session',
|
id: '/refresh-session',
|
||||||
@@ -221,6 +222,12 @@ const AuthedAdminTournamentsIdTeamsRoute =
|
|||||||
path: '/tournaments/$id/teams',
|
path: '/tournaments/$id/teams',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedAdminTournamentsIdAssignPartnersRoute =
|
||||||
|
AuthedAdminTournamentsIdAssignPartnersRouteImport.update({
|
||||||
|
id: '/tournaments/$id/assign-partners',
|
||||||
|
path: '/tournaments/$id/assign-partners',
|
||||||
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof AuthedIndexRoute
|
'/': typeof AuthedIndexRoute
|
||||||
@@ -252,6 +259,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
||||||
@@ -286,6 +294,7 @@ export interface FileRoutesByTo {
|
|||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
||||||
@@ -323,6 +332,7 @@ export interface FileRoutesById {
|
|||||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||||
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/_authed/admin/tournaments/$id/assign-partners': typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
'/_authed/admin/tournaments/$id/teams': typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileRoute
|
||||||
@@ -360,6 +370,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments/'
|
| '/tournaments/'
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/admin/tournaments/'
|
| '/admin/tournaments/'
|
||||||
|
| '/admin/tournaments/$id/assign-partners'
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/api/files/$collection/$recordId/$file'
|
| '/api/files/$collection/$recordId/$file'
|
||||||
@@ -394,6 +405,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/tournaments/$id/bracket'
|
| '/tournaments/$id/bracket'
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
|
| '/admin/tournaments/$id/assign-partners'
|
||||||
| '/admin/tournaments/$id/teams'
|
| '/admin/tournaments/$id/teams'
|
||||||
| '/admin/tournaments/run/$id'
|
| '/admin/tournaments/run/$id'
|
||||||
| '/api/files/$collection/$recordId/$file'
|
| '/api/files/$collection/$recordId/$file'
|
||||||
@@ -430,6 +442,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/tournaments/'
|
| '/_authed/tournaments/'
|
||||||
| '/_authed/tournaments/$id/bracket'
|
| '/_authed/tournaments/$id/bracket'
|
||||||
| '/_authed/admin/tournaments/'
|
| '/_authed/admin/tournaments/'
|
||||||
|
| '/_authed/admin/tournaments/$id/assign-partners'
|
||||||
| '/_authed/admin/tournaments/$id/teams'
|
| '/_authed/admin/tournaments/$id/teams'
|
||||||
| '/_authed/admin/tournaments/run/$id'
|
| '/_authed/admin/tournaments/run/$id'
|
||||||
| '/api/files/$collection/$recordId/$file'
|
| '/api/files/$collection/$recordId/$file'
|
||||||
@@ -695,6 +708,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
|
preLoaderRoute: typeof AuthedAdminTournamentsIdTeamsRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
|
'/_authed/admin/tournaments/$id/assign-partners': {
|
||||||
|
id: '/_authed/admin/tournaments/$id/assign-partners'
|
||||||
|
path: '/tournaments/$id/assign-partners'
|
||||||
|
fullPath: '/admin/tournaments/$id/assign-partners'
|
||||||
|
preLoaderRoute: typeof AuthedAdminTournamentsIdAssignPartnersRouteImport
|
||||||
|
parentRoute: typeof AuthedAdminRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,6 +724,7 @@ interface AuthedAdminRouteChildren {
|
|||||||
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
AuthedAdminPreviewRoute: typeof AuthedAdminPreviewRoute
|
||||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
AuthedAdminTournamentsIdAssignPartnersRoute: typeof AuthedAdminTournamentsIdAssignPartnersRoute
|
||||||
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
|
AuthedAdminTournamentsIdTeamsRoute: typeof AuthedAdminTournamentsIdTeamsRoute
|
||||||
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
||||||
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
|
AuthedAdminTournamentsIdIndexRoute: typeof AuthedAdminTournamentsIdIndexRoute
|
||||||
@@ -715,6 +736,8 @@ const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
|||||||
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
AuthedAdminPreviewRoute: AuthedAdminPreviewRoute,
|
||||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||||
|
AuthedAdminTournamentsIdAssignPartnersRoute:
|
||||||
|
AuthedAdminTournamentsIdAssignPartnersRoute,
|
||||||
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
|
AuthedAdminTournamentsIdTeamsRoute: AuthedAdminTournamentsIdTeamsRoute,
|
||||||
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
||||||
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,
|
AuthedAdminTournamentsIdIndexRoute: AuthedAdminTournamentsIdIndexRoute,
|
||||||
|
|||||||
167
src/app/routes/_authed/admin/tournaments/$id/assign-partners.tsx
Normal file
167
src/app/routes/_authed/admin/tournaments/$id/assign-partners.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
|
||||||
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
|
import { Stack, Text, Button, Alert, LoadingOverlay, Group } from "@mantine/core";
|
||||||
|
import { useState } from "react";
|
||||||
|
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
|
||||||
|
import useConfirmTeamAssignments from "@/features/tournaments/hooks/use-confirm-team-assignments";
|
||||||
|
import TeamAssignmentPreview from "@/features/tournaments/components/team-assignment-preview";
|
||||||
|
import { WarningCircleIcon, ShuffleIcon, CheckCircleIcon } from "@phosphor-icons/react";
|
||||||
|
import { PlayerInfo } from "@/features/players/types";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authed/admin/tournaments/$id/assign-partners")({
|
||||||
|
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: `Manage ${context.tournament.name}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface TeamAssignment {
|
||||||
|
player1: PlayerInfo;
|
||||||
|
player2: PlayerInfo;
|
||||||
|
teamName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: freeAgents } = useFreeAgents(id);
|
||||||
|
const [assignments, setAssignments] = useState<TeamAssignment[] | null>(null);
|
||||||
|
const [currentSeed, setCurrentSeed] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const generateMutation = useGenerateRandomTeams();
|
||||||
|
const confirmMutation = useConfirmTeamAssignments();
|
||||||
|
|
||||||
|
const hasOddPlayers = freeAgents.length % 2 !== 0;
|
||||||
|
const hasEnoughPlayers = freeAgents.length >= 2;
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
generateMutation.mutate(
|
||||||
|
{ data: { tournamentId: id, seed: currentSeed } },
|
||||||
|
{
|
||||||
|
onSuccess: (result) => {
|
||||||
|
setAssignments(result.assignments);
|
||||||
|
setCurrentSeed(result.seed);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReroll = () => {
|
||||||
|
if (currentSeed === undefined) return;
|
||||||
|
generateMutation.mutate(
|
||||||
|
{ data: { tournamentId: id, seed: currentSeed + 1 } },
|
||||||
|
{
|
||||||
|
onSuccess: (result) => {
|
||||||
|
setAssignments(result.assignments);
|
||||||
|
setCurrentSeed(result.seed);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!assignments) return;
|
||||||
|
|
||||||
|
const formattedAssignments = assignments.map((a) => ({
|
||||||
|
player1Id: a.player1.id,
|
||||||
|
player2Id: a.player2.id,
|
||||||
|
teamName: a.teamName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
confirmMutation.mutate(
|
||||||
|
{ data: { tournamentId: id, assignments: formattedAssignments } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentQueries.details(id).queryKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentQueries.free_agents(id).queryKey });
|
||||||
|
navigate({ to: "/admin/tournaments/$id", params: { id } });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg" pos="relative">
|
||||||
|
<LoadingOverlay visible={confirmMutation.isPending} />
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group gap="xs" align="baseline">
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
{freeAgents.length}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{freeAgents.length === 1 ? "player enrolled" : "players enrolled"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{!hasEnoughPlayers && (
|
||||||
|
<Alert color="yellow" icon={<WarningCircleIcon size={16} />}>
|
||||||
|
Need at least 2 players to create teams
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasOddPlayers && (
|
||||||
|
<Alert color="red" icon={<WarningCircleIcon size={16} />}>
|
||||||
|
Cannot create teams with an odd number of players. Please have one player unenroll.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!assignments && hasEnoughPlayers && !hasOddPlayers && (
|
||||||
|
<Button
|
||||||
|
leftSection={<ShuffleIcon size={18} />}
|
||||||
|
onClick={handleGenerate}
|
||||||
|
loading={generateMutation.isPending}
|
||||||
|
>
|
||||||
|
Generate Random Pairings
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{assignments && (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text size="lg" fw={600}>
|
||||||
|
Partner Assignments
|
||||||
|
</Text>
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
leftSection={<ShuffleIcon size={16} />}
|
||||||
|
onClick={handleReroll}
|
||||||
|
loading={generateMutation.isPending}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Re-roll
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<CheckCircleIcon size={18} />}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
loading={confirmMutation.isPending}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Confirm & Create Teams
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<TeamAssignmentPreview assignments={assignments} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { TeamInfo, Team } from "../teams/types";
|
|||||||
import { TournamentInfo } from "../tournaments/types";
|
import { TournamentInfo } from "../tournaments/types";
|
||||||
|
|
||||||
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
||||||
|
export type MatchType = "group_stage" | "knockout" | "winners" | "losers" | "bracket";
|
||||||
|
|
||||||
export interface Match {
|
export interface Match {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,6 +30,8 @@ export interface Match {
|
|||||||
updated: string;
|
updated: string;
|
||||||
home_seed?: number;
|
home_seed?: number;
|
||||||
away_seed?: number;
|
away_seed?: number;
|
||||||
|
match_type?: MatchType;
|
||||||
|
group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const matchInputSchema = z.object({
|
export const matchInputSchema = z.object({
|
||||||
@@ -53,6 +56,8 @@ export const matchInputSchema = z.object({
|
|||||||
away: z.string().min(1).optional(),
|
away: z.string().min(1).optional(),
|
||||||
home_seed: z.number().int().min(1).optional(),
|
home_seed: z.number().int().min(1).optional(),
|
||||||
away_seed: z.number().int().min(1).optional(),
|
away_seed: z.number().int().min(1).optional(),
|
||||||
|
match_type: z.enum(["group_stage", "knockout", "winners", "losers", "bracket"]).optional(),
|
||||||
|
group: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MatchInput = z.infer<typeof matchInputSchema>;
|
export type MatchInput = z.infer<typeof matchInputSchema>;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const teamInputSchema = z
|
|||||||
song_start: z.number().int().optional(),
|
song_start: z.number().int().optional(),
|
||||||
song_end: z.number().int().optional(),
|
song_end: z.number().int().optional(),
|
||||||
song_image_url: z.url("Invalid song image URL").optional(),
|
song_image_url: z.url("Invalid song image URL").optional(),
|
||||||
|
private: z.boolean().optional(),
|
||||||
players: z.array(z.string()).min(1, "At least one player is required").max(10, "Maximum 10 players allowed"),
|
players: z.array(z.string()).min(1, "At least one player is required").max(10, "Maximum 10 players allowed"),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
|
|||||||
162
src/features/tournaments/components/edit-enrolled-players.tsx
Normal file
162
src/features/tournaments/components/edit-enrolled-players.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
ActionIcon,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { TrashIcon } from "@phosphor-icons/react";
|
||||||
|
import { useCallback, memo } from "react";
|
||||||
|
import { useFreeAgents } from "../queries";
|
||||||
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
|
import { PlayerInfo, Player } from "@/features/players/types";
|
||||||
|
import Typeahead, { TypeaheadOption } from "@/components/typeahead";
|
||||||
|
import { usePlayers } from "@/features/players/queries";
|
||||||
|
import useAdminEnrollPlayer from "../hooks/use-admin-enroll-player";
|
||||||
|
import useAdminUnenrollPlayer from "../hooks/use-admin-unenroll-player";
|
||||||
|
|
||||||
|
interface EditEnrolledPlayersProps {
|
||||||
|
tournamentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlayerItemProps {
|
||||||
|
player: PlayerInfo;
|
||||||
|
onRemove: (playerId: string) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayerItem = memo(({ player, onRemove, disabled }: PlayerItemProps) => {
|
||||||
|
return (
|
||||||
|
<Group py="xs" px="sm" w="100%" gap="sm" align="center">
|
||||||
|
<PlayerAvatar
|
||||||
|
name={`${player.first_name} ${player.last_name}`}
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Text fw={500} truncate>
|
||||||
|
{player.first_name} {player.last_name}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
onClick={() => onRemove(player.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<TrashIcon size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
|
||||||
|
const { data: freeAgents = [], isLoading } = useFreeAgents(tournamentId);
|
||||||
|
const { data: allPlayers = [] } = usePlayers();
|
||||||
|
|
||||||
|
const { mutate: removeFreeAgent, isPending: isRemoving } = useAdminUnenrollPlayer();
|
||||||
|
const { mutate: enrollPlayer, isPending: isEnrolling } = useAdminEnrollPlayer();
|
||||||
|
|
||||||
|
const handleRemovePlayer = useCallback(
|
||||||
|
(playerId: string) => {
|
||||||
|
removeFreeAgent({ tournamentId, playerId });
|
||||||
|
},
|
||||||
|
[removeFreeAgent, tournamentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEnrollPlayer = useCallback(
|
||||||
|
(option: TypeaheadOption<Player>) => {
|
||||||
|
enrollPlayer({ tournamentId, playerId: option.data.id });
|
||||||
|
},
|
||||||
|
[enrollPlayer, tournamentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const enrolledPlayers = freeAgents.map(agent => agent.player).filter((p): p is PlayerInfo => p !== undefined);
|
||||||
|
const enrolledPlayerIds = new Set(enrolledPlayers.map(p => p.id));
|
||||||
|
const hasEnrolledPlayers = enrolledPlayers.length > 0;
|
||||||
|
|
||||||
|
const searchPlayers = async (query: string): Promise<TypeaheadOption<Player>[]> => {
|
||||||
|
if (!query.trim()) return [];
|
||||||
|
|
||||||
|
const filtered = allPlayers.filter((player: Player) => {
|
||||||
|
const fullName = `${player.first_name} ${player.last_name}`.toLowerCase();
|
||||||
|
return fullName.includes(query.toLowerCase()) && !enrolledPlayerIds.has(player.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered.map((player: Player) => ({
|
||||||
|
id: player.id,
|
||||||
|
data: player
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPlayerOption = (option: TypeaheadOption<Player>) => {
|
||||||
|
const player = option.data;
|
||||||
|
return (
|
||||||
|
<Group py="xs" px="sm" gap="sm" align="center">
|
||||||
|
<PlayerAvatar
|
||||||
|
name={`${player.first_name} ${player.last_name}`}
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
<Text fw={500} truncate>
|
||||||
|
{player.first_name} {player.last_name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPlayer = (option: TypeaheadOption<Player>) => {
|
||||||
|
return `${option.data.first_name} ${option.data.last_name}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="lg" w="100%">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
Add Player
|
||||||
|
</Text>
|
||||||
|
<Typeahead
|
||||||
|
placeholder="Search for players to enroll..."
|
||||||
|
onSelect={handleEnrollPlayer}
|
||||||
|
searchFn={searchPlayers}
|
||||||
|
renderOption={renderPlayerOption}
|
||||||
|
format={formatPlayer}
|
||||||
|
disabled={isEnrolling}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
Enrolled Players
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{enrolledPlayers.length} players
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Group justify="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
) : !hasEnrolledPlayers ? (
|
||||||
|
<Text size="sm" c="dimmed" ta="center" py="lg">
|
||||||
|
No players enrolled yet
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Stack gap="xs" w="100%">
|
||||||
|
{enrolledPlayers.map((player) => (
|
||||||
|
<PlayerItem
|
||||||
|
key={player.id}
|
||||||
|
player={player}
|
||||||
|
onRemove={handleRemovePlayer}
|
||||||
|
disabled={isRemoving}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditEnrolledPlayers;
|
||||||
@@ -9,11 +9,12 @@ import {
|
|||||||
TreeStructureIcon,
|
TreeStructureIcon,
|
||||||
UsersThreeIcon,
|
UsersThreeIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
|
ShuffleIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useSheet } from "@/hooks/use-sheet";
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
import EditEnrolledTeams from "./edit-enrolled-teams";
|
import EditEnrolledTeams from "./edit-enrolled-teams";
|
||||||
|
import EditEnrolledPlayers from "./edit-enrolled-players";
|
||||||
import ListLink from "@/components/list-link";
|
import ListLink from "@/components/list-link";
|
||||||
import { RichTextEditor } from "@/components/rich-text-editor";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import EditRules from "./edit-rules";
|
import EditRules from "./edit-rules";
|
||||||
|
|
||||||
@@ -61,11 +62,20 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
|||||||
Icon={UsersThreeIcon}
|
Icon={UsersThreeIcon}
|
||||||
onClick={openEditTeams}
|
onClick={openEditTeams}
|
||||||
/>
|
/>
|
||||||
<ListLink
|
{tournament.regional && (
|
||||||
label="Manage Team Songs/Logos"
|
<ListLink
|
||||||
Icon={UsersIcon}
|
label="Assign Partners"
|
||||||
to={`/admin/tournaments/${tournamentId}/teams`}
|
Icon={ShuffleIcon}
|
||||||
/>
|
to={`/admin/tournaments/${tournamentId}/assign-partners`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!tournament.regional && (
|
||||||
|
<ListLink
|
||||||
|
label="Manage Team Songs/Logos"
|
||||||
|
Icon={UsersIcon}
|
||||||
|
to={`/admin/tournaments/${tournamentId}/teams`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ListLink
|
<ListLink
|
||||||
label="Run Tournament"
|
label="Run Tournament"
|
||||||
Icon={TreeStructureIcon}
|
Icon={TreeStructureIcon}
|
||||||
@@ -102,11 +112,15 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
title="Edit Enrolled Teams"
|
title={tournament.regional === true ? "Manage Enrollments" : "Edit Enrolled Teams"}
|
||||||
opened={editTeamsOpened}
|
opened={editTeamsOpened}
|
||||||
onChange={closeEditTeams}
|
onChange={closeEditTeams}
|
||||||
>
|
>
|
||||||
<EditEnrolledTeams tournamentId={tournamentId} />
|
{tournament.regional === true ? (
|
||||||
|
<EditEnrolledPlayers tournamentId={tournamentId} />
|
||||||
|
) : (
|
||||||
|
<EditEnrolledTeams tournamentId={tournamentId} />
|
||||||
|
)}
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Card, Group, Stack, Text, Avatar } from "@mantine/core";
|
||||||
|
import { PlayerInfo } from "@/features/players/types";
|
||||||
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
|
|
||||||
|
interface TeamAssignment {
|
||||||
|
player1: PlayerInfo;
|
||||||
|
player2: PlayerInfo;
|
||||||
|
teamName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamAssignmentPreviewProps {
|
||||||
|
assignments: TeamAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamAssignmentPreview: React.FC<TeamAssignmentPreviewProps> = ({ assignments }) => {
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{assignments.map((assignment, index) => (
|
||||||
|
<Card key={index} withBorder radius="md" p="md">
|
||||||
|
<Group gap="md" align="center" wrap="nowrap">
|
||||||
|
<Text size="sm" fw={600} c="dimmed" w={40}>
|
||||||
|
#{index + 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group gap="sm" style={{ flex: 1 }} align="center">
|
||||||
|
<PlayerAvatar name={`${assignment.player1.first_name} ${assignment.player1.last_name}`} size={32} />
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{assignment.player1.first_name} {assignment.player1.last_name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="lg" c="dimmed">
|
||||||
|
&
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group gap="sm" style={{ flex: 1 }} align="center">
|
||||||
|
<PlayerAvatar name={`${assignment.player2.first_name} ${assignment.player2.last_name}`} size={32} />
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{assignment.player2.first_name} {assignment.player2.last_name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamAssignmentPreview;
|
||||||
@@ -5,13 +5,12 @@ import { useSheet } from "@/hooks/use-sheet";
|
|||||||
import { Stack, Text } from "@mantine/core";
|
import { Stack, Text } from "@mantine/core";
|
||||||
import useEnrollFreeAgent from "../../hooks/use-enroll-free-agent";
|
import useEnrollFreeAgent from "../../hooks/use-enroll-free-agent";
|
||||||
|
|
||||||
const EnrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
const EnrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string, isRegional?: boolean} ) => {
|
||||||
const { open, isOpen, toggle } = useSheet();
|
const { open, isOpen, toggle } = useSheet();
|
||||||
const { user, phone } = useAuth();
|
const { user, phone } = useAuth();
|
||||||
|
|
||||||
const { mutate: enrollFreeAgent, isPending: isEnrolling } = useEnrollFreeAgent();
|
const { mutate: enrollFreeAgent, isPending: isEnrolling } = useEnrollFreeAgent(isRegional);
|
||||||
const handleEnroll = () => {
|
const handleEnroll = () => {
|
||||||
console.log('enrolling...')
|
|
||||||
enrollFreeAgent({ playerId: user!.id, tournamentId, phone }, {
|
enrollFreeAgent({ playerId: user!.id, tournamentId, phone }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toggle();
|
toggle();
|
||||||
@@ -22,21 +21,31 @@ const EnrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="subtle" size="sm" onClick={open}>
|
<Button variant="subtle" size="sm" onClick={open}>
|
||||||
Enroll As Free Agent
|
{isRegional ? "Enroll" : "Enroll As Free Agent"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}>
|
<Sheet title={isRegional ? "Enrollment" : "Free Agent Enrollment"} opened={isOpen} onChange={toggle}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="md">
|
{isRegional ? (
|
||||||
Enrolling as a free agent adds you to a pool of players looking for teammates.
|
<>
|
||||||
</Text>
|
<Text size="md">
|
||||||
<Text size="sm" c='dimmed'>
|
Enroll in this regional tournament to be assigned a random partner.
|
||||||
Once enrolled, you can view other free agents and their phone number in order to coordinate teams and walkout songs.
|
</Text>
|
||||||
</Text>
|
</>
|
||||||
<Text size="xs" c="dimmed">
|
) : (
|
||||||
Important: Enrolling as a free agent does not guarantee a tournament spot. To secure a spot, one team member must register through the app and select a walkout song.
|
<>
|
||||||
</Text>
|
<Text size="md">
|
||||||
<Button onClick={handleEnroll}>Confirm</Button>
|
Enrolling as a free agent adds you to a pool of players looking for teammates.
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c='dimmed'>
|
||||||
|
Once enrolled, you can view other free agents and their phone number in order to coordinate teams and walkout songs.
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Important: Enrolling as a free agent does not guarantee a tournament spot. To secure a spot, one team member must register through the app and select a walkout song.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button onClick={handleEnroll} loading={isEnrolling}>Confirm</Button>
|
||||||
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Group, Stack, Text, Card, Badge, Box, ActionIcon } from "@mantine/core";
|
import { Group, Stack, Text, Card, Badge, Box, ActionIcon } from "@mantine/core";
|
||||||
import { UserIcon, PhoneIcon } from "@phosphor-icons/react";
|
import { PhoneIcon, CheckCircleIcon } from "@phosphor-icons/react";
|
||||||
import { useFreeAgents } from "../../queries";
|
import { useFreeAgents } from "../../queries";
|
||||||
import UnenrollFreeAgent from "./unenroll-free-agent";
|
import UnenrollFreeAgent from "./unenroll-free-agent";
|
||||||
import toast from "@/lib/sonner";
|
import toast from "@/lib/sonner";
|
||||||
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
import PlayerAvatar from "@/components/player-avatar";
|
||||||
|
|
||||||
const EnrolledFreeAgent: React.FC<{ tournamentId: string }> = ({
|
const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }> = ({
|
||||||
tournamentId
|
tournamentId,
|
||||||
|
isRegional
|
||||||
}) => {
|
}) => {
|
||||||
const { data: freeAgents } = useFreeAgents(tournamentId);
|
const { data: freeAgents } = useFreeAgents(tournamentId);
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const copyToClipboard = async (phone: string) => {
|
const copyToClipboard = async (phone: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -38,33 +42,66 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string }> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isRegional) {
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Card withBorder radius="md" p="md">
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Group gap="md" align="center">
|
||||||
|
<PlayerAvatar name={`${user?.first_name} ${user?.last_name}`} size={48} />
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{user?.first_name} {user?.last_name}
|
||||||
|
</Text>
|
||||||
|
<Group gap={4} align="center">
|
||||||
|
<CheckCircleIcon size={14} weight="fill" color="var(--mantine-color-green-6)" />
|
||||||
|
<Text size="xs" c="green" fw={500}>
|
||||||
|
Enrolled
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Text size="xs" c="dimmed" ta="center">
|
||||||
|
Partners will be randomly assigned when enrollment closes
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<UnenrollFreeAgent tournamentId={tournamentId} isRegional={isRegional} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between" align="center">
|
<Text size="sm" fw={600} c="green">
|
||||||
<Group gap="xs" align="center">
|
✓ Enrolled as Free Agent
|
||||||
<UserIcon size={16} />
|
</Text>
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
Enrolled as Free Agent
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
You're on the free agent list. Other free agents looking for teams:
|
Other players looking for teammates:
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{freeAgents.length > 1 ? (
|
{freeAgents.length > 1 ? (
|
||||||
<Card withBorder radius="md" p="sm">
|
<Stack gap="xs">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="xs" fw={500} c="dimmed">
|
||||||
|
Free Agents
|
||||||
|
</Text>
|
||||||
|
<Badge variant="light" size="xs" color="blue">
|
||||||
|
{freeAgents.length}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{freeAgents
|
{freeAgents
|
||||||
.filter(agent => agent.player)
|
.filter(agent => agent.player)
|
||||||
.map((agent) => (
|
.map((agent) => (
|
||||||
<Group key={agent.id} justify="space-between" align="center" wrap="nowrap">
|
<Group key={agent.id} justify="space-between" align="center" wrap="nowrap" p="xs" style={{ borderRadius: '8px', backgroundColor: 'var(--mantine-color-gray-0)' }}>
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
<Text size="sm" fw={500} truncate>
|
||||||
<Text size="sm" fw={500} truncate>
|
{agent.player?.first_name} {agent.player?.last_name}
|
||||||
{agent.player?.first_name} {agent.player?.last_name}
|
</Text>
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{agent.phone && (
|
{agent.phone && (
|
||||||
<Group gap={4} align="center" style={{ flexShrink: 0 }}>
|
<Group gap={4} align="center" style={{ flexShrink: 0 }}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -87,27 +124,15 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string }> = ({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{freeAgents.length > 1 && (
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
size="xs"
|
|
||||||
color="blue"
|
|
||||||
style={{ alignSelf: 'flex-start', marginTop: '4px' }}
|
|
||||||
>
|
|
||||||
{freeAgents.length} free agents total
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Card withBorder radius="md" p="sm">
|
<Text size="xs" c="dimmed" py="sm">
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
You're the first free agent!
|
||||||
You're the only free agent so far
|
</Text>
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
<UnenrollFreeAgent tournamentId={tournamentId} />
|
|
||||||
|
<UnenrollFreeAgent tournamentId={tournamentId} isRegional={isRegional} />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import ListButton from "@/components/list-button";
|
||||||
|
import Sheet from "@/components/sheet/sheet";
|
||||||
|
import { useSheet } from "@/hooks/use-sheet";
|
||||||
|
import { UserListIcon } from "@phosphor-icons/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useFreeAgents } from "../../queries";
|
||||||
|
import { Text } from "@mantine/core";
|
||||||
|
import PlayerList from "@/features/players/components/player-list";
|
||||||
|
import { Player } from "@/features/players/types";
|
||||||
|
|
||||||
|
interface EnrolledPlayersListButtonProps {
|
||||||
|
tournamentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnrolledPlayersListButton: React.FC<EnrolledPlayersListButtonProps> = ({ tournamentId }) => {
|
||||||
|
const { data: freeAgents } = useFreeAgents(tournamentId);
|
||||||
|
const count = useMemo(() => freeAgents.length, [freeAgents]);
|
||||||
|
const { open, isOpen, toggle } = useSheet();
|
||||||
|
|
||||||
|
const players = useMemo(() =>
|
||||||
|
freeAgents.map(agent => agent.player).filter((player): player is Player => player !== undefined),
|
||||||
|
[freeAgents]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ListButton
|
||||||
|
label={`View Enrolled Players (${count})`}
|
||||||
|
Icon={UserListIcon}
|
||||||
|
onClick={open}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Sheet title="Enrolled Players" opened={isOpen} onChange={toggle}>
|
||||||
|
{count === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed" ta="center" py="xl">
|
||||||
|
No players enrolled yet
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<PlayerList players={players} />
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnrolledPlayersListButton;
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Suspense, useCallback, useMemo } from "react";
|
import { Suspense, useMemo } from "react";
|
||||||
import { Tournament } from "../../types";
|
import { Tournament } from "../../types";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { Box, Button, Card, Divider, Group, Stack, Text, Title } from "@mantine/core";
|
import { Box, Card, Divider, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import Countdown from "@/components/countdown";
|
import Countdown from "@/components/countdown";
|
||||||
import ListLink from "@/components/list-link";
|
import ListLink from "@/components/list-link";
|
||||||
import ListButton from "@/components/list-button";
|
|
||||||
import { TreeStructureIcon, UsersIcon } from "@phosphor-icons/react";
|
import { TreeStructureIcon, UsersIcon } from "@phosphor-icons/react";
|
||||||
import EnrollTeam from "./enroll-team";
|
import EnrollTeam from "./enroll-team";
|
||||||
import EnrollFreeAgent from "./enroll-free-agent";
|
import EnrollFreeAgent from "./enroll-free-agent";
|
||||||
import TeamListButton from "./team-list-button";
|
import TeamListButton from "./team-list-button";
|
||||||
|
import EnrolledPlayersListButton from "./enrolled-players-list-button";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
import TeamCardSkeleton from "@/features/teams/components/team-card-skeleton";
|
import TeamCardSkeleton from "@/features/teams/components/team-card-skeleton";
|
||||||
import TeamCard from "@/features/teams/components/team-card";
|
import TeamCard from "@/features/teams/components/team-card";
|
||||||
@@ -80,12 +80,19 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
|||||||
|
|
||||||
{!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
|
{!isUserEnrolled && isEnrollmentOpen && !isFreeAgent && (
|
||||||
<>
|
<>
|
||||||
<EnrollTeam
|
{!tournament.regional && (
|
||||||
|
<>
|
||||||
|
<EnrollTeam
|
||||||
|
tournamentId={tournament.id}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
<Divider my={0} label="or" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<EnrollFreeAgent
|
||||||
tournamentId={tournament.id}
|
tournamentId={tournament.id}
|
||||||
onSubmit={handleSubmit}
|
isRegional={tournament.regional}
|
||||||
/>
|
/>
|
||||||
<Divider my={0} label="or" />
|
|
||||||
<EnrollFreeAgent tournamentId={tournament.id} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -107,7 +114,10 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
|||||||
|
|
||||||
{
|
{
|
||||||
isFreeAgent && isEnrollmentOpen && (
|
isFreeAgent && isEnrollmentOpen && (
|
||||||
<EnrolledFreeAgent tournamentId={tournament.id} />
|
<EnrolledFreeAgent
|
||||||
|
tournamentId={tournament.id}
|
||||||
|
isRegional={tournament.regional}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +140,11 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
|
|||||||
Icon={TreeStructureIcon}
|
Icon={TreeStructureIcon}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<TeamListButton teams={tournament.teams || []} />
|
{tournament.regional === true ? (
|
||||||
|
<EnrolledPlayersListButton tournamentId={tournament.id} />
|
||||||
|
) : (
|
||||||
|
<TeamListButton teams={tournament.teams || []} />
|
||||||
|
)}
|
||||||
<RulesListButton tournamentId={tournament.id} />
|
<RulesListButton tournamentId={tournament.id} />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { useSheet } from "@/hooks/use-sheet";
|
|||||||
import { Stack, Text } from "@mantine/core";
|
import { Stack, Text } from "@mantine/core";
|
||||||
import useUnenrollFreeAgent from "../../hooks/use-unenroll-free-agent";
|
import useUnenrollFreeAgent from "../../hooks/use-unenroll-free-agent";
|
||||||
|
|
||||||
const UnenrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
const UnenrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string, isRegional?: boolean} ) => {
|
||||||
const { open, isOpen, toggle } = useSheet();
|
const { open, isOpen, toggle } = useSheet();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { mutate: unenrollFreeAgent, isPending: isEnrolling } = useUnenrollFreeAgent();
|
const { mutate: unenrollFreeAgent, isPending: isEnrolling } = useUnenrollFreeAgent(isRegional);
|
||||||
const handleUnenroll = () => {
|
const handleUnenroll = () => {
|
||||||
unenrollFreeAgent({ playerId: user!.id, tournamentId }, {
|
unenrollFreeAgent({ playerId: user!.id, tournamentId }, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -20,17 +20,21 @@ const UnenrollFreeAgent = ({ tournamentId }: {tournamentId: string} ) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button variant="subtle" size="sm" onClick={open}>
|
<Button variant="subtle" size="sm" color="red" onClick={open}>
|
||||||
Unenroll
|
Unenroll
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Sheet title="Are you sure?" opened={isOpen} onChange={toggle}>
|
<Sheet title="Unenroll from tournament?" opened={isOpen} onChange={toggle}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text size="md">
|
<Text size="md">
|
||||||
This will remove you from the free agent list.
|
{isRegional
|
||||||
|
? "This will remove you from the tournament enrollment."
|
||||||
|
: "This will remove you from the free agent list."}
|
||||||
</Text>
|
</Text>
|
||||||
<Button onClick={handleUnenroll}>Confirm</Button>
|
<Button color="red" onClick={handleUnenroll} loading={isEnrolling}>
|
||||||
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
|
Confirm Unenrollment
|
||||||
|
</Button>
|
||||||
|
<Button variant="subtle" onClick={toggle}>Cancel</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</>
|
</>
|
||||||
|
|||||||
21
src/features/tournaments/hooks/use-admin-enroll-player.ts
Normal file
21
src/features/tournaments/hooks/use-admin-enroll-player.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||||
|
import { adminEnrollPlayer } from "@/features/tournaments/server";
|
||||||
|
import { tournamentKeys } from "../queries";
|
||||||
|
|
||||||
|
const useAdminEnrollPlayer = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useServerMutation({
|
||||||
|
mutationFn: (data: { tournamentId: string, playerId: string }) => {
|
||||||
|
return adminEnrollPlayer({ data });
|
||||||
|
},
|
||||||
|
onSuccess: (data, { tournamentId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.details(tournamentId) });
|
||||||
|
},
|
||||||
|
successMessage: "Player enrolled successfully",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAdminEnrollPlayer;
|
||||||
21
src/features/tournaments/hooks/use-admin-unenroll-player.ts
Normal file
21
src/features/tournaments/hooks/use-admin-unenroll-player.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||||
|
import { adminUnenrollPlayer } from "@/features/tournaments/server";
|
||||||
|
import { tournamentKeys } from "../queries";
|
||||||
|
|
||||||
|
const useAdminUnenrollPlayer = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useServerMutation({
|
||||||
|
mutationFn: (data: { tournamentId: string, playerId: string }) => {
|
||||||
|
return adminUnenrollPlayer({ data });
|
||||||
|
},
|
||||||
|
onSuccess: (data, { tournamentId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.details(tournamentId) });
|
||||||
|
},
|
||||||
|
successMessage: "Player removed successfully",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAdminUnenrollPlayer;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
||||||
|
import { confirmTeamAssignments } from "../server";
|
||||||
|
|
||||||
|
export default function useConfirmTeamAssignments() {
|
||||||
|
return useServerMutation({
|
||||||
|
mutationFn: confirmTeamAssignments,
|
||||||
|
successMessage: "Teams created successfully!",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
|||||||
import { enrollFreeAgent } from "@/features/tournaments/server";
|
import { enrollFreeAgent } from "@/features/tournaments/server";
|
||||||
import { tournamentKeys } from "../queries";
|
import { tournamentKeys } from "../queries";
|
||||||
|
|
||||||
const useEnrollFreeAgent = () => {
|
const useEnrollFreeAgent = (isRegional?: boolean) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useServerMutation({
|
return useServerMutation({
|
||||||
@@ -13,7 +13,7 @@ const useEnrollFreeAgent = () => {
|
|||||||
onSuccess: (data, { tournamentId }) => {
|
onSuccess: (data, { tournamentId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||||
},
|
},
|
||||||
successMessage: 'You\'ve been added as a free agent!',
|
successMessage: isRegional ? "You've enrolled in regionals!" : "You've been added as a free agent!",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
||||||
|
import { generateRandomTeams } from "../server";
|
||||||
|
|
||||||
|
export default function useGenerateRandomTeams() {
|
||||||
|
return useServerMutation({
|
||||||
|
mutationFn: generateRandomTeams,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
|||||||
import { unenrollFreeAgent } from "@/features/tournaments/server";
|
import { unenrollFreeAgent } from "@/features/tournaments/server";
|
||||||
import { tournamentKeys } from "../queries";
|
import { tournamentKeys } from "../queries";
|
||||||
|
|
||||||
const useUnenrollFreeAgent = () => {
|
const useUnenrollFreeAgent = (isRegional?: boolean) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useServerMutation({
|
return useServerMutation({
|
||||||
@@ -13,7 +13,7 @@ const useUnenrollFreeAgent = () => {
|
|||||||
onSuccess: (data, { tournamentId }) => {
|
onSuccess: (data, { tournamentId }) => {
|
||||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
queryClient.invalidateQueries({ queryKey: tournamentKeys.free_agents(tournamentId) });
|
||||||
},
|
},
|
||||||
successMessage: 'You\'ve been removed as a free agent.',
|
successMessage: isRegional ? "You've been removed from regionals!" : "You've been removed as a free agent.",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -129,3 +129,260 @@ export const unenrollFreeAgent = createServerFn()
|
|||||||
logger.info('Player unenrolled as free agent', { playerId: player.id });
|
logger.info('Player unenrolled as free agent', { playerId: player.id });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const generateRandomTeams = createServerFn()
|
||||||
|
.inputValidator(z.object({
|
||||||
|
tournamentId: z.string(),
|
||||||
|
seed: z.number().optional()
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
toServerResult(async () => {
|
||||||
|
const freeAgents = await pbAdmin.getFreeAgents(data.tournamentId);
|
||||||
|
|
||||||
|
if (freeAgents.length < 2) {
|
||||||
|
throw new Error("Need at least 2 players to create teams");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeAgents.length % 2 !== 0) {
|
||||||
|
throw new Error("Need an even number of players to create teams");
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerIds = freeAgents.map(fa => fa.player?.id).filter(Boolean) as string[];
|
||||||
|
|
||||||
|
const allTeams = await pbAdmin.getTeamsWithFilter(
|
||||||
|
playerIds.map(id => `players.id ?= "${id}"`).join(" || "),
|
||||||
|
"players,tournaments"
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidPairings = new Set<string>();
|
||||||
|
const mostRecentRegionalPartners = new Map<string, string>();
|
||||||
|
|
||||||
|
let mostRecentRegionalDate: Date | null = null;
|
||||||
|
|
||||||
|
for (const team of allTeams) {
|
||||||
|
const teamPlayers = (team.expand?.players || []) as any[];
|
||||||
|
if (teamPlayers.length !== 2) continue;
|
||||||
|
|
||||||
|
const [p1, p2] = teamPlayers.map((p: any) => p.id).sort();
|
||||||
|
const pairKey = `${p1}|${p2}`;
|
||||||
|
|
||||||
|
const teamTournaments = (team.expand?.tournaments || []) as any[];
|
||||||
|
const hasMainlineTournament = teamTournaments.some((t: any) => !t.regional);
|
||||||
|
|
||||||
|
if (hasMainlineTournament) {
|
||||||
|
invalidPairings.add(pairKey);
|
||||||
|
} else if (team.private && teamTournaments.length > 0) {
|
||||||
|
const regionalTournaments = teamTournaments.filter((t: any) => t.regional);
|
||||||
|
for (const tournament of regionalTournaments) {
|
||||||
|
const tournamentDate = new Date(tournament.start_time);
|
||||||
|
if (!mostRecentRegionalDate || tournamentDate > mostRecentRegionalDate) {
|
||||||
|
mostRecentRegionalDate = tournamentDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostRecentRegionalDate) {
|
||||||
|
for (const team of allTeams) {
|
||||||
|
if (!team.private) continue;
|
||||||
|
|
||||||
|
const teamPlayers = (team.expand?.players || []) as any[];
|
||||||
|
if (teamPlayers.length !== 2) continue;
|
||||||
|
|
||||||
|
const teamTournaments = (team.expand?.tournaments || []) as any[];
|
||||||
|
const regionalTournaments = teamTournaments.filter((t: any) => t.regional);
|
||||||
|
|
||||||
|
for (const tournament of regionalTournaments) {
|
||||||
|
const tournamentDate = new Date(tournament.start_time);
|
||||||
|
if (tournamentDate.getTime() === mostRecentRegionalDate.getTime()) {
|
||||||
|
const [p1Id, p2Id] = teamPlayers.map((p: any) => p.id);
|
||||||
|
mostRecentRegionalPartners.set(p1Id, p2Id);
|
||||||
|
mostRecentRegionalPartners.set(p2Id, p1Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canPairPlayers(p1Id: string, p2Id: string): boolean {
|
||||||
|
const pairKey = [p1Id, p2Id].sort().join('|');
|
||||||
|
if (invalidPairings.has(pairKey)) return false;
|
||||||
|
|
||||||
|
const p1LastPartner = mostRecentRegionalPartners.get(p1Id);
|
||||||
|
if (p1LastPartner === p2Id) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seed = data.seed || Math.floor(Math.random() * 1000000);
|
||||||
|
|
||||||
|
function seededRandom(s: number) {
|
||||||
|
const x = Math.sin(s++) * 10000;
|
||||||
|
return x - Math.floor(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSeed = seed;
|
||||||
|
const shuffled = [...freeAgents];
|
||||||
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(seededRandom(currentSeed++) * (i + 1));
|
||||||
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignments = [];
|
||||||
|
const paired = new Set<string>();
|
||||||
|
const MAX_ATTEMPTS = 1000;
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
while (paired.size < shuffled.length && attempts < MAX_ATTEMPTS) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
for (let i = 0; i < shuffled.length; i++) {
|
||||||
|
if (paired.has(shuffled[i].player!.id)) continue;
|
||||||
|
|
||||||
|
for (let j = i + 1; j < shuffled.length; j++) {
|
||||||
|
if (paired.has(shuffled[j].player!.id)) continue;
|
||||||
|
|
||||||
|
const player1 = shuffled[i].player!;
|
||||||
|
const player2 = shuffled[j].player!;
|
||||||
|
|
||||||
|
if (canPairPlayers(player1.id, player2.id)) {
|
||||||
|
const teamName = `${player1.first_name} And ${player2.first_name}`;
|
||||||
|
|
||||||
|
assignments.push({
|
||||||
|
player1,
|
||||||
|
player2,
|
||||||
|
teamName
|
||||||
|
});
|
||||||
|
|
||||||
|
paired.add(player1.id);
|
||||||
|
paired.add(player2.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paired.size < shuffled.length) {
|
||||||
|
currentSeed++;
|
||||||
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(seededRandom(currentSeed++) * (i + 1));
|
||||||
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
|
}
|
||||||
|
assignments.length = 0;
|
||||||
|
paired.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paired.size < shuffled.length) {
|
||||||
|
throw new Error("Unable to create valid pairings with current restrictions. Please manually adjust enrollments.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Generated random team assignments with restrictions', {
|
||||||
|
tournamentId: data.tournamentId,
|
||||||
|
teamCount: assignments.length,
|
||||||
|
seed,
|
||||||
|
attempts
|
||||||
|
});
|
||||||
|
|
||||||
|
return { assignments, seed };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const confirmTeamAssignments = createServerFn()
|
||||||
|
.inputValidator(z.object({
|
||||||
|
tournamentId: z.string(),
|
||||||
|
assignments: z.array(z.object({
|
||||||
|
player1Id: z.string(),
|
||||||
|
player2Id: z.string(),
|
||||||
|
teamName: z.string()
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
toServerResult(async () => {
|
||||||
|
const createdTeams = [];
|
||||||
|
let reusedCount = 0;
|
||||||
|
|
||||||
|
for (const assignment of data.assignments) {
|
||||||
|
const existingTeams = await pbAdmin.getTeamsWithFilter(
|
||||||
|
`private = true && players.id ?= "${assignment.player1Id}" && players.id ?= "${assignment.player2Id}"`,
|
||||||
|
"players,tournaments"
|
||||||
|
);
|
||||||
|
|
||||||
|
let teamToUse = null;
|
||||||
|
|
||||||
|
for (const team of existingTeams) {
|
||||||
|
const teamPlayers = (team.expand?.players || []) as any[];
|
||||||
|
|
||||||
|
if (teamPlayers.length !== 2) continue;
|
||||||
|
|
||||||
|
const playerIds = teamPlayers.map((p: any) => p.id).sort();
|
||||||
|
const assignmentIds = [assignment.player1Id, assignment.player2Id].sort();
|
||||||
|
if (playerIds[0] !== assignmentIds[0] || playerIds[1] !== assignmentIds[1]) continue;
|
||||||
|
|
||||||
|
const teamTournaments = (team.expand?.tournaments || []) as any[];
|
||||||
|
const hasMainlineTournament = teamTournaments.some((t: any) => !t.regional);
|
||||||
|
|
||||||
|
if (!hasMainlineTournament) {
|
||||||
|
teamToUse = team;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamToUse) {
|
||||||
|
await pbAdmin.enrollTeam(data.tournamentId, teamToUse.id);
|
||||||
|
createdTeams.push(teamToUse);
|
||||||
|
reusedCount++;
|
||||||
|
logger.info('Reusing existing regional team', { teamId: teamToUse.id, teamName: teamToUse.name });
|
||||||
|
} else {
|
||||||
|
const team = await pbAdmin.createTeam({
|
||||||
|
name: assignment.teamName,
|
||||||
|
players: [assignment.player1Id, assignment.player2Id],
|
||||||
|
private: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await pbAdmin.enrollTeam(data.tournamentId, team.id);
|
||||||
|
createdTeams.push(team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const assignment of data.assignments) {
|
||||||
|
await pbAdmin.unenrollFreeAgent(assignment.player1Id, data.tournamentId);
|
||||||
|
await pbAdmin.unenrollFreeAgent(assignment.player2Id, data.tournamentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Confirmed team assignments', {
|
||||||
|
tournamentId: data.tournamentId,
|
||||||
|
teamCount: createdTeams.length,
|
||||||
|
reusedCount,
|
||||||
|
newCount: createdTeams.length - reusedCount
|
||||||
|
});
|
||||||
|
|
||||||
|
return { teams: createdTeams };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const adminEnrollPlayer = createServerFn()
|
||||||
|
.inputValidator(z.object({
|
||||||
|
playerId: z.string(),
|
||||||
|
tournamentId: z.string()
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
toServerResult(async () => {
|
||||||
|
await pbAdmin.enrollFreeAgent(data.playerId, "", data.tournamentId);
|
||||||
|
logger.info('Admin enrolled player', { playerId: data.playerId, tournamentId: data.tournamentId });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const adminUnenrollPlayer = createServerFn()
|
||||||
|
.inputValidator(z.object({
|
||||||
|
playerId: z.string(),
|
||||||
|
tournamentId: z.string()
|
||||||
|
}))
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
|
||||||
|
.handler(async ({ data }) =>
|
||||||
|
toServerResult(async () => {
|
||||||
|
await pbAdmin.unenrollFreeAgent(data.playerId, data.tournamentId);
|
||||||
|
logger.info('Admin unenrolled player', { playerId: data.playerId, tournamentId: data.tournamentId });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,6 +2,27 @@ import { TeamInfo } from "@/features/teams/types";
|
|||||||
import { Match } from "@/features/matches/types";
|
import { Match } from "@/features/matches/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export type TournamentFormat = "double_elim" | "group_single_elim";
|
||||||
|
export type TournamentPhase = "seeding" | "group_stage" | "knockout" | "completed";
|
||||||
|
|
||||||
|
export interface GroupConfig {
|
||||||
|
num_groups: number;
|
||||||
|
teams_per_group: number;
|
||||||
|
advance_per_group: number;
|
||||||
|
matches_guaranteed: number;
|
||||||
|
seeding_method: "random" | "ranked";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
id: string;
|
||||||
|
tournament: string;
|
||||||
|
name: string;
|
||||||
|
order: number;
|
||||||
|
teams: TeamInfo[];
|
||||||
|
created: string;
|
||||||
|
updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TournamentTeamStats {
|
export interface TournamentTeamStats {
|
||||||
id: string;
|
id: string;
|
||||||
team_id: string;
|
team_id: string;
|
||||||
@@ -52,6 +73,10 @@ export interface Tournament {
|
|||||||
third_place?: TeamInfo;
|
third_place?: TeamInfo;
|
||||||
team_stats?: TournamentTeamStats[];
|
team_stats?: TournamentTeamStats[];
|
||||||
regional?: boolean;
|
regional?: boolean;
|
||||||
|
format?: TournamentFormat;
|
||||||
|
group_config?: GroupConfig;
|
||||||
|
phase?: TournamentPhase;
|
||||||
|
groups?: Group[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tournamentInputSchema = z.object({
|
export const tournamentInputSchema = z.object({
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { createMatchesService } from "./services/matches";
|
|||||||
import { createReactionsService } from "./services/reactions";
|
import { createReactionsService } from "./services/reactions";
|
||||||
import { createActivitiesService } from "./services/activities";
|
import { createActivitiesService } from "./services/activities";
|
||||||
import { createBadgesService } from "./services/badges";
|
import { createBadgesService } from "./services/badges";
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
class PocketBaseAdminClient {
|
class PocketBaseAdminClient {
|
||||||
private pb: PocketBase;
|
private pb: PocketBase;
|
||||||
|
|||||||
@@ -110,5 +110,14 @@ export function createTeamsService(pb: PocketBase) {
|
|||||||
|
|
||||||
return result.map((match) => transformMatch(match));
|
return result.map((match) => transformMatch(match));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getTeamsWithFilter(filter: string, expand?: string): Promise<any[]> {
|
||||||
|
logger.info("PocketBase | Getting teams with filter", { filter, expand });
|
||||||
|
const result = await pb.collection("teams").getFullList({
|
||||||
|
filter,
|
||||||
|
expand,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user