diff --git a/client/src/pages/presentationEditor/components/Alternatives.tsx b/client/src/pages/presentationEditor/components/Alternatives.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e699d003f7f3433e2caf25a26185ad5141c6f694 --- /dev/null +++ b/client/src/pages/presentationEditor/components/Alternatives.tsx @@ -0,0 +1,136 @@ +import { Checkbox, ListItem, ListItemText, withStyles } from '@material-ui/core' +import { CheckboxProps } from '@material-ui/core/Checkbox' +import { green, grey } from '@material-ui/core/colors' +import CloseIcon from '@material-ui/icons/Close' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { QuestionAlternative } from '../../../interfaces/ApiModels' +import { RichSlide } from '../../../interfaces/ApiRichModels' +import { AddButton, Center, Clickable, SettingsList, TextInput, WhiteBackground } from './styled' + +type AlternativeProps = { + activeSlide: RichSlide + competitionId: string +} + +const Alternatives = ({ activeSlide, competitionId }: AlternativeProps) => { + const dispatch = useAppDispatch() + const competition = useAppSelector((state) => state.editor.competition) + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) + const GreenCheckbox = withStyles({ + root: { + color: grey[900], + '&$checked': { + color: green[600], + }, + }, + checked: {}, + })((props: CheckboxProps) => <Checkbox color="default" {...props} />) + + const numberToBool = (num: number) => { + if (num === 0) return false + else return true + } + + const updateAlternativeValue = async (alternative: QuestionAlternative) => { + if (activeSlide && activeSlide.questions[0]) { + let newValue: number + if (alternative.value === 0) { + newValue = 1 + } else newValue = 0 + await axios + .put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`, + { value: newValue } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const updateAlternativeText = async (alternative_id: number, newText: string) => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`, + { text: newText } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const addAlternative = async () => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .post( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives`, + { text: '', value: 0 } + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + const handleCloseAnswerClick = async (alternative_id: number) => { + if (activeSlide && activeSlide.questions[0]) { + await axios + .delete( + `/api/competitions/${competitionId}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}` + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + + return ( + <SettingsList> + <WhiteBackground> + <ListItem divider> + <Center> + <ListItemText + primary="Svarsalternativ" + secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" + /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + <TextInput + id="outlined-basic" + defaultValue={alt.text} + onChange={(event) => updateAlternativeText(alt.id, event.target.value)} + variant="outlined" + /> + <GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} /> + <Clickable> + <CloseIcon onClick={() => handleCloseAnswerClick(alt.id)} /> + </Clickable> + </ListItem> + </div> + ))} + <ListItem button onClick={addAlternative}> + <Center> + <AddButton variant="button">Lägg till svarsalternativ</AddButton> + </Center> + </ListItem> + </WhiteBackground> + </SettingsList> + ) +} + +export default Alternatives diff --git a/client/src/pages/presentationEditor/components/Images.tsx b/client/src/pages/presentationEditor/components/Images.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ac739bf440b56492009316b093f6e18e63ca988 --- /dev/null +++ b/client/src/pages/presentationEditor/components/Images.tsx @@ -0,0 +1,137 @@ +import { ListItem, ListItemText, Typography } from '@material-ui/core' +import CloseIcon from '@material-ui/icons/Close' +import React, { useState } from 'react' +import { useDispatch } from 'react-redux' +import { + Center, + HiddenInput, + SettingsList, + AddImageButton, + ImportedImage, + WhiteBackground, + AddButton, + Clickable, + NoPadding, +} from './styled' +import axios from 'axios' +import { getEditorCompetition } from '../../../actions/editor' +import { RichSlide } from '../../../interfaces/ApiRichModels' +import { ImageComponent, Media } from '../../../interfaces/ApiModels' +import { useAppSelector } from '../../../hooks' + +type ImagesProps = { + activeSlide: RichSlide + competitionId: string +} + +const Images = ({ activeSlide, competitionId }: ImagesProps) => { + const pictureList = [ + { id: 'picture1', name: 'Picture1.jpeg' }, + { id: 'picture2', name: 'Picture2.jpeg' }, + ] + const handleClosePictureClick = (id: string) => { + setPictures(pictures.filter((item) => item.id !== id)) //Will not be done like this when api is used + } + const [pictures, setPictures] = useState(pictureList) + + const dispatch = useDispatch() + + const uploadFile = async (formData: FormData) => { + // Uploads the file to the server and creates a Media object in database + // Returns media id + return await axios + .post(`/api/media/images`, formData) + .then((response) => { + dispatch(getEditorCompetition(competitionId)) + return response.data as Media + }) + .catch(console.log) + } + + const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.target.files !== null && e.target.files[0]) { + const files = Array.from(e.target.files) + const file = files[0] + const formData = new FormData() + formData.append('image', file) + const response = await uploadFile(formData) + if (response) { + const newComponent = createImageComponent(response) + } + } + } + + const createImageComponent = async (media: Media) => { + const imageData = { + x: 0, + y: 0, + data: { + media_id: media.id, + filename: media.filename, + }, + type_id: 2, + } + await axios + .post(`/api/competitions/${competitionId}/slides/${activeSlide?.id}/components`, imageData) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const handleCloseimageClick = async (image: ImageComponent) => { + await axios + .delete(`/api/media/images/${image.data.media_id}`) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + + await axios + .delete(`/api/competitions/${competitionId}/slides/${activeSlide?.id}/components/${image.id}`) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const images = useAppSelector( + (state) => + state.editor.competition.slides + .find((slide) => slide.id === state.editor.activeSlideId) + ?.components.filter((component) => component.type_id === 2) as ImageComponent[] + ) + + return ( + <SettingsList> + <WhiteBackground> + <ListItem divider> + <Center> + <ListItemText primary="Bilder" /> + </Center> + </ListItem> + {images && + images.map((image) => ( + <div key={image.id}> + <ListItem divider button> + <ImportedImage src={`http://localhost:5000/static/images/thumbnail_${image.data.filename}`} /> + <Center> + <ListItemText primary={image.data.filename} /> + </Center> + <CloseIcon onClick={() => handleCloseimageClick(image)} /> + </ListItem> + </div> + ))} + + <ListItem button> + <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} /> + <AddImageButton htmlFor="contained-button-file"> + <AddButton variant="button">Lägg till bild</AddButton> + </AddImageButton> + </ListItem> + </WhiteBackground> + </SettingsList> + ) +} + +export default Images diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index ed8170ff17718ad5cd76307648194cf396526724..02344a36aa223b245fe7170c9186ffa8b509e6dc 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -40,7 +40,9 @@ const RndComponent = ({ component }: ImageComponentProps) => { return ( <TextComponentContainer hover={hover} - dangerouslySetInnerHTML={{ __html: (component as TextComponent).data.text }} + dangerouslySetInnerHTML={{ + __html: `<div style="font-size: 24px;"> ${(component as TextComponent).data.text} </div>`, + }} /> ) case ComponentTypes.Image: diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index ffa95333cbce25364f5fb0bd3abd80adeb298a9f..557e4809aa0f4aa5f1ff76d5d2e5dcd17bd16174 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -1,490 +1,51 @@ -import { - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Divider, - FormControl, - InputLabel, - List, - ListItem, - ListItemText, - 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 axios from 'axios' -import { ImageComponent, TextComponent, QuestionAlternative, Media } from '../../../interfaces/ApiModels' -import React, { useEffect, useState } from 'react' +import { Divider, List, ListItem, ListItemText, TextField, Typography } from '@material-ui/core' +import React, { useState } from 'react' import { useParams } from 'react-router-dom' -import { getEditorCompetition } from '../../../actions/editor' -import { useAppDispatch, useAppSelector } from '../../../hooks' -import { HiddenInput, TextCard } from './styled' -import TextComponentEdit from './TextComponentEdit' - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - textInputContainer: { - '& > *': { - margin: theme.spacing(1), - width: '100%', - background: 'white', - }, - }, - textInput: { - margin: theme.spacing(2), - width: '87%', - background: 'white', - }, - textCenter: { - textAlign: 'center', - }, - center: { - display: 'flex', - justifyContent: 'center', - background: 'white', - }, - dropDown: { - margin: theme.spacing(2), - width: '87%', - background: 'white', - padding: 0, - }, - clickableIcon: { - cursor: 'pointer', - background: 'white', - }, - importedImage: { - width: 70, - height: 50, - background: 'white', - }, - whiteBackground: { - background: 'white', - }, - addButtons: { - padding: 5, - }, - panelList: { - padding: 0, - }, - addImageButton: { - padding: 5, - cursor: 'pointer', - }, - }) -) +import { useAppSelector } from '../../../hooks' +import Alternatives from './Alternatives' +import SlideType from './SlideType' +import { Center, ImportedImage, SettingsList, SlidePanel } from './styled' +import Timer from './Timer' +import Images from './Images' +import Texts from './Texts' interface CompetitionParams { id: string } const SlideSettings: React.FC = () => { - const classes = useStyles() const { id }: CompetitionParams = useParams() - const dispatch = useAppDispatch() - const competition = useAppSelector((state) => state.editor.competition) - 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_id: number) => { - if (activeSlide && activeSlide.questions[0]) { - await axios - .delete( - `/api/competitions/${id}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}` - ) - .then(() => { - dispatch(getEditorCompetition(id)) - }) - .catch(console.log) - } - } - - const texts = useAppSelector( - (state) => - state.editor.competition.slides - .find((slide) => slide.id === state.editor.activeSlideId) - ?.components.filter((component) => component.type_id === 1) as TextComponent[] - ) - - const images = useAppSelector( - (state) => - state.editor.competition.slides - .find((slide) => slide.id === state.editor.activeSlideId) - ?.components.filter((component) => component.type_id === 2) as ImageComponent[] - ) - - const handleCloseimageClick = async (image: ImageComponent) => { - await axios - .delete(`/api/media/images/${image.data.media_id}`) - .then(() => { - dispatch(getEditorCompetition(id)) - }) - .catch(console.log) - - await axios - .delete(`/api/competitions/${id}/slides/${activeSlide?.id}/components/${image.id}`) - .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 (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( - `/api/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( - `/api/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 uploadFile = async (formData: FormData) => { - // Uploads the file to the server and creates a Media object in database - // Returns media id - return await axios - .post(`/api/media/images`, formData) - .then((response) => { - dispatch(getEditorCompetition(id)) - return response.data as Media - }) - .catch(console.log) - } - - const createImageComponent = async (media: Media) => { - const imageData = { - x: 0, - y: 0, - data: { - media_id: media.id, - filename: media.filename, - }, - type_id: 2, - } - - await axios - .post(`/api/competitions/${id}/slides/${activeSlide?.id}/components`, imageData) - .then(() => { - dispatch(getEditorCompetition(id)) - }) - .catch(console.log) - } - - const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => { - if (e.target.files !== null && e.target.files[0]) { - const files = Array.from(e.target.files) - const file = files[0] - const formData = new FormData() - formData.append('image', file) - const response = await uploadFile(formData) - - if (response) { - const newComponent = createImageComponent(response) - } - } - } - - const handleAddText = async () => { - 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({ - root: { - color: grey[900], - '&$checked': { - color: green[600], - }, - }, - 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>Sidtyp</InputLabel> - <Select value={activeSlide?.questions[0]?.type_id || 0} label="Sidtyp" className={classes.panelList}> - <MenuItem value={0}> - <Typography variant="button" onClick={() => openSlideTypeDialog(0)}> - Informationssida - </Typography> - </MenuItem> - <MenuItem value={1}> - <Typography variant="button" onClick={() => openSlideTypeDialog(1)}> - Skriftlig fråga - </Typography> - </MenuItem> - <MenuItem value={2}> - <Typography variant="button" onClick={() => openSlideTypeDialog(2)}> - Praktisk fråga - </Typography> - </MenuItem> - <MenuItem value={3}> - <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> + <SlidePanel> + <SettingsList> + {activeSlide && <SlideType activeSlide={activeSlide} competitionId={id} />} + <Divider /> + {activeSlide && <Timer activeSlide={activeSlide} competitionId={id} />} + </SettingsList> - <ListItem> - <TextField - id="standard-number" - variant="outlined" - placeholder="Antal sekunder" - helperText="Lämna blank för att inte använda timerfunktionen" - label="Timer" - type="number" - onChange={updateTimer} - value={timer || ''} - /> - </ListItem> + {activeSlide && <Alternatives activeSlide={activeSlide} competitionId={id} />} - <List className={classes.panelList}> - <ListItem divider> - <ListItemText - className={classes.textCenter} - primary="Svarsalternativ" - secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" - /> - </ListItem> - {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" - defaultValue={alt.text} - onChange={(event) => updateAlternativeText(alt.id, event.target.value)} - variant="outlined" - /> - <GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} /> - <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} /> - </ListItem> - </div> - ))} - <ListItem className={classes.center} button onClick={addAlternative}> - <Typography className={classes.addButtons} variant="button"> - Lägg till svarsalternativ - </Typography> - </ListItem> - </List> + {activeSlide && <Texts activeSlide={activeSlide} competitionId={id} />} - <List className={classes.panelList}> - <ListItem divider> - <ListItemText className={classes.textCenter} primary="Text" /> - </ListItem> - {texts && - texts.map((text) => ( - <TextCard elevation={4} key={text.id}> - <TextComponentEdit component={text} /> - - <Divider /> - </TextCard> - ))} - - <ListItem className={classes.center} button onClick={handleAddText}> - <Typography className={classes.addButtons} variant="button"> - Lägg till text - </Typography> - </ListItem> - </List> + {activeSlide && <Images activeSlide={activeSlide} competitionId={id} />} - <List className={classes.panelList}> - <ListItem divider> - <ListItemText className={classes.textCenter} primary="Bilder" /> - </ListItem> - {images && - images.map((image) => ( - <div key={image.id}> - <ListItem divider button> - <img - src={`http://localhost:5000/static/images/thumbnail_${image.data.filename}`} - className={classes.importedImage} - /> - <ListItemText className={classes.textCenter} primary={image.data.filename} /> - <CloseIcon onClick={() => handleCloseimageClick(image)} /> - </ListItem> - </div> - ))} - <ListItem className={classes.center} button> - <HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} /> - - <label className={classes.addImageButton} htmlFor="contained-button-file"> - <Typography variant="button">Lägg till bild</Typography> - </label> + <SettingsList> + <ListItem button> + <ImportedImage + id="temp source, todo: add image source to elements of pictureList" + src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" + /> + <Center> + <ListItemText>Välj bakgrundsbild ...</ListItemText> + </Center> </ListItem> - </List> - - <ListItem button> - <img - id="temp source, todo: add image source to elements of imageList" - src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" - className={classes.importedImage} - /> - <ListItemText className={classes.textCenter}>Välj bakgrundsbild ...</ListItemText> - </ListItem> - </div> + </SettingsList> + </SlidePanel> ) } diff --git a/client/src/pages/presentationEditor/components/SlideType.tsx b/client/src/pages/presentationEditor/components/SlideType.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ba104fe6ce7088d6aec076359d9f3f1361de3262 --- /dev/null +++ b/client/src/pages/presentationEditor/components/SlideType.tsx @@ -0,0 +1,137 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + InputLabel, + ListItem, + MenuItem, + Select, + Typography, +} from '@material-ui/core' +import axios from 'axios' +import React, { useState } from 'react' +import { getEditorCompetition } from '../../../actions/editor' +import { useAppDispatch } from '../../../hooks' +import { RichSlide } from '../../../interfaces/ApiRichModels' +import { Center, FormControlDropdown, SlideTypeInputLabel, WhiteBackground } from './styled' + +type SlideTypeProps = { + activeSlide: RichSlide + competitionId: string +} + +const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { + const dispatch = useAppDispatch() + + // 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 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/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}` + ) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } else { + // Change slide type from question type to another question type + await axios + .delete( + `/api/competitions/${competitionId}/slides/${activeSlide.id}/questions/${activeSlide.questions[0].id}` + ) + .catch(console.log) + await axios + .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions`, { + name: 'Ny fråga', + total_score: 0, + type_id: selectedSlideType, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } else if (selectedSlideType !== 0) { + // Change slide type from information to a question type + await axios + .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/questions`, { + name: 'Ny fråga', + total_score: 0, + type_id: selectedSlideType, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + } + return ( + <WhiteBackground> + <FormControlDropdown variant="outlined"> + <SlideTypeInputLabel>Sidtyp</SlideTypeInputLabel> + <Select fullWidth={true} value={activeSlide?.questions[0]?.type_id || 0} label="Sidtyp"> + <MenuItem value={0}> + <Typography variant="button" onClick={() => openSlideTypeDialog(0)}> + Informationssida + </Typography> + </MenuItem> + <MenuItem value={1}> + <Typography variant="button" onClick={() => openSlideTypeDialog(1)}> + Skriftlig fråga + </Typography> + </MenuItem> + <MenuItem value={2}> + <Typography variant="button" onClick={() => openSlideTypeDialog(2)}> + Praktisk fråga + </Typography> + </MenuItem> + <MenuItem value={3}> + <Typography variant="button" onClick={() => openSlideTypeDialog(3)}> + Flervalsfråga + </Typography> + </MenuItem> + </Select> + </FormControlDropdown> + <Dialog open={slideTypeDialog} onClose={closeSlideTypeDialog}> + <Center> + <DialogTitle color="secondary">Varning!</DialogTitle> + </Center> + <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> + </WhiteBackground> + ) +} + +export default SlideType diff --git a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx index c347bd5b6845c0413251907d514b31e825bd1e68..0a5c8522447c3111629ac179ce27532048d55c7a 100644 --- a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx @@ -57,15 +57,17 @@ const TextComponentEdit = ({ component }: ImageComponentProps) => { init={{ height: '300px', menubar: false, + fontsize_formats: '8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 30pt 36pt 48pt 60pt 72pt 96pt 120pt 144pt', + content_style: 'body {font-size: 24pt;}', 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', + 'fontsizeselect | bold italic backcolor | help | \ + fontselect | formatselect | undo redo | \ + alignleft aligncenter alignright alignjustify bullist numlist outdent indent | removeformat |', }} onEditorChange={(a, e) => handleSaveText(a)} /> diff --git a/client/src/pages/presentationEditor/components/Texts.tsx b/client/src/pages/presentationEditor/components/Texts.tsx new file mode 100644 index 0000000000000000000000000000000000000000..22cde214c92bb64788b4f69d9993977c85fe981d --- /dev/null +++ b/client/src/pages/presentationEditor/components/Texts.tsx @@ -0,0 +1,61 @@ +import { Divider, ListItem, ListItemText, Typography } from '@material-ui/core' +import React from 'react' +import { useAppSelector } from '../../../hooks' +import { TextComponent } from '../../../interfaces/ApiModels' +import { RichSlide } from '../../../interfaces/ApiRichModels' +import { AddButton, Center, SettingsList, TextCard } from './styled' +import TextComponentEdit from './TextComponentEdit' +import axios from 'axios' +import { getEditorCompetition } from '../../../actions/editor' +import { useDispatch } from 'react-redux' + +type TextsProps = { + activeSlide: RichSlide + competitionId: string +} + +const Texts = ({ activeSlide, competitionId }: TextsProps) => { + const texts = useAppSelector( + (state) => + state.editor.competition.slides + .find((slide) => slide.id === state.editor.activeSlideId) + ?.components.filter((component) => component.type_id === 1) as TextComponent[] + ) + + const dispatch = useDispatch() + const handleAddText = async () => { + if (activeSlide) { + await axios.post(`/api/competitions/${competitionId}/slides/${activeSlide?.id}/components`, { + type_id: 1, + data: { text: 'Ny text' }, + w: 315, + h: 50, + }) + dispatch(getEditorCompetition(competitionId)) + } + } + + return ( + <SettingsList> + <ListItem divider> + <Center> + <ListItemText primary="Text" /> + </Center> + </ListItem> + {texts && + texts.map((text) => ( + <TextCard elevation={4} key={text.id}> + <TextComponentEdit component={text} /> + <Divider /> + </TextCard> + ))} + <ListItem button onClick={handleAddText}> + <Center> + <AddButton variant="button">Lägg till text</AddButton> + </Center> + </ListItem> + </SettingsList> + ) +} + +export default Texts diff --git a/client/src/pages/presentationEditor/components/Timer.tsx b/client/src/pages/presentationEditor/components/Timer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..124635f423df9d0bfcca20a9a02309c646cb3dca --- /dev/null +++ b/client/src/pages/presentationEditor/components/Timer.tsx @@ -0,0 +1,53 @@ +import { ListItem, TextField } from '@material-ui/core' +import axios from 'axios' +import React, { useEffect, useState } from 'react' +import { getEditorCompetition } from '../../../actions/editor' +import { useAppDispatch } from '../../../hooks' +import { RichSlide } from '../../../interfaces/ApiRichModels' +import { Center, WhiteBackground } from './styled' + +type TimerProps = { + activeSlide: RichSlide + competitionId: string +} + +const Timer = ({ activeSlide, competitionId }: TimerProps) => { + const dispatch = useAppDispatch() + const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + setTimer(+event.target.value) + if (activeSlide) { + await axios + .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: event.target.value }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + const [timer, setTimer] = useState<number | undefined>(0) + useEffect(() => { + setTimer(activeSlide?.timer) + }, [activeSlide]) + return ( + <WhiteBackground> + <ListItem> + <Center> + <TextField + id="standard-number" + fullWidth={true} + variant="outlined" + placeholder="Antal sekunder" + helperText="Lämna blank för att inte använda timerfunktionen" + label="Timer" + type="number" + defaultValue={activeSlide?.timer || 0} + onChange={updateTimer} + value={timer} + /> + </Center> + </ListItem> + </WhiteBackground> + ) +} + +export default Timer diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 8bee7f0dc7291047771197af4d18f26c184f3f6c..a636d9d2d4d3f7faac3eef6166e5c07544c5a596 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -1,4 +1,15 @@ -import { Button, Card, Tab } from '@material-ui/core' +import { + FormControl, + List, + Tab, + TextField, + Typography, + Button, + Card, + ListItem, + Select, + InputLabel, +} from '@material-ui/core' import styled from 'styled-components' export const SettingsTab = styled(Tab)` @@ -45,6 +56,80 @@ export const ToolbarPadding = styled.div` padding-top: 55px; ` +export const FormControlDropdown = styled(FormControl)` + width: 100%; + margin-top: 10px; + padding: 8px; + padding-left: 16px; + padding-right: 16px; +` + +export const SlideTypeInputLabel = styled(InputLabel)` + width: 100%; + padding: 10px; + padding-left: 22px; +` + +export const TextInput = styled(TextField)` + width: 87%; +` + +export const NoPadding = styled.div` + padding: 0; + height: 100%; + width: 100%; +` + +export const Center = styled.div` + display: flex; + justify-content: center; + text-align: center; + height: 100%; + width: 100%; +` + +export const SlidePanel = styled.div` + padding: 10px; + width: 100%; +` + +export const WhiteBackground = styled.div` + background: white; +` + +export const AddButton = styled(Typography)` + padding-left: 8px; + padding-right: 8px; + padding-top: 7px; + padding-bottom: 7px; +` + +export const ImportedImage = styled.img` + width: 70px; + height: 50px; +` + +export const Clickable = styled.div` + cursor: pointer; +` + +export const AddImageButton = styled.label` + padding: 0; + cursor: 'pointer'; + display: flex; + justify-content: center; + text-align: center; + height: 100%; + width: 100%; + cursor: pointer; +` + +export const SettingsList = styled(List)` + margin-bottom: 10px; + padding: 0; + background: white; +` + export const TextCard = styled(Card)` margin-bottom: 15px; margin-top: 10px; @@ -62,5 +147,6 @@ interface TextComponentContainerProps { export const TextComponentContainer = styled.div<TextComponentContainerProps>` height: 100%; width: 100%; + padding: ${(props) => (props.hover ? 0 : 1)}px; border: solid ${(props) => (props.hover ? 1 : 0)}px; ` diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index eb0a77117a2df976d08c4e5d8f34e2f94db5f26f..57acb27ef28a17a19a7fa5a6b6f7de6229090e10 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -86,6 +86,12 @@ def slide_count(competition_id): return Slide.query.filter(Slide.competition_id == competition_id).count() +def slide_count(competition_id): + """ Gets the number of slides in the provided competition. """ + + return Slide.query.filter(Slide.competition_id == competition_id).count() + + ### Teams ### def team(competition_id, team_id): """ Gets the team object associated with the provided id and competition id. """ @@ -103,6 +109,8 @@ def team_list(competition_id): return Team.query.join(Competition, join_competition).filter(filters).all() + return Team.query.join(Competition, join_competition).filter(filters).all() + ### Questions ### def question(competition_id, slide_id, question_id): @@ -173,6 +181,14 @@ def question_alternative_list(competition_id, slide_id, question_id): .all() ) + return ( + QuestionAlternative.query.join(Competition, join_competition) + .join(Slide, join_slide) + .join(Question, join_question) + .filter(filters) + .all() + ) + ### Question Answers ### def question_answer(competition_id, team_id, answer_id):