diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 6172466ad..fa8b729ad 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -29,6 +29,8 @@ "auto_create_matches_button": "Add new matches automatically", "back_home_nav": "Take me back to home page", "back_to_login_nav": "Back to login page", + "calculate_datetime_match_label": "Calculate date and time of the match", + "calculate_label": "Calculate", "checkbox_status_checked": "Checked", "checkbox_status_unchecked": "Unchecked", "club_choose_title": "Please choose a club", @@ -141,6 +143,7 @@ "negative_score_validation": "Score cannot be negative", "next_matches_badge": "Next matches", "next_stage_button": "Next Stage", + "next_match_time_label": "Next match time", "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", diff --git a/frontend/src/components/brackets/match.tsx b/frontend/src/components/brackets/match.tsx index d3dfa91a2..697e9d8f7 100644 --- a/frontend/src/components/brackets/match.tsx +++ b/frontend/src/components/brackets/match.tsx @@ -1,4 +1,4 @@ -import { Center, Grid, UnstyledButton, useMantineTheme } from '@mantine/core'; +import { Center, Grid, Text, UnstyledButton, useMantineTheme } from '@mantine/core'; import assert from 'assert'; import React, { useState } from 'react'; import { SWRResponse } from 'swr'; @@ -13,7 +13,7 @@ import { import { TournamentMinimal } from '../../interfaces/tournament'; import { getMatchLookup, getStageItemLookup } from '../../services/lookups'; import MatchModal from '../modals/match_modal'; -import { Time } from '../utils/datetime'; +import { DateTime } from '../utils/datetime'; import classes from './match.module.css'; export function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }) { @@ -30,10 +30,14 @@ export function MatchBadge({ match, theme }: { match: MatchInterface; theme: any }} >
- - {match.court?.name} |{' '} - {match.start_time != null ? +
+ + {match.court?.name} + + + {match.start_time != null ? : null} + +
@@ -128,6 +132,7 @@ export default function Match({ opened={opened} setOpened={setOpened} dynamicSchedule={dynamicSchedule} + priorMatch={null} /> ); diff --git a/frontend/src/components/builder/builder.tsx b/frontend/src/components/builder/builder.tsx index 8ca895922..796b5b69c 100644 --- a/frontend/src/components/builder/builder.tsx +++ b/frontend/src/components/builder/builder.tsx @@ -28,12 +28,10 @@ function StageItemInputSectionLast({ lastInList, }: { input: StageItemInput; - team: TeamInterface | null; - teamStageItem: TeamInterface | null; + team: TeamInterface; + teamStageItem: StageItemWithRounds; lastInList: boolean; }) { - assert(team != null || teamStageItem != null); - const content = team ? team.name : // @ts-ignore @@ -53,7 +51,7 @@ function StageItemRow({ stageItem, swrStagesResponse, }: { - teamsMap: any; + teamsMap: NonNullable>; tournament: Tournament; stageItem: StageItemWithRounds; swrStagesResponse: SWRResponse; @@ -70,12 +68,14 @@ function StageItemRow({ ? stageItemsLookup[input.winner_from_stage_item_id] : null; + assert(team != null || teamStageItem != null); + return ( ); diff --git a/frontend/src/components/modals/match_modal.tsx b/frontend/src/components/modals/match_modal.tsx index e221d8e84..381c8e252 100644 --- a/frontend/src/components/modals/match_modal.tsx +++ b/frontend/src/components/modals/match_modal.tsx @@ -1,7 +1,20 @@ -import { Button, Center, Checkbox, Divider, Grid, Modal, NumberInput, Text } from '@mantine/core'; +import { + Button, + Center, + Checkbox, + Divider, + Grid, + Input, + Modal, + NumberInput, + Text, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; import { useForm } from '@mantine/form'; +import { showNotification } from '@mantine/notifications'; +import { format, fromUnixTime, getUnixTime, parseISO } from 'date-fns'; import { useTranslation } from 'next-i18next'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { SWRResponse } from 'swr'; import { @@ -15,18 +28,33 @@ import { getMatchLookup, getStageItemLookup } from '../../services/lookups'; import { deleteMatch, updateMatch } from '../../services/match'; import DeleteButton from '../buttons/delete'; +interface MatchModalBaseProps { + tournamentData: TournamentMinimal; + swrUpcomingMatchesResponse: SWRResponse | null; + dynamicSchedule: boolean; +} + +interface MatchModalProps extends MatchModalBaseProps { + match: MatchInterface | null; + swrStagesResponse: SWRResponse; + setOpened: (value: boolean) => void; + priorMatch: MatchInterface | null; +} + +/** + * A typical implementation for opening a match modal. Useful for other components, especially in pages. + */ +export type OpenMatchModalFn = (match: MatchInterface, priorMatch: MatchInterface | null) => void; + function MatchDeleteButton({ tournamentData, match, swrRoundsResponse, swrUpcomingMatchesResponse, dynamicSchedule, -}: { - tournamentData: TournamentMinimal; +}: MatchModalBaseProps & { match: MatchInterface; swrRoundsResponse: SWRResponse; - swrUpcomingMatchesResponse: SWRResponse | null; - dynamicSchedule: boolean; }) { const { t } = useTranslation(); if (!dynamicSchedule) return null; @@ -52,14 +80,8 @@ function MatchModalForm({ swrUpcomingMatchesResponse, setOpened, dynamicSchedule, -}: { - tournamentData: TournamentMinimal; - match: MatchInterface | null; - swrStagesResponse: SWRResponse; - swrUpcomingMatchesResponse: SWRResponse | null; - setOpened: any; - dynamicSchedule: boolean; -}) { + priorMatch, +}: MatchModalProps) { if (match == null) { return null; } @@ -90,6 +112,26 @@ function MatchModalForm({ match.custom_margin_minutes != null ); + const [date, setDate] = useState(null); + + const matchDuration = useMemo(() => { + const value = customDurationEnabled + ? form.values.custom_duration_minutes + : match.duration_minutes; + return value ?? 0; + }, [customDurationEnabled, form.values.custom_duration_minutes, match.duration_minutes]); + + const matchMargin = useMemo(() => { + const value = customMarginEnabled ? form.values.custom_margin_minutes : match.margin_minutes; + return value ?? 0; + }, [customMarginEnabled, form.values.custom_margin_minutes, match.margin_minutes]); + + const endDatetime = useMemo( + () => + fromUnixTime(getUnixTime(parseISO(match.start_time)) + matchDuration * 60 + matchMargin * 60), + [match.start_time, matchDuration, matchMargin] + ); + const stageItemsLookup = getStageItemLookup(swrStagesResponse); const matchesLookup = getMatchLookup(swrStagesResponse); @@ -123,19 +165,18 @@ function MatchModalForm({ /> - - - {t('custom_match_duration_label')} - + + {t('minutes')}} placeholder={`${match.duration_minutes}`} @@ -144,6 +185,7 @@ function MatchModalForm({ /> +
- - {t('custom_match_margin_label')} - {t('minutes')}} @@ -170,6 +210,7 @@ function MatchModalForm({ /> +
+ + + {format(endDatetime, 'd LLLL yyyy HH:mm')} + + + + {priorMatch && ( + <> + + + + + + + + + + + + + )} + @@ -205,14 +310,9 @@ export default function MatchModal({ opened, setOpened, dynamicSchedule, -}: { - tournamentData: TournamentMinimal; - match: MatchInterface | null; - swrStagesResponse: SWRResponse; - swrUpcomingMatchesResponse: SWRResponse | null; + priorMatch, +}: MatchModalProps & { opened: boolean; - setOpened: any; - dynamicSchedule: boolean; }) { const { t } = useTranslation(); @@ -226,6 +326,7 @@ export default function MatchModal({ match={match} setOpened={setOpened} dynamicSchedule={dynamicSchedule} + priorMatch={priorMatch} /> diff --git a/frontend/src/components/utils/datetime.tsx b/frontend/src/components/utils/datetime.tsx index a91d2300f..d29f2c0ae 100644 --- a/frontend/src/components/utils/datetime.tsx +++ b/frontend/src/components/utils/datetime.tsx @@ -1,13 +1,25 @@ import { format, parseISO } from 'date-fns'; +export function BareDateTime({ + datetime, + formatStr, + datetimeAttr = datetime instanceof Date ? datetime.toISOString() : datetime.toString(), +}: { + datetime: string | number | Date; + datetimeAttr?: string; + formatStr: string; +}) { + return ; +} + export function DateTime({ datetime }: { datetime: string }) { const date = parseISO(datetime); - return ; + return ; } export function Time({ datetime }: { datetime: string }) { const date = parseISO(datetime); - return ; + return ; } export function formatTime(datetime: string) { diff --git a/frontend/src/components/utils/util.tsx b/frontend/src/components/utils/util.tsx index b57f502e5..97a43c0aa 100644 --- a/frontend/src/components/utils/util.tsx +++ b/frontend/src/components/utils/util.tsx @@ -73,11 +73,17 @@ export function getBaseURL() { return typeof window !== 'undefined' && window.location.origin ? window.location.origin : ''; } -export const groupBy = (keys: any) => (array: any) => - array.reduce((objectsByKeyValue: any, obj: any) => { - const value = keys.map((key: any) => obj[key]).join('-'); +export const groupBy = < + K extends PropertyKey = PropertyKey, + T extends Record = Record, +>( + keys: K[], + array: T[] +) => + array.reduce((objectsByKeyValue: Record, obj) => { + const value = keys.map((key) => obj[key]).join(''); // eslint-disable-next-line no-param-reassign - objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj); + objectsByKeyValue[value] = (objectsByKeyValue[value] ?? []).concat(obj); return objectsByKeyValue; }, {}); diff --git a/frontend/src/interfaces/stage.tsx b/frontend/src/interfaces/stage.tsx index 644251f33..40d0a8bda 100644 --- a/frontend/src/interfaces/stage.tsx +++ b/frontend/src/interfaces/stage.tsx @@ -12,5 +12,7 @@ export interface StageWithStageItems { } export function getStageById(swrStagesResponse: SWRResponse, stageId: number) { - return swrStagesResponse.data.data.filter((stage: StageWithStageItems) => stage.id === stageId); + return (swrStagesResponse.data.data as StageWithStageItems[]).filter( + (stage: StageWithStageItems) => stage.id === stageId + ); } diff --git a/frontend/src/interfaces/stage_item.tsx b/frontend/src/interfaces/stage_item.tsx index 46993c30b..57281e69a 100644 --- a/frontend/src/interfaces/stage_item.tsx +++ b/frontend/src/interfaces/stage_item.tsx @@ -3,7 +3,7 @@ import { StageItemInput } from './stage_item_input'; export interface StageItemWithRounds { id: number; - tournament_id: number; + stage_id: number; created: string; type: string; name: string; diff --git a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx index 9a3470a6a..2c17bfe03 100644 --- a/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx +++ b/frontend/src/pages/tournaments/[id]/dashboard/present/courts.tsx @@ -48,7 +48,7 @@ export default function CourtsPage() { const courts = responseIsValid(swrCourtsResponse) ? swrCourtsResponse.data.data : []; const matchesByCourtId = responseIsValid(swrStagesResponse) ? getMatchLookupByCourt(swrStagesResponse) - : []; + : {}; const rows = courts.map((court: Court) => { const matchesForCourt = matchesByCourtId[court.id] || []; diff --git a/frontend/src/pages/tournaments/[id]/results.tsx b/frontend/src/pages/tournaments/[id]/results.tsx index 312d06d57..832c7b794 100644 --- a/frontend/src/pages/tournaments/[id]/results.tsx +++ b/frontend/src/pages/tournaments/[id]/results.tsx @@ -14,9 +14,9 @@ import { import { IconAlertCircle } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; -import MatchModal from '../../../components/modals/match_modal'; +import MatchModal, { OpenMatchModalFn } from '../../../components/modals/match_modal'; import { NoContentDashboard } from '../../../components/no_content/empty_table_info'; import { Time, formatTime } from '../../../components/utils/datetime'; import { Translator } from '../../../components/utils/types'; @@ -31,11 +31,13 @@ function ScheduleRow({ openMatchModal, stageItemsLookup, matchesLookup, + previousMatch, }: { - data: any; - openMatchModal: any; + data: { match: MatchInterface; stageItem: any }; + openMatchModal: OpenMatchModalFn; stageItemsLookup: any; matchesLookup: any; + previousMatch: MatchInterface | null; }) { const winColor = '#2a8f37'; const drawColor = '#656565'; @@ -62,14 +64,14 @@ function ScheduleRow({ mt="md" pt="0rem" onClick={() => { - openMatchModal(data.match); + openMatchModal(data.match, previousMatch); }} > - {data.match.court.name} + {data.match.court!.name} @@ -147,10 +149,10 @@ function Schedule({ }: { t: Translator; stageItemsLookup: any; - openMatchModal: CallableFunction; - matchesLookup: any; + openMatchModal: OpenMatchModalFn; + matchesLookup: ReturnType; }) { - const matches: any[] = Object.values(matchesLookup); + const matches = Object.values(matchesLookup); const sortedMatches = matches .filter((m1: any) => m1.match.start_time != null) .sort((m1: any, m2: any) => (m1.match.court?.name > m2.match.court?.name ? 1 : -1)) @@ -182,6 +184,7 @@ function Schedule({ openMatchModal={openMatchModal} stageItemsLookup={stageItemsLookup} matchesLookup={matchesLookup} + previousMatch={c > 0 ? sortedMatches[c - 1].match : null} /> ); } @@ -193,7 +196,7 @@ function Schedule({ } const noItemsAlert = - matchesLookup.length < 1 ? ( + Object.getOwnPropertyNames(matchesLookup).length < 1 ? ( } title={t('no_matches_title')} @@ -217,6 +220,7 @@ function Schedule({ export default function SchedulePage() { const [modalOpened, modalSetOpened] = useState(false); const [match, setMatch] = useState(null); + const [priorMatch, setPriorMatch] = useState(null); const { t } = useTranslation(); const { tournamentData } = getTournamentIdFromRouter(); @@ -226,23 +230,20 @@ export default function SchedulePage() { const stageItemsLookup = responseIsValid(swrStagesResponse) ? getStageItemLookup(swrStagesResponse) : []; - const matchesLookup = responseIsValid(swrStagesResponse) ? getMatchLookup(swrStagesResponse) : []; + const matchesLookup = responseIsValid(swrStagesResponse) ? getMatchLookup(swrStagesResponse) : {}; + + const openMatchModal: OpenMatchModalFn = useCallback( + (matchToOpen: MatchInterface, priorMatchToOpen: MatchInterface | null) => { + setMatch(matchToOpen); + setPriorMatch(priorMatchToOpen); + modalSetOpened(true); + }, + [setMatch, setPriorMatch, modalSetOpened] + ); if (!responseIsValid(swrStagesResponse)) return null; if (!responseIsValid(swrCourtsResponse)) return null; - function openMatchModal(matchToOpen: MatchInterface) { - setMatch(matchToOpen); - modalSetOpened(true); - } - - function modalSetOpenedAndUpdateMatch(opened: boolean) { - if (!opened) { - setMatch(null); - } - modalSetOpened(opened); - } - return ( { + if (openend === false) setMatch(null); + modalSetOpened(openend); + }} dynamicSchedule={false} + priorMatch={priorMatch} /> {t('results_title')}
diff --git a/frontend/src/pages/tournaments/[id]/schedule.tsx b/frontend/src/pages/tournaments/[id]/schedule.tsx index 37de256c4..17d1c76b8 100644 --- a/frontend/src/pages/tournaments/[id]/schedule.tsx +++ b/frontend/src/pages/tournaments/[id]/schedule.tsx @@ -1,13 +1,25 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; -import { Alert, Badge, Button, Card, Grid, Group, Stack, Text, Title } from '@mantine/core'; +import { + ActionIcon, + Alert, + Badge, + Button, + Card, + Grid, + Group, + Stack, + Text, + Title, +} from '@mantine/core'; import { IconAlertCircle, IconCalendarPlus } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; +import { BiEditAlt } from 'react-icons/bi'; -import MatchModal from '../../../components/modals/match_modal'; +import MatchModal, { OpenMatchModalFn } from '../../../components/modals/match_modal'; import { NoContent } from '../../../components/no_content/empty_table_info'; -import { Time } from '../../../components/utils/datetime'; +import { DateTime } from '../../../components/utils/datetime'; import { Translator } from '../../../components/utils/types'; import { getTournamentIdFromRouter, responseIsValid } from '../../../components/utils/util'; import { Court } from '../../../interfaces/court'; @@ -29,12 +41,14 @@ function ScheduleRow({ openMatchModal, stageItemsLookup, matchesLookup, + previousMatch, }: { index: number; match: MatchInterface; - openMatchModal: any; + openMatchModal: OpenMatchModalFn; stageItemsLookup: any; matchesLookup: any; + previousMatch: MatchInterface | null; }) { return ( @@ -47,7 +61,7 @@ function ScheduleRow({ withBorder mt="md" onClick={() => { - openMatchModal(match); + openMatchModal(match, previousMatch); }} {...provided.dragHandleProps} > @@ -57,10 +71,15 @@ function ScheduleRow({ {formatMatchTeam2(stageItemsLookup, matchesLookup, match)} - - - {match.start_time != null ? + + + + {match.start_time != null ? : null} + + + + + 0 ? matches[index - 1] : null} /> )); @@ -118,7 +138,7 @@ function ScheduleColumn({ {(provided) => (
-
+

{court.name}

{rows} {noItemsAlert} @@ -141,7 +161,7 @@ function Schedule({ stageItemsLookup: any; matchesLookup: any; schedule: { court: Court; matches: MatchInterface[] }[]; - openMatchModal: CallableFunction; + openMatchModal: OpenMatchModalFn; }) { const columns = schedule.map((item) => ( (null); + const [priorMatch, setPriorMatch] = useState(null); const { t } = useTranslation(); const { tournamentData } = getTournamentIdFromRouter(); @@ -184,17 +205,24 @@ export default function SchedulePage() { const data = responseIsValid(swrCourtsResponse) && responseIsValid(swrStagesResponse) - ? getScheduleData(swrCourtsResponse, matchesByCourtId) + ? getScheduleData( + swrCourtsResponse, + matchesByCourtId as ReturnType + ) : []; + const openMatchModal: OpenMatchModalFn = useCallback( + (matchToOpen: MatchInterface, priorMatchToOpen: MatchInterface | null) => { + setMatch(matchToOpen); + setPriorMatch(priorMatchToOpen); + modalSetOpened(true); + }, + [setMatch, setPriorMatch, modalSetOpened] + ); + if (!responseIsValid(swrStagesResponse)) return null; if (!responseIsValid(swrCourtsResponse)) return null; - function openMatchModal(matchToOpen: MatchInterface) { - setMatch(matchToOpen); - modalSetOpened(true); - } - return ( {match != null ? ( @@ -206,6 +234,7 @@ export default function SchedulePage() { opened={modalOpened} setOpened={modalSetOpened} dynamicSchedule={false} + priorMatch={priorMatch} /> ) : null} diff --git a/frontend/src/pages/tournaments/[id]/swiss/[stage_item_id].tsx b/frontend/src/pages/tournaments/[id]/swiss/[stage_item_id].tsx index c819760cb..bec942525 100644 --- a/frontend/src/pages/tournaments/[id]/swiss/[stage_item_id].tsx +++ b/frontend/src/pages/tournaments/[id]/swiss/[stage_item_id].tsx @@ -18,8 +18,11 @@ import { 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 { StageWithStageItems, getStageById } from '../../../../interfaces/stage'; +import { + StageItemWithRounds, + stageItemIsHandledAutomatically, +} from '../../../../interfaces/stage_item'; import { getTournamentEndpoint } from '../../../../interfaces/tournament'; import { checkForAuthError, @@ -71,12 +74,12 @@ export default function TournamentPage() { const tournamentDataFull = swrTournamentResponse.data != null ? swrTournamentResponse.data.data : null; - let activeStage = null; - let draftRound = null; - let stageItem = null; + let activeStage: StageWithStageItems | null = null; + let draftRound: RoundInterface | null = null; + let stageItem: StageItemWithRounds | null = null; - if (responseIsValid(swrStagesResponse) && stageItemId != null) { - stageItem = getStageItemLookup(swrStagesResponse)[stageItemId]; + if (responseIsValid(swrStagesResponse)) { + stageItem = getStageItemLookup(swrStagesResponse)[stageItemId] ?? null; [activeStage] = getStageById(swrStagesResponse, stageItem.stage_id); if (activeStage != null && activeStage.stage_items != null) { @@ -90,7 +93,11 @@ export default function TournamentPage() { } } - const swrUpcomingMatchesResponse = getUpcomingMatches(id, draftRound?.id, schedulerSettings); + const swrUpcomingMatchesResponse = getUpcomingMatches( + id, + draftRound?.id ?? null, + schedulerSettings + ); const scheduler = draftRound != null && stageItem != null && @@ -163,7 +170,7 @@ export default function TournamentPage() { swrStagesResponse={swrStagesResponse} swrUpcomingMatchesResponse={swrUpcomingMatchesResponse} readOnly={false} - stageItem={stageItem} + stageItem={stageItem!} // TODO: Actually check if stageItem exists before using it, remove this once proper checks are in place displaySettings={displaySettings} /> {scheduler} diff --git a/frontend/src/services/adapter.tsx b/frontend/src/services/adapter.tsx index 4e6015c6a..9c6739505 100644 --- a/frontend/src/services/adapter.tsx +++ b/frontend/src/services/adapter.tsx @@ -187,7 +187,7 @@ export function getUser(): SWRResponse { export function getUpcomingMatches( tournament_id: number, - round_id: number, + round_id: number | null, schedulerSettings: SchedulerSettings ): SWRResponse { return useSWR( diff --git a/frontend/src/services/lookups.tsx b/frontend/src/services/lookups.tsx index 039336ed5..246141555 100644 --- a/frontend/src/services/lookups.tsx +++ b/frontend/src/services/lookups.tsx @@ -5,6 +5,7 @@ import { groupBy, responseIsValid } from '../components/utils/util'; import { Court } from '../interfaces/court'; import { MatchInterface } from '../interfaces/match'; import { StageWithStageItems } from '../interfaces/stage'; +import { StageItemWithRounds } from '../interfaces/stage_item'; import { TeamInterface } from '../interfaces/team'; import { getTeams } from './adapter'; @@ -15,11 +16,13 @@ export function getTeamsLookup(tournamentId: number) { if (!isResponseValid) { return null; } - return Object.fromEntries(swrTeamsResponse.data.data.teams.map((x: TeamInterface) => [x.id, x])); + return Object.fromEntries( + (swrTeamsResponse.data.data.teams as TeamInterface[]).map((x: TeamInterface) => [x.id, x]) + ); } export function getStageItemLookup(swrStagesResponse: SWRResponse) { - let result: any[] = []; + let result: [number, StageItemWithRounds][] = []; swrStagesResponse.data.data.map((stage: StageWithStageItems) => stage.stage_items.forEach((stage_item) => { @@ -30,7 +33,7 @@ export function getStageItemLookup(swrStagesResponse: SWRResponse) { } export function getStageItemList(swrStagesResponse: SWRResponse) { - let result: any[] = []; + let result: [StageItemWithRounds][] = []; swrStagesResponse.data.data.map((stage: StageWithStageItems) => stage.stage_items.forEach((stage_item) => { @@ -41,7 +44,7 @@ export function getStageItemList(swrStagesResponse: SWRResponse) { } export function getStageItemTeamIdsLookup(swrStagesResponse: SWRResponse) { - let result: any[] = []; + let result: [number, (number | null)[]][] = []; swrStagesResponse.data.data.map((stage: StageWithStageItems) => stage.stage_items.forEach((stageItem) => { @@ -79,7 +82,7 @@ export function getStageItemTeamsLookup( } export function getMatchLookup(swrStagesResponse: SWRResponse) { - let result: any[] = []; + let result: [number, { match: MatchInterface; stageItem: StageItemWithRounds }][] = []; swrStagesResponse.data.data.map((stage: StageWithStageItems) => stage.stage_items.forEach((stageItem) => { @@ -117,12 +120,12 @@ export function stringToColour(input: string) { export function getMatchLookupByCourt(swrStagesResponse: SWRResponse) { const matches = Object.values(getMatchLookup(swrStagesResponse)).map((x) => x.match); - return groupBy(['court_id'])(matches); + return groupBy(['court_id'], matches); } export function getScheduleData( swrCourtsResponse: SWRResponse, - matchesByCourtId: any + matchesByCourtId: ReturnType> ): { court: Court; matches: MatchInterface[] }[] { return swrCourtsResponse.data.data.map((court: Court) => ({ matches: (matchesByCourtId[court.id] || [])