diff --git a/public/media/tutorial/block_en.svg b/public/media/tutorial/block_en.svg new file mode 100644 index 0000000..13b559a --- /dev/null +++ b/public/media/tutorial/block_en.svg @@ -0,0 +1 @@ +">Arduino run first:Arduino loop forever: diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js index f200cf0..3d3effa 100644 --- a/src/actions/tutorialBuilderActions.js +++ b/src/actions/tutorialBuilderActions.js @@ -85,10 +85,18 @@ export const removeErrorStep = (index) => (dispatch, getState) => { }); }; -export const changeContent = (index, property, content) => (dispatch, getState) => { +export const changeContent = (content, index, property1, property2) => (dispatch, getState) => { var steps = getState().builder.steps; var step = steps[index]; - step[property] = content; + if(property2){ + if(step[property1] && step[property1][property2]){ + step[property1][property2] = content; + } else { + step[property1] = {[property2]: content}; + } + } else { + step[property1] = content; + } dispatch({ type: BUILDER_CHANGE_STEP, payload: steps @@ -96,10 +104,16 @@ export const changeContent = (index, property, content) => (dispatch, getState) dispatch(changeTutorialBuilder()); }; -export const deleteProperty = (index, property) => (dispatch, getState) => { +export const deleteProperty = (index, property1, property2) => (dispatch, getState) => { var steps = getState().builder.steps; var step = steps[index]; - delete step[property]; + if(property2){ + if(step[property1] && step[property1][property2]){ + delete step[property1][property2]; + } + } else { + delete step[property1]; + } dispatch({ type: BUILDER_DELETE_PROPERTY, payload: steps @@ -170,12 +184,14 @@ export const setSubmitError = () => (dispatch, getState) => { dispatch(setError(undefined, 'title')); } var type = builder.steps.map((step, i) => { + // media and xml are directly checked for errors in their components and + // therefore do not have to be checked again step.id = i+1; if(i === 0){ if(step.requirements && step.requirements.length > 0){ var requirements = step.requirements.filter(requirement => typeof(requirement)==='number'); if(requirements.length < step.requirements.length){ - dispatch(changeContent(i, 'requirements', requirements)); + dispatch(changeContent(requirements, i, 'requirements')); } } if(step.hardware === undefined || step.hardware.length < 1){ @@ -185,7 +201,7 @@ export const setSubmitError = () => (dispatch, getState) => { var hardwareIds = data.map(hardware => hardware.id); var hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware)); if(hardware.length < step.hardware.length){ - dispatch(changeContent(i, 'hardware', hardware)); + dispatch(changeContent(hardware, i, 'hardware')); } } } @@ -255,9 +271,35 @@ export const readJSON = (json) => (dispatch, getState) => { steps: json.steps.map(() => {return {};}) } }); + // accept only valid attributes + var steps = json.steps.map((step, i) => { + var object = { + id: step.id, + type: step.type, + headline: step.headline, + text: step.text + }; + if(i === 0){ + object.hardware = step.hardware; + object.requirements = step.requirements; + } + if(step.xml){ + object.xml = step.xml; + } + if(step.media && step.type === 'instruction'){ + object.media = {}; + if(step.media.picture){ + object.media.picture = step.media.picture; + } + else if(step.media.youtube){ + object.media.youtube = step.media.youtube; + } + } + return object; + }); dispatch(tutorialTitle(json.title)); dispatch(tutorialId(json.id)); - dispatch(tutorialSteps(json.steps)); + dispatch(tutorialSteps(steps)); dispatch(setSubmitError()); dispatch(progress(false)); }; diff --git a/src/components/Tutorial/Builder/BlocklyExample.js b/src/components/Tutorial/Builder/BlocklyExample.js index f93a282..01f52f4 100644 --- a/src/components/Tutorial/Builder/BlocklyExample.js +++ b/src/components/Tutorial/Builder/BlocklyExample.js @@ -107,7 +107,7 @@ class BlocklyExample extends Component { setXml = () => { var xml = this.props.xml; - this.props.changeContent(this.props.index, 'xml', xml); + this.props.changeContent(xml, this.props.index, 'xml'); this.setState({input: moment(Date.now()).format('LTS')}); } @@ -134,7 +134,8 @@ class BlocklyExample extends Component { {this.state.checked && !this.props.task ? Anmerkung: Man kann den initialen Setup()- bzw. Endlosschleifen()-Block löschen. Zusätzlich ist es möglich u.a. nur einen beliebigen Block auszuwählen, ohne dass dieser als deaktiviert dargestellt wird. : null} - {this.state.checked ? (() => { + {/* ensure that the correct xml-file is displayed in the workspace */} + {this.state.checked && this.state.xml? (() => { return(
diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 1c6c819..2a177f2 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -58,10 +58,17 @@ class Builder extends Component { window.scrollTo(0, 0); } else{ + // export steps without attribute 'url' + var steps = this.props.steps.map(step => { + if(step.url){ + delete step.url; + } + return step; + }); var tutorial = { id: this.props.id, title: this.props.title, - steps: this.props.steps + steps: steps } var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' }); saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`); diff --git a/src/components/Tutorial/Builder/Hardware.js b/src/components/Tutorial/Builder/Hardware.js index f469851..770cc8c 100644 --- a/src/components/Tutorial/Builder/Hardware.js +++ b/src/components/Tutorial/Builder/Hardware.js @@ -56,7 +56,7 @@ class Requirements extends Component { this.props.deleteError(this.props.index, 'hardware'); } } - this.props.changeContent(this.props.index, 'hardware', hardwareArray); + this.props.changeContent(hardwareArray, this.props.index, 'hardware'); if(hardwareArray.length === 0){ this.props.setError(this.props.index, 'hardware'); } diff --git a/src/components/Tutorial/Builder/Media.js b/src/components/Tutorial/Builder/Media.js new file mode 100644 index 0000000..3f1896b --- /dev/null +++ b/src/components/Tutorial/Builder/Media.js @@ -0,0 +1,178 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions'; + +import Textfield from './Textfield'; + +import { withStyles } from '@material-ui/core/styles'; +import Switch from '@material-ui/core/Switch'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import Button from '@material-ui/core/Button'; + +const styles = (theme) => ({ + errorColor: { + color: theme.palette.error.dark + }, + errorBorder: { + border: `1px solid ${theme.palette.error.dark}` + }, + errorButton: { + marginTop: '5px', + height: '40px', + backgroundColor: theme.palette.error.dark, + '&:hover':{ + backgroundColor: theme.palette.error.dark + } + } +}); + +class Media extends Component { + + constructor(props){ + super(props); + this.state={ + checked: props.value ? true : false, + error: false, + radioValue: !props.picture && !props.youtube ? 'picture' : props.picture ? 'picture' : 'youtube' + }; + } + + componentDidUpdate(props){ + if(props.value !== this.props.value){ + this.setState({ checked: this.props.value ? true : false }); + } + } + + onChangeSwitch = (value) => { + var oldValue = this.state.checked; + this.setState({checked: value}); + if(oldValue !== value){ + if(value){ + this.props.setError(this.props.index, 'media'); + } else { + this.props.deleteError(this.props.index, 'media'); + this.props.deleteProperty(this.props.index, 'media'); + this.props.deleteProperty(this.props.index, 'url'); + this.setState({ error: false}); + } + } + } + + onChangeRadio = (value) => { + this.props.setError(this.props.index, 'media'); + var oldValue = this.state.radioValue; + this.setState({radioValue: value, error: false}); + // delete property 'oldValue', so that all old media files are reset + this.props.deleteProperty(this.props.index, 'media', oldValue); + if(oldValue === 'picture'){ + this.props.deleteProperty(this.props.index, 'url'); + } + } + + uploadPicture = (pic) => { + if(!(/^image\/.*/.test(pic.type))){ + this.props.setError(this.props.index, 'media'); + this.setState({ error: true }); + this.props.deleteProperty(this.props.index, 'url'); + } + else { + this.props.deleteError(this.props.index, 'media'); + this.setState({ error: false }); + this.props.changeContent(URL.createObjectURL(pic), this.props.index, 'url'); + } + this.props.changeContent(pic.name, this.props.index, 'media', 'picture'); + } + + render() { + return ( +
+ this.onChangeSwitch(e.target.checked)} + color="primary" + /> + } + /> + {this.state.checked ? +
+ {this.onChangeRadio(e.target.value);}}> + } + label="Bild" + labelPlacement="end" + /> + } + label="Youtube-Video" + labelPlacement="end" + /> + + {this.state.radioValue === 'picture' ? +
+ {!this.props.error ? +
+ {`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.picture}' abgespeichert werden muss.`} + {this.props.url +
+ :
+ {this.state.error ? + {`Die übergebene Datei entspricht nicht dem geforderten Bild-Format. Überprüfe, ob es sich um ein Bild handelt und versuche es nochmal.`} + : {`Wähle ein Bild aus.`} + } +
} + {/*upload picture*/} +
+ {this.uploadPicture(e.target.files[0]);}} + id={`picture ${this.props.index}`} + type="file" + /> + +
+
+ : + /*youtube-video*/ +
+ + {this.props.youtube && !this.props.error ? +
+ {`Stelle sicher, dass das unten angezeigte Youtube-Video funktioniert, andernfalls überprüfe die Youtube-ID.`} +
+