mirror of
https://github.com/evroon/bracket.git
synced 2026-01-28 16:11:23 -05:00
Add behavior to go to next stage (#265)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from enum import auto
|
||||
from typing import Literal
|
||||
|
||||
from heliclockter import datetime_utc
|
||||
|
||||
@@ -26,6 +27,10 @@ class StageUpdateBody(BaseModelORM):
|
||||
is_active: bool
|
||||
|
||||
|
||||
class StageActivateBody(BaseModelORM):
|
||||
direction: Literal['next', 'previous'] = 'next'
|
||||
|
||||
|
||||
class StageCreateBody(BaseModelORM):
|
||||
type: StageType
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from heliclockter import datetime_utc
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.court import Court, CourtBody, CourtToInsert
|
||||
@@ -8,6 +9,7 @@ from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import CourtsResponse, SingleCourtResponse, SuccessResponse
|
||||
from bracket.schema import courts
|
||||
from bracket.sql.courts import get_all_courts_in_tournament, update_court
|
||||
from bracket.sql.stages import get_stages_with_rounds_and_matches
|
||||
from bracket.utils.db import fetch_one_parsed
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
@@ -51,6 +53,20 @@ async def update_court_by_id(
|
||||
async def delete_court(
|
||||
tournament_id: int, court_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
) -> SuccessResponse:
|
||||
stages = await get_stages_with_rounds_and_matches(tournament_id, no_draft_rounds=False)
|
||||
used_in_matches_count = 0
|
||||
for stage in stages:
|
||||
for round_ in stage.rounds:
|
||||
for match in round_.matches:
|
||||
if match.court_id == court_id:
|
||||
used_in_matches_count += 1
|
||||
|
||||
if used_in_matches_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Could not delete court since it's used by {used_in_matches_count} matches",
|
||||
)
|
||||
|
||||
await database.execute(
|
||||
query=courts.delete().where(
|
||||
courts.c.id == court_id and courts.c.tournament_id == tournament_id
|
||||
|
||||
@@ -10,8 +10,10 @@ from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import user_authenticated_for_tournament
|
||||
from bracket.routes.models import SingleMatchResponse, SuccessResponse, UpcomingMatchesResponse
|
||||
from bracket.routes.util import match_dependency, round_dependency
|
||||
from bracket.sql.courts import get_all_free_courts_in_round
|
||||
from bracket.sql.matches import sql_create_match, sql_delete_match, sql_update_match
|
||||
from bracket.sql.stages import get_stages_with_rounds_and_matches
|
||||
from bracket.sql.tournaments import sql_get_tournament
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
@@ -75,7 +77,17 @@ async def create_match(
|
||||
match_body: MatchCreateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SingleMatchResponse:
|
||||
return SingleMatchResponse(data=await sql_create_match(match_body))
|
||||
tournament = await sql_get_tournament(tournament_id)
|
||||
next_free_court_id = None
|
||||
|
||||
if tournament.auto_assign_courts:
|
||||
free_courts = await get_all_free_courts_in_round(tournament_id, match_body.round_id)
|
||||
if len(free_courts) > 0:
|
||||
next_free_court_id = free_courts[0].id
|
||||
|
||||
match_body = match_body.copy(update={'court_id': next_free_court_id})
|
||||
match = await sql_create_match(match_body)
|
||||
return SingleMatchResponse(data=match)
|
||||
|
||||
|
||||
@router.patch("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
|
||||
|
||||
@@ -4,7 +4,7 @@ from starlette import status
|
||||
from bracket.database import database
|
||||
from bracket.logic.elo import recalculate_elo_for_tournament_id
|
||||
from bracket.models.db.round import StageWithRounds
|
||||
from bracket.models.db.stage import Stage, StageCreateBody, StageUpdateBody
|
||||
from bracket.models.db.stage import Stage, StageActivateBody, StageCreateBody, StageUpdateBody
|
||||
from bracket.models.db.user import UserPublic
|
||||
from bracket.routes.auth import (
|
||||
user_authenticated_for_tournament,
|
||||
@@ -13,7 +13,9 @@ from bracket.routes.auth import (
|
||||
from bracket.routes.models import RoundsWithMatchesResponse, SuccessResponse
|
||||
from bracket.routes.util import stage_dependency
|
||||
from bracket.sql.stages import (
|
||||
get_next_stage_in_tournament,
|
||||
get_stages_with_rounds_and_matches,
|
||||
sql_activate_next_stage,
|
||||
sql_create_stage,
|
||||
sql_delete_stage,
|
||||
)
|
||||
@@ -94,3 +96,20 @@ async def update_stage(
|
||||
values={**values, 'is_active': stage_body.is_active},
|
||||
)
|
||||
return SuccessResponse()
|
||||
|
||||
|
||||
@router.post("/tournaments/{tournament_id}/stages/activate", response_model=SuccessResponse)
|
||||
async def activate_next_stage(
|
||||
tournament_id: int,
|
||||
stage_body: StageActivateBody,
|
||||
_: UserPublic = Depends(user_authenticated_for_tournament),
|
||||
) -> SuccessResponse:
|
||||
new_active_stage_id = await get_next_stage_in_tournament(tournament_id, stage_body.direction)
|
||||
if new_active_stage_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="There is no next stage",
|
||||
)
|
||||
|
||||
await sql_activate_next_stage(new_active_stage_id, tournament_id)
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -12,6 +12,25 @@ async def get_all_courts_in_tournament(tournament_id: int) -> list[Court]:
|
||||
return [Court.parse_obj(x._mapping) for x in result]
|
||||
|
||||
|
||||
async def get_all_free_courts_in_round(tournament_id: int, round_id: int) -> list[Court]:
|
||||
query = '''
|
||||
SELECT *
|
||||
FROM courts
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM matches
|
||||
WHERE matches.court_id = courts.id
|
||||
AND matches.round_id = :round_id
|
||||
)
|
||||
AND courts.tournament_id = :tournament_id
|
||||
ORDER BY courts.name
|
||||
'''
|
||||
result = await database.fetch_all(
|
||||
query=query, values={'tournament_id': tournament_id, 'round_id': round_id}
|
||||
)
|
||||
return [Court.parse_obj(x._mapping) for x in result]
|
||||
|
||||
|
||||
async def update_court(tournament_id: int, court_id: int, court_body: CourtBody) -> list[Court]:
|
||||
query = '''
|
||||
UPDATE courts
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Literal, cast
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.round import StageWithRounds
|
||||
from bracket.models.db.stage import Stage, StageCreateBody
|
||||
@@ -88,3 +90,58 @@ async def sql_create_stage(stage: StageCreateBody, tournament_id: int) -> Stage:
|
||||
raise ValueError('Could not create stage')
|
||||
|
||||
return Stage.parse_obj(result._mapping)
|
||||
|
||||
|
||||
async def get_next_stage_in_tournament(
|
||||
tournament_id: int, direction: Literal['next', 'previous']
|
||||
) -> int | None:
|
||||
select_query = '''
|
||||
SELECT id
|
||||
FROM stages
|
||||
WHERE
|
||||
CASE WHEN :direction='next'
|
||||
THEN (
|
||||
id > COALESCE(
|
||||
(
|
||||
SELECT id FROM stages AS t
|
||||
WHERE is_active IS TRUE
|
||||
AND stages.tournament_id = :tournament_id
|
||||
ORDER BY id ASC
|
||||
),
|
||||
-1
|
||||
)
|
||||
)
|
||||
ELSE (
|
||||
id < COALESCE(
|
||||
(
|
||||
SELECT id FROM stages AS t
|
||||
WHERE is_active IS TRUE
|
||||
AND stages.tournament_id = :tournament_id
|
||||
ORDER BY id DESC
|
||||
),
|
||||
-1
|
||||
)
|
||||
)
|
||||
END
|
||||
AND stages.tournament_id = :tournament_id
|
||||
'''
|
||||
return cast(
|
||||
int,
|
||||
await database.execute(
|
||||
query=select_query,
|
||||
values={'tournament_id': tournament_id, 'direction': direction},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def sql_activate_next_stage(new_active_stage_id: int, tournament_id: int) -> None:
|
||||
update_query = '''
|
||||
UPDATE stages
|
||||
SET is_active = (stages.id = :new_active_stage_id)
|
||||
WHERE stages.tournament_id = :tournament_id
|
||||
|
||||
'''
|
||||
await database.execute(
|
||||
query=update_query,
|
||||
values={'tournament_id': tournament_id, 'new_active_stage_id': new_active_stage_id},
|
||||
)
|
||||
|
||||
10
backend/bracket/sql/tournaments.py
Normal file
10
backend/bracket/sql/tournaments.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.schema import tournaments
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
|
||||
|
||||
async def sql_get_tournament(tournament_id: int) -> Tournament:
|
||||
return await fetch_one_parsed_certain(
|
||||
database, Tournament, tournaments.select().where(tournaments.c.id == tournament_id)
|
||||
)
|
||||
@@ -206,7 +206,12 @@ async def sql_create_dev_db() -> None:
|
||||
)
|
||||
await insert_dummy(
|
||||
DUMMY_MATCH3.copy(
|
||||
update={'round_id': round_id_2, 'team1_id': team_id_2, 'team2_id': team_id_4}
|
||||
update={
|
||||
'round_id': round_id_2,
|
||||
'team1_id': team_id_2,
|
||||
'team2_id': team_id_4,
|
||||
'court_id': court_id_1,
|
||||
}
|
||||
),
|
||||
)
|
||||
await insert_dummy(
|
||||
|
||||
@@ -38,14 +38,14 @@ DUMMY_TOURNAMENT = Tournament(
|
||||
DUMMY_STAGE1 = Stage(
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_active=False,
|
||||
is_active=True,
|
||||
type=StageType.ROUND_ROBIN,
|
||||
)
|
||||
|
||||
DUMMY_STAGE2 = Stage(
|
||||
tournament_id=DB_PLACEHOLDER_ID,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_active=True,
|
||||
is_active=False,
|
||||
type=StageType.SWISS,
|
||||
)
|
||||
|
||||
@@ -59,15 +59,14 @@ DUMMY_ROUND1 = Round(
|
||||
DUMMY_ROUND2 = Round(
|
||||
stage_id=DB_PLACEHOLDER_ID,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_active=True,
|
||||
is_draft=False,
|
||||
is_draft=True,
|
||||
name='Round 2',
|
||||
)
|
||||
|
||||
DUMMY_ROUND3 = Round(
|
||||
stage_id=2,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_draft=True,
|
||||
is_draft=False,
|
||||
name='Round 3',
|
||||
)
|
||||
|
||||
@@ -98,7 +97,7 @@ DUMMY_MATCH3 = Match(
|
||||
team2_id=4,
|
||||
team1_score=23,
|
||||
team2_score=26,
|
||||
court_id=None,
|
||||
court_id=DB_PLACEHOLDER_ID,
|
||||
)
|
||||
|
||||
DUMMY_MATCH4 = Match(
|
||||
|
||||
@@ -3,7 +3,13 @@ import pytest
|
||||
from bracket.models.db.stage import StageType
|
||||
from bracket.schema import stages
|
||||
from bracket.sql.stages import get_stages_with_rounds_and_matches
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_ROUND1, DUMMY_STAGE1, DUMMY_TEAM1
|
||||
from bracket.utils.dummy_records import (
|
||||
DUMMY_MOCK_TIME,
|
||||
DUMMY_ROUND1,
|
||||
DUMMY_STAGE1,
|
||||
DUMMY_STAGE2,
|
||||
DUMMY_TEAM1,
|
||||
)
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from bracket.utils.types import assert_some
|
||||
from tests.integration_tests.api.shared import (
|
||||
@@ -47,7 +53,7 @@ async def test_stages_endpoint(
|
||||
'created': DUMMY_MOCK_TIME.isoformat(),
|
||||
'type': 'ROUND_ROBIN',
|
||||
'type_name': 'Round robin',
|
||||
'is_active': False,
|
||||
'is_active': True,
|
||||
'rounds': [
|
||||
{
|
||||
'id': round_inserted.id,
|
||||
@@ -88,7 +94,7 @@ async def test_delete_stage(
|
||||
async with (
|
||||
inserted_team(DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})),
|
||||
inserted_stage(
|
||||
DUMMY_STAGE1.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
DUMMY_STAGE2.copy(update={'tournament_id': auth_context.tournament.id})
|
||||
) as stage_inserted,
|
||||
):
|
||||
assert (
|
||||
@@ -123,3 +129,27 @@ async def test_update_stage(
|
||||
assert patched_stage.is_active == body['is_active']
|
||||
|
||||
await assert_row_count_and_clear(stages, 1)
|
||||
|
||||
|
||||
async def test_activate_stage(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {'type': StageType.ROUND_ROBIN.value, 'is_active': False}
|
||||
async with (
|
||||
inserted_team(DUMMY_TEAM1.copy(update={'tournament_id': auth_context.tournament.id})),
|
||||
inserted_stage(DUMMY_STAGE1.copy(update={'tournament_id': auth_context.tournament.id})),
|
||||
inserted_stage(DUMMY_STAGE2.copy(update={'tournament_id': auth_context.tournament.id})),
|
||||
):
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.POST, 'stages/activate?direction=next', auth_context, None, body
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
[prev_stage, next_stage] = await get_stages_with_rounds_and_matches(
|
||||
assert_some(auth_context.tournament.id)
|
||||
)
|
||||
assert prev_stage.is_active is False
|
||||
assert next_stage.is_active is True
|
||||
|
||||
await assert_row_count_and_clear(stages, 1)
|
||||
|
||||
@@ -10,14 +10,14 @@ import Round from './round';
|
||||
|
||||
function getRoundsGridCols(
|
||||
stages_map: { [p: string]: any },
|
||||
activeStageId: number,
|
||||
selectedStageId: number,
|
||||
tournamentData: TournamentMinimal,
|
||||
swrStagesResponse: SWRResponse,
|
||||
swrCourtsResponse: SWRResponse,
|
||||
swrUpcomingMatchesResponse: SWRResponse | null,
|
||||
readOnly: boolean
|
||||
) {
|
||||
const rounds = stages_map[activeStageId].rounds
|
||||
return stages_map[selectedStageId].rounds
|
||||
.sort((r1: any, r2: any) => (r1.name > r2.name ? 1 : 0))
|
||||
.map((round: RoundInterface) => (
|
||||
<Grid.Col sm={6} lg={4} xl={3} key={round.id}>
|
||||
@@ -31,7 +31,34 @@ function getRoundsGridCols(
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
return rounds;
|
||||
}
|
||||
|
||||
function NoRoundsAlert({ readOnly }: { readOnly: boolean }) {
|
||||
if (readOnly) {
|
||||
return (
|
||||
<Alert icon={<IconAlertCircle size={16} />} title="No rounds found" color="blue" radius="lg">
|
||||
Please wait for the organiser to add them.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert icon={<IconAlertCircle size={16} />} title="No rounds found" color="blue" radius="lg">
|
||||
Add a round using the top right button.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Brackets({
|
||||
@@ -40,57 +67,34 @@ export default function Brackets({
|
||||
swrCourtsResponse,
|
||||
swrUpcomingMatchesResponse,
|
||||
readOnly,
|
||||
activeStageId,
|
||||
selectedStageId,
|
||||
}: {
|
||||
tournamentData: TournamentMinimal;
|
||||
swrStagesResponse: SWRResponse;
|
||||
swrCourtsResponse: SWRResponse;
|
||||
swrUpcomingMatchesResponse: SWRResponse | null;
|
||||
readOnly: boolean;
|
||||
activeStageId: number | null;
|
||||
selectedStageId: number | null;
|
||||
}) {
|
||||
if (
|
||||
activeStageId == null ||
|
||||
selectedStageId == null ||
|
||||
(!swrStagesResponse.isLoading && !responseIsValid(swrStagesResponse))
|
||||
) {
|
||||
if (readOnly) {
|
||||
return (
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="No rounds found"
|
||||
color="blue"
|
||||
radius="lg"
|
||||
>
|
||||
Please wait for the organiser to add them.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert icon={<IconAlertCircle size={16} />} title="No rounds found" color="blue" radius="lg">
|
||||
Add a round using the top right button.
|
||||
</Alert>
|
||||
);
|
||||
return <NoRoundsAlert readOnly={readOnly} />;
|
||||
}
|
||||
|
||||
if (swrStagesResponse.isLoading) {
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={6} lg={4} xl={3}>
|
||||
<Skeleton height={500} mb="xl" radius="xl" />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
const stages_map = Object.fromEntries(
|
||||
swrStagesResponse.data.data.map((x: RoundInterface) => [x.id, x])
|
||||
);
|
||||
const rounds =
|
||||
stages_map[activeStageId].rounds.length > 0 ? (
|
||||
stages_map[selectedStageId].rounds.length > 0 ? (
|
||||
getRoundsGridCols(
|
||||
stages_map,
|
||||
activeStageId,
|
||||
selectedStageId,
|
||||
tournamentData,
|
||||
swrStagesResponse,
|
||||
swrCourtsResponse,
|
||||
|
||||
39
frontend/src/components/buttons/next_stage_button.tsx
Normal file
39
frontend/src/components/buttons/next_stage_button.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Button } from '@mantine/core';
|
||||
import { IconSquareArrowLeft, IconSquareArrowRight } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
|
||||
import { activateNextStage } from '../../services/stage';
|
||||
|
||||
export function NextStageButton({ tournamentData, swrStagesResponse }: any) {
|
||||
return (
|
||||
<Button
|
||||
size="md"
|
||||
style={{ marginBottom: 10 }}
|
||||
color="indigo"
|
||||
leftIcon={<IconSquareArrowRight size={24} />}
|
||||
onClick={async () => {
|
||||
await activateNextStage(tournamentData.id, 'next');
|
||||
swrStagesResponse.mutate();
|
||||
}}
|
||||
>
|
||||
Go to next stage
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviousStageButton({ tournamentData, swrStagesResponse }: any) {
|
||||
return (
|
||||
<Button
|
||||
size="md"
|
||||
style={{ marginBottom: 10 }}
|
||||
color="indigo"
|
||||
leftIcon={<IconSquareArrowLeft size={24} />}
|
||||
onClick={async () => {
|
||||
await activateNextStage(tournamentData.id, 'previous');
|
||||
swrStagesResponse.mutate();
|
||||
}}
|
||||
>
|
||||
Go to previous stage
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,6 @@ export default function CourtsTable({
|
||||
<thead>
|
||||
<tr>
|
||||
<ThNotSortable>Title</ThNotSortable>
|
||||
<ThNotSortable>Status</ThNotSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Badge } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
@@ -27,7 +28,14 @@ export default function StagesTable({
|
||||
.map((stage) => (
|
||||
<tr key={stage.type_name}>
|
||||
<td>{stage.type_name}</td>
|
||||
<td>{stage.status}</td>
|
||||
<td>
|
||||
{' '}
|
||||
{stage.is_active ? (
|
||||
<Badge color="green">Active</Badge>
|
||||
) : (
|
||||
<Badge color="dark">Inactive</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ function StyledTabs(props: TabsProps & { setSelectedStageId: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function StagesTab({ swrStagesResponse, activeStageId, setActiveStageId }: any) {
|
||||
export default function StagesTab({ swrStagesResponse, selectedStageId, setSelectedStageId }: any) {
|
||||
if (!responseIsValid(swrStagesResponse)) {
|
||||
return <></>;
|
||||
}
|
||||
@@ -80,8 +80,10 @@ export default function StagesTab({ swrStagesResponse, activeStageId, setActiveS
|
||||
</Tabs.Tab>
|
||||
));
|
||||
return (
|
||||
<StyledTabs value={activeStageId} setSelectedStageId={setActiveStageId}>
|
||||
<Tabs.List>{items}</Tabs.List>
|
||||
</StyledTabs>
|
||||
<>
|
||||
<StyledTabs value={selectedStageId} setSelectedStageId={setSelectedStageId}>
|
||||
<Tabs.List>{items}</Tabs.List>
|
||||
</StyledTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SWRResponse } from 'swr';
|
||||
|
||||
import NotFoundTitle from '../404';
|
||||
import Brackets from '../../components/brackets/brackets';
|
||||
import { NextStageButton } from '../../components/buttons/next_stage_button';
|
||||
import SaveButton from '../../components/buttons/save';
|
||||
import Scheduler from '../../components/scheduling/scheduling';
|
||||
import StagesTab from '../../components/utils/stages_tab';
|
||||
@@ -35,7 +36,7 @@ export default function TournamentPage() {
|
||||
const [eloThreshold, setEloThreshold] = useState(100);
|
||||
const [iterations, setIterations] = useState(200);
|
||||
const [limit, setLimit] = useState(50);
|
||||
const [activeStageId, setActiveStageId] = useState(null);
|
||||
const [selectedStageId, setSelectedStageId] = useState(null);
|
||||
|
||||
const schedulerSettings: SchedulerSettings = {
|
||||
eloThreshold,
|
||||
@@ -65,11 +66,11 @@ export default function TournamentPage() {
|
||||
[[draftRound]] = draftRounds;
|
||||
}
|
||||
|
||||
const activeTab = swrStagesResponse.data.data.filter(
|
||||
const selectedTab = swrStagesResponse.data.data.filter(
|
||||
(stage: RoundInterface) => stage.is_active
|
||||
);
|
||||
if (activeTab.length > 0 && activeStageId == null && activeTab[0].id != null) {
|
||||
setActiveStageId(activeTab[0].id.toString());
|
||||
if (selectedTab.length > 0 && selectedStageId == null && selectedTab[0].id != null) {
|
||||
setSelectedStageId(selectedTab[0].id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +118,14 @@ export default function TournamentPage() {
|
||||
>
|
||||
View dashboard
|
||||
</Button>
|
||||
{activeStageId == null ? null : (
|
||||
<NextStageButton
|
||||
tournamentData={tournamentData}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
/>
|
||||
{selectedStageId == null ? null : (
|
||||
<SaveButton
|
||||
onClick={async () => {
|
||||
await createRound(tournamentData.id, activeStageId);
|
||||
await createRound(tournamentData.id, selectedStageId);
|
||||
await swrStagesResponse.mutate();
|
||||
}}
|
||||
leftIcon={<GoPlus size={24} />}
|
||||
@@ -134,8 +139,8 @@ export default function TournamentPage() {
|
||||
<Center>
|
||||
<StagesTab
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
activeStageId={activeStageId}
|
||||
setActiveStageId={setActiveStageId}
|
||||
selectedStageId={selectedStageId}
|
||||
setSelectedStageId={setSelectedStageId}
|
||||
/>
|
||||
</Center>
|
||||
<Brackets
|
||||
@@ -144,7 +149,7 @@ export default function TournamentPage() {
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
|
||||
readOnly={false}
|
||||
activeStageId={activeStageId}
|
||||
selectedStageId={selectedStageId}
|
||||
/>
|
||||
{scheduler}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function Dashboard() {
|
||||
const swrCourtsResponse: SWRResponse = getCourts(tournamentData.id);
|
||||
|
||||
const swrTournamentsResponse = getTournament(tournamentData.id);
|
||||
const [activeStageId, setActiveStageId] = useState(null);
|
||||
const [selectedStageId, setSelectedStageId] = useState(null);
|
||||
|
||||
const tournamentDataFull: Tournament =
|
||||
swrTournamentsResponse.data != null ? swrTournamentsResponse.data.data : null;
|
||||
@@ -61,8 +61,8 @@ export default function Dashboard() {
|
||||
(stage: StageWithRounds) => stage.is_active
|
||||
);
|
||||
|
||||
if (activeTab.length > 0 && activeStageId == null && activeTab[0].id != null) {
|
||||
setActiveStageId(activeTab[0].id.toString());
|
||||
if (activeTab.length > 0 && selectedStageId == null && activeTab[0].id != null) {
|
||||
setSelectedStageId(activeTab[0].id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ export default function Dashboard() {
|
||||
<Grid.Col span={10}>
|
||||
<Center>
|
||||
<StagesTab
|
||||
activeStageId={activeStageId}
|
||||
selectedStageId={selectedStageId}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
setActiveStageId={setActiveStageId}
|
||||
setSelectedStageId={setSelectedStageId}
|
||||
/>
|
||||
</Center>
|
||||
<Brackets
|
||||
@@ -90,7 +90,7 @@ export default function Dashboard() {
|
||||
swrCourtsResponse={swrCourtsResponse}
|
||||
swrUpcomingMatchesResponse={null}
|
||||
readOnly
|
||||
activeStageId={activeStageId}
|
||||
selectedStageId={selectedStageId}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Button, Container, Divider, Select } from '@mantine/core';
|
||||
import { Button, Container, Divider, Group, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import {
|
||||
NextStageButton,
|
||||
PreviousStageButton,
|
||||
} from '../../../components/buttons/next_stage_button';
|
||||
import StagesTable from '../../../components/tables/stages';
|
||||
import { getTournamentIdFromRouter } from '../../../components/utils/util';
|
||||
import { Tournament } from '../../../interfaces/tournament';
|
||||
@@ -56,6 +61,13 @@ export default function StagesPage() {
|
||||
<Container>
|
||||
<StagesTable tournament={tournamentDataFull} swrStagesResponse={swrStagesResponse} />
|
||||
{CreateStageForm(tournamentDataFull, swrStagesResponse)}
|
||||
<Group grow mt="1rem">
|
||||
<PreviousStageButton
|
||||
tournamentData={tournamentData}
|
||||
swrStagesResponse={swrStagesResponse}
|
||||
/>
|
||||
<NextStageButton tournamentData={tournamentData} swrStagesResponse={swrStagesResponse} />
|
||||
</Group>
|
||||
</Container>
|
||||
</TournamentLayout>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,12 @@ export async function createStage(tournament_id: number, type: string) {
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function activateNextStage(tournament_id: number, direction: string) {
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/stages/activate`, { direction })
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteStage(tournament_id: number, stage_id: number) {
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/stages/${stage_id}`)
|
||||
|
||||
Reference in New Issue
Block a user