Merge branch 'tutorial-builder'
This commit is contained in:
commit
1518787e13
@ -14,6 +14,7 @@
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"blockly": "^3.20200625.2",
|
||||
"file-saver": "^2.0.2",
|
||||
"moment": "^2.28.0",
|
||||
"prismjs": "^1.20.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 630 KiB |
@ -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({
|
||||
|
257
src/actions/tutorialBuilderActions.js
Normal file
257
src/actions/tutorialBuilderActions.js
Normal file
@ -0,0 +1,257 @@
|
||||
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'));
|
||||
}
|
||||
for(var i = 0; i < builder.steps.length; i++){
|
||||
builder.steps[i].id = i+1;
|
||||
if(i === 0){
|
||||
if(builder.steps[i].requirements && builder.steps[i].requirements.length > 0){
|
||||
var requirements = builder.steps[i].requirements.filter(requirement => typeof(requirement)==='number');
|
||||
if(requirements.length < builder.steps[i].requirements.length){
|
||||
dispatch(changeContent(i, 'requirements', requirements));
|
||||
}
|
||||
}
|
||||
if(builder.steps[i].hardware === undefined || builder.steps[i].hardware.length < 1){
|
||||
dispatch(setError(i, 'hardware'));
|
||||
}
|
||||
else{
|
||||
var hardwareIds = data.map(hardware => hardware.id);
|
||||
var hardware = builder.steps[i].hardware.filter(hardware => hardwareIds.includes(hardware));
|
||||
if(hardware.length < builder.steps[i].hardware.length){
|
||||
dispatch(changeContent(i, 'hardware', hardware));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(builder.steps[i].headline === undefined || builder.steps[i].headline === ''){
|
||||
dispatch(setError(i, 'headline'));
|
||||
}
|
||||
if(builder.steps[i].text === undefined || builder.steps[i].text === ''){
|
||||
dispatch(setError(i, 'text'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkError = () => (dispatch, getState) => {
|
||||
dispatch(setSubmitError());
|
||||
var error = getState().builder.error;
|
||||
if(error.id || error.title){
|
||||
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(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: new Array(json.steps.length).fill({})
|
||||
}
|
||||
});
|
||||
dispatch(tutorialTitle(json.title));
|
||||
dispatch(tutorialId(json.id));
|
||||
dispatch(tutorialSteps(json.steps));
|
||||
dispatch(setSubmitError());
|
||||
dispatch(progress(false));
|
||||
};
|
@ -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';
|
||||
|
@ -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
src/components/Dialog.js
Normal file
38
src/components/Dialog.js
Normal 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;
|
@ -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}].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} />
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
||||
@ -45,7 +44,7 @@ class Assessment extends Component {
|
||||
<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 />
|
||||
|
150
src/components/Tutorial/Builder/BlocklyExample.js
Normal file
150
src/components/Tutorial/Builder/BlocklyExample.js
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions';
|
||||
|
||||
import moment from 'moment';
|
||||
import localization from 'moment/locale/de';
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
import BlocklyWindow from '../../Blockly/BlocklyWindow';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
|
||||
const styles = (theme) => ({
|
||||
errorColor: {
|
||||
color: theme.palette.error.dark
|
||||
},
|
||||
errorBorder: {
|
||||
border: `1px solid ${theme.palette.error.dark}`
|
||||
},
|
||||
errorButton: {
|
||||
marginTop: '5px',
|
||||
height: '40px',
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
'&:hover':{
|
||||
backgroundColor: theme.palette.error.dark
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class BlocklyExample extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state={
|
||||
checked: props.task ? props.task : props.value ? true : false,
|
||||
input: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.isError();
|
||||
// if(this.props.task){
|
||||
// this.props.setError(this.props.index, 'xml');
|
||||
// }
|
||||
}
|
||||
|
||||
componentDidUpdate(props, state){
|
||||
if(props.task !== this.props.task || props.value !== this.props.value){
|
||||
this.setState({checked: this.props.task ? this.props.task : this.props.value ? true : false},
|
||||
() => this.isError()
|
||||
);
|
||||
}
|
||||
if(state.checked !== this.state.checked){
|
||||
this.isError();
|
||||
}
|
||||
}
|
||||
|
||||
isError = () => {
|
||||
if(this.state.checked && !this.props.value){
|
||||
this.props.setError(this.props.index, 'xml');
|
||||
}
|
||||
else {
|
||||
this.props.deleteError(this.props.index, 'xml');
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (value) => {
|
||||
var oldValue = this.state.checked;
|
||||
this.setState({checked: value});
|
||||
if(oldValue !== value && !value){
|
||||
this.props.deleteProperty(this.props.index, 'xml');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
moment.locale('de', localization);
|
||||
return (
|
||||
<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 rot gefärbten 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 ? (() => {
|
||||
var initialXml = this.props.value;
|
||||
// check if value is valid xml;
|
||||
try{
|
||||
Blockly.Xml.textToDom(initialXml);
|
||||
}
|
||||
catch(err){
|
||||
initialXml = null;
|
||||
this.props.setError(this.props.index, 'xml');
|
||||
}
|
||||
return (
|
||||
<div style={{marginTop: '10px'}}>
|
||||
<Grid container className={!this.props.value || this.props.error ? this.props.classes.errorBorder : null}>
|
||||
<Grid item xs={12}>
|
||||
<BlocklyWindow initialXml={initialXml}/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button
|
||||
className={!this.props.value || this.props.error ? this.props.classes.errorButton : null }
|
||||
style={{marginTop: '5px', height: '40px'}}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => {this.props.changeContent(this.props.index, 'xml', this.props.xml); this.setState({input: moment(Date.now()).format('LTS')})}}
|
||||
>
|
||||
{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));
|
201
src/components/Tutorial/Builder/Builder.js
Normal file
201
src/components/Tutorial/Builder/Builder.js
Normal file
@ -0,0 +1,201 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { checkError, readJSON, jsonString, progress, resetTutorial } from '../../../actions/tutorialBuilderActions';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../../helpers/whitespace';
|
||||
|
||||
import Breadcrumbs from '../../Breadcrumbs';
|
||||
import Id from './Id';
|
||||
import Textfield from './Textfield';
|
||||
import Step from './Step';
|
||||
import Dialog from '../../Dialog';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Backdrop from '@material-ui/core/Backdrop';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
|
||||
const styles = (theme) => ({
|
||||
backdrop: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
}
|
||||
});
|
||||
|
||||
class Builder extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
title: '',
|
||||
content: '',
|
||||
string: false
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
var isError = this.props.checkError();
|
||||
if(isError){
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
else{
|
||||
var tutorial = {
|
||||
id: this.props.id,
|
||||
title: this.props.title,
|
||||
steps: this.props.steps
|
||||
}
|
||||
var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' });
|
||||
saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`);
|
||||
}
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.props.resetTutorial();
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
uploadJsonFile = (jsonFile) => {
|
||||
this.props.progress(true);
|
||||
if(jsonFile.type !== 'application/json'){
|
||||
this.props.progress(false);
|
||||
this.setState({ open: true, string: false, title: 'Unzulässiger Dateityp', content: 'Die übergebene Datei entspricht nicht dem geforderten Format. Es sind nur JSON-Dateien zulässig.'});
|
||||
}
|
||||
else {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(jsonFile);
|
||||
reader.onloadend = () => {
|
||||
this.readJson(reader.result, true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
uploadJsonString = () => {
|
||||
this.setState({ open: true, string: true, title: 'JSON-String einfügen', content: ''});
|
||||
}
|
||||
|
||||
readJson = (jsonString, isFile) => {
|
||||
try {
|
||||
var result = JSON.parse(jsonString);
|
||||
if(!this.checkSteps(result.steps)){
|
||||
result.steps = [{}];
|
||||
}
|
||||
this.props.readJSON(result);
|
||||
} catch(err){
|
||||
console.log(err);
|
||||
this.props.progress(false);
|
||||
this.props.jsonString('');
|
||||
this.setState({ open: true, string: false, title: 'Ungültiges JSON-Format', content: `${isFile ? 'Die übergebene Datei' : 'Der übergebene String'} enthält nicht valides JSON. Bitte überprüfe ${isFile ? 'die JSON-Datei' : 'den JSON-String'} und versuche es erneut.`});
|
||||
}
|
||||
}
|
||||
|
||||
checkSteps = (steps) => {
|
||||
if(!(steps && steps.length > 0)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.setState({ open: !this.state });
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs content={[{link: '/', title: 'Home'},{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 30px 0'}}/>
|
||||
|
||||
{/*Tutorial-Builder-Form*/}
|
||||
<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} />
|
||||
)}
|
||||
|
||||
{/*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>
|
||||
|
||||
</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
src/components/Tutorial/Builder/Hardware.js
Normal file
105
src/components/Tutorial/Builder/Hardware.js
Normal 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 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)));
|
109
src/components/Tutorial/Builder/Id.js
Normal file
109
src/components/Tutorial/Builder/Id.js
Normal file
@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
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){
|
||||
this.props.tutorialId(0+step);
|
||||
}
|
||||
else {
|
||||
this.props.tutorialId(this.props.value+step);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{display: 'inline-flex'}}>
|
||||
<FormControl variant="outlined" /*fullWidth*/ style={{marginBottom: '10px', width: 'max-content'}}>
|
||||
<InputLabel htmlFor="id">ID</InputLabel>
|
||||
<OutlinedInput
|
||||
style={{borderRadius: '25px', padding: '0 0 0 10px', width: '200px'}}
|
||||
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 || !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: '10px', 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));
|
64
src/components/Tutorial/Builder/Requirements.js
Normal file
64
src/components/Tutorial/Builder/Requirements.js
Normal file
@ -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);
|
119
src/components/Tutorial/Builder/Step.js
Normal file
119
src/components/Tutorial/Builder/Step.js
Normal file
@ -0,0 +1,119 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { addStep, removeStep, changeStepIndex } from '../../../actions/tutorialBuilderActions';
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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={this.props.classes.button}
|
||||
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));
|
37
src/components/Tutorial/Builder/StepType.js
Normal file
37
src/components/Tutorial/Builder/StepType.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeContent } 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 {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RadioGroup row value={this.props.value === 'task' ? 'task' : 'instruction'} onChange={(e) => {this.props.changeContent(this.props.index, 'type', 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
|
||||
};
|
||||
|
||||
export default connect(null, { changeContent })(StepType);
|
80
src/components/Tutorial/Builder/Textfield.js
Normal file
80
src/components/Tutorial/Builder/Textfield.js
Normal file
@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
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}>{this.props.label}</InputLabel>
|
||||
<OutlinedInput
|
||||
style={{borderRadius: '25px'}}
|
||||
classes={{multiline: this.props.classes.multiline}}
|
||||
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));
|
@ -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 unter: <Link href={this.state.hardwareInfo.url} color="primary">{this.state.hardwareInfo.url}</Link>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 ?
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -1,85 +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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -12,14 +12,11 @@ import { initialXml } from './Blockly/initialXml.js';
|
||||
|
||||
import Compile from './Compile';
|
||||
import SolutionCheck from './Tutorial/SolutionCheck';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
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';
|
||||
@ -195,23 +192,23 @@ 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.saveXml ? this.saveXmlFile() : this.props.workspaceName(this.state.name); this.toggleDialog();}}>Eingabe</Button>
|
||||
</div>
|
||||
: null}
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
128
src/data/hardware.json
Normal file
128
src/data/hardware.json
Normal file
@ -0,0 +1,128 @@
|
||||
[
|
||||
{
|
||||
"id": "bmp280",
|
||||
"name": "Luftdruck und Temperatursensor",
|
||||
"src": "bmp280.png",
|
||||
"url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.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/blockly/de/uebersicht/sensebox_components.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/blockly/de/uebersicht/sensebox_components.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/blockly/de/uebersicht/sensebox_components.html"
|
||||
},
|
||||
{
|
||||
"id": "senseboxmcu",
|
||||
"name": "senseBox MCU",
|
||||
"src": "senseboxmcu.png",
|
||||
"url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.html"
|
||||
},
|
||||
{
|
||||
"id": "usb-cable",
|
||||
"name": "USB-Kabel",
|
||||
"src": "usb-cable.png",
|
||||
"url": "https://sensebox.github.io/books-v2/blockly/de/uebersicht/sensebox_components.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/blockly/de/uebersicht/sensebox_components.html"
|
||||
}
|
||||
]
|
85
src/data/tutorials.json
Normal file
85
src/data/tutorials.json
Normal file
@ -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":"<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",
|
||||
"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":"<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",
|
||||
"text":"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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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
|
||||
});
|
||||
|
68
src/reducers/tutorialBuilderReducer.js
Normal file
68
src/reducers/tutorialBuilderReducer.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
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')){
|
||||
|
Loading…
x
Reference in New Issue
Block a user