From 393ec4a8669b28b5c29a29312b2dc6ae865a6317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se> Date: Mon, 17 May 2021 14:37:15 +0000 Subject: [PATCH] Add synced scoreboard --- client/src/actions/presentation.ts | 5 ++ client/src/actions/types.ts | 1 + client/src/pages/views/AudienceViewPage.tsx | 4 ++ client/src/pages/views/OperatorViewPage.tsx | 57 +++------------ .../src/pages/views/components/Scoreboard.tsx | 69 +++++++++++++++++++ client/src/pages/views/components/Timer.tsx | 13 ---- .../src/reducers/presentationReducer.test.ts | 3 + client/src/reducers/presentationReducer.ts | 7 ++ client/src/sockets.ts | 11 ++- server/app/core/sockets.py | 3 +- 10 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 client/src/pages/views/components/Scoreboard.tsx diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 0faa64a2..17e2e57d 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -40,3 +40,8 @@ export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => export const setPresentationTimer = (timer: TimerState) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer }) } + +/** Set show_scoreboard to input value */ +export const setPresentationShowScoreboard = (show_scoreboard: boolean) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_SHOW_SCOREBOARD, payload: show_scoreboard }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index eca08ccd..59dd672d 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -44,6 +44,7 @@ export default { SET_PRESENTATION_SLIDE_ID: 'SET_PRESENTATION_SLIDE_ID', SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE', SET_PRESENTATION_TIMER: 'SET_PRESENTATION_TIMER', + SET_PRESENTATION_SHOW_SCOREBOARD: 'SET_PRESENTATION_SHOW_SCOREBOARD', // Cities action types SET_CITIES: 'SET_CITIES', diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx index e78db8b0..0024e701 100644 --- a/client/src/pages/views/AudienceViewPage.tsx +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react' import { useAppSelector } from '../../hooks' import { socketConnect } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' +import Scoreboard from './components/Scoreboard' import { PresentationBackground, PresentationContainer } from './styled' const AudienceViewPage: React.FC = () => { @@ -12,6 +13,8 @@ const AudienceViewPage: React.FC = () => { const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id const [successMessageOpen, setSuccessMessageOpen] = useState(true) const competitionName = useAppSelector((state) => state.presentation.competition.name) + const showScoreboard = useAppSelector((state) => state.presentation.show_scoreboard) + useEffect(() => { if (code && code !== '') { socketConnect('Audience') @@ -26,6 +29,7 @@ const AudienceViewPage: React.FC = () => { <Snackbar open={successMessageOpen} autoHideDuration={4000} onClose={() => setSuccessMessageOpen(false)}> <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som åskådare`}</Alert> </Snackbar> + {showScoreboard && <Scoreboard />} </PresentationBackground> ) } diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index 6193b4ce..d99def17 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -7,11 +7,9 @@ import { DialogContent, DialogContentText, DialogTitle, - List, ListItem, ListItemText, makeStyles, - Popover, Snackbar, Theme, Tooltip, @@ -30,10 +28,10 @@ import axios from 'axios' import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { useAppSelector } from '../../hooks' -import { RichTeam } from '../../interfaces/ApiRichModels' import { socketConnect, socketEndPresentation, socketSync } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { Center } from '../presentationEditor/components/styled' +import Scoreboard from './components/Scoreboard' import Timer from './components/Timer' import { OperatorButton, @@ -111,6 +109,8 @@ const OperatorViewPage: React.FC = () => { ) const isFirstSlide = activeSlideOrder === 0 const isLastSlide = useAppSelector((state) => activeSlideOrder === state.presentation.competition.slides.length - 1) + const showScoreboard = useAppSelector((state) => state.presentation.show_scoreboard) + useEffect(() => { socketConnect('Operator') }, []) @@ -121,10 +121,6 @@ const OperatorViewPage: React.FC = () => { endCompetition() } - const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { setOpen(false) setOpenCode(false) @@ -183,15 +179,6 @@ const OperatorViewPage: React.FC = () => { return typeName } - /** Sums the scores for the teams. */ - const addScore = (team: RichTeam) => { - let totalScore = 0 - for (let j = 0; j < team.question_answers.length; j++) { - totalScore = totalScore + team.question_answers[j].score - } - return totalScore - } - const handleStartTimer = () => { if (!slideTimer) return @@ -259,7 +246,6 @@ const OperatorViewPage: React.FC = () => { </Button> </DialogActions> </Dialog> - <OperatorHeader> <Tooltip title="Avsluta tävling" arrow> <OperatorButton onClick={handleVerifyExit} variant="contained" color="secondary"> @@ -301,14 +287,14 @@ const OperatorViewPage: React.FC = () => { <div style={{ height: 0, paddingTop: 140 }} /> <OperatorFooter> <ToolBarContainer> - <Tooltip title="Föregående" arrow> + <Tooltip title="Föregående sida" arrow> <OperatorButton onClick={handleSetPrevSlide} variant="contained" disabled={isFirstSlide}> <ChevronLeftIcon fontSize="large" /> </OperatorButton> </Tooltip> {slideTimer && ( - <Tooltip title="Starta Timer" arrow> + <Tooltip title="Starta timer" arrow> <OperatorButton onClick={handleStartTimer} variant="contained" @@ -320,48 +306,27 @@ const OperatorViewPage: React.FC = () => { </Tooltip> )} - <Tooltip title="Ställning" arrow> - <OperatorButton onClick={handleOpenPopover} variant="contained"> + <Tooltip title="Visa ställning för publik" arrow> + <OperatorButton onClick={() => socketSync({ show_scoreboard: true })} variant="contained"> <AssignmentIcon fontSize="large" /> </OperatorButton> </Tooltip> + {showScoreboard && <Scoreboard isOperator />} - <Tooltip title="Koder" arrow> + <Tooltip title="Visa koder" arrow> <OperatorButton onClick={handleOpenCodes} variant="contained"> <SupervisorAccountIcon fontSize="large" /> </OperatorButton> </Tooltip> - <Tooltip title="Nästa" arrow> + <Tooltip title="Nästa sida" arrow> <OperatorButton onClick={handleSetNextSlide} variant="contained" disabled={isLastSlide}> <ChevronRightIcon fontSize="large" /> </OperatorButton> </Tooltip> </ToolBarContainer> </OperatorFooter> - <Popover - open={Boolean(anchorEl)} - anchorEl={anchorEl} - onClose={handleClose} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'center', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'center', - }} - > - {(!teams || teams.length === 0) && 'Det finns inga lag i denna tävling'} - <List> - {teams && - teams.map((team) => ( - <ListItem key={team.id}> - {team.name} score:{addScore(team)} - </ListItem> - ))} - </List> - </Popover> + <Snackbar open={successMessageOpen && Boolean(competitionName)} autoHideDuration={4000} diff --git a/client/src/pages/views/components/Scoreboard.tsx b/client/src/pages/views/components/Scoreboard.tsx new file mode 100644 index 00000000..7591a3cd --- /dev/null +++ b/client/src/pages/views/components/Scoreboard.tsx @@ -0,0 +1,69 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + List, + ListItem, + ListItemText, +} from '@material-ui/core' +import React from 'react' +import { useAppSelector } from '../../../hooks' +import { RichTeam } from '../../../interfaces/ApiRichModels' +import { socketSync } from '../../../sockets' +import { Center } from '../../presentationEditor/components/styled' + +type ScoreboardProps = { + isOperator?: boolean +} + +const Scoreboard = ({ isOperator }: ScoreboardProps) => { + const teams = useAppSelector((state) => state.presentation.competition.teams) + + /** Sums the scores for the teams. */ + const addScore = (team: RichTeam) => { + let totalScore = 0 + for (let j = 0; j < team.question_answers.length; j++) { + totalScore = totalScore + team.question_answers[j].score + } + return totalScore + } + + return ( + <Dialog open aria-labelledby="max-width-dialog-title" maxWidth="xl"> + <Center> + <DialogTitle id="max-width-dialog-title" style={{ width: '100%' }}> + <h1>Ställning</h1> + </DialogTitle> + </Center> + <DialogContent> + {(!teams || teams.length === 0) && 'Det finns inga lag i denna tävling'} + <List> + {teams && + teams + .sort((a, b) => (addScore(a) < addScore(b) ? 1 : 0)) + .map((team) => ( + <ListItem key={team.id}> + <ListItemText primary={team.name} /> + <ListItemText + primary={`${addScore(team)} poäng`} + style={{ textAlign: 'right', marginLeft: '25px' }} + /> + </ListItem> + ))} + </List> + </DialogContent> + + {isOperator && ( + <DialogActions> + <Button onClick={() => socketSync({ show_scoreboard: false })} color="primary"> + Stäng + </Button> + </DialogActions> + )} + </Dialog> + ) +} + +export default Scoreboard diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx index 620fcd34..e04c8bc1 100644 --- a/client/src/pages/views/components/Timer.tsx +++ b/client/src/pages/views/components/Timer.tsx @@ -21,23 +21,12 @@ const Timer = ({ disableText }: TimerProps) => { }, [slideTimer]) useEffect(() => { - console.log(timer) if (!timer.enabled) { - console.log('interval id: ', timerIntervalId) - console.log('slide timer: ', slideTimer) - if (timerIntervalId !== null) clearInterval(timerIntervalId) - console.log('timer enabled false') - console.log('timer: ', timer) - if (timer.value !== null) { - console.log('timer value not null') - setRemainingTimer(0) } else if (slideTimer) { - console.log('timer value null and slideTimer has value') - setRemainingTimer(slideTimer * 1000) } @@ -46,12 +35,10 @@ const Timer = ({ disableText }: TimerProps) => { setTimerIntervalId( setInterval(() => { - console.log('interval tick') if (timer.value === null) return if (timer.enabled === false && timerIntervalId !== null) clearInterval(timerIntervalId) if (timer.value - Date.now() < 0) { - console.log('timer reached zero') setRemainingTimer(0) dispatch(setPresentationTimer({ ...timer, enabled: false })) if (timerIntervalId !== null) clearInterval(timerIntervalId) diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index 10362e68..638d98d9 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -17,6 +17,7 @@ const initialState = { value: null, enabled: false, }, + show_scoreboard: false, } it('should return the initial state', () => { @@ -44,6 +45,7 @@ it('should handle SET_PRESENTATION_COMPETITION', () => { activeSlideId: initialState.activeSlideId, code: initialState.code, timer: initialState.timer, + show_scoreboard: initialState.show_scoreboard, }) }) @@ -59,5 +61,6 @@ it('should handle SET_PRESENTATION_SLIDE_ID', () => { activeSlideId: testSlideId, code: initialState.code, timer: initialState.timer, + show_scoreboard: initialState.show_scoreboard, }) }) diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index 69c7817f..06baa7d9 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -9,6 +9,7 @@ interface PresentationState { activeSlideId: number code: string timer: TimerState + show_scoreboard: boolean } /** Define the initial values for the presentation state */ @@ -28,6 +29,7 @@ const initialState: PresentationState = { value: null, enabled: false, }, + show_scoreboard: false, } /** Intercept actions for presentation state and update the state */ @@ -53,6 +55,11 @@ export default function (state = initialState, action: AnyAction) { ...state, timer: action.payload as TimerState, } + case Types.SET_PRESENTATION_SHOW_SCOREBOARD: + return { + ...state, + show_scoreboard: action.payload as boolean, + } default: return state } diff --git a/client/src/sockets.ts b/client/src/sockets.ts index 35c9abc6..3c27298f 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -1,11 +1,12 @@ import io from 'socket.io-client' -import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation' +import { setCurrentSlideByOrder, setPresentationShowScoreboard, setPresentationTimer } from './actions/presentation' import { TimerState } from './interfaces/Timer' import store from './store' interface SyncInterface { slide_order?: number timer?: TimerState + show_scoreboard?: boolean } let socket: SocketIOClient.Socket @@ -28,6 +29,7 @@ export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') // The order of these is important, for some reason if (data.timer !== undefined) setPresentationTimer(data.timer)(store.dispatch) if (data.slide_order !== undefined) setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) + if (data.show_scoreboard !== undefined) setPresentationShowScoreboard(data.show_scoreboard)(store.dispatch) }) socket.on('end_presentation', () => { @@ -39,9 +41,6 @@ export const socketEndPresentation = () => { socket.emit('end_presentation') } -export const socketSync = ({ slide_order, timer }: SyncInterface) => { - socket.emit('sync', { - slide_order, - timer, - }) +export const socketSync = (syncData: SyncInterface) => { + socket.emit('sync', syncData) } diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index be9873c7..47af826b 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -72,7 +72,7 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar allowed_views = allowed_views or [] if not _is_allowed(allowed_views, view): - logger.error(f"Won't call function '{f.__name__}': View '{view}' is not {' or '.join(allowed_views)}") + logger.error(f"Won't call function '{f.__name__}': View '{view}' is not '{' or '.join(allowed_views)}'") return return f(*args, **kwargs) @@ -103,6 +103,7 @@ def connect() -> None: "value": None, "enabled": False, }, + "show_scoreboard": False, } logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'") else: -- GitLab