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.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"
+ />
+
+ Bild auswählen
+
+
+
+ :
+ /*youtube-video*/
+
+
+ {this.props.youtube && !this.props.error ?
+
+
{`Stelle sicher, dass das unten angezeigte Youtube-Video funktioniert, andernfalls überprüfe die Youtube-ID.`}
+
+ VIDEO
+
+
+ : null}
+
+ }
+
+ : null}
+
+ );
+ };
+}
+
+Media.propTypes = {
+ changeContent: PropTypes.func.isRequired,
+ deleteProperty: PropTypes.func.isRequired,
+ setError: PropTypes.func.isRequired,
+ deleteError: PropTypes.func.isRequired,
+};
+
+
+export default connect(null, { changeContent, deleteProperty, setError, deleteError })(withStyles(styles, {withTheme: true})(Media));
diff --git a/src/components/Tutorial/Builder/Requirements.js b/src/components/Tutorial/Builder/Requirements.js
index d06c62f..2867325 100644
--- a/src/components/Tutorial/Builder/Requirements.js
+++ b/src/components/Tutorial/Builder/Requirements.js
@@ -23,7 +23,7 @@ class Requirements extends Component {
else {
requirements = requirements.filter(requirement => requirement !== value);
}
- this.props.changeContent(this.props.index, 'requirements', requirements);
+ this.props.changeContent(requirements, this.props.index, 'requirements');
}
render() {
diff --git a/src/components/Tutorial/Builder/Step.js b/src/components/Tutorial/Builder/Step.js
index 24c75ac..ba0e18c 100644
--- a/src/components/Tutorial/Builder/Step.js
+++ b/src/components/Tutorial/Builder/Step.js
@@ -10,6 +10,7 @@ import StepType from './StepType';
import BlocklyExample from './BlocklyExample';
import Requirements from './Requirements';
import Hardware from './Hardware';
+import Media from './Media';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
@@ -103,6 +104,9 @@ class Step extends Component {
: null}
+ {this.props.step.type === 'instruction' ?
+
+ : null}
diff --git a/src/components/Tutorial/Builder/StepType.js b/src/components/Tutorial/Builder/StepType.js
index cfc2253..971660c 100644
--- a/src/components/Tutorial/Builder/StepType.js
+++ b/src/components/Tutorial/Builder/StepType.js
@@ -10,7 +10,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
class StepType extends Component {
onChange = (value) => {
- this.props.changeContent(this.props.index, 'type', value);
+ this.props.changeContent(value, this.props.index, 'type');
// delete property 'xml', so that all used blocks are reset
this.props.deleteProperty(this.props.index, 'xml');
if(value === 'task'){
diff --git a/src/components/Tutorial/Builder/Textfield.js b/src/components/Tutorial/Builder/Textfield.js
index d601784..fa39547 100644
--- a/src/components/Tutorial/Builder/Textfield.js
+++ b/src/components/Tutorial/Builder/Textfield.js
@@ -28,7 +28,9 @@ class Textfield extends Component {
componentDidMount(){
if(this.props.error){
- this.props.deleteError(this.props.index, this.props.property);
+ if(this.props.property !== 'media'){
+ this.props.deleteError(this.props.index, this.props.property);
+ }
}
}
@@ -41,7 +43,7 @@ class Textfield extends Component {
this.props.jsonString(value);
}
else {
- this.props.changeContent(this.props.index, this.props.property, value);
+ this.props.changeContent(value, this.props.index, this.props.property, this.props.property2);
}
if(value.replace(/\s/g,'') === ''){
this.props.setError(this.props.index, this.props.property);
diff --git a/src/components/Tutorial/Instruction.js b/src/components/Tutorial/Instruction.js
index 739d798..255f45c 100644
--- a/src/components/Tutorial/Instruction.js
+++ b/src/components/Tutorial/Instruction.js
@@ -23,6 +23,17 @@ class Instruction extends Component {
: null}
{areRequirements > 0 ?
: null}
+ {step.media ?
+ step.media.picture ?
+
+
+
+ : step.media.youtube ?
+
+ VIDEO
+
+ : null
+ : null}
{step.xml ?
diff --git a/src/data/tutorials.json b/src/data/tutorials.json
index e8f6bde..67f0384 100644
--- a/src/data/tutorials.json
+++ b/src/data/tutorials.json
@@ -57,14 +57,19 @@
"type": "instruction",
"headline": "Programmierung",
"text": "Man benötigt folgenden Block:",
+ "media": {
+ "picture": "block_en.svg"
+ },
"xml": "SSID Password "
},
{
"id": 3,
"type": "instruction",
"headline": "Block richtig einbinden",
- "text": "",
- "xml": "SSID Password "
+ "text": "Dies ist ein Test.",
+ "media": {
+ "youtube": "sf3RzXq6iVo"
+ }
},
{
"id": 4,