From ee7b18cf90dd497d8b03282116204885840af536 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Sun, 4 Apr 2021 16:33:41 +0200
Subject: [PATCH 01/20] #39: Add first socketio

---
 client/package-lock.json |  88 +++++++++++++++++++++++++++++++++++++++
 client/package.json      |   2 +
 client/src/App.tsx       |   3 ++
 server/app/__init__.py   |   7 +++-
 server/app/sockets.py    |  13 ++++++
 server/configmodule.py   |   1 +
 server/main.py           |  23 ++--------
 server/requirements.txt  | Bin 2106 -> 2308 bytes
 server/tests/__init__.py |   2 +-
 9 files changed, 117 insertions(+), 22 deletions(-)
 create mode 100644 server/app/sockets.py

diff --git a/client/package-lock.json b/client/package-lock.json
index babf4067..7edbd634 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2318,6 +2318,11 @@
         "@types/node": "*"
       }
     },
+    "@types/component-emitter": {
+      "version": "1.2.10",
+      "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
+      "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
+    },
     "@types/enzyme": {
       "version": "3.10.8",
       "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.8.tgz",
@@ -2526,6 +2531,11 @@
         "@types/node": "*"
       }
     },
+    "@types/socket.io-client": {
+      "version": "1.4.36",
+      "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz",
+      "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag=="
+    },
     "@types/source-list-map": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -3909,6 +3919,11 @@
       "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
       "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
     },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -3964,6 +3979,11 @@
         }
       }
     },
+    "base64-arraybuffer": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
+      "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5928,6 +5948,30 @@
         "once": "^1.4.0"
       }
     },
+    "engine.io-client": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.0.tgz",
+      "integrity": "sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ==",
+      "requires": {
+        "base64-arraybuffer": "0.1.4",
+        "component-emitter": "~1.3.0",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~4.0.1",
+        "has-cors": "1.1.0",
+        "parseqs": "0.0.6",
+        "parseuri": "0.0.6",
+        "ws": "~7.4.2",
+        "yeast": "0.1.2"
+      }
+    },
+    "engine.io-parser": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
+      "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
+      "requires": {
+        "base64-arraybuffer": "0.1.4"
+      }
+    },
     "enhanced-resolve": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
@@ -7845,6 +7889,11 @@
       "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
       "dev": true
     },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+    },
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -11816,6 +11865,16 @@
         }
       }
     },
+    "parseqs": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
+      "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
+    },
+    "parseuri": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
+      "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
+    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -15008,6 +15067,30 @@
         }
       }
     },
+    "socket.io-client": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.0.tgz",
+      "integrity": "sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ==",
+      "requires": {
+        "@types/component-emitter": "^1.2.10",
+        "backo2": "~1.0.2",
+        "component-emitter": "~1.3.0",
+        "debug": "~4.3.1",
+        "engine.io-client": "~5.0.0",
+        "parseuri": "0.0.6",
+        "socket.io-parser": "~4.0.4"
+      }
+    },
+    "socket.io-parser": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
+      "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
+      "requires": {
+        "@types/component-emitter": "^1.2.10",
+        "component-emitter": "~1.3.0",
+        "debug": "~4.3.1"
+      }
+    },
     "sockjs": {
       "version": "0.3.20",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz",
@@ -17951,6 +18034,11 @@
         }
       }
     },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/client/package.json b/client/package.json
index e5fbde97..6ae23b2e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -14,6 +14,7 @@
     "@types/node": "^12.19.16",
     "@types/react": "^17.0.1",
     "@types/react-dom": "^17.0.0",
+    "@types/socket.io-client": "^1.4.36",
     "axios": "^0.21.1",
     "formik": "^2.2.6",
     "react": "^17.0.1",
@@ -24,6 +25,7 @@
     "react-scripts": "4.0.2",
     "redux": "^4.0.5",
     "redux-devtools-extension": "^2.13.8",
+    "socket.io-client": "^4.0.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..33268c68 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,9 +1,12 @@
 import { createMuiTheme, MuiThemeProvider, StylesProvider } from '@material-ui/core'
 import React from 'react'
+import io from 'socket.io-client'
 import { ThemeProvider } from 'styled-components'
 import Main from './Main'
 import { Wrapper } from './styled'
 
+const socket = io('localhost:5000')
+
 const theme = createMuiTheme({
   palette: {
     primary: {
diff --git a/server/app/__init__.py b/server/app/__init__.py
index 7b5a8add..5f0a8b52 100644
--- a/server/app/__init__.py
+++ b/server/app/__init__.py
@@ -14,6 +14,11 @@ def create_app(config_name="configmodule.DevelopmentConfig"):
         bcrypt.init_app(app)
         jwt.init_app(app)
         db.init_app(app)
+        db.create_all()
+
+        from .sockets import sio
+
+        sio.init_app(app)
 
         from app.apis import flask_api
 
@@ -25,7 +30,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"):
             if rp != "/" and rp.endswith("/"):
                 return redirect(rp[:-1])
 
-        return app
+    return app, sio
 
 
 def identity(payload):
diff --git a/server/app/sockets.py b/server/app/sockets.py
new file mode 100644
index 00000000..598d10f5
--- /dev/null
+++ b/server/app/sockets.py
@@ -0,0 +1,13 @@
+from flask_socketio import SocketIO
+
+sio = SocketIO(cors_allowed_origins="http://localhost:3000")
+
+
+@sio.on("connect")
+def connect():
+    print("A client connected!")
+
+
+@sio.on("message")
+def handle_message(data):
+    print("received message" + data)
diff --git a/server/configmodule.py b/server/configmodule.py
index f7c2935f..35afc88a 100644
--- a/server/configmodule.py
+++ b/server/configmodule.py
@@ -16,6 +16,7 @@ class Config:
 class DevelopmentConfig(Config):
     DEBUG = True
     SQLALCHEMY_DATABASE_URI = "sqlite:///database.db"
+    SQLALCHEMY_TRACK_MODIFICATIONS = False
 
 
 class TestingConfig(Config):
diff --git a/server/main.py b/server/main.py
index cb616855..bf4a2393 100644
--- a/server/main.py
+++ b/server/main.py
@@ -1,22 +1,5 @@
-from app import create_app, db
-
-# Development port
-DEFAULT_DEV_PORT = 5000
-
-# Production port
-DEFAULT_PRO_PORT = 8080
+from app import create_app
 
 if __name__ == "__main__":
-    app = create_app("configmodule.DevelopmentConfig")
-    with app.app_context():
-        db.create_all()
-    app.run(port=5000)
-    # CONFIG = "configmodule.DevelopmentConfig"
-
-    # if "production-teknik8" in os.environ:
-    #     CONFIG = "configmodule.ProductionConfig"
-
-    # if "configmodule.DevelopmentConfig" == CONFIG:
-    #     app.run(port=DEFAULT_DEV_PORT)
-    # else:
-    #     app.run(host="0.0.0.0", port=DEFAULT_PRO_PORT)
+    app, sio = create_app("configmodule.DevelopmentConfig")
+    sio.run(app, port=5000)
diff --git a/server/requirements.txt b/server/requirements.txt
index 463ef4fe07f14b03b7771a25842170c8903f73f7..c64222b754a34a97ff5c2f57bd017ac682dbd42c 100644
GIT binary patch
delta 236
zcmdlb&?2-Ugt<PQA(f#Fi1Qdq7;J&ih{2FS4~&f%cp11DkmYq5%7H4A7>XJ48Il>Y
zfozB#1F#MV3DHvolmnTW16Knw6{NxdY-%P$3Q#tY!FF>jb09M-SaS1bwsyvlDxlc~
oKwWx3jO;EGpol44KSEU|*yX7VMGR>`^OAwOK)wK}G={4N0R1N>^#A|>

delta 23
ecmZn>+9j|dgn9ER<|t-X0|q??lg*m!?Ti3d7zRfG

diff --git a/server/tests/__init__.py b/server/tests/__init__.py
index 0b0deaf0..c5b8f20d 100644
--- a/server/tests/__init__.py
+++ b/server/tests/__init__.py
@@ -4,7 +4,7 @@ from app import create_app, db
 
 @pytest.fixture
 def app():
-    app = create_app("configmodule.TestingConfig")
+    app, _ = create_app("configmodule.TestingConfig")
 
     """
     with app.app_context():
-- 
GitLab


From 9d8820b3b36d904f30c0fb6d4f48a7f5f1393b0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Mon, 5 Apr 2021 20:25:09 +0200
Subject: [PATCH 02/20] Add competition state to redux and sockets

---
 client/src/actions/competition.ts         | 19 +++++++++++
 client/src/actions/sockets.ts             |  3 --
 client/src/actions/types.ts               |  4 +++
 client/src/pages/views/JudgeViewPage.tsx  | 24 +++++++++++--
 client/src/reducers/allReducers.ts        |  2 ++
 client/src/reducers/competitionReducer.ts | 41 +++++++++++++++++++++++
 client/src/sockets.ts                     | 24 +++++++++++++
 server/app/sockets.py                     | 23 ++++++++++---
 8 files changed, 130 insertions(+), 10 deletions(-)
 create mode 100644 client/src/actions/competition.ts
 delete mode 100644 client/src/actions/sockets.ts
 create mode 100644 client/src/reducers/competitionReducer.ts
 create mode 100644 client/src/sockets.ts

diff --git a/client/src/actions/competition.ts b/client/src/actions/competition.ts
new file mode 100644
index 00000000..009f1107
--- /dev/null
+++ b/client/src/actions/competition.ts
@@ -0,0 +1,19 @@
+import types from './types'
+
+export const setSlide = (slide: number) => ({
+  type: types.SET_SLIDE,
+  payload: slide,
+})
+
+export const nextSlide = () => ({
+  type: types.NEXT_SLIDE,
+})
+
+export const prevSlide = () => ({
+  type: types.PREV_SLIDE,
+})
+
+export const setCompetitionID = (competitionID: string) => ({
+  type: types.SET_COMPEITION_ID,
+  payload: competitionID,
+})
diff --git a/client/src/actions/sockets.ts b/client/src/actions/sockets.ts
deleted file mode 100644
index b7157820..00000000
--- a/client/src/actions/sockets.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import io from 'socket.io-client'
-
-export const connect = () => io('localhost:5000')
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index d265a692..0d9313f4 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -17,4 +17,8 @@ export default {
   AXIOS_POST: 'AXIOS_POST',
   AXIOS_POST_SUCCESS: 'AXIOS_POST_SUCCESS',
   AXIOS_POST_ERROR: 'AXIOS_POST_ERROR',
+  SET_SLIDE: 'SET_SLIDE',
+  NEXT_SLIDE: 'NEXT_SLIDE',
+  PREV_SLIDE: 'PREV_SLIDE',
+  SET_COMPEITION_ID: 'SET_COMPETITION_ID',
 }
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 457ebe08..d3ee6cbe 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -1,7 +1,27 @@
-import React from 'react'
+import React, { useEffect } from 'react'
+import { nextSlide, prevSlide, setCompetitionID, setSlide } from '../../actions/competition'
+import { useAppDispatch } from '../../hooks'
+import { connect, syncSlide } from '../../sockets'
+import store from '../../store'
 
 const JudgeViewPage: React.FC = () => {
-  return <div>Judge</div>
+  const dispatch = useAppDispatch()
+
+  useEffect(() => {
+    dispatch(setCompetitionID('123456'))
+    dispatch(setSlide(1))
+    connect()
+  }, [])
+
+  return (
+    <div>
+      <div>Judge</div>
+      <button onClick={syncSlide}>Sync</button>
+      <button onClick={() => dispatch(nextSlide())}>Next</button>
+      <button onClick={() => dispatch(prevSlide())}>Prev</button>
+      <div>{store.getState().competion.slide}</div>
+    </div>
+  )
 }
 
 export default JudgeViewPage
diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts
index b12b5346..700720bc 100644
--- a/client/src/reducers/allReducers.ts
+++ b/client/src/reducers/allReducers.ts
@@ -2,6 +2,7 @@
 
 import { combineReducers } from 'redux'
 import citiesReducer from './citiesReducer'
+import competitionReducer from './competitionReducer'
 import competitionsReducer from './competitionsReducer'
 import uiReducer from './uiReducer'
 import userReducer from './userReducer'
@@ -11,6 +12,7 @@ const allReducers = combineReducers({
   user: userReducer,
   UI: uiReducer,
   competitions: competitionsReducer,
+  competion: competitionReducer,
   cities: citiesReducer,
 })
 export default allReducers
diff --git a/client/src/reducers/competitionReducer.ts b/client/src/reducers/competitionReducer.ts
new file mode 100644
index 00000000..65bd5a0c
--- /dev/null
+++ b/client/src/reducers/competitionReducer.ts
@@ -0,0 +1,41 @@
+import { AnyAction } from 'redux'
+import Types from '../actions/types'
+
+interface CompetitionState {
+  competitionID: string | null
+  slide: number
+  timer: number | null
+}
+
+const initialState: CompetitionState = {
+  competitionID: null,
+  slide: 0,
+  timer: null,
+}
+
+export default function (state = initialState, action: AnyAction) {
+  switch (action.type) {
+    case Types.SET_SLIDE:
+      return {
+        ...state,
+        slide: action.payload,
+      }
+    case Types.NEXT_SLIDE:
+      return {
+        ...state,
+        slide: state.slide + 1,
+      }
+    case Types.PREV_SLIDE:
+      return {
+        ...state,
+        slide: state.slide - 1,
+      }
+    case Types.SET_COMPEITION_ID:
+      return {
+        ...state,
+        competitionID: action.payload,
+      }
+    default:
+      return state
+  }
+}
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
new file mode 100644
index 00000000..7de89b5e
--- /dev/null
+++ b/client/src/sockets.ts
@@ -0,0 +1,24 @@
+import io from 'socket.io-client'
+import { setSlide } from './actions/competition'
+import store from './store'
+
+let socket: SocketIOClient.Socket
+
+interface SyncSlideInterface {
+  slide: number
+}
+
+export const connect = () => {
+  if (!socket) {
+    socket = io('localhost:5000')
+    socket.emit('join_competition', { competitionID: store.getState().competion.competitionID })
+  }
+
+  socket.on('sync_slide', (data: SyncSlideInterface) => {
+    store.dispatch(setSlide(data.slide))
+  })
+}
+
+export const syncSlide = () => {
+  socket.emit('sync_slide', { slide: store.getState().competion.slide })
+}
diff --git a/server/app/sockets.py b/server/app/sockets.py
index 598d10f5..cb1d776c 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -1,13 +1,26 @@
-from flask_socketio import SocketIO
+from flask.globals import request
+from flask_socketio import SocketIO, emit, join_room, leave_room
 
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
 
 @sio.on("connect")
 def connect():
-    print("A client connected!")
+    print(f"[Connected]: {request.sid}")
 
 
-@sio.on("message")
-def handle_message(data):
-    print("received message" + data)
+@sio.on("disconnect")
+def disconnect():
+    print(f"[Disconnected]: {request.sid}")
+
+
+@sio.on("join_competition")
+def join_competition(data):
+    join_room(data["competitionID"])
+    print(f"[Join room]: {request.sid} -> {data['competitionID']}")
+
+
+@sio.on("sync_slide")
+def sync_slide(data):
+    emit("sync_slide", {"slide": data["slide"]}, room="123456")
+    print("[Sync slide]")
-- 
GitLab


From 4d161e137d52945c75901f2df0d36173ce6f73e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Mon, 5 Apr 2021 21:28:29 +0200
Subject: [PATCH 03/20] Add timer state to redux

---
 client/src/actions/competition.ts         |  9 +++++++++
 client/src/actions/types.ts               |  2 ++
 client/src/pages/views/JudgeViewPage.tsx  | 17 +++++++++++++++--
 client/src/reducers/competitionReducer.ts | 16 ++++++++++++++--
 client/src/sockets.ts                     | 22 ++++++++++++++++++++--
 server/app/sockets.py                     | 10 ++++++++--
 6 files changed, 68 insertions(+), 8 deletions(-)

diff --git a/client/src/actions/competition.ts b/client/src/actions/competition.ts
index 009f1107..d3010bed 100644
--- a/client/src/actions/competition.ts
+++ b/client/src/actions/competition.ts
@@ -17,3 +17,12 @@ export const setCompetitionID = (competitionID: string) => ({
   type: types.SET_COMPEITION_ID,
   payload: competitionID,
 })
+
+export const tickTimer = () => ({
+  type: types.TICK_TIMER,
+})
+
+export const setTimer = (timer: number) => ({
+  type: types.SET_TIMER,
+  payload: timer,
+})
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index 0d9313f4..514f4d8e 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -21,4 +21,6 @@ export default {
   NEXT_SLIDE: 'NEXT_SLIDE',
   PREV_SLIDE: 'PREV_SLIDE',
   SET_COMPEITION_ID: 'SET_COMPETITION_ID',
+  TICK_TIMER: 'TICK_TIMER',
+  SET_TIMER: 'SET_TIMER',
 }
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index d3ee6cbe..4d12e031 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -1,7 +1,7 @@
 import React, { useEffect } from 'react'
-import { nextSlide, prevSlide, setCompetitionID, setSlide } from '../../actions/competition'
+import { nextSlide, prevSlide, setCompetitionID, setSlide, setTimer, tickTimer } from '../../actions/competition'
 import { useAppDispatch } from '../../hooks'
-import { connect, syncSlide } from '../../sockets'
+import { connect, syncSlide, syncTimer } from '../../sockets'
 import store from '../../store'
 
 const JudgeViewPage: React.FC = () => {
@@ -10,7 +10,20 @@ const JudgeViewPage: React.FC = () => {
   useEffect(() => {
     dispatch(setCompetitionID('123456'))
     dispatch(setSlide(1))
+    dispatch(setTimer(5))
     connect()
+
+    const timerTimer = setInterval(() => {
+      dispatch(tickTimer())
+      syncTimer()
+
+      console.log(store.getState().competion.timer)
+
+      if (store.getState().competion.timer === 0) {
+        clearInterval(timerTimer)
+        // TODO: lock the competitors slide when this happens
+      }
+    }, 1000)
   }, [])
 
   return (
diff --git a/client/src/reducers/competitionReducer.ts b/client/src/reducers/competitionReducer.ts
index 65bd5a0c..a2de6903 100644
--- a/client/src/reducers/competitionReducer.ts
+++ b/client/src/reducers/competitionReducer.ts
@@ -4,13 +4,15 @@ import Types from '../actions/types'
 interface CompetitionState {
   competitionID: string | null
   slide: number
-  timer: number | null
+  timer: number
+  isLocked: boolean
 }
 
 const initialState: CompetitionState = {
   competitionID: null,
   slide: 0,
-  timer: null,
+  timer: 0,
+  isLocked: false,
 }
 
 export default function (state = initialState, action: AnyAction) {
@@ -35,6 +37,16 @@ export default function (state = initialState, action: AnyAction) {
         ...state,
         competitionID: action.payload,
       }
+    case Types.TICK_TIMER:
+      return {
+        ...state,
+        timer: state.timer - 1,
+      }
+    case Types.SET_TIMER:
+      return {
+        ...state,
+        timer: action.payload,
+      }
     default:
       return state
   }
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 7de89b5e..501547c7 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,5 +1,5 @@
 import io from 'socket.io-client'
-import { setSlide } from './actions/competition'
+import { setSlide, setTimer } from './actions/competition'
 import store from './store'
 
 let socket: SocketIOClient.Socket
@@ -8,6 +8,10 @@ interface SyncSlideInterface {
   slide: number
 }
 
+interface SyncTimerInterface {
+  timer: number
+}
+
 export const connect = () => {
   if (!socket) {
     socket = io('localhost:5000')
@@ -17,8 +21,22 @@ export const connect = () => {
   socket.on('sync_slide', (data: SyncSlideInterface) => {
     store.dispatch(setSlide(data.slide))
   })
+
+  socket.on('sync_timer', (data: SyncTimerInterface) => {
+    store.dispatch(setTimer(data.timer))
+  })
 }
 
 export const syncSlide = () => {
-  socket.emit('sync_slide', { slide: store.getState().competion.slide })
+  socket.emit('sync_slide', {
+    slide: store.getState().competion.slide,
+    room: store.getState().competion.competitionID,
+  })
+}
+
+export const syncTimer = () => {
+  socket.emit('sync_timer', {
+    room: store.getState().competion.competitionID,
+    timer: store.getState().competion.timer,
+  })
 }
diff --git a/server/app/sockets.py b/server/app/sockets.py
index cb1d776c..76011dc5 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -22,5 +22,11 @@ def join_competition(data):
 
 @sio.on("sync_slide")
 def sync_slide(data):
-    emit("sync_slide", {"slide": data["slide"]}, room="123456")
-    print("[Sync slide]")
+    emit("sync_slide", {"slide": data["slide"]}, room=data["room"])
+    print(f"[Sync slide]: {data['slide']} -> {data['room']}")
+
+
+@sio.on("sync_timer")
+def sync_timer(data):
+    emit("sync_timer", {"timer": data["timer"]}, room=data["room"])
+    print(f"[Sync timer]: {data['timer']} -> {data['room']}")
-- 
GitLab


From f2ef03182b54917551e837854d1d1fcf4305039f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Mon, 5 Apr 2021 21:43:07 +0200
Subject: [PATCH 04/20] Refactor competitionReducer a bit

---
 client/src/actions/competition.ts         | 10 +++++++---
 client/src/actions/types.ts               |  3 ---
 client/src/reducers/competitionReducer.ts | 15 ---------------
 server/app/sockets.py                     |  4 ++--
 4 files changed, 9 insertions(+), 23 deletions(-)

diff --git a/client/src/actions/competition.ts b/client/src/actions/competition.ts
index d3010bed..b3c7d434 100644
--- a/client/src/actions/competition.ts
+++ b/client/src/actions/competition.ts
@@ -1,3 +1,4 @@
+import store from '../store'
 import types from './types'
 
 export const setSlide = (slide: number) => ({
@@ -6,11 +7,13 @@ export const setSlide = (slide: number) => ({
 })
 
 export const nextSlide = () => ({
-  type: types.NEXT_SLIDE,
+  type: types.SET_SLIDE,
+  payload: store.getState().competion.slide + 1,
 })
 
 export const prevSlide = () => ({
-  type: types.PREV_SLIDE,
+  type: types.SET_SLIDE,
+  payload: store.getState().competion.slide - 1,
 })
 
 export const setCompetitionID = (competitionID: string) => ({
@@ -19,7 +22,8 @@ export const setCompetitionID = (competitionID: string) => ({
 })
 
 export const tickTimer = () => ({
-  type: types.TICK_TIMER,
+  type: types.SET_TIMER,
+  payload: store.getState().competion.timer - 1,
 })
 
 export const setTimer = (timer: number) => ({
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index 514f4d8e..c8a85764 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -18,9 +18,6 @@ export default {
   AXIOS_POST_SUCCESS: 'AXIOS_POST_SUCCESS',
   AXIOS_POST_ERROR: 'AXIOS_POST_ERROR',
   SET_SLIDE: 'SET_SLIDE',
-  NEXT_SLIDE: 'NEXT_SLIDE',
-  PREV_SLIDE: 'PREV_SLIDE',
   SET_COMPEITION_ID: 'SET_COMPETITION_ID',
-  TICK_TIMER: 'TICK_TIMER',
   SET_TIMER: 'SET_TIMER',
 }
diff --git a/client/src/reducers/competitionReducer.ts b/client/src/reducers/competitionReducer.ts
index a2de6903..19866e2d 100644
--- a/client/src/reducers/competitionReducer.ts
+++ b/client/src/reducers/competitionReducer.ts
@@ -22,26 +22,11 @@ export default function (state = initialState, action: AnyAction) {
         ...state,
         slide: action.payload,
       }
-    case Types.NEXT_SLIDE:
-      return {
-        ...state,
-        slide: state.slide + 1,
-      }
-    case Types.PREV_SLIDE:
-      return {
-        ...state,
-        slide: state.slide - 1,
-      }
     case Types.SET_COMPEITION_ID:
       return {
         ...state,
         competitionID: action.payload,
       }
-    case Types.TICK_TIMER:
-      return {
-        ...state,
-        timer: state.timer - 1,
-      }
     case Types.SET_TIMER:
       return {
         ...state,
diff --git a/server/app/sockets.py b/server/app/sockets.py
index 76011dc5..b4f97045 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -22,11 +22,11 @@ def join_competition(data):
 
 @sio.on("sync_slide")
 def sync_slide(data):
-    emit("sync_slide", {"slide": data["slide"]}, room=data["room"])
+    emit("sync_slide", {"slide": data["slide"]}, room=data["room"], include_self=False)
     print(f"[Sync slide]: {data['slide']} -> {data['room']}")
 
 
 @sio.on("sync_timer")
 def sync_timer(data):
-    emit("sync_timer", {"timer": data["timer"]}, room=data["room"])
+    emit("sync_timer", {"timer": data["timer"]}, room=data["room"], include_self=False)
     print(f"[Sync timer]: {data['timer']} -> {data['room']}")
-- 
GitLab


From fc59d94cf777e91809478a2f23d11235d5d27653 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Tue, 6 Apr 2021 10:46:05 +0200
Subject: [PATCH 05/20] Add proof-of-concept timer and slide sync

---
 client/src/actions/competition.ts           | 17 ++++---
 client/src/actions/types.ts                 |  1 +
 client/src/pages/views/JudgeViewPage.tsx    | 28 ++++++------
 client/src/pages/views/components/Timer.tsx | 50 +++++++++++++++++++++
 client/src/reducers/allReducers.ts          |  2 +-
 client/src/reducers/competitionReducer.ts   | 22 +++++++--
 client/src/sockets.ts                       | 22 +++++----
 server/app/sockets.py                       | 16 ++++---
 8 files changed, 119 insertions(+), 39 deletions(-)
 create mode 100644 client/src/pages/views/components/Timer.tsx

diff --git a/client/src/actions/competition.ts b/client/src/actions/competition.ts
index b3c7d434..ef18927b 100644
--- a/client/src/actions/competition.ts
+++ b/client/src/actions/competition.ts
@@ -8,12 +8,12 @@ export const setSlide = (slide: number) => ({
 
 export const nextSlide = () => ({
   type: types.SET_SLIDE,
-  payload: store.getState().competion.slide + 1,
+  payload: store.getState().competition.slide + 1,
 })
 
 export const prevSlide = () => ({
   type: types.SET_SLIDE,
-  payload: store.getState().competion.slide - 1,
+  payload: store.getState().competition.slide - 1,
 })
 
 export const setCompetitionID = (competitionID: string) => ({
@@ -21,12 +21,17 @@ export const setCompetitionID = (competitionID: string) => ({
   payload: competitionID,
 })
 
-export const tickTimer = () => ({
+export const setTimer = (timer: number) => ({
   type: types.SET_TIMER,
-  payload: store.getState().competion.timer - 1,
+  payload: timer,
 })
 
-export const setTimer = (timer: number) => ({
+export const tickTimer = (timestep: number) => ({
   type: types.SET_TIMER,
-  payload: timer,
+  payload: store.getState().competition.timer.value - timestep,
+})
+
+export const setTimerEnabled = (enabled: boolean) => ({
+  type: types.SET_TIMER_ENABLED,
+  payload: enabled,
 })
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index c8a85764..3cc59479 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -20,4 +20,5 @@ export default {
   SET_SLIDE: 'SET_SLIDE',
   SET_COMPEITION_ID: 'SET_COMPETITION_ID',
   SET_TIMER: 'SET_TIMER',
+  SET_TIMER_ENABLED: 'SET_TIMER_ENABLED',
 }
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 4d12e031..c1b0f11c 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -1,8 +1,8 @@
 import React, { useEffect } from 'react'
-import { nextSlide, prevSlide, setCompetitionID, setSlide, setTimer, tickTimer } from '../../actions/competition'
+import { nextSlide, prevSlide, setCompetitionID, setSlide, setTimer, setTimerEnabled } from '../../actions/competition'
 import { useAppDispatch } from '../../hooks'
 import { connect, syncSlide, syncTimer } from '../../sockets'
-import store from '../../store'
+import Timer from './components/Timer'
 
 const JudgeViewPage: React.FC = () => {
   const dispatch = useAppDispatch()
@@ -12,27 +12,25 @@ const JudgeViewPage: React.FC = () => {
     dispatch(setSlide(1))
     dispatch(setTimer(5))
     connect()
-
-    const timerTimer = setInterval(() => {
-      dispatch(tickTimer())
-      syncTimer()
-
-      console.log(store.getState().competion.timer)
-
-      if (store.getState().competion.timer === 0) {
-        clearInterval(timerTimer)
-        // TODO: lock the competitors slide when this happens
-      }
-    }, 1000)
+    // syncTimer()
   }, [])
 
+  useEffect
+
   return (
     <div>
       <div>Judge</div>
+      Slides:
       <button onClick={syncSlide}>Sync</button>
       <button onClick={() => dispatch(nextSlide())}>Next</button>
       <button onClick={() => dispatch(prevSlide())}>Prev</button>
-      <div>{store.getState().competion.slide}</div>
+      <br></br>
+      Timer:
+      <button onClick={syncTimer}>Sync</button>
+      <button onClick={() => dispatch(setTimerEnabled(true))}>Enable</button>
+      <button onClick={() => dispatch(setTimerEnabled(false))}>Disable</button>
+      <button onClick={() => dispatch(setTimer(5))}>5 Seconds</button>
+      <Timer></Timer>
     </div>
   )
 }
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
new file mode 100644
index 00000000..4cbdab5a
--- /dev/null
+++ b/client/src/pages/views/components/Timer.tsx
@@ -0,0 +1,50 @@
+import React, { useEffect } from 'react'
+import { connect } from 'react-redux'
+import { setTimerEnabled, tickTimer } from '../../../actions/competition'
+import { useAppDispatch } from '../../../hooks'
+
+const mapStateToProps = (state: any) => {
+  return {
+    timer: state.competition.timer,
+  }
+}
+
+const mapDispatchToProps = (dispatch: any) => {
+  return {
+    tickTimer: () => dispatch(tickTimer(1)),
+  }
+}
+
+let timerIntervalId: NodeJS.Timeout
+
+const Timer: React.FC = (props: any) => {
+  const dispatch = useAppDispatch()
+
+  useEffect(() => {
+    if (props.timer.enabled) {
+      timerIntervalId = setInterval(() => {
+        if (!props.timer.enabled) {
+          clearInterval(timerIntervalId)
+        } else {
+          dispatch(tickTimer(1))
+        }
+      }, 1000)
+    }
+  }, [props.timer.enabled])
+
+  useEffect(() => {
+    if (props.timer.value <= 0) {
+      clearInterval(timerIntervalId)
+      dispatch(setTimerEnabled(false))
+    }
+  }, [props.timer.value])
+
+  return (
+    <>
+      <div>Timer: {props.timer.value}</div>
+      <div>Enabled: {props.timer.enabled.toString()}</div>
+    </>
+  )
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Timer)
diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts
index 700720bc..4453ad92 100644
--- a/client/src/reducers/allReducers.ts
+++ b/client/src/reducers/allReducers.ts
@@ -12,7 +12,7 @@ const allReducers = combineReducers({
   user: userReducer,
   UI: uiReducer,
   competitions: competitionsReducer,
-  competion: competitionReducer,
+  competition: competitionReducer,
   cities: citiesReducer,
 })
 export default allReducers
diff --git a/client/src/reducers/competitionReducer.ts b/client/src/reducers/competitionReducer.ts
index 19866e2d..431ee504 100644
--- a/client/src/reducers/competitionReducer.ts
+++ b/client/src/reducers/competitionReducer.ts
@@ -1,17 +1,22 @@
 import { AnyAction } from 'redux'
 import Types from '../actions/types'
 
+interface TimerState {
+  value: number
+  enabled: boolean
+}
+
 interface CompetitionState {
   competitionID: string | null
   slide: number
-  timer: number
+  timer: TimerState
   isLocked: boolean
 }
 
 const initialState: CompetitionState = {
   competitionID: null,
   slide: 0,
-  timer: 0,
+  timer: { value: 0, enabled: false },
   isLocked: false,
 }
 
@@ -30,7 +35,18 @@ export default function (state = initialState, action: AnyAction) {
     case Types.SET_TIMER:
       return {
         ...state,
-        timer: action.payload,
+        timer: {
+          ...state.timer,
+          value: action.payload,
+        },
+      }
+    case Types.SET_TIMER_ENABLED:
+      return {
+        ...state,
+        timer: {
+          ...state.timer,
+          enabled: action.payload,
+        },
       }
     default:
       return state
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 501547c7..7e3e3ad1 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,5 +1,5 @@
 import io from 'socket.io-client'
-import { setSlide, setTimer } from './actions/competition'
+import { setSlide, setTimer, setTimerEnabled } from './actions/competition'
 import store from './store'
 
 let socket: SocketIOClient.Socket
@@ -8,14 +8,19 @@ interface SyncSlideInterface {
   slide: number
 }
 
+interface TimerInterface {
+  value: number
+  enabled: boolean
+}
+
 interface SyncTimerInterface {
-  timer: number
+  timer: TimerInterface
 }
 
 export const connect = () => {
   if (!socket) {
     socket = io('localhost:5000')
-    socket.emit('join_competition', { competitionID: store.getState().competion.competitionID })
+    socket.emit('join_competition', { competitionID: store.getState().competition.competitionID })
   }
 
   socket.on('sync_slide', (data: SyncSlideInterface) => {
@@ -23,20 +28,21 @@ export const connect = () => {
   })
 
   socket.on('sync_timer', (data: SyncTimerInterface) => {
-    store.dispatch(setTimer(data.timer))
+    store.dispatch(setTimer(data.timer.value))
+    store.dispatch(setTimerEnabled(data.timer.enabled))
   })
 }
 
 export const syncSlide = () => {
   socket.emit('sync_slide', {
-    slide: store.getState().competion.slide,
-    room: store.getState().competion.competitionID,
+    slide: store.getState().competition.slide,
+    competitionID: store.getState().competition.competitionID,
   })
 }
 
 export const syncTimer = () => {
   socket.emit('sync_timer', {
-    room: store.getState().competion.competitionID,
-    timer: store.getState().competion.timer,
+    competitionID: store.getState().competition.competitionID,
+    timer: store.getState().competition.timer,
   })
 }
diff --git a/server/app/sockets.py b/server/app/sockets.py
index b4f97045..d9407a69 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -1,5 +1,5 @@
 from flask.globals import request
-from flask_socketio import SocketIO, emit, join_room, leave_room
+from flask_socketio import SocketIO, emit, join_room
 
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
@@ -16,17 +16,21 @@ def disconnect():
 
 @sio.on("join_competition")
 def join_competition(data):
+    competitionID = data["competitionID"]
     join_room(data["competitionID"])
-    print(f"[Join room]: {request.sid} -> {data['competitionID']}")
+    print(f"[Join room]: {request.sid} -> {competitionID}")
 
 
 @sio.on("sync_slide")
 def sync_slide(data):
-    emit("sync_slide", {"slide": data["slide"]}, room=data["room"], include_self=False)
-    print(f"[Sync slide]: {data['slide']} -> {data['room']}")
+    slide, competitionID = data["slide"], data["competitionID"]
+    emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False)
+    print(f"[Sync slide]: {slide} -> {competitionID}")
 
 
 @sio.on("sync_timer")
 def sync_timer(data):
-    emit("sync_timer", {"timer": data["timer"]}, room=data["room"], include_self=False)
-    print(f"[Sync timer]: {data['timer']} -> {data['room']}")
+    competitionID = data["competitionID"]
+    timer = data["timer"]
+    emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False)
+    print(f"[Sync timer]: {competitionID=} {timer=}")
-- 
GitLab


From 1c1692fc1773a296f0ef14f08b82789461d52932 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <victor.l0fgr3n@gmail.com>
Date: Tue, 6 Apr 2021 12:01:36 +0200
Subject: [PATCH 06/20] Fix disable function in timer

---
 client/src/pages/views/components/Timer.tsx | 8 +++-----
 client/src/sockets.ts                       | 2 +-
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 4cbdab5a..26f42133 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -23,12 +23,10 @@ const Timer: React.FC = (props: any) => {
   useEffect(() => {
     if (props.timer.enabled) {
       timerIntervalId = setInterval(() => {
-        if (!props.timer.enabled) {
-          clearInterval(timerIntervalId)
-        } else {
-          dispatch(tickTimer(1))
-        }
+        dispatch(tickTimer(1))
       }, 1000)
+    } else {
+      clearInterval(timerIntervalId)
     }
   }, [props.timer.enabled])
 
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 7e3e3ad1..12dcbb3e 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -28,8 +28,8 @@ export const connect = () => {
   })
 
   socket.on('sync_timer', (data: SyncTimerInterface) => {
-    store.dispatch(setTimer(data.timer.value))
     store.dispatch(setTimerEnabled(data.timer.enabled))
+    store.dispatch(setTimer(data.timer.value))
   })
 }
 
-- 
GitLab


From 02c7c5fafb25370804c3473304aca4e028fb4215 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 13 Apr 2021 09:33:21 +0200
Subject: [PATCH 07/20] Save code in redux

---
 client/package.json                          | 2 +-
 client/src/actions/presentation.ts           | 4 ++++
 client/src/actions/types.ts                  | 1 +
 client/src/pages/views/PresenterViewPage.tsx | 4 ++++
 client/src/reducers/presentationReducer.ts   | 7 +++++++
 client/src/sockets.ts                        | 8 ++++----
 server/app/sockets.py                        | 6 +++---
 7 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/client/package.json b/client/package.json
index bbc78d52..7129c30e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -92,4 +92,4 @@
     ]
   },
   "proxy": "http://localhost:5000/api/"
-}
\ No newline at end of file
+}
diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index 9e482d2b..f53d34cc 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -42,3 +42,7 @@ export const setCurrentSlidePrevious = () => (dispatch: AppDispatch) => {
 export const setCurrentSlideNext = () => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_SLIDE_NEXT })
 }
+
+export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => {
+  dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
+}
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index 7d0ad17c..7698dc94 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -20,6 +20,7 @@ export default {
   SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS',
   SET_PRESENTATION_SLIDE_NEXT: 'SET_PRESENTATION_SLIDE_NEXT',
   SET_PRESENTATION_TEAMS: 'SET_PRESENTATION_TEAMS',
+  SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE',
   SET_CITIES: 'SET_CITIES',
   SET_CITIES_TOTAL: 'SET_CITIES_TOTAL',
   SET_CITIES_COUNT: 'SET_CITIES_COUNT',
diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx
index 131bde22..8c73c420 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -7,9 +7,11 @@ import {
   getPresentationTeams,
   setCurrentSlideNext,
   setCurrentSlidePrevious,
+  setPresentationCode,
 } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
+import { connectSocket } from '../../sockets'
 import SlideDisplay from './components/SlideDisplay'
 import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled'
 
@@ -22,6 +24,8 @@ const PresenterViewPage: React.FC = () => {
   useEffect(() => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
+    dispatch(setPresentationCode(code))
+    connectSocket()
   }, [])
   const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget)
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index d71bcafb..8b56a09b 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -8,6 +8,7 @@ interface PresentationState {
   competition: RichCompetition
   slide: Slide
   teams: Team[]
+  code: string
 }
 
 const initialState: PresentationState = {
@@ -30,6 +31,7 @@ const initialState: PresentationState = {
     title: '',
   },
   teams: [],
+  code: '',
 }
 
 export default function (state = initialState, action: AnyAction) {
@@ -45,6 +47,11 @@ export default function (state = initialState, action: AnyAction) {
         ...state,
         teams: action.payload as Team[],
       }
+    case Types.SET_PRESENTATION_CODE:
+      return {
+        ...state,
+        code: action.payload,
+      }
     case Types.SET_PRESENTATION_SLIDE:
       return {
         ...state,
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 12dcbb3e..f07956a0 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -17,10 +17,10 @@ interface SyncTimerInterface {
   timer: TimerInterface
 }
 
-export const connect = () => {
+export const connectSocket = () => {
   if (!socket) {
     socket = io('localhost:5000')
-    socket.emit('join_competition', { competitionID: store.getState().competition.competitionID })
+    socket.emit('join_competition', { code: store.getState().presentation.code })
   }
 
   socket.on('sync_slide', (data: SyncSlideInterface) => {
@@ -33,14 +33,14 @@ export const connect = () => {
   })
 }
 
-export const syncSlide = () => {
+export const syncSlideSocket = () => {
   socket.emit('sync_slide', {
     slide: store.getState().competition.slide,
     competitionID: store.getState().competition.competitionID,
   })
 }
 
-export const syncTimer = () => {
+export const syncTimerSocket = () => {
   socket.emit('sync_timer', {
     competitionID: store.getState().competition.competitionID,
     timer: store.getState().competition.timer,
diff --git a/server/app/sockets.py b/server/app/sockets.py
index d9407a69..f526c21a 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -16,9 +16,9 @@ def disconnect():
 
 @sio.on("join_competition")
 def join_competition(data):
-    competitionID = data["competitionID"]
-    join_room(data["competitionID"])
-    print(f"[Join room]: {request.sid} -> {competitionID}")
+    code = data["code"]
+    join_room(data["code"])
+    print(f"[Join room]: {request.sid} -> {code}")
 
 
 @sio.on("sync_slide")
-- 
GitLab


From daca37d2773fb407e6eeca1104c338bd69ad1cbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Tue, 13 Apr 2021 10:45:48 +0200
Subject: [PATCH 08/20] Add functionality to sync slide

---
 client/src/actions/presentation.ts           |  4 ++
 client/src/actions/types.ts                  |  1 +
 client/src/pages/views/JudgeViewPage.tsx     | 12 ++++-
 client/src/pages/views/PresenterViewPage.tsx | 17 ++++++--
 client/src/reducers/presentationReducer.ts   |  7 +++
 client/src/sockets.ts                        | 46 ++++++++++----------
 server/app/sockets.py                        |  7 +--
 7 files changed, 62 insertions(+), 32 deletions(-)

diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index f53d34cc..70345f42 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -43,6 +43,10 @@ export const setCurrentSlideNext = () => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_SLIDE_NEXT })
 }
 
+export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch) => {
+  dispatch({ type: Types.SET_PRESENTATION_SLIDE_BY_ORDER, payload: order })
+}
+
 export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
 }
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index 7698dc94..afd71e8b 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -19,6 +19,7 @@ export default {
   SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE',
   SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS',
   SET_PRESENTATION_SLIDE_NEXT: 'SET_PRESENTATION_SLIDE_NEXT',
+  SET_PRESENTATION_SLIDE_BY_ORDER: 'SET_PRESENTATION_SLIDE_BY_ORDER',
   SET_PRESENTATION_TEAMS: 'SET_PRESENTATION_TEAMS',
   SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE',
   SET_CITIES: 'SET_CITIES',
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 3d373c85..5c664557 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -2,9 +2,15 @@ import { Divider, List, ListItemText } from '@material-ui/core'
 import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
 import React, { useEffect, useState } from 'react'
 import { useParams } from 'react-router-dom'
-import { getPresentationCompetition, getPresentationTeams, setCurrentSlide } from '../../actions/presentation'
+import {
+  getPresentationCompetition,
+  getPresentationTeams,
+  setCurrentSlide,
+  setPresentationCode,
+} from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
+import { connect } from '../../sockets'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import SlideDisplay from './components/SlideDisplay'
@@ -15,7 +21,7 @@ import {
   JudgeQuestionsLabel,
   JudgeToolbar,
   LeftDrawer,
-  RightDrawer
+  RightDrawer,
 } from './styled'
 
 const leftDrawerWidth = 150
@@ -41,6 +47,8 @@ const JudgeViewPage: React.FC = () => {
   useEffect(() => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
+    dispatch(setPresentationCode(code))
+    connect()
   }, [])
   const teams = useAppSelector((state) => state.presentation.teams)
   const slides = useAppSelector((state) => state.presentation.competition.slides)
diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx
index 8c73c420..ee569110 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -11,7 +11,7 @@ import {
 } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
-import { connectSocket } from '../../sockets'
+import { connect, syncSlide } from '../../sockets'
 import SlideDisplay from './components/SlideDisplay'
 import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled'
 
@@ -25,7 +25,7 @@ const PresenterViewPage: React.FC = () => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
     dispatch(setPresentationCode(code))
-    connectSocket()
+    connect()
   }, [])
   const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget)
@@ -33,6 +33,15 @@ const PresenterViewPage: React.FC = () => {
   const handleClose = () => {
     setAnchorEl(null)
   }
+  const handleNextSlidePressed = () => {
+    dispatch(setCurrentSlideNext())
+    syncSlide()
+  }
+  const handlePreviousSlidePressed = () => {
+    dispatch(setCurrentSlidePrevious())
+    syncSlide()
+  }
+
   return (
     <PresenterContainer>
       <PresenterHeader>
@@ -45,10 +54,10 @@ const PresenterViewPage: React.FC = () => {
       </PresenterHeader>
       <SlideDisplay />
       <PresenterFooter>
-        <PresenterButton onClick={() => dispatch(setCurrentSlidePrevious())} variant="contained">
+        <PresenterButton onClick={handlePreviousSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
-        <PresenterButton onClick={() => dispatch(setCurrentSlideNext())} variant="contained">
+        <PresenterButton onClick={handleNextSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
       </PresenterFooter>
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 8b56a09b..d14e5623 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -73,6 +73,13 @@ export default function (state = initialState, action: AnyAction) {
         }
       }
       return state
+    case Types.SET_PRESENTATION_SLIDE_BY_ORDER:
+      if (0 <= action.payload && action.payload < state.competition.slides.length)
+        return {
+          ...state,
+          slide: state.competition.slides[action.payload],
+        }
+      return state
     default:
       return state
   }
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index f07956a0..b0efdcb6 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,5 +1,5 @@
 import io from 'socket.io-client'
-import { setSlide, setTimer, setTimerEnabled } from './actions/competition'
+import { setCurrentSlideByOrder } from './actions/presentation'
 import store from './store'
 
 let socket: SocketIOClient.Socket
@@ -8,41 +8,41 @@ interface SyncSlideInterface {
   slide: number
 }
 
-interface TimerInterface {
-  value: number
-  enabled: boolean
-}
+// interface TimerInterface {
+//   value: number
+//   enabled: boolean
+// }
 
-interface SyncTimerInterface {
-  timer: TimerInterface
-}
+// interface SyncTimerInterface {
+//   timer: TimerInterface
+// }
 
-export const connectSocket = () => {
+export const connect = () => {
   if (!socket) {
     socket = io('localhost:5000')
     socket.emit('join_competition', { code: store.getState().presentation.code })
   }
 
   socket.on('sync_slide', (data: SyncSlideInterface) => {
-    store.dispatch(setSlide(data.slide))
+    setCurrentSlideByOrder(data.slide)(store.dispatch)
   })
 
-  socket.on('sync_timer', (data: SyncTimerInterface) => {
-    store.dispatch(setTimerEnabled(data.timer.enabled))
-    store.dispatch(setTimer(data.timer.value))
-  })
+  // socket.on('sync_timer', (data: SyncTimerInterface) => {
+  //   store.dispatch(setTimerEnabled(data.timer.enabled))
+  //   store.dispatch(setTimer(data.timer.value))
+  // })
 }
 
-export const syncSlideSocket = () => {
+export const syncSlide = () => {
   socket.emit('sync_slide', {
-    slide: store.getState().competition.slide,
-    competitionID: store.getState().competition.competitionID,
+    code: store.getState().presentation.code,
+    slide: store.getState().presentation.slide.order,
   })
 }
 
-export const syncTimerSocket = () => {
-  socket.emit('sync_timer', {
-    competitionID: store.getState().competition.competitionID,
-    timer: store.getState().competition.timer,
-  })
-}
+// export const syncTimerSocket = () => {
+//   socket.emit('sync_timer', {
+//     competitionID: store.getState().competition.competitionID,
+//     timer: store.getState().competition.timer,
+//   })
+// }
diff --git a/server/app/sockets.py b/server/app/sockets.py
index f526c21a..d06ae769 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -23,9 +23,10 @@ def join_competition(data):
 
 @sio.on("sync_slide")
 def sync_slide(data):
-    slide, competitionID = data["slide"], data["competitionID"]
-    emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False)
-    print(f"[Sync slide]: {slide} -> {competitionID}")
+    code = data["code"]
+    slide = data["slide"]
+    emit("sync_slide", {"slide": slide}, room=code, include_self=False)
+    print(f"[Sync slide]: {slide} -> {code}")
 
 
 @sio.on("sync_timer")
-- 
GitLab


From 8ff4d82ac72aebf0c6a6202c2f1b6ef749d8e4c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Wed, 14 Apr 2021 10:10:06 +0200
Subject: [PATCH 09/20] Add sync timer (not finished)

---
 client/src/pages/views/JudgeViewPage.tsx    |  2 +
 client/src/pages/views/components/Timer.tsx | 14 ++++++-
 client/src/sockets.ts                       | 41 +++++++++++----------
 server/app/sockets.py                       | 31 +++++++++++++---
 4 files changed, 61 insertions(+), 27 deletions(-)

diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 5c664557..c6601532 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -14,6 +14,7 @@ import { connect } from '../../sockets'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import SlideDisplay from './components/SlideDisplay'
+import Timer from './components/Timer'
 import {
   Content,
   JudgeAnswersLabel,
@@ -73,6 +74,7 @@ const JudgeViewPage: React.FC = () => {
         anchor="left"
       >
         <div className={classes.toolbar} />
+        <Timer></Timer>
         <List>
           {slides.map((slide, index) => (
             <SlideListItem
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 26f42133..4fe85973 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -1,7 +1,8 @@
 import React, { useEffect } from 'react'
 import { connect } from 'react-redux'
-import { setTimerEnabled, tickTimer } from '../../../actions/competition'
+import { setTimer, setTimerEnabled, tickTimer } from '../../../actions/competition'
 import { useAppDispatch } from '../../../hooks'
+import { syncTimer } from '../../../sockets'
 
 const mapStateToProps = (state: any) => {
   return {
@@ -41,6 +42,17 @@ const Timer: React.FC = (props: any) => {
     <>
       <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
+      <button onClick={syncTimer}>Sync</button>
+      <button onClick={() => dispatch(setTimer(5))}>5 Sec</button>
+      <button
+        onClick={() => {
+          dispatch(setTimer(5))
+          dispatch(setTimerEnabled(true))
+          syncTimer()
+        }}
+      >
+        Sync and 5 sec
+      </button>
     </>
   )
 }
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index b0efdcb6..4d1600ec 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,36 +1,37 @@
 import io from 'socket.io-client'
+import { setTimer, setTimerEnabled } from './actions/competition'
 import { setCurrentSlideByOrder } from './actions/presentation'
 import store from './store'
 
-let socket: SocketIOClient.Socket
-
 interface SyncSlideInterface {
   slide: number
 }
 
-// interface TimerInterface {
-//   value: number
-//   enabled: boolean
-// }
+interface TimerInterface {
+  value: number
+  enabled: boolean
+}
+
+interface SyncTimerInterface {
+  timer: TimerInterface
+}
 
-// interface SyncTimerInterface {
-//   timer: TimerInterface
-// }
+let socket: SocketIOClient.Socket
 
 export const connect = () => {
   if (!socket) {
     socket = io('localhost:5000')
-    socket.emit('join_competition', { code: store.getState().presentation.code })
+    socket.emit('join_presentation', { code: store.getState().presentation.code })
   }
 
   socket.on('sync_slide', (data: SyncSlideInterface) => {
     setCurrentSlideByOrder(data.slide)(store.dispatch)
   })
 
-  // socket.on('sync_timer', (data: SyncTimerInterface) => {
-  //   store.dispatch(setTimerEnabled(data.timer.enabled))
-  //   store.dispatch(setTimer(data.timer.value))
-  // })
+  socket.on('sync_timer', (data: SyncTimerInterface) => {
+    store.dispatch(setTimerEnabled(data.timer.enabled))
+    store.dispatch(setTimer(data.timer.value))
+  })
 }
 
 export const syncSlide = () => {
@@ -40,9 +41,9 @@ export const syncSlide = () => {
   })
 }
 
-// export const syncTimerSocket = () => {
-//   socket.emit('sync_timer', {
-//     competitionID: store.getState().competition.competitionID,
-//     timer: store.getState().competition.timer,
-//   })
-// }
+export const syncTimer = () => {
+  socket.emit('sync_timer', {
+    code: store.getState().presentation.code,
+    timer: store.getState().competition.timer,
+  })
+}
diff --git a/server/app/sockets.py b/server/app/sockets.py
index d06ae769..830c2489 100644
--- a/server/app/sockets.py
+++ b/server/app/sockets.py
@@ -3,6 +3,20 @@ from flask_socketio import SocketIO, emit, join_room
 
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
+# competitions = {
+#     "code": {
+#         "clients": {
+#             "id": {
+#                 "type": "judge",
+#             },
+#             "id2": {},
+#         },
+#         "current_slide": 2,
+#         "timer": {"enabled": True, "start_value": 100, "value": 10},
+#     },
+#     "code1": {},
+# }
+
 
 @sio.on("connect")
 def connect():
@@ -14,11 +28,16 @@ def disconnect():
     print(f"[Disconnected]: {request.sid}")
 
 
-@sio.on("join_competition")
-def join_competition(data):
+@sio.on("join_presentation")
+def join_presentation(data):
     code = data["code"]
+    # client_type = data["type"]
+
+    # if client_type == "judge":
+    #     judges.append(request.sid)
+
     join_room(data["code"])
-    print(f"[Join room]: {request.sid} -> {code}")
+    print(f"[Join presentation]: {request.sid} -> {code}")
 
 
 @sio.on("sync_slide")
@@ -31,7 +50,7 @@ def sync_slide(data):
 
 @sio.on("sync_timer")
 def sync_timer(data):
-    competitionID = data["competitionID"]
+    code = data["code"]
     timer = data["timer"]
-    emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False)
-    print(f"[Sync timer]: {competitionID=} {timer=}")
+    emit("sync_timer", {"timer": timer}, room=code, include_self=False)
+    print(f"[Sync timer]: {code=} {timer=}")
-- 
GitLab


From 6a0b929a8a239ef87c85150f71e3e463f18e87d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 00:03:23 +0200
Subject: [PATCH 10/20] Add presentations in backend

---
 client/package-lock.json                      |  12 +--
 client/package.json                           |   2 +-
 client/src/pages/views/JudgeViewPage.tsx      |   4 -
 client/src/pages/views/PresenterViewPage.tsx  |  20 ++--
 .../src/pages/views/components/SocketTest.tsx |  79 ++++++++++++++
 client/src/sockets.ts                         |  47 +++++---
 server/app/__init__.py                        |   2 +-
 server/app/core/sockets.py                    | 100 +++++++++++++++---
 server/app/sockets.py                         |  56 ----------
 server/populate.py                            |   2 +-
 10 files changed, 214 insertions(+), 110 deletions(-)
 create mode 100644 client/src/pages/views/components/SocketTest.tsx
 delete mode 100644 server/app/sockets.py

diff --git a/client/package-lock.json b/client/package-lock.json
index 52dd6723..667b7585 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -6082,9 +6082,9 @@
       }
     },
     "engine.io-client": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.0.tgz",
-      "integrity": "sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz",
+      "integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==",
       "requires": {
         "base64-arraybuffer": "0.1.4",
         "component-emitter": "~1.3.0",
@@ -15263,9 +15263,9 @@
       }
     },
     "socket.io-client": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.0.tgz",
-      "integrity": "sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz",
+      "integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==",
       "requires": {
         "@types/component-emitter": "^1.2.10",
         "backo2": "~1.0.2",
diff --git a/client/package.json b/client/package.json
index 067c9846..7c354ece 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,9 +31,9 @@
     "react-scripts": "4.0.2",
     "redux": "^4.0.5",
     "redux-devtools-extension": "^2.13.8",
-    "socket.io-client": "^4.0.0",
     "redux-mock-store": "^1.5.4",
     "redux-thunk": "^2.3.0",
+    "socket.io-client": "^4.0.1",
     "styled-components": "^5.2.1",
     "typescript": "^4.1.3",
     "web-vitals": "^1.1.0",
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index c6601532..12e866a0 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -10,11 +10,9 @@ import {
 } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
-import { connect } from '../../sockets'
 import { SlideListItem } from '../presentationEditor/styled'
 import JudgeScoreDisplay from './components/JudgeScoreDisplay'
 import SlideDisplay from './components/SlideDisplay'
-import Timer from './components/Timer'
 import {
   Content,
   JudgeAnswersLabel,
@@ -49,7 +47,6 @@ const JudgeViewPage: React.FC = () => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
     dispatch(setPresentationCode(code))
-    connect()
   }, [])
   const teams = useAppSelector((state) => state.presentation.teams)
   const slides = useAppSelector((state) => state.presentation.competition.slides)
@@ -74,7 +71,6 @@ const JudgeViewPage: React.FC = () => {
         anchor="left"
       >
         <div className={classes.toolbar} />
-        <Timer></Timer>
         <List>
           {slides.map((slide, index) => (
             <SlideListItem
diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx
index ee569110..0958bc51 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -2,17 +2,11 @@ import { List, ListItem, Popover } from '@material-ui/core'
 import ChevronRightIcon from '@material-ui/icons/ChevronRight'
 import React, { useEffect } from 'react'
 import { useHistory, useParams } from 'react-router-dom'
-import {
-  getPresentationCompetition,
-  getPresentationTeams,
-  setCurrentSlideNext,
-  setCurrentSlidePrevious,
-  setPresentationCode,
-} from '../../actions/presentation'
+import { getPresentationCompetition, getPresentationTeams, setPresentationCode } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
-import { connect, syncSlide } from '../../sockets'
 import SlideDisplay from './components/SlideDisplay'
+import SocketTest from './components/SocketTest'
 import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled'
 
 const PresenterViewPage: React.FC = () => {
@@ -25,7 +19,6 @@ const PresenterViewPage: React.FC = () => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
     dispatch(setPresentationCode(code))
-    connect()
   }, [])
   const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget)
@@ -34,12 +27,12 @@ const PresenterViewPage: React.FC = () => {
     setAnchorEl(null)
   }
   const handleNextSlidePressed = () => {
-    dispatch(setCurrentSlideNext())
-    syncSlide()
+    // dispatch(setCurrentSlideNext())
+    // syncSlide()
   }
   const handlePreviousSlidePressed = () => {
-    dispatch(setCurrentSlidePrevious())
-    syncSlide()
+    // dispatch(setCurrentSlidePrevious())
+    // syncSlide()
   }
 
   return (
@@ -57,6 +50,7 @@ const PresenterViewPage: React.FC = () => {
         <PresenterButton onClick={handlePreviousSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
+        <SocketTest></SocketTest>
         <PresenterButton onClick={handleNextSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
new file mode 100644
index 00000000..9e7e5379
--- /dev/null
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect } from 'react'
+import { connect } from 'react-redux'
+import { setTimerEnabled, tickTimer } from '../../../actions/competition'
+import { getPresentationCompetition, getPresentationTeams } from '../../../actions/presentation'
+import { useAppDispatch } from '../../../hooks'
+import {
+  socket_connect,
+  socket_end_presentation,
+  socket_join_presentation,
+  socket_set_slide,
+  socket_start_presentation,
+} from '../../../sockets'
+
+const mapStateToProps = (state: any) => {
+  return {
+    timer: state.competition.timer,
+    slide_order: state.presentation.slide.order,
+  }
+}
+
+const mapDispatchToProps = (dispatch: any) => {
+  return {
+    tickTimer: () => dispatch(tickTimer(1)),
+  }
+}
+
+let timerIntervalId: NodeJS.Timeout
+
+const SocketTest: React.FC = (props: any) => {
+  const dispatch = useAppDispatch()
+
+  useEffect(() => {
+    socket_connect()
+    dispatch(getPresentationCompetition('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
+    dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
+  }, [])
+
+  useEffect(() => {
+    if (props.timer.enabled) {
+      timerIntervalId = setInterval(() => {
+        dispatch(tickTimer(1))
+      }, 1000)
+    } else {
+      clearInterval(timerIntervalId)
+    }
+  }, [props.timer.enabled])
+
+  useEffect(() => {
+    if (props.timer.value <= 0) {
+      clearInterval(timerIntervalId)
+      dispatch(setTimerEnabled(false))
+    }
+  }, [props.timer.value])
+
+  return (
+    <>
+      <button onClick={socket_start_presentation}>Start presentation</button>
+      <button onClick={socket_join_presentation}>Join presentation</button>
+      <button onClick={socket_end_presentation}>End presentation</button>
+      <button onClick={() => socket_set_slide(1)}>Set slide</button> {/* TODO: Send next slide */}
+      {/* <div>Current slide: {props.slide_order}</div> */}
+      {/* <div>Timer: {props.timer.value}</div>
+      <div>Enabled: {props.timer.enabled.toString()}</div>
+      <button onClick={syncTimer}>Sync</button>
+      <button onClick={() => dispatch(setTimer(5))}>5 Sec</button>
+      <button
+        onClick={() => {
+          dispatch(setTimer(5))
+          dispatch(setTimerEnabled(true))
+          syncTimer()
+        }}
+      >
+        Sync and 5 sec
+      </button> */}
+    </>
+  )
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SocketTest)
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 4d1600ec..f9c2ee48 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -3,8 +3,8 @@ import { setTimer, setTimerEnabled } from './actions/competition'
 import { setCurrentSlideByOrder } from './actions/presentation'
 import store from './store'
 
-interface SyncSlideInterface {
-  slide: number
+interface SetSlideInterface {
+  slide_order: number
 }
 
 interface TimerInterface {
@@ -18,29 +18,46 @@ interface SyncTimerInterface {
 
 let socket: SocketIOClient.Socket
 
-export const connect = () => {
+export const socket_connect = () => {
   if (!socket) {
     socket = io('localhost:5000')
-    socket.emit('join_presentation', { code: store.getState().presentation.code })
+
+    socket.on('set_slide', (data: SetSlideInterface) => {
+      setCurrentSlideByOrder(data.slide_order)(store.dispatch)
+    })
+
+    socket.on('sync_timer', (data: SyncTimerInterface) => {
+      store.dispatch(setTimerEnabled(data.timer.enabled))
+      store.dispatch(setTimer(data.timer.value))
+    })
+
+    socket.on('end_presentation', () => {
+      socket.disconnect()
+    })
   }
+}
 
-  socket.on('sync_slide', (data: SyncSlideInterface) => {
-    setCurrentSlideByOrder(data.slide)(store.dispatch)
-  })
+export const socket_join_presentation = () => {
+  socket.emit('join_presentation', { code: '8WZ12G' }) // TODO: Send code gotten from auth/login/<code> api call
+}
 
-  socket.on('sync_timer', (data: SyncTimerInterface) => {
-    store.dispatch(setTimerEnabled(data.timer.enabled))
-    store.dispatch(setTimer(data.timer.value))
-  })
+export const socket_start_presentation = () => {
+  socket.emit('start_presentation', { competition_id: 1 }) // TODO: Send current competition id
 }
 
-export const syncSlide = () => {
-  socket.emit('sync_slide', {
-    code: store.getState().presentation.code,
-    slide: store.getState().presentation.slide.order,
+export const socket_end_presentation = () => {
+  socket.emit('end_presentation', { competition_id: 1 }) // TODO: Send current competition id
+}
+
+export const socket_set_slide = (slide_order: number) => {
+  socket.emit('set_slide', {
+    competition_id: 1, // TODO: Send current competition id
+    slide_order: slide_order,
   })
 }
 
+// Add socket_next_slide and socket_prev_slide
+
 export const syncTimer = () => {
   socket.emit('sync_timer', {
     code: store.getState().presentation.code,
diff --git a/server/app/__init__.py b/server/app/__init__.py
index 54c2470b..8eebb113 100644
--- a/server/app/__init__.py
+++ b/server/app/__init__.py
@@ -19,7 +19,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"):
         ma.init_app(app)
         configure_uploads(app, (MediaDTO.image_set,))
 
-        from .sockets import sio
+        from app.core.sockets import sio
 
         sio.init_app(app)
 
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index d9407a69..903f49c6 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -1,8 +1,16 @@
+import app.database.controller as dbc
+from app.core import db
+from app.database.models import ViewType
 from flask.globals import request
 from flask_socketio import SocketIO, emit, join_room
 
+# Presentation is an active competition
+
+
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
+presentations = {}
+
 
 @sio.on("connect")
 def connect():
@@ -14,23 +22,89 @@ def disconnect():
     print(f"[Disconnected]: {request.sid}")
 
 
-@sio.on("join_competition")
-def join_competition(data):
-    competitionID = data["competitionID"]
-    join_room(data["competitionID"])
-    print(f"[Join room]: {request.sid} -> {competitionID}")
+@sio.on("start_presentation")
+def start_presentation(data):
+    competition_id = data["competition_id"]
+
+    # TODO: Do proper error handling
+    if competition_id in presentations:
+        print("THAT PRESENTATION IS ALREADY ACTIVE")
+        return
+
+    presentations[competition_id] = {
+        "clients": {request.sid: {"view_type": "Operator"}},
+        "slide": None,
+        "timer": {"enabled": False, "start_value": None, "value": None},
+    }
+
+    join_room(competition_id)
+    print(f"[start_presentation]: {request.sid} -> {competition_id}.")
+
+
+@sio.on("end_presentation")
+def end_presentation(data):
+    competition_id = data["competition_id"]
+
+    if competition_id not in presentations:
+        print("NO PRESENTATION WITH THAT NAME EXISTS")
+        return
+
+    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
+        print("YOU DONT HAVE ACCESS TO DO THAT")
+        return
+
+    emit("end_presentation", room=competition_id, include_self=True)
+
+
+@sio.on("join_presentation")
+def join_presentation(data):
+    team_view_id = 1
+    code = data["code"]
+    item_code = dbc.get.code_by_code(code)
+
+    # TODO: Do proper error handling
+    if not item_code:
+        print("CODE DOES NOT EXIST")
+        return
+
+    if item_code.view_type_id == team_view_id:
+        pass  # TODO: Get competition_id from team instead
+    else:
+        competition_id = item_code.pointer
+
+    if competition_id not in presentations:
+        print("THAT COMPETITION IS CURRENTLY NOT ACTIVE")
+        return
+
+    if request.sid in presentations[competition_id]["clients"]:
+        print("CLIENT ALREADY IN COMPETITION")
+        return
+
+    # TODO: Write function in database controller to do this
+    view_type_name = db.session.query(ViewType).filter(ViewType.id == item_code.view_type_id).one().name
+
+    presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name}
+    join_room(competition_id)
+
+    print(f"[Join presentation]: {request.sid} -> {competition_id}. {view_type_name=}")
+
+
+@sio.on("set_slide")
+def set_slide(data):
+    competition_id = data["competition_id"]
+    slide_order = data["slide_order"]
 
+    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
+        print("YOU DONT HAVE ACCESS TO DO THAT")
+        return
 
-@sio.on("sync_slide")
-def sync_slide(data):
-    slide, competitionID = data["slide"], data["competitionID"]
-    emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False)
-    print(f"[Sync slide]: {slide} -> {competitionID}")
+    emit("set_slide", {"slide": slide_order}, room=competition_id, include_self=True)
+    print(f"[Set slide]: {slide_order} -> {competition_id}")
 
 
 @sio.on("sync_timer")
 def sync_timer(data):
-    competitionID = data["competitionID"]
+    code = data["code"]
     timer = data["timer"]
-    emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False)
-    print(f"[Sync timer]: {competitionID=} {timer=}")
+    emit("sync_timer", {"timer": timer}, room=code, include_self=False)
+    print(f"[Sync timer]: {code=} {timer=}")
diff --git a/server/app/sockets.py b/server/app/sockets.py
deleted file mode 100644
index 830c2489..00000000
--- a/server/app/sockets.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from flask.globals import request
-from flask_socketio import SocketIO, emit, join_room
-
-sio = SocketIO(cors_allowed_origins="http://localhost:3000")
-
-# competitions = {
-#     "code": {
-#         "clients": {
-#             "id": {
-#                 "type": "judge",
-#             },
-#             "id2": {},
-#         },
-#         "current_slide": 2,
-#         "timer": {"enabled": True, "start_value": 100, "value": 10},
-#     },
-#     "code1": {},
-# }
-
-
-@sio.on("connect")
-def connect():
-    print(f"[Connected]: {request.sid}")
-
-
-@sio.on("disconnect")
-def disconnect():
-    print(f"[Disconnected]: {request.sid}")
-
-
-@sio.on("join_presentation")
-def join_presentation(data):
-    code = data["code"]
-    # client_type = data["type"]
-
-    # if client_type == "judge":
-    #     judges.append(request.sid)
-
-    join_room(data["code"])
-    print(f"[Join presentation]: {request.sid} -> {code}")
-
-
-@sio.on("sync_slide")
-def sync_slide(data):
-    code = data["code"]
-    slide = data["slide"]
-    emit("sync_slide", {"slide": slide}, room=code, include_self=False)
-    print(f"[Sync slide]: {slide} -> {code}")
-
-
-@sio.on("sync_timer")
-def sync_timer(data):
-    code = data["code"]
-    timer = data["timer"]
-    emit("sync_timer", {"timer": timer}, room=code, include_self=False)
-    print(f"[Sync timer]: {code=} {timer=}")
diff --git a/server/populate.py b/server/populate.py
index bebcaa8b..5a9988f7 100644
--- a/server/populate.py
+++ b/server/populate.py
@@ -7,7 +7,7 @@ def _add_items():
     media_types = ["Image", "Video"]
     question_types = ["Boolean", "Multiple", "Text"]
     component_types = ["Text", "Image"]
-    view_types = ["Team", "Judge", "Audience"]
+    view_types = ["Team", "Judge", "Audience", "Operator"]
 
     roles = ["Admin", "Editor"]
     cities = ["Linköping", "Stockholm", "Norrköping", "Örkelljunga"]
-- 
GitLab


From 43874499a4b3ef78d5e7b995e82abd774f862336 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 00:25:51 +0200
Subject: [PATCH 11/20] Remove competition and fix sync slide

---
 client/src/actions/competition.ts             | 37 ------------
 client/src/actions/types.ts                   |  4 --
 .../src/pages/views/components/SocketTest.tsx | 31 ++--------
 client/src/pages/views/components/Timer.tsx   | 60 -------------------
 client/src/reducers/allReducers.ts            |  2 -
 client/src/reducers/competitionReducer.ts     | 54 -----------------
 client/src/reducers/presentationReducer.ts    |  1 +
 client/src/sockets.ts                         | 41 +++++++------
 server/app/core/sockets.py                    | 16 ++---
 9 files changed, 39 insertions(+), 207 deletions(-)
 delete mode 100644 client/src/actions/competition.ts
 delete mode 100644 client/src/pages/views/components/Timer.tsx
 delete mode 100644 client/src/reducers/competitionReducer.ts

diff --git a/client/src/actions/competition.ts b/client/src/actions/competition.ts
deleted file mode 100644
index ef18927b..00000000
--- a/client/src/actions/competition.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import store from '../store'
-import types from './types'
-
-export const setSlide = (slide: number) => ({
-  type: types.SET_SLIDE,
-  payload: slide,
-})
-
-export const nextSlide = () => ({
-  type: types.SET_SLIDE,
-  payload: store.getState().competition.slide + 1,
-})
-
-export const prevSlide = () => ({
-  type: types.SET_SLIDE,
-  payload: store.getState().competition.slide - 1,
-})
-
-export const setCompetitionID = (competitionID: string) => ({
-  type: types.SET_COMPEITION_ID,
-  payload: competitionID,
-})
-
-export const setTimer = (timer: number) => ({
-  type: types.SET_TIMER,
-  payload: timer,
-})
-
-export const tickTimer = (timestep: number) => ({
-  type: types.SET_TIMER,
-  payload: store.getState().competition.timer.value - timestep,
-})
-
-export const setTimerEnabled = (enabled: boolean) => ({
-  type: types.SET_TIMER_ENABLED,
-  payload: enabled,
-})
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index fe41c262..9ab39b4d 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -26,9 +26,5 @@ export default {
   SET_CITIES: 'SET_CITIES',
   SET_CITIES_TOTAL: 'SET_CITIES_TOTAL',
   SET_CITIES_COUNT: 'SET_CITIES_COUNT',
-  SET_SLIDE: 'SET_SLIDE',
-  SET_COMPEITION_ID: 'SET_COMPETITION_ID',
-  SET_TIMER: 'SET_TIMER',
-  SET_TIMER_ENABLED: 'SET_TIMER_ENABLED',
   SET_TYPES: 'SET_TYPES',
 }
diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
index 9e7e5379..a03c2b9c 100644
--- a/client/src/pages/views/components/SocketTest.tsx
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -1,31 +1,28 @@
 import React, { useEffect } from 'react'
 import { connect } from 'react-redux'
-import { setTimerEnabled, tickTimer } from '../../../actions/competition'
 import { getPresentationCompetition, getPresentationTeams } from '../../../actions/presentation'
 import { useAppDispatch } from '../../../hooks'
 import {
   socket_connect,
   socket_end_presentation,
   socket_join_presentation,
-  socket_set_slide,
+  socket_set_slide_next,
+  socket_set_slide_prev,
   socket_start_presentation,
 } from '../../../sockets'
 
 const mapStateToProps = (state: any) => {
   return {
-    timer: state.competition.timer,
     slide_order: state.presentation.slide.order,
   }
 }
 
 const mapDispatchToProps = (dispatch: any) => {
   return {
-    tickTimer: () => dispatch(tickTimer(1)),
+    // tickTimer: () => dispatch(tickTimer(1)),
   }
 }
 
-let timerIntervalId: NodeJS.Timeout
-
 const SocketTest: React.FC = (props: any) => {
   const dispatch = useAppDispatch()
 
@@ -35,30 +32,14 @@ const SocketTest: React.FC = (props: any) => {
     dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
   }, [])
 
-  useEffect(() => {
-    if (props.timer.enabled) {
-      timerIntervalId = setInterval(() => {
-        dispatch(tickTimer(1))
-      }, 1000)
-    } else {
-      clearInterval(timerIntervalId)
-    }
-  }, [props.timer.enabled])
-
-  useEffect(() => {
-    if (props.timer.value <= 0) {
-      clearInterval(timerIntervalId)
-      dispatch(setTimerEnabled(false))
-    }
-  }, [props.timer.value])
-
   return (
     <>
       <button onClick={socket_start_presentation}>Start presentation</button>
       <button onClick={socket_join_presentation}>Join presentation</button>
       <button onClick={socket_end_presentation}>End presentation</button>
-      <button onClick={() => socket_set_slide(1)}>Set slide</button> {/* TODO: Send next slide */}
-      {/* <div>Current slide: {props.slide_order}</div> */}
+      <button onClick={socket_set_slide_next}>Next slide</button>
+      <button onClick={socket_set_slide_prev}>Prev slide</button>
+      <div>Current slide: {props.slide_order}</div>
       {/* <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
       <button onClick={syncTimer}>Sync</button>
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
deleted file mode 100644
index 4fe85973..00000000
--- a/client/src/pages/views/components/Timer.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React, { useEffect } from 'react'
-import { connect } from 'react-redux'
-import { setTimer, setTimerEnabled, tickTimer } from '../../../actions/competition'
-import { useAppDispatch } from '../../../hooks'
-import { syncTimer } from '../../../sockets'
-
-const mapStateToProps = (state: any) => {
-  return {
-    timer: state.competition.timer,
-  }
-}
-
-const mapDispatchToProps = (dispatch: any) => {
-  return {
-    tickTimer: () => dispatch(tickTimer(1)),
-  }
-}
-
-let timerIntervalId: NodeJS.Timeout
-
-const Timer: React.FC = (props: any) => {
-  const dispatch = useAppDispatch()
-
-  useEffect(() => {
-    if (props.timer.enabled) {
-      timerIntervalId = setInterval(() => {
-        dispatch(tickTimer(1))
-      }, 1000)
-    } else {
-      clearInterval(timerIntervalId)
-    }
-  }, [props.timer.enabled])
-
-  useEffect(() => {
-    if (props.timer.value <= 0) {
-      clearInterval(timerIntervalId)
-      dispatch(setTimerEnabled(false))
-    }
-  }, [props.timer.value])
-
-  return (
-    <>
-      <div>Timer: {props.timer.value}</div>
-      <div>Enabled: {props.timer.enabled.toString()}</div>
-      <button onClick={syncTimer}>Sync</button>
-      <button onClick={() => dispatch(setTimer(5))}>5 Sec</button>
-      <button
-        onClick={() => {
-          dispatch(setTimer(5))
-          dispatch(setTimerEnabled(true))
-          syncTimer()
-        }}
-      >
-        Sync and 5 sec
-      </button>
-    </>
-  )
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Timer)
diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts
index 7f98d8eb..2aee4697 100644
--- a/client/src/reducers/allReducers.ts
+++ b/client/src/reducers/allReducers.ts
@@ -2,7 +2,6 @@
 
 import { combineReducers } from 'redux'
 import citiesReducer from './citiesReducer'
-import competitionReducer from './competitionReducer'
 import competitionsReducer from './competitionsReducer'
 import editorReducer from './editorReducer'
 import presentationReducer from './presentationReducer'
@@ -17,7 +16,6 @@ const allReducers = combineReducers({
   user: userReducer,
   UI: uiReducer,
   competitions: competitionsReducer,
-  competition: competitionReducer,
   cities: citiesReducer,
   editor: editorReducer,
   presentation: presentationReducer,
diff --git a/client/src/reducers/competitionReducer.ts b/client/src/reducers/competitionReducer.ts
deleted file mode 100644
index 431ee504..00000000
--- a/client/src/reducers/competitionReducer.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { AnyAction } from 'redux'
-import Types from '../actions/types'
-
-interface TimerState {
-  value: number
-  enabled: boolean
-}
-
-interface CompetitionState {
-  competitionID: string | null
-  slide: number
-  timer: TimerState
-  isLocked: boolean
-}
-
-const initialState: CompetitionState = {
-  competitionID: null,
-  slide: 0,
-  timer: { value: 0, enabled: false },
-  isLocked: false,
-}
-
-export default function (state = initialState, action: AnyAction) {
-  switch (action.type) {
-    case Types.SET_SLIDE:
-      return {
-        ...state,
-        slide: action.payload,
-      }
-    case Types.SET_COMPEITION_ID:
-      return {
-        ...state,
-        competitionID: action.payload,
-      }
-    case Types.SET_TIMER:
-      return {
-        ...state,
-        timer: {
-          ...state.timer,
-          value: action.payload,
-        },
-      }
-    case Types.SET_TIMER_ENABLED:
-      return {
-        ...state,
-        timer: {
-          ...state.timer,
-          enabled: action.payload,
-        },
-      }
-    default:
-      return state
-  }
-}
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 39d22a5e..e04f5d69 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -71,6 +71,7 @@ export default function (state = initialState, action: AnyAction) {
       }
       return state
     case Types.SET_PRESENTATION_SLIDE_BY_ORDER:
+      console.log(action.payload)
       if (0 <= action.payload && action.payload < state.competition.slides.length)
         return {
           ...state,
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index f9c2ee48..5395fdc3 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,5 +1,4 @@
 import io from 'socket.io-client'
-import { setTimer, setTimerEnabled } from './actions/competition'
 import { setCurrentSlideByOrder } from './actions/presentation'
 import store from './store'
 
@@ -26,10 +25,10 @@ export const socket_connect = () => {
       setCurrentSlideByOrder(data.slide_order)(store.dispatch)
     })
 
-    socket.on('sync_timer', (data: SyncTimerInterface) => {
-      store.dispatch(setTimerEnabled(data.timer.enabled))
-      store.dispatch(setTimer(data.timer.value))
-    })
+    // socket.on('sync_timer', (data: SyncTimerInterface) => {
+    //   // store.dispatch(setTimerEnabled(data.timer.enabled))
+    //   // store.dispatch(setTimer(data.timer.value))
+    // })
 
     socket.on('end_presentation', () => {
       socket.disconnect()
@@ -37,30 +36,36 @@ export const socket_connect = () => {
   }
 }
 
+export const socket_start_presentation = () => {
+  socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
+}
+
 export const socket_join_presentation = () => {
   socket.emit('join_presentation', { code: '8WZ12G' }) // TODO: Send code gotten from auth/login/<code> api call
 }
 
-export const socket_start_presentation = () => {
-  socket.emit('start_presentation', { competition_id: 1 }) // TODO: Send current competition id
+export const socket_end_presentation = () => {
+  socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
 }
 
-export const socket_end_presentation = () => {
-  socket.emit('end_presentation', { competition_id: 1 }) // TODO: Send current competition id
+export const socket_set_slide_next = () => {
+  socket_set_slide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists
+}
+
+export const socket_set_slide_prev = () => {
+  socket_set_slide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists
 }
 
 export const socket_set_slide = (slide_order: number) => {
   socket.emit('set_slide', {
-    competition_id: 1, // TODO: Send current competition id
+    competition_id: store.getState().presentation.competition.id,
     slide_order: slide_order,
   })
 }
 
-// Add socket_next_slide and socket_prev_slide
-
-export const syncTimer = () => {
-  socket.emit('sync_timer', {
-    code: store.getState().presentation.code,
-    timer: store.getState().competition.timer,
-  })
-}
+// export const syncTimer = () => {
+//   socket.emit('sync_timer', {
+//     // code: store.getState().presentation.code,
+//     // timer: store.getState().competition.timer,
+//   })
+// }
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 903f49c6..de7078ee 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -53,6 +53,8 @@ def end_presentation(data):
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
 
+    del presentations[competition_id]
+
     emit("end_presentation", room=competition_id, include_self=True)
 
 
@@ -98,13 +100,13 @@ def set_slide(data):
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
 
-    emit("set_slide", {"slide": slide_order}, room=competition_id, include_self=True)
+    emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
     print(f"[Set slide]: {slide_order} -> {competition_id}")
 
 
-@sio.on("sync_timer")
-def sync_timer(data):
-    code = data["code"]
-    timer = data["timer"]
-    emit("sync_timer", {"timer": timer}, room=code, include_self=False)
-    print(f"[Sync timer]: {code=} {timer=}")
+# @sio.on("sync_timer")
+# def sync_timer(data):
+#     code = data["code"]
+#     timer = data["timer"]
+#     emit("sync_timer", {"timer": timer}, room=code, include_self=False)
+#     print(f"[Sync timer]: {code=} {timer=}")
-- 
GitLab


From b23c519c8109132166382a06f34a9cea07aa86db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 20:50:21 +0200
Subject: [PATCH 12/20] Remove self from competition if disconnect

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

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index de7078ee..c42e92df 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -19,6 +19,16 @@ def connect():
 
 @sio.on("disconnect")
 def disconnect():
+    for competition_id, presentation in presentations.items():
+        if request.sid in presentation["clients"]:
+            del presentation["clients"][request.sid]
+            break
+
+    if presentations and not presentations[competition_id]["clients"]:
+        del presentations[competition_id]
+
+    print(f"{presentations=}")
+
     print(f"[Disconnected]: {request.sid}")
 
 
@@ -37,6 +47,8 @@ def start_presentation(data):
         "timer": {"enabled": False, "start_value": None, "value": None},
     }
 
+    print(f"{presentations=}")
+
     join_room(competition_id)
     print(f"[start_presentation]: {request.sid} -> {competition_id}.")
 
@@ -55,6 +67,8 @@ def end_presentation(data):
 
     del presentations[competition_id]
 
+    print(f"{presentations=}")
+
     emit("end_presentation", room=competition_id, include_self=True)
 
 
@@ -88,6 +102,8 @@ def join_presentation(data):
     presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name}
     join_room(competition_id)
 
+    print(f"{presentations=}")
+
     print(f"[Join presentation]: {request.sid} -> {competition_id}. {view_type_name=}")
 
 
@@ -100,6 +116,10 @@ def set_slide(data):
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
 
+    presentations[competition_id]["slide"] = slide_order
+
+    print(f"{presentations=}")
+
     emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
     print(f"[Set slide]: {slide_order} -> {competition_id}")
 
-- 
GitLab


From c1c3548336669b218243c927f529bd088f3cc724 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 21:06:11 +0200
Subject: [PATCH 13/20] Prevent client from changing to non existent slide

---
 client/src/pages/views/components/SocketTest.tsx | 2 +-
 client/src/reducers/presentationReducer.ts       | 1 -
 client/src/sockets.ts                            | 5 +++++
 server/app/core/sockets.py                       | 8 +++++++-
 4 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
index a03c2b9c..d0ac015d 100644
--- a/client/src/pages/views/components/SocketTest.tsx
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -37,8 +37,8 @@ const SocketTest: React.FC = (props: any) => {
       <button onClick={socket_start_presentation}>Start presentation</button>
       <button onClick={socket_join_presentation}>Join presentation</button>
       <button onClick={socket_end_presentation}>End presentation</button>
-      <button onClick={socket_set_slide_next}>Next slide</button>
       <button onClick={socket_set_slide_prev}>Prev slide</button>
+      <button onClick={socket_set_slide_next}>Next slide</button>
       <div>Current slide: {props.slide_order}</div>
       {/* <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index e04f5d69..39d22a5e 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -71,7 +71,6 @@ export default function (state = initialState, action: AnyAction) {
       }
       return state
     case Types.SET_PRESENTATION_SLIDE_BY_ORDER:
-      console.log(action.payload)
       if (0 <= action.payload && action.payload < state.competition.slides.length)
         return {
           ...state,
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 5395fdc3..4fd2c189 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -57,6 +57,11 @@ export const socket_set_slide_prev = () => {
 }
 
 export const socket_set_slide = (slide_order: number) => {
+  if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
+    console.log('CANT CHANGE TO NON EXISTENT SLIDE')
+    return
+  }
+
   socket.emit('set_slide', {
     competition_id: store.getState().presentation.competition.id,
     slide_order: slide_order,
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index c42e92df..108fd20b 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -1,6 +1,6 @@
 import app.database.controller as dbc
 from app.core import db
-from app.database.models import ViewType
+from app.database.models import Competition, Slide, ViewType
 from flask.globals import request
 from flask_socketio import SocketIO, emit, join_room
 
@@ -116,6 +116,12 @@ def set_slide(data):
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
 
+    num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count()
+
+    if not (0 <= slide_order < num_slides):
+        print("CANT CHANGE TO NON EXISTENT SLIDE")
+        return
+
     presentations[competition_id]["slide"] = slide_order
 
     print(f"{presentations=}")
-- 
GitLab


From 340b9cf41a7a61dc5248e9a98f8ed5d69fd273cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 21:12:11 +0200
Subject: [PATCH 14/20] Rename socket functions in client

---
 .../src/pages/views/components/SocketTest.tsx | 20 ++++++-------
 client/src/sockets.ts                         | 28 +++++++++----------
 2 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
index d0ac015d..b0474bb8 100644
--- a/client/src/pages/views/components/SocketTest.tsx
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -3,12 +3,12 @@ import { connect } from 'react-redux'
 import { getPresentationCompetition, getPresentationTeams } from '../../../actions/presentation'
 import { useAppDispatch } from '../../../hooks'
 import {
+  socketEndPresentation,
+  socketJoinPresentation,
+  socketSetSlideNext,
+  socketSetSlidePrev,
+  socketStartPresentation,
   socket_connect,
-  socket_end_presentation,
-  socket_join_presentation,
-  socket_set_slide_next,
-  socket_set_slide_prev,
-  socket_start_presentation,
 } from '../../../sockets'
 
 const mapStateToProps = (state: any) => {
@@ -34,11 +34,11 @@ const SocketTest: React.FC = (props: any) => {
 
   return (
     <>
-      <button onClick={socket_start_presentation}>Start presentation</button>
-      <button onClick={socket_join_presentation}>Join presentation</button>
-      <button onClick={socket_end_presentation}>End presentation</button>
-      <button onClick={socket_set_slide_prev}>Prev slide</button>
-      <button onClick={socket_set_slide_next}>Next slide</button>
+      <button onClick={socketStartPresentation}>Start presentation</button>
+      <button onClick={socketJoinPresentation}>Join presentation</button>
+      <button onClick={socketEndPresentation}>End presentation</button>
+      <button onClick={socketSetSlidePrev}>Prev slide</button>
+      <button onClick={socketSetSlideNext}>Next slide</button>
       <div>Current slide: {props.slide_order}</div>
       {/* <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 4fd2c189..519ed6ed 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -36,27 +36,27 @@ export const socket_connect = () => {
   }
 }
 
-export const socket_start_presentation = () => {
+export const socketStartPresentation = () => {
   socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
 }
 
-export const socket_join_presentation = () => {
+export const socketJoinPresentation = () => {
   socket.emit('join_presentation', { code: '8WZ12G' }) // TODO: Send code gotten from auth/login/<code> api call
 }
 
-export const socket_end_presentation = () => {
+export const socketEndPresentation = () => {
   socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
 }
 
-export const socket_set_slide_next = () => {
-  socket_set_slide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists
+export const socketSetSlideNext = () => {
+  socketSetSlide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists
 }
 
-export const socket_set_slide_prev = () => {
-  socket_set_slide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists
+export const socketSetSlidePrev = () => {
+  socketSetSlide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists
 }
 
-export const socket_set_slide = (slide_order: number) => {
+export const socketSetSlide = (slide_order: number) => {
   if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
     console.log('CANT CHANGE TO NON EXISTENT SLIDE')
     return
@@ -68,9 +68,9 @@ export const socket_set_slide = (slide_order: number) => {
   })
 }
 
-// export const syncTimer = () => {
-//   socket.emit('sync_timer', {
-//     // code: store.getState().presentation.code,
-//     // timer: store.getState().competition.timer,
-//   })
-// }
+export const socket_set_timer = () => {
+  socket.emit('sync_timer', {
+    // code: store.getState().presentation.code,
+    // timer: store.getState().competition.timer,
+  })
+}
-- 
GitLab


From 26ea211b688ae7bdc92f5386c9d3eff38211cfca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 21:12:25 +0200
Subject: [PATCH 15/20] Check that competition exists when setting slide

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

diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 108fd20b..920f1207 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -112,6 +112,10 @@ def set_slide(data):
     competition_id = data["competition_id"]
     slide_order = data["slide_order"]
 
+    if competition_id not in presentations:
+        print("CANT SET SLIDE IN NON ACTIVE COMPETITION")
+        return
+
     if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
-- 
GitLab


From d1aa5e0cc8150cc40a8edda820d43f1844fba92b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 22:21:28 +0200
Subject: [PATCH 16/20] Add socketSetTimer

---
 client/src/actions/presentation.ts            |  5 ++
 client/src/actions/types.ts                   |  1 +
 client/src/interfaces/Timer.ts                |  4 ++
 client/src/pages/views/PresenterViewPage.tsx  |  2 +
 .../src/pages/views/components/SocketTest.tsx |  7 +-
 client/src/pages/views/components/Timer.tsx   | 65 +++++++++++++++++++
 client/src/reducers/presentationReducer.ts    | 12 ++++
 client/src/sockets.ts                         | 28 ++++----
 server/app/core/sockets.py                    | 22 +++++--
 server/populate.py                            |  4 +-
 10 files changed, 129 insertions(+), 21 deletions(-)
 create mode 100644 client/src/interfaces/Timer.ts
 create mode 100644 client/src/pages/views/components/Timer.tsx

diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index 70345f42..db6f2ec4 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -1,5 +1,6 @@
 import axios from 'axios'
 import { Slide } from '../interfaces/Slide'
+import { Timer } from '../interfaces/Timer'
 import { AppDispatch } from './../store'
 import Types from './types'
 
@@ -50,3 +51,7 @@ export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch)
 export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
 }
+
+export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => {
+  dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer })
+}
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index 9ab39b4d..848d41f0 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -23,6 +23,7 @@ export default {
   SET_PRESENTATION_SLIDE_BY_ORDER: 'SET_PRESENTATION_SLIDE_BY_ORDER',
   SET_PRESENTATION_TEAMS: 'SET_PRESENTATION_TEAMS',
   SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE',
+  SET_PRESENTATION_TIMER: 'SET_PRESENTATION_TIMER',
   SET_CITIES: 'SET_CITIES',
   SET_CITIES_TOTAL: 'SET_CITIES_TOTAL',
   SET_CITIES_COUNT: 'SET_CITIES_COUNT',
diff --git a/client/src/interfaces/Timer.ts b/client/src/interfaces/Timer.ts
new file mode 100644
index 00000000..49d1909e
--- /dev/null
+++ b/client/src/interfaces/Timer.ts
@@ -0,0 +1,4 @@
+export interface Timer {
+  enabled: boolean
+  value: number
+}
diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx
index 0958bc51..22a672ba 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -7,6 +7,7 @@ import { useAppDispatch, useAppSelector } from '../../hooks'
 import { ViewParams } from '../../interfaces/ViewParams'
 import SlideDisplay from './components/SlideDisplay'
 import SocketTest from './components/SocketTest'
+import Timer from './components/Timer'
 import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled'
 
 const PresenterViewPage: React.FC = () => {
@@ -51,6 +52,7 @@ const PresenterViewPage: React.FC = () => {
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
         <SocketTest></SocketTest>
+        <Timer></Timer>
         <PresenterButton onClick={handleNextSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
index b0474bb8..7647fa65 100644
--- a/client/src/pages/views/components/SocketTest.tsx
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -1,6 +1,5 @@
 import React, { useEffect } from 'react'
 import { connect } from 'react-redux'
-import { getPresentationCompetition, getPresentationTeams } from '../../../actions/presentation'
 import { useAppDispatch } from '../../../hooks'
 import {
   socketEndPresentation,
@@ -8,6 +7,7 @@ import {
   socketSetSlideNext,
   socketSetSlidePrev,
   socketStartPresentation,
+  socketStartTimer,
   socket_connect,
 } from '../../../sockets'
 
@@ -28,8 +28,8 @@ const SocketTest: React.FC = (props: any) => {
 
   useEffect(() => {
     socket_connect()
-    dispatch(getPresentationCompetition('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
-    dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
+    // dispatch(getPresentationCompetition('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
+    // dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call
   }, [])
 
   return (
@@ -39,6 +39,7 @@ const SocketTest: React.FC = (props: any) => {
       <button onClick={socketEndPresentation}>End presentation</button>
       <button onClick={socketSetSlidePrev}>Prev slide</button>
       <button onClick={socketSetSlideNext}>Next slide</button>
+      <button onClick={socketStartTimer}>Set timer</button>
       <div>Current slide: {props.slide_order}</div>
       {/* <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
new file mode 100644
index 00000000..2304aa2a
--- /dev/null
+++ b/client/src/pages/views/components/Timer.tsx
@@ -0,0 +1,65 @@
+import React, { useEffect } from 'react'
+import { connect } from 'react-redux'
+import { setPresentationTimer } from '../../../actions/presentation'
+import { useAppDispatch } from '../../../hooks'
+import store from '../../../store'
+
+const mapStateToProps = (state: any) => {
+  return {
+    timer: state.presentation.timer,
+    timer_start_value: state.presentation.slide.timer,
+  }
+}
+
+const mapDispatchToProps = (dispatch: any) => {
+  return {
+    // tickTimer: () => dispatch(tickTimer(1)),
+  }
+}
+
+let timerIntervalId: NodeJS.Timeout
+
+const Timer: React.FC = (props: any) => {
+  const dispatch = useAppDispatch()
+
+  useEffect(() => {
+    dispatch(setPresentationTimer({ enabled: false, value: store.getState().presentation.slide.timer }))
+  }, [props.timer_start_value])
+
+  // useEffect(() => {
+  //   if (props.timer.enabled) {
+  //     timerIntervalId = setInterval(() => {
+  //       dispatch(tickTimer(1))
+  //     }, 1000)
+  //   } else {
+  //     clearInterval(timerIntervalId)
+  //   }
+  // }, [props.timer.enabled])
+
+  // useEffect(() => {
+  //   if (props.timer.value <= 0) {
+  //     clearInterval(timerIntervalId)
+  //   }
+  // }, [props.timer.value])
+
+  return (
+    <>
+      <div>Timer: {props.timer.value}</div>
+      <div>Enabled: {props.timer.enabled.toString()}</div>
+
+      {/* <button onClick={syncTimer}>Sync</button>
+      <button onClick={() => dispatch(setTimer(5))}>5 Sec</button>
+      <button
+        onClick={() => {
+          dispatch(setTimer(5))
+          dispatch(setTimerEnabled(true))
+          syncTimer()
+        }}
+      >
+        Sync and 5 sec
+      </button> */}
+    </>
+  )
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Timer)
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 39d22a5e..46908033 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -1,5 +1,6 @@
 import { AnyAction } from 'redux'
 import Types from '../actions/types'
+import { Timer } from '../interfaces/Timer'
 import { RichCompetition } from './../interfaces/ApiRichModels'
 import { Slide } from './../interfaces/Slide'
 import { Team } from './../interfaces/Team'
@@ -9,6 +10,7 @@ interface PresentationState {
   slide: Slide
   teams: Team[]
   code: string
+  timer: Timer
 }
 
 const initialState: PresentationState = {
@@ -29,6 +31,10 @@ const initialState: PresentationState = {
   },
   teams: [],
   code: '',
+  timer: {
+    enabled: false,
+    value: 0,
+  },
 }
 
 export default function (state = initialState, action: AnyAction) {
@@ -77,6 +83,12 @@ export default function (state = initialState, action: AnyAction) {
           slide: state.competition.slides[action.payload],
         }
       return state
+    case Types.SET_PRESENTATION_TIMER:
+      console.log(action.payload)
+      return {
+        ...state,
+        timer: action.payload,
+      }
     default:
       return state
   }
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index 519ed6ed..e4e9b923 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,5 +1,6 @@
 import io from 'socket.io-client'
-import { setCurrentSlideByOrder } from './actions/presentation'
+import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
+import { Timer } from './interfaces/Timer'
 import store from './store'
 
 interface SetSlideInterface {
@@ -11,7 +12,7 @@ interface TimerInterface {
   enabled: boolean
 }
 
-interface SyncTimerInterface {
+interface SetTimerInterface {
   timer: TimerInterface
 }
 
@@ -25,10 +26,11 @@ export const socket_connect = () => {
       setCurrentSlideByOrder(data.slide_order)(store.dispatch)
     })
 
-    // socket.on('sync_timer', (data: SyncTimerInterface) => {
-    //   // store.dispatch(setTimerEnabled(data.timer.enabled))
-    //   // store.dispatch(setTimer(data.timer.value))
-    // })
+    socket.on('set_timer', (data: SetTimerInterface) => {
+      setPresentationTimer(data.timer)(store.dispatch)
+      // store.dispatch(setTimerEnabled(data.timer.enabled))
+      // store.dispatch(setTimer(data.timer.value))
+    })
 
     socket.on('end_presentation', () => {
       socket.disconnect()
@@ -41,7 +43,7 @@ export const socketStartPresentation = () => {
 }
 
 export const socketJoinPresentation = () => {
-  socket.emit('join_presentation', { code: '8WZ12G' }) // TODO: Send code gotten from auth/login/<code> api call
+  socket.emit('join_presentation', { code: 'OEM1V4' }) // TODO: Send code gotten from auth/login/<code> api call
 }
 
 export const socketEndPresentation = () => {
@@ -68,9 +70,13 @@ export const socketSetSlide = (slide_order: number) => {
   })
 }
 
-export const socket_set_timer = () => {
-  socket.emit('sync_timer', {
-    // code: store.getState().presentation.code,
-    // timer: store.getState().competition.timer,
+export const socketSetTimer = (timer: Timer) => {
+  socket.emit('set_timer', {
+    competition_id: store.getState().presentation.competition.id,
+    timer: timer,
   })
 }
+
+export const socketStartTimer = () => {
+  socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value })
+}
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 920f1207..93be095b 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -134,9 +134,19 @@ def set_slide(data):
     print(f"[Set slide]: {slide_order} -> {competition_id}")
 
 
-# @sio.on("sync_timer")
-# def sync_timer(data):
-#     code = data["code"]
-#     timer = data["timer"]
-#     emit("sync_timer", {"timer": timer}, room=code, include_self=False)
-#     print(f"[Sync timer]: {code=} {timer=}")
+@sio.on("set_timer")
+def sync_timer(data):
+    competition_id = data["competition_id"]
+    timer = data["timer"]
+
+    if competition_id not in presentations:
+        print("CANT SET TIMER IN NON EXISTENT COMPETITION")
+        return
+
+    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
+        print("YOU DONT HAVE ACCESS TO DO THAT")
+        return
+
+    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
+
+    print(f"[Set timer]: {timer=}, {competition_id=}")
diff --git a/server/populate.py b/server/populate.py
index 5a9988f7..7044db60 100644
--- a/server/populate.py
+++ b/server/populate.py
@@ -50,6 +50,8 @@ def _add_items():
 
     for item_comp in item_comps:
         for item_slide in item_comp.slides:
+            dbc.edit.slide(item_slide, timer=5)
+
             for i in range(3):
                 dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide)
 
@@ -59,7 +61,7 @@ def _add_items():
 
 
 if __name__ == "__main__":
-    app = create_app("configmodule.DevelopmentConfig")
+    app, _ = create_app("configmodule.DevelopmentConfig")
 
     with app.app_context():
         db.drop_all()
-- 
GitLab


From 6d65e8b866ae7fbf343e76a313b565879ccaa5e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 22:35:41 +0200
Subject: [PATCH 17/20] Timer now also counts down when enabled

---
 client/src/actions/presentation.ts            | 12 +++++-
 .../src/pages/views/components/SocketTest.tsx |  2 +-
 client/src/pages/views/components/Timer.tsx   | 38 +++++--------------
 client/src/reducers/presentationReducer.ts    |  4 +-
 server/app/core/sockets.py                    |  5 ++-
 5 files changed, 29 insertions(+), 32 deletions(-)

diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index db6f2ec4..60248e1f 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -1,7 +1,7 @@
 import axios from 'axios'
 import { Slide } from '../interfaces/Slide'
 import { Timer } from '../interfaces/Timer'
-import { AppDispatch } from './../store'
+import store, { AppDispatch } from './../store'
 import Types from './types'
 
 export const getPresentationCompetition = (id: string) => async (dispatch: AppDispatch) => {
@@ -55,3 +55,13 @@ export const setPresentationCode = (code: string) => (dispatch: AppDispatch) =>
 export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer })
 }
+
+export const setPresentationTimerDecrement = () => (dispatch: AppDispatch) => {
+  dispatch({
+    type: Types.SET_PRESENTATION_TIMER,
+    payload: {
+      enabled: store.getState().presentation.timer.enabled,
+      value: store.getState().presentation.timer.value - 1,
+    },
+  })
+}
diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
index 7647fa65..d99f5b2a 100644
--- a/client/src/pages/views/components/SocketTest.tsx
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -39,7 +39,7 @@ const SocketTest: React.FC = (props: any) => {
       <button onClick={socketEndPresentation}>End presentation</button>
       <button onClick={socketSetSlidePrev}>Prev slide</button>
       <button onClick={socketSetSlideNext}>Next slide</button>
-      <button onClick={socketStartTimer}>Set timer</button>
+      <button onClick={socketStartTimer}>Start timer</button>
       <div>Current slide: {props.slide_order}</div>
       {/* <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index 2304aa2a..b4401a69 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -1,6 +1,6 @@
 import React, { useEffect } from 'react'
 import { connect } from 'react-redux'
-import { setPresentationTimer } from '../../../actions/presentation'
+import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation'
 import { useAppDispatch } from '../../../hooks'
 import store from '../../../store'
 
@@ -26,38 +26,20 @@ const Timer: React.FC = (props: any) => {
     dispatch(setPresentationTimer({ enabled: false, value: store.getState().presentation.slide.timer }))
   }, [props.timer_start_value])
 
-  // useEffect(() => {
-  //   if (props.timer.enabled) {
-  //     timerIntervalId = setInterval(() => {
-  //       dispatch(tickTimer(1))
-  //     }, 1000)
-  //   } else {
-  //     clearInterval(timerIntervalId)
-  //   }
-  // }, [props.timer.enabled])
-
-  // useEffect(() => {
-  //   if (props.timer.value <= 0) {
-  //     clearInterval(timerIntervalId)
-  //   }
-  // }, [props.timer.value])
+  useEffect(() => {
+    if (props.timer.enabled) {
+      timerIntervalId = setInterval(() => {
+        dispatch(setPresentationTimerDecrement())
+      }, 1000)
+    } else {
+      clearInterval(timerIntervalId)
+    }
+  }, [props.timer.enabled])
 
   return (
     <>
       <div>Timer: {props.timer.value}</div>
       <div>Enabled: {props.timer.enabled.toString()}</div>
-
-      {/* <button onClick={syncTimer}>Sync</button>
-      <button onClick={() => dispatch(setTimer(5))}>5 Sec</button>
-      <button
-        onClick={() => {
-          dispatch(setTimer(5))
-          dispatch(setTimerEnabled(true))
-          syncTimer()
-        }}
-      >
-        Sync and 5 sec
-      </button> */}
     </>
   )
 }
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index 46908033..2b3c5eb1 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -84,7 +84,9 @@ export default function (state = initialState, action: AnyAction) {
         }
       return state
     case Types.SET_PRESENTATION_TIMER:
-      console.log(action.payload)
+      if (action.payload.value == 0) {
+        action.payload.enabled = false
+      }
       return {
         ...state,
         timer: action.payload,
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 93be095b..9c6137ca 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -147,6 +147,9 @@ def sync_timer(data):
         print("YOU DONT HAVE ACCESS TO DO THAT")
         return
 
-    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
+    # TODO: Save timer in presentation, maybe?
+
+    print(f"{presentations=}")
 
+    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
     print(f"[Set timer]: {timer=}, {competition_id=}")
-- 
GitLab


From 278a1713a1ecc46bd05d7b8fa72cd14773f16af5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 22:45:55 +0200
Subject: [PATCH 18/20] Get competition id from team when joining with a team
 code

---
 client/src/sockets.ts      |  2 --
 server/app/core/sockets.py | 11 ++++++-----
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index e4e9b923..874bfc46 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -28,8 +28,6 @@ export const socket_connect = () => {
 
     socket.on('set_timer', (data: SetTimerInterface) => {
       setPresentationTimer(data.timer)(store.dispatch)
-      // store.dispatch(setTimerEnabled(data.timer.enabled))
-      // store.dispatch(setTimer(data.timer.value))
     })
 
     socket.on('end_presentation', () => {
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index 9c6137ca..f4909531 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -1,6 +1,6 @@
 import app.database.controller as dbc
 from app.core import db
-from app.database.models import Competition, Slide, ViewType
+from app.database.models import Competition, Slide, Team, ViewType
 from flask.globals import request
 from flask_socketio import SocketIO, emit, join_room
 
@@ -83,10 +83,11 @@ def join_presentation(data):
         print("CODE DOES NOT EXIST")
         return
 
-    if item_code.view_type_id == team_view_id:
-        pass  # TODO: Get competition_id from team instead
-    else:
-        competition_id = item_code.pointer
+    competition_id = (
+        item_code.pointer
+        if item_code.view_type_id != team_view_id
+        else db.session.query(Team).filter(Team.id == item_code.pointer).one().competition_id
+    )
 
     if competition_id not in presentations:
         print("THAT COMPETITION IS CURRENTLY NOT ACTIVE")
-- 
GitLab


From b6d3752a15d0c10e97e3327ef0e521beb46393e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 23:00:35 +0200
Subject: [PATCH 19/20] Add all dependencies to requirements.txt

---
 server/requirements.txt | Bin 2308 -> 2448 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/server/requirements.txt b/server/requirements.txt
index c64222b754a34a97ff5c2f57bd017ac682dbd42c..bda47a88036c81470bc7a6b2112b2ecedd904ed7 100644
GIT binary patch
delta 180
zcmZn>njpNvj!D#+A)ld$p_suI2#p!^7z}{eaPmPW;mLMP(`17fQW;7a3K(*LsuCGe
zfV>p2N<)y!$@`fyH-|7EVYEyH8<D|~3sjT?R$R`Y3pBJ4$OefdGh_g@<}y@*HG`}$
oVlV-blLc9)O9n7x!gYb1VF8v0TCjOBYa=5oNV&;oP4?T209K(StpET3

delta 80
zcmbOr+#<BWj%jil)3nKY%pvM-3^@#m48;uD47v=V3<W@5K9HXR6tM+D0|q??BOo^1
dT+4itadI2$gv};wHH@r=Krw^OTiI_j0ssk<6HNdB

-- 
GitLab


From 32dd6ea08b8d2dec1972bb7beb13e8f6b6a84f69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Victor=20L=C3=B6fgren?= <viclo211@student.liu.se>
Date: Sun, 18 Apr 2021 23:13:25 +0200
Subject: [PATCH 20/20] Fix client tests

---
 .../src/reducers/presentationReducer.test.ts  | 21 ++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts
index 15bcd337..a155eab5 100644
--- a/client/src/reducers/presentationReducer.test.ts
+++ b/client/src/reducers/presentationReducer.test.ts
@@ -20,6 +20,11 @@ const initialState = {
     title: '',
   },
   teams: [],
+  code: '',
+  timer: {
+    enabled: false,
+    value: 0,
+  },
 }
 
 it('should return the initial state', () => {
@@ -46,7 +51,9 @@ it('should handle SET_PRESENTATION_COMPETITION', () => {
   ).toEqual({
     competition: testCompetition,
     slide: testCompetition.slides[0],
-    teams: [],
+    teams: initialState.teams,
+    code: initialState.code,
+    timer: initialState.timer,
   })
 })
 
@@ -70,6 +77,8 @@ it('should handle SET_PRESENTATION_TEAMS', () => {
     competition: initialState.competition,
     slide: initialState.slide,
     teams: testTeams,
+    code: initialState.code,
+    timer: initialState.timer,
   })
 })
 
@@ -92,6 +101,8 @@ it('should handle SET_PRESENTATION_SLIDE', () => {
     competition: initialState.competition,
     slide: testSlide,
     teams: initialState.teams,
+    code: initialState.code,
+    timer: initialState.timer,
   })
 })
 
@@ -107,6 +118,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => {
       },
       teams: initialState.teams,
       slide: { competition_id: 0, order: 1 } as Slide,
+      code: initialState.code,
+      timer: initialState.timer,
     }
     expect(
       presentationReducer(testPresentationState, {
@@ -116,6 +129,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => {
       competition: testPresentationState.competition,
       slide: testPresentationState.competition.slides[0],
       teams: testPresentationState.teams,
+      code: initialState.code,
+      timer: initialState.timer,
     })
   })
   it('by not changing slide if there is no previous one', () => {
@@ -129,6 +144,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => {
       },
       teams: initialState.teams,
       slide: { competition_id: 0, order: 0 } as Slide,
+      code: initialState.code,
+      timer: initialState.timer,
     }
     expect(
       presentationReducer(testPresentationState, {
@@ -138,6 +155,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => {
       competition: testPresentationState.competition,
       slide: testPresentationState.competition.slides[0],
       teams: testPresentationState.teams,
+      code: initialState.code,
+      timer: initialState.timer,
     })
   })
 })
-- 
GitLab