mirror of
https://github.com/evroon/bracket.git
synced 2026-03-06 08:08:37 -05:00
Add stages modal (#217)
This commit is contained in:
@@ -49,7 +49,7 @@ async def delete_stage(
|
||||
if len(stage.rounds) > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Round contains matches, delete those first",
|
||||
detail="Stage contains rounds, please delete those first",
|
||||
)
|
||||
|
||||
await sql_delete_stage(tournament_id, stage_id)
|
||||
|
||||
73
frontend/src/components/modals/stage_modal.tsx
Normal file
73
frontend/src/components/modals/stage_modal.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Button, Divider, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { BiTrophy } from '@react-icons/all-files/bi/BiTrophy';
|
||||
import { useState } from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { Tournament } from '../../interfaces/tournament';
|
||||
import { createStage } from '../../services/stage';
|
||||
import StagesTable from '../tables/stages';
|
||||
|
||||
function CreateStageForm(
|
||||
tournament: Tournament,
|
||||
swrClubsResponse: SWRResponse,
|
||||
setOpened: (value: ((prevState: boolean) => boolean) | boolean) => void
|
||||
) {
|
||||
const form = useForm({
|
||||
initialValues: { type: 'ROUND_ROBIN' },
|
||||
validate: {},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
await createStage(tournament.id, values.type);
|
||||
await swrClubsResponse.mutate(null);
|
||||
})}
|
||||
>
|
||||
<Divider mt={12} />
|
||||
<h5>Add Stage</h5>
|
||||
<Select
|
||||
label="Stage Type"
|
||||
data={[
|
||||
{ value: 'ROUND_ROBIN', label: 'Round Robin' },
|
||||
{ value: 'SINGLE_ELIMINATION', label: 'Single Elimination' },
|
||||
{ value: 'DOUBLE_ELIMINATION', label: 'Double Elimination' },
|
||||
]}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Button fullWidth style={{ marginTop: 10 }} color="green" type="submit">
|
||||
Create Stage
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StagesModal({
|
||||
tournament,
|
||||
swrStagesResponse,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
swrStagesResponse: SWRResponse;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title="Add or remove stages">
|
||||
<StagesTable tournament={tournament} swrStagesResponse={swrStagesResponse} />
|
||||
{CreateStageForm(tournament, swrStagesResponse, setOpened)}
|
||||
</Modal>
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
size="md"
|
||||
style={{ marginBottom: 10 }}
|
||||
onClick={() => setOpened(true)}
|
||||
leftIcon={<BiTrophy size={24} color="white" />}
|
||||
>
|
||||
Edit Stages
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Button, Checkbox, Group, Image, Modal, Select, TextInput } from '@mantine/core';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CopyButton,
|
||||
Group,
|
||||
Image,
|
||||
Modal,
|
||||
Select,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { BiEditAlt } from '@react-icons/all-files/bi/BiEditAlt';
|
||||
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
|
||||
@@ -12,12 +21,112 @@ import { getBaseApiUrl, getClubs } from '../../services/adapter';
|
||||
import { createTournament, updateTournament } from '../../services/tournament';
|
||||
import SaveButton from '../buttons/save';
|
||||
import { DropzoneButton } from '../utils/file_upload';
|
||||
import { getBaseURL } from '../utils/util';
|
||||
|
||||
export function TournamentLogo({ tournament }: { tournament: Tournament | null }) {
|
||||
if (tournament == null || tournament.logo_path == null) return null;
|
||||
return <Image radius="md" src={`${getBaseApiUrl()}/static/${tournament.logo_path}`} />;
|
||||
}
|
||||
|
||||
function GeneralTournamentForm({
|
||||
setOpened,
|
||||
is_create_form,
|
||||
tournament,
|
||||
swrTournamentsResponse,
|
||||
clubs,
|
||||
}: {
|
||||
setOpened: any;
|
||||
is_create_form: boolean;
|
||||
tournament: Tournament | null;
|
||||
swrTournamentsResponse: SWRResponse;
|
||||
clubs: Club[];
|
||||
}) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: tournament == null ? '' : tournament.name,
|
||||
club_id: tournament == null ? null : `${tournament.club_id}`,
|
||||
dashboard_public: tournament == null ? true : tournament.dashboard_public,
|
||||
players_can_be_in_multiple_teams:
|
||||
tournament == null ? true : tournament.players_can_be_in_multiple_teams,
|
||||
},
|
||||
|
||||
validate: {
|
||||
name: (value) => (value.length > 0 ? null : 'Name too short'),
|
||||
club_id: (value) => (value != null ? null : 'Please choose a club'),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
assert(values.club_id != null);
|
||||
if (is_create_form) {
|
||||
await createTournament(
|
||||
parseInt(values.club_id, 10),
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
);
|
||||
} else {
|
||||
assert(tournament != null);
|
||||
await updateTournament(
|
||||
tournament.id,
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
);
|
||||
}
|
||||
await swrTournamentsResponse.mutate(null);
|
||||
setOpened(false);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="Name"
|
||||
placeholder="Best Tournament Ever"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
data={clubs.map((p) => ({ value: `${p.id}`, label: p.name }))}
|
||||
label="Club"
|
||||
placeholder="Pick a club for this tournament"
|
||||
searchable
|
||||
limit={20}
|
||||
style={{ marginTop: 10 }}
|
||||
{...form.getInputProps('club_id')}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="md"
|
||||
label="Allow anyone to see the dashboard of rounds and matches"
|
||||
{...form.getInputProps('dashboard_public', { type: 'checkbox' })}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="md"
|
||||
label="Allow players to be in multiple teams"
|
||||
{...form.getInputProps('players_can_be_in_multiple_teams', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
{tournament != null ? <DropzoneButton tournament={tournament} /> : null}
|
||||
|
||||
<TournamentLogo tournament={tournament} />
|
||||
<Button fullWidth mt={8} color="green" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
|
||||
{tournament != null ? (
|
||||
<CopyButton value={`${getBaseURL()}/tournaments/${tournament.id}/dashboard`}>
|
||||
{({ copied, copy }) => (
|
||||
<Button fullWidth mt={8} color={copied ? 'teal' : 'blue'} onClick={copy}>
|
||||
{copied ? 'Copied dashboard URL' : 'Copy dashboard URL'}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TournamentModal({
|
||||
tournament,
|
||||
swrTournamentsResponse,
|
||||
@@ -53,82 +162,17 @@ export default function TournamentModal({
|
||||
const swrClubsResponse: SWRResponse = getClubs();
|
||||
const clubs: Club[] = swrClubsResponse.data != null ? swrClubsResponse.data.data : [];
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: tournament == null ? '' : tournament.name,
|
||||
club_id: tournament == null ? null : `${tournament.club_id}`,
|
||||
dashboard_public: tournament == null ? true : tournament.dashboard_public,
|
||||
players_can_be_in_multiple_teams:
|
||||
tournament == null ? true : tournament.players_can_be_in_multiple_teams,
|
||||
},
|
||||
|
||||
validate: {
|
||||
name: (value) => (value.length > 0 ? null : 'Name too short'),
|
||||
club_id: (value) => (value != null ? null : 'Please choose a club'),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title={operation_text}>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
assert(values.club_id != null);
|
||||
if (is_create_form) {
|
||||
await createTournament(
|
||||
parseInt(values.club_id, 10),
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
);
|
||||
} else {
|
||||
await updateTournament(
|
||||
tournament.id,
|
||||
values.name,
|
||||
values.dashboard_public,
|
||||
values.players_can_be_in_multiple_teams
|
||||
);
|
||||
}
|
||||
await swrTournamentsResponse.mutate(null);
|
||||
setOpened(false);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
label="Name"
|
||||
placeholder="Best Tournament Ever"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
data={clubs.map((p) => ({ value: `${p.id}`, label: p.name }))}
|
||||
label="Club"
|
||||
placeholder="Pick a club for this tournament"
|
||||
searchable
|
||||
limit={20}
|
||||
style={{ marginTop: 10 }}
|
||||
{...form.getInputProps('club_id')}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="md"
|
||||
label="Allow anyone to see the dashboard of rounds and matches"
|
||||
{...form.getInputProps('dashboard_public', { type: 'checkbox' })}
|
||||
/>
|
||||
<Checkbox
|
||||
mt="md"
|
||||
label="Allow players to be in multiple teams"
|
||||
{...form.getInputProps('players_can_be_in_multiple_teams', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
{tournament != null ? <DropzoneButton tournament={tournament} /> : null}
|
||||
|
||||
<TournamentLogo tournament={tournament} />
|
||||
<Button fullWidth style={{ marginTop: 10 }} color="green" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<GeneralTournamentForm
|
||||
setOpened={setOpened}
|
||||
is_create_form={is_create_form}
|
||||
tournament={tournament}
|
||||
swrTournamentsResponse={swrTournamentsResponse}
|
||||
clubs={clubs}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{modalOpenButton}
|
||||
</>
|
||||
);
|
||||
|
||||
54
frontend/src/components/tables/stages.tsx
Normal file
54
frontend/src/components/tables/stages.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
import { StageInterface } from '../../interfaces/stage';
|
||||
import { Tournament } from '../../interfaces/tournament';
|
||||
import { deleteStage } from '../../services/stage';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import RequestErrorAlert from '../utils/error_alert';
|
||||
import TableLayout, { ThNotSortable, getTableState, sortTableEntries } from './table';
|
||||
|
||||
export default function StagesTable({
|
||||
tournament,
|
||||
swrStagesResponse,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
swrStagesResponse: SWRResponse;
|
||||
}) {
|
||||
const stages: StageInterface[] =
|
||||
swrStagesResponse.data != null ? swrStagesResponse.data.data : [];
|
||||
const tableState = getTableState('id');
|
||||
|
||||
if (swrStagesResponse.error) return <RequestErrorAlert error={swrStagesResponse.error} />;
|
||||
|
||||
const rows = stages
|
||||
.sort((s1: StageInterface, s2: StageInterface) => sortTableEntries(s1, s2, tableState))
|
||||
.map((stage) => (
|
||||
<tr key={stage.type_name}>
|
||||
<td>{stage.type_name}</td>
|
||||
<td>{stage.status}</td>
|
||||
<td>
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
await deleteStage(tournament.id, stage.id);
|
||||
await swrStagesResponse.mutate(null);
|
||||
}}
|
||||
title="Delete Stage"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<TableLayout>
|
||||
<thead>
|
||||
<tr>
|
||||
<ThNotSortable>Title</ThNotSortable>
|
||||
<ThNotSortable>Status</ThNotSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</TableLayout>
|
||||
);
|
||||
}
|
||||
@@ -116,13 +116,7 @@ export default function TableLayout({ children }: any) {
|
||||
return (
|
||||
<>
|
||||
<ScrollArea>
|
||||
<Table
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="xs"
|
||||
striped
|
||||
highlightOnHover
|
||||
sx={{ minWidth: 700 }}
|
||||
>
|
||||
<Table horizontalSpacing="md" verticalSpacing="xs" striped highlightOnHover>
|
||||
{children}
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -47,3 +47,7 @@ export function getTournamentIdFromRouter() {
|
||||
export function responseIsValid(response: SWRResponse) {
|
||||
return response.data != null && response.data.data != null;
|
||||
}
|
||||
|
||||
export function getBaseURL() {
|
||||
return typeof window !== 'undefined' && window.location.origin ? window.location.origin : '';
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ export interface StageInterface {
|
||||
created: string;
|
||||
type: string;
|
||||
type_name: string;
|
||||
name: string;
|
||||
status: string;
|
||||
is_active: boolean;
|
||||
rounds: StageInterface[];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SWRResponse } from 'swr';
|
||||
import NotFoundTitle from '../404';
|
||||
import Brackets from '../../components/brackets/brackets';
|
||||
import SaveButton from '../../components/buttons/save';
|
||||
import StagesModal from '../../components/modals/stage_modal';
|
||||
import TournamentModal from '../../components/modals/tournament_modal';
|
||||
import Scheduler from '../../components/scheduling/scheduler';
|
||||
import StagesTab from '../../components/utils/stages_tab';
|
||||
@@ -94,6 +95,11 @@ export default function TournamentPage() {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const stagesModal =
|
||||
tournamentData != null ? (
|
||||
<StagesModal tournament={tournamentDataFull} swrStagesResponse={swrStagesResponse} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<TournamentLayout tournament_id={tournamentData.id}>
|
||||
<Grid grow>
|
||||
@@ -114,6 +120,7 @@ export default function TournamentPage() {
|
||||
View dashboard
|
||||
</Button>
|
||||
{tournamentModal}
|
||||
{stagesModal}
|
||||
<SaveButton
|
||||
onClick={async () => {
|
||||
await createRound(tournamentData.id);
|
||||
|
||||
13
frontend/src/services/stage.tsx
Normal file
13
frontend/src/services/stage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createAxios, handleRequestError } from './adapter';
|
||||
|
||||
export async function createStage(tournament_id: number, type: string) {
|
||||
return createAxios()
|
||||
.post(`tournaments/${tournament_id}/stages`, { type })
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
|
||||
export async function deleteStage(tournament_id: number, stage_id: number) {
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}/stages/${stage_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}
|
||||
Reference in New Issue
Block a user