Improve UX of swiss items (#981)

This commit is contained in:
Erik Vroon
2024-11-04 17:55:27 +01:00
committed by GitHub
parent 0fec0d9341
commit b00a0f3819
21 changed files with 383 additions and 334 deletions

View File

@@ -34,6 +34,7 @@
"at_least_two_team_validation": "Need at least two teams",
"auto_assign_courts_label": "Automatically assign courts to matches",
"auto_create_matches_button": "Add new matches automatically",
"courts_filled_badge": "courts filled",
"back_home_nav": "Take me back to home page",
"back_to_login_nav": "Back to login page",
"checkbox_status_checked": "Checked",
@@ -45,7 +46,9 @@
"clubs_spotlight_description": "View, add or delete clubs",
"clubs_title": "clubs",
"copied_dashboard_url_button": "Copied Dashboard URL",
"copied_url_button": "Copied URL",
"copy_dashboard_url_button": "Copy Dashboard URL",
"copy_url_button": "Copy URL",
"could_not_find_any_alert": "Could not find any",
"court_name_input_placeholder": "Best Court Ever",
"courts_title": "courts",
@@ -145,8 +148,6 @@
"multiple_teams_input_label": "Add multiple teams. Put every team on a separate line",
"multiple_teams_input_placeholder": "Team 1",
"name_field_text": "name",
"name_filter_options_player": "Player names",
"name_filter_options_team": "Team names",
"name_input_label": "Name",
"name_input_placeholder": "Your name",
"name_table_header": "Name",
@@ -156,7 +157,9 @@
"next_matches_badge": "Next matches",
"next_stage_button": "Next Stage",
"no_courts_description": "No courts have been created yet. First, create the tournament structure by adding stages and stage items. Then, create courts here and schedule matches on these courts.",
"no_courts_description_swiss": "No courts have been created yet. First add courts before managing a Swiss stage item.",
"no_courts_title": "No courts yet",
"go_to_courts_page": "Go to courts page",
"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",
@@ -244,6 +247,8 @@
"tournament_title": "tournament",
"tournaments_title": "tournaments",
"upcoming_matches_empty_table_info": "upcoming matches",
"no_more_matches_title": "No more matches to schedule",
"all_matches_scheduled_description": "Matches have been scheduled on all courts in this round. Add a new round or add a new court for more matches.",
"upload_placeholder_team": "Drop a file here to upload as team logo.",
"upload_placeholder_tournament": "Drop a file here to upload as tournament logo.",
"uppercase_required": "Includes uppercase letter",

View File

@@ -1,30 +1,97 @@
import { Alert, Button, Container, Grid, Group, Skeleton } from '@mantine/core';
import {
Alert,
Button,
Center,
Container,
Divider,
Grid,
Group,
Progress,
Skeleton,
Stack,
Switch,
} from '@mantine/core';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { MdOutlineAutoFixHigh } from 'react-icons/md';
import { SWRResponse } from 'swr';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import { SchedulerSettings } from '../../interfaces/match';
import { RoundInterface } from '../../interfaces/round';
import { StageItemWithRounds, stageItemIsHandledAutomatically } from '../../interfaces/stage_item';
import { TournamentMinimal } from '../../interfaces/tournament';
import { Tournament, TournamentMinimal } from '../../interfaces/tournament';
import { createRound } from '../../services/round';
import { AutoCreateMatchesButton } from '../buttons/create_matches_auto';
import ActivateNextRoundModal from '../modals/activate_next_round_modal';
import { NoContent } from '../no_content/empty_table_info';
import { Translator } from '../utils/types';
import { responseIsValid } from '../utils/util';
import Round from './round';
function getRoundsGridCols(
t: Translator,
stageItem: StageItemWithRounds,
tournamentData: TournamentMinimal,
swrStagesResponse: SWRResponse,
swrUpcomingMatchesResponse: SWRResponse | null,
readOnly: boolean,
displaySettings: BracketDisplaySettings
) {
let rounds: React.JSX.Element[] | React.JSX.Element = stageItem.rounds
function AddRoundButton({
t,
tournamentData,
stageItem,
swrStagesResponse,
size,
}: {
t: Translator;
tournamentData: TournamentMinimal;
stageItem: StageItemWithRounds;
swrStagesResponse: SWRResponse;
size: 'md' | 'lg';
}) {
return (
<Button
color="green"
size={size}
leftSection={<GoPlus size={24} />}
variant="outline"
onClick={async () => {
await createRound(tournamentData.id, stageItem.id);
await swrStagesResponse.mutate();
}}
>
{t('add_round_button')}
</Button>
);
}
export function RoundsGridCols({
stageItem,
tournamentData,
swrStagesResponse,
swrCourtsResponse,
swrUpcomingMatchesResponse,
schedulerSettings,
readOnly,
displaySettings,
draftRound,
}: {
stageItem: StageItemWithRounds;
tournamentData: Tournament;
swrStagesResponse: SWRResponse;
swrCourtsResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse;
schedulerSettings: SchedulerSettings;
readOnly: boolean;
displaySettings: BracketDisplaySettings;
draftRound: RoundInterface;
}) {
const { t } = useTranslation();
if (swrStagesResponse.isLoading) {
return <LoadingSkeleton />;
}
if (!responseIsValid(swrStagesResponse)) {
return <NoRoundsAlert readOnly={readOnly} />;
}
const items = stageItem.rounds
.sort((r1: any, r2: any) => (r1.name > r2.name ? 1 : -1))
.map((round: RoundInterface) => (
<Round
@@ -39,42 +106,92 @@ function getRoundsGridCols(
/>
));
if (rounds.length < 1) {
rounds = (
<Alert
icon={<IconAlertCircle size={16} />}
title={t('no_round_title')}
color="blue"
radius="lg"
>
{t('no_round_description')}
</Alert>
let result: React.JSX.Element[] | React.JSX.Element = items;
if (result.length < 1) {
result = (
<Container mt="1rem">
<Stack align="center">
<NoContent title={t('no_round_description')} />
<AddRoundButton
t={t}
tournamentData={tournamentData}
stageItem={stageItem}
swrStagesResponse={swrStagesResponse}
size="lg"
/>
</Stack>
</Container>
);
}
const hideAddRoundButton =
tournamentData == null || readOnly || stageItemIsHandledAutomatically(stageItem);
const courtsCount = swrCourtsResponse.data?.data?.length || 0;
const scheduledMatchesCount = draftRound?.matches.length;
return (
<React.Fragment key={stageItem.id}>
<div style={{ width: '100%' }}>
<Grid grow>
<Grid.Col span={6}></Grid.Col>
<Grid.Col span={6} mb="2rem">
<Group>
<Center>
{scheduledMatchesCount == null ? null : (
<>
<Stack gap="6px">
<>
{scheduledMatchesCount} / {courtsCount} {t('courts_filled_badge')}
</>
<Progress
value={(scheduledMatchesCount * 100) / courtsCount}
miw="12rem"
striped
color="indigo"
/>
</Stack>
<Divider orientation="vertical" mx="1rem" />
</>
)}
<Switch
size="md"
onLabel={<MdOutlineAutoFixHigh size={16} />}
offLabel={<IoOptions size={16} />}
checked={displaySettings.showManualSchedulingOptions === 'false'}
label={
displaySettings.showManualSchedulingOptions === 'true' ? 'Manual' : 'Automatic'
}
color="indigo"
onChange={(event) => {
displaySettings.setShowManualSchedulingOptions(
event.currentTarget.checked ? 'false' : 'true'
);
}}
miw="9rem"
/>
<Divider orientation="vertical" mx="1rem" />
<AutoCreateMatchesButton
swrStagesResponse={swrStagesResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
tournamentData={tournamentData}
stageItemId={stageItem.id}
schedulerSettings={schedulerSettings}
displaySettings={displaySettings}
/>
</Center>
</Group>
</Grid.Col>
<Grid.Col span={6}>
<Group justify="right">
{hideAddRoundButton ? null : (
<Button
color="green"
<AddRoundButton
t={t}
tournamentData={tournamentData}
stageItem={stageItem}
swrStagesResponse={swrStagesResponse}
size="md"
leftSection={<GoPlus size={24} />}
variant="outline"
onClick={async () => {
await createRound(tournamentData.id, stageItem.id);
await swrStagesResponse.mutate();
}}
>
{t('add_round_button')}
</Button>
/>
)}
{hideAddRoundButton ? null : (
<ActivateNextRoundModal
@@ -87,7 +204,7 @@ function getRoundsGridCols(
</Grid.Col>
</Grid>
</div>
<Group align="top">{rounds}</Group>
<Group align="top">{result}</Group>
</React.Fragment>
);
}
@@ -132,44 +249,3 @@ function LoadingSkeleton() {
</Group>
);
}
export default function Brackets({
tournamentData,
swrStagesResponse,
swrUpcomingMatchesResponse,
readOnly,
displaySettings,
stageItem,
}: {
tournamentData: TournamentMinimal;
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse | null;
readOnly: boolean;
displaySettings: BracketDisplaySettings;
stageItem: StageItemWithRounds;
}) {
const { t } = useTranslation();
if (swrStagesResponse.isLoading) {
return <LoadingSkeleton />;
}
if (!swrStagesResponse.isLoading && !responseIsValid(swrStagesResponse)) {
return <NoRoundsAlert readOnly={readOnly} />;
}
if (swrStagesResponse.isLoading) {
return <LoadingSkeleton />;
}
const rounds = getRoundsGridCols(
t,
stageItem,
tournamentData,
swrStagesResponse,
swrUpcomingMatchesResponse,
readOnly,
displaySettings
);
return <div>{rounds}</div>;
}

View File

@@ -3,7 +3,6 @@ import assert from 'assert';
import React, { useState } from 'react';
import { SWRResponse } from 'swr';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import {
MatchInterface,
formatMatchInput1,
@@ -47,7 +46,6 @@ export default function Match({
match,
readOnly,
dynamicSchedule,
displaySettings,
}: {
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse | null;
@@ -55,7 +53,6 @@ export default function Match({
match: MatchInterface;
readOnly: boolean;
dynamicSchedule: boolean;
displaySettings?: BracketDisplaySettings | null;
}) {
// const { classes } = useStyles();
const theme = useMantineTheme();
@@ -63,8 +60,6 @@ export default function Match({
// backgroundColor: theme.colorScheme === 'dark' ? theme.colors.green[9] : theme.colors.green[4],
backgroundColor: theme.colors.green[9],
};
const showTeamMemberNames =
displaySettings != null && displaySettings.teamNamesDisplay === 'player-names';
const stageItemsLookup = getStageItemLookup(swrStagesResponse);
const matchesLookup = getMatchLookup(swrStagesResponse);
@@ -74,22 +69,8 @@ export default function Match({
const team2_style =
match.stage_item_input1_score < match.stage_item_input2_score ? winner_style : {};
const team1_players = match.stage_item_input1?.team
? match.stage_item_input1.team.players.map((player) => player.name).join(', ')
: '';
const team2_players = match.stage_item_input2?.team
? match.stage_item_input2.team.players.map((player) => player.name).join(', ')
: '';
const team1_players_label = team1_players === '' ? 'No players' : team1_players;
const team2_players_label = team2_players === '' ? 'No players' : team2_players;
const team1_label = showTeamMemberNames
? team1_players_label
: formatMatchInput1(stageItemsLookup, matchesLookup, match);
const team2_label = showTeamMemberNames
? team2_players_label
: formatMatchInput2(stageItemsLookup, matchesLookup, match);
const team1_label = formatMatchInput1(stageItemsLookup, matchesLookup, match);
const team2_label = formatMatchInput2(stageItemsLookup, matchesLookup, match);
const [opened, setOpened] = useState(false);

View File

@@ -49,7 +49,6 @@ export default function Round({
match={match}
readOnly={readOnly}
dynamicSchedule={dynamicSchedule}
displaySettings={displaySettings}
/>
));
const active_round_style = round.is_active

View File

@@ -8,6 +8,7 @@ import {
Menu,
Stack,
Text,
Tooltip,
useCombobox,
useMantineColorScheme,
useMantineTheme,
@@ -282,44 +283,56 @@ function StageItemRow({
setOpened={setOpened}
rankings={rankings}
/>
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="transparent" color="gray">
<IconDots size="1.25rem" />
<Group gap="0rem">
<Tooltip label={t('handle_swiss_system')}>
<ActionIcon
variant="transparent"
color="gray"
component={Link}
href={`/tournaments/${tournament.id}/stages/swiss/${stageItem.id}`}
>
<BiSolidWrench size="1.25rem" />
</ActionIcon>
</Menu.Target>
</Tooltip>
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="transparent" color="gray">
<IconDots size="1.25rem" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconPencil size="1.5rem" />}
onClick={() => {
setOpened(true);
}}
>
{t('edit_stage_item_label')}
</Menu.Item>
{stageItem.type === 'SWISS' ? (
<Menu.Dropdown>
<Menu.Item
leftSection={<BiSolidWrench size="1.5rem" />}
component={Link}
href={`/tournaments/${tournament.id}/swiss/${stageItem.id}`}
leftSection={<IconPencil size="1.5rem" />}
onClick={() => {
setOpened(true);
}}
>
{t('handle_swiss_system')}
{t('edit_stage_item_label')}
</Menu.Item>
) : null}
<Menu.Item
leftSection={<IconTrash size="1.5rem" />}
onClick={async () => {
await deleteStageItem(tournament.id, stageItem.id);
await swrStagesResponse.mutate();
await swrAvailableInputsResponse.mutate();
}}
color="red"
>
{t('delete_button')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
{stageItem.type === 'SWISS' ? (
<Menu.Item
leftSection={<BiSolidWrench size="1.5rem" />}
component={Link}
href={`/tournaments/${tournament.id}/swiss/${stageItem.id}`}
>
{t('handle_swiss_system')}
</Menu.Item>
) : null}
<Menu.Item
leftSection={<IconTrash size="1.5rem" />}
onClick={async () => {
await deleteStageItem(tournament.id, stageItem.id);
await swrStagesResponse.mutate();
await swrAvailableInputsResponse.mutate();
}}
color="red"
>
{t('delete_button')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Group>
</Card.Section>
{inputs}

View File

@@ -1,9 +1,10 @@
import { Button } from '@mantine/core';
import { IconTool } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { MdOutlineAutoFixHigh } from 'react-icons/md';
import { SWRResponse } from 'swr';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import { SchedulerSettings } from '../../interfaces/match';
import { Tournament } from '../../interfaces/tournament';
import { createMatchesAuto } from '../../services/round';
@@ -14,21 +15,22 @@ export function AutoCreateMatchesButton({
swrUpcomingMatchesResponse,
stageItemId,
schedulerSettings,
displaySettings,
}: {
schedulerSettings: SchedulerSettings;
stageItemId: number;
tournamentData: Tournament;
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse;
displaySettings: BracketDisplaySettings;
}) {
const { t } = useTranslation();
return (
<Button
size="md"
mt="1rem"
mb="1rem"
color="indigo"
leftSection={<IconTool size={24} />}
leftSection={<MdOutlineAutoFixHigh size={24} />}
disabled={displaySettings.showManualSchedulingOptions === 'true'}
onClick={async () => {
await createMatchesAuto(
tournamentData.id,

View File

@@ -13,7 +13,7 @@ import { useRouter } from 'next/router';
import React from 'react';
import QRCode from 'react-qr-code';
import { Tournament, getTournamentEndpoint } from '../../interfaces/tournament';
import { Tournament } from '../../interfaces/tournament';
import { getBaseApiUrl } from '../../services/adapter';
import { getBaseURL } from '../utils/util';
import classes from './layout.module.css';
@@ -80,7 +80,7 @@ export function TournamentTitle({ tournamentDataFull }: { tournamentDataFull: To
export function DoubleHeader({ tournamentData }: { tournamentData: Tournament }) {
const router = useRouter();
const endpoint = getTournamentEndpoint(tournamentData);
const endpoint = tournamentData.dashboard_endpoint;
const pathName = router.pathname.replace('[id]', endpoint).replace(/\/+$/, '');
const mainLinks = [

View File

@@ -1,32 +1,5 @@
import { Text } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import { TeamInterface } from '../../interfaces/team';
import { truncateString } from '../utils/util';
export default function PlayerList({
team,
displaySettings,
}: {
team: TeamInterface;
displaySettings?: BracketDisplaySettings | null;
}) {
const { t } = useTranslation();
if (displaySettings != null && displaySettings.teamNamesDisplay === 'team-names') {
return <span>{team.name}</span>;
}
if (team.players.length < 1) {
return <i>{t('no_team_members_description')}</i>;
}
const playerNames = team.players
.map((player) => truncateString(player.name, 15))
.sort()
.join(', ');
return (
<Text span truncate="end">
{playerNames}
</Text>
);
export default function PlayerList({ team }: { team: TeamInterface }) {
return <span>{team.name}</span>;
}

View File

@@ -60,7 +60,7 @@ function MainLink({ item, pathName }: { item: MainLinkProps; pathName: String })
component={Link}
href={item.link}
className={classes.link}
data-active={pathName === item.link || undefined}
data-active={pathName.startsWith(item.link) || undefined}
>
<item.icon stroke={1.5} />
</UnstyledButton>

View File

@@ -1,5 +1,4 @@
import { Alert, Container, Text, Title } from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
@@ -29,32 +28,18 @@ export function EmptyTableInfo({
);
}
export function NoContent({ title, description }: { title: string; description?: string }) {
return (
<Container mt="md">
<div className={classes.label}>
<MdOutlineConstruction />
</div>
<Title className={classes.title}>{title}</Title>
<Text size="lg" ta="center" className={classes.description}>
{description}
</Text>
</Container>
);
}
export function NoContentDashboard({
export function NoContent({
title,
description,
icon,
}: {
title: string;
description?: string;
icon?: React.ReactNode;
}) {
return (
<Container mt="md">
<div className={classes.label}>
<AiOutlineHourglass />
</div>
<div className={classes.label}>{icon || <MdOutlineConstruction />}</div>
<Title className={classes.title}>{title}</Title>
<Text size="lg" ta="center" className={classes.description}>
{description}

View File

@@ -1,29 +1,24 @@
import { Divider, Grid, Group } from '@mantine/core';
import { Divider } from '@mantine/core';
import React from 'react';
import { SWRResponse } from 'swr';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import { SchedulerSettings } from '../../interfaces/match';
import { RoundInterface } from '../../interfaces/round';
import { StageWithStageItems } from '../../interfaces/stage';
import { StageItemWithRounds } from '../../interfaces/stage_item';
import { Tournament } from '../../interfaces/tournament';
import { AutoCreateMatchesButton } from '../buttons/create_matches_auto';
import UpcomingMatchesTable from '../tables/upcoming_matches';
import SwissSettings from './settings/ladder_fixed';
function SchedulingSystem({
tournamentData,
draftRound,
swrRoundsResponse,
swrStagesResponse,
swrUpcomingMatchesResponse,
displaySettings,
}: {
tournamentData: Tournament;
draftRound: RoundInterface;
swrRoundsResponse: SWRResponse;
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse;
displaySettings: BracketDisplaySettings;
}) {
return (
<>
@@ -31,9 +26,8 @@ function SchedulingSystem({
<UpcomingMatchesTable
round_id={draftRound.id}
tournamentData={tournamentData}
swrRoundsResponse={swrRoundsResponse}
swrStagesResponse={swrStagesResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
displaySettings={displaySettings}
/>
</>
);
@@ -41,50 +35,30 @@ function SchedulingSystem({
export default function Scheduler({
activeStage,
stageItem,
tournamentData,
draftRound,
swrRoundsResponse,
swrStagesResponse,
swrUpcomingMatchesResponse,
schedulerSettings,
displaySettings,
}: {
activeStage: StageWithStageItems;
stageItem: StageItemWithRounds;
draftRound: RoundInterface;
tournamentData: Tournament;
swrRoundsResponse: SWRResponse;
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse;
schedulerSettings: SchedulerSettings;
displaySettings: BracketDisplaySettings;
}) {
return (
<div style={{ marginTop: '1rem' }}>
<h2>
Schedule new matches for <u>{draftRound.name}</u> in <u>{activeStage.name}</u>
</h2>
<Grid>
<Grid.Col span="auto">
<SwissSettings schedulerSettings={schedulerSettings} />
</Grid.Col>
<Grid.Col span="content">
<Group justify="right">
<AutoCreateMatchesButton
swrStagesResponse={swrRoundsResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
tournamentData={tournamentData}
stageItemId={stageItem.id}
schedulerSettings={schedulerSettings}
/>
</Group>
</Grid.Col>
</Grid>
<SwissSettings schedulerSettings={schedulerSettings} />
<SchedulingSystem
draftRound={draftRound}
tournamentData={tournamentData}
swrRoundsResponse={swrRoundsResponse}
swrStagesResponse={swrStagesResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
displaySettings={displaySettings}
/>
</div>
);

View File

@@ -2,29 +2,26 @@ import { Badge, Button, Table } from '@mantine/core';
import { IconCalendarPlus, IconCheck } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { FaCheck } from 'react-icons/fa6';
import { SWRResponse } from 'swr';
import { BracketDisplaySettings } from '../../interfaces/brackets';
import { MatchCreateBodyInterface, UpcomingMatchInterface } from '../../interfaces/match';
import { Tournament } from '../../interfaces/tournament';
import { createMatch } from '../../services/match';
import PlayerList from '../info/player_list';
import { EmptyTableInfo } from '../no_content/empty_table_info';
import { NoContent } from '../no_content/empty_table_info';
import RequestErrorAlert from '../utils/error_alert';
import TableLayout, { ThNotSortable, ThSortable, getTableState, sortTableEntries } from './table';
export default function UpcomingMatchesTable({
round_id,
tournamentData,
swrRoundsResponse,
swrStagesResponse,
swrUpcomingMatchesResponse,
displaySettings,
}: {
round_id: number;
tournamentData: Tournament;
swrRoundsResponse: SWRResponse;
swrStagesResponse: SWRResponse;
swrUpcomingMatchesResponse: SWRResponse;
displaySettings: BracketDisplaySettings;
}) {
const { t } = useTranslation();
const upcoming_matches: UpcomingMatchInterface[] =
@@ -53,7 +50,7 @@ export default function UpcomingMatchesTable({
await createMatch(tournamentData.id, match_to_schedule);
}
await swrRoundsResponse.mutate();
await swrStagesResponse.mutate();
await swrUpcomingMatchesResponse.mutate();
}
@@ -72,22 +69,8 @@ export default function UpcomingMatchesTable({
</Badge>
) : null}
</Table.Td>
<Table.Td>
{upcoming_match.stage_item_input1.team != null ? (
<PlayerList
team={upcoming_match.stage_item_input1.team}
displaySettings={displaySettings}
/>
) : null}
</Table.Td>
<Table.Td>
{upcoming_match.stage_item_input2.team != null ? (
<PlayerList
team={upcoming_match.stage_item_input2.team}
displaySettings={displaySettings}
/>
) : null}
</Table.Td>
<Table.Td>{upcoming_match.stage_item_input1.team?.name}</Table.Td>
<Table.Td>{upcoming_match.stage_item_input2.team?.name}</Table.Td>
<Table.Td>{Number(upcoming_match.elo_diff).toFixed(0)}</Table.Td>
<Table.Td>{Number(upcoming_match.swiss_diff).toFixed(1)}</Table.Td>
<Table.Td>
@@ -105,7 +88,13 @@ export default function UpcomingMatchesTable({
));
if (rows.length < 1) {
return <EmptyTableInfo entity_name={t('upcoming_matches_empty_table_info')} />;
return (
<NoContent
title={t('no_more_matches_title')}
description={`${t('all_matches_scheduled_description')}`}
icon={<FaCheck />}
/>
);
}
return (

View File

@@ -36,10 +36,6 @@ export function getDefaultTimeRange(selectMultipleDates: boolean) {
return [minDate, maxDate];
}
export function onlyUnique(value: any, index: number, self: any) {
return self.indexOf(value) === index;
}
export function getTournamentIdFromRouter() {
const router = useRouter();
const { id: idString }: any = router.query;

View File

@@ -3,4 +3,6 @@ export interface BracketDisplaySettings {
setMatchVisibility: React.Dispatch<any>;
teamNamesDisplay: string;
setTeamNamesDisplay: React.Dispatch<any>;
showManualSchedulingOptions: string;
setShowManualSchedulingOptions: React.Dispatch<any>;
}

View File

@@ -15,7 +15,3 @@ export interface Tournament {
export interface TournamentMinimal {
id: number;
}
export function getTournamentEndpoint(tournament: Tournament) {
return tournament.dashboard_endpoint != null ? tournament.dashboard_endpoint : `${tournament.id}`;
}

View File

@@ -1,4 +1,5 @@
import { Alert, Badge, Card, Center, Flex, Grid, Group, Stack, Text } from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -7,7 +8,7 @@ import React from 'react';
import { DashboardFooter } from '../../../../components/dashboard/footer';
import { DoubleHeader, TournamentHeadTitle } from '../../../../components/dashboard/layout';
import { NoContentDashboard } from '../../../../components/no_content/empty_table_info';
import { NoContent } from '../../../../components/no_content/empty_table_info';
import { Time, formatTime } from '../../../../components/utils/datetime';
import { Translator } from '../../../../components/utils/types';
import { responseIsValid } from '../../../../components/utils/util';
@@ -162,7 +163,7 @@ export function Schedule({
}
if (rows.length < 1) {
return <NoContentDashboard title={t('no_matches_title')} description="" />;
return <NoContent title={t('no_matches_title')} description="" icon={<AiOutlineHourglass />} />;
}
const noItemsAlert =

View File

@@ -1,4 +1,5 @@
import { Container, Text } from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
@@ -8,7 +9,7 @@ import { SWRResponse } from 'swr';
import NotFoundTitle from '../../../404';
import { DashboardFooter } from '../../../../components/dashboard/footer';
import { DoubleHeader, TournamentHeadTitle } from '../../../../components/dashboard/layout';
import { NoContentDashboard } from '../../../../components/no_content/empty_table_info';
import { NoContent } from '../../../../components/no_content/empty_table_info';
import { StandingsTableForStageItem } from '../../../../components/tables/standings';
import { TableSkeletonTwoColumns } from '../../../../components/utils/skeletons';
import { responseIsValid } from '../../../../components/utils/util';
@@ -43,9 +44,10 @@ function StandingsContent({ swrStagesResponse }: { swrStagesResponse: SWRRespons
if (rows.length < 1) {
return (
<NoContentDashboard
<NoContent
title={`${t('could_not_find_any_alert')} ${t('teams_title')}`}
description=""
icon={<AiOutlineHourglass />}
/>
);
}

View File

@@ -11,13 +11,14 @@ import {
Title,
UnstyledButton,
} from '@mantine/core';
import { AiOutlineHourglass } from '@react-icons/all-files/ai/AiOutlineHourglass';
import { IconAlertCircle } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
import MatchModal from '../../../components/modals/match_modal';
import { NoContentDashboard } from '../../../components/no_content/empty_table_info';
import { NoContent } from '../../../components/no_content/empty_table_info';
import { Time, formatTime } from '../../../components/utils/datetime';
import { Translator } from '../../../components/utils/types';
import { getTournamentIdFromRouter, responseIsValid } from '../../../components/utils/util';
@@ -188,7 +189,11 @@ function Schedule({
if (rows.length < 1) {
return (
<NoContentDashboard title={t('no_matches_title')} description={t('no_matches_description')} />
<NoContent
title={t('no_matches_title')}
description={t('no_matches_description')}
icon={<AiOutlineHourglass />}
/>
);
}

View File

@@ -15,7 +15,7 @@ import {
import { DateTimePicker } from '@mantine/dates';
import { useForm } from '@mantine/form';
import { MdDelete } from '@react-icons/all-files/md/MdDelete';
import { IconCalendar, IconCalendarTime } from '@tabler/icons-react';
import { IconCalendar, IconCalendarTime, IconCopy } from '@tabler/icons-react';
import assert from 'assert';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
@@ -28,7 +28,7 @@ import { DropzoneButton } from '../../../components/utils/file_upload';
import { GenericSkeleton } from '../../../components/utils/skeletons';
import { capitalize, getBaseURL, getTournamentIdFromRouter } from '../../../components/utils/util';
import { Club } from '../../../interfaces/club';
import { Tournament, getTournamentEndpoint } from '../../../interfaces/tournament';
import { Tournament } from '../../../interfaces/tournament';
import {
getBaseApiUrl,
getClubs,
@@ -162,11 +162,32 @@ function GeneralTournamentForm({
</Grid>
</Fieldset>
<Fieldset legend={t('dashboard_settings_title')} mt="lg" radius="md">
<TextInput
label={t('dashboard_link_label')}
placeholder={t('dashboard_link_placeholder')}
{...form.getInputProps('dashboard_endpoint')}
/>
<Text fz="sm">{t('dashboard_link_label')}</Text>
<Grid>
<Grid.Col span={{ sm: 9 }}>
<TextInput
placeholder={t('dashboard_link_placeholder')}
{...form.getInputProps('dashboard_endpoint')}
/>
</Grid.Col>
<Grid.Col span={{ sm: 3 }}>
<CopyButton
value={`${getBaseURL()}/tournaments/${tournament.dashboard_endpoint}/dashboard`}
>
{({ copied, copy }) => (
<Button
disabled={form.values.dashboard_endpoint === ''}
leftSection={<IconCopy size="1.1rem" stroke={1.5} />}
fullWidth
color={copied ? 'teal' : 'indigo'}
onClick={copy}
>
{copied ? t('copied_url_button') : t('copy_url_button')}
</Button>
)}
</CopyButton>
</Grid.Col>
</Grid>
<Checkbox
mt="lg"
@@ -212,17 +233,6 @@ function GeneralTournamentForm({
{t('save_button')}
</Button>
{tournament != null ? (
<CopyButton
value={`${getBaseURL()}/tournaments/${getTournamentEndpoint(tournament)}/dashboard`}
>
{({ copied, copy }) => (
<Button fullWidth mt="sm" color={copied ? 'teal' : 'blue'} onClick={copy}>
{copied ? t('copied_dashboard_url_button') : t('copy_dashboard_url_button')}
</Button>
)}
</CopyButton>
) : null}
<Button
fullWidth
variant="outline"

View File

@@ -3,22 +3,22 @@ import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import Builder from '../../../components/builder/builder';
import { CreateStageButtonLarge } from '../../../components/buttons/create_stage';
import ActivateNextStageModal from '../../../components/modals/activate_next_stage_modal';
import ActivatePreviousStageModal from '../../../components/modals/activate_previous_stage_modal';
import { NoContent } from '../../../components/no_content/empty_table_info';
import { TableSkeletonTwoColumnsSmall } from '../../../components/utils/skeletons';
import { getTournamentIdFromRouter } from '../../../components/utils/util';
import { Ranking } from '../../../interfaces/ranking';
import { StageWithStageItems } from '../../../interfaces/stage';
import Builder from '../../../../components/builder/builder';
import { CreateStageButtonLarge } from '../../../../components/buttons/create_stage';
import ActivateNextStageModal from '../../../../components/modals/activate_next_stage_modal';
import ActivatePreviousStageModal from '../../../../components/modals/activate_previous_stage_modal';
import { NoContent } from '../../../../components/no_content/empty_table_info';
import { TableSkeletonTwoColumnsSmall } from '../../../../components/utils/skeletons';
import { getTournamentIdFromRouter } from '../../../../components/utils/util';
import { Ranking } from '../../../../interfaces/ranking';
import { StageWithStageItems } from '../../../../interfaces/stage';
import {
getAvailableStageItemInputs,
getRankings,
getStages,
getTournamentById,
} from '../../../services/adapter';
import TournamentLayout from '../_tournament_layout';
} from '../../../../services/adapter';
import TournamentLayout from '../../_tournament_layout';
export default function StagesPage() {
const { t } = useTranslation();

View File

@@ -1,34 +1,39 @@
import { Button, Grid, Group, SegmentedControl, Title } from '@mantine/core';
import { Button, Container, Grid, Group, SegmentedControl, Stack, Title } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Link from 'next/link';
import React from 'react';
import { LuNavigation } from 'react-icons/lu';
import { SWRResponse } from 'swr';
import NotFoundTitle from '../../../404';
import Brackets from '../../../../components/brackets/brackets';
import Scheduler from '../../../../components/scheduling/scheduling';
import classes from '../../../../components/utility.module.css';
import { useRouterQueryState } from '../../../../components/utils/query_parameters';
import NotFoundTitle from '../../../../404';
import { RoundsGridCols } from '../../../../../components/brackets/brackets';
import { NoContent } from '../../../../../components/no_content/empty_table_info';
import Scheduler from '../../../../../components/scheduling/scheduling';
import classes from '../../../../../components/utility.module.css';
import { useRouterQueryState } from '../../../../../components/utils/query_parameters';
import { Translator } from '../../../../../components/utils/types';
import {
getStageItemIdFromRouter,
getTournamentIdFromRouter,
responseIsValid,
} from '../../../../components/utils/util';
import { BracketDisplaySettings } from '../../../../interfaces/brackets';
import { SchedulerSettings } from '../../../../interfaces/match';
import { RoundInterface } from '../../../../interfaces/round';
import { getStageById } from '../../../../interfaces/stage';
import { stageItemIsHandledAutomatically } from '../../../../interfaces/stage_item';
import { getTournamentEndpoint } from '../../../../interfaces/tournament';
} from '../../../../../components/utils/util';
import { BracketDisplaySettings } from '../../../../../interfaces/brackets';
import { SchedulerSettings } from '../../../../../interfaces/match';
import { RoundInterface } from '../../../../../interfaces/round';
import { getStageById } from '../../../../../interfaces/stage';
import { stageItemIsHandledAutomatically } from '../../../../../interfaces/stage_item';
import { Tournament } from '../../../../../interfaces/tournament';
import {
checkForAuthError,
getCourts,
getStages,
getTournamentById,
getUpcomingMatches,
} from '../../../../services/adapter';
import { getStageItemLookup } from '../../../../services/lookups';
import TournamentLayout from '../../_tournament_layout';
} from '../../../../../services/adapter';
import { getStageItemLookup } from '../../../../../services/lookups';
import TournamentLayout from '../../../_tournament_layout';
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
@@ -36,6 +41,25 @@ export const getServerSideProps = async ({ locale }: { locale: string }) => ({
},
});
function NoCourtsButton({ t, tournamentData }: { t: Translator; tournamentData: Tournament }) {
return (
<Stack align="center">
<NoContent title={t('no_courts_title')} description={t('no_courts_description_swiss')} />
<Button
color="green"
size="lg"
leftSection={<LuNavigation size={24} />}
variant="outline"
component={Link}
className={classes.mobileLink}
href={`/tournaments/${tournamentData.id}/schedule`}
>
{t('go_to_courts_page')}
</Button>
</Stack>
);
}
export default function TournamentPage() {
const { id, tournamentData } = getTournamentIdFromRouter();
const stageItemId = getStageItemIdFromRouter();
@@ -44,17 +68,24 @@ export default function TournamentPage() {
const swrTournamentResponse = getTournamentById(tournamentData.id);
checkForAuthError(swrTournamentResponse);
const swrStagesResponse: SWRResponse = getStages(id);
const swrCourtsResponse = getCourts(tournamentData.id);
const [onlyRecommended, setOnlyRecommended] = useRouterQueryState('only-recommended', 'true');
const [eloThreshold, setEloThreshold] = useRouterQueryState('max-elo-diff', 100);
const [iterations, setIterations] = useRouterQueryState('iterations', 1000);
const [limit, setLimit] = useRouterQueryState('limit', 50);
const [matchVisibility, setMatchVisibility] = useRouterQueryState('match-visibility', 'all');
const [teamNamesDisplay, setTeamNamesDisplay] = useRouterQueryState('which-names', 'team-names');
const [showAdvancedSchedulingOptions, setShowAdvancedSchedulingOptions] = useRouterQueryState(
'advanced',
'false'
);
const displaySettings: BracketDisplaySettings = {
matchVisibility,
setMatchVisibility,
teamNamesDisplay,
setTeamNamesDisplay,
showManualSchedulingOptions: showAdvancedSchedulingOptions,
setShowManualSchedulingOptions: setShowAdvancedSchedulingOptions,
};
const schedulerSettings: SchedulerSettings = {
@@ -81,7 +112,7 @@ export default function TournamentPage() {
if (activeStage != null && activeStage.stage_items != null) {
const draftRounds = stageItem.rounds.filter(
(round: RoundInterface) => round.stage_item_id === stageItemId
(round: RoundInterface) => round.stage_item_id === stageItemId && round.is_draft
);
if (draftRounds != null && draftRounds.length > 0) {
@@ -96,17 +127,16 @@ export default function TournamentPage() {
stageItem != null &&
!stageItemIsHandledAutomatically(stageItem) &&
activeStage != null &&
displaySettings.showManualSchedulingOptions === 'true' &&
swrUpcomingMatchesResponse != null ? (
<>
<Scheduler
activeStage={activeStage}
stageItem={stageItem}
draftRound={draftRound}
tournamentData={tournamentDataFull}
swrRoundsResponse={swrStagesResponse}
swrStagesResponse={swrStagesResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
schedulerSettings={schedulerSettings}
displaySettings={displaySettings}
/>
</>
) : null;
@@ -115,6 +145,18 @@ export default function TournamentPage() {
return <NotFoundTitle />;
}
if (!swrCourtsResponse.isLoading && swrCourtsResponse.data.data.length < 1) {
return (
<TournamentLayout tournament_id={tournamentData.id}>
<Container mt="1rem">
<Stack align="center">
<NoCourtsButton t={t} tournamentData={tournamentDataFull} />
</Stack>
</Container>
</TournamentLayout>
);
}
return (
<TournamentLayout tournament_id={tournamentData.id}>
<Grid grow>
@@ -133,39 +175,37 @@ export default function TournamentPage() {
{ label: t('match_filter_option_current'), value: 'present-only' },
]}
/>
<SegmentedControl
className={classes.fullWithMobile}
value={teamNamesDisplay}
onChange={setTeamNamesDisplay}
data={[
{ label: t('name_filter_options_team'), value: 'team-names' },
{ label: t('name_filter_options_player'), value: 'player-names' },
]}
/>
<Button
className={classes.fullWithMobile}
color="blue"
size="sm"
variant="outline"
leftSection={<IconExternalLink size={24} />}
onClick={() => {
const endpoint = getTournamentEndpoint(tournamentDataFull);
window.open(`/tournaments/${endpoint}/dashboard`, '_ blank');
}}
>
{t('view_dashboard_button')}
</Button>
{tournamentDataFull?.dashboard_endpoint && (
<Button
className={classes.fullWithMobile}
color="blue"
size="sm"
variant="outline"
leftSection={<IconExternalLink size={24} />}
onClick={() => {
window.open(
`/tournaments/${tournamentDataFull.dashboard_endpoint}/dashboard`,
'_ blank'
);
}}
>
{t('view_dashboard_button')}
</Button>
)}
</Group>
</Grid.Col>
</Grid>
<div style={{ marginTop: '1rem', marginLeft: '1rem', marginRight: '1rem' }}>
<Brackets
<RoundsGridCols
tournamentData={tournamentDataFull}
swrStagesResponse={swrStagesResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
swrCourtsResponse={swrCourtsResponse}
readOnly={false}
stageItem={stageItem}
displaySettings={displaySettings}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
schedulerSettings={schedulerSettings}
draftRound={draftRound}
/>
{scheduler}
</div>