diff --git a/client/package-lock.json b/client/package-lock.json index 72fcea719f2d7503990a778ded3a56f7b0e55e5f..2655ad2e5dbc1fb024ba896a3701f49adbfe399d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17285,7 +17285,8 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "requires": { "figgy-pudding": "^3.5.1" } diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts index c0aa738479a2c081ef0529bb6b4a317570b6841d..7ff75bd8d867762550dd9bcb6997038ac8d4b233 100644 --- a/client/src/enum/ComponentTypes.ts +++ b/client/src/enum/ComponentTypes.ts @@ -1,5 +1,5 @@ export enum ComponentTypes { Text = 1, Image, - QuestionAlternative, + Question, } diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 2734884b7783b662113dc00f61d112f8a58f5dd6..9bcd24c1b916ff925a9472dffd8b2fabd5395bfb 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -94,6 +94,16 @@ export interface TextComponent extends Component { font: string } -export interface QuestionAlternativeComponent extends Component { +export interface QuestionComponent extends Component { + id: number + x: number + y: number + w: number + h: number + slide_id: number + type_id: number + view_type_id: number + text: string + media: Media question_id: number } diff --git a/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dd8e2fa85a8e2562595c85c7068b0073f744092a --- /dev/null +++ b/client/src/pages/presentationEditor/components/QuestionComponentDisplay.tsx @@ -0,0 +1,84 @@ +import { Card, Divider, ListItem, Typography } from '@material-ui/core' +import React from 'react' +import { useAppSelector } from '../../../hooks' +import AnswerMultiple from './answerComponents/AnswerMultiple' +import AnswerSingle from './answerComponents/AnswerSingle' +import AnswerText from './answerComponents/AnswerText' +import { Center } from './styled' + +type QuestionComponentProps = { + variant: 'editor' | 'presentation' +} + +const QuestionComponentDisplay = ({ variant }: QuestionComponentProps) => { + const activeSlide = useAppSelector((state) => { + if (variant === 'editor') + return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId) + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id) + }) + + const timer = activeSlide?.timer + const total_score = activeSlide?.questions[0].total_score + const questionName = activeSlide?.questions[0].name + + const questionTypeId = activeSlide?.questions[0].type_id + const questionTypeName = useAppSelector( + (state) => state.types.questionTypes.find((qType) => qType.id === questionTypeId)?.name + ) + + const getAlternatives = () => { + switch (questionTypeName) { + case 'Text': + if (activeSlide) { + return <AnswerText activeSlide={activeSlide} competitionId={activeSlide.competition_id.toString()} /> + } + return + + case 'Practical': + return + + case 'Multiple': + if (activeSlide) { + return ( + <AnswerMultiple + variant={variant} + activeSlide={activeSlide} + competitionId={activeSlide.competition_id.toString()} + /> + ) + } + return + + case 'Single': + if (activeSlide) { + return ( + <AnswerSingle + variant={variant} + activeSlide={activeSlide} + competitionId={activeSlide.competition_id.toString()} + /> + ) + } + return + + default: + break + } + } + + return ( + <Card style={{ maxHeight: '100%', overflowY: 'auto' }}> + <ListItem> + <Center style={{ justifyContent: 'space-evenly' }}> + <Typography>Poäng: {total_score}</Typography> + <Typography>{questionName}</Typography> + <Typography>Timer: {timer}</Typography> + </Center> + </ListItem> + <Divider /> + {getAlternatives()} + </Card> + ) +} + +export default QuestionComponentDisplay diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index a134ee14c9ac268159b6ae0013350012e638dc65..4c09280042bd038fee1e3adc4b5bf3b7aef8ef6a 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -9,6 +9,7 @@ import { Component, ImageComponent, TextComponent } from '../../../interfaces/Ap import { Position, Size } from '../../../interfaces/Components' import { RemoveMenuItem } from '../../admin/styledComp' import ImageComponentDisplay from './ImageComponentDisplay' +import QuestionComponentDisplay from './QuestionComponentDisplay' import { HoverContainer } from './styled' import TextComponentDisplay from './TextComponentDisplay' //import NestedMenuItem from 'material-ui-nested-menu-item' @@ -126,6 +127,12 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => /> </HoverContainer> ) + case ComponentTypes.Question: + return ( + <HoverContainer hover={hover}> + <QuestionComponentDisplay variant="editor" /> + </HoverContainer> + ) default: break } diff --git a/client/src/pages/presentationEditor/components/SlideDisplay.tsx b/client/src/pages/presentationEditor/components/SlideDisplay.tsx index aef4ca7c48d29bbfc47cac8f29b214ec6866f360..e6b60f290784627d9c5446428062f2e1042cacef 100644 --- a/client/src/pages/presentationEditor/components/SlideDisplay.tsx +++ b/client/src/pages/presentationEditor/components/SlideDisplay.tsx @@ -77,15 +77,7 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { scale={scale} /> ) - return ( - <PresentationComponent - height={height} - width={width} - key={component.id} - component={component} - scale={scale} - /> - ) + return <PresentationComponent key={component.id} component={component} scale={scale} /> })} </SlideEditorPaper> </SlideEditorContainerRatio> diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index d48b4565712d043d52d3b1e93e3633ec6b927d38..029187f49e06fcffbb147a82fc7e754ec09e3a0f 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -1,18 +1,19 @@ /* This file compiles and renders the right hand slide settings bar, under the tab "SIDA". */ -import { Divider, List, ListItem, ListItemText, TextField, Typography } from '@material-ui/core' -import React, { useState } from 'react' +import { Divider } from '@material-ui/core' +import React from 'react' import { useParams } from 'react-router-dom' import { useAppSelector } from '../../../hooks' +import BackgroundImageSelect from './BackgroundImageSelect' +import Images from './slideSettingsComponents/Images' import Instructions from './slideSettingsComponents/Instructions' import MultipleChoiceAlternatives from './slideSettingsComponents/MultipleChoiceAlternatives' +import QuestionSettings from './slideSettingsComponents/QuestionSettings' +import SingleChoiceAlternatives from './slideSettingsComponents/SingleChoiceAlternatives' import SlideType from './slideSettingsComponents/SlideType' -import { Center, ImportedImage, SettingsList, PanelContainer } from './styled' -import Timer from './slideSettingsComponents/Timer' -import Images from './slideSettingsComponents/Images' import Texts from './slideSettingsComponents/Texts' -import QuestionSettings from './slideSettingsComponents/QuestionSettings' -import BackgroundImageSelect from './BackgroundImageSelect' +import Timer from './slideSettingsComponents/Timer' +import { PanelContainer, SettingsList } from './styled' interface CompetitionParams { competitionId: string @@ -36,19 +37,21 @@ const SlideSettings: React.FC = () => { </SettingsList> {activeSlide?.questions[0] && <QuestionSettings activeSlide={activeSlide} competitionId={competitionId} />} + { - // Choose answer alternatives depending on the slide type + // Choose answer alternatives, depending on the slide type } - {activeSlide?.questions[0]?.type_id === 1 && ( - <Instructions activeSlide={activeSlide} competitionId={competitionId} /> - )} - {activeSlide?.questions[0]?.type_id === 2 && ( + {(activeSlide?.questions[0]?.type_id === 1 || activeSlide?.questions[0]?.type_id === 2) && ( <Instructions activeSlide={activeSlide} competitionId={competitionId} /> )} {activeSlide?.questions[0]?.type_id === 3 && ( <MultipleChoiceAlternatives activeSlide={activeSlide} competitionId={competitionId} /> )} + {activeSlide?.questions[0]?.type_id === 4 && ( + <SingleChoiceAlternatives activeSlide={activeSlide} competitionId={competitionId} /> + )} + {activeSlide && ( <Texts activeViewTypeId={activeViewTypeId} activeSlide={activeSlide} competitionId={competitionId} /> )} diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7a2b1c59acf0277197d6126a7aba5b81a8799a37 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -0,0 +1,106 @@ +import { Checkbox, ListItem, ListItemText, Typography, withStyles } from '@material-ui/core' +import { CheckboxProps } from '@material-ui/core/Checkbox' +import { green, grey } from '@material-ui/core/colors' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { getPresentationCompetition } from '../../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { QuestionAlternative } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center } from '../styled' + +type AnswerMultipleProps = { + variant: 'editor' | 'presentation' + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleProps) => { + const dispatch = useAppDispatch() + const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) + const team = useAppSelector((state) => state.presentation.competition.teams.find((team) => team.id === teamId)) + const answer = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id) + + const decideChecked = (alternative: QuestionAlternative) => { + const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer + if (alternative.text === teamAnswer) return true + else return false + } + + const updateAnswer = async (alternative: QuestionAlternative) => { + // TODO: fix. Make list of alternatives and delete & post instead of put to allow multiple boxes checked. + if (activeSlide) { + if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { + if (answer?.answer === alternative.text) { + // Uncheck checkbox + deleteAnswer() + } else { + // Check another box + // TODO + } + } else { + // Check first checkbox + await axios + .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { + answer: alternative.text, + score: 0, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } + }) + .catch(console.log) + } + } + } + + const deleteAnswer = async () => { + await axios + .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) // TODO: fix + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const GreenCheckbox = withStyles({ + root: { + color: grey[900], + '&$checked': { + color: green[600], + }, + }, + checked: {}, + })((props: CheckboxProps) => <Checkbox color="default" {...props} />) + + return ( + <div> + <ListItem divider> + <Center> + <ListItemText primary="Välj ett eller flera svar:" /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + { + //<GreenCheckbox checked={checkbox} onChange={(event) => updateAnswer(alt, event.target.checked)} /> + } + <GreenCheckbox checked={decideChecked(alt)} onChange={() => updateAnswer(alt)} /> + <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> + </ListItem> + </div> + ))} + </div> + ) +} + +export default AnswerMultiple diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8c2fad17395e9c602a7108a72301739064ab4533 --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx @@ -0,0 +1,132 @@ +import { Checkbox, ListItem, ListItemText, Typography, withStyles } from '@material-ui/core' +import { CheckboxProps } from '@material-ui/core/Checkbox' +import { green, grey } from '@material-ui/core/colors' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined' +import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { getPresentationCompetition } from '../../../../actions/presentation' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { QuestionAlternative } from '../../../../interfaces/ApiModels' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center, Clickable } from '../styled' + +type AnswerSingleProps = { + variant: 'editor' | 'presentation' + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps) => { + const dispatch = useAppDispatch() + const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) + const team = useAppSelector((state) => { + if (variant === 'editor') return state.editor.competition.teams.find((team) => team.id === teamId) + return state.presentation.competition.teams.find((team) => team.id === teamId) + }) + const answerId = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id)?.id + + const decideChecked = (alternative: QuestionAlternative) => { + const teamAnswer = team?.question_answers.find((answer) => answer.answer === alternative.text)?.answer + if (teamAnswer) return true + else return false + } + + const updateAnswer = async (alternative: QuestionAlternative) => { + if (activeSlide) { + // TODO: ignore API calls when an answer is already checked + if (team?.question_answers[0]) { + await axios + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { + answer: alternative.text, + }) + .then(() => { + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } + }) + .catch(console.log) + } else { + await axios + .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { + answer: alternative.text, + score: 0, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + if (variant === 'editor') { + dispatch(getEditorCompetition(competitionId)) + } else { + dispatch(getPresentationCompetition(competitionId)) + } + }) + .catch(console.log) + } + } + } + + const deleteAnswer = async () => { + await axios + .delete(`/api/competitions/${competitionId}/teams/${teamId}/answers`) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const GreenCheckbox = withStyles({ + root: { + color: grey[900], + '&$checked': { + color: green[600], + }, + }, + checked: {}, + })((props: CheckboxProps) => <Checkbox color="default" {...props} />) + + const renderRadioButton = (alt: QuestionAlternative) => { + if (variant === 'presentation') { + if (decideChecked(alt)) { + return ( + <Clickable> + <RadioButtonCheckedIcon onClick={() => updateAnswer(alt)} /> + </Clickable> + ) + } else { + return ( + <Clickable> + <RadioButtonUncheckedIcon onClick={() => updateAnswer(alt)} /> + </Clickable> + ) + } + } else { + return <RadioButtonUncheckedIcon onClick={() => updateAnswer(alt)} /> + } + } + + return ( + <div> + <ListItem divider> + <Center> + <ListItemText primary="Välj ett svar:" /> + </Center> + </ListItem> + {activeSlide && + activeSlide.questions[0] && + activeSlide.questions[0].alternatives && + activeSlide.questions[0].alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + {renderRadioButton(alt)} + <Typography style={{ wordBreak: 'break-all' }}>{alt.text}</Typography> + </ListItem> + </div> + ))} + </div> + ) +} + +export default AnswerSingle diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e54b0dd2fe56f3ebf96b65fa8a05e6af4467da9f --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx @@ -0,0 +1,80 @@ +import { ListItem, ListItemText, TextField } from '@material-ui/core' +import axios from 'axios' +import React from 'react' +import { getEditorCompetition } from '../../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../../hooks' +import { RichSlide } from '../../../../interfaces/ApiRichModels' +import { Center } from '../styled' +import { AnswerTextFieldContainer } from './styled' + +type AnswerTextProps = { + activeSlide: RichSlide | undefined + competitionId: string +} + +const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) + const dispatch = useAppDispatch() + const teamId = useAppSelector((state) => state.competitionLogin.data?.team_id) + const team = useAppSelector((state) => state.presentation.competition.teams.find((team) => team.id === teamId)) + const answerId = team?.question_answers.find((answer) => answer.question_id === activeSlide?.questions[0].id)?.id + const onAnswerChange = (answer: string) => { + if (timerHandle) { + clearTimeout(timerHandle) + setTimerHandle(undefined) + } + //Only updates answer 100ms after last input was made + setTimerHandle(window.setTimeout(() => updateAnswer(answer), 100)) + } + + const updateAnswer = async (answer: string) => { + if (activeSlide && team) { + if (team?.question_answers.find((answer) => answer.question_id === activeSlide.questions[0].id)) { + await axios + .put(`/api/competitions/${competitionId}/teams/${teamId}/answers/${answerId}`, { + answer, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } else { + await axios + .post(`/api/competitions/${competitionId}/teams/${teamId}/answers`, { + answer, + score: 0, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + } + } + + return ( + <AnswerTextFieldContainer> + <ListItem divider> + <Center> + <ListItemText primary="Skriv ditt svar nedan" /> + </Center> + </ListItem> + <ListItem style={{ height: '100%' }}> + <TextField + disabled={team === undefined} + defaultValue={ + team?.question_answers.find((questionAnswer) => questionAnswer.id === answerId)?.answer || 'Svar...' + } + style={{ height: '100%' }} + variant="outlined" + fullWidth={true} + multiline + onChange={(event) => onAnswerChange(event.target.value)} + /> + </ListItem> + </AnswerTextFieldContainer> + ) +} + +export default AnswerText diff --git a/client/src/pages/presentationEditor/components/answerComponents/styled.tsx b/client/src/pages/presentationEditor/components/answerComponents/styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9140e3c2776f5bda3d2f317740bb4bbb67f657ac --- /dev/null +++ b/client/src/pages/presentationEditor/components/answerComponents/styled.tsx @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +export const AnswerTextFieldContainer = styled.div` + height: calc(100% - 90px); +` diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e38c39fe443b5c1e78810cb5731690870f8f614 --- /dev/null +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SingleChoiceAlternatives.tsx @@ -0,0 +1,131 @@ +import { ListItem, ListItemText } from '@material-ui/core' +import CloseIcon from '@material-ui/icons/Close' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonCheckedOutlined' +import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined' +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, AlternativeTextField, Center, Clickable, SettingsList } from '../styled' + +type SingleChoiceAlternativeProps = { + activeSlide: RichSlide + competitionId: string +} + +const SingleChoiceAlternatives = ({ activeSlide, competitionId }: SingleChoiceAlternativeProps) => { + const dispatch = useAppDispatch() + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) + + const updateAlternativeValue = async (alternative: QuestionAlternative) => { + if (activeSlide && activeSlide.questions[0]) { + // Remove check from previously checked alternative + const previousCheckedAltId = activeSlide.questions[0].alternatives.find((alt) => alt.value === 1)?.id + if (previousCheckedAltId !== alternative.id) { + if (previousCheckedAltId) { + axios.put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${previousCheckedAltId}`, + { value: 0 } + ) + } + // Set new checked alternative + await axios + .put( + `/api/competitions/${competitionId}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`, + { value: 1 } + ) + .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) + } + } + + const renderRadioButton = (alt: QuestionAlternative) => { + if (alt.value) return <RadioButtonCheckedIcon onClick={() => updateAlternativeValue(alt)} /> + else return <RadioButtonUncheckedIcon onClick={() => updateAlternativeValue(alt)} /> + } + + return ( + <SettingsList> + <ListItem divider> + <Center> + <ListItemText + primary="Svarsalternativ" + secondary="(Fyll i cirkeln 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> + <AlternativeTextField + id="outlined-basic" + defaultValue={alt.text} + onChange={(event) => updateAlternativeText(alt.id, event.target.value)} + variant="outlined" + /> + <Clickable>{renderRadioButton(alt)}</Clickable> + <Clickable> + <CloseIcon onClick={() => handleCloseAnswerClick(alt.id)} /> + </Clickable> + </ListItem> + </div> + ))} + <ListItem button onClick={addAlternative}> + <Center> + <AddButton variant="button">Lägg till svarsalternativ</AddButton> + </Center> + </ListItem> + </SettingsList> + ) +} + +export default SingleChoiceAlternatives diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx index bc251b91e092c05457db1b64bb8bbe566d76dd55..bef33d943883981ee62e9dc49dcd09902d907737 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/SlideType.tsx @@ -15,7 +15,7 @@ import { import axios from 'axios' import React, { useState } from 'react' import { getEditorCompetition } from '../../../../actions/editor' -import { useAppDispatch } from '../../../../hooks' +import { useAppDispatch, useAppSelector } from '../../../../hooks' import { RichSlide } from '../../../../interfaces/ApiRichModels' import { Center, FirstItem } from '../styled' @@ -30,6 +30,11 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { // For "slide type" dialog const [selectedSlideType, setSelectedSlideType] = useState(0) const [slideTypeDialog, setSlideTypeDialog] = useState(false) + const components = useAppSelector( + (state) => state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.components + ) + const questionComponentId = components?.find((qCompId) => qCompId.type_id === 3)?.id + const openSlideTypeDialog = (type_id: number) => { setSelectedSlideType(type_id) setSlideTypeDialog(true) @@ -41,6 +46,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { const updateSlideType = async () => { closeSlideTypeDialog() if (activeSlide) { + deleteQuestionComponent(questionComponentId) if (activeSlide.questions[0] && activeSlide.questions[0].type_id !== selectedSlideType) { if (selectedSlideType === 0) { // Change slide type from a question type to information @@ -67,6 +73,7 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { }) .then(() => { dispatch(getEditorCompetition(competitionId)) + createQuestionComponent() }) .catch(console.log) } @@ -80,11 +87,38 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { }) .then(() => { dispatch(getEditorCompetition(competitionId)) + createQuestionComponent() }) .catch(console.log) } } } + + const createQuestionComponent = () => { + axios + .post(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components`, { + x: 0, + y: 0, + w: 400, + h: 250, + type_id: 3, + view_type_id: 1, + question_id: activeSlide.questions[0].id, + }) + .then(() => { + dispatch(getEditorCompetition(competitionId)) + }) + .catch(console.log) + } + + const deleteQuestionComponent = (componentId: number | undefined) => { + if (componentId) { + axios + .delete(`/api/competitions/${competitionId}/slides/${activeSlide.id}/components/${componentId}`) + .catch(console.log) + } + } + return ( <FirstItem> <ListItem> @@ -108,7 +142,12 @@ const SlideType = ({ activeSlide, competitionId }: SlideTypeProps) => { </MenuItem> <MenuItem value={3}> <Typography variant="button" onClick={() => openSlideTypeDialog(3)}> - Flervalsfråga + Kryssfråga + </Typography> + </MenuItem> + <MenuItem value={4}> + <Typography variant="button" onClick={() => openSlideTypeDialog(4)}> + Alternativfråga </Typography> </MenuItem> </Select> diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 605972d2c4ee798388f9437f373cbabeab777d3c..31e40d51de8a4ecb45fbddf1cda9b7e96d4151f5 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -1,16 +1,4 @@ -import { - FormControl, - List, - Tab, - TextField, - Typography, - Button, - Card, - ListItem, - Select, - InputLabel, - ListItemText, -} from '@material-ui/core' +import { Button, Card, List, ListItemText, Tab, TextField, Typography } from '@material-ui/core' import styled from 'styled-components' export const SettingsTab = styled(Tab)` @@ -148,3 +136,7 @@ export const HoverContainer = styled.div<HoverContainerProps>` export const ImageNameText = styled(ListItemText)` word-break: break-all; ` + +export const QuestionComponent = styled.div` + outline-style: double; +` diff --git a/client/src/pages/views/components/PresentationComponent.tsx b/client/src/pages/views/components/PresentationComponent.tsx index a41f7912469256a6e946522790f4f8203f8da60f..0a688dc1c1bec3373cbf7a4e782b57fdab58d411 100644 --- a/client/src/pages/views/components/PresentationComponent.tsx +++ b/client/src/pages/views/components/PresentationComponent.tsx @@ -1,21 +1,17 @@ -import { Typography } from '@material-ui/core' import React from 'react' import { Rnd } from 'react-rnd' import { ComponentTypes } from '../../../enum/ComponentTypes' -import { useAppSelector } from '../../../hooks' import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import ImageComponentDisplay from '../../presentationEditor/components/ImageComponentDisplay' +import QuestionComponentDisplay from '../../presentationEditor/components/QuestionComponentDisplay' import TextComponentDisplay from '../../presentationEditor/components/TextComponentDisplay' -import { SlideContainer } from './styled' type PresentationComponentProps = { component: Component - width: number - height: number scale: number } -const PresentationComponent = ({ component, width, height, scale }: PresentationComponentProps) => { +const PresentationComponent = ({ component, scale }: PresentationComponentProps) => { const renderInnerComponent = () => { switch (component.type_id) { case ComponentTypes.Text: @@ -28,6 +24,8 @@ const PresentationComponent = ({ component, width, height, scale }: Presentation component={component as ImageComponent} /> ) + case ComponentTypes.Question: + return <QuestionComponentDisplay variant="presentation" /> default: break } diff --git a/client/src/utils/renderSlideIcon.tsx b/client/src/utils/renderSlideIcon.tsx index ba1eafcb8052469dd8b627b95d2eee518bfd5fe8..22f6405abbc8574ae17bf86dd8b745ba98e3b4cf 100644 --- a/client/src/utils/renderSlideIcon.tsx +++ b/client/src/utils/renderSlideIcon.tsx @@ -1,9 +1,10 @@ -import { RichSlide } from '../interfaces/ApiRichModels' -import React from 'react' import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined' +import CheckBoxOutlinedIcon from '@material-ui/icons/CheckBoxOutlined' import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined' -import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined' import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked' +import React from 'react' +import { RichSlide } from '../interfaces/ApiRichModels' export const renderSlideIcon = (slide: RichSlide) => { if (slide.questions && slide.questions[0] && slide.questions[0].type_id) { @@ -13,7 +14,9 @@ export const renderSlideIcon = (slide: RichSlide) => { case 2: return <BuildOutlinedIcon /> // practical qustion case 3: - return <DnsOutlinedIcon /> // multiple choice question + return <CheckBoxOutlinedIcon /> // multiple choice question + case 4: + return <RadioButtonCheckedIcon /> // single choice question } } else { return <InfoOutlinedIcon /> // information slide diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 38227a46d90fe2afb5ea4a2fc7924412661c5259..9fff1d07a72115bdb215bb441606aa189c962702 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -11,7 +11,7 @@ schema = ComponentDTO.schema list_schema = ComponentDTO.list_schema component_parser_add = reqparse.RequestParser() -component_parser_add.add_argument("x", type=str, default=0, location="json") +component_parser_add.add_argument("x", type=int, default=0, location="json") component_parser_add.add_argument("y", type=int, default=0, location="json") component_parser_add.add_argument("w", type=int, default=1, location="json") component_parser_add.add_argument("h", type=int, default=1, location="json") @@ -22,7 +22,7 @@ component_parser_add.add_argument("media_id", type=int, default=None, location=" component_parser_add.add_argument("question_id", type=int, default=None, location="json") component_parser_edit = reqparse.RequestParser() -component_parser_edit.add_argument("x", type=str, default=sentinel, location="json") +component_parser_edit.add_argument("x", type=int, default=sentinel, location="json") component_parser_edit.add_argument("y", type=int, default=sentinel, location="json") component_parser_edit.add_argument("w", type=int, default=sentinel, location="json") component_parser_edit.add_argument("h", type=int, default=sentinel, location="json") diff --git a/server/populate.py b/server/populate.py index 34f73822f789ddfdabe1092192c6dede7af85355..9981f0c02f430d56c88738a9b67e89d5fc17af89 100644 --- a/server/populate.py +++ b/server/populate.py @@ -11,8 +11,8 @@ from app.database.models import City, QuestionType, Role def _add_items(): media_types = ["Image", "Video"] - question_types = ["Boolean", "Multiple", "Text"] - component_types = ["Text", "Image"] + question_types = ["Text", "Practical", "Multiple", "Single"] + component_types = ["Text", "Image", "Question"] view_types = ["Team", "Judge", "Audience", "Operator"] roles = ["Admin", "Editor"]