mirror of
https://github.com/evroon/bracket.git
synced 2026-04-20 07:16:59 -04:00
UX improvements (#722)
This commit is contained in:
@@ -61,7 +61,7 @@ async def delete_stage(
|
||||
detail="Stage contains stage items, please delete those first",
|
||||
)
|
||||
|
||||
if stage.is_active:
|
||||
if stage.is_active and len(stage.stage_items) > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Stage is active, please activate another stage first",
|
||||
|
||||
@@ -109,7 +109,14 @@ async def update_tournament_by_id(
|
||||
async def delete_tournament(
|
||||
tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament)
|
||||
) -> SuccessResponse:
|
||||
with check_foreign_key_violation({ForeignKey.stages_tournament_id_fkey}):
|
||||
with check_foreign_key_violation(
|
||||
{
|
||||
ForeignKey.stages_tournament_id_fkey,
|
||||
ForeignKey.teams_tournament_id_fkey,
|
||||
ForeignKey.players_tournament_id_fkey,
|
||||
ForeignKey.courts_tournament_id_fkey,
|
||||
}
|
||||
):
|
||||
await sql_delete_tournament(tournament_id)
|
||||
|
||||
return SuccessResponse()
|
||||
|
||||
@@ -16,12 +16,16 @@ class UniqueIndex(EnumAutoStr):
|
||||
|
||||
|
||||
class ForeignKey(EnumAutoStr):
|
||||
stages_tournament_id_fkey = auto()
|
||||
tournaments_club_id_fkey = auto()
|
||||
stage_item_inputs_team_id_fkey = auto()
|
||||
courts_tournament_id_fkey = auto()
|
||||
matches_team1_id_fkey = auto()
|
||||
matches_team2_id_fkey = auto()
|
||||
matches_team1_winner_from_stage_item_id_fkey = auto()
|
||||
matches_team2_id_fkey = auto()
|
||||
matches_team2_winner_from_stage_item_id_fkey = auto()
|
||||
players_tournament_id_fkey = auto()
|
||||
stage_item_inputs_team_id_fkey = auto()
|
||||
stages_tournament_id_fkey = auto()
|
||||
teams_tournament_id_fkey = auto()
|
||||
tournaments_club_id_fkey = auto()
|
||||
|
||||
|
||||
unique_index_violation_error_lookup = {
|
||||
@@ -31,13 +35,18 @@ unique_index_violation_error_lookup = {
|
||||
|
||||
|
||||
foreign_key_violation_error_lookup = {
|
||||
ForeignKey.stages_tournament_id_fkey: "This tournament still has stages, delete those first",
|
||||
ForeignKey.tournaments_club_id_fkey: "This club still has tournaments, delete those first",
|
||||
ForeignKey.stage_item_inputs_team_id_fkey: "This team is still used in stage items",
|
||||
ForeignKey.courts_tournament_id_fkey: "This tournament still has courts, delete those first",
|
||||
ForeignKey.matches_team1_id_fkey: "This team is still part of matches",
|
||||
ForeignKey.matches_team2_id_fkey: "This team is still part of matches",
|
||||
ForeignKey.matches_team1_winner_from_stage_item_id_fkey: "This stage item is referenced by "
|
||||
"other stage items",
|
||||
ForeignKey.matches_team2_id_fkey: "This team is still part of matches",
|
||||
ForeignKey.matches_team2_winner_from_stage_item_id_fkey: "This stage item is referenced by "
|
||||
"other stage items",
|
||||
ForeignKey.players_tournament_id_fkey: "This tournament still has players, delete those first",
|
||||
ForeignKey.stage_item_inputs_team_id_fkey: "This team is still used in stage items",
|
||||
ForeignKey.stages_tournament_id_fkey: "This tournament still has stages, delete those first",
|
||||
ForeignKey.teams_tournament_id_fkey: "This tournament still has teams, delete those first",
|
||||
ForeignKey.tournaments_club_id_fkey: "This club still has tournaments, delete those first",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ disable = [
|
||||
'unspecified-encoding',
|
||||
'unused-argument', # Gives false positives.
|
||||
'wrong-import-position',
|
||||
'contextmanager-generator-missing-cleanup', # Gives false positives.
|
||||
]
|
||||
|
||||
[tool.bandit]
|
||||
|
||||
@@ -72,6 +72,5 @@ async def reinit_database(event_loop: AbstractEventLoop, worker_id: str) -> Asyn
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def auth_context(reinit_database: Database) -> AsyncIterator[AuthContext]:
|
||||
async with reinit_database:
|
||||
async with inserted_auth_context() as auth_context:
|
||||
yield auth_context
|
||||
async with reinit_database, inserted_auth_context() as auth_context:
|
||||
yield auth_context
|
||||
|
||||
@@ -140,9 +140,11 @@
|
||||
"negative_match_margin_validation": "Match margin cannot be negative",
|
||||
"negative_score_validation": "Score cannot be negative",
|
||||
"next_matches_badge": "Next matches",
|
||||
"next_stage_button": "Go to Next Stage",
|
||||
"next_stage_button": "Next Stage",
|
||||
"no_matches_description": "First, add matches by creating stages and stage items. Then, schedule them using the button in the topright corner.",
|
||||
"no_matches_title": "No matches scheduled yet",
|
||||
"no_players_title": "No players yet",
|
||||
"no_teams_title": "No teams yet",
|
||||
"no_round_description": "There are no rounds in this stage item yet",
|
||||
"no_round_found_description": "Please wait for the organiser to add them.",
|
||||
"no_round_found_in_stage_description": "There are no rounds in this stage yet",
|
||||
@@ -168,7 +170,7 @@
|
||||
"players_spotlight_description": "View, add or delete players",
|
||||
"players_title": "players",
|
||||
"policy_not_accepted": "Please indicate that you have read the policy",
|
||||
"previous_stage_button": "Go to Previous Stage",
|
||||
"previous_stage_button": "Previous Stage",
|
||||
"recommended_badge_title": "Recommended",
|
||||
"remove_logo": "Remove logo",
|
||||
"remove_match_button": "Remove Match",
|
||||
|
||||
@@ -47,7 +47,7 @@ export function CreateStageButtonLarge({
|
||||
variant="outline"
|
||||
color="green"
|
||||
size="lg"
|
||||
style={{ marginRight: 10, width: '25%' }}
|
||||
style={{ marginRight: 10 }}
|
||||
onClick={async () => {
|
||||
await createStage(tournament.id);
|
||||
await swrStagesResponse.mutate();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Group, Modal, TextInput } from '@mantine/core';
|
||||
import { Button, Modal, 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';
|
||||
@@ -23,13 +23,13 @@ export default function ClubModal({
|
||||
const icon = is_create_form ? <GoPlus size={20} /> : <BiEditAlt size={20} />;
|
||||
const [opened, setOpened] = useState(false);
|
||||
const modalOpenButton = is_create_form ? (
|
||||
<Group justify="right">
|
||||
<SaveButton
|
||||
onClick={() => setOpened(true)}
|
||||
leftSection={<GoPlus size={24} />}
|
||||
title={operation_text}
|
||||
/>
|
||||
</Group>
|
||||
<SaveButton
|
||||
mx="0px"
|
||||
fullWidth
|
||||
onClick={() => setOpened(true)}
|
||||
leftSection={<GoPlus size={24} />}
|
||||
title={operation_text}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
color="green"
|
||||
|
||||
@@ -190,6 +190,7 @@ export default function TournamentModal({
|
||||
/>
|
||||
</Modal>
|
||||
<SaveButton
|
||||
mx="0px"
|
||||
fullWidth
|
||||
onClick={() => setOpened(true)}
|
||||
leftSection={<GoPlus size={24} />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, Table, Text } from '@mantine/core';
|
||||
import { Badge, Center, Pagination, Table, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
@@ -10,7 +10,7 @@ import DeleteButton from '../buttons/delete';
|
||||
import { PlayerScore } from '../info/player_score';
|
||||
import { WinDistribution } from '../info/player_statistics';
|
||||
import PlayerUpdateModal from '../modals/player_update_modal';
|
||||
import { EmptyTableInfo } from '../no_content/empty_table_info';
|
||||
import { NoContent } from '../no_content/empty_table_info';
|
||||
import { DateTime } from '../utils/datetime';
|
||||
import RequestErrorAlert from '../utils/error_alert';
|
||||
import { TableSkeletonSingleColumn } from '../utils/skeletons';
|
||||
@@ -39,10 +39,12 @@ export default function PlayersTable({
|
||||
swrPlayersResponse,
|
||||
tournamentData,
|
||||
tableState,
|
||||
playerCount,
|
||||
}: {
|
||||
swrPlayersResponse: SWRResponse;
|
||||
tournamentData: TournamentMinimal;
|
||||
tableState: TableState;
|
||||
playerCount: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const players: Player[] =
|
||||
@@ -111,36 +113,46 @@ export default function PlayersTable({
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
if (rows.length < 1) return <EmptyTableInfo entity_name={t('players_title')} />;
|
||||
if (rows.length < 1) return <NoContent title={t('no_players_title')} />;
|
||||
|
||||
return (
|
||||
<TableLayout miw={900}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<ThSortable state={tableState} field="active">
|
||||
{t('status')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="name">
|
||||
{t('title')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>
|
||||
<>
|
||||
<WinDistributionTitle />
|
||||
</>
|
||||
</ThNotSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</TableLayout>
|
||||
<>
|
||||
<TableLayout miw={900}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<ThSortable state={tableState} field="active">
|
||||
{t('status')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="name">
|
||||
{t('title')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>
|
||||
<>
|
||||
<WinDistributionTitle />
|
||||
</>
|
||||
</ThNotSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</TableLayout>
|
||||
<Center mt="1rem">
|
||||
<Pagination
|
||||
value={tableState.page}
|
||||
onChange={tableState.setPage}
|
||||
total={1 + playerCount / tableState.pageSize}
|
||||
size="lg"
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, Table } from '@mantine/core';
|
||||
import { Badge, Center, Pagination, Table } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
@@ -9,7 +9,7 @@ import { deleteTeam } from '../../services/team';
|
||||
import DeleteButton from '../buttons/delete';
|
||||
import PlayerList from '../info/player_list';
|
||||
import TeamUpdateModal from '../modals/team_update_modal';
|
||||
import { EmptyTableInfo } from '../no_content/empty_table_info';
|
||||
import { NoContent } from '../no_content/empty_table_info';
|
||||
import { DateTime } from '../utils/datetime';
|
||||
import RequestErrorAlert from '../utils/error_alert';
|
||||
import { TableSkeletonSingleColumn } from '../utils/skeletons';
|
||||
@@ -20,11 +20,13 @@ export default function TeamsTable({
|
||||
swrTeamsResponse,
|
||||
teams,
|
||||
tableState,
|
||||
teamCount,
|
||||
}: {
|
||||
tournamentData: TournamentMinimal;
|
||||
swrTeamsResponse: SWRResponse;
|
||||
teams: TeamInterface[];
|
||||
tableState: TableState;
|
||||
teamCount: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (swrTeamsResponse.error) return <RequestErrorAlert error={swrTeamsResponse.error} />;
|
||||
@@ -70,32 +72,43 @@ export default function TeamsTable({
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
if (rows.length < 1) return <EmptyTableInfo entity_name={t('teams_title')} />;
|
||||
if (rows.length < 1) return <NoContent title={t('no_teams_title')} />;
|
||||
|
||||
return (
|
||||
<TableLayout miw={850}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<ThSortable state={tableState} field="active">
|
||||
{t('status')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="name">
|
||||
{t('name_table_header')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{t('members_table_header')}</ThNotSortable>
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</TableLayout>
|
||||
<>
|
||||
<TableLayout miw={850}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<ThSortable state={tableState} field="active">
|
||||
{t('status')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="name">
|
||||
{t('name_table_header')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{t('members_table_header')}</ThNotSortable>
|
||||
<ThSortable state={tableState} field="created">
|
||||
{t('created')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="swiss_score">
|
||||
{t('swiss_score')}
|
||||
</ThSortable>
|
||||
<ThSortable state={tableState} field="elo_score">
|
||||
{t('elo_score')}
|
||||
</ThSortable>
|
||||
<ThNotSortable>{null}</ThNotSortable>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</TableLayout>
|
||||
|
||||
<Center mt="1rem">
|
||||
<Pagination
|
||||
value={tableState.page}
|
||||
onChange={tableState.setPage}
|
||||
total={1 + teamCount / tableState.pageSize}
|
||||
size="lg"
|
||||
/>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import ClubsTable from '../components/tables/clubs';
|
||||
import { capitalize } from '../components/utils/util';
|
||||
import { checkForAuthError, getClubs } from '../services/adapter';
|
||||
import Layout from './_layout';
|
||||
import classes from './index.module.css';
|
||||
|
||||
export default function HomePage() {
|
||||
const swrClubsResponse = getClubs();
|
||||
@@ -16,11 +17,11 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Grid grow>
|
||||
<Grid.Col span={9}>
|
||||
<Grid justify="space-between">
|
||||
<Grid.Col span="auto">
|
||||
<Title>{capitalize(t('clubs_title'))}</Title>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Grid.Col span="content" className={classes.fullWithMobile}>
|
||||
<ClubModal swrClubsResponse={swrClubsResponse} club={null} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Center, Grid, Pagination, Title } from '@mantine/core';
|
||||
import { Grid, Title } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import React from 'react';
|
||||
@@ -33,18 +33,11 @@ export default function Players() {
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<PlayersTable
|
||||
playerCount={playerCount}
|
||||
swrPlayersResponse={swrPlayersResponse}
|
||||
tournamentData={tournamentData}
|
||||
tableState={tableState}
|
||||
/>
|
||||
<Center mt="1rem">
|
||||
<Pagination
|
||||
value={tableState.page}
|
||||
onChange={tableState.setPage}
|
||||
total={1 + playerCount / tableState.pageSize}
|
||||
size="lg"
|
||||
/>
|
||||
</Center>
|
||||
</TournamentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { IconCalendar, IconCalendarTime } from '@tabler/icons-react';
|
||||
import assert from 'assert';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { SWRResponse } from 'swr';
|
||||
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
getBaseApiUrl,
|
||||
getClubs,
|
||||
getTournamentById,
|
||||
handleRequestError,
|
||||
removeTournamentLogo,
|
||||
} from '../../../services/adapter';
|
||||
import { deleteTournament, updateTournament } from '../../../services/tournament';
|
||||
@@ -53,6 +55,7 @@ function GeneralTournamentForm({
|
||||
swrTournamentResponse: SWRResponse;
|
||||
clubs: Club[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm({
|
||||
@@ -228,7 +231,11 @@ function GeneralTournamentForm({
|
||||
size="sm"
|
||||
leftSection={<MdDelete size={20} />}
|
||||
onClick={async () => {
|
||||
await deleteTournament(tournament.id);
|
||||
await deleteTournament(tournament.id)
|
||||
.then(async () => {
|
||||
await router.push('/');
|
||||
})
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
}}
|
||||
>
|
||||
{t('delete_tournament_button')}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Center, Grid, Pagination, Select, Title } from '@mantine/core';
|
||||
import { Grid, Select, Title } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import React, { useState } from 'react';
|
||||
@@ -95,15 +95,8 @@ export default function Teams() {
|
||||
tournamentData={tournamentData}
|
||||
teams={teams}
|
||||
tableState={tableState}
|
||||
teamCount={teamCount}
|
||||
/>
|
||||
<Center mt="1rem">
|
||||
<Pagination
|
||||
value={tableState.page}
|
||||
onChange={tableState.setPage}
|
||||
total={1 + teamCount / tableState.pageSize}
|
||||
size="lg"
|
||||
/>
|
||||
</Center>
|
||||
</TournamentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,7 @@ export async function createTournament(
|
||||
}
|
||||
|
||||
export async function deleteTournament(tournament_id: number) {
|
||||
return createAxios()
|
||||
.delete(`tournaments/${tournament_id}`)
|
||||
.catch((response: any) => handleRequestError(response));
|
||||
return createAxios().delete(`tournaments/${tournament_id}`);
|
||||
}
|
||||
|
||||
export async function updateTournament(
|
||||
|
||||
Reference in New Issue
Block a user