diff --git a/client/src/Main.tsx b/client/src/Main.tsx index bc0a2e85175899a8a06f8c34c4e8ae04be337a56..b7c74b556e3e072f8da96d328617d7461977b4c9 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -23,7 +23,7 @@ const Main: React.FC = () => { <SecureRoute login exact path="/" component={LoginPage} /> <SecureRoute path="/admin" component={AdminPage} /> <SecureRoute path="/editor/competition-id=:id" component={PresentationEditorPage} /> - <Route exact path="/view" component={ViewSelectPage} /> + <Route exact path="/:code" component={ViewSelectPage} /> <Route exact path="/participant/id=:id&code=:code" component={ParticipantViewPage} /> <SecureRoute exact path="/presenter/id=:id&code=:code" component={PresenterViewPage} /> <Route exact path="/judge/id=:id&code=:code" component={JudgeViewPage} /> diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff177a2bc82f8d37174f4f2df4bd050ebb3822de --- /dev/null +++ b/client/src/actions/competitionLogin.ts @@ -0,0 +1,23 @@ +import axios from 'axios' +import { History } from 'history' +import { AppDispatch } from '../store' +import { AccountLoginModel } from './../interfaces/FormModels' +import Types from './types' + +export const loginCompetition = (code: string, history: History) => async (dispatch: AppDispatch) => { + dispatch({ type: Types.LOADING_COMPETITION_LOGIN }) + await axios + .post('/api/auth/login/code', { code }) + .then((res) => { + console.log(code, res.data[0]) + dispatch({ type: Types.CLEAR_COMPETITION_LOGIN_ERRORS }) // no error + // history.push('/admin') //redirecting to admin page after login success + if (res.data && res.data[0] && res.data[0].view_type_id) { + history.push(`/${code}`) + } + }) + .catch((err) => { + dispatch({ type: Types.SET_COMPETITION_LOGIN_ERRORS, payload: err && err.response && err.response.data }) + console.log(err) + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 5932deb0b24a027b5fc329113f5513253a82ff2a..512572bcf6f73dbbeb4c76fcdc1207b00333149a 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -1,6 +1,7 @@ export default { LOADING_UI: 'LOADING_UI', LOADING_USER: 'LOADING_USER', + LOADING_COMPETITION_LOGIN: 'LOADING_COMPETITION_LOGIN', SET_ROLES: 'SET_ROLES', SET_USER: 'SET_USER', SET_SEARCH_USERS: 'SET_SEARCH_USERS', @@ -9,6 +10,8 @@ export default { SET_SEARCH_USERS_TOTAL_COUNT: 'SET_SEARCH_USERS_TOTAL_COUNT', SET_ERRORS: 'SET_ERRORS', CLEAR_ERRORS: 'CLEAR_ERRORS', + SET_COMPETITION_LOGIN_ERRORS: 'SET_COMPETITION_LOGIN_ERRORS', + CLEAR_COMPETITION_LOGIN_ERRORS: 'CLEAR_COMPETITION_LOGIN_ERRORS', SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', SET_AUTHENTICATED: 'SET_AUTHENTICATED', SET_COMPETITIONS: 'SET_COMPETITIONS', diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 1b543083da7fbfb1e5c1a48d44a40a2d7fdcc17e..a8b1746aa8ee4e7564a6f13dc074b11082002747 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -16,6 +16,7 @@ import ExitToAppIcon from '@material-ui/icons/ExitToApp' import LocationCityIcon from '@material-ui/icons/LocationCity' import PeopleIcon from '@material-ui/icons/People' import SettingsOverscanIcon from '@material-ui/icons/SettingsOverscan' +import axios from 'axios' import React, { useEffect } from 'react' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { getCities } from '../../actions/cities' diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index 7f478caf14a7dae4cbc866e0f67dbf2efce49efd..964fb8abd148a9a0735206e9dbe3288cbe72ea3a 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -1,4 +1,4 @@ -import { Button, TextField } from '@material-ui/core' +import { Button, TextField, Typography } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import { Formik, FormikHelpers } from 'formik' import React, { useEffect, useState } from 'react' @@ -83,7 +83,8 @@ const AdminLogin: React.FC = () => { {errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {errors.message} + <Typography>Någonting gick fel. Kontrollera</Typography> + <Typography>dina användaruppgifter och försök igen</Typography> </Alert> )} {loading && <CenteredCircularProgress color="secondary" />} diff --git a/client/src/pages/login/components/CompetitionLogin.test.tsx b/client/src/pages/login/components/CompetitionLogin.test.tsx index 29213c943578afb01b27199d0a3fcb5f2db44092..862880bc5006288dc76b022dc9ec171c607b13d5 100644 --- a/client/src/pages/login/components/CompetitionLogin.test.tsx +++ b/client/src/pages/login/components/CompetitionLogin.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' import CompetitionLogin from './CompetitionLogin' it('renders competition login', () => { - render(<CompetitionLogin />) + render( + <Provider store={store}> + <CompetitionLogin /> + </Provider> + ) }) diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index 75f73c5be187569fcc5adec2baf591a7cbdd62d3..d89cafcf8216197de081d65550116e2e06b56f22 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -1,53 +1,44 @@ -import { Button, TextField } from '@material-ui/core' +import { Button, TextField, Typography } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' -import React from 'react' +import { useHistory } from 'react-router-dom' +import React, { useEffect, useState } from 'react' import * as Yup from 'yup' +import { loginCompetition } from '../../../actions/competitionLogin' +import { useAppDispatch, useAppSelector } from '../../../hooks' import { CompetitionLoginModel } from '../../../interfaces/FormModels' -import { LoginForm } from './styled' +import { CenteredCircularProgress, LoginForm } from './styled' interface CompetitionLoginFormModel { model: CompetitionLoginModel error?: string } -interface ServerResponse { - code: number +interface formError { message: string } const competitionSchema: Yup.SchemaOf<CompetitionLoginFormModel> = Yup.object({ model: Yup.object() .shape({ - code: Yup.string().required('Mata in kod').min(6, 'Koden måste vara minst 6 tecken'), + code: Yup.string().required('Mata in kod').length(6, 'Koden måste vara 6 tecken'), }) .required(), error: Yup.string().optional(), }) -const handleCompetitionSubmit = async ( - values: CompetitionLoginFormModel, - actions: FormikHelpers<CompetitionLoginFormModel> -) => { - await axios - .post<ServerResponse>(`users/login`, { code: values.model.code }) - .then(() => { - actions.resetForm() - }) - .catch(({ response }) => { - console.log(response.data.message) - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} - const CompetitionLogin: React.FC = () => { + const dispatch = useAppDispatch() + const history = useHistory() + const errors = useAppSelector((state) => state.competitionLogin.errors) + const loading = useAppSelector((state) => state.competitionLogin.loading) const competitionInitialValues: CompetitionLoginFormModel = { model: { code: '' }, } + const handleCompetitionSubmit = async (values: CompetitionLoginFormModel) => { + dispatch(loginCompetition(values.model.code, history)) + } return ( <Formik initialValues={competitionInitialValues} @@ -68,14 +59,14 @@ const CompetitionLogin: React.FC = () => { <Button type="submit" fullWidth variant="contained" color="secondary" disabled={!formik.isValid}> Anslut till tävling </Button> - {formik.errors.error ? ( + {errors && errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {formik.errors.error} + <Typography>En tävling med den koden hittades ej.</Typography> + <Typography>kontrollera koden och försök igen</Typography> </Alert> - ) : ( - <div /> )} + {loading && <CenteredCircularProgress color="secondary" />} </LoginForm> )} </Formik> diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 60018624457a6242cbf1f27f4c8201fdb12ebb89..66450f3a1f8ac98f14423d7040e215c0c96881c6 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -14,6 +14,7 @@ import { socket_connect } from '../../sockets' import { SlideListItem } from '../presentationEditor/styled' import JudgeScoreDisplay from './components/JudgeScoreDisplay' import SlideDisplay from './components/SlideDisplay' +import { useHistory } from 'react-router-dom' import { Content, JudgeAnswersLabel, @@ -41,6 +42,7 @@ const useStyles = makeStyles((theme: Theme) => const JudgeViewPage: React.FC = () => { const classes = useStyles() + const history = useHistory() const { id, code }: ViewParams = useParams() const dispatch = useAppDispatch() const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0) @@ -50,12 +52,13 @@ const JudgeViewPage: React.FC = () => { setActiveSlideIndex(index) dispatch(setCurrentSlide(slides[index])) } - useEffect(() => { socket_connect() dispatch(getPresentationCompetition(id)) dispatch(getPresentationTeams(id)) dispatch(setPresentationCode(code)) + //hides the url so people can't sneak peak + history.push('judge') }, []) return ( diff --git a/client/src/pages/views/ParticipantViewPage.test.tsx b/client/src/pages/views/ParticipantViewPage.test.tsx index 85360e4ffe754f5887e9b654c45d0c921a67a3a0..c0950b3c6d3dfeaf1b1ce2d1293829c10651fe33 100644 --- a/client/src/pages/views/ParticipantViewPage.test.tsx +++ b/client/src/pages/views/ParticipantViewPage.test.tsx @@ -1,13 +1,16 @@ import { render } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' import store from '../../store' import ParticipantViewPage from './ParticipantViewPage' it('renders participant view page', () => { render( - <Provider store={store}> - <ParticipantViewPage /> - </Provider> + <BrowserRouter> + <Provider store={store}> + <ParticipantViewPage /> + </Provider> + </BrowserRouter> ) }) diff --git a/client/src/pages/views/ParticipantViewPage.tsx b/client/src/pages/views/ParticipantViewPage.tsx index 55c28af06cff72a24862acaa03e8066d8a8f4a0c..f531ad76db15bc5ba8578b4015e2d5feed8ea1e8 100644 --- a/client/src/pages/views/ParticipantViewPage.tsx +++ b/client/src/pages/views/ParticipantViewPage.tsx @@ -1,7 +1,13 @@ -import React from 'react' +import React, { useEffect } from 'react' import SlideDisplay from './components/SlideDisplay' +import { useHistory } from 'react-router-dom' const ParticipantViewPage: React.FC = () => { + const history = useHistory() + useEffect(() => { + //hides the url so people can't sneak peak + history.push('participant') + }, []) return <SlideDisplay /> } diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx index 40aa252d6770ad4a2359f2038c3da7e09b47c14b..1abeee92c519c3aae977d667f68470ca78cafe2d 100644 --- a/client/src/pages/views/PresenterViewPage.tsx +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -52,7 +52,6 @@ const PresenterViewPage: React.FC = () => { const [openAlert, setOpen] = React.useState(false) const theme = useTheme() const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) - const teams = useAppSelector((state) => state.presentation.teams) const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) const { id, code }: ViewParams = useParams() diff --git a/client/src/pages/views/ViewSelectPage.test.tsx b/client/src/pages/views/ViewSelectPage.test.tsx index 2e6499776df4a1eaeeac94e34e40bd70defdbdbb..83b71db05a13abc23b629003877c5698d99b4481 100644 --- a/client/src/pages/views/ViewSelectPage.test.tsx +++ b/client/src/pages/views/ViewSelectPage.test.tsx @@ -1,12 +1,26 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' +import store from '../../store' import ViewSelectPage from './ViewSelectPage' +import mockedAxios from 'axios' +import { act } from 'react-dom/test-utils' -it('renders view select page', () => { - render( - <BrowserRouter> - <ViewSelectPage /> - </BrowserRouter> - ) +it('renders view select page', async () => { + await act(async () => { + const res = { + data: {}, + } + ;(mockedAxios.post as jest.Mock).mockImplementation(() => { + return Promise.resolve(res) + }) + render( + <BrowserRouter> + <Provider store={store}> + <ViewSelectPage /> + </Provider> + </BrowserRouter> + ) + }) }) diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx index 2781f60c9959ebe0a1298a2f3b04cf0e690c9e9e..3c3599edeaf3d9d46ff6c462506d196d79d1a9f7 100644 --- a/client/src/pages/views/ViewSelectPage.tsx +++ b/client/src/pages/views/ViewSelectPage.tsx @@ -1,22 +1,63 @@ import Button from '@material-ui/core/Button' -import React from 'react' +import React, { useEffect, useState } from 'react' import { Link, useRouteMatch } from 'react-router-dom' import { ViewSelectButtonGroup, ViewSelectContainer } from './styled' +import { useParams } from 'react-router-dom' +import { CircularProgress, Typography } from '@material-ui/core' +import ParticipantViewPage from './ParticipantViewPage' +import axios from 'axios' +import PresenterViewPage from './PresenterViewPage' +import JudgeViewPage from './JudgeViewPage' +import AudienceViewPage from './AudienceViewPage' +import { useAppSelector } from '../../hooks' +interface ViewSelectParams { + code: string +} const ViewSelectPage: React.FC = () => { - const url = useRouteMatch().url + const [loading, setLoading] = useState(true) + const [error, setError] = useState(false) + const [viewTypeId, setViewTypeId] = useState(undefined) + const [competitionId, setCompetitionId] = useState<number | undefined>(undefined) + const { code }: ViewSelectParams = useParams() + const viewType = useAppSelector((state) => state.types.viewTypes.find((viewType) => viewType.id === viewTypeId)?.name) + + const renderView = (viewTypeId: number | undefined) => { + //Renders the correct view depending on view type + if (competitionId) { + switch (viewType) { + case 'Team': + return <ParticipantViewPage /> + case 'Judge': + return <JudgeViewPage /> + case 'Audience': + return <AudienceViewPage /> + default: + return <Typography>Inkorrekt vy</Typography> + } + } + } + + useEffect(() => { + axios + .post('/api/auth/login/code', { code }) + .then((response) => { + setLoading(false) + setViewTypeId(response.data[0].view_type_id) + setCompetitionId(response.data[0].competition_id) + }) + .catch(() => { + setLoading(false) + setError(true) + }) + }, []) + return ( <ViewSelectContainer> <ViewSelectButtonGroup> - <Button color="primary" variant="contained" component={Link} to={`${url}/participant`}> - Deltagarvy - </Button> - <Button color="primary" variant="contained" component={Link} to={`${url}/audience`}> - Åskådarvy - </Button> - <Button color="primary" variant="contained" component={Link} to={`${url}/judge`}> - Domarvy - </Button> + {loading && <CircularProgress />} + {!loading && renderView(viewTypeId)} + {error && <Typography>Något gick fel, dubbelkolla koden och försök igen</Typography>} </ViewSelectButtonGroup> </ViewSelectContainer> ) diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 398ec0a71669a6eab2aaf851ac0ea0f10b226b91..90cb24144612c5a45f611ad99871c54c51969bbd 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -2,6 +2,7 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' +import competitionLoginReducer from './competitionLoginReducer' import competitionsReducer from './competitionsReducer' import editorReducer from './editorReducer' import mediaReducer from './mediaReducer' @@ -26,5 +27,6 @@ const allReducers = combineReducers({ types: typesReducer, media: mediaReducer, statistics: statisticsReducer, + competitionLogin: competitionLoginReducer, }) export default allReducers diff --git a/client/src/reducers/competitionLoginReducer.ts b/client/src/reducers/competitionLoginReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..81c426a4a297f138fdfa6350e17a3cd4fc72d3fd --- /dev/null +++ b/client/src/reducers/competitionLoginReducer.ts @@ -0,0 +1,38 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' + +interface UIError { + message: string +} + +interface UserState { + loading: boolean + errors: null | UIError +} + +const initialState: UserState = { + loading: false, + errors: null, +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_COMPETITION_LOGIN_ERRORS: + return { + errors: action.payload as UIError, + loading: false, + } + case Types.CLEAR_COMPETITION_LOGIN_ERRORS: + return { + loading: false, + errors: null, + } + case Types.LOADING_COMPETITION_LOGIN: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index d249fec586a0d9974a3802bb55700cc635f57792..87d7f1d19041760131560db52de5dada55dc34fe 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -79,7 +79,7 @@ class AuthLoginCode(Resource): if not verify_code(code): api.abort(codes.BAD_REQUEST, "Invalid code") - item_code = dbc.get.code_by_code(code, True, "A presentation with that code does not exist") + item_code = dbc.get.code_by_code(code) return item_response(CodeDTO.schema.dump(item_code)), codes.OK diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 57acb27ef28a17a19a7fa5a6b6f7de6229090e10..3e5e7f872c2866c70131dd1b4f1205c3bd131798 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -34,7 +34,7 @@ def one(db_type, id): def code_by_code(code): """ Gets the code object associated with the provided code. """ - return Code.query.filter(Code.code == code.upper()).first_extended() + return Code.query.filter(Code.code == code.upper()).first_extended( True, "A presentation with that code does not exist") def code_list(competition_id):