diff --git a/README.md b/README.md index 54ef094..5b7a213 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,28 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + senseBox Logo -## Available Scripts +# React Ardublockly -In the project directory, you can run: +This repository contains the source code and documentation of [sensebox-ardublockly](https://sensebox-ardublockly.netlify.app/). -### `npm start` +This project was created with [Create React App](https://github.com/facebook/create-react-app) and represents the continuation or improvement of [blockly.sensebox.de](https://blockly.sensebox.de/ardublockly/?lang=de&board=sensebox-mcu). -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. -The page will reload if you make edits.
-You will also see any lint errors in the console. +## Getting Started -### `npm test` +1. [Download](https://github.com/sensebox/React-Ardublockly/archive/master.zip) or clone the GitHub Repository ``git clone https://github.com/sensebox/React-Ardublockly`` and checkout to branch ``master``. -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +2. install [Node.js v10.xx](https://nodejs.org/en/) on your local machine -### `npm run build` +3. open shell and navigate inside folder ``React-Ardublockly`` + * run ``npm install`` + * run ``npm start`` +4. open [localhost:3000](http://localhost:3000) -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. +## Troubleshoot +Ensure that line 14 in [store.js](https://github.com/sensebox/React-Ardublockly/blob/master/src/store.js#L14) is commented out or otherwise you have installed [Redux DevTools Extension](http://extension.remotedev.io/). -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `npm run build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +## Demo +A demo of the current status of the master branch can be accessed via [sensebox-ardublockly.netlify.app](https://sensebox-ardublockly.netlify.app/) :rocket:. +* [Home](https://sensebox-ardublockly.netlify.app/) +* [Tutorial Overview](https://sensebox-ardublockly.netlify.app/tutorial) +* [Tutorial-Builder](https://sensebox-ardublockly.netlify.app/tutorial/builder) diff --git a/package.json b/package.json index d241d76..a7a911e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@testing-library/user-event": "^7.2.1", "blockly": "^3.20200924.0", "file-saver": "^2.0.2", + "moment": "^2.28.0", "prismjs": "^1.20.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..ad37e2c --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/public/media/hardware/resistor.png b/public/media/hardware/resistor.png deleted file mode 100644 index e6f1458..0000000 Binary files a/public/media/hardware/resistor.png and /dev/null differ diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index b741c3e..c902f70 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -1,6 +1,6 @@ import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types'; -import tutorials from '../components/Tutorial/tutorials.json'; +import tutorials from '../data/tutorials.json'; export const tutorialChange = () => (dispatch) => { dispatch({ diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js new file mode 100644 index 0000000..f200cf0 --- /dev/null +++ b/src/actions/tutorialBuilderActions.js @@ -0,0 +1,263 @@ +import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from './types'; + +import data from '../data/hardware.json'; + +export const changeTutorialBuilder = () => (dispatch) => { + dispatch({ + type: BUILDER_CHANGE + }); +}; + +export const jsonString = (json) => (dispatch) => { + dispatch({ + type: JSON_STRING, + payload: json + }); +}; + +export const tutorialTitle = (title) => (dispatch) => { + dispatch({ + type: BUILDER_TITLE, + payload: title + }); + dispatch(changeTutorialBuilder()); +}; + +export const tutorialSteps = (steps) => (dispatch) => { + dispatch({ + type: BUILDER_ADD_STEP, + payload: steps + }); + dispatch(changeTutorialBuilder()); +}; + +export const tutorialId = (id) => (dispatch) => { + dispatch({ + type: BUILDER_ID, + payload: id + }); + dispatch(changeTutorialBuilder()); +}; + +export const addStep = (index) => (dispatch, getState) => { + var steps = getState().builder.steps; + var step = { + id: index+1, + type: 'instruction', + headline: '', + text: '' + }; + steps.splice(index, 0, step); + dispatch({ + type: BUILDER_ADD_STEP, + payload: steps + }); + dispatch(addErrorStep(index)); + dispatch(changeTutorialBuilder()); +}; + +export const addErrorStep = (index) => (dispatch, getState) => { + var error = getState().builder.error; + error.steps.splice(index, 0, {}); + dispatch({ + type: BUILDER_ERROR, + payload: error + }); +}; + +export const removeStep = (index) => (dispatch, getState) => { + var steps = getState().builder.steps; + steps.splice(index, 1); + dispatch({ + type: BUILDER_DELETE_STEP, + payload: steps + }); + dispatch(removeErrorStep(index)); + dispatch(changeTutorialBuilder()); +}; + +export const removeErrorStep = (index) => (dispatch, getState) => { + var error = getState().builder.error; + error.steps.splice(index, 1); + dispatch({ + type: BUILDER_ERROR, + payload: error + }); +}; + +export const changeContent = (index, property, content) => (dispatch, getState) => { + var steps = getState().builder.steps; + var step = steps[index]; + step[property] = content; + dispatch({ + type: BUILDER_CHANGE_STEP, + payload: steps + }); + dispatch(changeTutorialBuilder()); +}; + +export const deleteProperty = (index, property) => (dispatch, getState) => { + var steps = getState().builder.steps; + var step = steps[index]; + delete step[property]; + dispatch({ + type: BUILDER_DELETE_PROPERTY, + payload: steps + }); + dispatch(changeTutorialBuilder()); +}; + +export const changeStepIndex = (fromIndex, toIndex) => (dispatch, getState) => { + var steps = getState().builder.steps; + var step = steps[fromIndex]; + steps.splice(fromIndex, 1); + steps.splice(toIndex, 0, step); + dispatch({ + type: BUILDER_CHANGE_ORDER, + payload: steps + }); + dispatch(changeErrorStepIndex(fromIndex, toIndex)); + dispatch(changeTutorialBuilder()); +}; + +export const changeErrorStepIndex = (fromIndex, toIndex) => (dispatch, getState) => { + var error = getState().builder.error; + var errorStep = error.steps[fromIndex]; + error.steps.splice(fromIndex, 1); + error.steps.splice(toIndex, 0, errorStep); + dispatch({ + type: BUILDER_ERROR, + payload: error + }); +}; + +export const setError = (index, property) => (dispatch, getState) => { + var error = getState().builder.error; + if(index !== undefined){ + error.steps[index][property] = true; + } + else { + error[property] = true; + } + dispatch({ + type: BUILDER_ERROR, + payload: error + }); + dispatch(changeTutorialBuilder()); +}; + +export const deleteError = (index, property) => (dispatch, getState) => { + var error = getState().builder.error; + if(index !== undefined){ + delete error.steps[index][property]; + } + else { + delete error[property]; + } + dispatch({ + type: BUILDER_ERROR, + payload: error + }); + dispatch(changeTutorialBuilder()); +}; + +export const setSubmitError = () => (dispatch, getState) => { + var builder = getState().builder; + if(builder.id === undefined || builder.id === ''){ + dispatch(setError(undefined, 'id')); + } + if(builder.id === undefined || builder.title === ''){ + dispatch(setError(undefined, 'title')); + } + var type = builder.steps.map((step, i) => { + step.id = i+1; + if(i === 0){ + if(step.requirements && step.requirements.length > 0){ + var requirements = step.requirements.filter(requirement => typeof(requirement)==='number'); + if(requirements.length < step.requirements.length){ + dispatch(changeContent(i, 'requirements', requirements)); + } + } + if(step.hardware === undefined || step.hardware.length < 1){ + dispatch(setError(i, 'hardware')); + } + else{ + var hardwareIds = data.map(hardware => hardware.id); + var hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware)); + if(hardware.length < step.hardware.length){ + dispatch(changeContent(i, 'hardware', hardware)); + } + } + } + if(step.headline === undefined || step.headline === ''){ + dispatch(setError(i, 'headline')); + } + if(step.text === undefined || step.text === ''){ + dispatch(setError(i, 'text')); + } + return step.type; + }); + if(!(type.filter(item => item === 'task').length > 0 && type.filter(item => item === 'instruction').length > 0)){ + dispatch(setError(undefined, 'type')); + } +}; + + +export const checkError = () => (dispatch, getState) => { + dispatch(setSubmitError()); + var error = getState().builder.error; + if(error.id || error.title || error.type){ + return true; + } + for(var i = 0; i < error.steps.length; i++){ + if(Object.keys(error.steps[i]).length > 0){ + return true + } + } + return false; +} + +export const progress = (inProgress) => (dispatch) => { + dispatch({ + type: PROGRESS, + payload: inProgress + }) +}; + +export const resetTutorial = () => (dispatch, getState) => { + dispatch(jsonString('')); + dispatch(tutorialTitle('')); + dispatch(tutorialId('')); + var steps = [ + { + id: 1, + type: 'instruction', + headline: '', + text: '', + hardware: [], + requirements: [] + } + ]; + dispatch(tutorialSteps(steps)); + dispatch({ + type: BUILDER_ERROR, + payload: { + steps: [{}] + } + }); +}; + +export const readJSON = (json) => (dispatch, getState) => { + dispatch(resetTutorial()); + dispatch({ + type: BUILDER_ERROR, + payload: { + steps: json.steps.map(() => {return {};}) + } + }); + dispatch(tutorialTitle(json.title)); + dispatch(tutorialId(json.id)); + dispatch(tutorialSteps(json.steps)); + dispatch(setSubmitError()); + dispatch(progress(false)); +}; diff --git a/src/actions/types.js b/src/actions/types.js index a086a29..0a1dc72 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -14,3 +14,16 @@ export const TUTORIAL_CHANGE = 'TUTORIAL_CHANGE'; export const TUTORIAL_XML = 'TUTORIAL_XML'; export const TUTORIAL_ID = 'TUTORIAL_ID'; export const TUTORIAL_STEP = 'TUTORIAL_STEP'; +export const JSON_STRING = 'JSON_STRING'; + + +export const BUILDER_CHANGE = 'BUILDER_CHANGE'; +export const BUILDER_TITLE = 'BUILDER_TITLE'; +export const BUILDER_ID = 'BUILDER_ID'; +export const BUILDER_ADD_STEP = 'BUILDER_ADD_STEP'; +export const BUILDER_DELETE_STEP = 'BUILDER_DELETE_STEP'; +export const BUILDER_CHANGE_STEP = 'BUILDER_CHANGE_STEP'; +export const BUILDER_CHANGE_ORDER = 'BUILDER_CHANGE_ORDER'; +export const BUILDER_DELETE_PROPERTY = 'BUILDER_DELETE_PROPERTY'; +export const BUILDER_ERROR = 'BUILDER_ERROR'; +export const PROGRESS = 'PROGRESS'; diff --git a/src/components/Blockly/BlocklyComponent.css b/src/components/Blockly/BlocklyComponent.css deleted file mode 100644 index 8705fbc..0000000 --- a/src/components/Blockly/BlocklyComponent.css +++ /dev/null @@ -1,7 +0,0 @@ -#blocklyDiv { - height: 100%; - min-height: 500px; - width: 100%; - /* border: 1px solid #4EAF47; */ - position: relative; -} diff --git a/src/components/Blockly/BlocklyComponent.jsx b/src/components/Blockly/BlocklyComponent.jsx index 048e9c8..b025821 100644 --- a/src/components/Blockly/BlocklyComponent.jsx +++ b/src/components/Blockly/BlocklyComponent.jsx @@ -22,7 +22,6 @@ */ import React from 'react'; -import './BlocklyComponent.css'; import Blockly from 'blockly/core'; import locale from 'blockly/msg/en'; diff --git a/src/components/Blockly/BlocklySvg.js b/src/components/Blockly/BlocklySvg.js new file mode 100644 index 0000000..174b7c3 --- /dev/null +++ b/src/components/Blockly/BlocklySvg.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; + +import * as Blockly from 'blockly/core'; + +class BlocklySvg extends Component { + + constructor(props) { + super(props); + this.state = { + svg: '' + }; + } + + componentDidMount() { + this.getSvg(); + } + + componentDidUpdate(props) { + if(props.initialXml !== this.props.initialXml){ + this.getSvg(); + } + } + + getSvg = () => { + const workspace = Blockly.getMainWorkspace(); + workspace.clear(); + Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(this.props.initialXml), workspace); + var canvas = workspace.svgBlockCanvas_.cloneNode(true); + + if (canvas.children[0] !== undefined) { + canvas.removeAttribute("transform"); + + // does not work in react + // var cssContent = Blockly.Css.CONTENT.join(''); + var cssContent = ''; + for (var i = 0; i < document.getElementsByTagName('style').length; i++) { + if(/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)){ + cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.'); + } + } + + var css = ''; + + var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox(); + var content = new XMLSerializer().serializeToString(canvas); + + var xml = ` + ${css}">${content}`; + + this.setState({svg: xml}); + } + } + + render() { + return ( +
+ ); + }; +} + +export default BlocklySvg; diff --git a/src/components/Blockly/BlocklyWindow.js b/src/components/Blockly/BlocklyWindow.js index b988186..3a4b97d 100644 --- a/src/components/Blockly/BlocklyWindow.js +++ b/src/components/Blockly/BlocklyWindow.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { onChangeWorkspace, clearStats } from '../../actions/workspaceActions'; import * as De from './msg/de'; -import BlocklyComponent from './'; +import BlocklyComponent from './BlocklyComponent'; +import BlocklySvg from './BlocklySvg'; import * as Blockly from 'blockly/core'; import './blocks/index'; import './generator/index'; @@ -25,56 +26,63 @@ class BlocklyWindow extends Component { this.props.clearStats(); workspace.addChangeListener((event) => { this.props.onChangeWorkspace(event); - Blockly.Events.disableOrphans(event); + // switch on that a block is displayed disabled or not depending on whether it is correctly connected + // for SVG display, a deactivated block in the display is undesirable + if (this.props.blockDisabled) { + Blockly.Events.disableOrphans(event); + } }); Blockly.svgResize(workspace); } componentDidUpdate(props) { const workspace = Blockly.getMainWorkspace(); - var initialXML = this.props.initialXml - if (props.initialXml !== initialXml) { + var xml = this.props.initialXml; + // if svg is true, then the update process is done in the BlocklySvg component + if (props.initialXml !== xml && !this.props.svg) { // guarantees that the current xml-code (this.props.initialXml) is rendered workspace.clear(); - if (!initialXML) initialXML = initialXml; - Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(initialXML), workspace); + if (!xml) xml = initialXml; + Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), workspace); } Blockly.svgResize(workspace); } render() { return ( - + - - + grid={this.props.grid !== undefined && !this.props.grid ? {} : + { // https://developers.google.com/blockly/guides/configure/web/grid + spacing: 20, + length: 1, + colour: '#4EAF47', // senseBox-green + snap: false + }} + media={'/media/blockly/'} + move={this.props.move !== undefined && !this.props.move ? {} : + { // https://developers.google.com/blockly/guides/configure/web/move + scrollbars: true, + drag: true, + wheel: false + }} + initialXml={this.props.initialXml ? this.props.initialXml : initialXml} + > + + {this.props.svg && this.props.initialXml ? : null} +
); }; } diff --git a/src/components/Blockly/blocks/sensebox-display.js b/src/components/Blockly/blocks/sensebox-display.js index fc51aab..d9824d9 100644 --- a/src/components/Blockly/blocks/sensebox-display.js +++ b/src/components/Blockly/blocks/sensebox-display.js @@ -251,4 +251,4 @@ Blockly.Blocks['sensebox_display_drawRectangle'] = { } }, LOOP_TYPES: ['sensebox_display_show'], -}; \ No newline at end of file +}; diff --git a/src/components/Compile.js b/src/components/Compile.js index e966e62..c9ce34b 100644 --- a/src/components/Compile.js +++ b/src/components/Compile.js @@ -5,14 +5,12 @@ import { workspaceName } from '../actions/workspaceActions'; import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; +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 DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogActions from '@material-ui/core/DialogActions'; -import Dialog from '@material-ui/core/Dialog'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import TextField from '@material-ui/core/TextField'; @@ -129,22 +127,20 @@ class Compile extends Component { - - {this.state.title} - - {this.state.content} - {this.state.file ? -
- - -
- : null} -
- - - + {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog} + button={this.state.file ? 'Abbrechen' : 'Schließen'} + > + {this.state.file ? +
+ + +
+ : null}
); diff --git a/src/components/Dialog.js b/src/components/Dialog.js new file mode 100644 index 0000000..0dc7180 --- /dev/null +++ b/src/components/Dialog.js @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; + +import Button from '@material-ui/core/Button'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; +import MaterialUIDialog from '@material-ui/core/Dialog'; + +class Dialog extends Component { + + render() { + return ( + + {this.props.title} + + {this.props.content} + {this.props.children} + + + {this.props.actions ? this.props.actions : + + } + + + ); + }; +} + + +export default Dialog; diff --git a/src/components/Home.js b/src/components/Home.js index 1f2b6a4..fc0f35d 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -87,7 +87,7 @@ class Home extends Component { - + {this.state.codeOn ? diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 55db003..023981a 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -17,7 +17,7 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; -import { faBars, faChevronLeft, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher } from "@fortawesome/free-solid-svg-icons"; +import { faBars, faChevronLeft, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -96,8 +96,8 @@ class Navbar extends Component { - {[{text: 'Tutorials', icon: faChalkboardTeacher}, {text: 'Einstellungen', icon: faCog}].map((item, index) => ( - + {[{text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial"}, {text: 'Tutorial-Builder', icon: faFolderPlus, link: "/tutorial/builder"}, {text: 'Einstellungen', icon: faCog, link: "/settings"}].map((item, index) => ( + diff --git a/src/components/Routes.js b/src/components/Routes.js index b81bf51..70153de 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -5,6 +5,7 @@ import { Route, Switch } from 'react-router-dom'; import Home from './Home'; import Tutorial from './Tutorial/Tutorial'; import TutorialHome from './Tutorial/TutorialHome'; +import Builder from './Tutorial/Builder/Builder'; import NotFound from './NotFound'; class Routes extends Component { @@ -15,6 +16,7 @@ class Routes extends Component { + diff --git a/src/components/Snackbar.js b/src/components/Snackbar.js new file mode 100644 index 0000000..16fd4c2 --- /dev/null +++ b/src/components/Snackbar.js @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import MaterialUISnackbar from '@material-ui/core/Snackbar'; +import SnackbarContent from '@material-ui/core/SnackbarContent'; + +import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const styles = (theme) => ({ + success: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText + }, + error: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.error.contrastText + } +}); + +class Snackbar extends Component { + + constructor(props){ + super(props); + this.state = { + open: props.open + }; + this.timeout = null; + } + + componentDidMount(){ + if(this.state.open){ + this.autoHideDuration(); + } + } + + componentWillUnmount(){ + if(this.state.open){ + clearTimeout(this.timeout); + } + } + + onClose = () => { + this.setState({open: false}); + } + + autoHideDuration = () => { + this.timeout = setTimeout(() => { + this.onClose(); + }, 5000); + } + + render() { + return ( + + + + + } + message={this.props.message} + /> + + ); + }; +} + + +export default withStyles(styles, {withTheme: true})(Snackbar); diff --git a/src/components/Tutorial/Assessment.js b/src/components/Tutorial/Assessment.js index da5e7f1..2756ba2 100644 --- a/src/components/Tutorial/Assessment.js +++ b/src/components/Tutorial/Assessment.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { workspaceName } from '../../actions/workspaceActions'; import BlocklyWindow from '../Blockly/BlocklyWindow'; -import SolutionCheck from './SolutionCheck'; import CodeViewer from '../CodeViewer'; import WorkspaceFunc from '../WorkspaceFunc'; @@ -40,12 +39,16 @@ class Assessment extends Component {
- + Arbeitsauftrag - {currentTask.text1} + {currentTask.text}
diff --git a/src/components/Tutorial/Builder/BlocklyExample.js b/src/components/Tutorial/Builder/BlocklyExample.js new file mode 100644 index 0000000..f93a282 --- /dev/null +++ b/src/components/Tutorial/Builder/BlocklyExample.js @@ -0,0 +1,181 @@ +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 { initialXml } from '../../Blockly//initialXml.js'; +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, + disabled: false + }; + } + + componentDidMount(){ + moment.updateLocale('de', localization); + 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.state.checked){ + this.isError(); + } + if(props.xml !== this.props.xml){ + // check if there is at least one block, otherwise the workspace cannot be submitted + var workspace = Blockly.getMainWorkspace(); + var areBlocks = workspace.getAllBlocks().length > 0; + this.setState({disabled: !areBlocks}); + } + } + + isError = () => { + if(this.state.checked){ + var xml = this.props.value; + // check if value is valid xml; + try{ + Blockly.Xml.textToDom(xml); + this.props.deleteError(this.props.index, 'xml'); + } + catch(err){ + xml = initialXml; + // not valid xml, throw error in redux store + this.props.setError(this.props.index, 'xml'); + } + if(!this.props.task){ + // instruction can also display only one block, which does not necessarily + // have to be the initial block + xml = xml.replace('deletable="false"', 'deletable="true"'); + } + this.setState({xml: 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.deleteError(this.props.index, 'xml'); + this.props.deleteProperty(this.props.index, 'xml'); + } + } + + setXml = () => { + var xml = this.props.xml; + this.props.changeContent(this.props.index, 'xml', xml); + this.setState({input: moment(Date.now()).format('LTS')}); + } + + render() { + 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 '${this.props.task ? 'Musterlösung einreichen' : 'Beispiel einreichen'}'-Button klickst.`} + : this.state.input ? Die letzte Einreichung erfolgte um {this.state.input} Uhr. : null + : null} + {this.state.checked && !this.props.task ? + Anmerkung: Man kann den initialen Setup()- bzw. Endlosschleifen()-Block löschen. Zusätzlich ist es möglich u.a. nur einen beliebigen Block auszuwählen, ohne dass dieser als deaktiviert dargestellt wird. + : null} + {this.state.checked ? (() => { + 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..1c6c819 --- /dev/null +++ b/src/components/Tutorial/Builder/Builder.js @@ -0,0 +1,226 @@ +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 Snackbar from '../../Snackbar'; + +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'; +import FormHelperText from '@material-ui/core/FormHelperText'; + +const styles = (theme) => ({ + backdrop: { + zIndex: theme.zIndex.drawer + 1, + color: '#fff', + }, + errorColor: { + color: theme.palette.error.dark + } +}); + +class Builder extends Component { + + constructor(props){ + super(props); + this.state = { + open: false, + title: '', + content: '', + string: false, + snackbar: false, + key: '', + message: '' + }; + this.inputRef = React.createRef(); + } + + componentWillUnmount(){ + this.reset(); + } + + submit = () => { + var isError = this.props.checkError(); + if(isError){ + this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error'}); + 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(); + this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich zurückgesetzt.`, type: 'success'}); + 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); + this.setState({ snackbar: true, key: Date.now(), message: `${isFile ? 'Die übergebene JSON-Datei' : 'Der übergebene JSON-String'} wurde erfolgreich übernommen.`, type: 'success'}); + } 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.error.type ? + {`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`} + : null} + + + + {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..f469851 --- /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 ( +
+ Hardware + Beachte, dass die Reihenfolge des Auswählens maßgebend ist. + {this.props.error ? Wähle mindestens eine Hardware-Komponente aus. : null} + + {hardware.map((picture,i) => ( + this.onChange(picture.id)} classes={{tile: this.props.value.filter(value => value === picture.id).length > 0 ? this.props.classes.active : this.props.classes.border}}> +
+ {picture.name} +
+ + {picture.name} +
+ } + /> + + ))} + + + ); + }; +} + +Requirements.propTypes = { + changeContent: PropTypes.func.isRequired, + setError: PropTypes.func.isRequired, + deleteError: PropTypes.func.isRequired, + change: PropTypes.number.isRequired +}; + +const mapStateToProps = state => ({ + change: state.builder.change +}); + +export default connect(mapStateToProps, { changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(withWidth()(Requirements))); diff --git a/src/components/Tutorial/Builder/Id.js b/src/components/Tutorial/Builder/Id.js new file mode 100644 index 0000000..ad18dc2 --- /dev/null +++ b/src/components/Tutorial/Builder/Id.js @@ -0,0 +1,121 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { tutorialId, setError, deleteError } from '../../../actions/tutorialBuilderActions'; + +import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import OutlinedInput from '@material-ui/core/OutlinedInput'; +import InputLabel from '@material-ui/core/InputLabel'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; + +import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const styles = theme => ({ + errorColor: { + color: theme.palette.error.dark + }, + errorColorShrink: { + color: `rgba(0, 0, 0, 0.54) !important` + }, + errorBorder: { + borderColor: `${theme.palette.error.dark} !important` + } +}); + +class Id extends Component { + + handleChange = (e) => { + var value = parseInt(e.target.value); + if(Number.isInteger(value) && value > 0){ + this.props.tutorialId(value); + if(this.props.error){ + this.props.deleteError(undefined, 'id'); + } + } + else { + this.props.tutorialId(value.toString()); + this.props.setError(undefined,'id'); + } + }; + + handleCounter = (step) => { + if(this.props.value+step < 1){ + this.props.setError(undefined,'id'); + } + else if(this.props.error){ + this.props.deleteError(undefined, 'id'); + } + if(!this.props.value || !Number.isInteger(this.props.value)){ + this.props.tutorialId(0+step); + } + else { + this.props.tutorialId(this.props.value+step); + } + } + + render() { + return ( +
+ + + ID + + + + +
+ } + /> + {this.props.error ? Gib eine positive ganzzahlige Zahl ein. : null} + + Beachte, dass die ID eindeutig sein muss. Sie muss sich also zu den anderen Tutorials unterscheiden. + + ); + }; +} + +Id.propTypes = { + tutorialId: PropTypes.func.isRequired, + setError: PropTypes.func.isRequired, + deleteError: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + change: state.builder.change +}); + +export default connect(mapStateToProps, { tutorialId, setError, deleteError })(withStyles(styles, { withTheme: true })(Id)); diff --git a/src/components/Tutorial/Builder/Requirements.js b/src/components/Tutorial/Builder/Requirements.js new file mode 100644 index 0000000..d06c62f --- /dev/null +++ b/src/components/Tutorial/Builder/Requirements.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { changeContent } from '../../../actions/tutorialBuilderActions'; + +import tutorials from '../../../data/tutorials.json'; + +import FormGroup from '@material-ui/core/FormGroup'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormLabel from '@material-ui/core/FormLabel'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import FormControl from '@material-ui/core/FormControl'; + +class Requirements extends Component { + + onChange = (e) => { + var requirements = this.props.value; + var value = parseInt(e.target.value) + if(e.target.checked){ + requirements.push(value); + } + else { + requirements = requirements.filter(requirement => requirement !== value); + } + this.props.changeContent(this.props.index, 'requirements', requirements); + } + + render() { + return ( + + Voraussetzungen + Beachte, dass die Reihenfolge des Anhakens maßgebend ist. + + {tutorials.map((tutorial, i) => + id === tutorial.id).length > 0} + onChange={(e) => this.onChange(e)} + name="requirements" + color="primary" + /> + } + label={tutorial.title} + /> + )} + + + ); + }; +} + +Requirements.propTypes = { + changeContent: PropTypes.func.isRequired +}; + +const mapStateToProps = state => ({ + change: state.builder.change +}); + +export default connect(mapStateToProps, { changeContent })(Requirements); diff --git a/src/components/Tutorial/Builder/Step.js b/src/components/Tutorial/Builder/Step.js new file mode 100644 index 0000000..24c75ac --- /dev/null +++ b/src/components/Tutorial/Builder/Step.js @@ -0,0 +1,129 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { addStep, removeStep, changeStepIndex } from '../../../actions/tutorialBuilderActions'; + +import clsx from 'clsx'; + +import Textfield from './Textfield'; +import StepType from './StepType'; +import BlocklyExample from './BlocklyExample'; +import Requirements from './Requirements'; +import Hardware from './Hardware'; + +import { withStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { faPlus, faAngleDoubleUp, faAngleDoubleDown, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const styles = (theme) => ({ + button: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + width: '40px', + height: '40px', + '&:hover': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } + }, + delete: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.error.contrastText, + '&:hover': { + backgroundColor: theme.palette.error.dark, + color: theme.palette.error.contrastText, + } + } +}); + +class Step extends Component { + + render() { + var index = this.props.index; + var steps = this.props.steps; + return ( +
+ Schritt {index+1} +
+
+ + this.props.addStep(index+1)} + > + + + + {index !== 0 ? +
+ + this.props.changeStepIndex(index, index-1)} + > + + + + + this.props.changeStepIndex(index, index+1)} + > + + + + + this.props.removeStep(index)} + > + + + +
+ : null} +
+
+ + + + {index === 0 ? +
+ + +
+ : null} + +
+
+
+ ); + }; +} + +Step.propTypes = { + addStep: PropTypes.func.isRequired, + removeStep: PropTypes.func.isRequired, + changeStepIndex: PropTypes.func.isRequired, + steps: PropTypes.array.isRequired, + change: PropTypes.number.isRequired, + error: PropTypes.object.isRequired, +}; + +const mapStateToProps = state => ({ + steps: state.builder.steps, + change: state.builder.change, + error: state.builder.error +}); + +export default connect(mapStateToProps, { addStep, removeStep, changeStepIndex })(withStyles(styles, {withTheme: true})(Step)); diff --git a/src/components/Tutorial/Builder/StepType.js b/src/components/Tutorial/Builder/StepType.js new file mode 100644 index 0000000..cfc2253 --- /dev/null +++ b/src/components/Tutorial/Builder/StepType.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { changeContent, deleteProperty, deleteError } from '../../../actions/tutorialBuilderActions'; + +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; + +class StepType extends Component { + + onChange = (value) => { + this.props.changeContent(this.props.index, 'type', value); + // delete property 'xml', so that all used blocks are reset + this.props.deleteProperty(this.props.index, 'xml'); + if(value === 'task'){ + this.props.deleteError(undefined, 'type'); + } + } + + render() { + return ( + this.onChange(e.target.value)}> + } + label="Anleitung" + labelPlacement="end" + /> + } + label="Aufgabe" + labelPlacement="end" + /> + + ); + }; +} + +StepType.propTypes = { + changeContent: PropTypes.func.isRequired, + deleteProperty: PropTypes.func.isRequired, + deleteError: PropTypes.func.isRequired +}; + +export default connect(null, { changeContent, deleteProperty, deleteError })(StepType); diff --git a/src/components/Tutorial/Builder/Textfield.js b/src/components/Tutorial/Builder/Textfield.js new file mode 100644 index 0000000..d601784 --- /dev/null +++ b/src/components/Tutorial/Builder/Textfield.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { tutorialTitle, jsonString, changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions'; + +import { withStyles } from '@material-ui/core/styles'; +import OutlinedInput from '@material-ui/core/OutlinedInput'; +import InputLabel from '@material-ui/core/InputLabel'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; + +const styles = theme => ({ + multiline: { + padding: '18.5px 14px 18.5px 24px' + }, + errorColor: { + color: `${theme.palette.error.dark} !important` + }, + errorColorShrink: { + color: `rgba(0, 0, 0, 0.54) !important` + }, + errorBorder: { + borderColor: `${theme.palette.error.dark} !important` + } +}); + +class Textfield extends Component { + + componentDidMount(){ + if(this.props.error){ + this.props.deleteError(this.props.index, this.props.property); + } + } + + handleChange = (e) => { + var value = e.target.value; + if(this.props.property === 'title'){ + this.props.tutorialTitle(value); + } + else if(this.props.property === 'json'){ + this.props.jsonString(value); + } + else { + this.props.changeContent(this.props.index, this.props.property, value); + } + if(value.replace(/\s/g,'') === ''){ + this.props.setError(this.props.index, this.props.property); + } + else{ + this.props.deleteError(this.props.index, this.props.property); + } + }; + + render() { + return ( + + + {this.props.label} + + this.handleChange(e)} + /> + {this.props.error ? + this.props.property === 'title' ? Gib einen Titel für das Tutorial ein. + : this.props.property === 'json' ? Gib einen JSON-String ein und bestätige diesen mit einem Klick auf den entsprechenden Button + : {this.props.errorText} + : null} + + ); + }; +} + +Textfield.propTypes = { + tutorialTitle: PropTypes.func.isRequired, + jsonString: PropTypes.func.isRequired, + changeContent: PropTypes.func.isRequired, +}; + +export default connect(null, { tutorialTitle, jsonString, changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(Textfield)); diff --git a/src/components/Tutorial/Hardware.js b/src/components/Tutorial/Hardware.js index 614d6ae..495c2a6 100644 --- a/src/components/Tutorial/Hardware.js +++ b/src/components/Tutorial/Hardware.js @@ -1,18 +1,18 @@ import React, { Component } from 'react'; +import Dialog from '../Dialog'; + +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 Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; -import Button from '@material-ui/core/Button'; import GridList from '@material-ui/core/GridList'; import GridListTile from '@material-ui/core/GridListTile'; import GridListTileBar from '@material-ui/core/GridListTileBar'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExpandAlt } from "@fortawesome/free-solid-svg-icons"; @@ -41,16 +41,15 @@ class Hardware extends Component { state = { open: false, - title: '', - url: '' + hardwareInfo: {} }; - handleClickOpen = (title, url) => { - this.setState({open: true, title, url}); + handleClickOpen = (hardwareInfo) => { + this.setState({open: true, hardwareInfo}); }; handleClose = () => { - this.setState({open: false, title: '', url: ''}); + this.setState({open: false, hardwareInfo: {}}); }; render() { @@ -59,45 +58,46 @@ class Hardware extends Component {
Für die Umsetzung benötigst du folgende Hardware: - - {this.props.picture.map((picture,i) => ( - -
- {picture} this.handleClickOpen(picture, `/media/hardware/${picture}.png`)}/> -
- - {picture} -
- } - actionIcon={ - this.handleClickOpen(picture, `/media/hardware/${picture}.png`)}> - - - } - /> - - ))} - + + {this.props.picture.map((picture,i) => { + var hardwareInfo = hardware.filter(hardware => hardware.id === picture)[0]; + return( + +
+ {hardwareInfo.name} this.handleClickOpen(hardwareInfo)}/> +
+ + {hardwareInfo.name} + + } + actionIcon={ + this.handleClickOpen(hardwareInfo)}> + + + } + /> +
+ )})} +
- Hardware: {this.state.title} - - {this.state.title}/ - - - - +
+ {this.state.hardwareInfo.name}/ + Weitere Informationen zur Hardware-Komponente findest du hier. +
+ ); }; diff --git a/src/components/Tutorial/Instruction.js b/src/components/Tutorial/Instruction.js index ee5c82a..739d798 100644 --- a/src/components/Tutorial/Instruction.js +++ b/src/components/Tutorial/Instruction.js @@ -18,7 +18,7 @@ class Instruction extends Component { return (
{step.headline} - {step.text1} + {step.text} {isHardware ? : null} {areRequirements > 0 ? @@ -27,12 +27,8 @@ class Instruction extends Component { diff --git a/src/components/Tutorial/Requirement.js b/src/components/Tutorial/Requirement.js index 2d724b8..9c2542e 100644 --- a/src/components/Tutorial/Requirement.js +++ b/src/components/Tutorial/Requirement.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import clsx from 'clsx'; import { withRouter, Link } from 'react-router-dom'; -import tutorials from './tutorials.json'; +import tutorials from '../../data/tutorials.json'; import { fade } from '@material-ui/core/styles/colorManipulator'; import { withStyles } from '@material-ui/core/styles'; diff --git a/src/components/Tutorial/SolutionCheck.js b/src/components/Tutorial/SolutionCheck.js index 084d2d1..bc8c6c8 100644 --- a/src/components/Tutorial/SolutionCheck.js +++ b/src/components/Tutorial/SolutionCheck.js @@ -6,18 +6,16 @@ import { tutorialCheck, tutorialStep } from '../../actions/tutorialActions'; import { withRouter } from 'react-router-dom'; import Compile from '../Compile'; +import Dialog from '../Dialog'; -import tutorials from './tutorials.json'; +import tutorials from '../../data/tutorials.json'; import { checkXml } from '../../helpers/compareXml'; import { withStyles } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import Button from '@material-ui/core/Button'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogActions from '@material-ui/core/DialogActions'; -import Dialog from '@material-ui/core/Dialog'; + import { faPlay } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -69,41 +67,44 @@ class SolutionCheck extends Component { - - {this.state.msg.type === 'error' ? 'Fehler' : 'Erfolg'} - - {this.state.msg.text} - {this.state.msg.type === 'success' ? -
- - {this.props.activeStep === steps.length-1 ? - - : - - } -
- : null} -
- - - + + + {this.state.msg.type === 'success' ? +
+ + {this.props.activeStep === steps.length-1 ? + + : + + } +
+ : null}
+
); }; diff --git a/src/components/Tutorial/StepperHorizontal.js b/src/components/Tutorial/StepperHorizontal.js index 46e73dc..c446675 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.json'; +import tutorials from '../../data/tutorials.json'; import { fade } from '@material-ui/core/styles/colorManipulator'; import { withStyles } from '@material-ui/core/styles'; diff --git a/src/components/Tutorial/Tutorial.js b/src/components/Tutorial/Tutorial.js index acce0c9..f2a7bb7 100644 --- a/src/components/Tutorial/Tutorial.js +++ b/src/components/Tutorial/Tutorial.js @@ -13,7 +13,7 @@ import NotFound from '../NotFound'; import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; -import tutorials from './tutorials.json'; +import tutorials from '../../data/tutorials.json'; import Card from '@material-ui/core/Card'; import Button from '@material-ui/core/Button'; @@ -42,7 +42,7 @@ class Tutorial extends Component { var step = steps ? steps[this.props.activeStep] : null; var name = step ? `${detectWhitespacesAndReturnReadableResult(tutorial.title)}_${detectWhitespacesAndReturnReadableResult(step.headline)}` : null; return ( - !Number.isInteger(currentTutorialId) || currentTutorialId < 1 || currentTutorialId > tutorials.length ? + !Number.isInteger(currentTutorialId) || currentTutorialId < 1 || !tutorial ? :
diff --git a/src/components/Tutorial/TutorialHome.js b/src/components/Tutorial/TutorialHome.js index 2bf99f2..e56cf31 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.json'; +import tutorials from '../../data/tutorials.json'; import { Link } from 'react-router-dom'; diff --git a/src/components/Tutorial/tutorials.json b/src/components/Tutorial/tutorials.json deleted file mode 100644 index 0cbb6b3..0000000 --- a/src/components/Tutorial/tutorials.json +++ /dev/null @@ -1,96 +0,0 @@ -[ - { - "id": 1, - "title": "Erste Schritte", - "steps": [ - { - "id": 1, - "type": "instruction", - "headline": "Erste Schritte", - "text1": "In diesem Tutorial lernst du die ersten Schritte mit der senseBox kennen. Du erstellst ein erstes Programm, baust einen ersten Schaltkreis auf und lernst, wie du das Programm auf die senseBox MCU überträgst.", - "hardware": [ - "senseboxmcu", - "led", - "breadboard", - "jst-adapter", - "resistor" - ], - "requirements": [] - }, - { - "id": 2, - "type": "instruction", - "headline": "Aufbau der Schaltung", - "text1": "Stecke die LED auf das Breadboard und verbinde diese mithile des Widerstandes und dem JST Kabel mit dem Port Digital/Analog 1." - }, - { - "id": 3, - "type": "instruction", - "headline": "Programmierung", - "text1": "Jedes Programm für die senseBox besteht aus zwei Funktionen. Die Setup () Funktion wird zu Begin einmalig ausgeführt und der Programmcode Schrittweise ausgeführt. Nachdem die Setup () Funktion durchlaufen worden ist wird der Programmcode aus der zweiten Funktion, der Endlosschleife, fortlaufend wiederholt.", - "xml": "" - }, - { - "id": 4, - "type": "instruction", - "headline": "Leuchten der LED", - "text1": "Um nun die LED zum leuchten zu bringen wird folgender Block in die Endlosschleife eingefügt. Der Block bietet dir auszuwählen an welchen Pin die LED angeschlossen wurd und ob diese ein oder ausgeschaltet werden soll.", - "xml": "" - }, - { - "id": 5, - "type": "task", - "headline": "Aufgabe 1", - "text1": "Verwenden den Block zum leuchten der LED und übertrage dein erstes Programm auf die senseBox MCU.", - "xml": "" - } - ] - }, - { - "id": 2, - "title": "WLAN einrichten", - "steps": [ - { - "id": 1, - "type": "instruction", - "headline": "Einführung", - "text1": "In diesem Tutorial lernst du wie man die senseBox mit dem Internet verbindest.", - "hardware": [ - "senseboxmcu", - "wifi-bee" - ], - "requirements": [ - 1 - ] - }, - { - "id": 2, - "type": "instruction", - "headline": "Programmierung", - "text1": "Man benötigt folgenden Block:", - "xml": "SSIDPassword" - }, - { - "id": 3, - "type": "instruction", - "headline": "Block richtig einbinden", - "text1": "", - "xml": "SSIDPassword" - }, - { - "id": 4, - "type": "task", - "headline": "Aufgabe 1", - "text1": "Stelle eine WLAN-Verbindung mit einem beliebigen Netzwerk her.", - "xml": "SSIDPassword" - }, - { - "id": 5, - "type": "task", - "headline": "Aufgabe 2", - "text1": "Versuche das gleiche einfach nochmal. Übung macht den Meister! ;)", - "xml": "SSIDPassword" - } - ] - } -] \ No newline at end of file diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index 2beff96..96f3cb4 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -12,20 +12,18 @@ import { initialXml } from './Blockly/initialXml.js'; import Compile from './Compile'; import SolutionCheck from './Tutorial/SolutionCheck'; +import Dialog from './Dialog'; +import Snackbar from './Snackbar'; import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import { withStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogActions from '@material-ui/core/DialogActions'; -import Dialog from '@material-ui/core/Dialog'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; -import { faPen, faSave, faUpload, faShare } from "@fortawesome/free-solid-svg-icons"; +import { faPen, faSave, faUpload, faCamera, faShare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -61,8 +59,11 @@ class WorkspaceFunc extends Component { content: '', open: false, file: false, - saveXml: false, - name: props.name + saveFile: false, + name: props.name, + snackbar: false, + key: '', + message: '' }; } @@ -86,15 +87,49 @@ class WorkspaceFunc extends Component { saveAs(blob, fileName); } - createFileName = () => { - if(this.state.name){ - this.saveXmlFile(); - } - else{ - this.setState({ file: true, saveXml: true, open: true, title: 'Blöcke speichern', content: 'Bitte gib einen Namen für die Bennenung der XML-Datei ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }); + getSvg = () => { + const workspace = Blockly.getMainWorkspace(); + var canvas = workspace.svgBlockCanvas_.cloneNode(true); + + if (canvas.children[0] !== undefined) { + canvas.removeAttribute("transform"); + + // does not work in react + // var cssContent = Blockly.Css.CONTENT.join(''); + var cssContent = ''; + for (var i = 0; i < document.getElementsByTagName('style').length; i++) { + if(/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)){ + cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.'); + } + } + + var css = ''; + + var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox(); + var content = new XMLSerializer().serializeToString(canvas); + + var xml = ` + ${css}">${content}`; + var fileName = detectWhitespacesAndReturnReadableResult(this.state.name); + this.props.workspaceName(this.state.name); + fileName = `${fileName}.svg` + var blob = new Blob([xml], { type: 'image/svg+xml;base64' }); + saveAs(blob, fileName); } } + createFileName = (filetype) => { + this.setState({file: filetype}, () => { + if(this.state.name){ + this.state.file === 'xml' ? this.saveXmlFile() : this.getSvg() + } + else{ + this.setState({ saveFile: true, file: filetype, open: true, title: this.state.file === 'xml' ? 'Blöcke speichern' : 'Screenshot erstellen', content: `Bitte gib einen Namen für die Bennenung der ${this.state.file === 'xml' ? 'XML' : 'SVG'}-Datei ein und bestätige diesen mit einem Klick auf 'Eingabe'.` }); + } + }); + } + setFileName = (e) => { this.setState({name: e.target.value}); } @@ -124,6 +159,7 @@ class WorkspaceFunc extends Component { var extensionPosition = xmlFile.name.lastIndexOf('.'); this.props.workspaceName(xmlFile.name.substr(0, extensionPosition)); } + this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' }); } } catch(err){ this.setState({ open: true, file: false, title: 'Ungültige XML', content: 'Die XML-Datei konnte nicht in Blöcke zerlegt werden. Bitte überprüfe den XML-Code und versuche es erneut.' }); @@ -132,6 +168,12 @@ class WorkspaceFunc extends Component { } } + renameWorkspace = () => { + this.props.workspaceName(this.state.name); + this.toggleDialog(); + this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); + } + resetWorkspace = () => { const workspace = Blockly.getMainWorkspace(); Blockly.Events.disable(); // https://groups.google.com/forum/#!topic/blockly/m7e3g0TC75Y @@ -145,6 +187,7 @@ class WorkspaceFunc extends Component { if(!this.props.solutionCheck){ this.props.workspaceName(null); } + this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' }); } render() { @@ -152,7 +195,7 @@ class WorkspaceFunc extends Component {
{!this.props.solutionCheck ? -
{this.setState({file: true, open: true, saveXml: false, title: 'Projekt benennen', content: 'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.'})}}> +
{this.setState({file: true, open: true, saveFile: false, title: 'Projekt benennen', content: 'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.'})}}> {this.props.name && !isWidthDown('xs', this.props.width) ? {this.props.name} : null}
@@ -164,7 +207,7 @@ class WorkspaceFunc extends Component { this.createFileName()} + onClick={() => {this.createFileName('xml');}} > @@ -187,6 +230,14 @@ class WorkspaceFunc extends Component {
+ + {this.createFileName('svg');}} + > + + + - - {this.state.title} - - {this.state.content} - {this.state.file ? -
- - -
- : null} -
- - - + + {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog} + button={this.state.file ? 'Abbrechen' : 'Schließen'} + > + {this.state.file ? +
+ + +
+ : null}
+ + +
); }; diff --git a/src/data/hardware.json b/src/data/hardware.json new file mode 100644 index 0000000..65c310a --- /dev/null +++ b/src/data/hardware.json @@ -0,0 +1,128 @@ +[ + { + "id": "bmp280", + "name": "Luftdruck und Temperatursensor", + "src": "bmp280.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/sensoren/luftdruck-temperatur.html" + }, + { + "id": "breadboard", + "name": "Steckboard", + "src": "breadboard.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "button", + "name": "Knopf", + "src": "button.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "hc04", + "name": "Ultraschall-Distanzsensor", + "src": "hc04.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "hdc1080", + "name": "Temperatur und Luftfeuchtigkeitssensor", + "src": "hdc1080.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/sensoren/hdc1080.html" + }, + { + "id": "jst-adapter", + "name": "JST-Adapterkabel", + "src": "jst-adapter.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "jst-jst", + "name": "JST-JST Kabel", + "src": "jst-jst.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "jumperwire", + "name": "Steckkabel", + "src": "jumperwire.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "ldr", + "name": "LDR", + "src": "ldr.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "led", + "name": "LEDs", + "src": "led.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "microphone", + "name": "Mikrofon", + "src": "microphone.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "oled", + "name": "OLED-Display", + "src": "oled.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/zubehoer/led-display.html" + }, + { + "id": "piezo", + "name": "Piezo", + "src": "piezo.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "resistor-10kohm", + "name": "10 kOhm Widerstand", + "src": "resistor-10kohm.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "resistor-470ohm", + "name": "470 Ohm Widerstand", + "src": "resistor-470ohm.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "rgb-led", + "name": "RGB-LED", + "src": "rgb-led.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "sd-bee", + "name": "mSD-Bee", + "src": "sd-bee.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/bees/sd.html" + }, + { + "id": "senseboxmcu", + "name": "senseBox MCU", + "src": "senseboxmcu.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/sensebox-mcu.html" + }, + { + "id": "usb-cable", + "name": "USB-Kabel", + "src": "usb-cable.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/zubehoer/netzteil-und-usb-kabel.html" + }, + { + "id": "veml6070", + "name": "Helligkeit und UV-Sensor", + "src": "veml6070.png", + "url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html" + }, + { + "id": "wifi-bee", + "name": "WiFi-Bee", + "src": "wifi-bee.png", + "url": "https://sensebox.github.io/books-v2/edu/de/komponenten/bees/wifi.html" + } +] diff --git a/src/data/tutorials.json b/src/data/tutorials.json new file mode 100644 index 0000000..e8f6bde --- /dev/null +++ b/src/data/tutorials.json @@ -0,0 +1,85 @@ +[ + { + "id":1, + "title":"Erste Schritte", + "steps":[ + { + "id":1, + "type":"instruction", + "headline":"Erste Schritte", + "text":"In diesem Tutorial lernst du die ersten Schritte mit der senseBox kennen. Du erstellst ein erstes Programm, baust einen ersten Schaltkreis auf und lernst, wie du das Programm auf die senseBox MCU überträgst.", + "hardware":["senseboxmcu","led","breadboard","jst-adapter","resistor-470ohm"], + "requirements":[] + }, + { + "id":2, + "type":"instruction", + "headline":"Aufbau der Schaltung", + "text":"Stecke die LED auf das Breadboard und verbinde diese mithile des Widerstandes und dem JST Kabel mit dem Port Digital/Analog 1." + }, + { + "id":3, + "type":"instruction", + "headline":"Programmierung", + "text":"Jedes Programm für die senseBox besteht aus zwei Funktionen. Die Setup () Funktion wird zu Begin einmalig ausgeführt und der Programmcode Schrittweise ausgeführt. Nachdem die Setup () Funktion durchlaufen worden ist wird der Programmcode aus der zweiten Funktion, der Endlosschleife, fortlaufend wiederholt.", + "xml":"" + }, + { + "id":4, + "type":"instruction", + "headline":"Leuchten der LED", + "text":"Um nun die LED zum leuchten zu bringen wird folgender Block in die Endlosschleife eingefügt. Der Block bietet dir auszuwählen an welchen Pin die LED angeschlossen wurd und ob diese ein oder ausgeschaltet werden soll.", + "xml":"" + }, + { + "id":5, + "type":"task", + "headline":"Aufgabe 1", + "text":"Verwenden den Block zum leuchten der LED und übertrage dein erstes Programm auf die senseBox MCU.", + "xml":"" + } + ] + }, + { + "id": 2, + "title": "WLAN einrichten", + "steps": [ + { + "id": 1, + "type": "instruction", + "headline": "Einführung", + "text": "In diesem Tutorial lernst du wie man die senseBox mit dem Internet verbindest.", + "hardware": ["senseboxmcu", "wifi-bee"], + "requirements": [1] + }, + { + "id": 2, + "type": "instruction", + "headline": "Programmierung", + "text": "Man benötigt folgenden Block:", + "xml": "SSIDPassword" + }, + { + "id": 3, + "type": "instruction", + "headline": "Block richtig einbinden", + "text": "", + "xml": "SSIDPassword" + }, + { + "id": 4, + "type": "task", + "headline": "Aufgabe 1", + "text": "Stelle eine WLAN-Verbindung mit einem beliebigen Netzwerk her.", + "xml": "SSIDPassword" + }, + { + "id": 5, + "type": "task", + "headline": "Aufgabe 2", + "text": "Versuche das gleiche einfach nochmal. Übung macht den Meister! ;)", + "xml": "SSIDPassword" + } + ] + } +] diff --git a/src/reducers/index.js b/src/reducers/index.js index f8ec289..948fd68 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import workspaceReducer from './workspaceReducer'; import tutorialReducer from './tutorialReducer'; +import tutorialBuilderReducer from './tutorialBuilderReducer'; export default combineReducers({ workspace: workspaceReducer, - tutorial: tutorialReducer + tutorial: tutorialReducer, + builder: tutorialBuilderReducer }); diff --git a/src/reducers/tutorialBuilderReducer.js b/src/reducers/tutorialBuilderReducer.js new file mode 100644 index 0000000..132de0a --- /dev/null +++ b/src/reducers/tutorialBuilderReducer.js @@ -0,0 +1,68 @@ +import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP,BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from '../actions/types'; + +const initialState = { + change: 0, + progress: false, + json: '', + title: '', + id: '', + steps: [ + { + id: 1, + type: 'instruction', + headline: '', + text: '', + hardware: [], + requirements: [] + } + ], + error: { + steps: [{}] + } +}; + +export default function(state = initialState, action){ + switch(action.type){ + case BUILDER_CHANGE: + return { + ...state, + change: state.change += 1 + }; + case BUILDER_TITLE: + return { + ...state, + title: action.payload + }; + case BUILDER_ID: + return { + ...state, + id: action.payload + }; + case BUILDER_ADD_STEP: + case BUILDER_DELETE_STEP: + case BUILDER_CHANGE_STEP: + case BUILDER_CHANGE_ORDER: + case BUILDER_DELETE_PROPERTY: + return { + ...state, + steps: action.payload + }; + case BUILDER_ERROR: + return { + ...state, + error: action.payload + } + case PROGRESS: + return { + ...state, + progress: action.payload + } + case JSON_STRING: + return { + ...state, + json: action.payload + } + default: + return state; + } +} diff --git a/src/reducers/tutorialReducer.js b/src/reducers/tutorialReducer.js index ad80903..e96a132 100644 --- a/src/reducers/tutorialReducer.js +++ b/src/reducers/tutorialReducer.js @@ -1,34 +1,33 @@ import { TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from '../actions/types'; -import tutorials from '../components/Tutorial/tutorials.json'; +import tutorials from '../data/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){ + var existingTutorialIds = tutorials.map((tutorial, i) => { + var tutorialsId = tutorial.id; + var statusIndex = status.findIndex(status => status.id === tutorialsId); + if(statusIndex > -1){ + var tasks = tutorial.steps.filter(step => step.type === 'task'); + var existingTaskIds = tasks.map((task, j) => { + var tasksId = task.id; + if(status[statusIndex].tasks.findIndex(task => task.id === tasksId) === -1){ // task does not exist - status[i].tasks.push({id: tasksId}); + status[statusIndex].tasks.push({id: tasksId}); } - } + return 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); + status[statusIndex].tasks = status[statusIndex].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({})}); + status.push({id: tutorialsId, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => {return {id: task.id};})}); } - } + return tutorialsId; + }); // deleting old tutorials which do not longer exist if(existingTutorialIds.length > 0){ status = status.filter(status => existingTutorialIds.indexOf(status.id) > -1);