diff --git a/client/package-lock.json b/client/package-lock.json index 667b75859fe71467505839e687c7e478b8a52834..2655ad2e5dbc1fb024ba896a3701f49adbfe399d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2526,6 +2526,15 @@ "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", @@ -5161,6 +5170,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -11058,6 +11075,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -13519,6 +13541,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -13610,6 +13637,20 @@ "resolved": "https://registry.npmjs.org/react-axios/-/react-axios-2.0.4.tgz", "integrity": "sha512-QsTq7C/NwsjfrSmFVxPo29BdX6DtLpRF0fZTJv5/R4BanOm+c4639B3Xb4lF83ZfAOX5IW8XG7htz4V+WNF+WA==" }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dev-utils": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.3.tgz", @@ -16568,6 +16609,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/client/package.json b/client/package.json index 8733d1d45b7c2e4c2349167573e7411aa66afd45..a0c8b7f5c74a6587f48cdf6c7d478311f2529185 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,7 @@ "jwt-decode": "^3.1.2", "react": "^17.0.1", "react-axios": "^2.0.4", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.1", "react-redux": "^7.2.2", "react-rnd": "^10.2.4", @@ -41,6 +42,7 @@ }, "devDependencies": { "@types/enzyme": "^3.10.8", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "@types/redux-mock-store": "^1.0.2", diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 5cd27cc3d83e96bda772e2e9b07282903ed069f5..a44d182334d526e3632f038f8c8b1b5875d8dc8b 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -28,30 +28,10 @@ const Main: React.FC = () => { component={PresentationEditorPage} /> <Route exact path="/:code" component={ViewSelectPage} /> - <SecureRoute - authLevel="competition" - exact - path="/team/competition-id=:competitionId" - component={TeamViewPage} - /> - <SecureRoute - authLevel="competition" - exact - path="/operator/competition-id=:competitionId" - component={OperatorViewPage} - /> - <SecureRoute - authLevel="competition" - exact - path="/judge/competition-id=:competitionId" - component={JudgeViewPage} - /> - <SecureRoute - authLevel="competition" - exact - path="/audience/competition-id=:competitionId" - component={AudienceViewPage} - /> + <SecureRoute authLevel="Team" exact path="/view/team" component={TeamViewPage} /> + <SecureRoute authLevel="Operator" exact path="/view/operator" component={OperatorViewPage} /> + <SecureRoute authLevel="Judge" exact path="/view/judge" component={JudgeViewPage} /> + <SecureRoute authLevel="Audience" exact path="/view/audience" component={AudienceViewPage} /> </Switch> </BrowserRouter> ) diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts index 448eec249f360acbadeb6161abac6412daccb32b..97dc76e4499551bbfba87222aedae6e7a30bad72 100644 --- a/client/src/actions/competitionLogin.ts +++ b/client/src/actions/competitionLogin.ts @@ -4,12 +4,14 @@ This file handles actions for the competitionLogin redux state import axios from 'axios' import { History } from 'history' -import { AppDispatch } from '../store' +import { AppDispatch, RootState } from '../store' +import { getPresentationCompetition } from './presentation' import Types from './types' // Action creator to attempt to login with competition code export const loginCompetition = (code: string, history: History, redirect: boolean) => async ( - dispatch: AppDispatch + dispatch: AppDispatch, + getState: () => RootState ) => { dispatch({ type: Types.LOADING_COMPETITION_LOGIN }) await axios @@ -27,7 +29,8 @@ export const loginCompetition = (code: string, history: History, redirect: boole view: res.data.view, }, }) - if (redirect && res.data && res.data.view_type_id) { + getPresentationCompetition(res.data.competition_id)(dispatch, getState) + if (redirect && res.data && res.data.view) { history.push(`/${code}`) } }) diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index b422f95423539cc2f2ea0bf4e7ecdc58f78bdbc8..0ca8a02ffa1d115e165091276b740131be9d9758 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -15,7 +15,6 @@ export default { SET_ERRORS: 'SET_ERRORS', CLEAR_ERRORS: 'CLEAR_ERRORS', SET_COMPETITION_LOGIN_DATA: 'SET_COMPETITION_LOGIN_DATA', - SET_COMPETITION_LOGIN_AUTHENTICATED: 'SET_COMPETITION_LOGIN_AUTHENTICATED', SET_COMPETITION_LOGIN_UNAUTHENTICATED: 'SET_COMPETITION_LOGIN_UNAUTHENTICATED', SET_COMPETITION_LOGIN_ERRORS: 'SET_COMPETITION_LOGIN_ERRORS', CLEAR_COMPETITION_LOGIN_ERRORS: 'CLEAR_COMPETITION_LOGIN_ERRORS', diff --git a/client/src/pages/admin/competitions/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx index b7184d55595b23333c895604b2d311a0a80e530c..b6af32793c9eac86553408fc742430eaeddea7a6 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -1,17 +1,17 @@ import { + Box, Button, - Menu, - ListItem, - TablePagination, - TextField, - Typography, Dialog, - DialogTitle, - DialogContent, DialogActions, + DialogContent, + DialogTitle, + ListItem, ListItemText, + Menu, + TablePagination, + TextField, Tooltip, - Box, + Typography, } from '@material-ui/core' import FormControl from '@material-ui/core/FormControl' import InputLabel from '@material-ui/core/InputLabel' @@ -25,7 +25,10 @@ import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' +import FileCopyIcon from '@material-ui/icons/FileCopy' +import LinkIcon from '@material-ui/icons/Link' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import RefreshIcon from '@material-ui/icons/Refresh' import axios from 'axios' import React, { useEffect } from 'react' import { Link, useHistory } from 'react-router-dom' @@ -35,8 +38,6 @@ import { Team } from '../../../interfaces/ApiModels' import { CompetitionFilterParams } from '../../../interfaces/FilterParams' import { FilterContainer, RemoveMenuItem, TopBar, YearFilterTextField } from '../styledComp' import AddCompetition from './AddCompetition' -import FileCopyIcon from '@material-ui/icons/FileCopy' -import RefreshIcon from '@material-ui/icons/Refresh' /** * Component description: @@ -83,14 +84,16 @@ const CompetitionManager: React.FC = (props: any) => { const filterParams = useAppSelector((state) => state.competitions.filterParams) const competitionTotal = useAppSelector((state) => state.competitions.total) const cities = useAppSelector((state) => state.cities.cities) - const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() const history = useHistory() + const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { - setAnchorEl(event.currentTarget) setActiveId(id) + getCodes(id) + getTeams(id) + setAnchorEl(event.currentTarget) } const handleClose = () => { @@ -129,12 +132,15 @@ const CompetitionManager: React.FC = (props: any) => { } const handleStartCompetition = () => { - history.push(`/operator/id=${activeId}&code=123123`) + const operatorCode = codes.find((code) => code.view_type_id === 4)?.code + if (operatorCode) { + history.push(`/${operatorCode}`) + } } - const getCodes = async () => { + const getCodes = async (id: number) => { await axios - .get(`/api/competitions/${activeId}/codes`) + .get(`/api/competitions/${id}/codes`) .then((response) => { console.log(response.data) setCodes(response.data.items) @@ -142,9 +148,9 @@ const CompetitionManager: React.FC = (props: any) => { .catch(console.log) } - const getTeams = async () => { + const getTeams = async (id: number) => { await axios - .get(`/api/competitions/${activeId}/teams`) + .get(`/api/competitions/${id}/teams`) .then((response) => { console.log(response.data.items) setTeams(response.data.items) @@ -194,8 +200,6 @@ const CompetitionManager: React.FC = (props: any) => { } const handleOpenDialog = async () => { - await getCodes() - await getTeams() await getCompetitionName() setDialogIsOpen(true) } @@ -225,15 +229,17 @@ const CompetitionManager: React.FC = (props: any) => { } const refreshCode = async (code: Code) => { - await axios - .put(`/api/competitions/${activeId}/codes/${code.id}`) - .then(() => { - getCodes() - dispatch(getCompetitions()) - }) - .catch(({ response }) => { - console.warn(response.data) - }) + if (activeId) { + await axios + .put(`/api/competitions/${activeId}/codes/${code.id}`) + .then(() => { + getCodes(activeId) + dispatch(getCompetitions()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } } return ( @@ -373,6 +379,16 @@ const CompetitionManager: React.FC = (props: any) => { <FileCopyIcon fontSize="small" /> </Button> </Tooltip> + <Tooltip title="Kopiera länk" arrow> + <Button + margin-right="0px" + onClick={() => { + navigator.clipboard.writeText(`${window.location.host}/${code.code}`) + }} + > + <LinkIcon fontSize="small" /> + </Button> + </Tooltip> </ListItem> ))} </DialogContent> diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx index 5426152ff33783745f8dc1181237cd2926b0a16a..7254a69d3bdae4e39c6ce1097dc06e753ff333f0 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx @@ -13,7 +13,7 @@ it('renders presentation editor', () => { id: 0, year: 0, city_id: 0, - slides: [{ id: 5 }], + slides: [{ id: 5, order: 2 }], teams: [], }, } diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 6adb24586ad78bf9d3663d5fac3a1173478ed6dc..ddbe7468df6489d4abe69258b4fb504b90dde3b5 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -4,6 +4,7 @@ import ListItemText from '@material-ui/core/ListItemText' import AddOutlinedIcon from '@material-ui/icons/AddOutlined' import axios from 'axios' import React, { useEffect, useState } from 'react' +import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd' import { Link, useParams } from 'react-router-dom' import { getCities } from '../../actions/cities' import { getEditorCompetition, setEditorSlideId, setEditorViewId } from '../../actions/editor' @@ -109,6 +110,20 @@ const PresentationEditorPage: React.FC = () => { } } + const onDragEnd = async (result: DropResult) => { + // dropped outside the list + if (!result.destination) { + return + } + const draggedIndex = result.source.index + const draggedSlideId = competition.slides.find((slide) => slide.order === draggedIndex)?.id + if (draggedSlideId) { + await axios + .put(`/api/competitions/${competitionId}/slides/${draggedSlideId}/order`, { order: result.destination.index }) + .then(() => dispatch(getEditorCompetition(competitionId))) + .catch(console.log) + } + } return ( <PresentationEditorContainer> <CssBaseline /> @@ -143,20 +158,34 @@ const PresentationEditorPage: React.FC = () => { <FillLeftContainer $leftDrawerWidth={leftDrawerWidth} $rightDrawerWidth={undefined}> <ToolbarMargin /> <SlideList> - {competition.slides && - competition.slides.map((slide) => ( - <SlideListItem - divider - button - key={slide.id} - selected={slide.id === activeSlideId} - onClick={() => setActiveSlideId(slide.id)} - onContextMenu={(event) => handleRightClick(event, slide.id)} - > - {renderSlideIcon(slide)} - <ListItemText primary={`Sida ${slide.order + 1}`} /> - </SlideListItem> - ))} + <DragDropContext onDragEnd={onDragEnd}> + <Droppable droppableId="droppable"> + {(provided) => ( + <div key={provided.innerRef.toString()} ref={provided.innerRef} {...provided.droppableProps}> + {competition.slides && + competition.slides.map((slide, index) => ( + <Draggable key={slide.order} draggableId={slide.id.toString()} index={index}> + {(provided, snapshot) => ( + <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> + <SlideListItem + divider + button + selected={slide.id === activeSlideId} + onClick={() => setActiveSlideId(slide.id)} + onContextMenu={(event) => handleRightClick(event, slide.id)} + > + {renderSlideIcon(slide)} + <ListItemText primary={`Sida ${slide.order + 1}`} /> + </SlideListItem> + </div> + )} + </Draggable> + ))} + {provided.placeholder} + </div> + )} + </Droppable> + </DragDropContext> </SlideList> <PositionBottom> <Divider /> @@ -184,7 +213,7 @@ const PresentationEditorPage: React.FC = () => { <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> <InnerContent> - <SlideDisplay variant="editor" activeViewTypeId={activeViewTypeId} /> + {competitionLoading && <SlideDisplay variant="editor" activeViewTypeId={activeViewTypeId} />} </InnerContent> </Content> <Menu diff --git a/client/src/pages/presentationEditor/styled.tsx b/client/src/pages/presentationEditor/styled.tsx index ce9538b4c85908be74360aaa584db081b5a87888..779599dab4cd748333631b734decf2051389389c 100644 --- a/client/src/pages/presentationEditor/styled.tsx +++ b/client/src/pages/presentationEditor/styled.tsx @@ -63,6 +63,7 @@ export const LeftDrawer = styled(Drawer)<DrawerSizeProps>` flex-shrink: 0; position: relative; z-index: 1; + overflow: hidden; ` export const RightDrawer = styled(Drawer)<DrawerSizeProps>` diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx index 48b92c4686c762536c629e1c33f5bb33ade9cc59..28280c90bb0b370802c39b7646c7e59ded4b0f55 100644 --- a/client/src/pages/views/AudienceViewPage.tsx +++ b/client/src/pages/views/AudienceViewPage.tsx @@ -1,21 +1,15 @@ import { Typography } from '@material-ui/core' import React, { useEffect } from 'react' -import { useParams } from 'react-router-dom' -import { getPresentationCompetition } from '../../actions/presentation' -import { useAppDispatch, useAppSelector } from '../../hooks' -import { ViewParams } from '../../interfaces/ViewParams' +import { useAppSelector } from '../../hooks' import { socketConnect, socketJoinPresentation } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { PresentationBackground, PresentationContainer } from './styled' const AudienceViewPage: React.FC = () => { - const { competitionId }: ViewParams = useParams() const code = useAppSelector((state) => state.presentation.code) - const dispatch = useAppDispatch() const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id useEffect(() => { - dispatch(getPresentationCompetition(competitionId)) if (code && code !== '') { socketConnect() socketJoinPresentation() diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index e92dc1b9b470977c563b8bc6b955af7da984b6ea..5b75d2d2f505f05d18e797d8ef039a9a06b2f067 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -25,11 +25,8 @@ import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount' import TimerIcon from '@material-ui/icons/Timer' import axios from 'axios' import React, { useEffect } from 'react' -import { useHistory, useParams } from 'react-router-dom' -import { getPresentationCompetition } from '../../actions/presentation' -import { useAppDispatch, useAppSelector } from '../../hooks' -import { Team } from '../../interfaces/ApiModels' -import { ViewParams } from '../../interfaces/ViewParams' +import { useHistory } from 'react-router-dom' +import { useAppSelector } from '../../hooks' import { socketConnect, socketEndPresentation, @@ -100,24 +97,21 @@ const OperatorViewPage: React.FC = () => { const [openAlert, setOpen] = React.useState(false) const [openAlertCode, setOpenCode] = React.useState(false) const [codes, setCodes] = React.useState<Code[]>([]) - const [teams, setTeams] = React.useState<Team[]>([]) const [competitionName, setCompetitionName] = React.useState<string | undefined>(undefined) //const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) const classes = useStyles() - //const teams = useAppSelector((state) => state.presentation.competition.teams) + const teams = useAppSelector((state) => state.presentation.competition.teams) const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) - const { competitionId }: ViewParams = useParams() + const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id) const presentation = useAppSelector((state) => state.presentation) const activeId = useAppSelector((state) => state.presentation.competition.id) const history = useHistory() - const dispatch = useAppDispatch() const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id useEffect(() => { - dispatch(getPresentationCompetition(competitionId)) socketConnect() socketSetSlide // Behövs denna? handleOpenCodes() @@ -153,15 +147,10 @@ const OperatorViewPage: React.FC = () => { const handleOpenCodes = async () => { await getCodes() - await getTeams() await getCompetitionName() setOpenCode(true) } - const handleCopy = () => { - console.log('copied code to clipboard') - } - const endCompetition = () => { setOpen(false) socketEndPresentation() @@ -178,17 +167,6 @@ const OperatorViewPage: React.FC = () => { .catch(console.log) } - const getTeams = async () => { - await axios - .get(`/api/competitions/${activeId}/teams`) - .then((response) => { - setTeams(response.data.items) - }) - .catch((err) => { - console.log(err) - }) - } - const getCompetitionName = async () => { await axios .get(`/api/competitions/${activeId}`) diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx index 13791b3acd7b1ee5688ba3314aa4daa6e04f7272..c2d2ff8323acd61c2a503e5c0947650950a4a892 100644 --- a/client/src/pages/views/TeamViewPage.tsx +++ b/client/src/pages/views/TeamViewPage.tsx @@ -1,21 +1,14 @@ import React, { useEffect } from 'react' -import { useHistory, useParams } from 'react-router-dom' -import { getPresentationCompetition } from '../../actions/presentation' -import { useAppDispatch, useAppSelector } from '../../hooks' -import { ViewParams } from '../../interfaces/ViewParams' +import { useAppSelector } from '../../hooks' import { socketConnect, socketJoinPresentation } from '../../sockets' import SlideDisplay from '../presentationEditor/components/SlideDisplay' import { PresentationBackground, PresentationContainer } from './styled' const TeamViewPage: React.FC = () => { - const history = useHistory() const code = useAppSelector((state) => state.presentation.code) const viewTypes = useAppSelector((state) => state.types.viewTypes) const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Team')?.id - const { competitionId }: ViewParams = useParams() - const dispatch = useAppDispatch() useEffect(() => { - dispatch(getPresentationCompetition(competitionId)) if (code && code !== '') { socketConnect() socketJoinPresentation() diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx index d69dc8b4243d680647802fe9d6f72acbd23618ba..75bf84986e57ffdb552a803ea190243ad3426c4d 100644 --- a/client/src/pages/views/ViewSelectPage.tsx +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -22,13 +22,13 @@ const ViewSelectPage: React.FC = () => { if (competitionId) { switch (viewType) { case 'Team': - return <Redirect to={`/team/competition-id=${competitionId}`} /> + return <Redirect to={`/view/team`} /> case 'Judge': - return <Redirect to={`/judge/competition-id=${competitionId}`} /> + return <Redirect to={`/view/judge`} /> case 'Audience': - return <Redirect to={`/audience/competition-id=${competitionId}`} /> + return <Redirect to={`/view/audience`} /> case 'Operator': - return <Redirect to={`/operator/competition-id=${competitionId}`} /> + return <Redirect to={`/view/operator`} /> default: return ( <ViewSelectContainer> diff --git a/client/src/pages/views/components/JudgeScoringInstructions.tsx b/client/src/pages/views/components/JudgeScoringInstructions.tsx index 86d8eaf12e9a26db08c4b9d6dd77c58cf54b6d80..f8c21667504524720523004ce9e853616c84e41f 100644 --- a/client/src/pages/views/components/JudgeScoringInstructions.tsx +++ b/client/src/pages/views/components/JudgeScoringInstructions.tsx @@ -8,7 +8,6 @@ type JudgeScoringInstructionsProps = { } const JudgeScoringInstructions = ({ question }: JudgeScoringInstructionsProps) => { - console.log(question) return ( <JudgeScoringInstructionsContainer elevation={3}> <ScoringInstructionsInner> diff --git a/client/src/reducers/competitionLoginReducer.ts b/client/src/reducers/competitionLoginReducer.ts index 6d425f97209c810cd1646c3f9722dd9ec5666ff8..72e5e22ee81aa98b735f670a581ad0c39a810ea9 100644 --- a/client/src/reducers/competitionLoginReducer.ts +++ b/client/src/reducers/competitionLoginReducer.ts @@ -36,13 +36,6 @@ export default function (state = initialState, action: AnyAction) { authenticated: true, initialized: true, } - - case Types.SET_COMPETITION_LOGIN_AUTHENTICATED: - return { - ...state, - authenticated: true, - initialized: true, - } case Types.SET_COMPETITION_LOGIN_ERRORS: return { ...state, diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index e2885501748c468eb898e833481ae219516c5946..2b08b40cfd33cc630d67f80f91921e46aa163e4d 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -7,7 +7,7 @@ import { CheckAuthenticationCompetition } from './checkAuthenticationCompetition interface SecureRouteProps extends RouteProps { component: React.ComponentType<any> rest?: any - authLevel: 'competition' | 'admin' | 'login' + authLevel: 'admin' | 'login' | 'Operator' | 'Team' | 'Judge' | 'Audience' } /** Utility component to use for authentication, replace all routes that should be private with secure routes*/ @@ -16,6 +16,7 @@ const SecureRoute: React.FC<SecureRouteProps> = ({ component: Component, authLev const compAuthenticated = useAppSelector((state) => state.competitionLogin.authenticated) const [initialized, setInitialized] = React.useState(false) const compInitialized = useAppSelector((state) => state.competitionLogin.initialized) + const viewType = useAppSelector((state) => state.competitionLogin.data?.view) React.useEffect(() => { if (authLevel === 'admin' || authLevel === 'login') { CheckAuthenticationAdmin().then(() => setInitialized(true)) @@ -32,11 +33,16 @@ const SecureRoute: React.FC<SecureRouteProps> = ({ component: Component, authLev render={(props) => (userAuthenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> ) - else if (authLevel === 'competition' && compInitialized) + else if (compInitialized && viewType && authLevel !== 'admin') { return ( - <Route {...rest} render={(props) => (compAuthenticated ? <Component {...props} /> : <Redirect to="/" />)} /> + <Route + {...rest} + render={(props) => + compAuthenticated && viewType === authLevel ? <Component {...props} /> : <Redirect to="/" /> + } + /> ) - else + } else return ( <Route {...rest} render={(props) => (userAuthenticated ? <Component {...props} /> : <Redirect to="/" />)} /> ) diff --git a/client/src/utils/checkAuthenticationCompetition.ts b/client/src/utils/checkAuthenticationCompetition.ts index db0c32360a9928459dd4cac4e65bfb1715a8353f..9877b674214fc5fae2899148fd9e57c9a0a3bc28 100644 --- a/client/src/utils/checkAuthenticationCompetition.ts +++ b/client/src/utils/checkAuthenticationCompetition.ts @@ -1,7 +1,7 @@ import axios from 'axios' import jwtDecode from 'jwt-decode' import { logoutCompetition } from '../actions/competitionLogin' -import { setPresentationCode } from '../actions/presentation' +import { getPresentationCompetition, setPresentationCode } from '../actions/presentation' import Types from '../actions/types' import store from '../store' @@ -15,19 +15,18 @@ export const CheckAuthenticationCompetition = async () => { const decodedToken: any = jwtDecode(authToken) if (decodedToken.exp * 1000 >= Date.now()) { axios.defaults.headers.common['Authorization'] = authToken - console.log(decodedToken.user_claims) await axios .get('/api/auth/test') .then((res) => { - store.dispatch({ type: Types.SET_COMPETITION_LOGIN_AUTHENTICATED }) store.dispatch({ type: Types.SET_COMPETITION_LOGIN_DATA, payload: { competition_id: decodedToken.user_claims.competition_id, team_id: decodedToken.user_claims.team_id, - view: res.data.view, + view: decodedToken.user_claims.view, }, }) + getPresentationCompetition(decodedToken.user_claims.competition_id)(store.dispatch, store.getState) setPresentationCode(decodedToken.user_claims.code)(store.dispatch) }) .catch((error) => { diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index 12aeb088dc809d9936f82b0e7e9b9588f3ab1d19..6409109ef875efc13a5c12040a100b28822ed6ae 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -12,7 +12,7 @@ list_schema = CodeDTO.list_schema @api.route("") @api.param("competition_id") class CodesList(Resource): - @protect_route(allowed_roles=["*"]) + @protect_route(allowed_roles=["*"], allowed_views=["Operator"]) def get(self, competition_id): items = dbc.get.code_list(competition_id) return list_response(list_schema.dump(items), len(items)) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 52ce4cce1d20fc277c3ee9ec46ea3566a51ec633..aa5bee3cad42e58e83b067eb1970f6b60d2cca27 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -2,9 +2,8 @@ import app.core.http_codes as codes import app.database.controller as dbc from app.apis import item_response, list_response, protect_route from app.core.dto import SlideDTO -from flask_restx import Resource -from flask_restx import reqparse from app.core.parsers import sentinel +from flask_restx import Resource, reqparse api = SlideDTO.api schema = SlideDTO.schema