init matches, tournament runner
This commit is contained in:
247
pb_migrations/1756595097_created_matches.js
Normal file
247
pb_migrations/1756595097_created_matches.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/// <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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number4113142680",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "order",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1080860409",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "lid",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool1352515405",
|
||||||
|
"name": "reset",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number2650326517",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "home_cups",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number766636452",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "away_cups",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number3404566555",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "ot_count",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date1345189255",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "start_time",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date1096160257",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "end_time",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2000130356",
|
||||||
|
"name": "bye",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number3642169398",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "home_from_lid",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1662941821",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "away_from_lid",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool1093314320",
|
||||||
|
"name": "home_from_loser",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool4045114275",
|
||||||
|
"name": "away_from_loser",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool1628031220",
|
||||||
|
"name": "is_losers_bracket",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_340646327",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation3177167065",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "tournament",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1568971955",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation1909853392",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "home",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_1568971955",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2791285457",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "away",
|
||||||
|
"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_2541054544",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": null,
|
||||||
|
"name": "matches",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": null
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.save(collection);
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2541054544");
|
||||||
|
|
||||||
|
return app.delete(collection);
|
||||||
|
})
|
||||||
@@ -25,6 +25,7 @@ import { Route as AuthedProfilePlayerIdRouteImport } from './routes/_authed/prof
|
|||||||
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
import { Route as AuthedAdminPreviewRouteImport } from './routes/_authed/admin/preview'
|
||||||
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
import { Route as AuthedAdminTournamentsIndexRouteImport } from './routes/_authed/admin/tournaments/index'
|
||||||
import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id'
|
import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/admin/tournaments/$id'
|
||||||
|
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||||
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||||
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||||
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||||
@@ -104,6 +105,12 @@ const AuthedAdminTournamentsIdRoute =
|
|||||||
path: '/tournaments/$id',
|
path: '/tournaments/$id',
|
||||||
getParentRoute: () => AuthedAdminRoute,
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthedAdminTournamentsRunIdRoute =
|
||||||
|
AuthedAdminTournamentsRunIdRouteImport.update({
|
||||||
|
id: '/tournaments/run/$id',
|
||||||
|
path: '/tournaments/run/$id',
|
||||||
|
getParentRoute: () => AuthedAdminRoute,
|
||||||
|
} as any)
|
||||||
const ApiTournamentsUploadLogoServerRoute =
|
const ApiTournamentsUploadLogoServerRoute =
|
||||||
ApiTournamentsUploadLogoServerRouteImport.update({
|
ApiTournamentsUploadLogoServerRouteImport.update({
|
||||||
id: '/api/tournaments/upload-logo',
|
id: '/api/tournaments/upload-logo',
|
||||||
@@ -141,6 +148,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
@@ -155,6 +163,7 @@ export interface FileRoutesByTo {
|
|||||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||||
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
'/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
'/admin/tournaments': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -172,6 +181,7 @@ export interface FileRoutesById {
|
|||||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||||
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
'/_authed/admin/tournaments/$id': typeof AuthedAdminTournamentsIdRoute
|
||||||
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
'/_authed/admin/tournaments/': typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
'/_authed/admin/tournaments/run/$id': typeof AuthedAdminTournamentsRunIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -189,6 +199,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/admin/tournaments/$id'
|
| '/admin/tournaments/$id'
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
|
| '/admin/tournaments/run/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/login'
|
| '/login'
|
||||||
@@ -203,6 +214,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tournaments'
|
| '/tournaments'
|
||||||
| '/admin/tournaments/$id'
|
| '/admin/tournaments/$id'
|
||||||
| '/admin/tournaments'
|
| '/admin/tournaments'
|
||||||
|
| '/admin/tournaments/run/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/_authed'
|
| '/_authed'
|
||||||
@@ -219,6 +231,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authed/tournaments/'
|
| '/_authed/tournaments/'
|
||||||
| '/_authed/admin/tournaments/$id'
|
| '/_authed/admin/tournaments/$id'
|
||||||
| '/_authed/admin/tournaments/'
|
| '/_authed/admin/tournaments/'
|
||||||
|
| '/_authed/admin/tournaments/run/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -373,6 +386,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthedAdminTournamentsIdRouteImport
|
preLoaderRoute: typeof AuthedAdminTournamentsIdRouteImport
|
||||||
parentRoute: typeof AuthedAdminRoute
|
parentRoute: typeof AuthedAdminRoute
|
||||||
}
|
}
|
||||||
|
'/_authed/admin/tournaments/run/$id': {
|
||||||
|
id: '/_authed/admin/tournaments/run/$id'
|
||||||
|
path: '/tournaments/run/$id'
|
||||||
|
fullPath: '/admin/tournaments/run/$id'
|
||||||
|
preLoaderRoute: typeof AuthedAdminTournamentsRunIdRouteImport
|
||||||
|
parentRoute: typeof AuthedAdminRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
declare module '@tanstack/react-start/server' {
|
declare module '@tanstack/react-start/server' {
|
||||||
@@ -413,6 +433,7 @@ interface AuthedAdminRouteChildren {
|
|||||||
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
AuthedAdminIndexRoute: typeof AuthedAdminIndexRoute
|
||||||
AuthedAdminTournamentsIdRoute: typeof AuthedAdminTournamentsIdRoute
|
AuthedAdminTournamentsIdRoute: typeof AuthedAdminTournamentsIdRoute
|
||||||
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
AuthedAdminTournamentsIndexRoute: typeof AuthedAdminTournamentsIndexRoute
|
||||||
|
AuthedAdminTournamentsRunIdRoute: typeof AuthedAdminTournamentsRunIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
||||||
@@ -420,6 +441,7 @@ const AuthedAdminRouteChildren: AuthedAdminRouteChildren = {
|
|||||||
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
AuthedAdminIndexRoute: AuthedAdminIndexRoute,
|
||||||
AuthedAdminTournamentsIdRoute: AuthedAdminTournamentsIdRoute,
|
AuthedAdminTournamentsIdRoute: AuthedAdminTournamentsIdRoute,
|
||||||
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
AuthedAdminTournamentsIndexRoute: AuthedAdminTournamentsIndexRoute,
|
||||||
|
AuthedAdminTournamentsRunIdRoute: AuthedAdminTournamentsRunIdRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
const AuthedAdminRouteWithChildren = AuthedAdminRoute._addFileChildren(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function createRouter() {
|
|||||||
header: defaultHeaderConfig,
|
header: defaultHeaderConfig,
|
||||||
refresh: [],
|
refresh: [],
|
||||||
withPadding: true,
|
withPadding: true,
|
||||||
|
fullWidth: false,
|
||||||
},
|
},
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
defaultErrorComponent: DefaultCatchBoundary,
|
defaultErrorComponent: DefaultCatchBoundary,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const Route = createRootRouteWithContext<{
|
|||||||
header: HeaderConfig;
|
header: HeaderConfig;
|
||||||
refresh: string[];
|
refresh: string[];
|
||||||
withPadding: boolean;
|
withPadding: boolean;
|
||||||
|
fullWidth: boolean;
|
||||||
}>()({
|
}>()({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
|
|||||||
31
src/app/routes/_authed/admin/tournaments/run.$id.tsx
Normal file
31
src/app/routes/_authed/admin/tournaments/run.$id.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
|
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||||
|
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
|
||||||
|
import RunTournament from '@/features/tournaments/components/run-tournament'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
|
||||||
|
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,
|
||||||
|
header: {
|
||||||
|
withBackButton: true,
|
||||||
|
title: `Run ${context.tournament.name}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
return <RunTournament tournamentId={id} />
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ import useRouterConfig from "@/features/core/hooks/use-router-config";
|
|||||||
|
|
||||||
interface PageProps extends ContainerProps, React.PropsWithChildren {
|
interface PageProps extends ContainerProps, React.PropsWithChildren {
|
||||||
noPadding?: boolean;
|
noPadding?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = ({ children, noPadding, ...props }: PageProps) => {
|
const Page = ({ children, noPadding, fullWidth, ...props }: PageProps) => {
|
||||||
const { header } = useRouterConfig();
|
const { header } = useRouterConfig();
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
@@ -13,7 +14,7 @@ const Page = ({ children, noPadding, ...props }: PageProps) => {
|
|||||||
pt={header.collapsed ? 60 : 0}
|
pt={header.collapsed ? 60 : 0}
|
||||||
pb={20}
|
pb={20}
|
||||||
m={0}
|
m={0}
|
||||||
maw={600}
|
maw={fullWidth ? '100%' : 600}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
const { header } = useRouterConfig();
|
const { header } = useRouterConfig();
|
||||||
const viewport = useVisualViewportSize();
|
const viewport = useVisualViewportSize();
|
||||||
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
|
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
|
||||||
const { withPadding } = useRouterConfig();
|
const { withPadding, fullWidth } = useRouterConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
@@ -43,7 +43,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
style={{ transition: 'none' }}
|
style={{ transition: 'none' }}
|
||||||
>
|
>
|
||||||
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
|
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
|
||||||
<Page noPadding={!withPadding}>
|
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||||
{children}
|
{children}
|
||||||
</Page>
|
</Page>
|
||||||
</Pullable>
|
</Pullable>
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ const useRouterConfig = () => {
|
|||||||
return {
|
return {
|
||||||
header: headerConfig,
|
header: headerConfig,
|
||||||
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
|
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
|
||||||
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true
|
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true,
|
||||||
|
fullWidth: current && typeof current === 'object' && 'fullWidth' in current ? current.fullWidth : false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface Match {
|
|||||||
away_from_lid: number;
|
away_from_lid: number;
|
||||||
home_from_loser: boolean;
|
home_from_loser: boolean;
|
||||||
away_from_loser: boolean;
|
away_from_loser: boolean;
|
||||||
bracket_type: 'winners' | 'losers';
|
is_losers_bracket: 'winners' | 'losers';
|
||||||
tournament_id: string;
|
tournament_id: string;
|
||||||
home_id: string;
|
home_id: string;
|
||||||
away_id: string;
|
away_id: string;
|
||||||
@@ -39,7 +39,7 @@ export const matchInputSchema = z.object({
|
|||||||
away_from_lid: z.number().int().min(1).optional(),
|
away_from_lid: z.number().int().min(1).optional(),
|
||||||
home_from_loser: z.boolean().optional().default(false),
|
home_from_loser: z.boolean().optional().default(false),
|
||||||
away_from_loser: z.boolean().optional().default(false),
|
away_from_loser: z.boolean().optional().default(false),
|
||||||
losers_bracket: z.boolean().optional().default(false),
|
is_losers_bracket: z.boolean().optional().default(false),
|
||||||
tournament_id: z.string().min(1),
|
tournament_id: z.string().min(1),
|
||||||
home_id: z.string().min(1).optional(),
|
home_id: z.string().min(1).optional(),
|
||||||
away_id: z.string().min(1).optional(),
|
away_id: z.string().min(1).optional(),
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import TournamentForm from "./tournament-form";
|
|||||||
import {
|
import {
|
||||||
HardDrivesIcon,
|
HardDrivesIcon,
|
||||||
PencilLineIcon,
|
PencilLineIcon,
|
||||||
|
TreeStructureIcon,
|
||||||
UsersThreeIcon,
|
UsersThreeIcon,
|
||||||
} 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 ListLink from "@/components/list-link";
|
||||||
|
|
||||||
interface ManageTournamentProps {
|
interface ManageTournamentProps {
|
||||||
tournamentId: string;
|
tournamentId: string;
|
||||||
@@ -53,6 +55,11 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
|||||||
Icon={UsersThreeIcon}
|
Icon={UsersThreeIcon}
|
||||||
onClick={openEditTeams}
|
onClick={openEditTeams}
|
||||||
/>
|
/>
|
||||||
|
<ListLink
|
||||||
|
label="Run Tournament"
|
||||||
|
Icon={TreeStructureIcon}
|
||||||
|
to={`/admin/tournaments/run/${tournamentId}`}
|
||||||
|
/>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Sheet
|
<Sheet
|
||||||
|
|||||||
181
src/features/tournaments/components/run-tournament.tsx
Normal file
181
src/features/tournaments/components/run-tournament.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { useTournament } from '../queries'
|
||||||
|
import { Box, Grid, NumberInput, Stack, Text, Title, Group, Flex, Divider, ScrollArea, Button } from '@mantine/core'
|
||||||
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
|
import Avatar from '@/components/avatar'
|
||||||
|
import { useBracketPreview } from '@/features/bracket/queries'
|
||||||
|
import Bracket from '@/features/bracket/components/bracket'
|
||||||
|
import { createBracketMaps, BracketMaps } from '@/features/bracket/utils/bracket-maps'
|
||||||
|
import { Match, BracketData } from '@/features/bracket/types'
|
||||||
|
|
||||||
|
interface RunTournamentProps {
|
||||||
|
tournamentId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamWithSeed {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
seed: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunTournament = ({ tournamentId }: RunTournamentProps) => {
|
||||||
|
const { data: tournament } = useTournament(tournamentId)
|
||||||
|
const teamCount = tournament?.teams?.length || 0
|
||||||
|
const { data: bracketData, isLoading } = useBracketPreview(teamCount)
|
||||||
|
|
||||||
|
const [teamSeeds, setTeamSeeds] = useState<Record<string, number>>(() => {
|
||||||
|
if (!tournament?.teams) return {}
|
||||||
|
return tournament.teams.reduce((acc, team, index) => {
|
||||||
|
acc[team.id] = index + 1
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, number>)
|
||||||
|
})
|
||||||
|
|
||||||
|
const [seededWinnersBracket, setSeededWinnersBracket] = useState<Match[][]>([])
|
||||||
|
const [seededLosersBracket, setSeededLosersBracket] = useState<Match[][]>([])
|
||||||
|
const [bracketMaps, setBracketMaps] = useState<BracketMaps | null>(null)
|
||||||
|
|
||||||
|
const sortedTeams = useMemo(() => {
|
||||||
|
if (!tournament?.teams) return []
|
||||||
|
|
||||||
|
return tournament.teams
|
||||||
|
.map(team => ({
|
||||||
|
...team,
|
||||||
|
seed: teamSeeds[team.id] || 1,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.seed - b.seed)
|
||||||
|
}, [tournament?.teams, teamSeeds])
|
||||||
|
|
||||||
|
const handleSeedChange = (teamId: string, newSeed: number) => {
|
||||||
|
if (newSeed < 1 || !tournament?.teams) return
|
||||||
|
|
||||||
|
setTeamSeeds(prev => {
|
||||||
|
const currSeed = prev[teamId]
|
||||||
|
const newSeeds = { ...prev }
|
||||||
|
|
||||||
|
const otherTeams = tournament.teams!.filter(team => team.id !== teamId)
|
||||||
|
|
||||||
|
if (newSeed !== currSeed) {
|
||||||
|
const currTeam = otherTeams.find(team => prev[team.id] === newSeed)
|
||||||
|
|
||||||
|
if (currTeam) {
|
||||||
|
const affectedTeams = otherTeams.filter(team => {
|
||||||
|
const teamSeed = prev[team.id]
|
||||||
|
return newSeed < currSeed
|
||||||
|
? teamSeed >= newSeed && teamSeed < currSeed
|
||||||
|
: teamSeed > currSeed && teamSeed <= newSeed
|
||||||
|
})
|
||||||
|
|
||||||
|
affectedTeams.forEach(team => {
|
||||||
|
const teamSeed = prev[team.id]
|
||||||
|
if (newSeed < currSeed) {
|
||||||
|
newSeeds[team.id] = teamSeed + 1
|
||||||
|
} else {
|
||||||
|
newSeeds[team.id] = teamSeed - 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeeds[teamId] = newSeed
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSeeds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bracketData || !tournament?.teams || sortedTeams.length === 0) return
|
||||||
|
|
||||||
|
const maps = createBracketMaps(bracketData)
|
||||||
|
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 team = sortedTeams.find(t => t.seed === match.home.seed)
|
||||||
|
if (team) {
|
||||||
|
mappedMatch.home = {
|
||||||
|
...match.home,
|
||||||
|
team: team,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.away?.seed && match.away.seed > 0) {
|
||||||
|
const team = sortedTeams.find(t => t.seed === match.away.seed)
|
||||||
|
if (team) {
|
||||||
|
mappedMatch.away = {
|
||||||
|
...match.away,
|
||||||
|
team: team,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedMatch
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSeededWinnersBracket(mapBracket(bracketData.winners))
|
||||||
|
setSeededLosersBracket(mapBracket(bracketData.losers))
|
||||||
|
}, [bracketData, sortedTeams])
|
||||||
|
|
||||||
|
if (!tournament) throw new Error('Tournament not found.')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<ScrollArea offsetScrollbars type='always' h={700}>
|
||||||
|
<Title order={3}>Team Seeds</Title>
|
||||||
|
{sortedTeams.map((team) => (
|
||||||
|
<>
|
||||||
|
<Group
|
||||||
|
key={team.id}
|
||||||
|
justify="space-between"
|
||||||
|
p={4}
|
||||||
|
>
|
||||||
|
<Group gap="xs" style={{ flex: 1 }}>
|
||||||
|
<Avatar size={24} name={team.name} />
|
||||||
|
<Text fw={500} size="sm" truncate>{team.name}</Text>
|
||||||
|
</Group>
|
||||||
|
<NumberInput
|
||||||
|
value={teamSeeds[team.id]}
|
||||||
|
onChange={(value) => handleSeedChange(team.id, Number(value) || 1)}
|
||||||
|
min={1}
|
||||||
|
max={tournament.teams?.length || 1}
|
||||||
|
size="xs"
|
||||||
|
w={50}
|
||||||
|
styles={{ input: { textAlign: 'center' } }}
|
||||||
|
step={-1}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
<Button fullWidth mt='md' onClick={() => console.log(sortedTeams)}>Start Tournament</Button>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Title order={3}>Tournament Bracket</Title>
|
||||||
|
{isLoading ? (
|
||||||
|
<Flex justify="center" align="center" h="400px">
|
||||||
|
<Text c="dimmed">Loading bracket...</Text>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Bracket
|
||||||
|
winners={seededWinnersBracket}
|
||||||
|
losers={seededLosersBracket}
|
||||||
|
bracketMaps={bracketMaps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RunTournament
|
||||||
Reference in New Issue
Block a user