Merge branch 'master' into typed-variables

This commit is contained in:
Mario
2020-09-29 11:28:57 +02:00
41 changed files with 1995 additions and 369 deletions
@@ -1,7 +0,0 @@
#blocklyDiv {
height: 100%;
min-height: 500px;
width: 100%;
/* border: 1px solid #4EAF47; */
position: relative;
}
@@ -22,7 +22,6 @@
*/
import React from 'react';
import './BlocklyComponent.css';
import Blockly from 'blockly/core';
import locale from 'blockly/msg/en';
+65
View File
@@ -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 = '<defs><style type="text/css" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[' + cssContent + ']]></style></defs>';
var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox();
var content = new XMLSerializer().serializeToString(canvas);
var xml = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="${bbox.width}" height="${bbox.height}" viewBox="${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}">
${css}">${content}</svg>`;
this.setState({svg: xml});
}
}
render() {
return (
<div
style={{display: 'flex', justifyContent: 'center', transform: 'scale(0.8) translate(0, calc(100% * -0.2 / 2))'}}
dangerouslySetInnerHTML={{ __html: this.state.svg }}
/>
);
};
}
export default BlocklySvg;
+44 -36
View File
@@ -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 (
<BlocklyComponent ref={this.simpleWorkspace}
style={this.props.blocklyCSS}
readOnly={this.props.readOnly !== undefined ? this.props.readOnly : false}
trashcan={this.props.trashcan !== undefined ? this.props.trashcan : true}
renderer='geras'
zoom={{ // https://developers.google.com/blockly/guides/configure/web/zoom
controls: this.props.zoomControls !== undefined ? this.props.zoomControls : true,
wheel: false,
startScale: 1.0,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2
}}
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
<div>
<BlocklyComponent ref={this.simpleWorkspace}
style={this.props.svg ? { height: 0 } : this.props.blocklyCSS}
readOnly={this.props.readOnly !== undefined ? this.props.readOnly : false}
trashcan={this.props.trashcan !== undefined ? this.props.trashcan : true}
renderer='zelos'
zoom={{ // https://developers.google.com/blockly/guides/configure/web/zoom
controls: this.props.zoomControls !== undefined ? this.props.zoomControls : true,
wheel: false,
startScale: 0.8,
maxScale: 3,
minScale: 0.3,
scaleSpeed: 1.2
}}
initialXml={this.props.initialXml ? this.props.initialXml : initialXml}
>
</BlocklyComponent >
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}
>
</BlocklyComponent >
{this.props.svg && this.props.initialXml ? <BlocklySvg initialXml={this.props.initialXml} /> : null}
</div>
);
};
}
@@ -251,4 +251,4 @@ Blockly.Blocks['sensebox_display_drawRectangle'] = {
}
},
LOOP_TYPES: ['sensebox_display_show'],
};
};
+16 -20
View File
@@ -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 {
<Backdrop className={this.props.classes.backdrop} open={this.state.progress}>
<CircularProgress color="inherit" />
</Backdrop>
<Dialog onClose={this.toggleDialog} open={this.state.open}>
<DialogTitle>{this.state.title}</DialogTitle>
<DialogContent dividers>
{this.state.content}
{this.state.file ?
<div style={{marginTop: '10px'}}>
<TextField autoFocus placeholder='Dateiname' value={this.state.name} onChange={this.setFileName} style={{marginRight: '10px'}}/>
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => this.download()}>Eingabe</Button>
</div>
: null}
</DialogContent>
<DialogActions>
<Button onClick={this.state.file ? () => {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog} color="primary">
{this.state.file ? 'Abbrechen' : 'Schließen'}
</Button>
</DialogActions>
<Dialog
open={this.state.open}
title={this.state.title}
content={this.state.content}
onClose={this.toggleDialog}
onClick={this.state.file ? () => {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog}
button={this.state.file ? 'Abbrechen' : 'Schließen'}
>
{this.state.file ?
<div style={{marginTop: '10px'}}>
<TextField autoFocus placeholder='Dateiname' value={this.state.name} onChange={this.setFileName} style={{marginRight: '10px'}}/>
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => this.download()}>Eingabe</Button>
</div>
: null}
</Dialog>
</div>
);
+38
View File
@@ -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 (
<MaterialUIDialog
onClose={this.props.onClose}
open={this.props.open}
style={this.props.style}
maxWidth={this.props.maxWidth}
fullWidth={this.props.fullWidth}
>
<DialogTitle>{this.props.title}</DialogTitle>
<DialogContent dividers>
{this.props.content}
{this.props.children}
</DialogContent>
<DialogActions>
{this.props.actions ? this.props.actions :
<Button onClick={this.props.onClick} color="primary">
{this.props.button}
</Button>
}
</DialogActions>
</MaterialUIDialog>
);
};
}
export default Dialog;
+1 -1
View File
@@ -87,7 +87,7 @@ class Home extends Component {
</IconButton>
</Tooltip>
<TrashcanButtons />
<BlocklyWindow />
<BlocklyWindow blocklyCSS={{height: '500px'}} blockDisabled/>
</Grid>
{this.state.codeOn ?
<Grid item xs={12} md={6}>
+3 -3
View File
@@ -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 {
</div>
</div>
<List>
{[{text: 'Tutorials', icon: faChalkboardTeacher}, {text: 'Einstellungen', icon: faCog}].map((item, index) => (
<Link to={"/tutorial"} key={index} style={{textDecoration: 'none', color: 'inherit'}}>
{[{text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial"}, {text: 'Tutorial-Builder', icon: faFolderPlus, link: "/tutorial/builder"}, {text: 'Einstellungen', icon: faCog, link: "/settings"}].map((item, index) => (
<Link to={item.link} key={index} style={{textDecoration: 'none', color: 'inherit'}}>
<ListItem button onClick={this.toggleDrawer}>
<ListItemIcon><FontAwesomeIcon icon={item.icon}/></ListItemIcon>
<ListItemText primary={item.text} />
+2
View File
@@ -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 {
<Switch>
<Route path="/" exact component={Home} />
<Route path="/tutorial" exact component={TutorialHome} />
<Route path="/tutorial/builder" exact component={Builder}/>
<Route path="/tutorial/:tutorialId" exact component={Tutorial} />
<Route component={NotFound} />
</Switch>
+78
View File
@@ -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 (
<MaterialUISnackbar
anchorOrigin={{vertical: 'bottom', horizontal: 'left' }}
open={this.state.open}
key={this.props.key}
style={{left: '22px', bottom: '40px', width: '300px', zIndex: '100'}}
>
<SnackbarContent
style={{flexWrap: 'nowrap'}}
className={this.props.type === 'error' ? this.props.classes.error : this.props.classes.success}
action={
<IconButton onClick={this.onClose} style={{color: 'inherit'}}>
<FontAwesomeIcon icon={faTimes} size="xs"/>
</IconButton>
}
message={this.props.message}
/>
</MaterialUISnackbar>
);
};
}
export default withStyles(styles, {withTheme: true})(Snackbar);
+6 -3
View File
@@ -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 {
<div style={{float: 'right', height: '40px'}}><WorkspaceFunc solutionCheck/></div>
<Grid container spacing={2} style={{marginBottom: '5px'}}>
<Grid item xs={12} md={6} lg={8}>
<BlocklyWindow initialXml={statusTask ? statusTask.xml ? statusTask.xml : null : null}/>
<BlocklyWindow
initialXml={statusTask ? statusTask.xml ? statusTask.xml : null : null}
blockDisabled
blocklyCSS={{height: '500px'}}
/>
</Grid>
<Grid item xs={12} md={6} lg={4} style={isWidthDown('sm', this.props.width) ? {height: 'max-content'} : {}}>
<Card style={{height: 'calc(50% - 30px)', padding: '10px', marginBottom: '10px'}}>
<Typography variant='h5'>Arbeitsauftrag</Typography>
<Typography>{currentTask.text1}</Typography>
<Typography>{currentTask.text}</Typography>
</Card>
<div style={isWidthDown('sm', this.props.width) ? {height: '500px'} : {height: '50%'}}>
<CodeViewer />
@@ -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 (
<div style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
{!this.props.task ?
<FormControlLabel
labelPlacement="end"
label={"Blockly Beispiel"}
control={
<Switch
checked={this.state.checked}
onChange={(e) => this.onChange(e.target.checked)}
color="primary"
/>
}
/>
: <FormLabel style={{color: 'black'}}>Musterlösung</FormLabel>}
{this.state.checked ? !this.props.value || this.props.error ?
<FormHelperText style={{lineHeight: 'initial'}} className={this.props.classes.errorColor}>{`Reiche deine Blöcke ein, indem du auf den '${this.props.task ? 'Musterlösung einreichen' : 'Beispiel einreichen'}'-Button klickst.`}</FormHelperText>
: this.state.input ? <FormHelperText style={{lineHeight: 'initial'}}>Die letzte Einreichung erfolgte um {this.state.input} Uhr.</FormHelperText> : null
: null}
{this.state.checked && !this.props.task ?
<FormHelperText style={{lineHeight: 'initial'}}>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.</FormHelperText>
: null}
{this.state.checked ? (() => {
return(
<div style={{marginTop: '10px'}}>
<Grid container className={!this.props.value || this.props.error ? this.props.classes.errorBorder : null}>
<Grid item xs={12}>
<BlocklyWindow
blockDisabled={this.props.task}
trashcan={false}
initialXml={this.state.xml}
blocklyCSS={{height: '500px'}}
/>
</Grid>
</Grid>
<Button
className={!this.props.value || this.props.error ? this.props.classes.errorButton : null }
style={{marginTop: '5px', height: '40px'}}
variant='contained'
color='primary'
disabled={this.state.disabled}
onClick={() => this.setXml()}
>
{this.props.task ? 'Musterlösung einreichen' : 'Beispiel einreichen'}
</Button>
</div>
)})()
: null}
</div>
);
};
}
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));
+226
View File
@@ -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 (
<div>
<Breadcrumbs content={[{link: '/tutorial', title: 'Tutorial'}, {link: '/tutorial/builder', title: 'Builder'}]}/>
<h1>Tutorial-Builder</h1>
{/*upload JSON*/}
<div ref={this.inputRef}>
<input
style={{display: 'none'}}
accept="application/json"
onChange={(e) => {this.uploadJsonFile(e.target.files[0])}}
id="open-json"
type="file"
/>
<label htmlFor="open-json">
<Button component="span" style={{marginRight: '10px', marginBottom: '10px'}} variant='contained' color='primary'>Datei laden</Button>
</label>
<Button style={{marginRight: '10px', marginBottom: '10px'}} variant='contained' color='primary' onClick={() => this.uploadJsonString()}>String laden</Button>
</div>
<Divider variant='fullWidth' style={{margin: '10px 0 15px 0'}}/>
{/*Tutorial-Builder-Form*/}
{this.props.error.type ?
<FormHelperText style={{lineHeight: 'initial'}} className={this.props.classes.errorColor}>{`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`}</FormHelperText>
: null}
<Id error={this.props.error.id} value={this.props.id}/>
<Textfield value={this.props.title} property={'title'} label={'Titel'} error={this.props.error.title}/>
{this.props.steps.map((step, i) =>
<Step step={step} index={i} key={i}/>
)}
{/*submit or reset*/}
<Divider variant='fullWidth' style={{margin: '30px 0 10px 0'}}/>
<Button style={{marginRight: '10px', marginTop: '10px'}} variant='contained' color='primary' onClick={() => this.submit()}>Tutorial-Vorlage erstellen</Button>
<Button style={{marginTop: '10px'}} variant='contained' onClick={() => this.reset()}>Zurücksetzen</Button>
<Backdrop className={this.props.classes.backdrop} open={this.props.isProgress}>
<CircularProgress color="inherit" />
</Backdrop>
<Dialog
open={this.state.open}
maxWidth={this.state.string ? 'md' : 'sm'}
fullWidth={this.state.string}
title={this.state.title}
content={this.state.content}
onClose={this.toggle}
onClick={this.toggle}
button={'Schließen'}
actions={
this.state.string ?
<div>
<Button disabled={this.props.error.json || this.props.json === ''} variant='contained' onClick={() => {this.toggle(); this.props.progress(true); this.readJson(this.props.json, false);}} color="primary">Bestätigen</Button>
<Button onClick={() => {this.toggle(); this.props.jsonString('');}} color="primary">Abbrechen</Button>
</div>
: null
}
>
{this.state.string ?
<Textfield value={this.props.json} property={'json'} label={'JSON'} multiline error={this.props.error.json}/>
: null}
</Dialog>
<Snackbar
open={this.state.snackbar}
message={this.state.message}
type={this.state.type}
key={this.state.key}
/>
</div>
);
};
}
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));
+105
View File
@@ -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 (
<div style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
<FormLabel style={{color: 'black'}}>Hardware</FormLabel>
<FormHelperText style={this.props.error ? {lineHeight: 'initial', marginTop: '5px'} : {marginTop: '5px', lineHeight: 'initial', marginBottom: '10px'}}>Beachte, dass die Reihenfolge des Auswählens maßgebend ist.</FormHelperText>
{this.props.error ? <FormHelperText className={this.props.classes.errorColor}>Wähle mindestens eine Hardware-Komponente aus.</FormHelperText> : null}
<GridList cellHeight={100} cols={cols} spacing={10}>
{hardware.map((picture,i) => (
<GridListTile key={i} onClick={() => this.onChange(picture.id)} classes={{tile: this.props.value.filter(value => value === picture.id).length > 0 ? this.props.classes.active : this.props.classes.border}}>
<div style={{margin: 'auto', width: 'max-content'}}>
<img src={`/media/hardware/${picture.src}`} alt={picture.name} height={100} />
</div>
<GridListTileBar
classes={{root: this.props.classes.multiGridListTile}}
title={
<div style={{overflow: 'hidden', textOverflow: 'ellipsis'}} className={this.props.classes.multiGridListTileTitle}>
{picture.name}
</div>
}
/>
</GridListTile>
))}
</GridList>
</div>
);
};
}
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)));
+121
View File
@@ -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 (
<div style={{display: 'inline-flex', marginTop: '15px'}}>
<FormControl variant="outlined" style={{marginBottom: '10px', width: '250px'}}>
<InputLabel
htmlFor="id"
classes={{shrink: this.props.error ? this.props.classes.errorColorShrink : null}}
>
ID
</InputLabel>
<OutlinedInput
style={{borderRadius: '25px', padding: '0 0 0 10px', width: '200px'}}
classes={{notchedOutline: this.props.error ? this.props.classes.errorBorder : null}}
error={this.props.error}
value={this.props.value}
name='id'
label='ID'
id='id'
onChange={this.handleChange}
inputProps={{
style: {marginRight: '10px'}
}}
endAdornment={
<div style={{display: 'flex'}}>
<Button
disabled={this.props.value === 1 || !Number.isInteger(this.props.value)}
onClick={() => this.handleCounter(-1)}
variant='contained'
color='primary'
style={{borderRadius: '25px 0 0 25px', height: '56px', boxShadow: '0 0 transparent'}}
>
<FontAwesomeIcon icon={faMinus} />
</Button>
<Button
onClick={() => this.handleCounter(1)}
variant='contained'
color='primary'
style={{borderRadius: '0 25px 25px 0', height: '56px', boxShadow: '0 0 transparent'}}
>
<FontAwesomeIcon icon={faPlus} />
</Button>
</div>
}
/>
{this.props.error ? <FormHelperText className={this.props.classes.errorColor}>Gib eine positive ganzzahlige Zahl ein.</FormHelperText> : null}
</FormControl>
<FormHelperText style={{marginLeft: '-40px', marginTop: '5px', lineHeight: 'initial', marginBottom: '10px', width: '200px'}}>Beachte, dass die ID eindeutig sein muss. Sie muss sich also zu den anderen Tutorials unterscheiden.</FormHelperText>
</div>
);
};
}
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));
@@ -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 (
<FormControl style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
<FormLabel style={{color: 'black'}}>Voraussetzungen</FormLabel>
<FormHelperText style={{marginTop: '5px'}}>Beachte, dass die Reihenfolge des Anhakens maßgebend ist.</FormHelperText>
<FormGroup>
{tutorials.map((tutorial, i) =>
<FormControlLabel
key={i}
control={
<Checkbox
value={tutorial.id}
checked={this.props.value.filter(id => id === tutorial.id).length > 0}
onChange={(e) => this.onChange(e)}
name="requirements"
color="primary"
/>
}
label={tutorial.title}
/>
)}
</FormGroup>
</FormControl>
);
};
}
Requirements.propTypes = {
changeContent: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
change: state.builder.change
});
export default connect(mapStateToProps, { changeContent })(Requirements);
+129
View File
@@ -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 (
<div style={{borderRadius: '25px', border: '1px solid lightgrey', padding: '10px 14px 10px 10px', marginBottom: '20px'}}>
<Typography variant='h6' style={{marginBottom: '10px', marginLeft: '4px'}}>Schritt {index+1}</Typography>
<div style={{display: 'flex', position: 'relative'}}>
<div style={{width: '40px', marginRight: '10px', position: 'absolute', left: '4px', bottom: '10px'}}>
<Tooltip title='Schritt hinzufügen' arrow>
<IconButton
className={this.props.classes.button}
style={index === 0 ? {} : {marginBottom: '5px'}}
onClick={() => this.props.addStep(index+1)}
>
<FontAwesomeIcon icon={faPlus} size="xs"/>
</IconButton>
</Tooltip>
{index !== 0 ?
<div>
<Tooltip title={`Schritt ${index+1} nach oben schieben`} arrow>
<IconButton
disabled={index < 2}
className={this.props.classes.button}
style={{marginBottom: '5px'}}
onClick={() => this.props.changeStepIndex(index, index-1)}
>
<FontAwesomeIcon icon={faAngleDoubleUp} size="xs"/>
</IconButton>
</Tooltip>
<Tooltip title={`Schritt ${index+1} nach unten schieben`} arrow>
<IconButton
disabled={index === steps.length-1}
className={this.props.classes.button}
style={{marginBottom: '5px'}}
onClick={() => this.props.changeStepIndex(index, index+1)}
>
<FontAwesomeIcon icon={faAngleDoubleDown} size="xs"/>
</IconButton>
</Tooltip>
<Tooltip title={`Schritt ${index+1} löschen`} arrow>
<IconButton
disabled={index === 0}
className={clsx(this.props.classes.button, this.props.classes.delete)}
onClick={() => this.props.removeStep(index)}
>
<FontAwesomeIcon icon={faTrash} size="xs"/>
</IconButton>
</Tooltip>
</div>
: null}
</div>
<div style={{width: '100%', marginLeft: '54px'}}>
<StepType value={this.props.step.type} index={index} />
<Textfield value={this.props.step.headline} property={'headline'} label={'Überschrift'} index={index} error={this.props.error.steps[index].headline} errorText={`Gib eine Überschrift für die ${this.props.step.type === 'task' ? 'Aufgabe' : 'Anleitung'} ein.`} />
<Textfield value={this.props.step.text} property={'text'} label={this.props.step.type === 'task' ? 'Aufgabenstellung' : 'Instruktionen'} index={index} multiline error={this.props.error.steps[index].text} errorText={`Gib Instruktionen für die ${this.props.step.type === 'task' ? 'Aufgabe' : 'Anleitung'} ein.`}/>
{index === 0 ?
<div>
<Requirements value={this.props.step.requirements ? this.props.step.requirements : []} index={index}/>
<Hardware value={this.props.step.hardware ? this.props.step.hardware : []} index={index} error={this.props.error.steps[index].hardware}/>
</div>
: null}
<BlocklyExample value={this.props.step.xml} index={index} task={this.props.step.type === 'task'} error={this.props.error.steps[index].xml ? true : false}/>
</div>
</div>
</div>
);
};
}
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));
@@ -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 (
<RadioGroup row value={this.props.value === 'task' ? 'task' : 'instruction'} onChange={(e) => this.onChange(e.target.value)}>
<FormControlLabel style={{color: 'black'}}
value="instruction"
control={<Radio color="primary" />}
label="Anleitung"
labelPlacement="end"
/>
<FormControlLabel style={{color: 'black'}}
disabled={this.props.index === 0}
value="task"
control={<Radio color="primary" />}
label="Aufgabe"
labelPlacement="end"
/>
</RadioGroup>
);
};
}
StepType.propTypes = {
changeContent: PropTypes.func.isRequired,
deleteProperty: PropTypes.func.isRequired,
deleteError: PropTypes.func.isRequired
};
export default connect(null, { changeContent, deleteProperty, deleteError })(StepType);
@@ -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 (
<FormControl variant="outlined" fullWidth style={{marginBottom: '10px'}}>
<InputLabel
htmlFor={this.props.property}
classes={{shrink: this.props.error ? this.props.classes.errorColorShrink : null}}
>
{this.props.label}
</InputLabel>
<OutlinedInput
style={{borderRadius: '25px'}}
classes={{multiline: this.props.classes.multiline, notchedOutline: this.props.error ? this.props.classes.errorBorder : null}}
error={this.props.error}
value={this.props.value}
label={this.props.label}
id={this.props.property}
multiline={this.props.multiline}
rows={2}
rowsMax={10}
onChange={(e) => this.handleChange(e)}
/>
{this.props.error ?
this.props.property === 'title' ? <FormHelperText className={this.props.classes.errorColor}>Gib einen Titel für das Tutorial ein.</FormHelperText>
: this.props.property === 'json' ? <FormHelperText className={this.props.classes.errorColor}>Gib einen JSON-String ein und bestätige diesen mit einem Klick auf den entsprechenden Button</FormHelperText>
: <FormHelperText className={this.props.classes.errorColor}>{this.props.errorText}</FormHelperText>
: null}
</FormControl>
);
};
}
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));
+42 -42
View File
@@ -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 {
<div style={{marginTop: '10px', marginBottom: '5px'}}>
<Typography>Für die Umsetzung benötigst du folgende Hardware:</Typography>
<GridList cellHeight={100} cols={cols} spacing={10}>
{this.props.picture.map((picture,i) => (
<GridListTile key={i}>
<div style={{margin: 'auto', width: 'max-content'}}>
<img src={`/media/hardware/${picture}.png`} alt={picture} height={100} style={{cursor: 'pointer'}} onClick={() => this.handleClickOpen(picture, `/media/hardware/${picture}.png`)}/>
</div>
<GridListTileBar
classes={{root: this.props.classes.multiGridListTile}}
title={
<div style={{overflow: 'hidden', textOverflow: 'ellipsis'}} className={this.props.classes.multiGridListTileTitle}>
{picture}
</div>
}
actionIcon={
<IconButton className={this.props.classes.expand} aria-label='Vollbild' onClick={() => this.handleClickOpen(picture, `/media/hardware/${picture}.png`)}>
<FontAwesomeIcon icon={faExpandAlt} size="xs"/>
</IconButton>
}
/>
</GridListTile>
))}
</GridList>
<GridList cellHeight={100} cols={cols} spacing={10}>
{this.props.picture.map((picture,i) => {
var hardwareInfo = hardware.filter(hardware => hardware.id === picture)[0];
return(
<GridListTile key={i}>
<div style={{margin: 'auto', width: 'max-content'}}>
<img src={`/media/hardware/${hardwareInfo.src}`} alt={hardwareInfo.name} height={100} style={{cursor: 'pointer'}} onClick={() => this.handleClickOpen(hardwareInfo)}/>
</div>
<GridListTileBar
classes={{root: this.props.classes.multiGridListTile}}
title={
<div style={{overflow: 'hidden', textOverflow: 'ellipsis'}} className={this.props.classes.multiGridListTileTitle}>
{hardwareInfo.name}
</div>
}
actionIcon={
<IconButton className={this.props.classes.expand} aria-label='Vollbild' onClick={() => this.handleClickOpen(hardwareInfo)}>
<FontAwesomeIcon icon={faExpandAlt} size="xs"/>
</IconButton>
}
/>
</GridListTile>
)})}
</GridList>
<Dialog
style={{zIndex: 1500}}
fullWidth={true}
open={this.state.open}
title={`Hardware: ${this.state.hardwareInfo.name}`}
content={this.state.content}
onClose={this.handleClose}
onClick={this.handleClose}
button={'Schließen'}
>
<DialogTitle style={{padding: "10px 24px"}}>Hardware: {this.state.title}</DialogTitle>
<DialogContent style={{padding: "0px"}}>
<img src={this.state.url} width="100%" alt={this.state.title}/>
</DialogContent>
<DialogActions style={{padding: "10px 24px"}}>
<Button onClick={this.handleClose} color="primary">
Schließen
</Button>
</DialogActions>
<div>
<img src={`/media/hardware/${this.state.hardwareInfo.src}`} width="100%" alt={this.state.hardwareInfo.name}/>
Weitere Informationen zur Hardware-Komponente findest du <Link href={this.state.hardwareInfo.url} color="primary">hier</Link>.
</div>
</Dialog>
</div>
);
};
+3 -7
View File
@@ -18,7 +18,7 @@ class Instruction extends Component {
return (
<div>
<Typography variant='h4' style={{marginBottom: '5px'}}>{step.headline}</Typography>
<Typography style={isHardware ? {} : {marginBottom: '5px'}}>{step.text1}</Typography>
<Typography style={isHardware ? {} : {marginBottom: '5px'}}>{step.text}</Typography>
{isHardware ?
<Hardware picture={step.hardware}/> : null}
{areRequirements > 0 ?
@@ -27,12 +27,8 @@ class Instruction extends Component {
<Grid container spacing={2} style={{marginBottom: '5px'}}>
<Grid item xs={12}>
<BlocklyWindow
trashcan={false}
readOnly={true}
zoomControls={false}
grid={false}
move={false}
blocklyCSS={{minHeight: '300px'}}
svg
blockDisabled
initialXml={step.xml}
/>
</Grid>
+1 -1
View File
@@ -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';
+40 -39
View File
@@ -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 {
<FontAwesomeIcon icon={faPlay} size="xs"/>
</IconButton>
</Tooltip>
<Dialog fullWidth maxWidth={'sm'} onClose={this.toggleDialog} open={this.state.open} style={{zIndex: 9999999}}>
<DialogTitle>{this.state.msg.type === 'error' ? 'Fehler' : 'Erfolg'}</DialogTitle>
<DialogContent dividers>
{this.state.msg.text}
{this.state.msg.type === 'success' ?
<div style={{marginTop: '20px', display: 'flex'}}>
<Compile />
{this.props.activeStep === steps.length-1 ?
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.history.push(`/tutorial/`)}}
>
Tutorials-Übersicht
</Button>
:
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.tutorialStep(this.props.activeStep + 1)}}
>
nächster Schritt
</Button>
}
</div>
: null}
</DialogContent>
<DialogActions>
<Button onClick={this.toggleDialog} color="primary">
Schließen
</Button>
</DialogActions>
<Dialog
style={{zIndex: 9999999}}
fullWidth
maxWidth={'sm'}
open={this.state.open}
title={this.state.msg.type === 'error' ? 'Fehler' : 'Erfolg'}
content={this.state.msg.text}
onClose={this.toggleDialog}
onClick={this.toggleDialog}
button={'Schließen'}
>
{this.state.msg.type === 'success' ?
<div style={{marginTop: '20px', display: 'flex'}}>
<Compile />
{this.props.activeStep === steps.length-1 ?
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.history.push(`/tutorial/`)}}
>
Tutorials-Übersicht
</Button>
:
<Button
style={{marginLeft: '10px'}}
variant="contained"
color="primary"
onClick={() => {this.toggleDialog(); this.props.tutorialStep(this.props.activeStep + 1)}}
>
nächster Schritt
</Button>
}
</div>
: null}
</Dialog>
</div>
);
};
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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 ?
<NotFound button={{title: 'Zurück zur Tutorials-Übersicht', link: '/tutorial'}}/>
:
<div>
+1 -1
View File
@@ -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';
-96
View File
@@ -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": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'></block></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": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'></block></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": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'></block></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": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='sensebox_wifi' id='-!X.Ay]z1ACt!f5+Vfr8'><field name='SSID'>SSID</field><field name='Password'>Password</field></block></xml>"
},
{
"id": 3,
"type": "instruction",
"headline": "Block richtig einbinden",
"text1": "",
"xml": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'><statement name='SETUP_FUNC'><block type='sensebox_wifi' id='W}P2Y^g,muH@]|@anou}'><field name='SSID'>SSID</field><field name='Password'>Password</field></block></statement></block></xml>"
},
{
"id": 4,
"type": "task",
"headline": "Aufgabe 1",
"text1": "Stelle eine WLAN-Verbindung mit einem beliebigen Netzwerk her.",
"xml": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'><statement name='SETUP_FUNC'><block type='sensebox_wifi' id='W}P2Y^g,muH@]|@anou}'><field name='SSID'>SSID</field><field name='Password'>Password</field></block></statement></block></xml>"
},
{
"id": 5,
"type": "task",
"headline": "Aufgabe 2",
"text1": "Versuche das gleiche einfach nochmal. Übung macht den Meister! ;)",
"xml": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='arduino_functions' id='QWW|$jB8+*EL;}|#uA' deletable='false' x='27' y='16'><statement name='SETUP_FUNC'><block type='sensebox_wifi' id='W}P2Y^g,muH@]|@anou}'><field name='SSID'>SSID</field><field name='Password'>Password</field></block></statement></block></xml>"
}
]
}
]
+89 -31
View File
@@ -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 = '<defs><style type="text/css" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[' + cssContent + ']]></style></defs>';
var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox();
var content = new XMLSerializer().serializeToString(canvas);
var xml = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="${bbox.width}" height="${bbox.height}" viewBox="${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}">
${css}">${content}</svg>`;
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 {
<div style={{width: 'max-content', display: 'flex'}}>
{!this.props.solutionCheck ?
<Tooltip title={`Name des Projekts${this.props.name ? `: ${this.props.name}` : ''}`} arrow style={{marginRight: '5px'}}>
<div className={this.props.classes.workspaceName} onClick={() => {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\'.'})}}>
<div className={this.props.classes.workspaceName} onClick={() => {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) ? <Typography style={{margin: 'auto -3px auto 12px'}}>{this.props.name}</Typography> : null}
<div style={{width: '40px', display: 'flex'}}>
<FontAwesomeIcon icon={faPen} style={{height: '18px', width: '18px', margin: 'auto'}}/>
@@ -164,7 +207,7 @@ class WorkspaceFunc extends Component {
<Tooltip title='Blöcke speichern' arrow style={{marginRight: '5px'}}>
<IconButton
className={this.props.classes.button}
onClick={() => this.createFileName()}
onClick={() => {this.createFileName('xml');}}
>
<FontAwesomeIcon icon={faSave} size="xs"/>
</IconButton>
@@ -187,6 +230,14 @@ class WorkspaceFunc extends Component {
</Tooltip>
</label>
</div>
<Tooltip title='Screenshot erstellen' arrow style={{marginRight: '5px'}}>
<IconButton
className={this.props.classes.button}
onClick={() => {this.createFileName('svg');}}
>
<FontAwesomeIcon icon={faCamera} size="xs" />
</IconButton>
</Tooltip>
<Tooltip title='Workspace zurücksetzen' arrow>
<IconButton
className={this.props.classes.button}
@@ -195,23 +246,30 @@ class WorkspaceFunc extends Component {
<FontAwesomeIcon icon={faShare} size="xs" flip='horizontal'/>
</IconButton>
</Tooltip>
<Dialog onClose={this.toggleDialog} open={this.state.open}>
<DialogTitle>{this.state.title}</DialogTitle>
<DialogContent dividers>
{this.state.content}
{this.state.file ?
<div style={{marginTop: '10px'}}>
<TextField autoFocus placeholder={this.state.saveXml ?'Dateiname' : 'Projektname'} value={this.state.name} onChange={this.setFileName} style={{marginRight: '10px'}}/>
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => {this.state.saveXml ? this.saveXmlFile() : this.props.workspaceName(this.state.name); this.toggleDialog();}}>Eingabe</Button>
</div>
: null}
</DialogContent>
<DialogActions>
<Button onClick={this.state.file ? () => {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog} color="primary">
{this.state.file ? 'Abbrechen' : 'Schließen'}
</Button>
</DialogActions>
<Dialog
open={this.state.open}
title={this.state.title}
content={this.state.content}
onClose={this.toggleDialog}
onClick={this.state.file ? () => {this.toggleDialog(); this.setState({name: this.props.name})} : this.toggleDialog}
button={this.state.file ? 'Abbrechen' : 'Schließen'}
>
{this.state.file ?
<div style={{marginTop: '10px'}}>
<TextField autoFocus placeholder={this.state.saveXml ?'Dateiname' : 'Projektname'} value={this.state.name} onChange={this.setFileName} style={{marginRight: '10px'}}/>
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => {this.state.saveFile ? this.state.file === 'xml' ? this.saveXmlFile() : this.getSvg() : this.renameWorkspace(); this.toggleDialog();}}>Eingabe</Button>
</div>
: null}
</Dialog>
<Snackbar
open={this.state.snackbar}
message={this.state.message}
type='success'
key={this.state.key}
/>
</div>
);
};