Skip to content

Commit

Permalink
remade sync effects
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-flynn committed Apr 2, 2024
1 parent d143866 commit 8161771
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 22 deletions.
9 changes: 9 additions & 0 deletions front-end/src/api/use-match-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ export const useMatchAll = (
}
);

export const useMatchesForEvent = (
eventKey: string | null | undefined
): SWRResponse<Match<any>[], ApiResponseError> =>
useSWR<Match<any>[]>(
eventKey ? `match/${eventKey}` : undefined,
(url) => apiFetcher(url, 'GET', undefined, matchZod.array().parse),
{ revalidateOnFocus: false }
);

export const useMatchesForTournament = (
eventKey: string | null | undefined,
tournamentKey: string | null | undefined
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/apps/scorekeeper/hooks/use-start-match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const useMatchStartCallback = () => {
const { canStartMatch, setState } = useMatchControl();
return useRecoilCallback(
() => async () => {
if (canStartMatch) {
if (!canStartMatch) {
throw new Error('Attempted to start match when not allowed.');
}
sendStartMatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { MatchState } from '@toa-lib/models';
import { useMatchStartCallback } from '../hooks/use-start-match';
import { useSnackbar } from 'src/hooks/use-snackbar';
import { LoadingButton } from '@mui/lab';
import { sendAbortMatch } from 'src/api/use-socket';
import { sendAbortMatch, useSocket } from 'src/api/use-socket';
import { useModal } from '@ebay/nice-modal-react';
import AbortDialog from 'src/components/dialogs/AbortDialog';

export const StartMatchButton: FC = () => {
const [loading, setLoading] = useState(false);
const [, connected] = useSocket();
const { canStartMatch, canAbortMatch, setState } = useMatchControl();
const startMatch = useMatchStartCallback();
const { showSnackbar } = useSnackbar();
Expand Down Expand Up @@ -39,7 +40,7 @@ export const StartMatchButton: FC = () => {
color='error'
variant='contained'
onClick={sendStartMatch}
disabled={!canStartMatch || loading}
disabled={!canStartMatch || loading || !connected}
loading={loading}
>
Start Match
Expand Down
11 changes: 8 additions & 3 deletions front-end/src/apps/scorekeeper/match-header/match-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { Box, Chip, Paper, Typography } from '@mui/material';
import { FC } from 'react';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import { MatchTimer } from 'src/components/util/match-timer';
import { useRecoilValue } from 'recoil';
import { matchStatusAtom } from 'src/stores/recoil';
import { useSocket } from 'src/api/use-socket';

export const MatchInfo: FC = () => {
const connected = false;
const matchState = useRecoilValue(matchStatusAtom);
const [, connected] = useSocket();
return (
<Paper sx={{ height: '100%' }}>
<Box
Expand All @@ -17,10 +22,10 @@ export const MatchInfo: FC = () => {
}}
>
<Typography align='center' variant='h4'>
2:30
<MatchTimer />
</Typography>
<Typography gutterBottom align='center' variant='body1'>
UNKNOWN
{matchState}
</Typography>
<Chip
icon={connected ? <CheckCircleOutlineIcon /> : <ErrorOutlineIcon />}
Expand Down
38 changes: 23 additions & 15 deletions front-end/src/apps/scorekeeper/scorekeeper-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@ import { ScorekeeperTabs } from './tabs/scorekeeper-tabs';
import { MatchHeader } from './match-header/match-header';
import { useTeamsForEvent } from 'src/api/use-team-data';
import { Box } from '@mui/material';
import { SyncMatchesToRecoil } from 'src/components/sync-effects/sync-matches-to-recoi';
import { SyncMatchStateToRecoil } from 'src/components/sync-effects/sync-match-state-to-recoil';
import { SyncMatchOccurringToRecoil } from 'src/components/sync-effects/sync-match-occurring-to-recoil';

export const ScorekeeperApp: FC = () => {
const { data: event } = useCurrentEvent();
const { data: teams } = useTeamsForEvent(event?.eventKey);
return (
<DefaultLayout
containerWidth='xl'
title={`${event?.eventName} | Scorekeeper App`}
titleLink={`/${event?.eventKey}`}
>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<MatchHeader teams={teams} />
</Box>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<MatchControl />
</Box>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<ScorekeeperTabs eventKey={event?.eventKey} />
</Box>
</DefaultLayout>
<>
<SyncMatchStateToRecoil />
<SyncMatchesToRecoil />
<SyncMatchOccurringToRecoil />
<DefaultLayout
containerWidth='xl'
title={`${event?.eventName} | Scorekeeper App`}
titleLink={`/${event?.eventKey}`}
>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<MatchHeader teams={teams} />
</Box>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<MatchControl />
</Box>
<Box sx={{ marginBottom: (theme) => theme.spacing(3) }}>
<ScorekeeperTabs eventKey={event?.eventKey} />
</Box>
</DefaultLayout>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Match, MatchSocketEvent, MatchState } from '@toa-lib/models';
import { FC, useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { useSocket } from 'src/api/use-socket';
import { matchOccurringAtom, matchStateAtom } from 'src/stores/recoil';

interface Props {
stopAfterMatchEnd?: boolean;
}

export const SyncMatchOccurringToRecoil: FC<Props> = ({
stopAfterMatchEnd
}) => {
const [socket, connected] = useSocket();

useEffect(() => {
if (connected) {
socket?.on(MatchSocketEvent.UPDATE, onUpdate);
}
}, [connected]);

useEffect(() => {
return () => {
socket?.removeListener(MatchSocketEvent.UPDATE, onUpdate);
};
}, []);

const onUpdate = useRecoilCallback(
({ snapshot, set }) =>
async (newMatch: Match<any>) => {
const state = await snapshot.getPromise(matchStateAtom);
if (stopAfterMatchEnd && state >= MatchState.MATCH_COMPLETE) {
// Don't update anything.
return;
} else {
set(matchOccurringAtom, newMatch);
}
}
);

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MatchSocketEvent, MatchState } from '@toa-lib/models';
import { FC, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { useSocket } from 'src/api/use-socket';
import { matchStatusAtom, matchStateAtom } from 'src/stores/recoil';

export const SyncMatchStateToRecoil: FC = () => {
const setState = useSetRecoilState(matchStateAtom);
const setMode = useSetRecoilState(matchStatusAtom);
const [socket, connected] = useSocket();

useEffect(() => {
if (connected) {
socket?.on(MatchSocketEvent.PRESTART, onMatchPrestart);
socket?.on(MatchSocketEvent.START, onMatchStart);
socket?.on(MatchSocketEvent.END, onMatchEnd);
socket?.on(MatchSocketEvent.ABORT, onMatchAbort);

socket?.on(MatchSocketEvent.TELEOPERATED, onMatchTele);
socket?.on(MatchSocketEvent.ENDGAME, onMatchEndGame);
}
}, [connected]);

useEffect(() => {
return () => {
socket?.off(MatchSocketEvent.PRESTART, onMatchPrestart);
socket?.off(MatchSocketEvent.START, onMatchStart);
socket?.off(MatchSocketEvent.END, onMatchEnd);
socket?.off(MatchSocketEvent.ABORT, onMatchAbort);

socket?.off(MatchSocketEvent.TELEOPERATED, onMatchTele);
socket?.off(MatchSocketEvent.ENDGAME, onMatchEndGame);
};
}, []);

const onMatchPrestart = () => {
setState(MatchState.PRESTART_COMPLETE);
setMode('PRESTART COMPLETE');
};

const onMatchStart = () => {
setState(MatchState.MATCH_IN_PROGRESS);
setMode('MATCH STARTED');
};
const onMatchEnd = () => {
setState(MatchState.MATCH_COMPLETE);
setMode('MATCH COMPLETE');
};
const onMatchAbort = () => {
setState(MatchState.MATCH_ABORTED);
setMode('MATCH ABORTED');
};

const onMatchTele = () => setMode('TELEOPERATED');
const onMatchEndGame = () => setMode('ENDGAME');

return null;
};
19 changes: 19 additions & 0 deletions front-end/src/components/sync-effects/sync-matches-to-recoi.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FC, useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useMatchesForEvent } from 'src/api/use-match-data';
import { currentEventKeyAtom } from 'src/stores/NewRecoil';
import { matchesByEventKeyAtomFam } from 'src/stores/recoil';

export const SyncMatchesToRecoil: FC = () => {
const eventKey = useRecoilValue(currentEventKeyAtom);
const setMatches = useSetRecoilState(
matchesByEventKeyAtomFam(eventKey) ?? ''
);
const { data: matches } = useMatchesForEvent(eventKey);
useEffect(() => {
if (matches) {
setMatches(matches);
}
}, [matches]);
return null;
};
148 changes: 148 additions & 0 deletions front-end/src/components/util/match-timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
FGC_MATCH_CONFIG,
FRC_MATCH_CONFIG,
MatchKey,
MatchSocketEvent,
MatchState,
TimerEventPayload,
getSeasonKeyFromEventKey
} from '@toa-lib/models';
import { Duration } from 'luxon';
import { FC, useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useSocket } from 'src/api/use-socket';
import {
initAudio,
MATCH_START,
MATCH_TELE,
MATCH_TRANSITION,
MATCH_ABORT,
MATCH_ENDGAME,
MATCH_END
} from 'src/apps/AudienceDisplay/Audio';
import {
matchOccurringAtom,
matchStateAtom,
matchTimeAtom,
matchTimeModeAtom,
timer
} from 'src/stores/recoil';

const startAudio = initAudio(MATCH_START);
const transitionAudio = initAudio(MATCH_TRANSITION);
const teleAudio = initAudio(MATCH_TELE);
const abortAudio = initAudio(MATCH_ABORT);
const endgameAudio = initAudio(MATCH_ENDGAME);
const endAudio = initAudio(MATCH_END);

interface Props {
audio?: boolean;
mode?: 'modeTime' | 'timeLeft';
}

export const MatchTimer: FC<Props> = ({ audio, mode = 'timeLeft' }) => {
const matchState = useRecoilValue(matchStateAtom);
const [time, setTime] = useRecoilState(matchTimeAtom);
const [modeTime, setModeTime] = useRecoilState(matchTimeModeAtom);
const currentMatch = useRecoilValue(matchOccurringAtom);
const [socket, connected] = useSocket();

useEffect(() => {
if (connected) {
socket?.on(MatchSocketEvent.PRESTART, onPrestart);
socket?.on(MatchSocketEvent.START, onStart);
socket?.on(MatchSocketEvent.ABORT, onAbort);

timer.on('timer:transition', onTransition);
timer.on('timer:tele', onTele);
timer.on('timer:endgame', onEndgame);
timer.on('timer:end', onEnd);
}
}, [connected]);

useEffect(() => {
if (!timer.inProgress()) {
timer.reset();
setTime(timer.timeLeft);
setModeTime(timer.modeTimeLeft);
}

const tick = setInterval(() => {
setTime(timer.timeLeft);
setModeTime(timer.modeTimeLeft);
}, 500);

return () => {
socket?.off(MatchSocketEvent.PRESTART, onPrestart);
socket?.off(MatchSocketEvent.START, onStart);
socket?.off(MatchSocketEvent.ABORT, onAbort);

timer.off('timer:transition', onTransition);
timer.off('timer:tele', onTele);
timer.off('timer:endgame', onEndgame);
timer.off('timer:end', onEnd);
clearInterval(tick);
};
}, []);

useEffect(() => {
if (matchState === MatchState.MATCH_IN_PROGRESS && timer.inProgress()) {
setTime(timer.timeLeft);
setModeTime(timer.modeTimeLeft);
}
}, [matchState]);

const timeDuration = Duration.fromObject({
seconds: mode === 'timeLeft' ? time : modeTime
});

const onPrestart = (e: MatchKey) => {
timer.reset();

determineTimerConfig(e.eventKey);

setTime(timer.timeLeft);
};

const onStart = () => {
if (audio) startAudio.play();
if (currentMatch) determineTimerConfig(currentMatch.eventKey);
timer.start();
};
const onTransition = (payload: TimerEventPayload) => {
if (audio && payload.allowAudio) transitionAudio.play();
};
const onTele = (payload: TimerEventPayload) => {
if (audio && payload.allowAudio) teleAudio.play();
};
const onAbort = () => {
if (audio) abortAudio.play();
timer.abort();
};
const onEnd = (payload: TimerEventPayload) => {
if (audio && payload.allowAudio) endAudio.play();
timer.stop();
};
const onEndgame = (payload: TimerEventPayload) => {
if (audio && payload.allowAudio) endgameAudio.play();
};

const determineTimerConfig = (eventKeyLike: string) => {
// Get season key frome event key
const seasonKey = getSeasonKeyFromEventKey(eventKeyLike).toLowerCase();

// Set match config based on season key
const matchConfig = seasonKey.includes('frc')
? FRC_MATCH_CONFIG
: FGC_MATCH_CONFIG;
timer.matchConfig = matchConfig;
};

return (
<>
{mode === 'timeLeft'
? timeDuration.toFormat('m:ss')
: timeDuration.toFormat('s')}
</>
);
};
Loading

0 comments on commit 8161771

Please sign in to comment.