From 6d0cd7067bd46dbe204feaad6237b96bc61bd9ab Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 30 Mar 2021 10:51:52 +0200 Subject: [PATCH 01/12] Fix styling of login page and rename PrivateRoute --- client/package-lock.json | 5 ++ client/package.json | 2 + client/src/App.tsx | 12 ++-- client/src/Main.tsx | 8 +-- client/src/actions/login.ts | 54 +++++++++++++++-- client/src/actions/types.ts | 22 ++++--- client/src/index.css | 6 ++ client/src/index.tsx | 21 +------ client/src/pages/admin/AdminPage.tsx | 17 ++++-- client/src/pages/login/LoginPage.tsx | 14 +---- .../src/pages/login/components/AdminLogin.tsx | 58 +++++++++++-------- client/src/pages/login/components/styled.tsx | 6 +- client/src/pages/login/styled.tsx | 6 +- client/src/reducers/allReducers.ts | 4 ++ client/src/reducers/uiReducer.ts | 29 ++++++++++ client/src/reducers/userReducer.ts | 33 +++++++++++ client/src/store.ts | 28 +++++++++ client/src/styled.tsx | 4 +- client/src/utils/SecureRoute.tsx | 29 ++++++++++ client/src/utils/checkAuthentication.ts | 34 +++++++++++ 20 files changed, 302 insertions(+), 90 deletions(-) create mode 100644 client/src/reducers/uiReducer.ts create mode 100644 client/src/reducers/userReducer.ts create mode 100644 client/src/store.ts create mode 100644 client/src/utils/SecureRoute.tsx create mode 100644 client/src/utils/checkAuthentication.ts diff --git a/client/package-lock.json b/client/package-lock.json index babf4067..e38069bc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10575,6 +10575,11 @@ "object.assign": "^4.1.2" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", diff --git a/client/package.json b/client/package.json index e5fbde97..39af2db1 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "@types/react-dom": "^17.0.0", "axios": "^0.21.1", "formik": "^2.2.6", + "jwt-decode": "^3.1.2", "react": "^17.0.1", "react-axios": "^2.0.4", "react-dom": "^17.0.1", @@ -24,6 +25,7 @@ "react-scripts": "4.0.2", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", + "redux-thunk": "^2.3.0", "styled-components": "^5.2.1", "typescript": "^4.1.3", "web-vitals": "^1.1.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index d3e92beb..61b64bee 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,14 +18,14 @@ const theme = createMuiTheme({ const App: React.FC = () => { return ( <StylesProvider injectFirst> - <Wrapper> - <MuiThemeProvider theme={theme}> - <ThemeProvider theme={theme}> + <MuiThemeProvider theme={theme}> + <ThemeProvider theme={theme}> + <Wrapper> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <Main /> - </ThemeProvider> - </MuiThemeProvider> - </Wrapper> + </Wrapper> + </ThemeProvider> + </MuiThemeProvider> </StylesProvider> ) } diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 0d85c12a..c5494c88 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -7,14 +7,15 @@ import AudienceViewPage from './pages/views/AudienceViewPage' import JudgeViewPage from './pages/views/JudgeViewPage' import ParticipantViewPage from './pages/views/ParticipantViewPage' import ViewSelectPage from './pages/views/ViewSelectPage' +import SecureRoute from './utils/SecureRoute' const Main: React.FC = () => { return ( <BrowserRouter> <Switch> - <Route exact path="/" component={LoginPage} /> - <Route path="/admin" component={AdminPage} /> - <Route path="/editor/competition-id=:id" component={PresentationEditorPage} /> + <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="/view/participant" component={ParticipantViewPage} /> <Route exact path="/view/judge" component={JudgeViewPage} /> @@ -23,5 +24,4 @@ const Main: React.FC = () => { </BrowserRouter> ) } - export default Main diff --git a/client/src/actions/login.ts b/client/src/actions/login.ts index 3ad925a1..85e3e618 100644 --- a/client/src/actions/login.ts +++ b/client/src/actions/login.ts @@ -1,12 +1,56 @@ +import axios from 'axios' +import Types from './types' export const login = () => { - return{ - type: 'SIGN_IN' - }; -}; + return { + type: 'SIGN_IN', + } +} +export const loginUser = (userData: any, history: any) => (dispatch: any) => { + dispatch({ type: Types.LOADING_UI }) + axios + .post('/users/login', userData) + .then((res) => { + const token = `Bearer ${res.data.result[0].access_token}` + localStorage.setItem('token', token) //setting token to local storage + axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios + dispatch(getUserData()) + dispatch({ type: Types.CLEAR_ERRORS }) // no error + history.push('/admin') //redirecting to admin page after login success + }) + .catch((err) => { + console.error(err) + dispatch({ + type: Types.SET_ERRORS, + payload: err.response.data, + }) + }) +} +export const getUserData = () => (dispatch: any) => { + dispatch({ type: Types.LOADING_USER }) + axios + .get('/users/') + .then((res) => { + dispatch({ + type: Types.SET_USER, + payload: res.data.result[0], + }) + }) + .catch((err) => { + console.log(err) + }) +} +export const logoutUser = () => (dispatch: any) => { + localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] + dispatch({ + type: Types.SET_UNAUTHENTICATED, + }) + window.location.href = '/' //redirect to login page +} /* // Old code that can be used for comparison @@ -26,4 +70,4 @@ export function logout() { type: Types.USER_LOGOUT, } } -*/ \ No newline at end of file +*/ diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 3d640d40..d0fb0d7c 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -1,11 +1,15 @@ export default { - AXIOS_GET: "AXIOS_GET", - AXIOS_GET_SUCCESS: "AXIOS_GET_SUCCESS", - AXIOS_GET_ERROR: "AXIOS_GET_ERROR", - - AXIOS_POST: "AXIOS_POST", - AXIOS_POST_SUCCESS: "AXIOS_POST_SUCCESS", - AXIOS_POST_ERROR: "AXIOS_POST_ERROR", - - + LOADING_UI: 'LOADING_UI', + LOADING_USER: 'LOADING_USER', + SET_USER: 'SET_USER', + SET_ERRORS: 'SET_ERRORS', + CLEAR_ERRORS: 'SET_ERRORS', + SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', + SET_AUTHENTICATED: 'SET_AUTHENTICATED', + AXIOS_GET: 'AXIOS_GET', + AXIOS_GET_SUCCESS: 'AXIOS_GET_SUCCESS', + AXIOS_GET_ERROR: 'AXIOS_GET_ERROR', + AXIOS_POST: 'AXIOS_POST', + AXIOS_POST_SUCCESS: 'AXIOS_POST_SUCCESS', + AXIOS_POST_ERROR: 'AXIOS_POST_ERROR', } diff --git a/client/src/index.css b/client/src/index.css index 7323ae85..80bfba85 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,11 +1,17 @@ body { margin: 0; + height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +html, +#root { + height: 100%; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/client/src/index.tsx b/client/src/index.tsx index 6d8b17f9..fdd9cf8e 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,29 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' -import { compose, createStore } from 'redux' import App from './App' import './index.css' -import allReducers from './reducers/allReducers' import reportWebVitals from './reportWebVitals' - -/* - TypeScript does not know the type of the property. - Therefore, you will get the error; Property ‘__REDUX_DEVTOOLS_EXTENSION_COMPOSE__’ - does not exist on type ‘Window’. Hence, you need to add the property to the global window as below. -*/ -declare global { - interface Window { - __REDUX_DEVTOOLS_EXTENSION__: typeof compose - } -} - -// Create an Advanced global store with the name "store" -// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // allows Mozilla plugin to view state in a GUI, https://github.com/zalmoxisus/redux-devtools-extension#13-use-redux-devtools-extension-package-from-npm -// const store = createStore(allReducers, composeEnhancers(applyMiddleware())) - -// simple store with plugin -const store = createStore(allReducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) +import store from './store' // Provider wraps the app component so that it can access store ReactDOM.render( diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 7f4d4ce0..5a8bf492 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -9,13 +9,15 @@ import { ListItemIcon, ListItemText, Toolbar, - Typography + Typography, } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import DashboardIcon from '@material-ui/icons/Dashboard' import MailIcon from '@material-ui/icons/Mail' import React from 'react' +import { connect } from 'react-redux' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' +import { logoutUser } from '../../actions/login' import CompetitionManager from './components/CompetitionManager' import Regions from './components/Regions' @@ -49,10 +51,13 @@ const useStyles = makeStyles((theme: Theme) => }) ) -const AdminView: React.FC = () => { +const AdminView: React.FC = (props: any) => { const classes = useStyles() const [openIndex, setOpenIndex] = React.useState(0) const { path, url } = useRouteMatch() + const handleLogout = () => { + props.logoutUser() + } return ( <div className={classes.root}> <CssBaseline /> @@ -92,7 +97,7 @@ const AdminView: React.FC = () => { <Divider /> <List> <ListItem> - <Button component={Link} to="/" type="submit" fullWidth variant="contained" color="primary"> + <Button onClick={handleLogout} type="submit" fullWidth variant="contained" color="primary"> Logga ut </Button> </ListItem> @@ -123,5 +128,7 @@ const AdminView: React.FC = () => { </div> ) } - -export default AdminView +const mapDispatchToProps = { + logoutUser, +} +export default connect(null, mapDispatchToProps)(AdminView) diff --git a/client/src/pages/login/LoginPage.tsx b/client/src/pages/login/LoginPage.tsx index a821a26f..8f8e967c 100644 --- a/client/src/pages/login/LoginPage.tsx +++ b/client/src/pages/login/LoginPage.tsx @@ -1,9 +1,8 @@ import { AppBar, Tab, Tabs } from '@material-ui/core' -import { makeStyles, Theme } from '@material-ui/core/styles' import React from 'react' import AdminLogin from './components/AdminLogin' import CompetitionLogin from './components/CompetitionLogin' -import { LoginPageContainer } from './styled' +import { LoginPageContainer, LoginPaper } from './styled' interface TabPanelProps { activeTab: number @@ -17,18 +16,11 @@ function LoginContent(props: TabPanelProps) { return <CompetitionLogin /> } -const useStyles = makeStyles((theme: Theme) => ({ - root: { - backgroundColor: theme.palette.background.paper, - }, -})) - const LoginPage: React.FC = () => { - const classes = useStyles() const [loginTab, setLoginTab] = React.useState(0) return ( <LoginPageContainer> - <div className={classes.root}> + <LoginPaper elevation={3}> <AppBar position="static"> <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> <Tab label="Konto" id="simple-tab-0" /> @@ -36,7 +28,7 @@ const LoginPage: React.FC = () => { </Tabs> </AppBar> <LoginContent activeTab={loginTab} /> - </div> + </LoginPaper> </LoginPageContainer> ) } diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index f794862a..92bf6170 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -1,9 +1,11 @@ import { Button, TextField } 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 React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import { useHistory } from 'react-router-dom' import * as Yup from 'yup' +import { loginUser } from '../../../actions/login' import { AccountLoginModel } from '../../../interfaces/models' import { LoginForm } from './styled' @@ -17,6 +19,10 @@ interface ServerResponse { message: string } +interface formError { + message: string +} + const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ model: Yup.object() .shape({ @@ -27,22 +33,20 @@ const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ error: Yup.string().optional(), }) -const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { - await axios - .post<ServerResponse>(`users/login`, values.model) - .then(() => { - actions.resetForm() - }) - .catch(({ response }) => { - console.log(response.data.message) - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} +const AdminLogin: React.FC = (props: any) => { + const [errors, setErrors] = useState({} as formError) + const [loading, setLoading] = useState(false) + useEffect(() => { + if (props.UI.errors) { + setErrors(props.UI.errors) + } + setLoading(props.UI.loading) + }, [props.UI]) + const handleAccountSubmit = (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { + props.loginUser(values.model, history) + } -const AdminLogin: React.FC = () => { + const history = useHistory() const accountInitialValues: AccountLoginFormModel = { model: { email: '', password: '' }, } @@ -73,23 +77,27 @@ const AdminLogin: React.FC = () => { type="submit" fullWidth variant="contained" - color="primary" - disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email} + color="secondary" + disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email || loading} > Logga in </Button> - {formik.errors.error ? ( + {errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {formik.errors.error} + {errors.message} </Alert> - ) : ( - <div /> )} </LoginForm> )} </Formik> ) } - -export default AdminLogin +const mapStateToProps = (state: any) => ({ + user: state.user, + UI: state.UI, +}) +const mapDispatchToProps = { + loginUser, +} +export default connect(mapStateToProps, mapDispatchToProps)(AdminLogin) diff --git a/client/src/pages/login/components/styled.tsx b/client/src/pages/login/components/styled.tsx index 66be5e0a..a384e7b9 100644 --- a/client/src/pages/login/components/styled.tsx +++ b/client/src/pages/login/components/styled.tsx @@ -1,7 +1,11 @@ +import { CircularProgress } from '@material-ui/core' import styled from 'styled-components' export const LoginForm = styled.form` display: flex; flex-direction: column; ` - +export const CenteredCircularProgress = styled(CircularProgress)` + margin-top: 10px; + align-self: center; +` diff --git a/client/src/pages/login/styled.tsx b/client/src/pages/login/styled.tsx index f00e942d..c071747e 100644 --- a/client/src/pages/login/styled.tsx +++ b/client/src/pages/login/styled.tsx @@ -1,9 +1,11 @@ +import { Paper } from '@material-ui/core' import styled from 'styled-components' export const LoginPageContainer = styled.div` display: flex; - height: 100%; justify-content: center; - align-items: center; ` +export const LoginPaper = styled(Paper)` + padding: 10px; +` diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 4903f9d7..a7e5ad40 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -2,9 +2,13 @@ import { combineReducers } from 'redux' import loggedInReducer from './isLoggedIn' +import uiReducer from './uiReducer' +import userReducer from './userReducer' const allReducers = combineReducers({ // name: state isLoggedIn: loggedInReducer, // You can write "loggedInReducer" because its the same as "loggedInReducer: loggedInReducer" + user: userReducer, + UI: uiReducer, }) export default allReducers diff --git a/client/src/reducers/uiReducer.ts b/client/src/reducers/uiReducer.ts new file mode 100644 index 00000000..8aac6daf --- /dev/null +++ b/client/src/reducers/uiReducer.ts @@ -0,0 +1,29 @@ +import Types from '../actions/types' +const initialState = { + loading: false, + errors: null, +} + +export default function (state = initialState, action: any) { + switch (action.type) { + case Types.SET_ERRORS: + return { + ...state, + loading: false, + errors: action.payload, + } + case Types.CLEAR_ERRORS: + return { + ...state, + loading: false, + errors: null, + } + case Types.LOADING_UI: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/client/src/reducers/userReducer.ts b/client/src/reducers/userReducer.ts new file mode 100644 index 00000000..fff6875b --- /dev/null +++ b/client/src/reducers/userReducer.ts @@ -0,0 +1,33 @@ +//in userReducer.ts +import Types from '../actions/types' + +const initialState = { + authenticated: false, + credentials: {}, + loading: false, +} + +export default function (state = initialState, action: any) { + switch (action.type) { + case Types.SET_AUTHENTICATED: + return { + ...state, + authenticated: true, + } + case Types.SET_UNAUTHENTICATED: + return initialState + case Types.SET_USER: + return { + authenticated: true, + loading: false, + ...action.payload, + } + case Types.LOADING_USER: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/client/src/store.ts b/client/src/store.ts new file mode 100644 index 00000000..9be02014 --- /dev/null +++ b/client/src/store.ts @@ -0,0 +1,28 @@ +import { applyMiddleware, compose, createStore } from 'redux' +import thunk from 'redux-thunk' +import allReducers from './reducers/allReducers' +/* + TypeScript does not know the type of the property. + Therefore, you will get the error; Property ‘__REDUX_DEVTOOLS_EXTENSION_COMPOSE__’ + does not exist on type ‘Window’. Hence, you need to add the property to the global window as below. +*/ +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION__: typeof compose + } +} + +const initialState = {} +const middleware = [thunk] +// Create an Advanced global store with the name "store" +// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // allows Mozilla plugin to view state in a GUI, https://github.com/zalmoxisus/redux-devtools-extension#13-use-redux-devtools-extension-package-from-npm +// const store = createStore(allReducers, composeEnhancers(applyMiddleware())) + +// simple store with plugin +const store = createStore( + allReducers, + initialState, + compose(applyMiddleware(...middleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) +) + +export default store diff --git a/client/src/styled.tsx b/client/src/styled.tsx index d26b4dbb..58cf0c24 100644 --- a/client/src/styled.tsx +++ b/client/src/styled.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components' export const Wrapper = styled.div` - padding: 20px; -` \ No newline at end of file + height: 100%; +` diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx new file mode 100644 index 00000000..e2cb4f5e --- /dev/null +++ b/client/src/utils/SecureRoute.tsx @@ -0,0 +1,29 @@ +import React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import { Redirect, Route, RouteProps } from 'react-router-dom' +import { CheckAuthentication } from './checkAuthentication' + +interface SecureRouteProps extends RouteProps { + login?: boolean + component: any + authenticated: boolean + rest?: any +} +const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, authenticated, ...rest }: any) => { + const [isReady, setReady] = useState(false) + useEffect(() => { + CheckAuthentication() + setReady(true) + }, []) + if (isReady) { + if (login) + return ( + <Route {...rest} render={(props) => (authenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> + ) + else return <Route {...rest} render={(props) => (authenticated ? <Component {...props} /> : <Redirect to="/" />)} /> + } else return null +} +const mapStateToProps = (state: any) => ({ + authenticated: state.user.authenticated, +}) +export default connect(mapStateToProps)(SecureRoute) diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts new file mode 100644 index 00000000..a8e40491 --- /dev/null +++ b/client/src/utils/checkAuthentication.ts @@ -0,0 +1,34 @@ +import axios from 'axios' +import jwtDecode from 'jwt-decode' +import { getUserData, logoutUser } from '../actions/login' +import Types from '../actions/types' +import store from '../store' + +const UnAuthorized = async () => { + store.dispatch(logoutUser()) + console.log('Unauthorized') +} + +export const CheckAuthentication = async () => { + const authToken = localStorage.token + console.log(authToken) + if (authToken) { + const decodedToken: any = jwtDecode(authToken) + if (decodedToken.exp * 1000 >= Date.now()) { + axios.defaults.headers.common['Authorization'] = authToken + await axios + .get('/users/test_auth') + .then(() => { + console.log('Authorized') + store.dispatch({ type: Types.SET_AUTHENTICATED }) + store.dispatch(getUserData()) + }) + .catch((error) => { + console.error(error) + UnAuthorized() + }) + } else { + UnAuthorized() + } + } +} -- GitLab From de42404204af3f7d9d3e62eace0112a9eabdc5f8 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 30 Mar 2021 10:52:58 +0200 Subject: [PATCH 02/12] Change color of submit button in competition llogin --- client/src/pages/login/components/CompetitionLogin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index e70508d4..42b9af4e 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -66,7 +66,7 @@ const CompetitionLogin: React.FC = () => { onBlur={formik.handleBlur} margin="normal" /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + <Button type="submit" fullWidth variant="contained" color="secondary" disabled={!formik.isValid}> Anslut till tävling </Button> {formik.errors.error ? ( -- GitLab From bd21040989cf5b3cb81c2086f4e8586fe531de4e Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Tue, 30 Mar 2021 11:15:51 +0200 Subject: [PATCH 03/12] Fix incorrect styling and refactor user redux actions --- client/src/actions/login.ts | 48 ------------------- client/src/actions/user.ts | 47 ++++++++++++++++++ client/src/pages/admin/AdminPage.tsx | 2 +- .../src/pages/login/components/AdminLogin.tsx | 2 +- client/src/styled.tsx | 1 + client/src/utils/checkAuthentication.ts | 2 +- 6 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 client/src/actions/user.ts diff --git a/client/src/actions/login.ts b/client/src/actions/login.ts index 85e3e618..b778f5c5 100644 --- a/client/src/actions/login.ts +++ b/client/src/actions/login.ts @@ -1,57 +1,9 @@ -import axios from 'axios' -import Types from './types' - export const login = () => { return { type: 'SIGN_IN', } } -export const loginUser = (userData: any, history: any) => (dispatch: any) => { - dispatch({ type: Types.LOADING_UI }) - axios - .post('/users/login', userData) - .then((res) => { - const token = `Bearer ${res.data.result[0].access_token}` - localStorage.setItem('token', token) //setting token to local storage - axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios - dispatch(getUserData()) - dispatch({ type: Types.CLEAR_ERRORS }) // no error - history.push('/admin') //redirecting to admin page after login success - }) - .catch((err) => { - console.error(err) - dispatch({ - type: Types.SET_ERRORS, - payload: err.response.data, - }) - }) -} - -export const getUserData = () => (dispatch: any) => { - dispatch({ type: Types.LOADING_USER }) - axios - .get('/users/') - .then((res) => { - dispatch({ - type: Types.SET_USER, - payload: res.data.result[0], - }) - }) - .catch((err) => { - console.log(err) - }) -} - -export const logoutUser = () => (dispatch: any) => { - localStorage.removeItem('token') - delete axios.defaults.headers.common['Authorization'] - dispatch({ - type: Types.SET_UNAUTHENTICATED, - }) - window.location.href = '/' //redirect to login page -} - /* // Old code that can be used for comparison diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts new file mode 100644 index 00000000..a126aef6 --- /dev/null +++ b/client/src/actions/user.ts @@ -0,0 +1,47 @@ +import axios from 'axios' +import Types from './types' + +export const loginUser = (userData: any, history: any) => (dispatch: any) => { + dispatch({ type: Types.LOADING_UI }) + axios + .post('/users/login', userData) + .then((res) => { + const token = `Bearer ${res.data.result[0].access_token}` + localStorage.setItem('token', token) //setting token to local storage + axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios + dispatch(getUserData()) + dispatch({ type: Types.CLEAR_ERRORS }) // no error + history.push('/admin') //redirecting to admin page after login success + }) + .catch((err) => { + console.error(err) + dispatch({ + type: Types.SET_ERRORS, + payload: err.response.data, + }) + }) +} + +export const getUserData = () => (dispatch: any) => { + dispatch({ type: Types.LOADING_USER }) + axios + .get('/users/') + .then((res) => { + dispatch({ + type: Types.SET_USER, + payload: res.data.result[0], + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const logoutUser = () => (dispatch: any) => { + localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] + dispatch({ + type: Types.SET_UNAUTHENTICATED, + }) + window.location.href = '/' //redirect to login page +} diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 5a8bf492..5a524470 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -17,7 +17,7 @@ import MailIcon from '@material-ui/icons/Mail' import React from 'react' import { connect } from 'react-redux' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' -import { logoutUser } from '../../actions/login' +import { logoutUser } from '../../actions/user' import CompetitionManager from './components/CompetitionManager' import Regions from './components/Regions' diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index 92bf6170..8b9b1dcd 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react' import { connect } from 'react-redux' import { useHistory } from 'react-router-dom' import * as Yup from 'yup' -import { loginUser } from '../../../actions/login' +import { loginUser } from '../../../actions/user' import { AccountLoginModel } from '../../../interfaces/models' import { LoginForm } from './styled' diff --git a/client/src/styled.tsx b/client/src/styled.tsx index 58cf0c24..0c17f9cc 100644 --- a/client/src/styled.tsx +++ b/client/src/styled.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components' export const Wrapper = styled.div` + padding: 10px; height: 100%; ` diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts index a8e40491..1a4e6ad7 100644 --- a/client/src/utils/checkAuthentication.ts +++ b/client/src/utils/checkAuthentication.ts @@ -1,7 +1,7 @@ import axios from 'axios' import jwtDecode from 'jwt-decode' -import { getUserData, logoutUser } from '../actions/login' import Types from '../actions/types' +import { getUserData, logoutUser } from '../actions/user' import store from '../store' const UnAuthorized = async () => { -- GitLab From c58bb6150d4b8c6c1435ce73d1aa493d83f1f6de Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 31 Mar 2021 08:19:39 +0200 Subject: [PATCH 04/12] Add popover component for createCompetition button --- client/src/actions/competitions.ts | 18 ++ client/src/actions/user.ts | 11 +- client/src/interfaces/models.ts | 6 + client/src/pages/admin/AdminPage.tsx | 19 +-- .../pages/admin/components/AddCompetition.tsx | 158 ++++++++++++++++++ .../admin/components/CompetitionManager.tsx | 31 ++-- client/src/pages/admin/components/styled.tsx | 13 +- client/src/pages/admin/styled.tsx | 13 ++ 8 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 client/src/actions/competitions.ts create mode 100644 client/src/pages/admin/components/AddCompetition.tsx create mode 100644 client/src/pages/admin/styled.tsx diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts new file mode 100644 index 00000000..20fc5ba1 --- /dev/null +++ b/client/src/actions/competitions.ts @@ -0,0 +1,18 @@ +import axios from 'axios' +import Types from './types' + +export const getCompetitions = () => (dispatch: any) => { + dispatch({ type: Types.LOADING_USER }) + axios + .get('/competitions/') + .then((res) => { + console.log(res) + dispatch({ + type: Types.SET_USER, + payload: res.data.result[0], + }) + }) + .catch((err) => { + console.log(err) + }) +} diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index a126aef6..8e394970 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -4,9 +4,10 @@ import Types from './types' export const loginUser = (userData: any, history: any) => (dispatch: any) => { dispatch({ type: Types.LOADING_UI }) axios - .post('/users/login', userData) + .post('/auth/login', userData) .then((res) => { - const token = `Bearer ${res.data.result[0].access_token}` + const token = `Bearer ${res.data.access_token}` + console.log('token', token) localStorage.setItem('token', token) //setting token to local storage axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios dispatch(getUserData()) @@ -25,7 +26,11 @@ export const loginUser = (userData: any, history: any) => (dispatch: any) => { export const getUserData = () => (dispatch: any) => { dispatch({ type: Types.LOADING_USER }) axios - .get('/users/') + .get('/users/', { + headers: { + 'Content-Type': 'application/json', + }, + }) .then((res) => { dispatch({ type: Types.SET_USER, diff --git a/client/src/interfaces/models.ts b/client/src/interfaces/models.ts index f2bc0b12..91920bd6 100644 --- a/client/src/interfaces/models.ts +++ b/client/src/interfaces/models.ts @@ -3,6 +3,12 @@ export interface AccountLoginModel { password: string } +export interface AddCompetitionModel { + name: string + city: string + year: number +} + export interface CompetitionLoginModel { code: string } diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 5a524470..2a8354ff 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -3,7 +3,6 @@ import { Button, CssBaseline, Divider, - Drawer, List, ListItem, ListItemIcon, @@ -20,8 +19,9 @@ import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' import { logoutUser } from '../../actions/user' import CompetitionManager from './components/CompetitionManager' import Regions from './components/Regions' +import { LeftDrawer } from './styled' -const drawerWidth = 240 +const drawerWidth = 250 const menuItems = ['Startsida', 'Regioner', 'Användare', 'Tävlingshanterare'] const useStyles = makeStyles((theme: Theme) => @@ -30,14 +30,9 @@ const useStyles = makeStyles((theme: Theme) => display: 'flex', }, appBar: { - width: `calc(100% - ${drawerWidth}px)`, + width: '100%', marginLeft: drawerWidth, }, - drawer: { - width: drawerWidth, - flexShrink: 0, - marginRight: drawerWidth, - }, drawerPaper: { width: drawerWidth, }, @@ -68,12 +63,12 @@ const AdminView: React.FC = (props: any) => { </Typography> </Toolbar> </AppBar> - <Drawer - className={(classes.drawer, 'background')} - variant="permanent" + <LeftDrawer + width={drawerWidth} classes={{ paper: classes.drawerPaper, }} + variant="permanent" anchor="left" > <div> @@ -103,7 +98,7 @@ const AdminView: React.FC = (props: any) => { </ListItem> </List> </div> - </Drawer> + </LeftDrawer> <main className={classes.content}> <div className={classes.toolbar} /> <Switch> diff --git a/client/src/pages/admin/components/AddCompetition.tsx b/client/src/pages/admin/components/AddCompetition.tsx new file mode 100644 index 00000000..c499f321 --- /dev/null +++ b/client/src/pages/admin/components/AddCompetition.tsx @@ -0,0 +1,158 @@ +import { Button, Popover, TextField } from '@material-ui/core' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React, { useState } from 'react' +import * as Yup from 'yup' +import { AddCompetitionModel } from '../../../interfaces/models' +import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' + +interface ServerResponse { + code: number + message: string +} + +interface AddCompetitionFormModel { + model: AddCompetitionModel + error?: string +} + +interface formError { + message: string +} + +const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ + model: Yup.object() + .shape({ + name: Yup.string().required('Namn krävs'), + city: Yup.string().required('Stad krävs'), + year: Yup.number() + .integer('År måste vara ett heltal') + .required('År krävs') + .moreThan(1999, 'År måste vara minst 2000'), + }) + .required(), + error: Yup.string().optional(), +}) + +const AddCompetition: React.FC = () => { + const [errors, setErrors] = useState({} as formError) + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + const id = open ? 'simple-popover' : undefined + const currentYear = new Date().getFullYear() + + const handleCompetitionSubmit = async ( + values: AddCompetitionFormModel, + actions: FormikHelpers<AddCompetitionFormModel> + ) => { + const params = { name: values.model.name, year: values.model.year, city_id: values.model.city, style_id: 1 } + await axios + .post<ServerResponse>('/competitions', params) + .then(() => { + actions.resetForm() + }) + .catch(({ response }) => { + console.warn(response.data) + if (response.data && response.data.message) + actions.setFieldError('error', response.data && response.data.message) + else actions.setFieldError('error', 'Something went wrong, please try again') + }) + .finally(() => { + actions.setSubmitting(false) + }) + } + const competitionInitialValues: AddCompetitionFormModel = { + model: { name: '', city: '', year: currentYear }, + } + return ( + <div> + <AddCompetitionButton color="secondary" variant="contained" onClick={handleClick}> + Ny Tävling + </AddCompetitionButton> + <Popover + id={id} + open={open} + anchorEl={anchorEl} + onClose={handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <AddCompetitionContent> + <Formik + initialValues={competitionInitialValues} + validationSchema={competitionSchema} + onSubmit={handleCompetitionSubmit} + > + {(formik) => ( + <AddCompetitionForm onSubmit={formik.handleSubmit}> + <TextField + label="Namn" + name="model.name" + helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} + error={Boolean(formik.touched.model?.name && formik.errors.model?.name)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="Stad" + name="model.city" + helperText={formik.touched.model?.city ? formik.errors.model?.city : ''} + error={Boolean(formik.touched.model?.city && formik.errors.model?.city)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="År" + name="model.year" + type="number" + defaultValue={formik.initialValues.model.year} + helperText={formik.touched.model?.year ? formik.errors.model?.year : ''} + error={Boolean(formik.touched.model?.year && formik.errors.model?.year)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <Button + type="submit" + fullWidth + variant="contained" + color="secondary" + disabled={!formik.isValid || !formik.values.model?.name || !formik.values.model?.city} + > + Skapa + </Button> + {errors.message} + {formik.errors.error && ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + )} + </AddCompetitionForm> + )} + </Formik> + </AddCompetitionContent> + </Popover> + </div> + ) +} + +export default AddCompetition diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index b37bb2a2..9d69d4e5 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -13,9 +13,12 @@ import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' -import React from 'react' +import React, { useEffect } from 'react' +import { connect } from 'react-redux' import { Link } from 'react-router-dom' -import { NewCompetitionButton, RemoveCompetition, TopBar } from './styled' +import { getCompetitions } from '../../../actions/competitions' +import AddCompetition from './AddCompetition' +import { RemoveCompetition, TopBar } from './styled' const BootstrapInput = withStyles((theme: Theme) => createStyles({ @@ -80,7 +83,7 @@ const useStyles = makeStyles((theme: Theme) => }) ) -const CompetitionManager: React.FC = () => { +const CompetitionManager: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) const classes = useStyles() const yearInitialValue = 0 @@ -97,6 +100,10 @@ const CompetitionManager: React.FC = () => { setAnchorEl(null) } + useEffect(() => { + props.getCompetitions() + }, []) + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { setSearchInput(event.target.value) } @@ -151,9 +158,7 @@ const CompetitionManager: React.FC = () => { </Select> </FormControl> </div> - <NewCompetitionButton color="secondary" variant="contained"> - Ny Tävling - </NewCompetitionButton> + <AddCompetition /> </TopBar> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> @@ -195,12 +200,16 @@ const CompetitionManager: React.FC = () => { <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleClose}>Starta</MenuItem> <MenuItem onClick={handleClose}>Duplicera</MenuItem> - <RemoveCompetition onClick={handleClose}> - Ta bort - </RemoveCompetition> + <RemoveCompetition onClick={handleClose}>Ta bort</RemoveCompetition> </Menu> </div> ) } - -export default CompetitionManager +const mapStateToProps = (state: any) => ({ + user: state.user, + UI: state.UI, +}) +const mapDispatchToProps = { + getCompetitions, +} +export default connect(mapStateToProps, mapDispatchToProps)(CompetitionManager) diff --git a/client/src/pages/admin/components/styled.tsx b/client/src/pages/admin/components/styled.tsx index 7a7f75ed..59486787 100644 --- a/client/src/pages/admin/components/styled.tsx +++ b/client/src/pages/admin/components/styled.tsx @@ -7,12 +7,19 @@ export const TopBar = styled.div` align-items: flex-end; ` -export const NewCompetitionButton = styled(Button)` +export const AddCompetitionButton = styled(Button)` margin-bottom: 8px; ` -export const RemoveCompetition = styled(MenuItem)` - color:red; +export const AddCompetitionForm = styled.form` + display: flex; + flex-direction: column; ` +export const AddCompetitionContent = styled.div` + padding: 15px; +` +export const RemoveCompetition = styled(MenuItem)` + color: red; +` diff --git a/client/src/pages/admin/styled.tsx b/client/src/pages/admin/styled.tsx new file mode 100644 index 00000000..e4687523 --- /dev/null +++ b/client/src/pages/admin/styled.tsx @@ -0,0 +1,13 @@ +import { Drawer, DrawerProps } from '@material-ui/core' +import React from 'react' +import styled from 'styled-components' + +interface leftDrawerProps extends DrawerProps { + width: number +} +export const LeftDrawer = styled((props: leftDrawerProps) => <Drawer {...props} />)` + width: ${(props) => (props.width ? props.width : '500px')}; + flex-shrink: 0; + margin-right: ${(props) => (props.width ? props.width : '500px')}; + z-index: 1; +` -- GitLab From 17013bb54c8d353b8216ed74647f9bfc2cdf9011 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 31 Mar 2021 08:26:54 +0200 Subject: [PATCH 05/12] Remove old actions and reducers and console.logs --- client/src/actions/login.ts | 25 ------------------------- client/src/reducers/allReducers.ts | 2 -- client/src/reducers/isLoggedIn.ts | 10 ---------- client/src/utils/SecureRoute.tsx | 1 + client/src/utils/checkAuthentication.ts | 3 --- 5 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 client/src/actions/login.ts delete mode 100644 client/src/reducers/isLoggedIn.ts diff --git a/client/src/actions/login.ts b/client/src/actions/login.ts deleted file mode 100644 index b778f5c5..00000000 --- a/client/src/actions/login.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const login = () => { - return { - type: 'SIGN_IN', - } -} - -/* -// Old code that can be used for comparison - -export function login(name, email, id, token) { - return { - type: Types.USER_LOGIN, - name, - email, - id, - token - } -} - -export function logout() { - return { - type: Types.USER_LOGOUT, - } -} -*/ diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index a7e5ad40..98260e16 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -1,13 +1,11 @@ // Combines all the reducers so that we only have to pass "one" reducer to the store in src/index.tsx import { combineReducers } from 'redux' -import loggedInReducer from './isLoggedIn' import uiReducer from './uiReducer' import userReducer from './userReducer' const allReducers = combineReducers({ // name: state - isLoggedIn: loggedInReducer, // You can write "loggedInReducer" because its the same as "loggedInReducer: loggedInReducer" user: userReducer, UI: uiReducer, }) diff --git a/client/src/reducers/isLoggedIn.ts b/client/src/reducers/isLoggedIn.ts deleted file mode 100644 index cf5d7b43..00000000 --- a/client/src/reducers/isLoggedIn.ts +++ /dev/null @@ -1,10 +0,0 @@ -const loggedInReducer = (state = false, action: { type: any }) => { - // isLoggedIn has an initial state of false - switch (action.type) { - case 'SIGN_IN': - return !state - default: - return state - } -} -export default loggedInReducer diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index e2cb4f5e..57daca57 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -9,6 +9,7 @@ interface SecureRouteProps extends RouteProps { authenticated: boolean rest?: any } +/** Utility component to use for authentication, replace all routes that should be private with secure routes*/ const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, authenticated, ...rest }: any) => { const [isReady, setReady] = useState(false) useEffect(() => { diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts index 1a4e6ad7..fe0537a5 100644 --- a/client/src/utils/checkAuthentication.ts +++ b/client/src/utils/checkAuthentication.ts @@ -6,12 +6,10 @@ import store from '../store' const UnAuthorized = async () => { store.dispatch(logoutUser()) - console.log('Unauthorized') } export const CheckAuthentication = async () => { const authToken = localStorage.token - console.log(authToken) if (authToken) { const decodedToken: any = jwtDecode(authToken) if (decodedToken.exp * 1000 >= Date.now()) { @@ -19,7 +17,6 @@ export const CheckAuthentication = async () => { await axios .get('/users/test_auth') .then(() => { - console.log('Authorized') store.dispatch({ type: Types.SET_AUTHENTICATED }) store.dispatch(getUserData()) }) -- GitLab From e33549aa334c2b79f484a1ae736500d0b8d119d3 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 31 Mar 2021 08:54:12 +0200 Subject: [PATCH 06/12] Fix getUserData --- client/src/actions/user.ts | 8 ++------ client/src/pages/login/components/CompetitionLogin.tsx | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index 8e394970..4f5a3650 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -26,15 +26,11 @@ export const loginUser = (userData: any, history: any) => (dispatch: any) => { export const getUserData = () => (dispatch: any) => { dispatch({ type: Types.LOADING_USER }) axios - .get('/users/', { - headers: { - 'Content-Type': 'application/json', - }, - }) + .get('/users') .then((res) => { dispatch({ type: Types.SET_USER, - payload: res.data.result[0], + payload: res.data, }) }) .catch((err) => { diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index 42b9af4e..9ebf658f 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -30,7 +30,6 @@ const handleCompetitionSubmit = async ( values: CompetitionLoginFormModel, actions: FormikHelpers<CompetitionLoginFormModel> ) => { - console.log(values.model) await axios .post<ServerResponse>(`users/login`, { code: values.model.code }) .then(() => { -- GitLab From 3988615933091908efa3d8195b5b0d940079210a Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 31 Mar 2021 09:24:36 +0200 Subject: [PATCH 07/12] Pull dev and fix redirect issue --- client/src/actions/user.ts | 8 ++++---- client/src/utils/SecureRoute.tsx | 4 ++-- client/src/utils/checkAuthentication.ts | 11 +++++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index a126aef6..30a9973a 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -4,9 +4,9 @@ import Types from './types' export const loginUser = (userData: any, history: any) => (dispatch: any) => { dispatch({ type: Types.LOADING_UI }) axios - .post('/users/login', userData) + .post('/auth/login', userData) .then((res) => { - const token = `Bearer ${res.data.result[0].access_token}` + const token = `Bearer ${res.data.access_token}` localStorage.setItem('token', token) //setting token to local storage axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios dispatch(getUserData()) @@ -25,11 +25,11 @@ export const loginUser = (userData: any, history: any) => (dispatch: any) => { export const getUserData = () => (dispatch: any) => { dispatch({ type: Types.LOADING_USER }) axios - .get('/users/') + .get('/users') .then((res) => { dispatch({ type: Types.SET_USER, - payload: res.data.result[0], + payload: res.data, }) }) .catch((err) => { diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index 57daca57..29b5aef5 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -13,10 +13,10 @@ interface SecureRouteProps extends RouteProps { const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, authenticated, ...rest }: any) => { const [isReady, setReady] = useState(false) useEffect(() => { - CheckAuthentication() - setReady(true) + CheckAuthentication().then(() => setReady(true)) }, []) if (isReady) { + console.log(login, authenticated, Component) if (login) return ( <Route {...rest} render={(props) => (authenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts index fe0537a5..dbc3113a 100644 --- a/client/src/utils/checkAuthentication.ts +++ b/client/src/utils/checkAuthentication.ts @@ -1,7 +1,7 @@ import axios from 'axios' import jwtDecode from 'jwt-decode' import Types from '../actions/types' -import { getUserData, logoutUser } from '../actions/user' +import { logoutUser } from '../actions/user' import store from '../store' const UnAuthorized = async () => { @@ -15,10 +15,13 @@ export const CheckAuthentication = async () => { if (decodedToken.exp * 1000 >= Date.now()) { axios.defaults.headers.common['Authorization'] = authToken await axios - .get('/users/test_auth') - .then(() => { + .get('/users') + .then((res) => { store.dispatch({ type: Types.SET_AUTHENTICATED }) - store.dispatch(getUserData()) + store.dispatch({ + type: Types.SET_USER, + payload: res.data, + }) }) .catch((error) => { console.error(error) -- GitLab From a79575b4cc95009ac0cb11cb80428d831020358e Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Wed, 31 Mar 2021 16:34:23 +0200 Subject: [PATCH 08/12] Finish competition manager and usage of server side filtering --- client/src/actions/competitions.ts | 14 +- client/src/actions/types.ts | 2 + client/src/interfaces/Competition.ts | 7 + client/src/interfaces/CompetitionParams.ts | 6 + .../pages/admin/components/AddCompetition.tsx | 28 ++-- .../admin/components/CompetitionManager.tsx | 144 ++++++++++-------- client/src/reducers/allReducers.ts | 2 + client/src/reducers/competitionsReducer.ts | 22 +++ client/src/utils/SecureRoute.tsx | 1 - 9 files changed, 145 insertions(+), 81 deletions(-) create mode 100644 client/src/interfaces/Competition.ts create mode 100644 client/src/interfaces/CompetitionParams.ts create mode 100644 client/src/reducers/competitionsReducer.ts diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index 20fc5ba1..473fc14c 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -1,18 +1,24 @@ import axios from 'axios' +import { CompetitionParams } from '../interfaces/CompetitionParams' import Types from './types' -export const getCompetitions = () => (dispatch: any) => { +export const getCompetitions = () => (dispatch: any, getState: any) => { + const filterParams = getState().competitions.filterParams + console.log('filter params: ', filterParams) dispatch({ type: Types.LOADING_USER }) axios - .get('/competitions/') + .get('/competitions/search', { params: getState().competitions.filterParams }) .then((res) => { console.log(res) dispatch({ - type: Types.SET_USER, - payload: res.data.result[0], + type: Types.SET_COMPETITIONS, + payload: res.data, }) }) .catch((err) => { console.log(err) }) } +export const setFilterParams = (params: CompetitionParams) => (dispatch: any) => { + dispatch({ type: Types.SET_FILTER_PARAMS, payload: params }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index d0fb0d7c..cfdc8290 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -6,6 +6,8 @@ export default { CLEAR_ERRORS: 'SET_ERRORS', SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', SET_AUTHENTICATED: 'SET_AUTHENTICATED', + SET_COMPETITIONS: 'SET_COMPETITIONS', + SET_FILTER_PARAMS: 'SET_FILTER_PARAMS', AXIOS_GET: 'AXIOS_GET', AXIOS_GET_SUCCESS: 'AXIOS_GET_SUCCESS', AXIOS_GET_ERROR: 'AXIOS_GET_ERROR', diff --git a/client/src/interfaces/Competition.ts b/client/src/interfaces/Competition.ts new file mode 100644 index 00000000..433ae5a3 --- /dev/null +++ b/client/src/interfaces/Competition.ts @@ -0,0 +1,7 @@ +export interface Competition { + name: string + city_id: number + style_id: number + year: number + id: number +} diff --git a/client/src/interfaces/CompetitionParams.ts b/client/src/interfaces/CompetitionParams.ts new file mode 100644 index 00000000..4ff211c5 --- /dev/null +++ b/client/src/interfaces/CompetitionParams.ts @@ -0,0 +1,6 @@ +export interface CompetitionParams { + name?: string + year?: number + city_id?: number + style_id?: number +} diff --git a/client/src/pages/admin/components/AddCompetition.tsx b/client/src/pages/admin/components/AddCompetition.tsx index c499f321..b14a2200 100644 --- a/client/src/pages/admin/components/AddCompetition.tsx +++ b/client/src/pages/admin/components/AddCompetition.tsx @@ -2,8 +2,10 @@ import { Button, Popover, TextField } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' -import React, { useState } from 'react' +import React from 'react' +import { connect } from 'react-redux' import * as Yup from 'yup' +import { getCompetitions } from '../../../actions/competitions' import { AddCompetitionModel } from '../../../interfaces/models' import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' @@ -17,10 +19,6 @@ interface AddCompetitionFormModel { error?: string } -interface formError { - message: string -} - const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ model: Yup.object() .shape({ @@ -35,14 +33,11 @@ const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ error: Yup.string().optional(), }) -const AddCompetition: React.FC = () => { - const [errors, setErrors] = useState({} as formError) +const AddCompetition: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) } - const handleClose = () => { setAnchorEl(null) } @@ -60,6 +55,8 @@ const AddCompetition: React.FC = () => { .post<ServerResponse>('/competitions', params) .then(() => { actions.resetForm() + setAnchorEl(null) + props.getCompetitions() }) .catch(({ response }) => { console.warn(response.data) @@ -72,7 +69,7 @@ const AddCompetition: React.FC = () => { }) } const competitionInitialValues: AddCompetitionFormModel = { - model: { name: '', city: '', year: currentYear }, + model: { name: '', city: props.cityId, year: currentYear }, } return ( <div> @@ -113,6 +110,7 @@ const AddCompetition: React.FC = () => { <TextField label="Stad" name="model.city" + defaultValue={formik.initialValues.model.city} helperText={formik.touched.model?.city ? formik.errors.model?.city : ''} error={Boolean(formik.touched.model?.city && formik.errors.model?.city)} onChange={formik.handleChange} @@ -139,7 +137,6 @@ const AddCompetition: React.FC = () => { > Skapa </Button> - {errors.message} {formik.errors.error && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> @@ -154,5 +151,10 @@ const AddCompetition: React.FC = () => { </div> ) } - -export default AddCompetition +const mapStateToProps = (state: any) => ({ + cityId: state.user.city_id, +}) +const mapDispatchToProps = { + getCompetitions, +} +export default connect(mapStateToProps, mapDispatchToProps)(AddCompetition) diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index 9d69d4e5..c3e9dba7 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -13,10 +13,13 @@ import TableContainer from '@material-ui/core/TableContainer' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import axios from 'axios' import React, { useEffect } from 'react' import { connect } from 'react-redux' import { Link } from 'react-router-dom' -import { getCompetitions } from '../../../actions/competitions' +import { getCompetitions, setFilterParams } from '../../../actions/competitions' +import { Competition } from '../../../interfaces/Competition' +import { CompetitionParams } from '../../../interfaces/CompetitionParams' import AddCompetition from './AddCompetition' import { RemoveCompetition, TopBar } from './styled' @@ -44,38 +47,10 @@ const BootstrapInput = withStyles((theme: Theme) => }) )(InputBase) -function createCompetition(name: string, region: string, year: number, id: number) { - return { name, region, year, id } -} - -const competitions = [ - createCompetition('Tävling 1', 'Stockholm', 2021, 1), - createCompetition('Tävling 2', 'Stockholm', 2020, 2), - createCompetition('Tävling 3', 'Sala', 2020, 3), - createCompetition('Tävling 4', 'Sundsvall', 2020, 4), - createCompetition('Tävling 5', 'Linköping', 2020, 5), - createCompetition('Tävling 6', 'Linköping', 2020, 6), - createCompetition('Tävling 7', 'Sala', 2019, 7), - createCompetition('Tävling 8', 'Stockholm', 2019, 8), - createCompetition('Tävling 9', 'Stockholm', 2019, 9), - createCompetition('Tävling 10', 'Lidköping', 2019, 10), - createCompetition('Tävling 11', 'Stockholm', 2019, 11), - createCompetition('Tävling 12', 'Sala', 2018, 12), - createCompetition('Tävling 13', 'Tornby', 2018, 13), -] - -const regions = competitions - .map((competition) => competition.region) - .filter((competition, index, self) => self.indexOf(competition) === index) - -const years = competitions - .map((competition) => competition.year) - .filter((competition, index, self) => self.indexOf(competition) === index) - const useStyles = makeStyles((theme: Theme) => createStyles({ table: { - width: 1500, // TODO: Shrink table when smaller screen + width: '100%', }, margin: { margin: theme.spacing(1), @@ -85,19 +60,33 @@ const useStyles = makeStyles((theme: Theme) => const CompetitionManager: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const [activeId, setActiveId] = React.useState<number | undefined>(undefined) + const [filterParams, setFilterParams] = React.useState({} as CompetitionParams) + const competitions = props.competitions as Competition[] const classes = useStyles() const yearInitialValue = 0 - const regionInitialValue = '' const noFilterText = 'Alla' - const [searchInput, setSearchInput] = React.useState('') - const [year, setYear] = React.useState(yearInitialValue) - const [region, setRegion] = React.useState(regionInitialValue) - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + let timerHandle: number | undefined + const regions = competitions + ? competitions + .map((competition) => competition.city_id) + .filter((competition, index, self) => self.indexOf(competition) === index) + : undefined + + const years = competitions + ? competitions + .map((competition) => competition.year) + .filter((competition, index, self) => self.indexOf(competition) === index) + : undefined + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { setAnchorEl(event.currentTarget) + setActiveId(id) } const handleClose = () => { setAnchorEl(null) + setActiveId(undefined) } useEffect(() => { @@ -105,7 +94,32 @@ const CompetitionManager: React.FC = (props: any) => { }, []) const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { - setSearchInput(event.target.value) + if (timerHandle) { + clearTimeout(timerHandle) + timerHandle = undefined + } + //Only updates filter and api 100ms after last input was made + timerHandle = window.setTimeout(() => handleFilterChange({ ...filterParams, name: event.target.value }), 100) + } + + const handleDeleteCompetition = async () => { + if (activeId) { + await axios + .delete(`/competitions/${activeId}`) + .then(() => { + setAnchorEl(null) + props.getCompetitions() + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } + } + + const handleFilterChange = (newParams: CompetitionParams) => { + setFilterParams(newParams) + props.setFilterParams(newParams) + props.getCompetitions() } return ( @@ -125,17 +139,25 @@ const CompetitionManager: React.FC = (props: any) => { <Select labelId="demo-customized-select-label" id="demo-customized-select" - value={region === regionInitialValue ? noFilterText : region} + value={filterParams.city_id ? filterParams.city_id : noFilterText} input={<BootstrapInput />} > - <MenuItem value={noFilterText} onClick={() => setRegion(regionInitialValue)}> + <MenuItem + value={noFilterText} + onClick={() => handleFilterChange({ ...filterParams, city_id: undefined })} + > {noFilterText} </MenuItem> - {regions.map((text) => ( - <MenuItem key={text} value={text} onClick={() => setRegion(text)}> - {text} - </MenuItem> - ))} + {regions && + regions.map((region) => ( + <MenuItem + key={region} + value={region} + onClick={() => handleFilterChange({ ...filterParams, city_id: region })} + > + {region} + </MenuItem> + ))} </Select> </FormControl> <FormControl className={classes.margin}> @@ -144,17 +166,18 @@ const CompetitionManager: React.FC = (props: any) => { </InputLabel> <Select id="demo-customized-select" - value={year === yearInitialValue ? noFilterText : year} + value={filterParams.year ? filterParams.year : noFilterText} input={<BootstrapInput />} > - <MenuItem value={noFilterText} onClick={() => setYear(yearInitialValue)}> + <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, year: undefined })}> {noFilterText} </MenuItem> - {years.map((year) => ( - <MenuItem key={year} value={year} onClick={() => setYear(year)}> - {year} - </MenuItem> - ))} + {years && + years.map((year) => ( + <MenuItem key={year} value={year} onClick={() => handleFilterChange({ ...filterParams, year: year })}> + {year} + </MenuItem> + ))} </Select> </FormControl> </div> @@ -171,24 +194,18 @@ const CompetitionManager: React.FC = (props: any) => { </TableRow> </TableHead> <TableBody> - {competitions - .filter((row) => { - const nameOkay = row.name.match(RegExp(searchInput, 'i')) //Makes sure name matches search input case insensitively - const yearOkay = year == yearInitialValue || row.year == year - const regionOkay = region == regionInitialValue || row.region == region - return yearOkay && regionOkay && nameOkay - }) - .map((row) => ( + {competitions && + competitions.map((row) => ( <TableRow key={row.name}> <TableCell scope="row"> <Button color="primary" component={Link} to={`/editor/competition-id=${row.id}`}> {row.name} </Button> </TableCell> - <TableCell align="right">{row.region}</TableCell> + <TableCell align="right">{row.city_id}</TableCell> <TableCell align="right">{row.year}</TableCell> <TableCell align="right"> - <Button onClick={handleClick}> + <Button onClick={(event) => handleClick(event, row.id)}> <MoreHorizIcon /> </Button> </TableCell> @@ -200,16 +217,17 @@ const CompetitionManager: React.FC = (props: any) => { <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleClose}>Starta</MenuItem> <MenuItem onClick={handleClose}>Duplicera</MenuItem> - <RemoveCompetition onClick={handleClose}>Ta bort</RemoveCompetition> + <RemoveCompetition onClick={handleDeleteCompetition}>Ta bort</RemoveCompetition> </Menu> </div> ) } const mapStateToProps = (state: any) => ({ - user: state.user, - UI: state.UI, + competitions: state.competitions.competitions, }) + const mapDispatchToProps = { getCompetitions, + setFilterParams, } export default connect(mapStateToProps, mapDispatchToProps)(CompetitionManager) diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 98260e16..69a01448 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -1,6 +1,7 @@ // Combines all the reducers so that we only have to pass "one" reducer to the store in src/index.tsx import { combineReducers } from 'redux' +import competitionsReducer from './competitionsReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -8,5 +9,6 @@ const allReducers = combineReducers({ // name: state user: userReducer, UI: uiReducer, + competitions: competitionsReducer, }) export default allReducers diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts new file mode 100644 index 00000000..a01aac4b --- /dev/null +++ b/client/src/reducers/competitionsReducer.ts @@ -0,0 +1,22 @@ +import Types from '../actions/types' +const initialState = { + competitions: [], + filterParams: {}, +} + +export default function (state = initialState, action: any) { + switch (action.type) { + case Types.SET_COMPETITIONS: + return { + ...state, + competitions: action.payload, + } + case Types.SET_FILTER_PARAMS: + return { + ...state, + filterParams: action.payload, + } + default: + return state + } +} diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index 29b5aef5..19b4716c 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -16,7 +16,6 @@ const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, CheckAuthentication().then(() => setReady(true)) }, []) if (isReady) { - console.log(login, authenticated, Component) if (login) return ( <Route {...rest} render={(props) => (authenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> -- GitLab From 333209980e6d2c849f73716ba40ad5fd596845d7 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 1 Apr 2021 19:12:11 +0200 Subject: [PATCH 09/12] Finish up competition view --- client/src/actions/cities.ts | 18 ++++++ client/src/actions/competitions.ts | 8 ++- client/src/actions/types.ts | 2 + client/src/interfaces/City.ts | 4 ++ .../pages/admin/components/AddCompetition.tsx | 55 ++++++++++++++----- .../admin/components/CompetitionManager.tsx | 27 +++++---- client/src/reducers/allReducers.ts | 2 + client/src/reducers/citiesReducer.ts | 14 +++++ client/src/reducers/competitionsReducer.ts | 7 +++ 9 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 client/src/actions/cities.ts create mode 100644 client/src/interfaces/City.ts create mode 100644 client/src/reducers/citiesReducer.ts diff --git a/client/src/actions/cities.ts b/client/src/actions/cities.ts new file mode 100644 index 00000000..a4ff5897 --- /dev/null +++ b/client/src/actions/cities.ts @@ -0,0 +1,18 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getCities = () => (dispatch: AppDispatch) => { + dispatch({ type: Types.LOADING_USER }) + axios + .get('/misc/cities') + .then((res) => { + dispatch({ + type: Types.SET_CITIES, + payload: res.data, + }) + }) + .catch((err) => { + console.log(err) + }) +} diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index ae011ef0..b280b72b 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -20,11 +20,15 @@ export const getCompetitions = () => (dispatch: AppDispatch, getState: () => Roo .then((res) => { dispatch({ type: Types.SET_COMPETITIONS, - payload: res.data, + payload: res.data.competitions, }) dispatch({ type: Types.SET_COMPETITIONS_TOTAL, - payload: res.data.length, + payload: res.data.total, + }) + dispatch({ + type: Types.SET_COMPETITIONS_COUNT, + payload: res.data.count, }) }) .catch((err) => { diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 063ac178..d265a692 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -9,6 +9,8 @@ export default { SET_COMPETITIONS: 'SET_COMPETITIONS', SET_COMPETITIONS_FILTER_PARAMS: 'SET_COMPETITIONS_FILTER_PARAMS', SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', + SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', + SET_CITIES: 'SET_CITIES', AXIOS_GET: 'AXIOS_GET', AXIOS_GET_SUCCESS: 'AXIOS_GET_SUCCESS', AXIOS_GET_ERROR: 'AXIOS_GET_ERROR', diff --git a/client/src/interfaces/City.ts b/client/src/interfaces/City.ts new file mode 100644 index 00000000..e5190b6b --- /dev/null +++ b/client/src/interfaces/City.ts @@ -0,0 +1,4 @@ +export interface City { + id: number + name: string +} diff --git a/client/src/pages/admin/components/AddCompetition.tsx b/client/src/pages/admin/components/AddCompetition.tsx index b35860b5..76b9fc81 100644 --- a/client/src/pages/admin/components/AddCompetition.tsx +++ b/client/src/pages/admin/components/AddCompetition.tsx @@ -1,4 +1,4 @@ -import { Button, Popover, TextField } from '@material-ui/core' +import { Button, FormControl, InputLabel, MenuItem, Popover, TextField } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' import axios from 'axios' import { Formik, FormikHelpers } from 'formik' @@ -6,6 +6,7 @@ import React from 'react' import * as Yup from 'yup' import { getCompetitions } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' +import { City } from '../../../interfaces/City' import { AddCompetitionModel } from '../../../interfaces/models' import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' @@ -18,12 +19,13 @@ interface AddCompetitionFormModel { model: AddCompetitionModel error?: string } +const noCitySelected = 'Välj stad' const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ model: Yup.object() .shape({ name: Yup.string().required('Namn krävs'), - city: Yup.string().required('Stad krävs'), + city: Yup.string().required('Stad krävs').notOneOf([noCitySelected], 'Välj en stad'), year: Yup.number() .integer('År måste vara ett heltal') .required('År krävs') @@ -35,6 +37,8 @@ const competitionSchema: Yup.SchemaOf<AddCompetitionFormModel> = Yup.object({ const AddCompetition: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) + const [selectedCity, setSelectedCity] = React.useState<City | undefined>() + const cities = useAppSelector((state) => state.cities) const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) } @@ -49,12 +53,16 @@ const AddCompetition: React.FC = (props: any) => { const dispatch = useAppDispatch() const id = open ? 'simple-popover' : undefined const currentYear = new Date().getFullYear() - const handleCompetitionSubmit = async ( values: AddCompetitionFormModel, actions: FormikHelpers<AddCompetitionFormModel> ) => { - const params = { name: values.model.name, year: values.model.year, city_id: values.model.city, style_id: 1 } + const params = { + name: values.model.name, + year: values.model.year, + city_id: selectedCity?.id as number, + style_id: 1, + } await axios .post<ServerResponse>('/competitions', params) .then(() => { @@ -72,8 +80,9 @@ const AddCompetition: React.FC = (props: any) => { actions.setSubmitting(false) }) } + const competitionInitialValues: AddCompetitionFormModel = { - model: { name: '', city: userCityString, year: currentYear }, + model: { name: '', city: noCitySelected, year: currentYear }, } return ( <div> @@ -111,16 +120,32 @@ const AddCompetition: React.FC = (props: any) => { onBlur={formik.handleBlur} margin="normal" /> - <TextField - label="Stad" - name="model.city" - defaultValue={formik.initialValues.model.city} - helperText={formik.touched.model?.city ? formik.errors.model?.city : ''} - error={Boolean(formik.touched.model?.city && formik.errors.model?.city)} - onChange={formik.handleChange} - onBlur={formik.handleBlur} - margin="normal" - /> + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Region + </InputLabel> + <TextField + select + name="model.city" + id="standard-select-currency" + value={selectedCity ? selectedCity.name : noCitySelected} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={Boolean(formik.errors.model?.city && formik.touched.model?.city)} + helperText={formik.touched.model?.city && formik.errors.model?.city} + margin="normal" + > + <MenuItem value={noCitySelected} onClick={() => setSelectedCity(undefined)}> + {noCitySelected} + </MenuItem> + {cities && + cities.map((city) => ( + <MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}> + {city.name} + </MenuItem> + ))} + </TextField> + </FormControl> <TextField label="År" name="model.year" diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index a82182a0..2ac80cca 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -16,6 +16,7 @@ import MoreHorizIcon from '@material-ui/icons/MoreHoriz' import axios from 'axios' import React, { useEffect } from 'react' import { Link } from 'react-router-dom' +import { getCities } from '../../../actions/cities' import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' import { CompetitionFilterParams } from '../../../interfaces/CompetitionFilterParams' @@ -63,17 +64,12 @@ const CompetitionManager: React.FC = (props: any) => { const [page, setPage] = React.useState(0) const competitions = useAppSelector((state) => state.competitions.competitions) const filterParams = useAppSelector((state) => state.competitions.filterParams) + const cities = useAppSelector((state) => state.cities) const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() let timerHandle: number | undefined - const regions = competitions - ? competitions - .map((competition) => competition.city_id) - .filter((competition, index, self) => self.indexOf(competition) === index) - : undefined - const years = competitions ? competitions .map((competition) => competition.year) @@ -92,6 +88,7 @@ const CompetitionManager: React.FC = (props: any) => { useEffect(() => { dispatch(getCompetitions()) + dispatch(getCities()) }, []) const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { @@ -130,6 +127,7 @@ const CompetitionManager: React.FC = (props: any) => { <FormControl className={classes.margin}> <InputLabel shrink id="demo-customized-textbox"> Sök + {filterParams.cityId} </InputLabel> <BootstrapInput id="demo-customized-textbox" onChange={onSearchChange} /> </FormControl> @@ -137,23 +135,24 @@ const CompetitionManager: React.FC = (props: any) => { <InputLabel shrink id="demo-customized-select-native"> Region </InputLabel> + <Select labelId="demo-customized-select-label" id="demo-customized-select" - value={filterParams.cityId ? filterParams.cityId : noFilterText} + value={filterParams.cityId ? cities.find((city) => filterParams.cityId === city.id)?.name : noFilterText} input={<BootstrapInput />} > <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, cityId: undefined })}> {noFilterText} </MenuItem> - {regions && - regions.map((region) => ( + {cities && + cities.map((city) => ( <MenuItem - key={region} - value={region} - onClick={() => handleFilterChange({ ...filterParams, cityId: region })} + key={city.name} + value={city.name} + onClick={() => handleFilterChange({ ...filterParams, cityId: city.id })} > - {region} + {city.name} </MenuItem> ))} </Select> @@ -200,7 +199,7 @@ const CompetitionManager: React.FC = (props: any) => { {row.name} </Button> </TableCell> - <TableCell align="right">{row.city_id}</TableCell> + <TableCell align="right">{cities.find((city) => city.id === row.city_id)?.name || ''}</TableCell> <TableCell align="right">{row.year}</TableCell> <TableCell align="right"> <Button onClick={(event) => handleClick(event, row.id)}> diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 69a01448..b12b5346 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -1,6 +1,7 @@ // Combines all the reducers so that we only have to pass "one" reducer to the store in src/index.tsx import { combineReducers } from 'redux' +import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -10,5 +11,6 @@ const allReducers = combineReducers({ user: userReducer, UI: uiReducer, competitions: competitionsReducer, + cities: citiesReducer, }) export default allReducers diff --git a/client/src/reducers/citiesReducer.ts b/client/src/reducers/citiesReducer.ts new file mode 100644 index 00000000..871123d8 --- /dev/null +++ b/client/src/reducers/citiesReducer.ts @@ -0,0 +1,14 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { City } from '../interfaces/City' + +const initialState: City[] = [] + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_CITIES: + return action.payload as City[] + default: + return state + } +} diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts index 75ee7700..b3003d4a 100644 --- a/client/src/reducers/competitionsReducer.ts +++ b/client/src/reducers/competitionsReducer.ts @@ -6,12 +6,14 @@ import { CompetitionFilterParams } from './../interfaces/CompetitionFilterParams interface CompetitionState { competitions: Competition[] total: number + count: number filterParams: CompetitionFilterParams } const initialState: CompetitionState = { competitions: [], total: 0, + count: 0, filterParams: { pageSize: 10, page: 0 }, } @@ -32,6 +34,11 @@ export default function (state = initialState, action: AnyAction) { ...state, total: action.payload as number, } + case Types.SET_COMPETITIONS_COUNT: + return { + ...state, + count: action.payload as number, + } default: return state } -- GitLab From 6742d7f77533b5720265e9ed9311ae3ed1e371b9 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Fri, 2 Apr 2021 11:28:58 +0200 Subject: [PATCH 10/12] Finish competition screen --- client/src/actions/competitions.ts | 1 - .../pages/admin/components/AddCompetition.tsx | 4 +- .../admin/components/CompetitionManager.tsx | 105 ++++++------------ client/src/pages/admin/components/styled.tsx | 12 +- 4 files changed, 43 insertions(+), 79 deletions(-) diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index b280b72b..9cdf134a 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -14,7 +14,6 @@ export const getCompetitions = () => (dispatch: AppDispatch, getState: () => Roo page: currentParams.page, year: currentParams.year, } - dispatch({ type: Types.LOADING_USER }) axios .get('/competitions/search', { params }) .then((res) => { diff --git a/client/src/pages/admin/components/AddCompetition.tsx b/client/src/pages/admin/components/AddCompetition.tsx index 76b9fc81..3bb54db0 100644 --- a/client/src/pages/admin/components/AddCompetition.tsx +++ b/client/src/pages/admin/components/AddCompetition.tsx @@ -47,9 +47,6 @@ const AddCompetition: React.FC = (props: any) => { } const open = Boolean(anchorEl) - const userCityString = useAppSelector((state) => - state.user.userInfo !== null ? state.user.userInfo.cityId.toString() : '' - ) const dispatch = useAppDispatch() const id = open ? 'simple-popover' : undefined const currentYear = new Date().getFullYear() @@ -69,6 +66,7 @@ const AddCompetition: React.FC = (props: any) => { actions.resetForm() setAnchorEl(null) dispatch(getCompetitions()) + setSelectedCity(undefined) }) .catch(({ response }) => { console.warn(response.data) diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index 2ac80cca..60d88758 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -1,11 +1,10 @@ -import { Button, Menu, TablePagination } from '@material-ui/core' +import { Button, Menu, TablePagination, TextField, Typography } from '@material-ui/core' import FormControl from '@material-ui/core/FormControl' -import InputBase from '@material-ui/core/InputBase' import InputLabel from '@material-ui/core/InputLabel' import MenuItem from '@material-ui/core/MenuItem' import Paper from '@material-ui/core/Paper' import Select from '@material-ui/core/Select' -import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import Table from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' import TableCell from '@material-ui/core/TableCell' @@ -21,31 +20,7 @@ import { getCompetitions, setFilterParams } from '../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../hooks' import { CompetitionFilterParams } from '../../../interfaces/CompetitionFilterParams' import AddCompetition from './AddCompetition' -import { RemoveCompetition, TopBar } from './styled' - -const BootstrapInput = withStyles((theme: Theme) => - createStyles({ - root: { - 'label + &': { - marginTop: theme.spacing(3), - }, - }, - input: { - borderRadius: 4, - position: 'relative', - backgroundColor: theme.palette.background.paper, - border: '1px solid #ced4da', - fontSize: 16, - padding: '10px 26px 10px 12px', - transition: theme.transitions.create(['border-color', 'box-shadow']), - '&:focus': { - borderRadius: 4, - borderColor: '#80bdff', - boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)', - }, - }, - }) -)(InputBase) +import { FilterContainer, RemoveCompetition, TopBar, YearFilterTextField } from './styled' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -61,21 +36,16 @@ const useStyles = makeStyles((theme: Theme) => const CompetitionManager: React.FC = (props: any) => { const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) const [activeId, setActiveId] = React.useState<number | undefined>(undefined) - const [page, setPage] = React.useState(0) + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) const competitions = useAppSelector((state) => state.competitions.competitions) const filterParams = useAppSelector((state) => state.competitions.filterParams) + const competitionTotal = useAppSelector((state) => state.competitions.total) const cities = useAppSelector((state) => state.cities) const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() - let timerHandle: number | undefined - const years = competitions - ? competitions - .map((competition) => competition.year) - .filter((competition, index, self) => self.indexOf(competition) === index) - : undefined - + // let timerHandle: number | undefined const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { setAnchorEl(event.currentTarget) setActiveId(id) @@ -90,14 +60,14 @@ const CompetitionManager: React.FC = (props: any) => { dispatch(getCompetitions()) dispatch(getCities()) }, []) - const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { clearTimeout(timerHandle) - timerHandle = undefined + setTimerHandle(undefined) } //Only updates filter and api 100ms after last input was made - timerHandle = window.setTimeout(() => handleFilterChange({ ...filterParams, name: event.target.value }), 100) + setTimerHandle(window.setTimeout(() => dispatch(getCompetitions()), 100)) + dispatch(setFilterParams({ ...filterParams, name: event.target.value })) } const handleDeleteCompetition = async () => { @@ -115,7 +85,6 @@ const CompetitionManager: React.FC = (props: any) => { } const handleFilterChange = (newParams: CompetitionFilterParams) => { - // setFilterParams(newParams) dispatch(setFilterParams(newParams)) dispatch(getCompetitions()) } @@ -123,24 +92,21 @@ const CompetitionManager: React.FC = (props: any) => { return ( <div> <TopBar> - <div> - <FormControl className={classes.margin}> - <InputLabel shrink id="demo-customized-textbox"> - Sök - {filterParams.cityId} - </InputLabel> - <BootstrapInput id="demo-customized-textbox" onChange={onSearchChange} /> - </FormControl> + <FilterContainer> + <TextField + className={classes.margin} + value={filterParams.name || ''} + onChange={onSearchChange} + label="Sök" + ></TextField> <FormControl className={classes.margin}> <InputLabel shrink id="demo-customized-select-native"> Region </InputLabel> - <Select labelId="demo-customized-select-label" id="demo-customized-select" value={filterParams.cityId ? cities.find((city) => filterParams.cityId === city.id)?.name : noFilterText} - input={<BootstrapInput />} > <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, cityId: undefined })}> {noFilterText} @@ -157,27 +123,15 @@ const CompetitionManager: React.FC = (props: any) => { ))} </Select> </FormControl> - <FormControl className={classes.margin}> - <InputLabel shrink id="demo-customized-select-label"> - År - </InputLabel> - <Select - id="demo-customized-select" - value={filterParams.year ? filterParams.year : noFilterText} - input={<BootstrapInput />} - > - <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, year: undefined })}> - {noFilterText} - </MenuItem> - {years && - years.map((year) => ( - <MenuItem key={year} value={year} onClick={() => handleFilterChange({ ...filterParams, year: year })}> - {year} - </MenuItem> - ))} - </Select> - </FormControl> - </div> + <YearFilterTextField + label="År" + name="model.year" + type="number" + value={filterParams.year || new Date().getFullYear()} + onChange={(event) => handleFilterChange({ ...filterParams, year: +event.target.value })} + margin="normal" + /> + </FilterContainer> <AddCompetition /> </TopBar> <TableContainer component={Paper}> @@ -208,6 +162,9 @@ const CompetitionManager: React.FC = (props: any) => { </TableCell> </TableRow> ))} + {(!competitions || competitions.length === 0) && ( + <Typography>Inga tävlingar hittades med nuvarande filter</Typography> + )} </TableBody> </Table> </TableContainer> @@ -215,9 +172,9 @@ const CompetitionManager: React.FC = (props: any) => { component="div" rowsPerPageOptions={[]} rowsPerPage={filterParams.pageSize} - count={-1} //{props.total} - page={page} - onChangePage={(event, newPage) => setPage(newPage)} + count={competitionTotal} + page={filterParams.page} + onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} /> <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleClose}>Starta</MenuItem> diff --git a/client/src/pages/admin/components/styled.tsx b/client/src/pages/admin/components/styled.tsx index 59486787..69040178 100644 --- a/client/src/pages/admin/components/styled.tsx +++ b/client/src/pages/admin/components/styled.tsx @@ -1,4 +1,4 @@ -import { Button, MenuItem } from '@material-ui/core' +import { Button, MenuItem, TextField } from '@material-ui/core' import styled from 'styled-components' export const TopBar = styled.div` @@ -23,3 +23,13 @@ export const AddCompetitionContent = styled.div` export const RemoveCompetition = styled(MenuItem)` color: red; ` + +export const YearFilterTextField = styled(TextField)` + width: 70px; +` + +export const FilterContainer = styled.div` + display: flex; + align-items: flex-end; + margin: left 10px; +` -- GitLab From af824e3d475dfb3dfa2a94dd2f33938303712343 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Fri, 2 Apr 2021 13:52:56 +0200 Subject: [PATCH 11/12] Fix tests --- client/src/App.test.tsx | 12 ++++++------ client/src/App.tsx | 7 ++++++- client/src/actions/cities.ts | 9 +++------ client/src/actions/competitions.ts | 4 ++-- client/src/actions/user.ts | 8 ++++---- .../admin/components/CompetitionManager.test.tsx | 6 +++++- .../pages/admin/components/CompetitionManager.tsx | 8 +++----- client/src/reducers/userReducer.ts | 2 +- client/src/utils/SecureRoute.tsx | 8 ++++---- client/src/utils/checkAuthentication.ts | 8 +++++--- 10 files changed, 39 insertions(+), 33 deletions(-) diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx index f27dce75..2eca756c 100644 --- a/client/src/App.test.tsx +++ b/client/src/App.test.tsx @@ -1,13 +1,13 @@ +import { render } from '@testing-library/react' import React from 'react' import { Provider } from 'react-redux' import App from './App' import store from './store' test('renders app', () => { - ;<Provider store={store}> - render( - <App />) - </Provider> - // const linkElement = screen.getByText(/learn react/i) - // expect(linkElement).toBeInTheDocument() + render( + <Provider store={store}> + <App /> + </Provider> + ) }) diff --git a/client/src/App.tsx b/client/src/App.tsx index 61b64bee..fab142f7 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,9 @@ -import { createMuiTheme, MuiThemeProvider, StylesProvider } from '@material-ui/core' +import { + MuiThemeProvider, + StylesProvider, + unstable_createMuiStrictModeTheme as createMuiTheme, +} from '@material-ui/core' +// unstable version of createMuiTheme is required for React.StrictMode currently import React from 'react' import { ThemeProvider } from 'styled-components' import Main from './Main' diff --git a/client/src/actions/cities.ts b/client/src/actions/cities.ts index a4ff5897..bbd43387 100644 --- a/client/src/actions/cities.ts +++ b/client/src/actions/cities.ts @@ -2,9 +2,8 @@ import axios from 'axios' import { AppDispatch } from './../store' import Types from './types' -export const getCities = () => (dispatch: AppDispatch) => { - dispatch({ type: Types.LOADING_USER }) - axios +export const getCities = () => async (dispatch: AppDispatch) => { + await axios .get('/misc/cities') .then((res) => { dispatch({ @@ -12,7 +11,5 @@ export const getCities = () => (dispatch: AppDispatch) => { payload: res.data, }) }) - .catch((err) => { - console.log(err) - }) + .catch((err) => {}) } diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index 9cdf134a..35f12d31 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -3,7 +3,7 @@ import { CompetitionFilterParams } from '../interfaces/CompetitionFilterParams' import { AppDispatch, RootState } from './../store' import Types from './types' -export const getCompetitions = () => (dispatch: AppDispatch, getState: () => RootState) => { +export const getCompetitions = () => async (dispatch: AppDispatch, getState: () => RootState) => { const currentParams: CompetitionFilterParams = getState().competitions.filterParams // Send params in snake-case for api const params = { @@ -14,7 +14,7 @@ export const getCompetitions = () => (dispatch: AppDispatch, getState: () => Roo page: currentParams.page, year: currentParams.year, } - axios + await axios .get('/competitions/search', { params }) .then((res) => { dispatch({ diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index f37d0fef..104722eb 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -4,9 +4,9 @@ import { AppDispatch } from '../store' import { AdminLoginData } from './../interfaces/AdminLoginData' import Types from './types' -export const loginUser = (userData: AdminLoginData, history: History) => (dispatch: AppDispatch) => { +export const loginUser = (userData: AdminLoginData, history: History) => async (dispatch: AppDispatch) => { dispatch({ type: Types.LOADING_UI }) - axios + await axios .post('/auth/login', userData) .then((res) => { const token = `Bearer ${res.data.access_token}` @@ -25,9 +25,9 @@ export const loginUser = (userData: AdminLoginData, history: History) => (dispat }) } -export const getUserData = () => (dispatch: AppDispatch) => { +export const getUserData = () => async (dispatch: AppDispatch) => { dispatch({ type: Types.LOADING_USER }) - axios + await axios .get('/users') .then((res) => { dispatch({ diff --git a/client/src/pages/admin/components/CompetitionManager.test.tsx b/client/src/pages/admin/components/CompetitionManager.test.tsx index 2a92138f..9f893760 100644 --- a/client/src/pages/admin/components/CompetitionManager.test.tsx +++ b/client/src/pages/admin/components/CompetitionManager.test.tsx @@ -1,12 +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 CompetitionManager from './CompetitionManager' it('renders competition manager', () => { render( <BrowserRouter> - <CompetitionManager /> + <Provider store={store}> + <CompetitionManager /> + </Provider> </BrowserRouter> ) }) diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index 60d88758..67db4079 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -44,8 +44,6 @@ const CompetitionManager: React.FC = (props: any) => { const classes = useStyles() const noFilterText = 'Alla' const dispatch = useAppDispatch() - - // let timerHandle: number | undefined const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { setAnchorEl(event.currentTarget) setActiveId(id) @@ -162,11 +160,11 @@ const CompetitionManager: React.FC = (props: any) => { </TableCell> </TableRow> ))} - {(!competitions || competitions.length === 0) && ( - <Typography>Inga tävlingar hittades med nuvarande filter</Typography> - )} </TableBody> </Table> + {(!competitions || competitions.length === 0) && ( + <Typography>Inga tävlingar hittades med nuvarande filter</Typography> + )} </TableContainer> <TablePagination component="div" diff --git a/client/src/reducers/userReducer.ts b/client/src/reducers/userReducer.ts index 8b651426..b60221e6 100644 --- a/client/src/reducers/userReducer.ts +++ b/client/src/reducers/userReducer.ts @@ -17,7 +17,7 @@ interface UserState { const initialState: UserState = { authenticated: false, - loading: false, + loading: true, userInfo: null, } diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx index fb881c7e..df4eab9d 100644 --- a/client/src/utils/SecureRoute.tsx +++ b/client/src/utils/SecureRoute.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { Redirect, Route, RouteProps } from 'react-router-dom' import { useAppSelector } from '../hooks' import { CheckAuthentication } from './checkAuthentication' @@ -11,11 +11,11 @@ interface SecureRouteProps extends RouteProps { /** Utility component to use for authentication, replace all routes that should be private with secure routes*/ const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, ...rest }: SecureRouteProps) => { const authenticated = useAppSelector((state) => state.user.authenticated) - const [isReady, setReady] = useState(false) + const loading = useAppSelector((state) => state.user.loading) useEffect(() => { - CheckAuthentication().then(() => setReady(true)) + CheckAuthentication() }, []) - if (isReady) { + if (!loading) { if (login) return ( <Route {...rest} render={(props) => (authenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts index 1d306a22..41294b14 100644 --- a/client/src/utils/checkAuthentication.ts +++ b/client/src/utils/checkAuthentication.ts @@ -4,17 +4,19 @@ import Types from '../actions/types' import { logoutUser } from '../actions/user' import store from '../store' -const UnAuthorized = async () => { +const UnAuthorized = () => { logoutUser()(store.dispatch) } -export const CheckAuthentication = async () => { +export const CheckAuthentication = () => { const authToken = localStorage.token if (authToken) { const decodedToken: any = jwtDecode(authToken) if (decodedToken.exp * 1000 >= Date.now()) { axios.defaults.headers.common['Authorization'] = authToken - await axios + store.dispatch({ type: Types.LOADING_USER }) + console.log('loading user') + axios .get('/users') .then((res) => { store.dispatch({ type: Types.SET_AUTHENTICATED }) -- GitLab From 8295f773f3bee826fbfe8be8b920a97c3c038642 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Fri, 2 Apr 2021 17:23:22 +0200 Subject: [PATCH 12/12] Fix broken tests --- client/src/__mocks__/axios.js | 3 ++ client/src/actions/cities.ts | 2 +- .../components/CompetitionManager.test.tsx | 40 +++++++++++++++++++ .../admin/components/CompetitionManager.tsx | 3 +- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 client/src/__mocks__/axios.js diff --git a/client/src/__mocks__/axios.js b/client/src/__mocks__/axios.js new file mode 100644 index 00000000..c3547a13 --- /dev/null +++ b/client/src/__mocks__/axios.js @@ -0,0 +1,3 @@ +export default { + get: jest.fn().mockImplementation(), +} diff --git a/client/src/actions/cities.ts b/client/src/actions/cities.ts index bbd43387..381d0a04 100644 --- a/client/src/actions/cities.ts +++ b/client/src/actions/cities.ts @@ -11,5 +11,5 @@ export const getCities = () => async (dispatch: AppDispatch) => { payload: res.data, }) }) - .catch((err) => {}) + .catch((err) => console.log(err)) } diff --git a/client/src/pages/admin/components/CompetitionManager.test.tsx b/client/src/pages/admin/components/CompetitionManager.test.tsx index 9f893760..3fb03b69 100644 --- a/client/src/pages/admin/components/CompetitionManager.test.tsx +++ b/client/src/pages/admin/components/CompetitionManager.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react' +import mockedAxios from 'axios' import React from 'react' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' @@ -6,6 +7,45 @@ import store from '../../../store' import CompetitionManager from './CompetitionManager' it('renders competition manager', () => { + const cityRes: any = { + data: [ + { + id: 1, + name: 'Link\u00f6ping', + }, + { + id: 2, + name: 'Stockholm', + }, + ], + } + const compRes: any = { + data: { + competitions: [ + { + id: 21, + name: 'ggff', + year: 2021, + style_id: 1, + city_id: 1, + }, + { + id: 22, + name: 'sssss', + year: 2021, + style_id: 1, + city_id: 1, + }, + ], + count: 2, + total: 3, + }, + } + + ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { + if (path === '/competitions/search') return Promise.resolve(compRes) + else return Promise.resolve(cityRes) + }) render( <BrowserRouter> <Provider store={store}> diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index 67db4079..c6803b03 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -55,9 +55,10 @@ const CompetitionManager: React.FC = (props: any) => { } useEffect(() => { - dispatch(getCompetitions()) dispatch(getCities()) + dispatch(getCompetitions()) }, []) + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { if (timerHandle) { clearTimeout(timerHandle) -- GitLab