diff --git a/client/package-lock.json b/client/package-lock.json index 7e73859f4e13b504ee814cde74ed293dd53e33ed..667b75859fe71467505839e687c7e478b8a52834 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 2d393d3d80ce5126841f980c77e830355be56960..7c354ece5569314b108c67617fecf9e0f9d637e0 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 9e482d2b4c51d49fdd04dd8f18721a1e74e82d5a..60248e1f17f2d0fb555680092fe361bc0cee5c1d 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 7c9cce081ce01f9cd317ca921f0352b32c4b02ee..848d41f0cb05e9cc70465db7e61e5bf722a68a02 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 0000000000000000000000000000000000000000..49d1909e15692e68bb8cdef32ddd9f59d6b69409 --- /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 293a1f08ca5a7bff56b980844233bcb879edd215..12e866a085b506df3d22eb38151dcca4acd222e6 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 131bde22f5b23e70e9ac071563395f353b7b9e1f..22a672bac56b5cae4825128cd21216835f425d57 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 0000000000000000000000000000000000000000..d99f5b2aa740d9a2690b3aadeae6e1050b92d089 --- /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 0000000000000000000000000000000000000000..b4401a696c42d25cc8296f22059a780feb00e8e9 --- /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 15bcd3376110f565db2ed73bc4e2d848c8063f61..a155eab5c915e82c64f16c0093dd2fc296b10d7a 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 b4325680b0c1d87698a0aa798fc013528855f4f6..2b3c5eb15dbc25f21cc51856ee2590591eea37ac 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 0000000000000000000000000000000000000000..874bfc46ad3a003013d32c31585704aceb527f84 --- /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 a25293952d75dd64ff86e4fe76b105447edf8cfe..8eebb113a332ebca184f6a8282826638984715ea 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 d9407a69b764e7a70ea81459a7a18f924daee21d..f4909531961078663f4e9d014311fec126afd560 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 cb6168551e98b6f67ca19bc25ecf64f7a6614659..bf4a239311a451132d5c1dfb2b262ce29f4ccb32 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 bebcaa8b098cdffeaf070b71eb0570cd359e5bf9..7044db60bc058060c21d3082cedb47ab4033f3fc 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 Binary files a/server/requirements.txt and b/server/requirements.txt differ diff --git a/server/tests/__init__.py b/server/tests/__init__.py index 0b0deaf0c57b8faae6fa76a21b35cb394c049afa..c5b8f20d24cbfabe4d6c87f66a3e9b893a51d23d 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():