diff --git a/src/components/Tutorial/Builder/BlocklyExample.js b/src/components/Tutorial/Builder/BlocklyExample.js
new file mode 100644
index 0000000..2ecfa12
--- /dev/null
+++ b/src/components/Tutorial/Builder/BlocklyExample.js
@@ -0,0 +1,150 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions';
+
+import moment from 'moment';
+import localization from 'moment/locale/de';
+import * as Blockly from 'blockly/core';
+
+import BlocklyWindow from '../../Blockly/BlocklyWindow';
+
+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 FormLabel from '@material-ui/core/FormLabel';
+import Button from '@material-ui/core/Button';
+import Grid from '@material-ui/core/Grid';
+
+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 BlocklyExample extends Component {
+
+ constructor(props){
+ super(props);
+ this.state={
+ checked: props.task ? props.task : props.value ? true : false,
+ input: null,
+ };
+ }
+
+ componentDidMount(){
+ this.isError();
+ // if(this.props.task){
+ // this.props.setError(this.props.index, 'xml');
+ // }
+ }
+
+ componentDidUpdate(props, state){
+ if(props.task !== this.props.task || props.value !== this.props.value){
+ this.setState({checked: this.props.task ? this.props.task : this.props.value ? true : false},
+ () => this.isError()
+ );
+ }
+ if(state.checked !== this.state.checked){
+ this.isError();
+ }
+ }
+
+ isError = () => {
+ if(this.state.checked && !this.props.value){
+ this.props.setError(this.props.index, 'xml');
+ }
+ else {
+ this.props.deleteError(this.props.index, 'xml');
+ }
+ }
+
+ onChange = (value) => {
+ var oldValue = this.state.checked;
+ this.setState({checked: value});
+ if(oldValue !== value && !value){
+ this.props.deleteProperty(this.props.index, 'xml');
+ }
+ }
+
+ render() {
+ moment.locale('de', localization);
+ return (
+
+ {!this.props.task ?
+
this.onChange(e.target.checked)}
+ color="primary"
+ />
+ }
+ />
+ : Musterlösung}
+ {this.state.checked ? !this.props.value || this.props.error ?
+ Reiche deine Blöcke ein, indem du auf den rot gefärbten Button klickst.
+ : this.state.input ? Die letzte Einreichung erfolgte um {this.state.input} Uhr. : null
+ : null}
+ {this.state.checked ? (() => {
+ var initialXml = this.props.value;
+ // check if value is valid xml;
+ try{
+ Blockly.Xml.textToDom(initialXml);
+ }
+ catch(err){
+ initialXml = null;
+ this.props.setError(this.props.index, 'xml');
+ }
+ return (
+
+
+
+
+
+
+
+
+ )})()
+ : null}
+
+ );
+ };
+}
+
+BlocklyExample.propTypes = {
+ changeContent: PropTypes.func.isRequired,
+ deleteProperty: PropTypes.func.isRequired,
+ setError: PropTypes.func.isRequired,
+ deleteError: PropTypes.func.isRequired,
+ xml: PropTypes.string.isRequired
+};
+
+const mapStateToProps = state => ({
+ xml: state.workspace.code.xml
+});
+
+
+export default connect(mapStateToProps, { changeContent, deleteProperty, setError, deleteError })(withStyles(styles, {withTheme: true})(BlocklyExample));
diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js
new file mode 100644
index 0000000..88c9754
--- /dev/null
+++ b/src/components/Tutorial/Builder/Builder.js
@@ -0,0 +1,201 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { checkError, readJSON, jsonString, progress, resetTutorial } from '../../../actions/tutorialBuilderActions';
+
+import { saveAs } from 'file-saver';
+
+import { detectWhitespacesAndReturnReadableResult } from '../../../helpers/whitespace';
+
+import Breadcrumbs from '../../Breadcrumbs';
+import Id from './Id';
+import Textfield from './Textfield';
+import Step from './Step';
+import Dialog from '../../Dialog';
+
+import { withStyles } from '@material-ui/core/styles';
+import Button from '@material-ui/core/Button';
+import Backdrop from '@material-ui/core/Backdrop';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import Divider from '@material-ui/core/Divider';
+
+const styles = (theme) => ({
+ backdrop: {
+ zIndex: theme.zIndex.drawer + 1,
+ color: '#fff',
+ }
+});
+
+class Builder extends Component {
+
+ constructor(props){
+ super(props);
+ this.state = {
+ open: false,
+ title: '',
+ content: '',
+ string: false
+ };
+ this.inputRef = React.createRef();
+ }
+
+ submit = () => {
+ var isError = this.props.checkError();
+ if(isError){
+ window.scrollTo(0, 0);
+ }
+ else{
+ var tutorial = {
+ id: this.props.id,
+ title: this.props.title,
+ steps: this.props.steps
+ }
+ var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' });
+ saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`);
+ }
+ }
+
+ reset = () => {
+ this.props.resetTutorial();
+ window.scrollTo(0, 0);
+ }
+
+ uploadJsonFile = (jsonFile) => {
+ this.props.progress(true);
+ if(jsonFile.type !== 'application/json'){
+ this.props.progress(false);
+ this.setState({ open: true, string: false, title: 'Unzulässiger Dateityp', content: 'Die übergebene Datei entspricht nicht dem geforderten Format. Es sind nur JSON-Dateien zulässig.'});
+ }
+ else {
+ var reader = new FileReader();
+ reader.readAsText(jsonFile);
+ reader.onloadend = () => {
+ this.readJson(reader.result, true);
+ };
+ }
+ }
+
+ uploadJsonString = () => {
+ this.setState({ open: true, string: true, title: 'JSON-String einfügen', content: ''});
+ }
+
+ readJson = (jsonString, isFile) => {
+ try {
+ var result = JSON.parse(jsonString);
+ if(!this.checkSteps(result.steps)){
+ result.steps = [{}];
+ }
+ this.props.readJSON(result);
+ } catch(err){
+ console.log(err);
+ this.props.progress(false);
+ this.props.jsonString('');
+ this.setState({ open: true, string: false, title: 'Ungültiges JSON-Format', content: `${isFile ? 'Die übergebene Datei' : 'Der übergebene String'} enthält nicht valides JSON. Bitte überprüfe ${isFile ? 'die JSON-Datei' : 'den JSON-String'} und versuche es erneut.`});
+ }
+ }
+
+ checkSteps = (steps) => {
+ if(!(steps && steps.length > 0)){
+ return false;
+ }
+ return true;
+ }
+
+ toggle = () => {
+ this.setState({ open: !this.state });
+ }
+
+
+ render() {
+ return (
+
+
+
+
Tutorial-Builder
+
+ {/*upload JSON*/}
+
+ {this.uploadJsonFile(e.target.files[0])}}
+ id="open-json"
+ type="file"
+ />
+
+
+
+
+
+ {/*Tutorial-Builder-Form*/}
+
+
+
+ {this.props.steps.map((step, i) =>
+
+ )}
+
+ {/*submit or reset*/}
+
+
+
+
+
+
+
+
+
+ : null
+ }
+ >
+ {this.state.string ?
+
+ : null}
+
+
+
+ );
+ };
+}
+
+Builder.propTypes = {
+ checkError: PropTypes.func.isRequired,
+ readJSON: PropTypes.func.isRequired,
+ jsonString: PropTypes.func.isRequired,
+ progress: PropTypes.func.isRequired,
+ resetTutorial: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ steps: PropTypes.array.isRequired,
+ change: PropTypes.number.isRequired,
+ error: PropTypes.object.isRequired,
+ json: PropTypes.string.isRequired,
+ isProgress: PropTypes.bool.isRequired
+};
+
+const mapStateToProps = state => ({
+ title: state.builder.title,
+ id: state.builder.id,
+ steps: state.builder.steps,
+ change: state.builder.change,
+ error: state.builder.error,
+ json: state.builder.json,
+ isProgress: state.builder.progress
+});
+
+export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, resetTutorial })(withStyles(styles, {withTheme: true})(Builder));
diff --git a/src/components/Tutorial/Builder/Hardware.js b/src/components/Tutorial/Builder/Hardware.js
new file mode 100644
index 0000000..fa0f452
--- /dev/null
+++ b/src/components/Tutorial/Builder/Hardware.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions';
+
+import hardware from '../../../data/hardware.json';
+
+import { fade } from '@material-ui/core/styles/colorManipulator';
+import { withStyles } from '@material-ui/core/styles';
+import withWidth, { isWidthDown } from '@material-ui/core/withWidth';
+import GridList from '@material-ui/core/GridList';
+import GridListTile from '@material-ui/core/GridListTile';
+import GridListTileBar from '@material-ui/core/GridListTileBar';
+import FormHelperText from '@material-ui/core/FormHelperText';
+import FormLabel from '@material-ui/core/FormLabel';
+
+const styles = theme => ({
+ multiGridListTile: {
+ background: fade(theme.palette.secondary.main, 0.5),
+ height: '30px'
+ },
+ multiGridListTileTitle: {
+ color: theme.palette.text.primary
+ },
+ border: {
+ cursor: 'pointer',
+ '&:hover': {
+ width: 'calc(100% - 4px)',
+ height: 'calc(100% - 4px)',
+ border: `2px solid ${theme.palette.primary.main}`
+ }
+ },
+ active: {
+ cursor: 'pointer',
+ width: 'calc(100% - 4px)',
+ height: 'calc(100% - 4px)',
+ border: `2px solid ${theme.palette.primary.main}`
+ },
+ errorColor: {
+ color: theme.palette.error.dark,
+ lineHeight: 'initial',
+ marginBottom: '10px'
+ }
+});
+
+class Requirements extends Component {
+
+ onChange = (hardware) => {
+ var hardwareArray = this.props.value;
+ if(hardwareArray.filter(value => value === hardware).length > 0){
+ hardwareArray = hardwareArray.filter(value => value !== hardware);
+ }
+ else {
+ hardwareArray.push(hardware);
+ if(this.props.error){
+ this.props.deleteError(this.props.index, 'hardware');
+ }
+ }
+ this.props.changeContent(this.props.index, 'hardware', hardwareArray);
+ if(hardwareArray.length === 0){
+ this.props.setError(this.props.index, 'hardware');
+ }
+ }
+
+ render() {
+ var cols = isWidthDown('md', this.props.width) ? isWidthDown('sm', this.props.width) ? isWidthDown('xs', this.props.width) ? 2 : 3 : 4 : 6;
+ return (
+