Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • tddd96-grupp11/teknikattan-scoring-system
1 result
Show changes
Showing
with 755 additions and 197 deletions
......@@ -76,9 +76,7 @@ const AdminLogin: React.FC = () => {
fullWidth
variant="contained"
color="secondary"
disabled={
!formik.isValid || formik.values.model?.email === '' || formik.values.model?.email === '' || loading
}
disabled={!!formik.errors.model?.password || !!formik.errors.model?.email || loading}
>
Logga in
</Button>
......
......@@ -28,7 +28,7 @@ it('renders presentation editor', () => {
},
}
;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
if (path.startsWith('/competitions')) return Promise.resolve(competitionRes)
if (path.startsWith('/api/competitions')) return Promise.resolve(competitionRes)
return Promise.resolve(citiesRes)
})
render(
......
import { Button, CircularProgress, Divider, Menu, MenuItem, Typography } from '@material-ui/core'
import { Button, Checkbox, CircularProgress, Divider, Menu, MenuItem, Typography } from '@material-ui/core'
import AppBar from '@material-ui/core/AppBar'
import { CheckboxProps } from '@material-ui/core/Checkbox'
import CssBaseline from '@material-ui/core/CssBaseline'
import Drawer from '@material-ui/core/Drawer'
import ListItemText from '@material-ui/core/ListItemText'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'
import AddOutlinedIcon from '@material-ui/icons/AddOutlined'
import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined'
import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined'
import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined'
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'
import axios from 'axios'
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { getCities } from '../../actions/cities'
import { getEditorCompetition, setEditorSlideId } from '../../actions/editor'
......@@ -35,7 +36,7 @@ import {
const initialState = {
mouseX: null,
mouseY: null,
slideOrder: null,
slideId: null,
}
const leftDrawerWidth = 150
......@@ -72,6 +73,12 @@ const useStyles = makeStyles((theme: Theme) =>
backgroundColor: theme.palette.background.default,
padding: theme.spacing(3),
},
alignCheckboxText: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingRight: 20,
},
})
)
......@@ -86,7 +93,6 @@ const PresentationEditorPage: React.FC = () => {
const activeSlideId = useAppSelector((state) => state.editor.activeSlideId)
const competition = useAppSelector((state) => state.editor.competition)
const competitionLoading = useAppSelector((state) => state.editor.loading)
// TODO: wait for dispatch to finish
useEffect(() => {
dispatch(getEditorCompetition(id))
dispatch(getCities())
......@@ -98,22 +104,22 @@ const PresentationEditorPage: React.FC = () => {
}
const createNewSlide = async () => {
await axios.post(`/competitions/${id}/slides`, { title: 'new slide' })
await axios.post(`/api/competitions/${id}/slides`, { title: 'new slide' })
dispatch(getEditorCompetition(id))
}
const [contextState, setContextState] = React.useState<{
mouseX: null | number
mouseY: null | number
slideOrder: null | number
slideId: null | number
}>(initialState)
const handleRightClick = (event: React.MouseEvent<HTMLDivElement>, slideOrder: number) => {
const handleRightClick = (event: React.MouseEvent<HTMLDivElement>, slideId: number) => {
event.preventDefault()
setContextState({
mouseX: event.clientX - 2,
mouseY: event.clientY - 4,
slideOrder: slideOrder,
slideId: slideId,
})
}
......@@ -122,33 +128,43 @@ const PresentationEditorPage: React.FC = () => {
}
const handleRemoveSlide = async () => {
await axios.delete(`/competitions/${id}/slides/${contextState.slideOrder}`)
await axios.delete(`/api/competitions/${id}/slides/${contextState.slideId}`)
dispatch(getEditorCompetition(id))
setContextState(initialState)
}
const handleDuplicateSlide = async () => {
const response = await axios.post(`/competitions/${id}/slides`)
const newOrder = response.data.items[response.data.total_count - 1].order
const oldSlide = competition.slides.find((slide) => slide.order === contextState.slideOrder)
await axios.put(`/competitions/${id}/slides/${newOrder}`, { timer: oldSlide?.timer, title: oldSlide?.title })
await axios.post(`/api/competitions/${id}/slides/${contextState.slideId}/copy`)
dispatch(getEditorCompetition(id))
setContextState(initialState)
}
const renderSlideIcon = (slide: RichSlide) => {
switch (slide.questions[0].type_id) {
case 0:
return <InfoOutlinedIcon></InfoOutlinedIcon> // information slide
case 1:
return <CreateOutlinedIcon></CreateOutlinedIcon> // text question
case 2:
return <BuildOutlinedIcon></BuildOutlinedIcon> // practical qustion
case 3:
return <DnsOutlinedIcon></DnsOutlinedIcon> // multiple choice question
if (slide.questions && slide.questions[0] && slide.questions[0].type_id) {
switch (slide.questions[0].type_id) {
case 1:
return <CreateOutlinedIcon /> // text question
case 2:
return <BuildOutlinedIcon /> // practical qustion
case 3:
return <DnsOutlinedIcon /> // multiple choice question
}
} else {
return <InfoOutlinedIcon /> // information slide
}
}
const GreenCheckbox = withStyles({
root: {
color: '#FFFFFF',
'&$checked': {
color: '#FFFFFF',
},
},
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
const [checkbox, setCheckbox] = useState(false)
return (
<PresentationEditorContainer>
<CssBaseline />
......@@ -160,7 +176,12 @@ const PresentationEditorPage: React.FC = () => {
<Typography variant="h6" noWrap>
{competition.name}
</Typography>
<ViewButtonGroup>
<GreenCheckbox checked={checkbox} onChange={(event) => setCheckbox(event.target.checked)} />
<Typography className={classes.alignCheckboxText} variant="button">
Applicera ändringar på samtliga vyer
</Typography>
<ViewButton variant="contained" color="secondary">
Åskådarvy
</ViewButton>
......@@ -193,7 +214,7 @@ const PresentationEditorPage: React.FC = () => {
key={slide.id}
selected={slide.id === activeSlideId}
onClick={() => setActiveSlideId(slide.id)}
onContextMenu={(event) => handleRightClick(event, slide.order)}
onContextMenu={(event) => handleRightClick(event, slide.id)}
>
{renderSlideIcon(slide)}
<ListItemText primary={`Sida ${slide.order + 1}`} />
......
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
FormControl,
InputLabel,
......@@ -9,11 +14,12 @@ import {
MenuItem,
Select,
TextField,
Typography,
} from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close'
import axios from 'axios'
import React from 'react'
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
......@@ -35,7 +41,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
textCenter: {
textAlign: 'center',
background: 'white',
},
center: {
display: 'flex',
......@@ -52,6 +57,12 @@ const useStyles = makeStyles((theme: Theme) =>
width: '87%',
background: 'white',
},
addButtons: {
padding: 5,
},
panelList: {
padding: 0,
},
})
)
......@@ -66,16 +77,7 @@ const CompetitionSettings: React.FC = () => {
const competition = useAppSelector((state) => state.editor.competition)
const updateCompetitionName = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
await axios
.put(`/competitions/${id}`, { name: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const handleClick = async (tid: number) => {
await axios
.delete(`/competitions/${id}/teams/${tid}`)
.put(`/api/competitions/${id}`, { name: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
......@@ -85,7 +87,7 @@ const CompetitionSettings: React.FC = () => {
const cities = useAppSelector((state) => state.cities.cities)
const updateCompetitionCity = async (city: City) => {
await axios
.put(`/competitions/${id}`, { city_id: city.id })
.put(`/api/competitions/${id}`, { city_id: city.id })
.then(() => {
dispatch(getEditorCompetition(id))
})
......@@ -100,6 +102,36 @@ const CompetitionSettings: React.FC = () => {
})
}
const removeTeam = async (tid: number) => {
await axios
.delete(`/api/competitions/${id}/teams/${tid}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const addTeam = async () => {
setAddTeamOpen(false)
await axios
.post(`/api/competitions/${id}/teams`, { name: selectedTeamName })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
// For "add team" dialog
const [addTeamOpen, setAddTeamOpen] = useState(false)
const openAddTeam = () => {
setAddTeamOpen(true)
}
const closeAddTeam = () => {
setAddTeamOpen(false)
}
let selectedTeamName = ''
const updateSelectedTeamName = (event: React.ChangeEvent<{ value: string }>) => {
selectedTeamName = event.target.value
}
return (
<div className={classes.textInputContainer}>
<form noValidate autoComplete="off">
......@@ -113,8 +145,7 @@ const CompetitionSettings: React.FC = () => {
/>
<Divider />
<FormControl variant="outlined" className={classes.dropDown}>
<InputLabel id="region-selection-label">Region</InputLabel>
{/*TODO: fixa så cities laddar in i statet likt i CompetitionManager*/}
<InputLabel>Region</InputLabel>
<Select
value={cities.find((city) => city.id === competition.city_id)?.name || ''}
label="Region"
......@@ -129,7 +160,7 @@ const CompetitionSettings: React.FC = () => {
</FormControl>
</form>
<List>
<List className={classes.panelList}>
<ListItem>
<ListItemText className={classes.textCenter} primary="Lag" />
</ListItem>
......@@ -138,13 +169,31 @@ const CompetitionSettings: React.FC = () => {
<div key={team.id}>
<ListItem divider button>
<ListItemText primary={team.name} />
<CloseIcon onClick={() => handleClick(team.id)} />
<CloseIcon onClick={() => removeTeam(team.id)} />
</ListItem>
</div>
))}
<ListItem className={classes.center} button>
<Button>Lägg till lag</Button>
<ListItem className={classes.center} button onClick={openAddTeam}>
<Typography className={classes.addButtons} variant="button">
Lägg till lag
</Typography>
</ListItem>
<Dialog open={addTeamOpen} onClose={closeAddTeam}>
<DialogTitle className={classes.center}>Lägg till lag</DialogTitle>
<DialogContent>
<DialogContentText>Skriv namnet på laget och klicka sedan på bekräfta.</DialogContentText>
<TextField autoFocus margin="dense" label="Lagnamn" fullWidth onChange={updateSelectedTeamName} />
</DialogContent>
<DialogActions>
<Button onClick={closeAddTeam} color="secondary">
Avbryt
</Button>
<Button onClick={addTeam} color="primary">
Bekräfta
</Button>
</DialogActions>
</Dialog>
</List>
<ListItem button>
......@@ -153,7 +202,7 @@ const CompetitionSettings: React.FC = () => {
src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1"
className={classes.importedImage}
/>
<ListItemText className={classes.textCenter} primary="Välj bakgrundsbild ..." />
<ListItemText className={classes.textCenter}>Välj bakgrundsbild ...</ListItemText>
</ListItem>
</div>
)
......
import { Editor } from '@tinymce/tinymce-react'
import axios from 'axios'
import React, { useState } from 'react'
import { Rnd } from 'react-rnd'
import { ComponentTypes } from '../../../enum/ComponentTypes'
import { useAppSelector } from '../../../hooks'
import { TextComponent } from '../../../interfaces/ApiModels'
import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels'
import { Position, Size } from '../../../interfaces/Components'
import CheckboxComponent from './CheckboxComponent'
import ImageComponentDisplay from './ImageComponentDisplay'
import { TextComponentContainer } from './styled'
type ImageComponentProps = {
component: TextComponent
component: Component
}
const TextComponentDisplay = ({ component }: ImageComponentProps) => {
const RndComponent = ({ component }: ImageComponentProps) => {
const [hover, setHover] = useState(false)
const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y })
const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h })
const competitionId = useAppSelector((state) => state.editor.competition.id)
const slideId = useAppSelector((state) => state.editor.activeSlideId)
if (component.id === 1) console.log(component)
const handleEditorChange = (e: any) => {
console.log('Content was updated:', e.target.getContent())
axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
data: { ...component.data, text: e.target.getContent() },
})
}
const handleUpdatePos = (pos: Position) => {
axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
axios.put(`/api/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
x: pos.x,
y: pos.y,
})
}
const handleUpdateSize = () => {
axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
w: currentSize.w,
h: currentSize.h,
const handleUpdateSize = (size: Size) => {
axios.put(`/api/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
w: size.w,
h: size.h,
})
}
const renderInnerComponent = () => {
switch (component.type_id) {
case ComponentTypes.Checkbox:
return <CheckboxComponent key={component.id} component={component} />
case ComponentTypes.Text:
return (
<TextComponentContainer
hover={hover}
dangerouslySetInnerHTML={{ __html: (component as TextComponent).data.text }}
/>
)
case ComponentTypes.Image:
return <ImageComponentDisplay key={component.id} component={component as ImageComponent} />
default:
break
}
}
return (
<Rnd
minWidth={50}
......@@ -43,38 +59,29 @@ const TextComponentDisplay = ({ component }: ImageComponentProps) => {
setCurrentPos({ x: d.x, y: d.y })
handleUpdatePos(d)
}}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
size={{ width: currentSize.w, height: currentSize.h }}
position={{ x: currentPos.x, y: currentPos.y }}
onResize={(e, direction, ref, delta, position) => {
onResizeStop={(e, direction, ref, delta, position) => {
setCurrentSize({
w: ref.offsetWidth,
h: ref.offsetHeight,
})
setCurrentPos(position)
handleUpdateSize({ w: ref.offsetWidth, h: ref.offsetHeight })
handleUpdatePos(position)
}}
onResizeStop={handleUpdateSize}
onResize={(e, direction, ref, delta, position) =>
setCurrentSize({
w: ref.offsetWidth,
h: ref.offsetHeight,
})
}
>
<div style={{ height: '100%', width: '100%' }}>
<Editor
initialValue={component.data.text}
init={{
height: '100%',
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
],
toolbar:
'undo redo | formatselect | fontselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help',
}}
onChange={handleEditorChange}
/>
</div>
{renderInnerComponent()}
</Rnd>
)
}
export default TextComponentDisplay
export default RndComponent
import React from 'react'
import { ComponentTypes } from '../../../enum/ComponentTypes'
import { useAppSelector } from '../../../hooks'
import { ImageComponent, TextComponent } from '../../../interfaces/ApiModels'
import CheckboxComponent from './CheckboxComponent'
import ImageComponentDisplay from './ImageComponentDisplay'
import RndComponent from './RndComponent'
import { SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled'
import TextComponentDisplay from './TextComponentDisplay'
const SlideEditor: React.FC = () => {
const components = useAppSelector(
......@@ -16,19 +12,7 @@ const SlideEditor: React.FC = () => {
<SlideEditorContainer>
<SlideEditorContainerRatio>
<SlideEditorPaper>
{components &&
components.map((component) => {
switch (component.type_id) {
case ComponentTypes.Checkbox:
return <CheckboxComponent key={component.id} component={component} />
case ComponentTypes.Text:
return <TextComponentDisplay key={component.id} component={component as TextComponent} />
case ComponentTypes.Image:
return <ImageComponentDisplay key={component.id} component={component as ImageComponent} />
default:
break
}
})}
{components && components.map((component) => <RndComponent key={component.id} component={component} />)}
</SlideEditorPaper>
</SlideEditorContainerRatio>
</SlideEditorContainer>
......
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
FormControl,
InputLabel,
List,
......@@ -9,19 +15,20 @@ import {
MenuItem,
Select,
TextField,
Typography,
} from '@material-ui/core'
import { CheckboxProps } from '@material-ui/core/Checkbox'
import { green, grey } from '@material-ui/core/colors'
import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close'
import MoreHorizOutlinedIcon from '@material-ui/icons/MoreHorizOutlined'
import axios from 'axios'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { TextComponent } from '../../../interfaces/ApiModels'
import { HiddenInput } from './styled'
import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels'
import { HiddenInput, TextCard } from './styled'
import TextComponentEdit from './TextComponentEdit'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
......@@ -39,7 +46,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
textCenter: {
textAlign: 'center',
background: 'white',
},
center: {
display: 'flex',
......@@ -50,6 +56,7 @@ const useStyles = makeStyles((theme: Theme) =>
margin: theme.spacing(2),
width: '87%',
background: 'white',
padding: 0,
},
clickableIcon: {
cursor: 'pointer',
......@@ -63,6 +70,16 @@ const useStyles = makeStyles((theme: Theme) =>
whiteBackground: {
background: 'white',
},
addButtons: {
padding: 5,
},
panelList: {
padding: 0,
},
addImageButton: {
padding: 5,
cursor: 'pointer',
},
})
)
......@@ -75,23 +92,22 @@ const SlideSettings: React.FC = () => {
const { id }: CompetitionParams = useParams()
const dispatch = useAppDispatch()
const competition = useAppSelector((state) => state.editor.competition)
let currentSlide = competition.slides[0]
// Init currentSlide if slides are not in order
for (const slide of competition.slides) {
if (slide.order === 1) {
currentSlide = slide
break
}
}
const activeSlideId = useAppSelector((state) => state.editor.activeSlideId)
const activeSlide = useAppSelector((state) =>
state.editor.competition.slides.find((slide) => slide && slide.id === state.editor.activeSlideId)
)
const handleCloseAnswerClick = async (alternative: number) => {
await axios
// TODO: implementera API för att kunnata bort svarsalternativ
.delete(`/competitions/${id}/slide/question/alternative/${alternative}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
const handleCloseAnswerClick = async (alternative_id: number) => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.delete(
`/competitions/${id}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const texts = useAppSelector(
......@@ -110,25 +126,100 @@ const SlideSettings: React.FC = () => {
}
const [pictures, setPictures] = useState(pictureList)
const updateSlideType = async (event: React.ChangeEvent<{ value: unknown }>) => {
await axios
// TODO: implementera API för att kunna ändra i questions->type_id
.put(`/competitions/${id}/slides/${currentSlide?.id}`, { type_id: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
const updateSlideType = async () => {
closeSlideTypeDialog()
if (activeSlide) {
if (activeSlide.questions[0] && activeSlide.questions[0].type_id !== selectedSlideType) {
if (selectedSlideType === 0) {
// Change slide type from a question type to information
await axios
.delete(`/api/competitions/${id}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
} else {
// Change slide type from question type to another question type
await axios
.delete(`/api/competitions/${id}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}`)
.catch(console.log)
await axios
.post(`/api/competitions/${id}/slides/${activeSlide.id}/questions`, {
name: 'Ny fråga',
total_score: 0,
type_id: selectedSlideType,
slide_id: activeSlide.id,
})
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
} else if (selectedSlideType !== 0) {
// Change slide type from information to a question type
await axios
.post(`/api/competitions/${id}/slides/${activeSlide.id}/questions`, {
name: 'Ny fråga',
total_score: 0,
type_id: selectedSlideType,
slide_id: activeSlide.id,
})
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
}
const updateAlternativeValue = async (event: React.ChangeEvent<HTMLInputElement>) => {
// Wheter the alternative is true or false
await axios
// TODO: implementera API för att kunna ändra i alternatives->value
.put(`/competitions/${id}/slides/${currentSlide?.id}`, { value: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
const updateAlternativeValue = async (alternative: QuestionAlternative) => {
if (activeSlide && activeSlide.questions[0]) {
let newValue: number
if (alternative.value === 0) {
newValue = 1
} else newValue = 0
console.log('newValue: ' + newValue)
await axios
.put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`,
{ value: newValue }
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const updateAlternativeText = async (alternative_id: number, newText: string) => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`,
{ text: newText }
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const addAlternative = async () => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.post(
`/api/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives`,
{
text: '',
value: 0,
}
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
......@@ -148,9 +239,15 @@ const SlideSettings: React.FC = () => {
}
const handleAddText = async () => {
console.log('Add text component')
// TODO: post the new text]
// setTexts([...texts, { id: 'newText', name: 'New Text' }])
if (activeSlide) {
await axios.post(`/api/competitions/${id}/slides/${activeSlide?.id}/components`, {
type_id: 1,
data: { text: 'Ny text' },
w: 315,
h: 50,
})
dispatch(getEditorCompetition(id))
}
}
const GreenCheckbox = withStyles({
......@@ -163,27 +260,86 @@ const SlideSettings: React.FC = () => {
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setTimer(+event.target.value)
if (activeSlide) {
await axios
.put(`/api/competitions/${id}/slides/${activeSlide.id}`, { timer: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const [timer, setTimer] = useState<number | undefined>(0)
useEffect(() => {
setTimer(activeSlide?.timer)
}, [activeSlide])
// For "slide type" dialog
const [selectedSlideType, setSelectedSlideType] = useState(0)
const [slideTypeDialog, setSlideTypeDialog] = useState(false)
const openSlideTypeDialog = (type_id: number) => {
setSelectedSlideType(type_id)
setSlideTypeDialog(true)
}
const closeSlideTypeDialog = () => {
setSlideTypeDialog(false)
}
const numberToBool = (num: number) => {
if (num === 0) return false
else return true
}
return (
<div className={classes.textInputContainer}>
<div className={classes.whiteBackground}>
<FormControl variant="outlined" className={classes.dropDown}>
<InputLabel id="slide-type-selection-label">Sidtyp</InputLabel>
<Select value={currentSlide?.questions[0].type_id || 0} label="Sidtyp" onChange={updateSlideType}>
<InputLabel>Sidtyp</InputLabel>
<Select value={activeSlide?.questions[0]?.type_id || 0} label="Sidtyp" className={classes.panelList}>
<MenuItem value={0}>
<Button>Informationssida</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(0)}>
Informationssida
</Typography>
</MenuItem>
<MenuItem value={1}>
<Button>Skriftlig fråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(1)}>
Skriftlig fråga
</Typography>
</MenuItem>
<MenuItem value={2}>
<Button>Praktisk fråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(2)}>
Praktisk fråga
</Typography>
</MenuItem>
<MenuItem value={3}>
<Button>Flervalsfråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(3)}>
Flervalsfråga
</Typography>
</MenuItem>
</Select>
</FormControl>
</div>
<Dialog open={slideTypeDialog} onClose={closeSlideTypeDialog}>
<DialogTitle className={classes.center} color="secondary">
Varning!
</DialogTitle>
<DialogContent>
<DialogContentText>
Om du ändrar sidtypen kommer eventuella frågeinställningar gå förlorade. Det inkluderar: frågans namn, poäng
och svarsalternativ.{' '}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeSlideTypeDialog} color="secondary">
Avbryt
</Button>
<Button onClick={updateSlideType} color="primary">
Bekräfta
</Button>
</DialogActions>
</Dialog>
<ListItem>
<TextField
......@@ -193,11 +349,12 @@ const SlideSettings: React.FC = () => {
helperText="Lämna blank för att inte använda timerfunktionen"
label="Timer"
type="number"
value={currentSlide?.timer}
onChange={updateTimer}
value={timer || ''}
/>
</ListItem>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText
className={classes.textCenter}
......@@ -205,49 +362,52 @@ const SlideSettings: React.FC = () => {
secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)"
/>
</ListItem>
{currentSlide &&
currentSlide.questions[0] &&
currentSlide.questions[0].question_alternatives &&
currentSlide.questions[0].question_alternatives.map((alt) => (
{activeSlide &&
activeSlide.questions[0] &&
activeSlide.questions[0].alternatives &&
activeSlide.questions[0].alternatives.map((alt) => (
<div key={alt.id}>
<ListItem divider>
<TextField
className={classes.textInput}
id="outlined-basic"
label={`Svar ${alt.id}`}
value={alt.text}
defaultValue={alt.text}
onChange={(event) => updateAlternativeText(alt.id, event.target.value)}
variant="outlined"
/>
<GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} />
<GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} />
<CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} />
</ListItem>
</div>
))}
<ListItem className={classes.center} button>
<Button>Lägg till svarsalternativ</Button>
<ListItem className={classes.center} button onClick={addAlternative}>
<Typography className={classes.addButtons} variant="button">
Lägg till svarsalternativ
</Typography>
</ListItem>
</List>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText className={classes.textCenter} primary="Text" />
</ListItem>
{texts &&
texts.map((text) => (
<div key={text.id}>
<ListItem divider>
<TextField className={classes.textInput} label={text.data.text} variant="outlined" />
<MoreHorizOutlinedIcon className={classes.clickableIcon} />
<CloseIcon className={classes.clickableIcon} />
</ListItem>
</div>
<TextCard elevation={4} key={text.id}>
<TextComponentEdit component={text} />
<Divider />
</TextCard>
))}
<ListItem className={classes.center} button onClick={handleAddText}>
<Button>Lägg till text</Button>
<Typography className={classes.addButtons} variant="button">
Lägg till text
</Typography>
</ListItem>
</List>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText className={classes.textCenter} primary="Bilder" />
</ListItem>
......@@ -267,8 +427,8 @@ const SlideSettings: React.FC = () => {
<ListItem className={classes.center} button>
<HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} />
<label htmlFor="contained-button-file">
<Button component="span">Lägg till bild</Button>
<label className={classes.addImageButton} htmlFor="contained-button-file">
<Typography variant="button">Lägg till bild</Typography>
</label>
</ListItem>
</List>
......
import { Editor } from '@tinymce/tinymce-react'
import { mount } from 'enzyme'
import React from 'react'
import { Provider } from 'react-redux'
import store from '../../../store'
import TextComponentDisplay from './TextComponentDisplay'
it('renders text component display', () => {
const testText = 'TEST'
const container = mount(
<Provider store={store}>
<TextComponentDisplay
component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { text: testText, font: '123123' }, type_id: 2 }}
/>
</Provider>
)
expect(container.find(Editor).prop('initialValue')).toBe(testText)
})
import { Editor } from '@tinymce/tinymce-react'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { TextComponent } from '../../../interfaces/ApiModels'
import { DeleteTextButton } from './styled'
type ImageComponentProps = {
component: TextComponent
}
interface CompetitionParams {
id: string
}
const TextComponentEdit = ({ component }: ImageComponentProps) => {
const { id }: CompetitionParams = useParams()
const competitionId = useAppSelector((state) => state.editor.competition.id)
const [content, setContent] = useState('')
const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined)
const activeSlideId = useAppSelector((state) => state.editor.activeSlideId)
const dispatch = useAppDispatch()
useEffect(() => {
setContent(component.data.text)
}, [])
const handleSaveText = async (a: string) => {
setContent(a)
if (timerHandle) {
clearTimeout(timerHandle)
setTimerHandle(undefined)
}
//Only updates 250ms after last input was made to not spam
setTimerHandle(
window.setTimeout(async () => {
console.log('Content was updated on server. id: ', component.id)
await axios.put(`/api/competitions/${competitionId}/slides/${activeSlideId}/components/${component.id}`, {
data: { ...component.data, text: a },
})
dispatch(getEditorCompetition(id))
}, 250)
)
}
const handleDeleteText = async (componentId: number) => {
await axios.delete(`/api/competitions/${id}/slides/${activeSlideId}/components/${componentId}`)
dispatch(getEditorCompetition(id))
}
return (
<div style={{ minHeight: '300px', height: '100%', width: '100%' }}>
<Editor
value={content || ''}
init={{
height: '300px',
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount',
],
toolbar:
'undo redo save | fontselect | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help',
}}
onEditorChange={(a, e) => handleSaveText(a)}
/>
<DeleteTextButton variant="contained" color="secondary" onClick={() => handleDeleteText(component.id)}>
Ta bort
</DeleteTextButton>
</div>
)
}
export default TextComponentEdit
import { Tab } from '@material-ui/core'
import { Button, Card, Tab } from '@material-ui/core'
import styled from 'styled-components'
export const SettingsTab = styled(Tab)`
......@@ -44,3 +44,23 @@ export const ToolbarPadding = styled.div`
height: 0;
padding-top: 55px;
`
export const TextCard = styled(Card)`
margin-bottom: 15px;
margin-top: 10px;
`
export const DeleteTextButton = styled(Button)`
width: 100%;
margin-bottom: 7px;
`
interface TextComponentContainerProps {
hover: boolean
}
export const TextComponentContainer = styled.div<TextComponentContainerProps>`
height: 100%;
width: 100%;
border: solid ${(props) => (props.hover ? 1 : 0)}px;
`
import React from 'react'
import SlideDisplay from './components/SlideDisplay'
const AudienceViewPage: React.FC = () => {
return <div>Publik</div>
return <SlideDisplay />
}
export default AudienceViewPage
import { Divider, List, ListItemText } from '@material-ui/core'
import { Divider, List, ListItemText, Typography } 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'
......@@ -10,6 +10,7 @@ import {
} from '../../actions/presentation'
import { useAppDispatch, useAppSelector } from '../../hooks'
import { ViewParams } from '../../interfaces/ViewParams'
import { socket_connect } from '../../sockets'
import { SlideListItem } from '../presentationEditor/styled'
import JudgeScoreDisplay from './components/JudgeScoreDisplay'
import SlideDisplay from './components/SlideDisplay'
......@@ -43,17 +44,20 @@ const JudgeViewPage: React.FC = () => {
const { id, code }: ViewParams = useParams()
const dispatch = useAppDispatch()
const [activeSlideIndex, setActiveSlideIndex] = useState<number>(0)
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)
const handleSelectSlide = (index: number) => {
setActiveSlideIndex(index)
dispatch(setCurrentSlide(slides[index]))
}
useEffect(() => {
socket_connect()
dispatch(getPresentationCompetition(id))
dispatch(getPresentationTeams(id))
dispatch(setPresentationCode(code))
}, [])
return (
<div>
<JudgeAppBar position="fixed">
......@@ -80,6 +84,7 @@ const JudgeViewPage: React.FC = () => {
button
key={slide.id}
>
<Typography variant="h6">Slide ID: {slide.id} </Typography>
<ListItemText primary={slide.title} />
</SlideListItem>
))}
......@@ -103,7 +108,6 @@ const JudgeViewPage: React.FC = () => {
))}
</List>
</RightDrawer>
aaa
<Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}>
<div className={classes.toolbar} />
<SlideDisplay />
......
import { List, ListItem, Popover } from '@material-ui/core'
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
Popover,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from '@material-ui/core'
import AssignmentIcon from '@material-ui/icons/Assignment'
import BackspaceIcon from '@material-ui/icons/Backspace'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import TimerIcon from '@material-ui/icons/Timer'
import React, { useEffect } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import { getPresentationCompetition, getPresentationTeams, setPresentationCode } from '../../actions/presentation'
import { useAppDispatch, useAppSelector } from '../../hooks'
import { ViewParams } from '../../interfaces/ViewParams'
import {
socketEndPresentation,
socketSetSlide,
socketSetSlideNext,
socketSetSlidePrev,
socketStartPresentation,
socketStartTimer,
socket_connect,
} from '../../sockets'
import SlideDisplay from './components/SlideDisplay'
import SocketTest from './components/SocketTest'
import Timer from './components/Timer'
import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled'
import {
PresenterButton,
PresenterContainer,
PresenterFooter,
PresenterHeader,
SlideCounter,
ToolBarContainer,
} from './styled'
/**
* Presentation is an active competition
*/
const PresenterViewPage: React.FC = () => {
// for dialog alert
const [openAlert, setOpen] = React.useState(false)
const theme = useTheme()
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'))
const teams = useAppSelector((state) => state.presentation.teams)
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
const { id, code }: ViewParams = useParams()
const presentation = useAppSelector((state) => state.presentation)
const history = useHistory()
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(getPresentationCompetition(id))
dispatch(getPresentationTeams(id))
dispatch(setPresentationCode(code))
socket_connect()
socketSetSlide // Behövs denna?
setTimeout(startCompetition, 500) // Ghetto, wait for everything to load
// console.log(id)
}, [])
const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setOpen(false)
setAnchorEl(null)
}
const handleNextSlidePressed = () => {
// dispatch(setCurrentSlideNext())
// syncSlide()
const startCompetition = () => {
socketStartPresentation()
console.log('started competition for')
console.log(id)
}
const handleVerifyExit = () => {
setOpen(true)
}
const handlePreviousSlidePressed = () => {
// dispatch(setCurrentSlidePrevious())
// syncSlide()
const endCompetition = () => {
setOpen(false)
socketEndPresentation()
history.push('/admin/tävlingshanterare')
window.location.reload(false) // TODO: fix this ugly hack, we "need" to refresh site to be able to run the competition correctly again
}
return (
<PresenterContainer>
<PresenterHeader>
<PresenterButton onClick={handleOpenPopover} color="primary" variant="contained">
Visa ställning
</PresenterButton>
<PresenterButton onClick={() => history.push('/admin')} variant="contained" color="secondary">
Avsluta tävling
</PresenterButton>
<Tooltip title="Avsluta tävling" arrow>
<PresenterButton onClick={handleVerifyExit} variant="contained" color="secondary">
<BackspaceIcon fontSize="large" />
</PresenterButton>
</Tooltip>
<Dialog
fullScreen={fullScreen}
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>
<Typography variant="h3">{presentation.competition.name}</Typography>
<SlideCounter>
<Typography variant="h3">
{presentation.slide.order + 1} / {presentation.competition.slides.length}
</Typography>
</SlideCounter>
</PresenterHeader>
<SlideDisplay />
<PresenterFooter>
<PresenterButton onClick={handlePreviousSlidePressed} variant="contained">
<ChevronRightIcon fontSize="large" />
</PresenterButton>
<SocketTest></SocketTest>
<Timer></Timer>
<PresenterButton onClick={handleNextSlidePressed} variant="contained">
<ChevronRightIcon fontSize="large" />
</PresenterButton>
<ToolBarContainer>
<Tooltip title="Previous Slide" arrow>
<PresenterButton onClick={socketSetSlidePrev} variant="contained">
<ChevronLeftIcon fontSize="large" />
</PresenterButton>
</Tooltip>
{/*
// Manual start button
<Tooltip title="Start Presentation" arrow>
<PresenterButton onClick={startCompetition} variant="contained">
start
</PresenterButton>
</Tooltip>
// This creates a join button, but presenter should not join others, others should join presenter
<Tooltip title="Join Presentation" arrow>
<PresenterButton onClick={socketJoinPresentation} variant="contained">
<GroupAddIcon fontSize="large" />
</PresenterButton>
</Tooltip>
// This creates another end button, it might not be needed since we already have one
<Tooltip title="End Presentation" arrow>
<PresenterButton onClick={socketEndPresentation} variant="contained">
<CancelIcon fontSize="large" />
</PresenterButton>
</Tooltip>
*/}
<Tooltip title="Start Timer" arrow>
<PresenterButton onClick={socketStartTimer} variant="contained">
<TimerIcon fontSize="large" />
<Timer></Timer>
</PresenterButton>
</Tooltip>
<Tooltip title="Scoreboard" arrow>
<PresenterButton onClick={handleOpenPopover} variant="contained">
<AssignmentIcon fontSize="large" />
</PresenterButton>
</Tooltip>
<Tooltip title="Next Slide" arrow>
<PresenterButton onClick={socketSetSlideNext} variant="contained">
<ChevronRightIcon fontSize="large" />
</PresenterButton>
</Tooltip>
</ToolBarContainer>
</PresenterFooter>
<Popover
open={Boolean(anchorEl)}
......@@ -71,6 +201,9 @@ const PresenterViewPage: React.FC = () => {
}}
>
<List>
{/** TODO:
* Fix scoreboard
*/}
{teams.map((team) => (
<ListItem key={team.id}>{team.name} score: 20</ListItem>
))}
......@@ -81,3 +214,6 @@ const PresenterViewPage: React.FC = () => {
}
export default PresenterViewPage
function componentDidMount() {
throw new Error('Function not implemented.')
}
......@@ -5,10 +5,15 @@ import { SlideContainer } from './styled'
const SlideDisplay: React.FC = () => {
const currentSlide = useAppSelector((state) => state.presentation.slide)
return (
<SlideContainer>
<Typography variant="h3">{currentSlide.title}</Typography>
</SlideContainer>
<div>
<SlideContainer>
<Typography variant="h3">Slide Title: {currentSlide.title} </Typography>
<Typography variant="h3">Timer: {currentSlide.timer} </Typography>
<Typography variant="h3">Slide ID: {currentSlide.id} </Typography>
</SlideContainer>
</div>
)
}
......
......@@ -38,8 +38,7 @@ const Timer: React.FC = (props: any) => {
return (
<>
<div>Timer: {props.timer.value}</div>
<div>Enabled: {props.timer.enabled.toString()}</div>
<div>{props.timer.value}</div>
</>
)
}
......
......@@ -3,7 +3,14 @@ import styled from 'styled-components'
export const SlideContainer = styled.div`
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: auto;
margin-top: 5%;
justify-content: center;
background-color: grey;
width: 1280px;
height: 720px;
`
export const ScoreDisplayContainer = styled.div`
......
......@@ -50,8 +50,15 @@ export const PresenterFooter = styled.div`
export const PresenterButton = styled(Button)`
width: 100px;
height: 100px;
padding-top: 16px;
padding-bottom: 16px;
margin-left: 16px;
margin-right: 16px;
margin-top: 16px;
`
export const SlideCounter = styled(Button)`
margin-left: 16px;
margin-right: 16px;
margin-top: 16px;
`
export const PresenterContainer = styled.div`
......@@ -61,6 +68,18 @@ export const PresenterContainer = styled.div`
height: 100%;
`
export const ToolBarContainer = styled.div`
align-self: center;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 100%;
width: auto;
margin-right: auto;
margin-left: auto;
margin-bottom: 20px;
`
interface DrawerProps {
width: number
}
......
......@@ -17,7 +17,7 @@ const initialState: EditorState = {
slides: [],
teams: [],
},
activeSlideId: 0,
activeSlideId: -1,
loading: true,
}
......@@ -25,8 +25,8 @@ export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_EDITOR_COMPETITION:
return {
...state,
competition: action.payload as RichCompetition,
activeSlideId: action.payload.slides[0].id as number,
loading: false,
}
case Types.SET_EDITOR_SLIDE_ID:
......
......@@ -38,22 +38,27 @@ export const socket_connect = () => {
export const socketStartPresentation = () => {
socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
console.log('START PRESENTATION')
}
export const socketJoinPresentation = () => {
socket.emit('join_presentation', { code: 'OEM1V4' }) // TODO: Send code gotten from auth/login/<code> api call
socket.emit('join_presentation', { code: 'CO0ART' }) // TODO: Send code gotten from auth/login/<code> api call
console.log('JOIN PRESENTATION')
}
export const socketEndPresentation = () => {
socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
console.log('END PRESENTATION')
}
export const socketSetSlideNext = () => {
socketSetSlide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists
console.log('NEXT SLIDE +1')
}
export const socketSetSlidePrev = () => {
socketSetSlide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists
console.log('PREVIOUS SLIDE -1')
}
export const socketSetSlide = (slide_order: number) => {
......@@ -69,6 +74,7 @@ export const socketSetSlide = (slide_order: number) => {
}
export const socketSetTimer = (timer: Timer) => {
console.log('SET TIMER')
socket.emit('set_timer', {
competition_id: store.getState().presentation.competition.id,
timer: timer,
......@@ -76,5 +82,6 @@ export const socketSetTimer = (timer: Timer) => {
}
export const socketStartTimer = () => {
console.log('START TIMER')
socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value })
}
......@@ -16,7 +16,7 @@ export const CheckAuthentication = async () => {
axios.defaults.headers.common['Authorization'] = authToken
store.dispatch({ type: Types.LOADING_USER })
await axios
.get('/users')
.get('/api/users')
.then((res) => {
store.dispatch({ type: Types.SET_AUTHENTICATED })
store.dispatch({
......