diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts index d9f2833356bc66cc65e800bdc0481090ff3c0f16..cfc222b3afa0f9aa3f6135a2719160f515f095ba 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 bae4813822e83db6feac319c9f5179e1c2164255..64fd11e1a61614e30296f685bf85eac8700e7a96 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 095a352952680810cfcb3fc0f8f48a5eabc6b1a6..9fbc2a0ddeff68855b50a30d37a059e2edfc95a9 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 07536b213c7a8c95d5fc9b6c6515d4069a730b52..0c3f2466556ab573341d190c14f83a198c41fcdb 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 c73179c114c3512d0c8dca9230054ea44d81c195..eca08ccd5feeaaece2a2a665ba22e51dee9a9677 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 3a375210ae30a021d7a758e6ca9e3c9b18d4d45d..8d1935576ce23afe95682127f8072350a9627ea3 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 e404b112a1df1e6ab4f9d8a8759fb946a20ccd52..165d17d8dbb03305013b339001d8147ac57365d7 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 cf5332a089c1a2381f2cb813f16cc53c8e7639a1..0545102893ce342878602972d63c1dbe879ac59a 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 10cb22bc5c4b7527b05d8dfeb9b0f748dd6f0c65..14efed4ef6ef45d4819d66aed714cc4ba128d9c5 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 5e2531bb44b6e2e6d7d6a3ce65941ecdc03a3ee3..57f4c101e829c8cd57dd0f72bdb8309772b8b26a 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 63549edcd3180033e897c8bfb3924e6a691225bd..2634751d8b08f3ca027de0400dbf02f726a33161 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 c071747e54a760e2972b8d36201d67780942e47a..73c75d541df22422a16853c6664434176aef3e59 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 d84944b47440997de210a258c65a6316caab63e9..d3286065499cb5c3b4462f33fda358f582578405 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 9482b660550e729f2a5a4c443d0f935193679b1f..fd63a4b30069dc7866a7fc5ee729042b058fdb2f 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 e783bdb22e743c345611b92bfc11d161fc2fe957..273748f38cd88c3302fe946f3075593b06b168d1 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 dd8e2fa85a8e2562595c85c7068b0073f744092a..b43766eb0534eabf54d04f2498ceb57e708dd8df 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 4c09280042bd038fee1e3adc4b5bf3b7aef8ef6a..3b40ad74dd654e79193cfb6a6723c416f4226048 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 e6b60f290784627d9c5446428062f2e1042cacef..9fd6ed70b6063c117784ec9967e99da5a29c7937 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 e54b0dd2fe56f3ebf96b65fa8a05e6af4467da9f..465bffcbea76f2ba4273264b0246c131dc035824 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 91825662dbc65ec851c093ddf2aa48ddb98ce404..a633efec7e99d9390e5104284f2735d569f1b69e 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 28280c90bb0b370802c39b7646c7e59ded4b0f55..af1f18b3df30473ee466cd4b0acdf708123d5d89 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 81f70edbc86c72409f2df6ac178d35ac10b8bc72..a28348a74460d45201f9c9b8825ce23daee87acb 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 8bd782f74a56cc004b222002a3fde6b8e1d6ffda..5bb96fcb7c2b01abb91c19e6f8c3e106161dfd22 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 c2d2ff8323acd61c2a503e5c0947650950a4a892..fd51c8844f29c50bd2463519c15b02692aef99bf 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 2745e12970f3d5a20d2057adbc76ecc0ce2e6d6f..0b5e584574e08678f67159401438c76e413851fe 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 f8c21667504524720523004ce9e853616c84e41f..86d8eaf12e9a26db08c4b9d6dd77c58cf54b6d80 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 0a688dc1c1bec3373cbf7a4e782b57fdab58d411..71783f6a15921136e1c56e85d29b8f63b8d2aae8 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 8d3b9f70594ac416610bbd06b9d09cbee3ece0a4..29f95349a4ad436f95c8e7a5d20b9b1129b94d6e 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 8aa14592cb403652eb3c2e42c22312cfd824d0ca..01a3fbeaa4248099b882f20940f841fb008a3c7b 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 84f93684ccd31daec1d0e6a268efc9a6f1b3de8b..e0368f7fc342bcc2b241cbf6eca3d368d34d649f 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 374baf32c9cf88001ee0ff8bd740a211f12f3740..4df814a85a0d60a671aebd9db433fa8094526b15 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 73a7e16504cd12dcc705638ff6200f2980ad19fb..dabaf4b9c26cef497c179aad7df293cac9647bc7 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 2b08b40cfd33cc630d67f80f91921e46aa163e4d..64cb437afe0b0968a8054de523bbd3465a2a2085 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 0d0fdd90aea38f613a7a1b1347085a2201d793a6..478c4f5e0d7ae43211b45e37bcf92476d6dac004 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 4d542dc34b5cb78cca7dbf134638a8b2649c48de..f6dfd8fe227e5ef5bf71dc1d7cef0a4daf9b9ee9 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) } } }