Add stages modal (#217)

This commit is contained in:
Erik Vroon
2023-05-19 15:44:14 +02:00
committed by GitHub
parent 50d868358a
commit e4c8d716f0
9 changed files with 270 additions and 82 deletions

View File

@@ -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)

View 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>
</>
);
}

View File

@@ -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}
</>
);

View 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>
);
}

View File

@@ -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>

View File

@@ -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 : '';
}

View File

@@ -4,7 +4,6 @@ export interface StageInterface {
created: string;
type: string;
type_name: string;
name: string;
status: string;
is_active: boolean;
rounds: StageInterface[];

View File

@@ -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);

View 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));
}