Merge branch 'master' into instruction

This commit is contained in:
Delucse 2020-09-14 13:26:29 +02:00
commit 9724d01b76
22 changed files with 590 additions and 343 deletions

56
package-lock.json generated
View File

@ -1096,6 +1096,16 @@
"to-fast-properties": "^2.0.0"
}
},
"@blockly/block-plus-minus": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@blockly/block-plus-minus/-/block-plus-minus-2.0.8.tgz",
"integrity": "sha512-LRn+Js2rZ14XyrSoEf7wTz6/ESNW2MI5TkXJ2wWFJVA+/E4lTfBwXeZpRFYRP9DZwNEv9alZETyEcBbK+FCZKw=="
},
"@blockly/field-slider": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@blockly/field-slider/-/field-slider-2.0.7.tgz",
"integrity": "sha512-kSFeeyfJboj2zOz55hgunFzRHQZUTWmKgw695GOwOGvt4wTG5SQ2/pNnd+C41vdjaOdjaI8tlwiyWg4oJ/MPeA=="
},
"@cnakazawa/watch": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz",
@ -3694,6 +3704,17 @@
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="
},
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@ -4588,6 +4609,12 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"optional": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -6350,6 +6377,15 @@
}
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"optional": true,
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@ -10490,6 +10526,14 @@
}
}
},
"prismjs": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz",
"integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==",
"requires": {
"clipboard": "^2.0.0"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -11659,6 +11703,12 @@
"ajv-keywords": "^3.4.1"
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"optional": true
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -12827,6 +12877,12 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"optional": true
},
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",

View File

@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@blockly/block-plus-minus": "^2.0.8",
"@blockly/field-slider": "^2.0.7",
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@fortawesome/react-fontawesome": "^0.1.11",

View File

@ -1,4 +1,6 @@
import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_LEVEL } from './types';
import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types';
import tutorials from '../components/Tutorial/tutorials.json';
export const tutorialChange = () => (dispatch) => {
dispatch({
@ -6,10 +8,15 @@ export const tutorialChange = () => (dispatch) => {
});
};
export const tutorialCheck = (status) => (dispatch, getState) => {
export const tutorialCheck = (status, step) => (dispatch, getState) => {
var tutorialsStatus = getState().tutorial.status;
var id = getState().tutorial.currentId;
tutorialsStatus[id] = {...tutorialsStatus[id], status: status};
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id);
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === step.id);
tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = {
...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex],
type: status
};
dispatch({
type: status === 'success' ? TUTORIAL_SUCCESS : TUTORIAL_ERROR,
payload: tutorialsStatus
@ -19,24 +26,25 @@ export const tutorialCheck = (status) => (dispatch, getState) => {
export const storeTutorialXml = (code) => (dispatch, getState) => {
var id = getState().tutorial.currentId;
var level = getState().tutorial.level;
if(id !== null && level === 'assessment'){
if(id !== null){
var activeStep = getState().tutorial.activeStep;
var steps = tutorials.filter(tutorial => tutorial.id === id)[0].steps;
if(steps[activeStep].type === 'task'){
var tutorialsStatus = getState().tutorial.status;
tutorialsStatus[id] = {...tutorialsStatus[id], xml: code};
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id);
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === steps[activeStep].id);
tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = {
...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex],
xml: code
};
dispatch({
type: TUTORIAL_XML,
payload: tutorialsStatus
});
}
}
};
// level = "instruction" or "assessment"
export const setTutorialLevel = (level) => (dispatch) => {
dispatch({
type: TUTORIAL_LEVEL,
payload: level
});
}
export const tutorialId = (id) => (dispatch) => {
dispatch({
@ -44,3 +52,10 @@ export const tutorialId = (id) => (dispatch) => {
payload: id
});
};
export const tutorialStep = (step) => (dispatch) => {
dispatch({
type: TUTORIAL_STEP,
payload: step
});
};

View File

@ -12,4 +12,4 @@ export const TUTORIAL_ERROR = 'TUTORIAL_ERROR';
export const TUTORIAL_CHANGE = 'TUTORIAL_CHANGE';
export const TUTORIAL_XML = 'TUTORIAL_XML';
export const TUTORIAL_ID = 'TUTORIAL_ID';
export const TUTORIAL_LEVEL = 'TUTORIAL_LEVEL';
export const TUTORIAL_STEP = 'TUTORIAL_STEP';

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { onChangeWorkspace } from '../../actions/workspaceActions';
import { onChangeWorkspace, clearStats } from '../../actions/workspaceActions';
import * as De from './msg/de';
import BlocklyComponent from './';
import * as Blockly from 'blockly/core';
@ -22,23 +22,25 @@ class BlocklyWindow extends Component {
componentDidMount() {
const workspace = Blockly.getMainWorkspace();
this.props.onChangeWorkspace({});
this.props.clearStats();
workspace.addChangeListener((event) => {
this.props.onChangeWorkspace(event);
Blockly.Events.disableOrphans(event);
});
Blockly.svgResize(workspace);
}
componentDidUpdate(props) {
const workspace = Blockly.getMainWorkspace();
if(props.initialXml !== this.props.initialXml){
// guarantees that the current xml-code (this.props.initialXml) is rendered
const workspace = Blockly.getMainWorkspace();
workspace.clear();
Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(this.props.initialXml), workspace);
}
Blockly.svgResize(workspace);
}
render() {
console.log(this.props.initialXml);
return (
<BlocklyComponent ref={this.simpleWorkspace}
style={this.props.blocklyCSS}
@ -76,8 +78,9 @@ class BlocklyWindow extends Component {
}
BlocklyWindow.propTypes = {
onChangeWorkspace: PropTypes.func.isRequired
onChangeWorkspace: PropTypes.func.isRequired,
clearStats: PropTypes.func.isRequired
};
export default connect(null, { onChangeWorkspace })(BlocklyWindow);
export default connect(null, { onChangeWorkspace, clearStats })(BlocklyWindow);

View File

@ -7,6 +7,7 @@ import './sensebox-osem';
import './sensebox-web';
import './sensebox-display';
import './sensebox-lora';
import './sensebox-led';
import './io';
import './math';
import './map';

View File

@ -1,6 +1,8 @@
import * as Blockly from 'blockly/core';
import { getColour } from '../helpers/colour';
import * as Types from '../helpers/types'
import { FieldSlider } from '@blockly/field-slider';
import { Field } from '..';
Blockly.Blocks['sensebox_display_beginDisplay'] = {
@ -35,12 +37,15 @@ Blockly.Blocks['sensebox_display_printDisplay'] = {
this.appendDummyInput()
.appendField(Blockly.Msg.senseBox_display_color)
.appendField(new Blockly.FieldDropdown([[Blockly.Msg.senseBox_display_white, "WHITE,BLACK"], [Blockly.Msg.senseBox_display_black, "BLACK,WHITE"]]), "COLOR");
this.appendValueInput("SIZE", 'Number')
.appendField(Blockly.Msg.senseBox_display_setSize);
this.appendValueInput("X", 'Number')
.appendField(Blockly.Msg.senseBox_display_printDisplay_x);
this.appendValueInput("Y", 'Number')
.appendField(Blockly.Msg.senseBox_display_printDisplay_y);
this.appendDummyInput()
.appendField(Blockly.Msg.senseBox_display_setSize)
.appendField(new FieldSlider(1, 1, 4), "SIZE");
this.appendDummyInput()
.appendField(Blockly.Msg.senseBox_display_printDisplay_x)
.appendField(new FieldSlider(0, 0, 64), "X");
this.appendDummyInput()
.appendField(Blockly.Msg.senseBox_display_printDisplay_y)
.appendField(new FieldSlider(0, 0, 128), "Y");
this.appendValueInput('printDisplay')
.appendField(Blockly.Msg.senseBox_display_printDisplay_value)
.setCheck(null);

View File

@ -0,0 +1,28 @@
import * as Blockly from 'blockly';
import { FieldSlider } from '@blockly/field-slider';
import { getColour } from '../helpers/colour'
import { selectedBoard } from '../helpers/board'
Blockly.Blocks['sensebox_rgb_led'] = {
init: function () {
this.setColour(getColour().sensebox);
this.appendDummyInput()
.appendField(Blockly.Msg.senseBox_rgb_led)
.appendField("Pin:")
.appendField(new Blockly.FieldDropdown(selectedBoard().digitalPins), "PIN")
this.appendDummyInput()
.appendField(Blockly.Msg.COLOUR_RGB_RED)//Blockly.Msg.senseBox_basic_red
.appendField(new FieldSlider(255, 0, 255), "RED");
this.appendDummyInput()
.appendField(Blockly.Msg.COLOUR_RGB_GREEN)//Blockly.Msg.senseBox_basic_green
.appendField(new FieldSlider(255, 0, 255), "GREEN");
this.appendDummyInput()
.appendField(Blockly.Msg.COLOUR_RGB_BLUE)//Blockly.Msg.senseBox_basic_green
.appendField(new FieldSlider(255, 0, 255), "BLUE");
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip(Blockly.Msg.senseBox_rgb_led_tip);
this.setHelpUrl('https://sensebox.de/books');
}
};

View File

@ -6,6 +6,7 @@ import './sensebox-osem';
import './sensebox-web';
import './sensebox-display';
import './sensebox-lora';
import './sensebox-led';
import './logic';
import './math';
import './map';

View File

@ -0,0 +1,15 @@
import * as Blockly from 'blockly/core';
import { Block } from 'blockly';
Blockly.Arduino.sensebox_rgb_led = function () {
var dropdown_pin = this.getFieldValue('PIN');
var red = this.getFieldValue('RED') || '0'
var green = this.getFieldValue('GREEN') || '0'
var blue = this.getFieldValue('BLUE') || '0'
Blockly.Arduino.libraries_['define_rgb_led' + dropdown_pin] = '#include <Adafruit_NeoPixel.h>\n Adafruit_NeoPixel rgb_led_' + dropdown_pin + ' = Adafruit_NeoPixel(1,' + dropdown_pin + ',NEO_RGB + NEO_KHZ800);\n';
Blockly.Arduino.setupCode_['setup_rgb_led' + dropdown_pin] = 'rgb_led_' + dropdown_pin + '.begin();';
var code = 'rgb_led_' + dropdown_pin + '.setPixelColor(0,rgb_led_' + dropdown_pin + '.Color(' + red + ',' + green + ',' + blue + '));\n';
code += 'rgb_led_' + dropdown_pin + '.show();';
return code;
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Block, Value, Field, Shadow, Category } from '../';
import { getColour } from '../helpers/colour'
import '@blockly/block-plus-minus';
class Toolbox extends React.Component {
@ -23,6 +24,9 @@ class Toolbox extends React.Component {
<Block type="sensebox_wifi" />
<Block type="sensebox_startap" />
</Category>
<Category name="LED" colour={getColour().sensebox}>
<Block type="sensebox_rgb_led" />
</Category>
<Category name="Display" colour={getColour().sensebox}>
<Block type="sensebox_display_beginDisplay" />
<Block type="sensebox_display_show" />

View File

@ -0,0 +1,57 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import BlocklyWindow from '../Blockly/BlocklyWindow';
import SolutionCheck from './SolutionCheck';
import CodeViewer from '../CodeViewer';
import Grid from '@material-ui/core/Grid';
import Card from '@material-ui/core/Card';
import Typography from '@material-ui/core/Typography';
class Assessment extends Component {
render() {
var tutorialId = this.props.currentTutorialId;
var currentTask = this.props.step;
var status = this.props.status.filter(status => status.id === tutorialId)[0];
var taskIndex = status.tasks.findIndex(task => task.id === currentTask.id);
var statusTask = status.tasks[taskIndex];
return (
<div style={{width: '100%'}}>
<Typography variant='h4' style={{marginBottom: '5px'}}>{currentTask.headline}</Typography>
<Grid container spacing={2} style={{marginBottom: '5px'}}>
<Grid item xs={12} md={6} lg={8} style={{ position: 'relative' }}>
<SolutionCheck />
<BlocklyWindow initialXml={statusTask.xml ? statusTask.xml : null}/>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<Card style={{height: 'calc(50% - 30px)', padding: '10px', marginBottom: '10px'}}>
<Typography variant='h5'>Arbeitsauftrag</Typography>
<Typography>{currentTask.text1}</Typography>
</Card>
<div style={{height: '50%'}}>
<CodeViewer />
</div>
</Grid>
</Grid>
</div>
);
};
}
Assessment.propTypes = {
currentTutorialId: PropTypes.number,
status: PropTypes.array.isRequired,
change: PropTypes.number.isRequired
};
const mapStateToProps = state => ({
change: state.tutorial.change,
status: state.tutorial.status,
currentTutorialId: state.tutorial.currentId
});
export default connect(mapStateToProps, null)(Assessment);

View File

@ -5,8 +5,6 @@ import { connect } from 'react-redux';
import Hardware from './Hardware';
import BlocklyWindow from '../Blockly/BlocklyWindow';
import { tutorials } from './tutorials';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';

View File

@ -1,13 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { tutorialCheck } from '../../actions/tutorialActions';
import { tutorialCheck, tutorialStep } from '../../actions/tutorialActions';
import * as Blockly from 'blockly/core';
import { withRouter } from 'react-router-dom';
import Compile from '../Compile';
import { tutorials } from './tutorials';
import tutorials from './tutorials.json';
import { checkXml } from './compareXml';
import { withStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
@ -48,15 +49,16 @@ class SolutionCheck extends Component {
}
check = () => {
const workspace = Blockly.getMainWorkspace();
var msg = tutorials[this.props.currentTutorialId].test(workspace);
this.props.tutorialCheck(msg.type);
const tutorial = tutorials.filter(tutorial => tutorial.id === this.props.currentTutorialId)[0];
const step = tutorial.steps[this.props.activeStep];
var msg = checkXml(step.xml, this.props.xml);
this.props.tutorialCheck(msg.type, step);
this.setState({ msg, open: true });
}
render() {
const steps = tutorials.filter(tutorial => tutorial.id === this.props.currentTutorialId)[0].steps;
return (
tutorials[this.props.currentTutorialId].test ?
<div>
<Tooltip title='Lösung kontrollieren'>
<IconButton
@ -74,14 +76,25 @@ class SolutionCheck extends Component {
{this.state.msg.type === 'success' ?
<div style={{marginTop: '20px', display: 'flex'}}>
<Compile />
{this.props.activeStep === steps.length-1 ?
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.history.push(`/tutorial/${this.props.currentTutorialId+2}`)}}
onClick={() => {this.toggleDialog(); this.props.history.push(`/tutorial/`)}}
>
nächstes Tutorial
Tutorials-Übersicht
</Button>
:
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.tutorialStep(this.props.activeStep + 1)}}
>
nächster Schritt
</Button>
}
</div>
: null}
</DialogContent>
@ -92,18 +105,23 @@ class SolutionCheck extends Component {
</DialogActions>
</Dialog>
</div>
: null
);
};
}
SolutionCheck.propTypes = {
tutorialCheck: PropTypes.func.isRequired,
currentTutorialId: PropTypes.number
tutorialStep: PropTypes.func.isRequired,
currentTutorialId: PropTypes.number,
activeStep: PropTypes.number.isRequired,
xml: PropTypes.string.isRequired
};
const mapStateToProps = state => ({
currentTutorialId: state.tutorial.currentId
currentTutorialId: state.tutorial.currentId,
activeStep: state.tutorial.activeStep,
xml: state.workspace.code.xml
});
export default connect(mapStateToProps, { tutorialCheck })(withStyles(styles, {withTheme: true})(SolutionCheck));
export default connect(mapStateToProps, { tutorialCheck, tutorialStep })(withStyles(styles, {withTheme: true})(withRouter(SolutionCheck)));

View File

@ -6,7 +6,7 @@ import { withRouter } from 'react-router-dom';
import clsx from 'clsx';
import { tutorials } from './tutorials';
import tutorials from './tutorials.json';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
@ -21,6 +21,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const styles = (theme) => ({
stepper: {
width: 'calc(100% - 40px)',
height: '40px',
borderRadius: '25px',
padding: '0 20px',
margin: '20px 0',
@ -51,31 +52,44 @@ class StepperHorizontal extends Component {
render() {
var tutorialId = this.props.currentTutorialId;
var tutorialStatus = this.props.status[tutorialId].status === 'success' ? 'Success' :
this.props.status[tutorialId].status === 'error' ? 'Error' : 'Other';
var status = this.props.status.filter(status => status.id === tutorialId)[0];
var tasks = status.tasks;
var error = tasks.filter(task => task.type === 'error').length > 0;
var success = tasks.filter(task => task.type === 'success').length / tasks.length;
var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other';
return (
<div className={clsx(this.props.classes.stepper, this.props.classes['stepper'+tutorialStatus])}>
<div style={{position: 'relative'}}>
{error || success > 0 ?
<div style={{zIndex: -1, width: error ? 'calc(100% - 40px)' : `calc(${success*100}% - 40px)`, borderRadius: success === 1 || error ? '25px' : '25px 0 0 25px', position: 'absolute', margin: 0, left: 0}} className={clsx(this.props.classes.stepper, error ? this.props.classes.stepperError : this.props.classes.stepperSuccess)}>
</div>
: null}
{success < 1 && !error ?
<div style={{zIndex: -2, width: `calc(${(1-success)*100}% - 40px)`, borderRadius: success === 0 ? '25px' : '0px 25px 25px 0', position: 'absolute', margin: 0, right: 0}} className={clsx(this.props.classes.stepper, this.props.classes.stepperOther)}>
</div>
: null}
<div className={this.props.classes.stepper}>
<Button
disabled={tutorialId === 0}
onClick={() => {this.props.history.push(`/tutorial/${tutorialId}`)}}
disabled={tutorialId === 1}
onClick={() => {this.props.history.push(`/tutorial/${tutorialId-1}`)}}
>
{'<'}
</Button>
<Stepper activeStep={tutorialId+1} orientation="horizontal"
<Stepper activeStep={tutorialId} orientation="horizontal"
style={{padding: 0}} classes={{root: this.props.classes.color}}>
<Step expanded completed={false}>
<StepLabel icon={tutorialStatus !== 'Other' ? <div className={clsx(tutorialStatus === 'Error' ? this.props.classes.iconDivError: this.props.classes.iconDivSuccess)}><FontAwesomeIcon className={this.props.classes.icon} icon={tutorialStatus === 'Success' ? faCheck : faTimes}/></div> : ''}>
<h1 style={{margin: 0}}>{tutorials[tutorialId].title}</h1>
<StepLabel icon={tutorialStatus !== 'Other' ? <div className={tutorialStatus === 'Success' && success === 1 ? this.props.classes.iconDivSuccess : this.props.classes.iconDivError}><FontAwesomeIcon className={this.props.classes.icon} icon={tutorialStatus === 'Success' ? faCheck : faTimes}/></div> : ''}>
<h1 style={{margin: 0}}>{tutorials.filter(tutorial => tutorial.id === tutorialId)[0].title}</h1>
</StepLabel>
</Step>
</Stepper>
<Button
disabled={tutorialId+2 > tutorials.length}
onClick={() => {this.props.history.push(`/tutorial/${tutorialId+2}`)}}
disabled={tutorialId+1 > tutorials.length}
onClick={() => {this.props.history.push(`/tutorial/${tutorialId+1}`)}}
>
{'>'}
</Button>
</div>
</div>
);
};
}

View File

@ -1,17 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { tutorialStep } from '../../actions/tutorialActions';
import { withRouter, Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import clsx from 'clsx';
import { tutorials } from './tutorials';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
import Button from '@material-ui/core/Button';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
@ -26,30 +23,23 @@ const styles = (theme) => ({
borderStyle: `solid`,
// borderWidth: '2px',
borderRadius: '50%',
borderColor: theme.palette.secondary.main,
width: '12px',
height: '12px',
margin: '0 auto'
},
stepIconMedium: {
width: '18px',
height: '18px',
margin: '0 auto',
},
stepIconLarge: {
width: '24px',
height: '24px'
},
stepIconTransparent: {
borderColor: `transparent`,
cursor: 'default'
},
stepIconSuccess: {
stepIconLargeSuccess: {
borderColor: theme.palette.primary.main,
},
stepIconError: {
stepIconLargeError: {
borderColor: theme.palette.error.dark,
},
stepIconOther: {
borderColor: theme.palette.secondary.main,
stepIconActiveOther: {
backgroundColor: theme.palette.secondary.main
},
stepIconActiveSuccess: {
backgroundColor: fade(theme.palette.primary.main, 0.6)
@ -57,145 +47,64 @@ const styles = (theme) => ({
stepIconActiveError: {
backgroundColor: fade(theme.palette.error.dark, 0.6)
},
stepIconActiveOther: {
backgroundColor: fade(theme.palette.secondary.main, 0.6)
},
progress: {
position: 'absolute',
top: 0,
right: 0,
marginRight: '5px',
width: '3px',
},
progressForeground: {
backgroundColor: theme.palette.primary.main
},
progressBackground: {
backgroundColor: fade(theme.palette.primary.main, 0.2),
height: '100%',
borderRadius: '2px'
connector: {
height: '10px',
borderLeft: `2px solid black`,
margin: 'auto'
}
});
class StepperVertical extends Component {
constructor(props){
super(props);
this.state = {
tutorialArray: props.currentTutorialId === 0 ?
tutorials.slice(props.currentTutorialId, props.currentTutorialId+5)
: props.currentTutorialId === 1 ?
tutorials.slice(props.currentTutorialId-1, props.currentTutorialId+4)
: props.currentTutorialId === tutorials.length-1 ?
tutorials.slice(props.currentTutorialId-4, props.currentTutorialId+5)
: props.currentTutorialId === tutorials.length-2 ?
tutorials.slice(props.currentTutorialId-3, props.currentTutorialId+4)
: tutorials.slice(props.currentTutorialId-2, props.currentTutorialId+3),
selectedVerticalTutorialId: props.currentTutorialId
};
componentDidMount(){
this.props.tutorialStep(0);
}
componentDidUpdate(props){
if(props.currentTutorialId !== this.props.currentTutorialId){
this.setState({
tutorialArray: this.props.currentTutorialId === 0 ?
tutorials.slice(this.props.currentTutorialId, this.props.currentTutorialId+5)
: this.props.currentTutorialId === 1 ?
tutorials.slice(this.props.currentTutorialId-1, this.props.currentTutorialId+4)
: this.props.currentTutorialId === tutorials.length-1 ?
tutorials.slice(this.props.currentTutorialId-4, this.props.currentTutorialId+5)
: this.props.currentTutorialId === tutorials.length-2 ?
tutorials.slice(this.props.currentTutorialId-3, this.props.currentTutorialId+4)
: tutorials.slice(this.props.currentTutorialId-2, this.props.currentTutorialId+3),
selectedVerticalTutorialId: this.props.currentTutorialId
});
if(props.currentTutorialId !== Number(this.props.match.params.tutorialId)){
this.props.tutorialStep(0);
}
}
verticalStepper = (step) => {
var newTutorialId = this.state.selectedVerticalTutorialId + step;
var tutorialArray = newTutorialId === 0 ?
tutorials.slice(newTutorialId, newTutorialId+5)
: newTutorialId === 1 ?
tutorials.slice(newTutorialId-1, newTutorialId+4)
: newTutorialId === tutorials.length-1 ?
tutorials.slice(newTutorialId-4, newTutorialId+5)
: newTutorialId === tutorials.length-2 ?
tutorials.slice(newTutorialId-3, newTutorialId+4)
: tutorials.slice(newTutorialId-2, newTutorialId+3);
this.setState({ tutorialArray: tutorialArray, selectedVerticalTutorialId: newTutorialId });
}
render() {
var tutorialId = this.props.currentTutorialId;
var selectedVerticalTutorialId = this.state.selectedVerticalTutorialId;
var steps = this.props.steps;
var activeStep = this.props.activeStep;
var tutorialStatus = this.props.status.filter(status => status.id === this.props.currentTutorialId)[0];
return (
isWidthUp('sm', this.props.width) ?
<div style={{marginRight: '10px'}}>
<Button
style={{minWidth: '30px', margin: 'auto', minHeight: '25px', padding: '0', writingMode: 'vertical-rl'}}
disabled={selectedVerticalTutorialId === 0}
onClick={() => {this.verticalStepper(-1)}}
>
{'<'}
</Button>
<div style={{display: 'flex', height: 'max-content', width: 'max-content'}}>
<div style={{position: 'relative'}}>
<div
className={clsx(this.props.classes.progress, this.props.classes.progressForeground)}
style={{ zIndex: 1, borderRadius: `${selectedVerticalTutorialId/(tutorials.length-1) === 1 ? '2px' : '2px 2px 0 0'}`, height: `${((selectedVerticalTutorialId+1)/tutorials.length)*100}%`}}>
</div>
<div className={clsx(this.props.classes.progress, this.props.classes.progressBackground)}>
</div>
</div>
<Stepper
activeStep={tutorialId+1}
activeStep={activeStep}
orientation="vertical"
connector={<div style={{height: '10px'}}></div>}
connector={<div className={this.props.classes.connector}></div>}
classes={{root: this.props.classes.verticalStepper}}
>
{this.state.tutorialArray.map((tutorial, i) => {
var index = this.state.tutorialArray.indexOf(tutorials[selectedVerticalTutorialId]);
var verticalTutorialId = i === index ? selectedVerticalTutorialId+1 : selectedVerticalTutorialId+1 - index + i;
var tutorialStatus = this.props.status[verticalTutorialId-1].status === 'success' ? 'Success' :
this.props.status[verticalTutorialId-1].status === 'error' ? 'Error' : 'Other';
{steps.map((step, i) => {
var tasksIndex = tutorialStatus.tasks.findIndex(task => task.id === step.id);
var taskType = tasksIndex > -1 ? tutorialStatus.tasks[tasksIndex].type : null;
var taskStatus = taskType === 'success' ? 'Success' : taskType === 'error' ? 'Error' : 'Other';
return (
<Step key={i}>
<Tooltip title={Object.keys(tutorial).length > 0 ? tutorial.title : ''} placement='right' arrow >
<Link to={`/tutorial/${verticalTutorialId}`}>
<Tooltip title={step.headline} placement='right' arrow >
<div style={i === activeStep ? {padding: '5px 0'} : {padding: '5px 0', cursor: 'pointer'}} onClick={i === activeStep ? null : () => {this.props.tutorialStep(i)}}>
<StepLabel
StepIconComponent={'div'}
classes={{
root: tutorial === tutorials[selectedVerticalTutorialId] ?
tutorial === tutorials[tutorialId] ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIcon'+tutorialStatus], this.props.classes['stepIconActive'+tutorialStatus])
: clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIcon'+tutorialStatus])
: tutorial === tutorials[selectedVerticalTutorialId-1] || tutorial === tutorials[selectedVerticalTutorialId+1] ||
tutorial === tutorials[verticalTutorialId-2] ?
tutorial === tutorials[tutorialId] ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconMedium, this.props.classes['stepIcon'+tutorialStatus], this.props.classes['stepIconActive'+tutorialStatus])
: clsx(this.props.classes.stepIcon, this.props.classes.stepIconMedium, this.props.classes['stepIcon'+tutorialStatus])
: tutorial === tutorials[tutorialId] ?
clsx(this.props.classes.stepIcon, this.props.classes['stepIcon'+tutorialStatus], this.props.classes['stepIconActive'+tutorialStatus])
: clsx(this.props.classes.stepIcon, this.props.classes['stepIcon'+tutorialStatus])
root: step.type === 'task' ?
i === activeStep ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIconLarge'+taskStatus], this.props.classes['stepIconActive'+taskStatus])
: clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIconLarge'+taskStatus])
: i === activeStep ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconActiveOther)
: clsx(this.props.classes.stepIcon)
}}
>
</StepLabel>
</Link>
</div>
</Tooltip>
</Step>
)})}
</Stepper>
</div>
<Button
style={{minWidth: '30px', minHeight: '25px', padding: '0', writingMode: 'vertical-rl'}}
disabled={selectedVerticalTutorialId === tutorials.length-1}
onClick={() => {this.verticalStepper(1)}}
>
{'>'}
</Button>
</div>
: null
);
};
}
@ -204,13 +113,16 @@ class StepperVertical extends Component {
StepperVertical.propTypes = {
status: PropTypes.array.isRequired,
change: PropTypes.number.isRequired,
currentTutorialId: PropTypes.number.isRequired
currentTutorialId: PropTypes.number.isRequired,
activeStep: PropTypes.number.isRequired,
tutorialStep: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
change: state.tutorial.change,
status: state.tutorial.status,
currentTutorialId: state.tutorial.currentId
currentTutorialId: state.tutorial.currentId,
activeStep: state.tutorial.activeStep
});
export default connect(mapStateToProps, null)(withRouter(withStyles(styles, {withTheme: true})(withWidth()(StepperVertical))));
export default connect(mapStateToProps, { tutorialStep })(withRouter(withStyles(styles, {withTheme: true})(StepperVertical)));

View File

@ -1,35 +1,29 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { tutorialId, setTutorialLevel } from '../../actions/tutorialActions';
import { tutorialId, tutorialStep } from '../../actions/tutorialActions';
import Breadcrumbs from '../Breadcrumbs';
import StepperHorizontal from './StepperHorizontal';
import StepperVertical from './StepperVertical';
import Instruction from './Instruction';
import BlocklyWindow from '../Blockly/BlocklyWindow';
import SolutionCheck from './SolutionCheck';
import CodeViewer from '../CodeViewer';
import Assessment from './Assessment';
import NotFound from '../NotFound';
import { tutorials } from './tutorials';
import tutorials from './tutorials.json';
import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Grid from '@material-ui/core/Grid';
import Card from '@material-ui/core/Card';
import Button from '@material-ui/core/Button';
class Tutorial extends Component {
componentDidMount(){
this.props.tutorialId(Number(this.props.match.params.tutorialId)-1);
this.props.tutorialId(Number(this.props.match.params.tutorialId));
}
componentDidUpdate(props, state){
if(props.currentTutorialId+1 !== Number(this.props.match.params.tutorialId)){
this.props.tutorialId(Number(this.props.match.params.tutorialId)-1);
this.props.setTutorialLevel('instruction');
if(props.currentTutorialId !== Number(this.props.match.params.tutorialId)){
this.props.tutorialId(Number(this.props.match.params.tutorialId));
}
}
@ -37,57 +31,33 @@ class Tutorial extends Component {
this.props.tutorialId(null);
}
onChange = (e, value) => {
this.props.setTutorialLevel(value);
}
render() {
var currentTutorialId = this.props.currentTutorialId;
console.log(this.props);
var tutorial = tutorials.filter(tutorial => tutorial.id === currentTutorialId)[0];
var steps = tutorial ? tutorial.steps : null;
var step = steps ? steps[this.props.activeStep] : null;
return (
!Number.isInteger(currentTutorialId) || currentTutorialId+1 < 1 || currentTutorialId+1 > tutorials.length ?
!Number.isInteger(currentTutorialId) || currentTutorialId < 1 || currentTutorialId > tutorials.length ?
<NotFound button={{title: 'Zurück zur Tutorials-Übersicht', link: '/tutorial'}}/>
:
<div>
<Breadcrumbs content={[{link: '/', title: 'Home'},{link: '/tutorial', title: 'Tutorial'}, {link: `/tutorial/${currentTutorialId+1}`, title: tutorials[currentTutorialId].title}]}/>
<Breadcrumbs content={[{link: '/', title: 'Home'},{link: '/tutorial', title: 'Tutorial'}, {link: `/tutorial/${currentTutorialId}`, title: tutorial.title}]}/>
<StepperHorizontal />
<div style={{display: 'flex'}}>
<StepperVertical />
{/* width of vertical stepper is 30px*/}
<Card style={{width: isWidthUp('sm', this.props.width) ? 'calc(100% - 30px)' : '100%', padding: '10px'}}>
<Tabs
value={this.props.level}
indicatorColor="primary"
textColor="inherit"
variant='fullWidth'
onChange={this.onChange}
>
<Tab label="Anleitung" value='instruction' disableRipple/>
<Tab label="Aufgabe" value='assessment' disableRipple/>
</Tabs>
<div style={{marginTop: '20px'}}>
{this.props.level === 'instruction' ?
<Instruction /> : null }
{this.props.level === 'assessment' ?
<Grid container spacing={2}>
<Grid item xs={12} md={6} lg={8} style={{ position: 'relative' }}>
<SolutionCheck />
<BlocklyWindow initialXml={this.props.status[currentTutorialId].xml ? this.props.status[currentTutorialId].xml : null}/>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<Card style={{height: 'calc(50% - 30px)', padding: '10px', marginBottom: '10px'}}>
Hier könnte die Problemstellung stehen.
</Card>
<div style={{height: '50%'}}>
<CodeViewer />
</div>
</Grid>
</Grid>
<StepperVertical steps={steps}/>
{/* calc(Card-padding: 10px + Button-height: 35px + Button-marginTop: 15px)*/}
<Card style={{padding: '10px 10px 60px 10px', display: 'block', position: 'relative', height: 'max-content', width: '100%'}}>
{step ?
step.type === 'instruction' ?
<Instruction step={step}/>
: <Assessment step={step}/> // if step.type === 'assessment'
: null}
<div style={{marginTop: '20px', position: 'absolute', bottom: '10px'}}>
<Button style={{marginRight: '10px', height: '35px'}} variant='contained' disabled={this.props.activeStep === 0} onClick={() => this.props.tutorialStep(this.props.activeStep-1)}>Zurück</Button>
<Button style={{height: '35px'}}variant='contained' color='primary' disabled={this.props.activeStep === tutorial.steps.length-1} onClick={() => this.props.tutorialStep(this.props.activeStep+1)}>Weiter</Button>
</div>
</Card>
</div>
@ -98,18 +68,18 @@ class Tutorial extends Component {
Tutorial.propTypes = {
tutorialId: PropTypes.func.isRequired,
setTutorialLevel: PropTypes.func.isRequired,
tutorialStep: PropTypes.func.isRequired,
currentTutorialId: PropTypes.number,
status: PropTypes.array.isRequired,
change: PropTypes.number.isRequired,
level: PropTypes.string.isRequired
activeStep: PropTypes.number.isRequired
};
const mapStateToProps = state => ({
change: state.tutorial.change,
status: state.tutorial.status,
currentTutorialId: state.tutorial.currentId,
level: state.tutorial.level
activeStep: state.tutorial.activeStep
});
export default connect(mapStateToProps, { tutorialId, setTutorialLevel })(withWidth()(Tutorial));
export default connect(mapStateToProps, { tutorialId, tutorialStep })(Tutorial);

View File

@ -6,7 +6,7 @@ import clsx from 'clsx';
import Breadcrumbs from '../Breadcrumbs';
import { tutorials } from './tutorials';
import tutorials from './tutorials.json';
import { Link } from 'react-router-dom';
@ -14,6 +14,7 @@ import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -21,20 +22,23 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const styles = (theme) => ({
outerDiv: {
position: 'absolute',
right: '-29px',
bottom: '-29px',
width: '140px',
height: '140px',
borderStyle: 'solid',
borderWidth: '10px',
borderRadius: '50%',
borderColor: fade(theme.palette.primary.main, 0.2),
color: fade(theme.palette.primary.main, 0.2)
right: '-30px',
bottom: '-30px',
width: '160px',
height: '160px',
color: fade(theme.palette.secondary.main, 0.6)
},
outerDivError: {
borderColor: fade(theme.palette.error.dark, 0.2),
stroke: fade(theme.palette.error.dark, 0.2),
color: fade(theme.palette.error.dark, 0.2)
},
outerDivSuccess: {
stroke: fade(theme.palette.primary.main, 0.2),
color: fade(theme.palette.primary.main, 0.2)
},
outerDivOther: {
stroke: fade(theme.palette.secondary.main, 0.2)
},
innerDiv: {
width: 'inherit',
height: 'inherit',
@ -55,24 +59,38 @@ class TutorialHome extends Component {
<h1>Tutorial-Übersicht</h1>
<Grid container spacing={2}>
{tutorials.map((tutorial, i) => {
var tutorialStatus = this.props.status[i].status === 'success' ? 'Success' :
this.props.status[i].status === 'error' ? 'Error' : 'Other';
var steps = tutorial.steps;
var tasks = steps.filter(task => task.type === 'task');
var status = this.props.status.filter(status => status.id === tutorial.id)[0];
var error = status.tasks.filter(task => task.type === 'error').length > 0;
var success = status.tasks.filter(task => task.type === 'success').length/tasks.length
var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other';
return (
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
<Link to={`/tutorial/${i+1}`} style={{textDecoration: 'none', color: 'inherit'}}>
<Link to={`/tutorial/${tutorial.id}`} style={{textDecoration: 'none', color: 'inherit'}}>
<Paper style={{height: '150px', padding: '10px', position:'relative', overflow: 'hidden'}}>
{tutorials[i].title}
{tutorialStatus !== 'Other' ?
<div className={clsx(this.props.classes.outerDiv, tutorialStatus === 'Error' ? this.props.classes.outerDivError : null)}>
{tutorial.title}
<div className={clsx(this.props.classes.outerDiv)} style={{width: '160px', height: '160px', border: 0}}>
<svg style={{width: '100%', height: '100%'}}>
{error || success === 1 ?
<circle className={error ? this.props.classes.outerDivError : this.props.classes.outerDivSuccess} style={{transform: 'rotate(-44deg)', transformOrigin: "50% 50%"}} r="75" cx="50%" cy="50%" fill="none" stroke-width="10"></circle>
: <circle className={this.props.classes.outerDivOther} style={{transform: 'rotate(-44deg)', transformOrigin: "50% 50%"}} r="75" cx="50%" cy="50%" fill="none" stroke-width="10" stroke-dashoffset={`${(75*2*Math.PI)*(1-(50/100+success/2))}`} stroke-dasharray={`${(75*2*Math.PI)*(1-(50/100-success/2))} ${(75*2*Math.PI)*(1-(50/100+success/2))}`}></circle>}
{success < 1 && !error ?
<circle className={this.props.classes.outerDivSuccess} style={{transform: 'rotate(-44deg)', transformOrigin: "50% 50%"}} r="75" cx="50%" cy="50%" fill="none" stroke-width="10" stroke-dashoffset={`${(75*2*Math.PI)*(1-(50/100+success/2))}`} stroke-dasharray={`${(75*2*Math.PI)}`}>
</circle>
: null}
</svg>
</div>
<div className={clsx(this.props.classes.outerDiv, tutorialStatus === 'Error' ? this.props.classes.outerDivError : tutorialStatus === 'Success' ? this.props.classes.outerDivSuccess : null)}>
<div className={this.props.classes.innerDiv}>
{error || success === 1 ?
<FontAwesomeIcon size='4x' icon={tutorialStatus === 'Success' ? faCheck : faTimes}/>
</div>
</div>
: null
: <Typography variant='h3' className={success > 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success*100)}%</Typography>
}
</div>
</div>
</Paper>
</Link>
</Grid>
)})}
</Grid>

View File

@ -0,0 +1,78 @@
export const checkXml = (originalXmlString, userXmlString) => {
var originalXml = parseXml(originalXmlString);
var userXml = parseXml(userXmlString);
return compareXml(originalXml, userXml);
};
const parseXml = (xmlString) => {
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(xmlString, "text/xml");
return xmlDoc;
};
const compareNumberOfBlocks = (originalBlocks, userBlocks) => {
if(originalBlocks.length !== userBlocks.length){
if(originalBlocks.length > userBlocks.length){
return {text: 'Es wurden zu wenig Blöcke verwendet.', type: 'error'};
}
else {
return {text: 'Es wurden zu viele Blöcke verwendet.', type: 'error'};
}
}
};
const compareBlockType = (originalBlock, userBlock, index) => {
if(originalBlock.attributes['type'].value !== userBlock.attributes['type'].value){
return {text: `Es wurde ein falscher Blocktyp an Position ${index+1} verwendet`, type: 'error'};
}
};
const compareParentBlock = (originalBlock, userBlock, index) => {
// using parentNode instead of parenElement
// see https://stackoverflow.com/questions/8685739/difference-between-dom-parentnode-and-parentelement
if(originalBlock.parentNode.attributes['name']){
if(userBlock.parentNode.attributes['name']){
// do the blocks have the same name-properties?
if(originalBlock.parentNode.attributes['name'].value !== userBlock.parentNode.attributes['name'].value){
if(userBlock.parentNode.attributes['name'].value === 'LOOP_FUNC' || userBlock.parentNode.attributes['name'].value === 'SETUP_FUNC'){
return {text: `Der Block mit dem Typen '${userBlock.attributes['type'].value}' wurde irrtümlicherweise in die ${userBlock.parentNode.attributes['name'].value === 'SETUP_FUNC' ? 'Setup' : 'Endlosschleifen'}-Funktion geschrieben.
Verschiebe den gesamten Block (und alle dazugehörigen Blöcke) in die ${userBlock.parentNode.attributes['name'].value !== 'SETUP_FUNC' ? 'Setup' : 'Endlosschleifen'}-Funktion.`, type: 'error'};
}
// TODO: has a block two name-properties?
return {text: `Der Block mit dem Typen '${userBlock.attributes['type'].value}' hat ein falsches 'name'-Attribut`, type: 'error'};
}
}
// user-block has not a name-attribute
else {
// do the user-block has a xmlns-attribute -> user-block is not connected
if(userBlock.parentNode.attributes['xmlns']){
return {text: `Der Block mit dem Typen '${userBlock.attributes['type'].value}' hat keine Verbindung zu einem anderen Block.`, type: 'error'};
}
// user-block has not a xmlns- AND name-attribute
else {
return {text: `Der Block an Position ${index+1} ist falsch eingeordnet. Tipp: Block an Position ${index+1} einem vorherigen Block unterordnen.`, type: 'error'};
}
}
}
};
const compareXml = (originalXml, userXml) => {
var originalItemList = originalXml.getElementsByTagName("block");
var userItemList = userXml.getElementsByTagName("block");
// compare number of blocks
var number = compareNumberOfBlocks(originalItemList, userItemList);
if(number){return number;}
for(var i=0; i < originalItemList.length; i++){
// compare type
var type = compareBlockType(originalItemList[i], userItemList[i], i);
if(type){return type;}
// compare name
var parent = compareParentBlock(originalItemList[i], userItemList[i], i);
if(parent){return parent;}
}
return {text: 'Super. Alles richtig!', type: 'success'};
};

View File

@ -18,6 +18,23 @@ export const tutorials = [
</block>
</xml>`
},
"solution": `<xml xmlns="https://developers.google.com/blockly/xml">
<block type="arduino_functions" id="QWW|$jB8+*EL;}|#uA" deletable="false" x="37" y="20">
<statement name="LOOP_FUNC">
<block type="sensebox_telegram_do" id="K%yUabqRVQ{]9eX-8jZD">
<statement name="telegram_do">
<block type="controls_if" id="rA6:!p7,{y2MOuVpv[Pm">
<value name="IF0">
<block type="logic_boolean" id="=[Zh}O6_)fl?JD#2)2bL">
<field name="BOOL">TRUE</field>
</block>
</value>
</block>
</statement>
</block>
</statement>
</block>
</xml>`,
"test": function(workspace){
var wifi = workspace.getBlocksByType('sensebox_wifi'); // result is an array with Blocks as objects
if(wifi.length > 0){

View File

@ -42,7 +42,7 @@ class WorkspaceStats extends Component {
style={{ marginRight: '1rem' }}
color="primary"
avatar={<Avatar><FontAwesomeIcon icon={faPlus} /></Avatar>}
label={this.props.create > 0 ? this.props.create : 0}> // initialXML is created automatically, Block is not part of the statistics
label={this.props.create > 0 ? this.props.create : 0}> {/* initialXML is created automatically, Block is not part of the statistics */}
</Chip>
</Tooltip>
<Tooltip title="Anzahl veränderter Blöcke" >
@ -58,7 +58,7 @@ class WorkspaceStats extends Component {
style={{ marginRight: '1rem' }}
color="primary"
avatar={<Avatar><FontAwesomeIcon icon={faArrowsAlt} /></Avatar>}
label={this.props.move > 0 ? this.props.move : 0}> // initialXML is moved automatically, Block is not part of the statistics
label={this.props.move > 0 ? this.props.move : 0}> {/* initialXML is moved automatically, Block is not part of the statistics */}
</Chip>
</Tooltip>
<Tooltip title="Anzahl gelöschter Blöcke" >

View File

@ -1,13 +1,48 @@
import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_LEVEL } from '../actions/types';
import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from '../actions/types';
import { tutorials } from '../components/Tutorial/tutorials';
import tutorials from '../components/Tutorial/tutorials.json';
const initialStatus = () => {
if(window.localStorage.getItem('status')){
var status = JSON.parse(window.localStorage.getItem('status'));
var existingTutorialIds = [];
for(var i = 0; i < tutorials.length; i++){
var tutorialsId = tutorials[i].id
existingTutorialIds.push(tutorialsId);
if(status.findIndex(status => status.id === tutorialsId) > -1){
var tasks = tutorials[i].steps.filter(step => step.type === 'task');
var existingTaskIds = [];
for(var j = 0; j < tasks.length; j++){
var tasksId = tasks[j].id;
existingTaskIds.push(tasksId);
if(status[i].tasks.findIndex(task => task.id === tasksId) === -1){
// task does not exist
status[i].tasks.push({id: tasksId});
}
}
// deleting old tasks which do not longer exist
if(existingTaskIds.length > 0){
status[i].tasks = status[i].tasks.filter(task => existingTaskIds.indexOf(task.id) > -1);
}
}
else{
status.push({id: tutorialsId, tasks: new Array(tutorials[i].steps.filter(step => step.type === 'task').length).fill({})});
}
}
// deleting old tutorials which do not longer exist
if(existingTutorialIds.length > 0){
status = status.filter(status => existingTutorialIds.indexOf(status.id) > -1);
}
return status;
}
// window.localStorage.getItem('status') does not exist
return tutorials.map(tutorial => {return {id: tutorial.id, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => {return {id: task.id};})};});
};
const initialState = {
status: window.localStorage.getItem('tutorial') ?
JSON.parse(window.localStorage.getItem('tutorial'))
: new Array(tutorials.length).fill({}),
level: 'instruction',
status: initialStatus(),
currentId: null,
activeStep: 0,
change: 0
};
@ -17,7 +52,7 @@ export default function(state = initialState, action){
case TUTORIAL_ERROR:
case TUTORIAL_XML:
// update locale storage - sync with redux store
window.localStorage.setItem('tutorial', JSON.stringify(action.payload));
window.localStorage.setItem('status', JSON.stringify(action.payload));
return {
...state,
status: action.payload
@ -32,10 +67,10 @@ export default function(state = initialState, action){
...state,
currentId: action.payload
}
case TUTORIAL_LEVEL:
case TUTORIAL_STEP:
return {
...state,
level: action.payload
activeStep: action.payload
}
default:
return state;