diff --git a/package-lock.json b/package-lock.json index 6fd2bef..c24c9e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6a3180b..ff3634c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index edf070c..b741c3e 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -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'){ - var tutorialsStatus = getState().tutorial.status; - tutorialsStatus[id] = {...tutorialsStatus[id], xml: code}; - dispatch({ - type: TUTORIAL_XML, - payload: tutorialsStatus - }); + 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; + 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 + }); +}; diff --git a/src/actions/types.js b/src/actions/types.js index 9d26386..27bd18d 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -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'; diff --git a/src/components/Blockly/BlocklyWindow.js b/src/components/Blockly/BlocklyWindow.js index ca70826..7b66b81 100644 --- a/src/components/Blockly/BlocklyWindow.js +++ b/src/components/Blockly/BlocklyWindow.js @@ -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 ( + + + diff --git a/src/components/Tutorial/Assessment.js b/src/components/Tutorial/Assessment.js new file mode 100644 index 0000000..3e2db26 --- /dev/null +++ b/src/components/Tutorial/Assessment.js @@ -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 ( +
+ {currentTask.headline} + + + + + + + + Arbeitsauftrag + {currentTask.text1} + +
+ +
+
+
+
+ ); + }; +} + +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); diff --git a/src/components/Tutorial/Instruction.js b/src/components/Tutorial/Instruction.js index f128a92..c250d1c 100644 --- a/src/components/Tutorial/Instruction.js +++ b/src/components/Tutorial/Instruction.js @@ -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'; diff --git a/src/components/Tutorial/SolutionCheck.js b/src/components/Tutorial/SolutionCheck.js index bf05749..3156fc6 100644 --- a/src/components/Tutorial/SolutionCheck.js +++ b/src/components/Tutorial/SolutionCheck.js @@ -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,62 +49,79 @@ 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 ? -
- - this.check()} - > - - - - - {this.state.msg.type === 'error' ? 'Fehler' : 'Erfolg'} - - {this.state.msg.text} - {this.state.msg.type === 'success' ? +
+ + this.check()} + > + + + + + {this.state.msg.type === 'error' ? 'Fehler' : 'Erfolg'} + + {this.state.msg.text} + {this.state.msg.type === 'success' ?
- -
- : null} -
- - - -
-
- : null + {this.props.activeStep === steps.length-1 ? + + : + + } +
+ : 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))); diff --git a/src/components/Tutorial/StepperHorizontal.js b/src/components/Tutorial/StepperHorizontal.js index a914956..b54c1f7 100644 --- a/src/components/Tutorial/StepperHorizontal.js +++ b/src/components/Tutorial/StepperHorizontal.js @@ -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,30 +52,43 @@ 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 ( -
- - - -
: ''}> -

{tutorials[tutorialId].title}

- - - - +
+ {error || success > 0 ? +
+
+ : null} + {success < 1 && !error ? +
+
+ : null} +
+ + + +
: ''}> +

{tutorials.filter(tutorial => tutorial.id === tutorialId)[0].title}

+ + + + +
); }; diff --git a/src/components/Tutorial/StepperVertical.js b/src/components/Tutorial/StepperVertical.js index 44187c3..dcaf36e 100644 --- a/src/components/Tutorial/StepperVertical.js +++ b/src/components/Tutorial/StepperVertical.js @@ -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) ? -
- -
-
-
-
-
-
-
-
} - 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'; - return ( - - 0 ? tutorial.title : ''} placement='right' arrow > - - - - - - - )})} - -
- - - : null +
+
} + classes={{root: this.props.classes.verticalStepper}} + > + {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 ( + + +
{this.props.tutorialStep(i)}}> + + +
+
+
+ )})} + + ); }; } @@ -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))); diff --git a/src/components/Tutorial/Tutorial.js b/src/components/Tutorial/Tutorial.js index 710db70..e2a9d38 100644 --- a/src/components/Tutorial/Tutorial.js +++ b/src/components/Tutorial/Tutorial.js @@ -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,79 +31,55 @@ 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 ? : -
- +
+ - + -
- +
+ + {/* calc(Card-padding: 10px + Button-height: 35px + Button-marginTop: 15px)*/} + + {step ? + step.type === 'instruction' ? + + : // if step.type === 'assessment' + : null} - {/* width of vertical stepper is 30px*/} - - - - - - -
- {this.props.level === 'instruction' ? - : null } - {this.props.level === 'assessment' ? - - - - - - - - Hier könnte die Problemstellung stehen. - -
- -
-
-
- : null } -
-
-
+
+ + +
+
+
); }; } 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); diff --git a/src/components/Tutorial/TutorialHome.js b/src/components/Tutorial/TutorialHome.js index c00dc08..3b73ac7 100644 --- a/src/components/Tutorial/TutorialHome.js +++ b/src/components/Tutorial/TutorialHome.js @@ -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 {

Tutorial-Übersicht

{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 ( - + - {tutorials[i].title} - {tutorialStatus !== 'Other' ? -
-
+ {tutorial.title} +
+ + {error || success === 1 ? + + : } + {success < 1 && !error ? + + + : null} + +
+
+
+ {error || success === 1 ? -
+ : 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success*100)}% + }
- : null - } +
- )})} diff --git a/src/components/Tutorial/compareXml.js b/src/components/Tutorial/compareXml.js new file mode 100644 index 0000000..5ca4619 --- /dev/null +++ b/src/components/Tutorial/compareXml.js @@ -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'}; +}; diff --git a/src/components/Tutorial/tutorials.js b/src/components/Tutorial/tutorials.js index e16d078..5cdc203 100644 --- a/src/components/Tutorial/tutorials.js +++ b/src/components/Tutorial/tutorials.js @@ -18,6 +18,23 @@ export const tutorials = [ ` }, + "solution": ` + + + + + + + + TRUE + + + + + + + + `, "test": function(workspace){ var wifi = workspace.getBlocksByType('sensebox_wifi'); // result is an array with Blocks as objects if(wifi.length > 0){ diff --git a/src/components/WorkspaceStats.js b/src/components/WorkspaceStats.js index b18d4e3..9a98571 100644 --- a/src/components/WorkspaceStats.js +++ b/src/components/WorkspaceStats.js @@ -42,7 +42,7 @@ class WorkspaceStats extends Component { style={{ marginRight: '1rem' }} color="primary" 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 */} @@ -58,7 +58,7 @@ class WorkspaceStats extends Component { style={{ marginRight: '1rem' }} color="primary" 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 */} diff --git a/src/reducers/tutorialReducer.js b/src/reducers/tutorialReducer.js index e951cb0..ad80903 100644 --- a/src/reducers/tutorialReducer.js +++ b/src/reducers/tutorialReducer.js @@ -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;