From 590e16555adfd21c8b9a9f4235881828fdd52155 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:29:36 +0000
Subject: [PATCH] Resolve "Add Socketio"

---
 client/package-lock.json                      |  88 +++++++++++
 client/package.json                           |   2 +
 client/src/actions/presentation.ts            |  25 ++-
 client/src/actions/types.ts                   |   3 +
 client/src/interfaces/Timer.ts                |   4 +
 client/src/pages/views/JudgeViewPage.tsx      |   8 +-
 client/src/pages/views/PresenterViewPage.tsx  |  25 ++-
 .../src/pages/views/components/SocketTest.tsx |  61 ++++++++
 client/src/pages/views/components/Timer.tsx   |  47 ++++++
 .../src/reducers/presentationReducer.test.ts  |  21 ++-
 client/src/reducers/presentationReducer.ts    |  28 ++++
 client/src/sockets.ts                         |  80 ++++++++++
 server/app/__init__.py                        |   7 +-
 server/app/core/sockets.py                    | 148 ++++++++++++++++--
 server/main.py                                |  23 +--
 server/populate.py                            |   6 +-
 server/requirements.txt                       | Bin 2206 -> 2448 bytes
 server/tests/__init__.py                      |   2 +-
 18 files changed, 529 insertions(+), 49 deletions(-)
 create mode 100644 client/src/interfaces/Timer.ts
 create mode 100644 client/src/pages/views/components/SocketTest.tsx
 create mode 100644 client/src/pages/views/components/Timer.tsx
 create mode 100644 client/src/sockets.ts

diff --git a/client/package-lock.json b/client/package-lock.json
index 7e73859f..667b7585 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -2370,6 +2370,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",
@@ -2587,6 +2592,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",
@@ -3970,6 +3980,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",
@@ -4025,6 +4040,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",
@@ -6061,6 +6081,30 @@
         "once": "^1.4.0"
       }
     },
+    "engine.io-client": {
+      "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",
+        "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",
@@ -7983,6 +8027,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",
@@ -11969,6 +12018,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",
@@ -15203,6 +15262,30 @@
         }
       }
     },
+    "socket.io-client": {
+      "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",
+        "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",
@@ -18151,6 +18234,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 2d393d3d..7c354ece 100644
--- a/client/package.json
+++ b/client/package.json
@@ -18,6 +18,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",
     "jwt-decode": "^3.1.2",
@@ -32,6 +33,7 @@
     "redux-devtools-extension": "^2.13.8",
     "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/actions/presentation.ts b/client/src/actions/presentation.ts
index 9e482d2b..60248e1f 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -1,6 +1,7 @@
 import axios from 'axios'
 import { Slide } from '../interfaces/Slide'
-import { AppDispatch } from './../store'
+import { Timer } from '../interfaces/Timer'
+import store, { AppDispatch } from './../store'
 import Types from './types'
 
 export const getPresentationCompetition = (id: string) => async (dispatch: AppDispatch) => {
@@ -42,3 +43,25 @@ export const setCurrentSlidePrevious = () => (dispatch: AppDispatch) => {
 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 })
+}
+
+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/actions/types.ts b/client/src/actions/types.ts
index 7c9cce08..848d41f0 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -20,7 +20,10 @@ 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_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/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index 293a1f08..12e866a0 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -2,7 +2,12 @@ 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 { SlideListItem } from '../presentationEditor/styled'
@@ -41,6 +46,7 @@ const JudgeViewPage: React.FC = () => {
   useEffect(() => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
+    dispatch(setPresentationCode(code))
   }, [])
   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 131bde22..22a672ba 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -2,15 +2,12 @@ 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,
-} from '../../actions/presentation'
+import { getPresentationCompetition, getPresentationTeams, setPresentationCode } from '../../actions/presentation'
 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 = () => {
@@ -22,6 +19,7 @@ const PresenterViewPage: React.FC = () => {
   useEffect(() => {
     dispatch(getPresentationCompetition(id))
     dispatch(getPresentationTeams(id))
+    dispatch(setPresentationCode(code))
   }, [])
   const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget)
@@ -29,6 +27,15 @@ const PresenterViewPage: React.FC = () => {
   const handleClose = () => {
     setAnchorEl(null)
   }
+  const handleNextSlidePressed = () => {
+    // dispatch(setCurrentSlideNext())
+    // syncSlide()
+  }
+  const handlePreviousSlidePressed = () => {
+    // dispatch(setCurrentSlidePrevious())
+    // syncSlide()
+  }
+
   return (
     <PresenterContainer>
       <PresenterHeader>
@@ -41,10 +48,12 @@ 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">
+        <SocketTest></SocketTest>
+        <Timer></Timer>
+        <PresenterButton onClick={handleNextSlidePressed} variant="contained">
           <ChevronRightIcon fontSize="large" />
         </PresenterButton>
       </PresenterFooter>
diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx
new file mode 100644
index 00000000..d99f5b2a
--- /dev/null
+++ b/client/src/pages/views/components/SocketTest.tsx
@@ -0,0 +1,61 @@
+import React, { useEffect } from 'react'
+import { connect } from 'react-redux'
+import { useAppDispatch } from '../../../hooks'
+import {
+  socketEndPresentation,
+  socketJoinPresentation,
+  socketSetSlideNext,
+  socketSetSlidePrev,
+  socketStartPresentation,
+  socketStartTimer,
+  socket_connect,
+} from '../../../sockets'
+
+const mapStateToProps = (state: any) => {
+  return {
+    slide_order: state.presentation.slide.order,
+  }
+}
+
+const mapDispatchToProps = (dispatch: any) => {
+  return {
+    // tickTimer: () => dispatch(tickTimer(1)),
+  }
+}
+
+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
+  }, [])
+
+  return (
+    <>
+      <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>
+      <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>
+      <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/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
new file mode 100644
index 00000000..b4401a69
--- /dev/null
+++ b/client/src/pages/views/components/Timer.tsx
@@ -0,0 +1,47 @@
+import React, { useEffect } from 'react'
+import { connect } from 'react-redux'
+import { setPresentationTimer, setPresentationTimerDecrement } 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(setPresentationTimerDecrement())
+      }, 1000)
+    } else {
+      clearInterval(timerIntervalId)
+    }
+  }, [props.timer.enabled])
+
+  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/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,
     })
   })
 })
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index b4325680..2b3c5eb1 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'
@@ -8,6 +9,8 @@ interface PresentationState {
   competition: RichCompetition
   slide: Slide
   teams: Team[]
+  code: string
+  timer: Timer
 }
 
 const initialState: PresentationState = {
@@ -27,6 +30,11 @@ const initialState: PresentationState = {
     title: '',
   },
   teams: [],
+  code: '',
+  timer: {
+    enabled: false,
+    value: 0,
+  },
 }
 
 export default function (state = initialState, action: AnyAction) {
@@ -42,6 +50,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,
@@ -63,6 +76,21 @@ 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
+    case Types.SET_PRESENTATION_TIMER:
+      if (action.payload.value == 0) {
+        action.payload.enabled = false
+      }
+      return {
+        ...state,
+        timer: action.payload,
+      }
     default:
       return state
   }
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
new file mode 100644
index 00000000..874bfc46
--- /dev/null
+++ b/client/src/sockets.ts
@@ -0,0 +1,80 @@
+import io from 'socket.io-client'
+import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
+import { Timer } from './interfaces/Timer'
+import store from './store'
+
+interface SetSlideInterface {
+  slide_order: number
+}
+
+interface TimerInterface {
+  value: number
+  enabled: boolean
+}
+
+interface SetTimerInterface {
+  timer: TimerInterface
+}
+
+let socket: SocketIOClient.Socket
+
+export const socket_connect = () => {
+  if (!socket) {
+    socket = io('localhost:5000')
+
+    socket.on('set_slide', (data: SetSlideInterface) => {
+      setCurrentSlideByOrder(data.slide_order)(store.dispatch)
+    })
+
+    socket.on('set_timer', (data: SetTimerInterface) => {
+      setPresentationTimer(data.timer)(store.dispatch)
+    })
+
+    socket.on('end_presentation', () => {
+      socket.disconnect()
+    })
+  }
+}
+
+export const socketStartPresentation = () => {
+  socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
+}
+
+export const socketJoinPresentation = () => {
+  socket.emit('join_presentation', { code: 'OEM1V4' }) // TODO: Send code gotten from auth/login/<code> api call
+}
+
+export const socketEndPresentation = () => {
+  socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
+}
+
+export const socketSetSlideNext = () => {
+  socketSetSlide(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 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
+  }
+
+  socket.emit('set_slide', {
+    competition_id: store.getState().presentation.competition.id,
+    slide_order: slide_order,
+  })
+}
+
+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/__init__.py b/server/app/__init__.py
index a2529395..8eebb113 100644
--- a/server/app/__init__.py
+++ b/server/app/__init__.py
@@ -15,9 +15,14 @@ def create_app(config_name="configmodule.DevelopmentConfig"):
         bcrypt.init_app(app)
         jwt.init_app(app)
         db.init_app(app)
+        db.create_all()
         ma.init_app(app)
         configure_uploads(app, (MediaDTO.image_set,))
 
+        from app.core.sockets import sio
+
+        sio.init_app(app)
+
         from app.apis import flask_api
 
         flask_api.init_app(app)
@@ -34,7 +39,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"):
             header["Access-Control-Allow-Origin"] = "*"
             return response
 
-        return app
+    return app, sio
 
 
 def identity(payload):
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index d9407a69..f4909531 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 Competition, Slide, Team, 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():
@@ -11,26 +19,138 @@ 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}")
 
 
-@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},
+    }
+
+    print(f"{presentations=}")
+
+    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
+
+    del presentations[competition_id]
+
+    print(f"{presentations=}")
+
+    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
 
+    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
+    )
 
-@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}")
+    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
 
-@sio.on("sync_timer")
+    # 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"{presentations=}")
+
+    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 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
+
+    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=}")
+
+    emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
+    print(f"[Set slide]: {slide_order} -> {competition_id}")
+
+
+@sio.on("set_timer")
 def sync_timer(data):
-    competitionID = data["competitionID"]
+    competition_id = data["competition_id"]
     timer = data["timer"]
-    emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False)
-    print(f"[Sync timer]: {competitionID=} {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
+
+    # 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=}")
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/populate.py b/server/populate.py
index bebcaa8b..7044db60 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"]
@@ -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()
diff --git a/server/requirements.txt b/server/requirements.txt
index cbda8a7040167ce59a2209747e8d7d4204f7fe2d..bda47a88036c81470bc7a6b2112b2ecedd904ed7 100644
GIT binary patch
delta 256
zcmbOyI6-)W9h0atLq0<hLotIb5E?V+F&F@`;pBr%!kg`wWEkty8B!U_fH;q#1gyk}
z!4RYXh>aL{8Mqjb<#id#fhv=LhUGIPGh_qV5ItZcAqGSB6anQxX6C@vfJ_9bGXSf|
zWJm$ZCNkJ=PG+`d4XFYeSO8S22gJyZF#(F0!ZjgOWrCfX3iMYR(2!)HE|Bj)Dj_}u
GIUE2=Qz^*+

delta 17
YcmbOrJWp_g9n<DCCK<-fKbT!u0W?wtasU7T

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