From bfba672f7a65109db842023b0dea66eb53cd748c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 09:26:39 +0200
Subject: [PATCH 01/29] Return status code 409 if deleting a record fails due
 to integrity constraint

---
 server/app/core/http_codes.py            | 1 +
 server/app/database/controller/delete.py | 4 ++++
 2 files changed, 5 insertions(+)

diff --git a/server/app/core/http_codes.py b/server/app/core/http_codes.py
index f6f19ed1..1f6f5524 100644
--- a/server/app/core/http_codes.py
+++ b/server/app/core/http_codes.py
@@ -8,6 +8,7 @@ BAD_REQUEST = 400
 UNAUTHORIZED = 401
 FORBIDDEN = 403
 NOT_FOUND = 404
+CONFLICT = 409
 GONE = 410
 INTERNAL_SERVER_ERROR = 500
 SERVICE_UNAVAILABLE = 503
diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py
index 65737cb6..51070ed9 100644
--- a/server/app/database/controller/delete.py
+++ b/server/app/database/controller/delete.py
@@ -7,6 +7,7 @@ import app.database.controller as dbc
 from app.core import db
 from app.database.models import Whitelist
 from flask_restx import abort
+from sqlalchemy.exc import IntegrityError
 
 
 def default(item):
@@ -15,6 +16,9 @@ def default(item):
     try:
         db.session.delete(item)
         db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint")
     except:
         db.session.rollback()
         abort(
-- 
GitLab


From cbbe3d5ebfcae236525874f93924f0a774e7ebd6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 09:40:37 +0200
Subject: [PATCH 02/29] Timer for slide is now nullable

---
 client/src/interfaces/ApiModels.ts     | 2 +-
 client/src/interfaces/ApiRichModels.ts | 2 +-
 server/app/database/models.py          | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts
index 9bcd24c1..254c2bfd 100644
--- a/client/src/interfaces/ApiModels.ts
+++ b/client/src/interfaces/ApiModels.ts
@@ -36,7 +36,7 @@ export interface Slide {
   competition_id: number
   id: number
   order: number
-  timer: number
+  timer: number | null
   title: string
   background_image?: Media
 }
diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts
index 253565a4..9eac0181 100644
--- a/client/src/interfaces/ApiRichModels.ts
+++ b/client/src/interfaces/ApiRichModels.ts
@@ -13,7 +13,7 @@ export interface RichCompetition {
 export interface RichSlide {
   id: number
   order: number
-  timer: number
+  timer: number | null
   title: string
   competition_id: number
   background_image?: Media
diff --git a/server/app/database/models.py b/server/app/database/models.py
index 97eb6097..b352c688 100644
--- a/server/app/database/models.py
+++ b/server/app/database/models.py
@@ -146,7 +146,7 @@ class Slide(db.Model):
     order = db.Column(db.Integer, nullable=False)
     title = db.Column(db.String(STRING_SIZE), nullable=False, default="")
     body = db.Column(db.Text, nullable=False, default="")
-    timer = db.Column(db.Integer, nullable=False, default=0)
+    timer = db.Column(db.Integer, nullable=True)
     settings = db.Column(db.Text, nullable=False, default="{}")
     competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False)
 
-- 
GitLab


From 2a8c232311c4983631615f63ad66c58a96a176b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 10:05:54 +0200
Subject: [PATCH 03/29] Fix bug when copying components

---
 server/app/database/controller/add.py  | 22 ++++++++++++----------
 server/app/database/controller/copy.py |  1 +
 2 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py
index d0adf5a0..f0b205ff 100644
--- a/server/app/database/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -64,7 +64,7 @@ def db_add(item):
     return item
 
 
-def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data):
+def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, copy=False, **data):
     """
     Adds a component to the slide at the specified
     coordinates with the provided size and data.
@@ -77,15 +77,17 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, **data):
             current_app.config["UPLOADED_PHOTOS_DEST"],
             filename,
         )
-        with Image.open(path) as im:
-            h = im.height
-            w = im.width
-
-    largest = max(w, h)
-    if largest > 600:
-        ratio = 600 / largest
-        w *= ratio
-        h *= ratio
+
+        if not copy:
+            with Image.open(path) as im:
+                h = im.height
+                w = im.width
+
+            largest = max(w, h)
+            if largest > 600:
+                ratio = 600 / largest
+                w *= ratio
+                h *= ratio
 
     if type_id == ID_TEXT_COMPONENT:
         item = db_add(
diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py
index dd807334..07816e3e 100644
--- a/server/app/database/controller/copy.py
+++ b/server/app/database/controller/copy.py
@@ -68,6 +68,7 @@ def component(item_component, slide_id_new, view_type_id):
         item_component.y,
         item_component.w,
         item_component.h,
+        copy=True,
         **data,
     )
 
-- 
GitLab


From 38e0c3f64401f3058f3471ea7c965d7526907235 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 10:13:53 +0200
Subject: [PATCH 04/29] Update to current slide when joining competition and
 default to first slide

---
 server/app/core/sockets.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 71871cda..a1675b2f 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -98,7 +98,7 @@ def start_presentation(data: Dict) -> None:
 
     presentations[competition_id] = {
         "clients": {request.sid: {"view_type": "Operator"}},
-        "slide": None,
+        "slide": 0,
         "timer": {"enabled": False, "start_value": None, "value": None},
     }
 
@@ -185,6 +185,8 @@ def join_presentation(data: Dict) -> None:
 
     logger.info(f"Client '{request.sid}' joined competition '{competition_id}'")
 
+    emit("set_slide", {"slide_order": presentations[competition_id]["slide"]})
+
 
 @protect_route(allowed_views=["Operator"])
 @sio.on("set_slide")
-- 
GitLab


From 050838564975052f2c1165c3b02c441205514b5e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 10:47:17 +0200
Subject: [PATCH 05/29] Return 409 if something cant be done due to integrity
 constraint

---
 server/app/database/controller/add.py  | 2 ++
 server/app/database/controller/edit.py | 9 ++++++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py
index f0b205ff..751d5675 100644
--- a/server/app/database/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -46,6 +46,8 @@ def db_add(item):
         db.session.add(item)
         db.session.commit()
         db.session.refresh(item)
+    except (exc.IntegrityError):
+        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint")
     except (exc.SQLAlchemyError, exc.DBAPIError):
         db.session.rollback()
         # SQL errors such as item already exists
diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py
index efb49096..0934f58d 100644
--- a/server/app/database/controller/edit.py
+++ b/server/app/database/controller/edit.py
@@ -2,8 +2,11 @@
 This file contains functionality to get data from the database.
 """
 
+import app.core.http_codes as codes
 from app.core import db
 from app.core.parsers import sentinel
+from flask_restx.errors import abort
+from sqlalchemy import exc
 
 
 def switch_order(item1, item2):
@@ -49,6 +52,10 @@ def default(item, **kwargs):
             raise AttributeError(f"Item of type {type(item)} has no attribute '{key}'")
         if value is not sentinel:
             setattr(item, key, value)
-    db.session.commit()
+    try:
+        db.session.commit()
+    except exc.IntegrityError:
+        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint")
+
     db.session.refresh(item)
     return item
-- 
GitLab


From e20f6adc380b4f57c29642df484e6953bbf638cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 10:47:54 +0200
Subject: [PATCH 06/29] Remove unused function switch_order in edit

---
 server/app/database/controller/edit.py | 21 ---------------------
 1 file changed, 21 deletions(-)

diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py
index 0934f58d..197f1fff 100644
--- a/server/app/database/controller/edit.py
+++ b/server/app/database/controller/edit.py
@@ -9,27 +9,6 @@ from flask_restx.errors import abort
 from sqlalchemy import exc
 
 
-def switch_order(item1, item2):
-    """ Switches order between two slides. """
-
-    old_order = item1.order
-    new_order = item2.order
-
-    item2.order = -1
-    db.session.commit()
-    db.session.refresh(item2)
-
-    item1.order = new_order
-    db.session.commit()
-    db.session.refresh(item1)
-
-    item2.order = old_order
-    db.session.commit()
-    db.session.refresh(item2)
-
-    return item1
-
-
 def default(item, **kwargs):
     """
     For every keyword argument, set that attribute on item to the given value.
-- 
GitLab


From 19efb902a004054e53c543040b4ce6043f993c60 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 15:34:18 +0200
Subject: [PATCH 07/29] Add error message when unable to remove region

---
 client/src/pages/admin/regions/Regions.tsx           | 12 ++++++++----
 .../components/slideSettingsComponents/Timer.tsx     |  2 +-
 client/src/pages/views/components/Timer.tsx          |  2 +-
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/client/src/pages/admin/regions/Regions.tsx b/client/src/pages/admin/regions/Regions.tsx
index 57f4c101..0458266f 100644
--- a/client/src/pages/admin/regions/Regions.tsx
+++ b/client/src/pages/admin/regions/Regions.tsx
@@ -1,4 +1,4 @@
-import { Button, Menu, Typography } from '@material-ui/core'
+import { Button, Menu, Snackbar, Typography } from '@material-ui/core'
 import Paper from '@material-ui/core/Paper'
 import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
 import Table from '@material-ui/core/Table'
@@ -8,6 +8,7 @@ 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 { Alert } from '@material-ui/lab'
 import axios from 'axios'
 import React, { useEffect } from 'react'
 import { getCities } from '../../../actions/cities'
@@ -31,7 +32,7 @@ const useStyles = makeStyles((theme: Theme) =>
 const RegionManager: React.FC = (props: any) => {
   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
   const [activeId, setActiveId] = React.useState<number | undefined>(undefined)
-  const citiesTotal = useAppSelector((state) => state.cities.total)
+  const [errorActive, setErrorActive] = React.useState(false)
   const cities = useAppSelector((state) => state.cities.cities)
   const [newCity, setNewCity] = React.useState<string>()
   const classes = useStyles()
@@ -53,8 +54,8 @@ const RegionManager: React.FC = (props: any) => {
           setAnchorEl(null)
           dispatch(getCities())
         })
-        .catch(({ response }) => {
-          console.warn(response.data)
+        .catch((response) => {
+          if (response?.response?.status === 409) setErrorActive(true)
         })
     }
   }
@@ -98,6 +99,9 @@ const RegionManager: React.FC = (props: any) => {
           Ta bort
         </RemoveMenuItem>
       </Menu>
+      <Snackbar open={errorActive} autoHideDuration={4000} onClose={() => setErrorActive(false)}>
+        <Alert severity="error">{`Du kan inte ta bort regionen eftersom det finns användare eller tävlingar kopplade till den.`}</Alert>
+      </Snackbar>
     </div>
   )
 }
diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
index a633efec..7e90d0d3 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
@@ -24,7 +24,7 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
         .catch(console.log)
     }
   }
-  const [timer, setTimer] = useState<number | undefined>(activeSlide?.timer)
+  const [timer, setTimer] = useState<number | null>(activeSlide?.timer)
   useEffect(() => {
     setTimer(activeSlide?.timer)
   }, [activeSlide])
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 29f95349..1bab9968 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -26,7 +26,7 @@ const Timer: React.FC = () => {
   const timerStartValue = slide?.timer
   const timer = useAppSelector((state) => state.presentation.timer)
   useEffect(() => {
-    if (!slide) return
+    if (!slide || !slide.timer) return
     dispatch(setPresentationTimer({ enabled: false, value: slide.timer }))
   }, [timerStartValue])
 
-- 
GitLab


From 8022583852c10260322af86328e1c54cfa52aef3 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 16:09:19 +0200
Subject: [PATCH 08/29] Force region to be selected when creating competition

---
 client/src/pages/admin/competitions/AddCompetition.tsx | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/src/pages/admin/competitions/AddCompetition.tsx b/client/src/pages/admin/competitions/AddCompetition.tsx
index 165d17d8..f2cdd1d1 100644
--- a/client/src/pages/admin/competitions/AddCompetition.tsx
+++ b/client/src/pages/admin/competitions/AddCompetition.tsx
@@ -74,9 +74,11 @@ const AddCompetition: React.FC = (props: any) => {
       // if the post request fails
       .catch(({ response }) => {
         console.warn(response.data)
-        if (response.data && response.data.message)
+        if (response?.status === 409)
+          actions.setFieldError('error', 'En tävling med det namnet finns redan, välj ett nytt namn och försök igen')
+        else if (response.data && response.data.message)
           actions.setFieldError('error', response.data && response.data.message)
-        else actions.setFieldError('error', 'Something went wrong, please try again')
+        else actions.setFieldError('error', 'Någonting gick fel, försök igen')
       })
       .finally(() => {
         actions.setSubmitting(false)
@@ -184,7 +186,7 @@ const AddCompetition: React.FC = (props: any) => {
                   fullWidth
                   variant="contained"
                   color="secondary"
-                  disabled={!formik.isValid || !formik.values.model?.name || !formik.values.model?.city}
+                  disabled={!formik.isValid || !formik.values.model?.name || !selectedCity}
                 >
                   Skapa
                 </Button>
-- 
GitLab


From f7d98797bfb7b6a772978346b15f8992b29bb31a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 11 May 2021 16:17:20 +0200
Subject: [PATCH 09/29] Update error messages

---
 server/app/database/controller/add.py  | 2 +-
 server/app/database/controller/edit.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py
index 751d5675..6dfc6cca 100644
--- a/server/app/database/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -47,7 +47,7 @@ def db_add(item):
         db.session.commit()
         db.session.refresh(item)
     except (exc.IntegrityError):
-        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint")
+        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be added due to an Integrity Constraint")
     except (exc.SQLAlchemyError, exc.DBAPIError):
         db.session.rollback()
         # SQL errors such as item already exists
diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py
index 197f1fff..9f54df3e 100644
--- a/server/app/database/controller/edit.py
+++ b/server/app/database/controller/edit.py
@@ -34,7 +34,7 @@ def default(item, **kwargs):
     try:
         db.session.commit()
     except exc.IntegrityError:
-        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint")
+        abort(codes.CONFLICT, f"Item of type {type(item)} cannot be edited due to an Integrity Constraint")
 
     db.session.refresh(item)
     return item
-- 
GitLab


From f8489c6122b1af514f8378a7bfaa2b6bfa7558d2 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 16:32:24 +0200
Subject: [PATCH 10/29] Force all fields except name when creating user

---
 client/src/pages/admin/users/AddUser.tsx | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/client/src/pages/admin/users/AddUser.tsx b/client/src/pages/admin/users/AddUser.tsx
index 2634751d..5670c18b 100644
--- a/client/src/pages/admin/users/AddUser.tsx
+++ b/client/src/pages/admin/users/AddUser.tsx
@@ -212,7 +212,13 @@ const AddUser: React.FC = (props: any) => {
                   fullWidth
                   variant="contained"
                   color="secondary"
-                  disabled={!formik.isValid || !formik.values.model?.name || !formik.values.model?.city}
+                  disabled={
+                    !formik.isValid ||
+                    !formik.values.model?.email ||
+                    !formik.values.model?.password ||
+                    !selectedCity?.name ||
+                    !selectedRole?.name
+                  }
                 >
                   Lägg till
                 </Button>
-- 
GitLab


From a99b3ac87a038f7544faee5006fb8e2b09b1bc84 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 16:48:03 +0200
Subject: [PATCH 11/29] force credentials when creating user

---
 .../components/CompetitionSettings.tsx                 | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx
index fd63a4b3..64b4a820 100644
--- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx
+++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx
@@ -1,6 +1,6 @@
 import { Divider, FormControl, InputLabel, ListItem, MenuItem, Select, TextField, Typography } from '@material-ui/core'
 import axios from 'axios'
-import React from 'react'
+import React, { useState } from 'react'
 import { useParams } from 'react-router-dom'
 import { getEditorCompetition } from '../../../actions/editor'
 import { useAppDispatch, useAppSelector } from '../../../hooks'
@@ -15,6 +15,7 @@ interface CompetitionParams {
 
 const CompetitionSettings: React.FC = () => {
   const { competitionId }: CompetitionParams = useParams()
+  const [nameErrorText, setNameErrorText] = useState<string | undefined>(undefined)
   const dispatch = useAppDispatch()
   const competition = useAppSelector((state) => state.editor.competition)
   const cities = useAppSelector((state) => state.cities.cities)
@@ -23,9 +24,12 @@ const CompetitionSettings: React.FC = () => {
     await axios
       .put(`/api/competitions/${competitionId}`, { name: event.target.value })
       .then(() => {
+        setNameErrorText(undefined)
         dispatch(getEditorCompetition(competitionId))
       })
-      .catch(console.log)
+      .catch((response) => {
+        if (response?.response.status === 409) setNameErrorText('Det finns redan en tävling med det namnet.')
+      })
   }
 
   const updateCompetitionCity = async (city: City) => {
@@ -52,6 +56,8 @@ const CompetitionSettings: React.FC = () => {
         <FirstItem>
           <ListItem>
             <TextField
+              error={Boolean(nameErrorText)}
+              helperText={nameErrorText}
               id="outlined-basic"
               label={'Tävlingsnamn'}
               defaultValue={competition.name}
-- 
GitLab


From e5533405594e53625ae00d9cc9609838dac8aae7 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 16:58:31 +0200
Subject: [PATCH 12/29] Fix error messages when entering codes in url

---
 client/src/actions/competitionLogin.ts         | 9 ++++++++-
 client/src/pages/views/ViewSelectPage.tsx      | 4 ++--
 client/src/pages/views/styled.tsx              | 4 ++--
 client/src/reducers/competitionLoginReducer.ts | 8 ++------
 4 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts
index cfc222b3..42a05053 100644
--- a/client/src/actions/competitionLogin.ts
+++ b/client/src/actions/competitionLogin.ts
@@ -35,7 +35,14 @@ export const loginCompetition = (code: string, history: History, redirect: boole
       }
     })
     .catch((err) => {
-      dispatch({ type: Types.SET_COMPETITION_LOGIN_ERRORS, payload: err && err.response && err.response.data })
+      let errorMessage = err?.response?.data?.message
+      if (err?.response?.status === 401) {
+        errorMessage = 'Inkorrekt kod. Dubbelkolla koden och försök igen.'
+      }
+      if (err?.response?.status === 404) {
+        errorMessage = 'En tävling med den koden existerar inte. Dubbelkolla koden och försök igen.'
+      }
+      dispatch({ type: Types.SET_COMPETITION_LOGIN_ERRORS, payload: errorMessage })
       console.log(err)
     })
 }
diff --git a/client/src/pages/views/ViewSelectPage.tsx b/client/src/pages/views/ViewSelectPage.tsx
index 75bf8498..9bd8d2ae 100644
--- a/client/src/pages/views/ViewSelectPage.tsx
+++ b/client/src/pages/views/ViewSelectPage.tsx
@@ -12,14 +12,14 @@ const ViewSelectPage: React.FC = () => {
   const dispatch = useAppDispatch()
   const history = useHistory()
   const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id)
-  const errorMessage = useAppSelector((state) => state.competitionLogin.errors?.message)
+  const errorMessage = useAppSelector((state) => state.competitionLogin.errors)
   const loading = useAppSelector((state) => state.competitionLogin.loading)
   const { code }: ViewSelectParams = useParams()
   const viewType = useAppSelector((state) => state.competitionLogin.data?.view)
 
   const renderView = () => {
     //Renders the correct view depending on view type
-    if (competitionId) {
+    if (competitionId && !errorMessage) {
       switch (viewType) {
         case 'Team':
           return <Redirect to={`/view/team`} />
diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx
index 4b01d63a..c8946c22 100644
--- a/client/src/pages/views/styled.tsx
+++ b/client/src/pages/views/styled.tsx
@@ -21,8 +21,8 @@ export const JudgeAnswersLabel = styled(Typography)`
 export const ViewSelectContainer = styled.div`
   display: flex;
   justify-content: center;
-  margin-top: 12%;
-  height: 100%;
+  padding-top: 12%;
+  height: calc(100%-12%);
 `
 
 export const ViewSelectButtonGroup = styled.div`
diff --git a/client/src/reducers/competitionLoginReducer.ts b/client/src/reducers/competitionLoginReducer.ts
index f3ca764c..b95efdf5 100644
--- a/client/src/reducers/competitionLoginReducer.ts
+++ b/client/src/reducers/competitionLoginReducer.ts
@@ -7,15 +7,11 @@ interface CompetitionLoginData {
   team_id: number | null
   view: string
 }
-/** Define a type for UI error */
-interface UIError {
-  message: string
-}
 
 /** Define a type for the competition login state */
 interface CompetitionLoginState {
   loading: boolean
-  errors: null | UIError
+  errors: null | string
   authenticated: boolean
   data: CompetitionLoginData | null
   initialized: boolean
@@ -43,7 +39,7 @@ export default function (state = initialState, action: AnyAction) {
     case Types.SET_COMPETITION_LOGIN_ERRORS:
       return {
         ...state,
-        errors: action.payload as UIError,
+        errors: action.payload as string,
         loading: false,
       }
     case Types.CLEAR_COMPETITION_LOGIN_ERRORS:
-- 
GitLab


From 83f76c5a0f7f23251510578996c343d0f461cc2b Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 17:18:48 +0200
Subject: [PATCH 13/29] Fix operator code scroll-bar

---
 client/src/pages/login/components/CompetitionLogin.tsx    | 6 +++---
 client/src/pages/presentationEditor/components/styled.tsx | 1 +
 client/src/pages/views/OperatorViewPage.tsx               | 3 +--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx
index 44463474..4e2429a3 100644
--- a/client/src/pages/login/components/CompetitionLogin.tsx
+++ b/client/src/pages/login/components/CompetitionLogin.tsx
@@ -61,11 +61,11 @@ const CompetitionLogin: React.FC = () => {
           <Button type="submit" fullWidth variant="contained" color="secondary" disabled={!formik.isValid}>
             Anslut till tävling
           </Button>
-          {errors && errors.message && (
+          {errors && (
             <Alert severity="error">
               <AlertTitle>Error</AlertTitle>
-              <Typography>En tävling med den koden hittades ej.</Typography>
-              <Typography>kontrollera koden och försök igen</Typography>
+              <Typography>En tävling med den koden existerar ej.</Typography>
+              <Typography>Dubbelkolla koden och försök igen</Typography>
             </Alert>
           )}
           {loading && <CenteredCircularProgress color="secondary" />}
diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx
index 31e40d51..6e98f03a 100644
--- a/client/src/pages/presentationEditor/components/styled.tsx
+++ b/client/src/pages/presentationEditor/components/styled.tsx
@@ -58,6 +58,7 @@ export const Center = styled.div`
   text-align: center;
   height: 100%;
   width: 100%;
+  overflow-x: hidden;
 `
 
 export const ImageTextContainer = styled.div`
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index 5bb96fcb..6018f5e6 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -122,7 +122,6 @@ const OperatorViewPage: React.FC = () => {
   useEffect(() => {
     socketConnect('Operator')
     socketSetSlide
-    handleOpenCodes()
     setTimeout(startCompetition, 1000) // Wait for socket to connect
   }, [])
 
@@ -232,7 +231,7 @@ const OperatorViewPage: React.FC = () => {
         fullWidth={false}
         fullScreen={false}
       >
-        <Center>
+        <Center style={{ overflowX: 'hidden' }}>
           <DialogTitle id="max-width-dialog-title" className={classes.paper} style={{ width: '100%' }}>
             Koder för {competitionName}
           </DialogTitle>
-- 
GitLab


From 3e74193d5ec0055071006b3029b37efd76db3618 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 18:28:13 +0200
Subject: [PATCH 14/29] Fix timer and score inputs

---
 client/src/interfaces/ApiModels.ts            |  2 +-
 client/src/pages/admin/users/UserManager.tsx  | 17 ------
 .../QuestionSettings.tsx                      | 54 +++++++++++--------
 .../slideSettingsComponents/Timer.tsx         | 45 +++++++++-------
 .../presentationEditor/components/styled.tsx  |  4 ++
 client/src/pages/views/OperatorViewPage.tsx   | 33 ++++--------
 server/app/database/models.py                 |  5 +-
 7 files changed, 75 insertions(+), 85 deletions(-)

diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts
index 254c2bfd..5ea091a0 100644
--- a/client/src/interfaces/ApiModels.ts
+++ b/client/src/interfaces/ApiModels.ts
@@ -54,7 +54,7 @@ export interface Team extends NameID {
 
 export interface Question extends NameID {
   slide_id: number
-  total_score: number
+  total_score: number | null
   type_id: number
   correcting_instructions: string
 }
diff --git a/client/src/pages/admin/users/UserManager.tsx b/client/src/pages/admin/users/UserManager.tsx
index 20f57386..9713e90f 100644
--- a/client/src/pages/admin/users/UserManager.tsx
+++ b/client/src/pages/admin/users/UserManager.tsx
@@ -47,23 +47,6 @@ const UserManager: React.FC = (props: any) => {
   const dispatch = useAppDispatch()
 
   const open = Boolean(anchorEl)
-  const id = open ? 'simple-popover' : undefined
-
-  const handleClick = (event: React.MouseEvent<HTMLButtonElement>, user: User) => {
-    setAnchorEl(event.currentTarget)
-    setSelectedUser(user)
-  }
-
-  const handleClose = () => {
-    setAnchorEl(null)
-    setSelectedUser(undefined)
-    console.log('close')
-  }
-
-  const handleEditClose = () => {
-    setEditAnchorEl(null)
-    console.log('edit close')
-  }
 
   useEffect(() => {
     dispatch(getSearchUsers())
diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
index f714fe23..367ebd8a 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
@@ -1,10 +1,10 @@
 import { ListItem, ListItemText, TextField } from '@material-ui/core'
 import axios from 'axios'
-import React, { useEffect, useState } from 'react'
+import React, { useState } from 'react'
 import { getEditorCompetition } from '../../../../actions/editor'
 import { useAppDispatch } from '../../../../hooks'
 import { RichSlide } from '../../../../interfaces/ApiRichModels'
-import { Center, SettingsList } from '../styled'
+import { Center, SettingsItemContainer, SettingsList } from '../styled'
 
 type QuestionSettingsProps = {
   activeSlide: RichSlide
@@ -13,7 +13,18 @@ type QuestionSettingsProps = {
 
 const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps) => {
   const dispatch = useAppDispatch()
-
+  const [timerHandle, setTimerHandle] = useState<number | undefined>(undefined)
+  const handleChangeQuestion = (
+    updateTitle: boolean,
+    event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
+  ) => {
+    if (timerHandle) {
+      clearTimeout(timerHandle)
+      setTimerHandle(undefined)
+    }
+    //Only updates question and api 100ms after last input was made
+    setTimerHandle(window.setTimeout(() => updateQuestion(updateTitle, event), 100))
+  }
   const updateQuestion = async (
     updateTitle: boolean,
     event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
@@ -29,10 +40,9 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
           })
           .catch(console.log)
       } else {
-        setScore(+event.target.value)
         await axios
           .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}`, {
-            total_score: event.target.value,
+            total_score: event.target.value || null,
           })
           .then(() => {
             dispatch(getEditorCompetition(competitionId))
@@ -41,11 +51,7 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
       }
     }
   }
-
-  const [score, setScore] = useState<number | undefined>(0)
-  useEffect(() => {
-    setScore(activeSlide?.questions?.[0]?.total_score)
-  }, [activeSlide])
+  const score = activeSlide?.questions?.[0]?.total_score
 
   return (
     <SettingsList>
@@ -57,26 +63,28 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
       <ListItem divider>
         <TextField
           id="outlined-basic"
-          defaultValue={''}
+          defaultValue={activeSlide?.questions?.[0]?.name}
           label="Frågans titel"
-          onChange={(event) => updateQuestion(true, event)}
+          onChange={(event) => handleChangeQuestion(true, event)}
           variant="outlined"
           fullWidth={true}
         />
       </ListItem>
       <ListItem>
         <Center>
-          <TextField
-            fullWidth={true}
-            variant="outlined"
-            placeholder="Antal poäng"
-            helperText="Välj hur många poäng frågan ska ge för rätt svar."
-            label="Poäng"
-            type="number"
-            InputProps={{ inputProps: { min: 0 } }}
-            value={score || 0}
-            onChange={(event) => updateQuestion(false, event)}
-          />
+          <SettingsItemContainer>
+            <TextField
+              fullWidth={true}
+              variant="outlined"
+              placeholder="Antal poäng"
+              helperText="Välj hur många poäng frågan ska ge för rätt svar."
+              label="Poäng"
+              type="number"
+              InputProps={{ inputProps: { min: 0, max: 1000000 } }}
+              defaultValue={score || 0}
+              onChange={(event) => handleChangeQuestion(false, event)}
+            />
+          </SettingsItemContainer>
         </Center>
       </ListItem>
     </SettingsList>
diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
index 7e90d0d3..22919582 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
@@ -1,10 +1,10 @@
 import { ListItem, TextField } from '@material-ui/core'
 import axios from 'axios'
-import React, { useEffect, useState } from 'react'
+import React, { useState } from 'react'
 import { getEditorCompetition } from '../../../../actions/editor'
 import { useAppDispatch } from '../../../../hooks'
 import { RichSlide } from '../../../../interfaces/ApiRichModels'
-import { Center } from '../styled'
+import { Center, SettingsItemContainer } from '../styled'
 
 type TimerProps = {
   activeSlide: RichSlide
@@ -13,8 +13,17 @@ type TimerProps = {
 
 const Timer = ({ activeSlide, competitionId }: TimerProps) => {
   const dispatch = useAppDispatch()
+  const [timerHandle, setTimerHandle] = useState<number | undefined>(undefined)
+  const handleChangeTimer = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
+    if (timerHandle) {
+      clearTimeout(timerHandle)
+      setTimerHandle(undefined)
+    }
+    //Only updates question and api 100ms after last input was made
+    setTimerHandle(window.setTimeout(() => updateTimer(event), 100))
+  }
+
   const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
-    setTimer(+event.target.value)
     if (activeSlide) {
       await axios
         .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: event.target.value || null })
@@ -24,24 +33,24 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
         .catch(console.log)
     }
   }
-  const [timer, setTimer] = useState<number | null>(activeSlide?.timer)
-  useEffect(() => {
-    setTimer(activeSlide?.timer)
-  }, [activeSlide])
+
   return (
     <ListItem>
       <Center>
-        <TextField
-          id="standard-number"
-          fullWidth={true}
-          variant="outlined"
-          placeholder="Antal sekunder"
-          helperText="Lämna blank för att inte använda timerfunktionen"
-          label="Timer"
-          type="number"
-          onChange={updateTimer}
-          value={timer || ''}
-        />
+        <SettingsItemContainer>
+          <TextField
+            id="standard-number"
+            fullWidth={true}
+            variant="outlined"
+            placeholder="Antal sekunder"
+            helperText="Lämna blank för att inte använda timerfunktionen"
+            label="Timer"
+            type="number"
+            onChange={handleChangeTimer}
+            InputProps={{ inputProps: { min: 0, max: 1000000 } }}
+            defaultValue={activeSlide?.timer}
+          />
+        </SettingsItemContainer>
       </Center>
     </ListItem>
   )
diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx
index 6e98f03a..c214a7ce 100644
--- a/client/src/pages/presentationEditor/components/styled.tsx
+++ b/client/src/pages/presentationEditor/components/styled.tsx
@@ -141,3 +141,7 @@ export const ImageNameText = styled(ListItemText)`
 export const QuestionComponent = styled.div`
   outline-style: double;
 `
+
+export const SettingsItemContainer = styled.div`
+  padding: 5px;
+`
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index 6018f5e6..b091e21f 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -101,7 +101,7 @@ const OperatorViewPage: React.FC = () => {
   const [openAlert, setOpen] = React.useState(false)
   const [openAlertCode, setOpenCode] = React.useState(false)
   const [codes, setCodes] = React.useState<Code[]>([])
-  const [competitionName, setCompetitionName] = React.useState<string | undefined>(undefined)
+  const competitionName = useAppSelector((state) => state.presentation.competition.name)
 
   //const fullScreen = useMediaQuery(theme.breakpoints.down('sm'))
 
@@ -154,7 +154,6 @@ const OperatorViewPage: React.FC = () => {
 
   const handleOpenCodes = async () => {
     await getCodes()
-    await getCompetitionName()
     setOpenCode(true)
   }
 
@@ -173,18 +172,6 @@ const OperatorViewPage: React.FC = () => {
       })
       .catch(console.log)
   }
-
-  const getCompetitionName = async () => {
-    await axios
-      .get(`/api/competitions/${activeId}`)
-      .then((response) => {
-        setCompetitionName(response.data.name)
-      })
-      .catch((err) => {
-        console.log(err)
-      })
-  }
-
   const getTypeName = (code: Code) => {
     let typeName = ''
     switch (code.view_type_id) {
@@ -223,15 +210,8 @@ const OperatorViewPage: React.FC = () => {
 
   return (
     <OperatorContainer>
-      <Dialog
-        open={openAlertCode}
-        onClose={handleClose}
-        aria-labelledby="max-width-dialog-title"
-        maxWidth="xl"
-        fullWidth={false}
-        fullScreen={false}
-      >
-        <Center style={{ overflowX: 'hidden' }}>
+      <Dialog open={openAlertCode} onClose={handleClose} aria-labelledby="max-width-dialog-title" maxWidth="xl">
+        <Center>
           <DialogTitle id="max-width-dialog-title" className={classes.paper} style={{ width: '100%' }}>
             Koder för {competitionName}
           </DialogTitle>
@@ -363,6 +343,7 @@ const OperatorViewPage: React.FC = () => {
           horizontal: 'center',
         }}
       >
+        {(!teams || teams.length === 0) && 'Det finns inga lag i denna tävling'}
         <List>
           {teams &&
             teams.map((team) => (
@@ -372,7 +353,11 @@ const OperatorViewPage: React.FC = () => {
             ))}
         </List>
       </Popover>
-      <Snackbar open={successMessageOpen} autoHideDuration={4000} onClose={() => setSuccessMessageOpen(false)}>
+      <Snackbar
+        open={successMessageOpen && Boolean(competitionName)}
+        autoHideDuration={4000}
+        onClose={() => setSuccessMessageOpen(false)}
+      >
         <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som operatör`}</Alert>
       </Snackbar>
     </OperatorContainer>
diff --git a/server/app/database/models.py b/server/app/database/models.py
index b352c688..bc13c094 100644
--- a/server/app/database/models.py
+++ b/server/app/database/models.py
@@ -5,7 +5,8 @@ each other.
 """
 
 from app.core import bcrypt, db
-from app.database.types import ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT, ID_TEXT_COMPONENT
+from app.database.types import (ID_IMAGE_COMPONENT, ID_QUESTION_COMPONENT,
+                                ID_TEXT_COMPONENT)
 from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
 
 STRING_SIZE = 254
@@ -164,7 +165,7 @@ class Slide(db.Model):
 class Question(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String(STRING_SIZE), nullable=False)
-    total_score = db.Column(db.Integer, nullable=False, default=1)
+    total_score = db.Column(db.Integer, nullable=True, default=None)
     type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False)
     slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False)
     correcting_instructions = db.Column(db.Text, nullable=True, default=None)
-- 
GitLab


From e31d37160fdeebcea629b4ee37fa91cf6cf1aead Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Tue, 11 May 2021 18:51:43 +0200
Subject: [PATCH 15/29] Fix timer, question title and question score

---
 .../QuestionSettings.tsx                      | 22 +++++++++++++------
 .../slideSettingsComponents/Timer.tsx         | 14 +++++++-----
 2 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
index 367ebd8a..3b474923 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
@@ -1,6 +1,6 @@
 import { ListItem, ListItemText, TextField } from '@material-ui/core'
 import axios from 'axios'
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { getEditorCompetition } from '../../../../actions/editor'
 import { useAppDispatch } from '../../../../hooks'
 import { RichSlide } from '../../../../interfaces/ApiRichModels'
@@ -22,8 +22,11 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
       clearTimeout(timerHandle)
       setTimerHandle(undefined)
     }
-    //Only updates question and api 100ms after last input was made
-    setTimerHandle(window.setTimeout(() => updateQuestion(updateTitle, event), 100))
+    //Only updates question and api 500ms after last input was made
+    setTimerHandle(window.setTimeout(() => updateQuestion(updateTitle, event), 300))
+    if (updateTitle) {
+      setName(event.target.value)
+    } else setScore(+event.target.value)
   }
   const updateQuestion = async (
     updateTitle: boolean,
@@ -51,7 +54,12 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
       }
     }
   }
-  const score = activeSlide?.questions?.[0]?.total_score
+  const [score, setScore] = useState<number | undefined>(0)
+  const [name, setName] = useState<string | undefined>('')
+  useEffect(() => {
+    setName(activeSlide?.questions?.[0]?.name)
+    setScore(activeSlide?.questions?.[0]?.total_score)
+  }, [activeSlide])
 
   return (
     <SettingsList>
@@ -63,11 +71,11 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
       <ListItem divider>
         <TextField
           id="outlined-basic"
-          defaultValue={activeSlide?.questions?.[0]?.name}
           label="Frågans titel"
           onChange={(event) => handleChangeQuestion(true, event)}
           variant="outlined"
           fullWidth={true}
+          value={name || ''}
         />
       </ListItem>
       <ListItem>
@@ -77,11 +85,11 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
               fullWidth={true}
               variant="outlined"
               placeholder="Antal poäng"
-              helperText="Välj hur många poäng frågan ska ge för rätt svar."
+              helperText="Välj hur många poäng frågan ska ge för rätt svar.   Lämna blank för att inte använda poängfunktionen"
               label="Poäng"
               type="number"
               InputProps={{ inputProps: { min: 0, max: 1000000 } }}
-              defaultValue={score || 0}
+              value={score || ''}
               onChange={(event) => handleChangeQuestion(false, event)}
             />
           </SettingsItemContainer>
diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
index 22919582..89e4ce01 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
@@ -1,6 +1,6 @@
 import { ListItem, TextField } from '@material-ui/core'
 import axios from 'axios'
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { getEditorCompetition } from '../../../../actions/editor'
 import { useAppDispatch } from '../../../../hooks'
 import { RichSlide } from '../../../../interfaces/ApiRichModels'
@@ -19,8 +19,9 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
       clearTimeout(timerHandle)
       setTimerHandle(undefined)
     }
-    //Only updates question and api 100ms after last input was made
-    setTimerHandle(window.setTimeout(() => updateTimer(event), 100))
+    //Only updates slide and api 300s after last input was made
+    setTimerHandle(window.setTimeout(() => updateTimer(event), 300))
+    setTimer(+event.target.value)
   }
 
   const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
@@ -33,7 +34,10 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
         .catch(console.log)
     }
   }
-
+  const [timer, setTimer] = useState<number | null>(activeSlide?.timer)
+  useEffect(() => {
+    setTimer(activeSlide?.timer)
+  }, [activeSlide])
   return (
     <ListItem>
       <Center>
@@ -48,7 +52,7 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
             type="number"
             onChange={handleChangeTimer}
             InputProps={{ inputProps: { min: 0, max: 1000000 } }}
-            defaultValue={activeSlide?.timer}
+            value={timer || ''}
           />
         </SettingsItemContainer>
       </Center>
-- 
GitLab


From ea7a33dfc7cf2f053580e02d13da2746d7a3aaeb Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 09:54:35 +0200
Subject: [PATCH 16/29] Fix color of view select buttons

---
 client/src/pages/presentationEditor/styled.tsx | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/client/src/pages/presentationEditor/styled.tsx b/client/src/pages/presentationEditor/styled.tsx
index 779599da..4b6e7a0c 100644
--- a/client/src/pages/presentationEditor/styled.tsx
+++ b/client/src/pages/presentationEditor/styled.tsx
@@ -20,11 +20,7 @@ export const ToolBarContainer = styled(Toolbar)`
 `
 
 export const ViewButton = styled(Button)<ViewButtonProps>`
-  background: ${(props) => (props.$activeView ? '#5a0017' : undefined)};
-`
-
-export const ViewButtonClicked = styled(Button)`
-  background: #5a0017;
+  background: ${(props) => (!props.$activeView ? '#5a0017' : undefined)};
 `
 
 export const SlideList = styled(List)`
-- 
GitLab


From 5c1ffeb9fd71026f1f1a07124ea8805500c4a33d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 12 May 2021 09:57:00 +0200
Subject: [PATCH 17/29] Refactor add.component

---
 server/app/database/controller/add.py | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py
index 6dfc6cca..9ba36dc9 100644
--- a/server/app/database/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -72,15 +72,20 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, copy=False, *
     coordinates with the provided size and data.
     """
 
-    if type_id == 2:  # 2 is image
-        item_image = get.one(Media, data["media_id"])
-        filename = item_image.filename
-        path = os.path.join(
-            current_app.config["UPLOADED_PHOTOS_DEST"],
-            filename,
+    if type_id == ID_TEXT_COMPONENT:
+        item = db_add(
+            TextComponent(slide_id, type_id, view_type_id, x, y, w, h),
         )
+        item.text = data.get("text")
+    elif type_id == ID_IMAGE_COMPONENT:
+        if not copy:  # Scale image if adding a new one, a copied image should keep it's size
+            item_image = get.one(Media, data["media_id"])
+            filename = item_image.filename
+            path = os.path.join(
+                current_app.config["UPLOADED_PHOTOS_DEST"],
+                filename,
+            )
 
-        if not copy:
             with Image.open(path) as im:
                 h = im.height
                 w = im.width
@@ -91,12 +96,6 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, copy=False, *
                 w *= ratio
                 h *= ratio
 
-    if type_id == ID_TEXT_COMPONENT:
-        item = db_add(
-            TextComponent(slide_id, type_id, view_type_id, x, y, w, h),
-        )
-        item.text = data.get("text")
-    elif type_id == ID_IMAGE_COMPONENT:
         item = db_add(
             ImageComponent(slide_id, type_id, view_type_id, x, y, w, h),
         )
-- 
GitLab


From 34d398174196594a144f3d67c0809a1000957dfe Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 10:03:30 +0200
Subject: [PATCH 18/29] Fix styling of competition name in operator view

---
 client/src/pages/views/OperatorViewPage.tsx | 10 ++++++----
 client/src/pages/views/styled.tsx           |  2 +-
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index b091e21f..0936abaf 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -49,8 +49,8 @@ import {
   OperatorContent,
   OperatorFooter,
   OperatorHeader,
+  OperatorHeaderItem,
   OperatorInnerContent,
-  SlideCounter,
   ToolBarContainer,
 } from './styled'
 
@@ -282,12 +282,14 @@ const OperatorViewPage: React.FC = () => {
             </Button>
           </DialogActions>
         </Dialog>
-        <Typography variant="h3">{presentation.competition.name}</Typography>
-        <SlideCounter>
+        <OperatorHeaderItem>
+          <Typography variant="h3">{presentation.competition.name}</Typography>
+        </OperatorHeaderItem>
+        <OperatorHeaderItem>
           <Typography variant="h3">
             {activeSlideOrder !== undefined && activeSlideOrder + 1} / {presentation.competition.slides.length}
           </Typography>
-        </SlideCounter>
+        </OperatorHeaderItem>
       </OperatorHeader>
       <div style={{ height: 0, paddingTop: 120 }} />
       <OperatorContent>
diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx
index c8946c22..3fe60445 100644
--- a/client/src/pages/views/styled.tsx
+++ b/client/src/pages/views/styled.tsx
@@ -60,7 +60,7 @@ export const OperatorButton = styled(Button)`
   margin-top: 16px;
 `
 
-export const SlideCounter = styled(Button)`
+export const OperatorHeaderItem = styled(Button)`
   margin-left: 16px;
   margin-right: 16px;
   margin-top: 16px;
-- 
GitLab


From e2a9e7b3978d8def2cef0d859dc26728bc94728e Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 10:09:03 +0200
Subject: [PATCH 19/29] Reset slide id when joining admin view

---
 client/src/pages/admin/AdminPage.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx
index 8d193557..7b132f2a 100644
--- a/client/src/pages/admin/AdminPage.tsx
+++ b/client/src/pages/admin/AdminPage.tsx
@@ -20,6 +20,7 @@ import React, { useEffect } from 'react'
 import { Link, Route, Switch, useRouteMatch } from 'react-router-dom'
 import { getCities } from '../../actions/cities'
 import { setEditorLoading } from '../../actions/competitions'
+import { setEditorSlideId } from '../../actions/editor'
 import { getRoles } from '../../actions/roles'
 import { getStatistics } from '../../actions/statistics'
 import { getTypes } from '../../actions/typesAction'
@@ -75,6 +76,7 @@ const AdminView: React.FC = () => {
     dispatch(getTypes())
     dispatch(getStatistics())
     dispatch(setEditorLoading(true))
+    dispatch(setEditorSlideId(-1))
   }, [])
 
   const menuAdminItems = [
-- 
GitLab


From ca73320e22fcfd150ecd08d655a6a7154bbf064c Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 10:21:52 +0200
Subject: [PATCH 20/29] Fix typing for timer in presentation state

---
 client/src/pages/views/JudgeViewPage.tsx   | 8 ++++++++
 client/src/reducers/presentationReducer.ts | 5 +++--
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index a28348a7..6211aaf0 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -53,6 +53,7 @@ const JudgeViewPage: React.FC = () => {
   const [currentSlide, setCurrentSlide] = useState<RichSlide | undefined>(undefined)
   const currentQuestion = currentSlide?.questions[0]
   const operatorActiveSlideId = useAppSelector((state) => state.presentation.activeSlideId)
+  const timer = useAppSelector((state) => state.presentation.timer)
   const operatorActiveSlideOrder = useAppSelector(
     (state) => state.presentation.competition.slides.find((slide) => slide.id === operatorActiveSlideId)?.order
   )
@@ -74,9 +75,16 @@ const JudgeViewPage: React.FC = () => {
       dispatch(getPresentationCompetition(competitionId.toString()))
     }
   }, [operatorActiveSlideId])
+  useEffect(() => {
+    // Every third tic of the timer, load new answers
+    if (timer.value % 3 === 0 && competitionId) {
+      dispatch(getPresentationCompetition(competitionId.toString()))
+    }
+  }, [timer.value])
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
+        {timer.value}
         <JudgeToolbar>
           <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel>
           {operatorActiveSlideOrder !== undefined && (
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 4df814a8..cc183fad 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -49,12 +49,13 @@ export default function (state = initialState, action: AnyAction) {
         activeSlideId: action.payload as number,
       }
     case Types.SET_PRESENTATION_TIMER:
+      const timer = action.payload as Timer
       if (action.payload.value == 0) {
-        action.payload.enabled = false
+        timer.enabled = false
       }
       return {
         ...state,
-        timer: action.payload,
+        timer,
       }
     default:
       return state
-- 
GitLab


From 3b31d2dd69ade8308572efaaa6984f84c718e81a Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 10:35:23 +0200
Subject: [PATCH 21/29] Get competition every second tic of timer

---
 client/src/pages/views/JudgeViewPage.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 6211aaf0..9efae217 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -11,6 +11,7 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import JudgeScoringInstructions from './components/JudgeScoringInstructions'
+import Timer from './components/Timer'
 import {
   Content,
   InnerContent,
@@ -76,15 +77,15 @@ const JudgeViewPage: React.FC = () => {
     }
   }, [operatorActiveSlideId])
   useEffect(() => {
-    // Every third tic of the timer, load new answers
-    if (timer.value % 3 === 0 && competitionId) {
+    // Every second tic of the timer, load new answers
+    if (timer.value % 2 === 0 && competitionId) {
       dispatch(getPresentationCompetition(competitionId.toString()))
     }
   }, [timer.value])
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
-        {timer.value}
+        <Timer />
         <JudgeToolbar>
           <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel>
           {operatorActiveSlideOrder !== undefined && (
-- 
GitLab


From 6f1997bdcde1aba06dad8ea3ba97a77e7dc33512 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 10:36:51 +0200
Subject: [PATCH 22/29] remove testing timer

---
 client/src/pages/views/JudgeViewPage.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 9efae217..c25b09dd 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -11,7 +11,6 @@ import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import JudgeScoringInstructions from './components/JudgeScoringInstructions'
-import Timer from './components/Timer'
 import {
   Content,
   InnerContent,
@@ -85,7 +84,6 @@ const JudgeViewPage: React.FC = () => {
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
-        <Timer />
         <JudgeToolbar>
           <JudgeQuestionsLabel variant="h5">Frågor</JudgeQuestionsLabel>
           {operatorActiveSlideOrder !== undefined && (
-- 
GitLab


From 2c22c95d7ab24b90fcd11da10ca07cb85515d6bc Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 15:48:56 +0200
Subject: [PATCH 23/29] Fix inconsistency in selecting question type

---
 .../slideSettingsComponents/SlideType.tsx     | 30 +++++++------------
 1 file changed, 10 insertions(+), 20 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
index 8fbae438..1c08a7f1 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
@@ -125,30 +125,20 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
         <FormControl fullWidth variant="outlined">
           <InputLabel>Sidtyp</InputLabel>
           <Select fullWidth={true} value={activeSlide?.questions?.[0]?.type_id || 0} label="Sidtyp">
-            <MenuItem value={0}>
-              <Typography variant="button" onClick={() => openSlideTypeDialog(0)}>
-                Informationssida
-              </Typography>
+            <MenuItem value={0} button onClick={() => openSlideTypeDialog(0)}>
+              <Typography>Informationssida</Typography>
             </MenuItem>
-            <MenuItem value={1}>
-              <Typography variant="button" onClick={() => openSlideTypeDialog(1)}>
-                Skriftlig fråga
-              </Typography>
+            <MenuItem value={1} button onClick={() => openSlideTypeDialog(1)}>
+              <Typography>Skriftlig fråga</Typography>
             </MenuItem>
-            <MenuItem value={2}>
-              <Typography variant="button" onClick={() => openSlideTypeDialog(2)}>
-                Praktisk fråga
-              </Typography>
+            <MenuItem value={2} button onClick={() => openSlideTypeDialog(2)}>
+              <Typography>Praktisk fråga</Typography>
             </MenuItem>
-            <MenuItem value={3}>
-              <Typography variant="button" onClick={() => openSlideTypeDialog(3)}>
-                Kryssfråga
-              </Typography>
+            <MenuItem value={3} button onClick={() => openSlideTypeDialog(3)}>
+              <Typography>Kryssfråga</Typography>
             </MenuItem>
-            <MenuItem value={4}>
-              <Typography variant="button" onClick={() => openSlideTypeDialog(4)}>
-                Alternativfråga
-              </Typography>
+            <MenuItem value={4} button onClick={() => openSlideTypeDialog(4)}>
+              <Typography>Alternativfråga</Typography>
             </MenuItem>
           </Select>
         </FormControl>
-- 
GitLab


From e2dde13087cf5326282177a521d0fb233ca07fb1 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 16:14:56 +0200
Subject: [PATCH 24/29] Avoid creating double questions and residual question
 components

---
 .../slideSettingsComponents/SlideType.tsx        | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
index 1c08a7f1..5f11cffe 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
@@ -56,6 +56,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
             )
             .then(() => {
               dispatch(getEditorCompetition(competitionId))
+              removeQuestionComponent()
             })
             .catch(console.log)
         } else {
@@ -73,11 +74,11 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
             })
             .then(() => {
               dispatch(getEditorCompetition(competitionId))
-              createQuestionComponent()
+              removeQuestionComponent().then(() => createQuestionComponent())
             })
             .catch(console.log)
         }
-      } else if (selectedSlideType !== 0) {
+      } else if (activeSlide.questions[0].type_id === 0 && selectedSlideType !== 0) {
         // Change slide type from information to a question type
         await axios
           .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions`, {
@@ -94,8 +95,8 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
     }
   }
 
-  const createQuestionComponent = () => {
-    axios
+  const createQuestionComponent = async () => {
+    await axios
       .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components`, {
         x: 0,
         y: 0,
@@ -111,6 +112,13 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
       .catch(console.log)
   }
 
+  const removeQuestionComponent = async () => {
+    const questionComponentId = activeSlide.components.find((component) => component.type_id === 3)?.id
+    await axios
+      .delete(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components/${questionComponentId}`)
+      .catch(console.log)
+  }
+
   const deleteQuestionComponent = (componentId: number | undefined) => {
     if (componentId) {
       axios
-- 
GitLab


From 531bc4addb113e9321f7bda7785778d2930ea18c Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Wed, 12 May 2021 16:15:46 +0200
Subject: [PATCH 25/29] only remove qcomponent if questionComponentId is found

---
 .../components/slideSettingsComponents/SlideType.tsx      | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
index 5f11cffe..2e4ee216 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx
@@ -114,9 +114,11 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => {
 
   const removeQuestionComponent = async () => {
     const questionComponentId = activeSlide.components.find((component) => component.type_id === 3)?.id
-    await axios
-      .delete(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components/${questionComponentId}`)
-      .catch(console.log)
+    if (questionComponentId) {
+      await axios
+        .delete(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components/${questionComponentId}`)
+        .catch(console.log)
+    }
   }
 
   const deleteQuestionComponent = (componentId: number | undefined) => {
-- 
GitLab


From 389e3df94a079f5fd705a54ec6a96d2537af974e Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Fri, 14 May 2021 09:16:49 +0200
Subject: [PATCH 26/29] fix tests

---
 client/src/actions/competitionLogin.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/actions/competitionLogin.test.ts b/client/src/actions/competitionLogin.test.ts
index 9a988c23..a95dd002 100644
--- a/client/src/actions/competitionLogin.test.ts
+++ b/client/src/actions/competitionLogin.test.ts
@@ -62,7 +62,7 @@ it('dispatches correct action when failing to log in user', async () => {
   console.log = jest.fn()
   const errorMessage = 'getting teams failed'
   ;(mockedAxios.post as jest.Mock).mockImplementation(() => {
-    return Promise.reject({ response: { data: errorMessage } })
+    return Promise.reject({ response: { data: { message: errorMessage } } })
   })
   const store = mockStore({})
   const history = createMemoryHistory()
-- 
GitLab


From a4b8095b68836b262be1ad23cb34482a58b02caa Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Fri, 14 May 2021 09:37:06 +0200
Subject: [PATCH 27/29] fix team view page

---
 client/src/pages/views/TeamViewPage.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx
index 6c459475..e9566cef 100644
--- a/client/src/pages/views/TeamViewPage.tsx
+++ b/client/src/pages/views/TeamViewPage.tsx
@@ -34,11 +34,11 @@ const TeamViewPage: React.FC = () => {
         <Typography variant="h1">
           <Timer />
         </Typography>
-        <SlideCounter>
+        <OperatorHeaderItem>
           <Typography variant="h3">
             {activeSlideOrder !== undefined && activeSlideOrder + 1} / {presentation.competition.slides.length}
           </Typography>
-        </SlideCounter>
+        </OperatorHeaderItem>
       </OperatorHeader>
       <PresentationBackground>
         <PresentationContainer>
-- 
GitLab


From ece0650f1f5e4d306f26ddf4d2fdcbedc11f053e Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Fri, 14 May 2021 09:38:02 +0200
Subject: [PATCH 28/29] fix incorrect import

---
 client/src/pages/views/TeamViewPage.tsx | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx
index e9566cef..4c3266bc 100644
--- a/client/src/pages/views/TeamViewPage.tsx
+++ b/client/src/pages/views/TeamViewPage.tsx
@@ -5,7 +5,13 @@ import { useAppSelector } from '../../hooks'
 import { socketConnect, socketJoinPresentation } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import Timer from '../views/components/Timer'
-import { OperatorContainer, OperatorHeader, PresentationBackground, PresentationContainer } from './styled'
+import {
+  OperatorContainer,
+  OperatorHeader,
+  OperatorHeaderItem,
+  PresentationBackground,
+  PresentationContainer,
+} from './styled'
 
 const TeamViewPage: React.FC = () => {
   const code = useAppSelector((state) => state.presentation.code)
-- 
GitLab


From f1c6c1b025ac2719c43d0ec0a208d556f89d3d69 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Fri, 14 May 2021 09:43:55 +0200
Subject: [PATCH 29/29] Make sure timer and score is above 0

---
 .../components/slideSettingsComponents/QuestionSettings.tsx    | 3 ++-
 .../components/slideSettingsComponents/Timer.tsx               | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
index a9f9c195..fb70e055 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/QuestionSettings.tsx
@@ -45,7 +45,8 @@ const QuestionSettings = ({ activeSlide, competitionId }: QuestionSettingsProps)
           })
           .catch(console.log)
       } else {
-        const score = Math.min(+event.target.value, maxScore)
+        // Sets score to event.target.value if it's between 0 and max
+        const score = Math.max(0, Math.min(+event.target.value, maxScore))
         setScore(score)
         await axios
           .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}`, {
diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
index f8d871d0..086a64a4 100644
--- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
+++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx
@@ -27,7 +27,8 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => {
 
   const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
     /** If timer value is above the max value, set the timer value to max value to not overflow the server */
-    const timerValue = Math.min(+event.target.value, maxTime)
+    // Sets score to event.target.value if it's between 0 and max
+    const timerValue = Math.max(0, Math.min(+event.target.value, maxTime))
     if (activeSlide) {
       setTimer(timerValue)
       await axios
-- 
GitLab