Skip to content
Snippets Groups Projects
OperatorViewPage.tsx 11.4 KiB
Newer Older
import {
  Button,
  createStyles,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  ListItem,
Max Rüdiger's avatar
Max Rüdiger committed
  ListItemText,
  makeStyles,
Albin Henriksson's avatar
Albin Henriksson committed
  Snackbar,
  Tooltip,
  Typography,
} from '@material-ui/core'
import AssignmentIcon from '@material-ui/icons/Assignment'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import CloseIcon from '@material-ui/icons/Close'
import FileCopyIcon from '@material-ui/icons/FileCopy'
import LinkIcon from '@material-ui/icons/Link'
import SupervisorAccountIcon from '@material-ui/icons/SupervisorAccount'
import TimerIcon from '@material-ui/icons/Timer'
Albin Henriksson's avatar
Albin Henriksson committed
import { Alert } from '@material-ui/lab'
import axios from 'axios'
Albin Henriksson's avatar
Albin Henriksson committed
import React, { useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { useAppSelector } from '../../hooks'
import { socketConnect, socketEndPresentation, socketSync } from '../../sockets'
import SlideDisplay from '../presentationEditor/components/SlideDisplay'
Albin Henriksson's avatar
Albin Henriksson committed
import { Center } from '../presentationEditor/components/styled'
Victor Löfgren's avatar
Victor Löfgren committed
import Scoreboard from './components/Scoreboard'
Victor Löfgren's avatar
Victor Löfgren committed
import Timer from './components/Timer'
import {
Max Rüdiger's avatar
Max Rüdiger committed
  OperatorButton,
  OperatorContainer,
  OperatorContent,
Max Rüdiger's avatar
Max Rüdiger committed
  OperatorFooter,
  OperatorHeader,
  OperatorHeaderItem,
Max Rüdiger's avatar
Max Rüdiger committed
  OperatorInnerContent,
Albin Henriksson's avatar
Albin Henriksson committed
  OperatorQuitButton,
} from './styled'
Max Rüdiger's avatar
Max Rüdiger committed
 *  Description:
 *
 *  Presentation is an active competition
Max Rüdiger's avatar
Max Rüdiger committed
 *
 *
 *  ===========================================
 *  TODO:
 *  - When two userers are connected to the same Localhost:5000 and updates/starts/end competition it
 *    creates a bug where the competition can't be started.
 * ===========================================
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    table: {
      width: '100%',
    },
    margin: {
      margin: theme.spacing(1),
    },
    paper: {
      backgroundColor: theme.palette.background.paper,
      boxShadow: theme.shadows[5],
      padding: 4,
      outline: 'none',
    },
  })
)

interface Code {
  id: number
  code: string
  view_type_id: number
  competition_id: number
  team_id: number
}
Max Rüdiger's avatar
Max Rüdiger committed
const OperatorViewPage: React.FC = () => {
  // for dialog alert
  const [openAlert, setOpen] = React.useState(false)
  const [openAlertCode, setOpenCode] = React.useState(false)
  const [codes, setCodes] = React.useState<Code[]>([])
  const competitionName = useAppSelector((state) => state.presentation.competition.name)

  //const fullScreen = useMediaQuery(theme.breakpoints.down('sm'))

  const classes = useStyles()
  const teams = useAppSelector((state) => state.presentation.competition.teams)
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
  const presentation = useAppSelector((state) => state.presentation)
  const activeId = useAppSelector((state) => state.presentation.competition.id)
  const timer = useAppSelector((state) => state.presentation.timer)
  const history = useHistory()
  const viewTypes = useAppSelector((state) => state.types.viewTypes)
  const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id
Albin Henriksson's avatar
Albin Henriksson committed
  const [successMessageOpen, setSuccessMessageOpen] = useState(true)
  const activeSlideOrder = useAppSelector(
    (state) =>
      state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.order
  )
  const slideTimer = useAppSelector((state) =>
    activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null
  )
  const isFirstSlide = activeSlideOrder === 0
  const isLastSlide = useAppSelector((state) => activeSlideOrder === state.presentation.competition.slides.length - 1)
Victor Löfgren's avatar
Victor Löfgren committed
  const showScoreboard = useAppSelector((state) => state.presentation.show_scoreboard)

Albin Henriksson's avatar
Albin Henriksson committed
    socketConnect('Operator')
Björn Modée's avatar
Björn Modée committed
  /** Handles the browsers back button and if pressed cancels the ongoing competition */
Max Rüdiger's avatar
Max Rüdiger committed
  window.onpopstate = () => {
    alert('Tävlingen avslutas för alla')
    endCompetition()
  }

  const handleClose = () => {
    setOpen(false)
Max Rüdiger's avatar
Max Rüdiger committed
    setOpenCode(false)
Björn Modée's avatar
Björn Modée committed
  /** Making sure the user wants to exit the competition by displaying a dialog box */
  const handleVerifyExit = () => {
    setOpen(true)
  const handleOpenCodes = async () => {
    await getCodes()
Max Rüdiger's avatar
Max Rüdiger committed
    setOpenCode(true)
  }

  const endCompetition = () => {
    setOpen(false)
    socketEndPresentation()
Albin Henriksson's avatar
Albin Henriksson committed
    history.push('/admin/competition-manager')
Björn Modée's avatar
Björn Modée committed
    window.location.reload(false) // TODO: fix this, we "need" to refresh site to be able to run the competition correctly again
  const getCodes = async () => {
    await axios
      .get(`/api/competitions/${activeId}/codes`)
      .then((response) => {
        setCodes(response.data.items)
      })
      .catch(console.log)
  }
  const getTypeName = (code: Code) => {
    let typeName = ''
    switch (code.view_type_id) {
      case 1:
        const team = teams.find((team) => team.id === code.team_id)
        if (team) {
          typeName = team.name
        } else {
          typeName = 'Lagnamn hittades ej'
        }
        break
      case 2:
        typeName = 'Domare'
        break
      case 3:
        typeName = 'Publik'
        break
      case 4:
        typeName = 'Tävlingsoperatör'
        break
      default:
        typeName = 'Typ hittades ej'
        break
    }
    return typeName
  }

  const handleStartTimer = () => {
    if (!slideTimer) return

    if (!timer.enabled) socketSync({ timer: { value: Date.now() + 1000 * slideTimer, enabled: true } })
    else socketSync({ timer: { ...timer, enabled: false } })
  }

  const handleSetNextSlide = () => {
    if (activeSlideOrder !== undefined)
      socketSync({ slide_order: activeSlideOrder + 1, timer: { value: null, enabled: false } })
  }

  const handleSetPrevSlide = () => {
    if (activeSlideOrder !== undefined)
      socketSync({ slide_order: activeSlideOrder - 1, timer: { value: null, enabled: false } })
  }

Max Rüdiger's avatar
Max Rüdiger committed
    <OperatorContainer>
      <Dialog open={openAlertCode} onClose={handleClose} aria-labelledby="max-width-dialog-title" maxWidth="xl">
Albin Henriksson's avatar
Albin Henriksson committed
        <Center>
          <DialogTitle id="max-width-dialog-title" className={classes.paper} style={{ width: '100%' }}>
            Koder för {competitionName}
          </DialogTitle>
        </Center>
Max Rüdiger's avatar
Max Rüdiger committed
        <DialogContent>
          {/* <DialogContentText>Här visas tävlingskoderna till den valda tävlingen.</DialogContentText> */}
          {codes &&
            codes.map((code) => (
              <ListItem key={code.id} style={{ display: 'flex' }}>
                <ListItemText primary={`${getTypeName(code)}: `} />
                <Typography component="div">
                  <ListItemText style={{ textAlign: 'right', marginLeft: '10px' }}>
                    <Box fontFamily="Monospace" fontWeight="fontWeightBold">
                      {code.code}
                    </Box>
                  </ListItemText>
                </Typography>
                <Tooltip title="Kopiera kod" arrow>
                  <Button
                    margin-right="0px"
                    onClick={() => {
                      navigator.clipboard.writeText(code.code)
                    }}
                  >
                    <FileCopyIcon fontSize="small" />
                  </Button>
                </Tooltip>
                <Tooltip title="Kopiera länk" arrow>
                  <Button
                    margin-right="0px"
                    onClick={() => {
                      navigator.clipboard.writeText(`${window.location.host}/${code.code}`)
                    }}
                  >
                    <LinkIcon fontSize="small" />
                  </Button>
                </Tooltip>
Max Rüdiger's avatar
Max Rüdiger committed
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Stäng
Max Rüdiger's avatar
Max Rüdiger committed
          </Button>
        </DialogActions>
      </Dialog>
Albin Henriksson's avatar
Albin Henriksson committed
      <OperatorHeader color="primary" position="fixed">
        <Tooltip title="Avsluta tävling" arrow>
Albin Henriksson's avatar
Albin Henriksson committed
          <OperatorQuitButton onClick={handleVerifyExit} variant="contained" color="secondary">
            <CloseIcon fontSize="large" />
Albin Henriksson's avatar
Albin Henriksson committed
          </OperatorQuitButton>
        </Tooltip>
        <Dialog open={openAlert} onClose={handleClose} aria-labelledby="responsive-dialog-title">
          <DialogTitle id="responsive-dialog-title">{'Vill du avsluta tävlingen?'}</DialogTitle>
          <DialogContent>
            <DialogContentText>
              Genom att avsluta tävlingen kommer den avslutas för alla. Du kommer gå tillbaka till startsidan.
            </DialogContentText>
          </DialogContent>
          <DialogActions>
            <Button autoFocus onClick={handleClose} color="primary">
              Avbryt
            </Button>
            <Button onClick={endCompetition} color="primary" autoFocus>
              Avsluta tävling
            </Button>
          </DialogActions>
        </Dialog>
        <OperatorHeaderItem>
Albin Henriksson's avatar
Albin Henriksson committed
          <Typography variant="h4">{presentation.competition.name}</Typography>
        </OperatorHeaderItem>
        <OperatorHeaderItem>
Albin Henriksson's avatar
Albin Henriksson committed
          <Typography variant="h4">
Albin Henriksson's avatar
Albin Henriksson committed
            {activeSlideOrder !== undefined && activeSlideOrder + 1} / {presentation.competition.slides.length}
          </Typography>
        </OperatorHeaderItem>
Max Rüdiger's avatar
Max Rüdiger committed
      </OperatorHeader>
Albin Henriksson's avatar
Albin Henriksson committed
      {<div style={{ minHeight: 64 }} />}
Max Rüdiger's avatar
Max Rüdiger committed
      <OperatorContent>
        <OperatorInnerContent>
          {activeViewTypeId && <SlideDisplay variant="presentation" activeViewTypeId={activeViewTypeId} />}
Max Rüdiger's avatar
Max Rüdiger committed
        </OperatorInnerContent>
      </OperatorContent>
Albin Henriksson's avatar
Albin Henriksson committed
      {<div style={{ minHeight: 96 }} />}
      <OperatorFooter position="fixed">
        <Tooltip title="Föregående sida" arrow>
          <OperatorButton onClick={handleSetPrevSlide} variant="contained" disabled={isFirstSlide} color="primary">
            <ChevronLeftIcon fontSize="small" />
          </OperatorButton>
        </Tooltip>
Albin Henriksson's avatar
Albin Henriksson committed
        {slideTimer !== null && (
          <Tooltip title="Starta timer" arrow>
            <OperatorButton
              onClick={handleStartTimer}
              variant="contained"
              disabled={timer.value !== null && !timer.enabled}
              color="primary"
            >
              <TimerIcon fontSize="small" />
              <Timer disableText />
Max Rüdiger's avatar
Max Rüdiger committed
            </OperatorButton>
          </Tooltip>
Albin Henriksson's avatar
Albin Henriksson committed
        )}
Albin Henriksson's avatar
Albin Henriksson committed
        <Tooltip title="Visa ställning för publik" arrow>
          <OperatorButton onClick={() => socketSync({ show_scoreboard: true })} variant="contained" color="primary">
            <AssignmentIcon fontSize="small" />
          </OperatorButton>
        </Tooltip>
        {showScoreboard && <Scoreboard isOperator />}
Max Rüdiger's avatar
Max Rüdiger committed

Albin Henriksson's avatar
Albin Henriksson committed
        <Tooltip title="Visa koder" arrow>
          <OperatorButton onClick={handleOpenCodes} variant="contained" color="primary">
            <SupervisorAccountIcon fontSize="small" />
          </OperatorButton>
        </Tooltip>

        <Tooltip title="Nästa sida" arrow>
          <OperatorButton onClick={handleSetNextSlide} variant="contained" disabled={isLastSlide} color="primary">
            <ChevronRightIcon fontSize="small" />
          </OperatorButton>
        </Tooltip>
Max Rüdiger's avatar
Max Rüdiger committed
      </OperatorFooter>
      <Snackbar
        open={successMessageOpen && Boolean(competitionName)}
        autoHideDuration={4000}
        onClose={() => setSuccessMessageOpen(false)}
      >
Albin Henriksson's avatar
Albin Henriksson committed
        <Alert severity="success">{`Du har gått med i tävlingen "${competitionName}" som operatör`}</Alert>
      </Snackbar>
Max Rüdiger's avatar
Max Rüdiger committed
    </OperatorContainer>
Max Rüdiger's avatar
Max Rüdiger committed
export default OperatorViewPage