From 4cff23cbf788fa7c72bfca9865e9b31b5162d05d Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Sun, 9 May 2021 20:19:03 +0000 Subject: [PATCH] Resolve "Minor fixes" --- client/src/actions/competitionLogin.ts | 8 +- client/src/actions/competitions.ts | 5 + client/src/actions/presentation.test.ts | 36 ++--- client/src/actions/presentation.ts | 25 +--- client/src/actions/types.ts | 6 +- client/src/pages/admin/AdminPage.tsx | 27 +++- .../admin/competitions/AddCompetition.tsx | 5 +- .../admin/competitions/CompetitionManager.tsx | 9 +- client/src/pages/admin/regions/AddRegion.tsx | 5 +- client/src/pages/admin/regions/Regions.tsx | 18 +-- client/src/pages/admin/users/AddUser.tsx | 10 +- client/src/pages/login/styled.tsx | 1 + .../PresentationEditorPage.tsx | 4 +- .../components/CompetitionSettings.tsx | 18 +-- .../components/ImageComponentDisplay.tsx | 2 + .../components/QuestionComponentDisplay.tsx | 7 +- .../components/RndComponent.tsx | 2 +- .../components/SlideDisplay.tsx | 37 +++-- .../answerComponents/AnswerText.tsx | 7 +- .../slideSettingsComponents/Timer.tsx | 4 +- client/src/pages/views/AudienceViewPage.tsx | 12 +- client/src/pages/views/JudgeViewPage.tsx | 59 +++++--- client/src/pages/views/OperatorViewPage.tsx | 28 +++- client/src/pages/views/TeamViewPage.tsx | 15 +- .../views/components/JudgeScoreDisplay.tsx | 9 +- .../components/JudgeScoringInstructions.tsx | 1 + .../components/PresentationComponent.tsx | 5 +- client/src/pages/views/components/Timer.tsx | 28 ++-- client/src/reducers/editorReducer.ts | 5 + .../src/reducers/presentationReducer.test.ts | 138 +----------------- client/src/reducers/presentationReducer.ts | 39 +---- client/src/sockets.ts | 18 ++- client/src/utils/SecureRoute.tsx | 2 +- .../checkAuthenticationCompetition.test.ts | 14 +- .../utils/checkAuthenticationCompetition.ts | 12 +- 35 files changed, 263 insertions(+), 358 deletions(-) diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts index d9f28333..cfc222b3 100644 --- a/client/src/actions/competitionLogin.ts +++ b/client/src/actions/competitionLogin.ts @@ -18,7 +18,7 @@ export const loginCompetition = (code: string, history: History, redirect: boole .post('/api/auth/login/code', { code }) .then((res) => { const token = `Bearer ${res.data.access_token}` - localStorage.setItem('competitionToken', token) //setting token to local storage + localStorage.setItem(`${res.data.view}Token`, token) //setting token to local storage axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios dispatch({ type: Types.CLEAR_COMPETITION_LOGIN_ERRORS }) // no error dispatch({ @@ -41,8 +41,10 @@ export const loginCompetition = (code: string, history: History, redirect: boole } // Log out from competition and remove jwt token from local storage and axios -export const logoutCompetition = () => async (dispatch: AppDispatch) => { - localStorage.removeItem('competitionToken') +export const logoutCompetition = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => async ( + dispatch: AppDispatch +) => { + localStorage.removeItem(`${role}Token`) await axios.post('/api/auth/logout').then(() => { delete axios.defaults.headers.common['Authorization'] dispatch({ diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index bae48138..64fd11e1 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -44,3 +44,8 @@ export const getCompetitions = () => async (dispatch: AppDispatch, getState: () export const setFilterParams = (params: CompetitionFilterParams) => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_COMPETITIONS_FILTER_PARAMS, payload: params }) } + +// DIspatch action to set loading +export const setEditorLoading = (loading: boolean) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_EDITOR_LOADING, payload: loading }) +} diff --git a/client/src/actions/presentation.test.ts b/client/src/actions/presentation.test.ts index 095a3529..9fbc2a0d 100644 --- a/client/src/actions/presentation.test.ts +++ b/client/src/actions/presentation.test.ts @@ -3,12 +3,7 @@ import expect from 'expect' // You can use any testing library import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Slide } from '../interfaces/ApiModels' -import { - getPresentationCompetition, - setCurrentSlide, - setCurrentSlideNext, - setCurrentSlidePrevious, -} from './presentation' +import { getPresentationCompetition, setCurrentSlideByOrder } from './presentation' import Types from './types' const middlewares = [thunk] @@ -26,23 +21,16 @@ it('dispatches no actions when failing to get competitions', async () => { }) it('dispatches correct actions when setting slide', () => { - const testSlide: Slide = { competition_id: 0, id: 5, order: 5, timer: 20, title: '', background_image: undefined } - const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE, payload: testSlide }] - const store = mockStore({}) - setCurrentSlide(testSlide)(store.dispatch) - expect(store.getActions()).toEqual(expectedActions) -}) - -it('dispatches correct actions when setting previous slide', () => { - const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE_PREVIOUS }] - const store = mockStore({}) - setCurrentSlidePrevious()(store.dispatch) - expect(store.getActions()).toEqual(expectedActions) -}) - -it('dispatches correct actions when setting next slide', () => { - const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE_NEXT }] - const store = mockStore({}) - setCurrentSlideNext()(store.dispatch) + const testSlide: Slide = { + competition_id: 0, + id: 123123, + order: 43523, + timer: 20, + title: '', + background_image: undefined, + } + const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE_ID, payload: testSlide.id }] + const store = mockStore({ presentation: { competition: { id: 2, slides: [testSlide] } } }) + setCurrentSlideByOrder(testSlide.order)(store.dispatch, store.getState as any) expect(store.getActions()).toEqual(expectedActions) }) diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 07536b21..0c3f2466 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -3,7 +3,6 @@ This file handles actions for the presentation redux state */ import axios from 'axios' -import { Slide } from '../interfaces/ApiModels' import { Timer } from '../interfaces/Timer' import store, { AppDispatch, RootState } from './../store' import Types from './types' @@ -17,8 +16,8 @@ export const getPresentationCompetition = (id: string) => async (dispatch: AppDi type: Types.SET_PRESENTATION_COMPETITION, payload: res.data, }) - if (getState().presentation?.slide.id === -1 && res.data?.slides[0]) { - setCurrentSlideByOrder(0)(dispatch) + if (getState().presentation?.activeSlideId === -1 && res.data?.slides[0]) { + setCurrentSlideByOrder(0)(dispatch, getState) } }) .catch((err) => { @@ -26,24 +25,10 @@ export const getPresentationCompetition = (id: string) => async (dispatch: AppDi }) } -/** Set presentation slide using input slide id */ -export const setCurrentSlide = (slide: Slide) => (dispatch: AppDispatch) => { - dispatch({ type: Types.SET_PRESENTATION_SLIDE, payload: slide }) -} - -/** Set presentation slide to previous slide in list */ -export const setCurrentSlidePrevious = () => (dispatch: AppDispatch) => { - dispatch({ type: Types.SET_PRESENTATION_SLIDE_PREVIOUS }) -} - -/** Set presentation slide to next slide in list */ -export const setCurrentSlideNext = () => (dispatch: AppDispatch) => { - dispatch({ type: Types.SET_PRESENTATION_SLIDE_NEXT }) -} - /** Set presentation slide using input order */ -export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch) => { - dispatch({ type: Types.SET_PRESENTATION_SLIDE_BY_ORDER, payload: order }) +export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch, getState: () => RootState) => { + const slideId = getState().presentation.competition.slides.find((slide) => slide.order === order)?.id + dispatch({ type: Types.SET_PRESENTATION_SLIDE_ID, payload: slideId }) } /** Set code of presentation */ diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index c73179c1..eca08ccd 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -37,13 +37,11 @@ export default { SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION', SET_EDITOR_SLIDE_ID: 'SET_EDITOR_SLIDE_ID', SET_EDITOR_VIEW_ID: 'SET_EDITOR_VIEW_ID', + SET_EDITOR_LOADING: 'SET_EDITOR_LOADING', // Presentation action types SET_PRESENTATION_COMPETITION: 'SET_PRESENTATION_COMPETITION', - SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE', - SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS', - SET_PRESENTATION_SLIDE_NEXT: 'SET_PRESENTATION_SLIDE_NEXT', - SET_PRESENTATION_SLIDE_BY_ORDER: 'SET_PRESENTATION_SLIDE_BY_ORDER', + SET_PRESENTATION_SLIDE_ID: 'SET_PRESENTATION_SLIDE_ID', SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE', SET_PRESENTATION_TIMER: 'SET_PRESENTATION_TIMER', diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 3a375210..8d193557 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -19,6 +19,7 @@ import SettingsOverscanIcon from '@material-ui/icons/SettingsOverscan' import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { getCities } from '../../actions/cities' +import { setEditorLoading } from '../../actions/competitions' import { getRoles } from '../../actions/roles' import { getStatistics } from '../../actions/statistics' import { getTypes } from '../../actions/typesAction' @@ -73,6 +74,7 @@ const AdminView: React.FC = () => { dispatch(getRoles()) dispatch(getTypes()) dispatch(getStatistics()) + dispatch(setEditorLoading(true)) }, []) const menuAdminItems = [ @@ -87,6 +89,21 @@ const AdminView: React.FC = () => { { text: 'Tävlingshanterare', icon: SettingsOverscanIcon }, ] + const getPathName = (tabName: string) => { + switch (tabName) { + case 'Startsida': + return 'dashboard' + case 'Regioner': + return 'regions' + case 'Användare': + return 'users' + case 'Tävlingshanterare': + return 'competition-manager' + default: + return '' + } + } + const renderItems = () => { const menuItems = isAdmin ? menuAdminItems : menuEditorItems return menuItems.map((value, index) => ( @@ -95,7 +112,7 @@ const AdminView: React.FC = () => { button component={Link} key={value.text} - to={`${url}/${value.text.toLowerCase()}`} + to={`${url}/${getPathName(value.text)}`} selected={index === openIndex} onClick={() => setOpenIndex(index)} > @@ -147,16 +164,16 @@ const AdminView: React.FC = () => { <main className={classes.content}> <div className={classes.toolbar} /> <Switch> - <Route exact path={[path, `${path}/startsida`]}> + <Route exact path={[path, `${path}/dashboard`]}> <Dashboard /> </Route> - <Route path={`${path}/regioner`}> + <Route path={`${path}/regions`}> <RegionManager /> </Route> - <Route path={`${path}/användare`}> + <Route path={`${path}/users`}> <UserManager /> </Route> - <Route path={`${path}/tävlingshanterare`}> + <Route path={`${path}/competition-manager`}> <CompetitionManager /> </Route> </Switch> diff --git a/client/src/pages/admin/competitions/AddCompetition.tsx b/client/src/pages/admin/competitions/AddCompetition.tsx index e404b112..165d17d8 100644 --- a/client/src/pages/admin/competitions/AddCompetition.tsx +++ b/client/src/pages/admin/competitions/AddCompetition.tsx @@ -1,4 +1,5 @@ import { Button, FormControl, InputLabel, MenuItem, Popover, TextField } from '@material-ui/core' +import PostAddIcon from '@material-ui/icons/PostAdd' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' @@ -89,10 +90,10 @@ const AddCompetition: React.FC = (props: any) => { <div> <AddButton data-testid="addCompetition" - style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} - color="default" + color="secondary" variant="contained" onClick={handleClick} + endIcon={<PostAddIcon />} > Ny Tävling </AddButton> diff --git a/client/src/pages/admin/competitions/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx index cf5332a0..05451028 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -38,6 +38,7 @@ import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' import { Team } from '../../../interfaces/ApiModels' import { CompetitionFilterParams } from '../../../interfaces/FilterParams' +import { Center } from '../../presentationEditor/components/styled' import { FilterContainer, RemoveMenuItem, TopBar, YearFilterTextField } from '../styledComp' import AddCompetition from './AddCompetition' @@ -384,9 +385,11 @@ const CompetitionManager: React.FC = (props: any) => { fullWidth={false} fullScreen={false} > - <DialogTitle id="max-width-dialog-title" className={classes.paper}> - Koder för {competitionName} - </DialogTitle> + <Center> + <DialogTitle id="max-width-dialog-title" className={classes.paper} style={{ width: '100%' }}> + Koder för {competitionName} + </DialogTitle> + </Center> <DialogContent> {/* <DialogContentText>Här visas tävlingskoderna till den valda tävlingen.</DialogContentText> */} {codes.map((code) => ( diff --git a/client/src/pages/admin/regions/AddRegion.tsx b/client/src/pages/admin/regions/AddRegion.tsx index 10cb22bc..14efed4e 100644 --- a/client/src/pages/admin/regions/AddRegion.tsx +++ b/client/src/pages/admin/regions/AddRegion.tsx @@ -90,13 +90,12 @@ const AddRegion: React.FC = (props: any) => { /> <AddButton data-testid="regionSubmitButton" - style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} className={classes.button} - color="default" + color="secondary" variant="contained" type="submit" > - <AddIcon></AddIcon> + <AddIcon /> </AddButton> </Grid> </FormControl> diff --git a/client/src/pages/admin/regions/Regions.tsx b/client/src/pages/admin/regions/Regions.tsx index 5e2531bb..57f4c101 100644 --- a/client/src/pages/admin/regions/Regions.tsx +++ b/client/src/pages/admin/regions/Regions.tsx @@ -64,26 +64,10 @@ const RegionManager: React.FC = (props: any) => { setActiveId(id) } - const handleAddCity = async () => { - await axios - .post(`/api/misc/cities`, { name: newCity }) - .then(() => { - setAnchorEl(null) - dispatch(getCities()) - }) - .catch(({ response }) => { - console.warn(response.data) - }) - } - - const handleChange = (event: any) => { - setNewCity(event.target.value) - } - return ( <div> <TopBar> - <AddRegion></AddRegion> + <AddRegion /> </TopBar> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> diff --git a/client/src/pages/admin/users/AddUser.tsx b/client/src/pages/admin/users/AddUser.tsx index 63549edc..2634751d 100644 --- a/client/src/pages/admin/users/AddUser.tsx +++ b/client/src/pages/admin/users/AddUser.tsx @@ -24,10 +24,7 @@ const userSchema: Yup.SchemaOf<formType> = Yup.object({ .shape({ name: Yup.string(), email: Yup.string().email().required('Email krävs'), - password: Yup.string() - .required('Lösenord krävs.') - .min(6, 'Lösenord måste vara minst 6 tecken.') - .matches(/[a-zA-Z]/, 'Lösenord får enbart innehålla a-z, A-Z.'), + password: Yup.string().required('Lösenord krävs.').min(6, 'Lösenord måste vara minst 6 tecken.'), role: Yup.string().required('Roll krävs').notOneOf([noCitySelected], 'Välj en roll'), city: Yup.string().required('Stad krävs').notOneOf([noRoleSelected], 'Välj en stad'), }) @@ -88,11 +85,10 @@ const AddUser: React.FC = (props: any) => { <div> <AddButton data-testid="addUserButton" - style={{ backgroundColor: '#4caf50', color: '#fcfcfc' }} - color="default" + color="secondary" variant="contained" onClick={handleClick} - endIcon={<PersonAddIcon></PersonAddIcon>} + endIcon={<PersonAddIcon />} > Ny Användare </AddButton> diff --git a/client/src/pages/login/styled.tsx b/client/src/pages/login/styled.tsx index c071747e..73c75d54 100644 --- a/client/src/pages/login/styled.tsx +++ b/client/src/pages/login/styled.tsx @@ -2,6 +2,7 @@ import { Paper } from '@material-ui/core' import styled from 'styled-components' export const LoginPageContainer = styled.div` + padding-top: 5%; display: flex; justify-content: center; ` diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index d84944b4..d3286065 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -68,6 +68,7 @@ const PresentationEditorPage: React.FC = () => { const setActiveSlideId = (id: number) => { dispatch(setEditorSlideId(id)) + dispatch(getEditorCompetition(competitionId)) } const createNewSlide = async () => { @@ -114,6 +115,7 @@ const PresentationEditorPage: React.FC = () => { if (clickedViewTypeId) { dispatch(setEditorViewId(clickedViewTypeId)) } + dispatch(getEditorCompetition(competitionId)) } const onDragEnd = async (result: DropResult) => { @@ -138,7 +140,7 @@ const PresentationEditorPage: React.FC = () => { <CssBaseline /> <AppBarEditor $leftDrawerWidth={leftDrawerWidth} $rightDrawerWidth={rightDrawerWidth} position="fixed"> <ToolBarContainer> - <Button component={Link} to="/admin/tävlingshanterare" style={{ padding: 0 }}> + <Button component={Link} to="/admin/competition-manager" style={{ padding: 0 }}> <HomeIcon src="/t8.png" /> </Button> <CompetitionName variant="h5" noWrap> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index 9482b660..fd63a4b3 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -1,23 +1,13 @@ -import { - Divider, - FormControl, - InputLabel, - ListItem, - ListItemText, - MenuItem, - Select, - TextField, - Typography, -} from '@material-ui/core' -import { Center, ImportedImage, SettingsList, PanelContainer, FirstItem, AddButton } from './styled' +import { Divider, FormControl, InputLabel, ListItem, MenuItem, Select, TextField, Typography } from '@material-ui/core' import axios from 'axios' -import React, { useState } from 'react' +import React from 'react' import { useParams } from 'react-router-dom' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' import { City } from '../../../interfaces/ApiModels' -import Teams from './Teams' import BackgroundImageSelect from './BackgroundImageSelect' +import { FirstItem, PanelContainer, SettingsList } from './styled' +import Teams from './Teams' interface CompetitionParams { competitionId: string diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx index e783bdb2..273748f3 100644 --- a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx @@ -13,6 +13,8 @@ const ImageComponentDisplay = ({ component, width, height }: ImageComponentProps src={`http://localhost:5000/static/images/${component.media?.filename}`} height={height} width={width} + // Make sure the border looks good all around the image + style={{ paddingRight: 2, paddingBottom: 2 }} draggable={false} /> ) diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx index dd8e2fa8..b43766eb 100644 --- a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -8,13 +8,16 @@ import { Center } from './styled' type QuestionComponentProps = { variant: 'editor' | 'presentation' + currentSlideId?: number } -const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { +const QuestionComponentDisplay = ({ variant, currentSlideId }: QuestionComponentProps) => { const activeSlide = useAppSelector((state) => { + if (variant === 'presentation' && currentSlideId) + return state.presentation.competition.slides.find((slide) => slide.id === currentSlideId) if (variant === 'editor') return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId) - return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id) + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId) }) const timer = activeSlide?.timer diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index 4c092800..3b40ad74 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -172,7 +172,7 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => }} > {hover && ( - <Card elevation={6} style={{ position: 'absolute' }}> + <Card elevation={6} style={{ position: 'absolute', zIndex: 10 }}> <Tooltip title="Centrera horisontellt"> <IconButton onClick={handleCenterHorizontal}>X</IconButton> </Tooltip> diff --git a/client/src/pages/presentationEditor/components/SlideDisplay.tsx b/client/src/pages/presentationEditor/components/SlideDisplay.tsx index e6b60f29..9fd6ed70 100644 --- a/client/src/pages/presentationEditor/components/SlideDisplay.tsx +++ b/client/src/pages/presentationEditor/components/SlideDisplay.tsx @@ -1,33 +1,34 @@ +import { Typography } from '@material-ui/core' import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import { getTypes } from '../../../actions/typesAction' import { useAppDispatch, useAppSelector } from '../../../hooks' import PresentationComponent from '../../views/components/PresentationComponent' import RndComponent from './RndComponent' -import { SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled' +import { Center, SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled' type SlideDisplayProps = { //Prop to distinguish between editor and active competition variant: 'editor' | 'presentation' activeViewTypeId: number + //Can be used to force what slide it it's displaying (currently used in judge view) + currentSlideId?: number } -const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { - const components = useAppSelector((state) => { +const SlideDisplay = ({ variant, activeViewTypeId, currentSlideId }: SlideDisplayProps) => { + const slide = useAppSelector((state) => { + if (currentSlideId && variant === 'presentation') + return state.presentation.competition.slides.find((slide) => slide.id === currentSlideId) if (variant === 'editor') - return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.components - return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id)?.components + return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId) + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId) }) + const components = slide?.components const competitionBackgroundImage = useAppSelector((state) => { if (variant === 'editor') return state.editor.competition.background_image return state.presentation.competition.background_image }) - const slideBackgroundImage = useAppSelector((state) => { - if (variant === 'editor') - return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.background_image - return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide.id) - ?.background_image - }) + const slideBackgroundImage = slide?.background_image const dispatch = useAppDispatch() const editorPaperRef = useRef<HTMLDivElement>(null) const [width, setWidth] = useState(0) @@ -77,8 +78,20 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { scale={scale} /> ) - return <PresentationComponent key={component.id} component={component} scale={scale} /> + return ( + <PresentationComponent + key={component.id} + component={component} + scale={scale} + currentSlideId={currentSlideId} + /> + ) })} + {!slide && ( + <Center> + <Typography variant="body2"> Ingen sida är vald, välj en i vänstermenyn eller skapa en ny.</Typography> + </Center> + )} </SlideEditorPaper> </SlideEditorContainerRatio> </SlideEditorContainer> diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx index e54b0dd2..465bffcb 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx @@ -1,7 +1,7 @@ import { ListItem, ListItemText, TextField } from '@material-ui/core' import axios from 'axios' import React from 'react' -import { getEditorCompetition } from '../../../../actions/editor' +import { getPresentationCompetition } from '../../../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../../../hooks' import { RichSlide } from '../../../../interfaces/ApiRichModels' import { Center } from '../styled' @@ -29,13 +29,14 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { const updateAnswer = async (answer: string) => { if (activeSlide && team) { + console.log(team.question_answers) if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { await axios .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { answer, }) .then(() => { - dispatch(getEditorCompetition(competitionId)) + dispatch(getPresentationCompetition(competitionId)) }) .catch(console.log) } else { @@ -46,7 +47,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { question_id: activeSlide.questions[0].id, }) .then(() => { - dispatch(getEditorCompetition(competitionId)) + dispatch(getPresentationCompetition(competitionId)) }) .catch(console.log) } diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx index 91825662..a633efec 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx @@ -17,7 +17,7 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => { setTimer(+event.target.value) if (activeSlide) { await axios - .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: event.target.value }) + .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: event.target.value || null }) .then(() => { dispatch(getEditorCompetition(competitionId)) }) @@ -40,7 +40,7 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => { label="Timer" type="number" onChange={updateTimer} - value={timer} + value={timer || ''} /> </Center> </ListItem> diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx index 28280c90..af1f18b3 100644 --- a/client/src/pages/views/AudienceViewPage.tsx +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -1,5 +1,6 @@ -import { Typography } from '@material-ui/core' -import React, { useEffect } from 'react' +import { Snackbar, Typography } from '@material-ui/core' +import { Alert } from '@material-ui/lab' +import React, { useEffect, useState } from 'react' import { useAppSelector } from '../../hooks' import { socketConnect, socketJoinPresentation } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' @@ -9,9 +10,11 @@ const AudienceViewPage: React.FC = () => { const code = useAppSelector((state) => state.presentation.code) const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id + const [successMessageOpen, setSuccessMessageOpen] = useState(true) + const competitionName = useAppSelector((state) => state.presentation.competition.name) useEffect(() => { if (code && code !== '') { - socketConnect() + socketConnect('Audience') socketJoinPresentation() } }, []) @@ -21,6 +24,9 @@ const AudienceViewPage: React.FC = () => { <PresentationContainer> <SlideDisplay variant="presentation" activeViewTypeId={activeViewTypeId} /> </PresentationContainer> + <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> </PresentationBackground> ) } diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 81f70edb..a28348a7 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -1,11 +1,11 @@ -import { Divider, List, ListItemText, Typography } from '@material-ui/core' +import { Divider, List, ListItemText, Snackbar, Typography } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import { Alert } from '@material-ui/lab' import React, { useEffect, useState } from 'react' -import { useHistory, useParams } from 'react-router-dom' -import { getPresentationCompetition, setCurrentSlide } from '../../actions/presentation' +import { getPresentationCompetition } from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' -import { ViewParams } from '../../interfaces/ViewParams' -import { socketConnect } from '../../sockets' +import { RichSlide } from '../../interfaces/ApiRichModels' +import { socketConnect, socketJoinPresentation } from '../../sockets' import { renderSlideIcon } from '../../utils/renderSlideIcon' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { SlideListItem } from '../presentationEditor/styled' @@ -42,29 +42,46 @@ const useStyles = makeStyles((theme: Theme) => const JudgeViewPage: React.FC = () => { const classes = useStyles() - const history = useHistory() const dispatch = useAppDispatch() - const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0) const viewTypes = useAppSelector((state) => state.types.viewTypes) + const code = useAppSelector((state) => state.presentation.code) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Team')?.id const teams = useAppSelector((state) => state.presentation.competition.teams) const slides = useAppSelector((state) => state.presentation.competition.slides) - const currentQuestion = slides[activeSlideIndex]?.questions[0] - const { competitionId }: ViewParams = useParams() + const [successMessageOpen, setSuccessMessageOpen] = useState(true) + const competitionName = useAppSelector((state) => state.presentation.competition.name) + const [currentSlide, setCurrentSlide] = useState<RichSlide | undefined>(undefined) + const currentQuestion = currentSlide?.questions[0] + const operatorActiveSlideId = useAppSelector((state) => state.presentation.activeSlideId) + const operatorActiveSlideOrder = useAppSelector( + (state) => state.presentation.competition.slides.find((slide) => slide.id === operatorActiveSlideId)?.order + ) + const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id) const handleSelectSlide = (index: number) => { - setActiveSlideIndex(index) - dispatch(setCurrentSlide(slides[index])) + setCurrentSlide(slides[index]) } useEffect(() => { - socketConnect() - dispatch(getPresentationCompetition(competitionId)) + if (code && code !== '') { + socketConnect('Judge') + socketJoinPresentation() + } }, []) - + useEffect(() => { + if (!currentSlide) setCurrentSlide(slides?.[0]) + }, [slides]) + useEffect(() => { + if (competitionId) { + dispatch(getPresentationCompetition(competitionId.toString())) + } + }, [operatorActiveSlideId]) return ( <div style={{ height: '100%' }}> <JudgeAppBar position="fixed"> <JudgeToolbar> <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel> + {operatorActiveSlideOrder !== undefined && ( + <Typography variant="h5">Operatör är på sida: {operatorActiveSlideOrder + 1}</Typography> + )} <JudgeAnswersLabel variant="h5">Svar</JudgeAnswersLabel> </JudgeToolbar> </JudgeAppBar> @@ -80,11 +97,12 @@ const JudgeViewPage: React.FC = () => { <List> {slides.map((slide, index) => ( <SlideListItem - selected={index === activeSlideIndex} + selected={slide.order === currentSlide?.order} onClick={() => handleSelectSlide(index)} divider button key={slide.id} + style={{ border: 2, borderStyle: slide.id === operatorActiveSlideId ? 'dashed' : 'none' }} > {renderSlideIcon(slide)} <ListItemText primary={`Sida ${slide.order + 1}`} /> @@ -111,20 +129,25 @@ const JudgeViewPage: React.FC = () => { {teams && teams.map((answer, index) => ( <div key={answer.name}> - <JudgeScoreDisplay teamIndex={index} /> + {currentSlide && <JudgeScoreDisplay teamIndex={index} activeSlide={currentSlide} />} <Divider /> </div> ))} </List> <ScoreFooterPadding /> - <JudgeScoringInstructions question={currentQuestion} /> + {currentQuestion && <JudgeScoringInstructions question={currentQuestion} />} </RightDrawer> <div className={classes.toolbar} /> <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> <InnerContent> - {activeViewTypeId && <SlideDisplay variant="presentation" activeViewTypeId={activeViewTypeId} />} + {activeViewTypeId && currentSlide && ( + <SlideDisplay variant="presentation" currentSlideId={currentSlide.id} activeViewTypeId={activeViewTypeId} /> + )} </InnerContent> </Content> + <Snackbar open={successMessageOpen} autoHideDuration={4000} onClose={() => setSuccessMessageOpen(false)}> + <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som domare`}</Alert> + </Snackbar> </div> ) } diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index 8bd782f7..5bb96fcb 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -12,6 +12,7 @@ import { ListItemText, makeStyles, Popover, + Snackbar, Theme, Tooltip, Typography, @@ -24,8 +25,9 @@ import FileCopyIcon from '@material-ui/icons/FileCopy' import LinkIcon from '@material-ui/icons/Link' import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount' import TimerIcon from '@material-ui/icons/Timer' +import { Alert } from '@material-ui/lab' import axios from 'axios' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { useAppSelector } from '../../hooks' import { RichTeam } from '../../interfaces/ApiRichModels' @@ -39,6 +41,7 @@ import { socketStartTimer, } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' +import { Center } from '../presentationEditor/components/styled' import Timer from './components/Timer' import { OperatorButton, @@ -111,9 +114,13 @@ const OperatorViewPage: React.FC = () => { const history = useHistory() const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id - + const [successMessageOpen, setSuccessMessageOpen] = useState(true) + const activeSlideOrder = useAppSelector( + (state) => + state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.order + ) useEffect(() => { - socketConnect() + socketConnect('Operator') socketSetSlide handleOpenCodes() setTimeout(startCompetition, 1000) // Wait for socket to connect @@ -155,7 +162,7 @@ const OperatorViewPage: React.FC = () => { const endCompetition = () => { setOpen(false) socketEndPresentation() - history.push('/admin/tävlingshanterare') + history.push('/admin/competition-manager') window.location.reload(false) // TODO: fix this, we "need" to refresh site to be able to run the competition correctly again } @@ -225,9 +232,11 @@ const OperatorViewPage: React.FC = () => { fullWidth={false} fullScreen={false} > - <DialogTitle id="max-width-dialog-title" className={classes.paper}> - Koder för {competitionName} - </DialogTitle> + <Center> + <DialogTitle id="max-width-dialog-title" className={classes.paper} style={{ width: '100%' }}> + Koder för {competitionName} + </DialogTitle> + </Center> <DialogContent> {/* <DialogContentText>Här visas tävlingskoderna till den valda tävlingen.</DialogContentText> */} {codes && @@ -297,7 +306,7 @@ const OperatorViewPage: React.FC = () => { <Typography variant="h3">{presentation.competition.name}</Typography> <SlideCounter> <Typography variant="h3"> - {presentation.slide.order + 1} / {presentation.competition.slides.length} + {activeSlideOrder !== undefined && activeSlideOrder + 1} / {presentation.competition.slides.length} </Typography> </SlideCounter> </OperatorHeader> @@ -364,6 +373,9 @@ const OperatorViewPage: React.FC = () => { ))} </List> </Popover> + <Snackbar open={successMessageOpen} autoHideDuration={4000} onClose={() => setSuccessMessageOpen(false)}> + <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som operatör`}</Alert> + </Snackbar> </OperatorContainer> ) } diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx index c2d2ff83..fd51c884 100644 --- a/client/src/pages/views/TeamViewPage.tsx +++ b/client/src/pages/views/TeamViewPage.tsx @@ -1,4 +1,6 @@ -import React, { useEffect } from 'react' +import { Snackbar } from '@material-ui/core' +import { Alert } from '@material-ui/lab' +import React, { useEffect, useState } from 'react' import { useAppSelector } from '../../hooks' import { socketConnect, socketJoinPresentation } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' @@ -8,9 +10,15 @@ const TeamViewPage: React.FC = () => { const code = useAppSelector((state) => state.presentation.code) const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Team')?.id + const [successMessageOpen, setSuccessMessageOpen] = useState(true) + const competitionName = useAppSelector((state) => state.presentation.competition.name) + const teamName = useAppSelector( + (state) => + state.presentation.competition.teams.find((team) => team.id === state.competitionLogin.data?.team_id)?.name + ) useEffect(() => { if (code && code !== '') { - socketConnect() + socketConnect('Team') socketJoinPresentation() } }, []) @@ -19,6 +27,9 @@ const TeamViewPage: React.FC = () => { <PresentationContainer> {activeViewTypeId && <SlideDisplay variant="presentation" activeViewTypeId={activeViewTypeId} />} </PresentationContainer> + <Snackbar open={successMessageOpen} autoHideDuration={4000} onClose={() => setSuccessMessageOpen(false)}> + <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som lag ${teamName}`}</Alert> + </Snackbar> </PresentationBackground> ) } diff --git a/client/src/pages/views/components/JudgeScoreDisplay.tsx b/client/src/pages/views/components/JudgeScoreDisplay.tsx index 2745e129..0b5e5845 100644 --- a/client/src/pages/views/components/JudgeScoreDisplay.tsx +++ b/client/src/pages/views/components/JudgeScoreDisplay.tsx @@ -3,20 +3,19 @@ import axios from 'axios' import React from 'react' import { getPresentationCompetition } from '../../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../../hooks' +import { RichSlide } from '../../../interfaces/ApiRichModels' import { AnswerContainer, ScoreDisplayContainer, ScoreDisplayHeader, ScoreInput } from './styled' type ScoreDisplayProps = { teamIndex: number + activeSlide: RichSlide } -const JudgeScoreDisplay = ({ teamIndex }: ScoreDisplayProps) => { +const JudgeScoreDisplay = ({ teamIndex, activeSlide }: ScoreDisplayProps) => { const dispatch = useAppDispatch() const currentTeam = useAppSelector((state) => state.presentation.competition.teams[teamIndex]) const currentCompetititonId = useAppSelector((state) => state.presentation.competition.id) - const activeQuestion = useAppSelector( - (state) => - state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id)?.questions[0] - ) + const activeQuestion = activeSlide.questions[0] const scores = currentTeam.question_answers.map((questionAnswer) => questionAnswer.score) const questionMaxScore = activeQuestion?.total_score const activeAnswer = currentTeam.question_answers.find( diff --git a/client/src/pages/views/components/JudgeScoringInstructions.tsx b/client/src/pages/views/components/JudgeScoringInstructions.tsx index f8c21667..86d8eaf1 100644 --- a/client/src/pages/views/components/JudgeScoringInstructions.tsx +++ b/client/src/pages/views/components/JudgeScoringInstructions.tsx @@ -8,6 +8,7 @@ type JudgeScoringInstructionsProps = { } const JudgeScoringInstructions = ({ question }: JudgeScoringInstructionsProps) => { + console.log(question) return ( <JudgeScoringInstructionsContainer elevation={3}> <ScoringInstructionsInner> diff --git a/client/src/pages/views/components/PresentationComponent.tsx b/client/src/pages/views/components/PresentationComponent.tsx index 0a688dc1..71783f6a 100644 --- a/client/src/pages/views/components/PresentationComponent.tsx +++ b/client/src/pages/views/components/PresentationComponent.tsx @@ -9,9 +9,10 @@ import TextComponentDisplay from '../../presentationEditor/components/TextCompon type PresentationComponentProps = { component: Component scale: number + currentSlideId?: number } -const PresentationComponent = ({ component, scale }: PresentationComponentProps) => { +const PresentationComponent = ({ component, scale, currentSlideId }: PresentationComponentProps) => { const renderInnerComponent = () => { switch (component.type_id) { case ComponentTypes.Text: @@ -25,7 +26,7 @@ const PresentationComponent = ({ component, scale }: PresentationComponentProps) /> ) case ComponentTypes.Question: - return <QuestionComponentDisplay variant="presentation" /> + return <QuestionComponentDisplay variant="presentation" currentSlideId={currentSlideId} /> default: break } diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx index 8d3b9f70..29f95349 100644 --- a/client/src/pages/views/components/Timer.tsx +++ b/client/src/pages/views/components/Timer.tsx @@ -1,10 +1,9 @@ import React, { useEffect } from 'react' -import { connect } from 'react-redux' import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation' -import { useAppDispatch } from '../../../hooks' +import { useAppDispatch, useAppSelector } from '../../../hooks' import store from '../../../store' -const mapStateToProps = (state: any) => { +/* const mapStateToProps = (state: any) => { return { timer: state.presentation.timer, timer_start_value: state.presentation.slide.timer, @@ -15,28 +14,33 @@ const mapDispatchToProps = (dispatch: any) => { return { // tickTimer: () => dispatch(tickTimer(1)), } -} +} */ let timerIntervalId: NodeJS.Timeout -const Timer: React.FC = (props: any) => { +const Timer: React.FC = () => { const dispatch = useAppDispatch() - + const slide = store + .getState() + .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) + const timerStartValue = slide?.timer + const timer = useAppSelector((state) => state.presentation.timer) useEffect(() => { - dispatch(setPresentationTimer({ enabled: false, value: store.getState().presentation.slide.timer })) - }, [props.timer_start_value]) + if (!slide) return + dispatch(setPresentationTimer({ enabled: false, value: slide.timer })) + }, [timerStartValue]) useEffect(() => { - if (props.timer.enabled) { + if (timer.enabled) { timerIntervalId = setInterval(() => { dispatch(setPresentationTimerDecrement()) }, 1000) } else { clearInterval(timerIntervalId) } - }, [props.timer.enabled]) + }, [timer.enabled]) - return <div>{props.timer.value}</div> + return <div>{timer.value}</div> } -export default connect(mapStateToProps, mapDispatchToProps)(Timer) +export default Timer diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index 8aa14592..01a3fbea 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -45,6 +45,11 @@ export default function (state = initialState, action: AnyAction) { ...state, activeViewTypeId: action.payload as number, } + case Types.SET_EDITOR_LOADING: + return { + ...state, + loading: action.payload as boolean, + } default: return state } diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index 84f93684..e0368f7f 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -1,6 +1,4 @@ import Types from '../actions/types' -import { Slide } from '../interfaces/ApiModels' -import { RichSlide } from '../interfaces/ApiRichModels' import presentationReducer from './presentationReducer' const initialState = { @@ -13,14 +11,7 @@ const initialState = { year: 0, teams: [], }, - slide: { - competition_id: 0, - background_image: undefined, - id: -1, - order: 0, - timer: 0, - title: '', - }, + activeSlideId: -1, code: '', timer: { enabled: false, @@ -50,138 +41,23 @@ it('should handle SET_PRESENTATION_COMPETITION', () => { }) ).toEqual({ competition: testCompetition, - slide: initialState.slide, + activeSlideId: initialState.activeSlideId, code: initialState.code, timer: initialState.timer, }) }) -it('should handle SET_PRESENTATION_SLIDE', () => { - const testSlide = [ - { - competition_id: 20, - id: 4, - order: 3, - timer: 123, - title: 'testSlideTitle', - }, - ] +it('should handle SET_PRESENTATION_SLIDE_ID', () => { + const testSlideId = 123123123 expect( presentationReducer(initialState, { - type: Types.SET_PRESENTATION_SLIDE, - payload: testSlide, + type: Types.SET_PRESENTATION_SLIDE_ID, + payload: testSlideId, }) ).toEqual({ competition: initialState.competition, - slide: testSlide, + activeSlideId: testSlideId, code: initialState.code, timer: initialState.timer, }) }) - -describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => { - it('by changing slide to the previous if there is one', () => { - const testPresentationState = { - competition: { - ...initialState.competition, - slides: [ - { competition_id: 0, order: 0 }, - { competition_id: 0, order: 1 }, - ] as RichSlide[], - teams: [], - }, - slide: { competition_id: 0, order: 1 } as Slide, - code: initialState.code, - timer: initialState.timer, - } - expect( - presentationReducer(testPresentationState, { - type: Types.SET_PRESENTATION_SLIDE_PREVIOUS, - }) - ).toEqual({ - competition: testPresentationState.competition, - slide: testPresentationState.competition.slides[0], - - code: initialState.code, - timer: initialState.timer, - }) - }) - it('by not changing slide if there is no previous one', () => { - const testPresentationState = { - competition: { - ...initialState.competition, - slides: [ - { competition_id: 0, order: 0 }, - { competition_id: 0, order: 1 }, - ] as RichSlide[], - teams: [], - }, - slide: { competition_id: 0, order: 0 } as Slide, - code: initialState.code, - timer: initialState.timer, - } - expect( - presentationReducer(testPresentationState, { - type: Types.SET_PRESENTATION_SLIDE_PREVIOUS, - }) - ).toEqual({ - competition: testPresentationState.competition, - slide: testPresentationState.competition.slides[0], - code: initialState.code, - timer: initialState.timer, - }) - }) -}) - -describe('should handle SET_PRESENTATION_SLIDE_NEXT', () => { - it('by changing slide to the next if there is one', () => { - const testPresentationState = { - competition: { - ...initialState.competition, - slides: [ - { competition_id: 0, order: 0 }, - { competition_id: 0, order: 1 }, - ] as RichSlide[], - teams: [], - }, - slide: { competition_id: 0, order: 0 } as Slide, - code: initialState.code, - timer: initialState.timer, - } - expect( - presentationReducer(testPresentationState, { - type: Types.SET_PRESENTATION_SLIDE_NEXT, - }) - ).toEqual({ - competition: testPresentationState.competition, - slide: testPresentationState.competition.slides[1], - code: initialState.code, - timer: initialState.timer, - }) - }) - it('by not changing slide if there is no next one', () => { - const testPresentationState = { - competition: { - ...initialState.competition, - slides: [ - { competition_id: 0, order: 0 }, - { competition_id: 0, order: 1 }, - ] as RichSlide[], - teams: [], - }, - slide: { competition_id: 0, order: 1 } as Slide, - code: initialState.code, - timer: initialState.timer, - } - expect( - presentationReducer(testPresentationState, { - type: Types.SET_PRESENTATION_SLIDE_NEXT, - }) - ).toEqual({ - competition: testPresentationState.competition, - slide: testPresentationState.competition.slides[1], - code: initialState.code, - timer: initialState.timer, - }) - }) -}) diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index 374baf32..4df814a8 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -1,13 +1,12 @@ import { AnyAction } from 'redux' import Types from '../actions/types' -import { Slide } from '../interfaces/ApiModels' import { Timer } from '../interfaces/Timer' import { RichCompetition } from './../interfaces/ApiRichModels' /** Define a type for the presentation state */ interface PresentationState { competition: RichCompetition - slide: Slide + activeSlideId: number code: string timer: Timer } @@ -23,14 +22,7 @@ const initialState: PresentationState = { teams: [], background_image: undefined, }, - slide: { - competition_id: 0, - id: -1, - order: 0, - timer: 0, - title: '', - background_image: undefined, - }, + activeSlideId: -1, code: '', timer: { enabled: false, @@ -51,34 +43,11 @@ export default function (state = initialState, action: AnyAction) { ...state, code: action.payload, } - case Types.SET_PRESENTATION_SLIDE: + case Types.SET_PRESENTATION_SLIDE_ID: return { ...state, - slide: action.payload as Slide, + activeSlideId: action.payload as number, } - case Types.SET_PRESENTATION_SLIDE_PREVIOUS: - if (state.slide.order - 1 >= 0) { - return { - ...state, - slide: state.competition.slides[state.slide.order - 1], - } - } - return state - case Types.SET_PRESENTATION_SLIDE_NEXT: - if (state.slide.order + 1 < state.competition.slides.length) { - return { - ...state, - slide: state.competition.slides[state.slide.order + 1], - } - } - return state - case Types.SET_PRESENTATION_SLIDE_BY_ORDER: - if (0 <= action.payload && action.payload < state.competition.slides.length) - return { - ...state, - slide: state.competition.slides[action.payload], - } - return state case Types.SET_PRESENTATION_TIMER: if (action.payload.value == 0) { action.payload.enabled = false diff --git a/client/src/sockets.ts b/client/src/sockets.ts index 73a7e165..dabaf4b9 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -29,9 +29,9 @@ let socket: SocketIOClient.Socket * You can also comment functions, like usual. This will automatically appear * in the documentation, no more needed. */ -export const socketConnect = () => { +export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => { if (!socket) { - const token = localStorage.competitionToken + const token = localStorage[role] socket = io('localhost:5000', { transportOptions: { polling: { @@ -43,7 +43,7 @@ export const socketConnect = () => { }) socket.on('set_slide', (data: SetSlideInterface) => { - setCurrentSlideByOrder(data.slide_order)(store.dispatch) + setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState) }) socket.on('set_timer', (data: SetTimerInterface) => { @@ -69,11 +69,19 @@ export const socketEndPresentation = () => { } export const socketSetSlideNext = () => { - socketSetSlide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists + const activeSlide = store + .getState() + .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) + if (!activeSlide) return + socketSetSlide(activeSlide.order + 1) // TODO: Check that this slide exists } export const socketSetSlidePrev = () => { - socketSetSlide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists + const activeSlide = store + .getState() + .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId) + if (!activeSlide) return + socketSetSlide(activeSlide.order - 1) // TODO: Check that this slide exists } /** diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index 2b08b40c..64cb437a 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -21,7 +21,7 @@ const SecureRoute: React.FC<SecureRouteProps> = ({ component: Component, authLev if (authLevel === 'admin' || authLevel === 'login') { CheckAuthenticationAdmin().then(() => setInitialized(true)) } else { - CheckAuthenticationCompetition().then(() => setInitialized(true)) + CheckAuthenticationCompetition(authLevel).then(() => setInitialized(true)) } }, []) diff --git a/client/src/utils/checkAuthenticationCompetition.test.ts b/client/src/utils/checkAuthenticationCompetition.test.ts index 0d0fdd90..478c4f5e 100644 --- a/client/src/utils/checkAuthenticationCompetition.test.ts +++ b/client/src/utils/checkAuthenticationCompetition.test.ts @@ -20,8 +20,8 @@ it('dispatches correct actions when auth token is ok', async () => { const testToken = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' - localStorage.setItem('competitionToken', testToken) - await CheckAuthenticationCompetition() + localStorage.setItem('JudgeToken', testToken) + await CheckAuthenticationCompetition('Judge') expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_DATA, payload: { @@ -49,8 +49,8 @@ it('dispatches correct actions when getting user data fails', async () => { const spy = jest.spyOn(store, 'dispatch') const testToken = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' - localStorage.setItem('competitionToken', testToken) - await CheckAuthenticationCompetition() + localStorage.setItem('AudienceToken', testToken) + await CheckAuthenticationCompetition('Audience') expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }) expect(spy).toBeCalledTimes(1) expect(console.log).toHaveBeenCalled() @@ -61,7 +61,7 @@ it('dispatches no actions when no token exists', async () => { return Promise.resolve({ data: {} }) }) const spy = jest.spyOn(store, 'dispatch') - await CheckAuthenticationCompetition() + await CheckAuthenticationCompetition('Operator') expect(spy).not.toBeCalled() }) @@ -71,9 +71,9 @@ it('dispatches correct actions when token is expired', async () => { }) const testToken = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MjAyMjE1OTgsImV4cCI6OTU3NTMzNTk4LCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIn0.uFXtkAsf-cTlKrTIdZ3E-gXnHkzS08iPrhS8iNCGV2E' - localStorage.setItem('competitionToken', testToken) + localStorage.setItem('TeamToken', testToken) const spy = jest.spyOn(store, 'dispatch') - await CheckAuthenticationCompetition() + await CheckAuthenticationCompetition('Team') expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }) expect(spy).toBeCalledTimes(1) }) diff --git a/client/src/utils/checkAuthenticationCompetition.ts b/client/src/utils/checkAuthenticationCompetition.ts index 4d542dc3..f6dfd8fe 100644 --- a/client/src/utils/checkAuthenticationCompetition.ts +++ b/client/src/utils/checkAuthenticationCompetition.ts @@ -5,12 +5,12 @@ import { getPresentationCompetition, setPresentationCode } from '../actions/pres import Types from '../actions/types' import store from '../store' -const UnAuthorized = async () => { - await logoutCompetition()(store.dispatch) +const UnAuthorized = async (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => { + await logoutCompetition(role)(store.dispatch) } -export const CheckAuthenticationCompetition = async () => { - const authToken = localStorage.competitionToken +export const CheckAuthenticationCompetition = async (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => { + const authToken = localStorage[`${role}Token`] if (authToken) { const decodedToken: any = jwtDecode(authToken) if (decodedToken.exp * 1000 >= Date.now()) { @@ -31,10 +31,10 @@ export const CheckAuthenticationCompetition = async () => { }) .catch((error) => { console.log(error) - UnAuthorized() + UnAuthorized(role) }) } else { - await UnAuthorized() + await UnAuthorized(role) } } } -- GitLab