Improve planning page (#895)

This commit is contained in:
Erik Vroon
2024-09-08 11:14:50 +02:00
committed by GitHub
parent 72fe9ce818
commit 8a32b5c841
16 changed files with 163 additions and 189 deletions

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Dashboard-URL kopieren",
"could_not_find_any_alert": "Konnte keine finden",
"court_name_input_placeholder": "Bestes Spielfeld aller Zeiten",
"court_spotlight_description": "Spielfelder ansehen, hinzufügen oder löschen",
"courts_title": "Spielfeld",
"create_account_alert_description": "Kontoerstellung ist auf dieser Domain momentan deaktiviert, da sich bracket noch in der Beta-Phase befindet.",
"create_account_alert_title": "Nicht verfügbar",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Copy Dashboard URL",
"could_not_find_any_alert": "Could not find any",
"court_name_input_placeholder": "Best Court Ever",
"court_spotlight_description": "View, add or delete courts",
"courts_title": "courts",
"create_account_alert_description": "Account creation is disabled on this domain for now since bracket is still in beta phase",
"create_account_alert_title": "Unavailable",
@@ -152,6 +151,7 @@
"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_courts_title": "No courts yet",
"no_players_title": "No players 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.",
@@ -194,6 +194,7 @@
"save_players_button": "Save players",
"save_ranking_button": "Save Ranking",
"schedule_description": "Schedule All Unscheduled Matches",
"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.",
"schedule_title": "Schedule",
"score_of_label": "Score of",
"search_placeholder": "Search ...",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Copiar URL del Tablero",
"could_not_find_any_alert": "No se pudo encontrar ninguna",
"court_name_input_placeholder": "Mejor tribunal nunca",
"court_spotlight_description": "Visualizar, añadir o eliminar tribunales",
"courts_title": "cortes",
"create_account_alert_description": "La creación de cuenta está desactivada en este dominio por ahora ya que el corchete todavía está en fase beta",
"create_account_alert_title": "Unavailable",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Copier l'URL du tableau de bord",
"could_not_find_any_alert": "Aucun résultat trouvé",
"court_name_input_placeholder": "Le Meilleur Des Terrains",
"court_spotlight_description": "Voir, ajouter ou supprimer des terrains",
"courts_title": "Terrains",
"create_account_alert_description": "Pour le moment, la création de compte est désactivée sur ce domaine car Bracket est toujours en phase bêta",
"create_account_alert_title": "Indisponible",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Kopieer de Dashboard URL",
"could_not_find_any_alert": "Kon er geen vinden",
"court_name_input_placeholder": "Beste veld ooit",
"court_spotlight_description": "Velden bekijken, toevoegen of verwijderen",
"courts_title": "velden",
"create_account_alert_description": "Het aanmaken van een account is voorlopig uitgeschakeld op dit domein",
"create_account_alert_title": "Niet beschikbaar",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "Copiar URL do painel",
"could_not_find_any_alert": "Não foi possível encontrar nenhum",
"court_name_input_placeholder": "Melhor Tribunal já",
"court_spotlight_description": "Visualizar, adicionar ou excluir tribunais",
"courts_title": "Tribunais",
"create_account_alert_description": "A criação de conta está desabilitada neste domínio por enquanto já que o parêntese ainda está na fase beta",
"create_account_alert_title": "Unavailable",

View File

@@ -43,7 +43,6 @@
"copy_dashboard_url_button": "复制仪表板URL",
"could_not_find_any_alert": "找不到任何",
"court_name_input_placeholder": "最好的场地",
"court_spotlight_description": "查看、添加或删除场地",
"courts_title": "场地",
"create_account_alert_description": "由于括号仍处于测试阶段,因此暂时禁用了此域上的帐户创建",
"create_account_alert_title": "不可用",

View File

@@ -100,7 +100,7 @@ function StageItemRow({
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="transparent" color="gray">
<IconDots size="1.5rem" />
<IconDots size="1.25rem" />
</ActionIcon>
</Menu.Target>
@@ -190,7 +190,7 @@ function StageColumn({
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="transparent" color="gray">
<IconDots size="1rem" />
<IconDots size="1.25rem" />
</ActionIcon>
</Menu.Target>

View File

@@ -10,7 +10,7 @@ export function NextStageButton({ tournamentData, swrStagesResponse }: any) {
return (
<Button
size="md"
style={{ marginBottom: 10 }}
mb="10"
color="indigo"
leftSection={<IconSquareArrowRight size={24} />}
onClick={async () => {
@@ -29,7 +29,7 @@ export function PreviousStageButton({ tournamentData, swrStagesResponse }: any)
return (
<Button
size="md"
style={{ marginBottom: 10 }}
mb="10"
color="indigo"
leftSection={<IconSquareArrowLeft size={24} />}
onClick={async () => {

View File

@@ -0,0 +1,65 @@
import { Button, Modal, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { GoPlus } from '@react-icons/all-files/go/GoPlus';
import { useTranslation } from 'next-i18next';
import React, { useState } from 'react';
import { SWRResponse } from 'swr';
import { createCourt } from '../../services/court';
export default function CourtModal({
tournamentId,
swrCourtsResponse,
buttonSize,
}: {
buttonSize: 'xs' | 'lg';
tournamentId: number;
swrCourtsResponse: SWRResponse;
}) {
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const form = useForm({
initialValues: {
name: '',
},
validate: {
name: (value) => (value.length > 0 ? null : t('too_short_name_validation')),
},
});
return (
<>
<Modal opened={opened} onClose={() => setOpened(false)} title={t('add_court_title')}>
<form
onSubmit={form.onSubmit(async (values) => {
await createCourt(tournamentId, values.name);
await swrCourtsResponse.mutate();
setOpened(false);
})}
>
<TextInput
withAsterisk
label={t('name_input_label')}
placeholder={t('court_name_input_placeholder')}
{...form.getInputProps('name')}
/>
<Button fullWidth style={{ marginTop: 10 }} color="green" type="submit">
{t('save_button')}
</Button>
</form>
</Modal>
<Button
variant="outline"
color="green"
size={buttonSize}
style={{ marginRight: 10 }}
onClick={() => setOpened(true)}
leftSection={<GoPlus size={24} />}
>
{t('add_court_title')}
</Button>
</>
);
}

View File

@@ -203,7 +203,6 @@ export function CreateStageItemModal({
variant="outline"
color="green"
size="xs"
style={{ marginRight: 10 }}
onClick={() => setOpened(true)}
leftSection={<GoPlus size={24} />}
>

View File

@@ -7,7 +7,6 @@ import {
IconScoreboard,
IconSearch,
IconSettings,
IconSoccerField,
IconTrophy,
IconUser,
IconUsers,
@@ -84,13 +83,6 @@ export function BracketSpotlight() {
onClick: () => router.push(`/tournaments/${tournamentId}/stages`),
leftSection: <IconTrophy size="1.2rem" />,
},
{
id: 'courts',
title: t('courts_title'),
description: t('court_spotlight_description'),
onClick: () => router.push(`/tournaments/${tournamentId}/courts`),
leftSection: <IconSoccerField size="1.2rem" />,
},
{
id: 'tournament settings',
title: t('tournament_setting_title'),

View File

@@ -10,7 +10,6 @@ import {
IconHome,
IconScoreboard,
IconSettings,
IconSoccerField,
IconTrophy,
IconUser,
IconUsers,
@@ -130,11 +129,6 @@ export function TournamentLinks({ tournament_id }: any) {
label: capitalize(t('teams_title')),
link: `${tm_prefix}/teams`,
},
{
icon: IconSoccerField,
label: capitalize(t('courts_title')),
link: `${tm_prefix}/courts`,
},
{
icon: IconCalendar,
label: capitalize(t('planning_title')),

View File

@@ -1,64 +0,0 @@
import { Table } from '@mantine/core';
import React from 'react';
import { SWRResponse } from 'swr';
import { Court } from '../../interfaces/court';
import { Tournament } from '../../interfaces/tournament';
import { deleteCourt } from '../../services/court';
import DeleteButton from '../buttons/delete';
import { EmptyTableInfo } from '../no_content/empty_table_info';
import RequestErrorAlert from '../utils/error_alert';
import { TableSkeletonSingleColumn } from '../utils/skeletons';
import { Translator } from '../utils/types';
import TableLayout, { ThNotSortable, getTableState, sortTableEntries } from './table';
export default function CourtsTable({
t,
tournament,
swrCourtsResponse,
}: {
t: Translator;
tournament: Tournament;
swrCourtsResponse: SWRResponse;
}) {
const courts: Court[] = swrCourtsResponse.data != null ? swrCourtsResponse.data.data : [];
const tableState = getTableState('id');
if (swrCourtsResponse.isLoading) {
return <TableSkeletonSingleColumn />;
}
if (swrCourtsResponse.error) return <RequestErrorAlert error={swrCourtsResponse.error} />;
const rows = courts
.sort((s1: Court, s2: Court) => sortTableEntries(s1, s2, tableState))
.map((court) => (
<Table.Tr key={court.id}>
<Table.Td>{court.name}</Table.Td>
<Table.Td>
<DeleteButton
onClick={async () => {
await deleteCourt(tournament.id, court.id);
await swrCourtsResponse.mutate();
}}
title={t('delete_court_button')}
style={{ float: 'right' }}
/>
</Table.Td>
</Table.Tr>
));
if (rows.length < 1) return <EmptyTableInfo entity_name={t('courts_title')} />;
return (
<TableLayout>
<Table.Thead>
<Table.Tr>
<ThNotSortable>{t('title')}</ThNotSortable>
<ThNotSortable>{null}</ThNotSortable>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</TableLayout>
);
}

View File

@@ -1,79 +0,0 @@
import { Button, Container, Fieldset, Grid, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React from 'react';
import { SWRResponse } from 'swr';
import CourtsTable from '../../../components/tables/courts';
import { Translator } from '../../../components/utils/types';
import { getTournamentIdFromRouter } from '../../../components/utils/util';
import { Tournament } from '../../../interfaces/tournament';
import { getCourts, getTournamentById } from '../../../services/adapter';
import { createCourt } from '../../../services/court';
import TournamentLayout from '../_tournament_layout';
function CreateCourtForm(t: Translator, tournament: Tournament, swrCourtsResponse: SWRResponse) {
const form = useForm({
initialValues: { name: '' },
validate: {
name: (value) => (value.length > 0 ? null : t('too_short_name_validation')),
},
});
return (
<form
onSubmit={form.onSubmit(async (values) => {
await createCourt(tournament.id, values.name);
await swrCourtsResponse.mutate();
})}
>
<Fieldset legend={t('add_court_title')} radius="md">
<TextInput
withAsterisk
label={t('name_input_label')}
placeholder={t('court_name_input_placeholder')}
{...form.getInputProps('name')}
/>
<Button fullWidth style={{ marginTop: 16 }} color="green" type="submit">
{t('create_court_button')}
</Button>
</Fieldset>
</form>
);
}
export default function CourtsPage() {
const { tournamentData } = getTournamentIdFromRouter();
const swrCourtsResponse = getCourts(tournamentData.id);
const swrTournamentResponse = getTournamentById(tournamentData.id);
const tournamentDataFull =
swrTournamentResponse.data != null ? swrTournamentResponse.data.data : null;
const { t } = useTranslation();
return (
<TournamentLayout tournament_id={tournamentData.id}>
<Container maw="100rem">
<Grid grow>
<Grid.Col span={{ lg: 8 }}>
<CourtsTable
t={t}
tournament={tournamentDataFull}
swrCourtsResponse={swrCourtsResponse}
/>
</Grid.Col>
<Grid.Col span={{ lg: 4 }}>
{CreateCourtForm(t, tournamentDataFull, swrCourtsResponse)}
</Grid.Col>
</Grid>
</Container>
</TournamentLayout>
);
}
export const getServerSideProps = async ({ locale }: { locale: string }) => ({
props: {
...(await serverSideTranslations(locale, ['common'])),
},
});

View File

@@ -1,10 +1,24 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { Alert, Badge, Button, Card, Grid, Group, Stack, Text, Title } from '@mantine/core';
import { IconAlertCircle, IconCalendarPlus } from '@tabler/icons-react';
import {
ActionIcon,
Alert,
Badge,
Button,
Card,
Grid,
Group,
Menu,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconCalendarPlus, IconDots, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import React, { useState } from 'react';
import { SWRResponse } from 'swr';
import CourtModal from '../../../components/modals/create_court_modal';
import MatchModal from '../../../components/modals/match_modal';
import { NoContent } from '../../../components/no_content/empty_table_info';
import { Time } from '../../../components/utils/datetime';
@@ -12,7 +26,9 @@ import { Translator } from '../../../components/utils/types';
import { getTournamentIdFromRouter, responseIsValid } from '../../../components/utils/util';
import { Court } from '../../../interfaces/court';
import { MatchInterface, formatMatchTeam1, formatMatchTeam2 } from '../../../interfaces/match';
import { TournamentMinimal } from '../../../interfaces/tournament';
import { getCourts, getStages } from '../../../services/adapter';
import { deleteCourt } from '../../../services/court';
import {
getMatchLookup,
getMatchLookupByCourt,
@@ -78,16 +94,20 @@ function ScheduleRow({
}
function ScheduleColumn({
tournamentId,
court,
matches,
openMatchModal,
stageItemsLookup,
swrCourtsResponse,
matchesLookup,
}: {
tournamentId: number;
court: Court;
matches: MatchInterface[];
openMatchModal: any;
stageItemsLookup: any;
swrCourtsResponse: SWRResponse;
matchesLookup: any;
}) {
const { t } = useTranslation();
@@ -119,7 +139,31 @@ function ScheduleColumn({
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
<div style={{ width: '25rem' }}>
<h4>{court.name}</h4>
<Group justify="space-between">
<Group>
<h4 style={{ marginTop: '0', margin: 'auto' }}>{court.name}</h4>
</Group>
<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={<IconTrash size="1.5rem" />}
onClick={async () => {
await deleteCourt(tournamentId, court.id);
await swrCourtsResponse.mutate();
}}
color="red"
>
{t('delete_court_button')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
{rows}
{noItemsAlert}
{provided.placeholder}
@@ -132,12 +176,16 @@ function ScheduleColumn({
function Schedule({
t,
tournament,
swrCourtsResponse,
stageItemsLookup,
matchesLookup,
schedule,
openMatchModal,
}: {
t: Translator;
tournament: TournamentMinimal;
swrCourtsResponse: SWRResponse;
stageItemsLookup: any;
matchesLookup: any;
schedule: { court: Court; matches: MatchInterface[] }[];
@@ -145,6 +193,8 @@ function Schedule({
}) {
const columns = schedule.map((item) => (
<ScheduleColumn
tournamentId={tournament.id}
swrCourtsResponse={swrCourtsResponse}
stageItemsLookup={stageItemsLookup}
matchesLookup={matchesLookup}
key={item.court.id}
@@ -154,8 +204,26 @@ function Schedule({
/>
));
if (columns.length < 1) {
return <NoContent title={t('no_matches_title')} description={t('no_matches_description')} />;
columns.push(
<div style={{ width: '25rem' }}>
<CourtModal
swrCourtsResponse={swrCourtsResponse}
tournamentId={tournament.id}
buttonSize="xs"
/>
</div>
);
if (columns.length < 2) {
return (
<Stack align="center">
<NoContent title={t('no_courts_title')} description={t('no_courts_description')} />
<CourtModal
swrCourtsResponse={swrCourtsResponse}
tournamentId={tournament.id}
buttonSize="lg"
/>
</Stack>
);
}
return (
@@ -213,21 +281,23 @@ export default function SchedulePage() {
<Title>{t('planning_title')}</Title>
</Grid.Col>
<Grid.Col span={6}>
<Group justify="right">
<Button
color="indigo"
size="md"
variant="filled"
style={{ marginBottom: 10 }}
leftSection={<IconCalendarPlus size={24} />}
onClick={async () => {
await scheduleMatches(tournamentData.id);
await swrStagesResponse.mutate();
}}
>
{t('schedule_description')}
</Button>
</Group>
{data.length < 1 ? null : (
<Group justify="right">
<Button
color="indigo"
size="md"
variant="filled"
style={{ marginBottom: 10 }}
leftSection={<IconCalendarPlus size={24} />}
onClick={async () => {
await scheduleMatches(tournamentData.id);
await swrStagesResponse.mutate();
}}
>
{t('schedule_description')}
</Button>
</Group>
)}
</Grid.Col>
</Grid>
<Group grow mt="1rem">
@@ -245,6 +315,8 @@ export default function SchedulePage() {
>
<Schedule
t={t}
tournament={tournamentData}
swrCourtsResponse={swrCourtsResponse}
schedule={data}
stageItemsLookup={stageItemsLookup}
matchesLookup={matchesLookup}