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