Merge branch 'tutorial'
This commit is contained in:
commit
1cd00dab3f
1
public/media/tutorial/block_en.svg
Normal file
1
public/media/tutorial/block_en.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.4 KiB |
@ -85,10 +85,18 @@ export const removeErrorStep = (index) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeContent = (index, property, content) => (dispatch, getState) => {
|
export const changeContent = (content, index, property1, property2) => (dispatch, getState) => {
|
||||||
var steps = getState().builder.steps;
|
var steps = getState().builder.steps;
|
||||||
var step = steps[index];
|
var step = steps[index];
|
||||||
step[property] = content;
|
if(property2){
|
||||||
|
if(step[property1] && step[property1][property2]){
|
||||||
|
step[property1][property2] = content;
|
||||||
|
} else {
|
||||||
|
step[property1] = {[property2]: content};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step[property1] = content;
|
||||||
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: BUILDER_CHANGE_STEP,
|
type: BUILDER_CHANGE_STEP,
|
||||||
payload: steps
|
payload: steps
|
||||||
@ -96,10 +104,16 @@ export const changeContent = (index, property, content) => (dispatch, getState)
|
|||||||
dispatch(changeTutorialBuilder());
|
dispatch(changeTutorialBuilder());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteProperty = (index, property) => (dispatch, getState) => {
|
export const deleteProperty = (index, property1, property2) => (dispatch, getState) => {
|
||||||
var steps = getState().builder.steps;
|
var steps = getState().builder.steps;
|
||||||
var step = steps[index];
|
var step = steps[index];
|
||||||
delete step[property];
|
if(property2){
|
||||||
|
if(step[property1] && step[property1][property2]){
|
||||||
|
delete step[property1][property2];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete step[property1];
|
||||||
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: BUILDER_DELETE_PROPERTY,
|
type: BUILDER_DELETE_PROPERTY,
|
||||||
payload: steps
|
payload: steps
|
||||||
@ -170,12 +184,14 @@ export const setSubmitError = () => (dispatch, getState) => {
|
|||||||
dispatch(setError(undefined, 'title'));
|
dispatch(setError(undefined, 'title'));
|
||||||
}
|
}
|
||||||
var type = builder.steps.map((step, i) => {
|
var type = builder.steps.map((step, i) => {
|
||||||
|
// media and xml are directly checked for errors in their components and
|
||||||
|
// therefore do not have to be checked again
|
||||||
step.id = i+1;
|
step.id = i+1;
|
||||||
if(i === 0){
|
if(i === 0){
|
||||||
if(step.requirements && step.requirements.length > 0){
|
if(step.requirements && step.requirements.length > 0){
|
||||||
var requirements = step.requirements.filter(requirement => typeof(requirement)==='number');
|
var requirements = step.requirements.filter(requirement => typeof(requirement)==='number');
|
||||||
if(requirements.length < step.requirements.length){
|
if(requirements.length < step.requirements.length){
|
||||||
dispatch(changeContent(i, 'requirements', requirements));
|
dispatch(changeContent(requirements, i, 'requirements'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(step.hardware === undefined || step.hardware.length < 1){
|
if(step.hardware === undefined || step.hardware.length < 1){
|
||||||
@ -185,7 +201,7 @@ export const setSubmitError = () => (dispatch, getState) => {
|
|||||||
var hardwareIds = data.map(hardware => hardware.id);
|
var hardwareIds = data.map(hardware => hardware.id);
|
||||||
var hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware));
|
var hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware));
|
||||||
if(hardware.length < step.hardware.length){
|
if(hardware.length < step.hardware.length){
|
||||||
dispatch(changeContent(i, 'hardware', hardware));
|
dispatch(changeContent(hardware, i, 'hardware'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,9 +271,35 @@ export const readJSON = (json) => (dispatch, getState) => {
|
|||||||
steps: json.steps.map(() => {return {};})
|
steps: json.steps.map(() => {return {};})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// accept only valid attributes
|
||||||
|
var steps = json.steps.map((step, i) => {
|
||||||
|
var object = {
|
||||||
|
id: step.id,
|
||||||
|
type: step.type,
|
||||||
|
headline: step.headline,
|
||||||
|
text: step.text
|
||||||
|
};
|
||||||
|
if(i === 0){
|
||||||
|
object.hardware = step.hardware;
|
||||||
|
object.requirements = step.requirements;
|
||||||
|
}
|
||||||
|
if(step.xml){
|
||||||
|
object.xml = step.xml;
|
||||||
|
}
|
||||||
|
if(step.media && step.type === 'instruction'){
|
||||||
|
object.media = {};
|
||||||
|
if(step.media.picture){
|
||||||
|
object.media.picture = step.media.picture;
|
||||||
|
}
|
||||||
|
else if(step.media.youtube){
|
||||||
|
object.media.youtube = step.media.youtube;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
});
|
||||||
dispatch(tutorialTitle(json.title));
|
dispatch(tutorialTitle(json.title));
|
||||||
dispatch(tutorialId(json.id));
|
dispatch(tutorialId(json.id));
|
||||||
dispatch(tutorialSteps(json.steps));
|
dispatch(tutorialSteps(steps));
|
||||||
dispatch(setSubmitError());
|
dispatch(setSubmitError());
|
||||||
dispatch(progress(false));
|
dispatch(progress(false));
|
||||||
};
|
};
|
||||||
|
@ -107,7 +107,7 @@ class BlocklyExample extends Component {
|
|||||||
|
|
||||||
setXml = () => {
|
setXml = () => {
|
||||||
var xml = this.props.xml;
|
var xml = this.props.xml;
|
||||||
this.props.changeContent(this.props.index, 'xml', xml);
|
this.props.changeContent(xml, this.props.index, 'xml');
|
||||||
this.setState({input: moment(Date.now()).format('LTS')});
|
this.setState({input: moment(Date.now()).format('LTS')});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +134,8 @@ class BlocklyExample extends Component {
|
|||||||
{this.state.checked && !this.props.task ?
|
{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>
|
<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}
|
: null}
|
||||||
{this.state.checked ? (() => {
|
{/* ensure that the correct xml-file is displayed in the workspace */}
|
||||||
|
{this.state.checked && this.state.xml? (() => {
|
||||||
return(
|
return(
|
||||||
<div style={{marginTop: '10px'}}>
|
<div style={{marginTop: '10px'}}>
|
||||||
<Grid container className={!this.props.value || this.props.error ? this.props.classes.errorBorder : null}>
|
<Grid container className={!this.props.value || this.props.error ? this.props.classes.errorBorder : null}>
|
||||||
|
@ -58,10 +58,17 @@ class Builder extends Component {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
|
// export steps without attribute 'url'
|
||||||
|
var steps = this.props.steps.map(step => {
|
||||||
|
if(step.url){
|
||||||
|
delete step.url;
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
});
|
||||||
var tutorial = {
|
var tutorial = {
|
||||||
id: this.props.id,
|
id: this.props.id,
|
||||||
title: this.props.title,
|
title: this.props.title,
|
||||||
steps: this.props.steps
|
steps: steps
|
||||||
}
|
}
|
||||||
var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' });
|
var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' });
|
||||||
saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`);
|
saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`);
|
||||||
|
@ -56,7 +56,7 @@ class Requirements extends Component {
|
|||||||
this.props.deleteError(this.props.index, 'hardware');
|
this.props.deleteError(this.props.index, 'hardware');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.props.changeContent(this.props.index, 'hardware', hardwareArray);
|
this.props.changeContent(hardwareArray, this.props.index, 'hardware');
|
||||||
if(hardwareArray.length === 0){
|
if(hardwareArray.length === 0){
|
||||||
this.props.setError(this.props.index, 'hardware');
|
this.props.setError(this.props.index, 'hardware');
|
||||||
}
|
}
|
||||||
|
178
src/components/Tutorial/Builder/Media.js
Normal file
178
src/components/Tutorial/Builder/Media.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions';
|
||||||
|
|
||||||
|
import Textfield from './Textfield';
|
||||||
|
|
||||||
|
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 Radio from '@material-ui/core/Radio';
|
||||||
|
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
|
||||||
|
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 Media extends Component {
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state={
|
||||||
|
checked: props.value ? true : false,
|
||||||
|
error: false,
|
||||||
|
radioValue: !props.picture && !props.youtube ? 'picture' : props.picture ? 'picture' : 'youtube'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(props){
|
||||||
|
if(props.value !== this.props.value){
|
||||||
|
this.setState({ checked: this.props.value ? true : false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeSwitch = (value) => {
|
||||||
|
var oldValue = this.state.checked;
|
||||||
|
this.setState({checked: value});
|
||||||
|
if(oldValue !== value){
|
||||||
|
if(value){
|
||||||
|
this.props.setError(this.props.index, 'media');
|
||||||
|
} else {
|
||||||
|
this.props.deleteError(this.props.index, 'media');
|
||||||
|
this.props.deleteProperty(this.props.index, 'media');
|
||||||
|
this.props.deleteProperty(this.props.index, 'url');
|
||||||
|
this.setState({ error: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeRadio = (value) => {
|
||||||
|
this.props.setError(this.props.index, 'media');
|
||||||
|
var oldValue = this.state.radioValue;
|
||||||
|
this.setState({radioValue: value, error: false});
|
||||||
|
// delete property 'oldValue', so that all old media files are reset
|
||||||
|
this.props.deleteProperty(this.props.index, 'media', oldValue);
|
||||||
|
if(oldValue === 'picture'){
|
||||||
|
this.props.deleteProperty(this.props.index, 'url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPicture = (pic) => {
|
||||||
|
if(!(/^image\/.*/.test(pic.type))){
|
||||||
|
this.props.setError(this.props.index, 'media');
|
||||||
|
this.setState({ error: true });
|
||||||
|
this.props.deleteProperty(this.props.index, 'url');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.props.deleteError(this.props.index, 'media');
|
||||||
|
this.setState({ error: false });
|
||||||
|
this.props.changeContent(URL.createObjectURL(pic), this.props.index, 'url');
|
||||||
|
}
|
||||||
|
this.props.changeContent(pic.name, this.props.index, 'media', 'picture');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
|
||||||
|
<FormControlLabel
|
||||||
|
labelPlacement="end"
|
||||||
|
label={"Medien"}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={this.state.checked}
|
||||||
|
onChange={(e) => this.onChangeSwitch(e.target.checked)}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{this.state.checked ?
|
||||||
|
<div>
|
||||||
|
<RadioGroup row value={this.state.radioValue} onChange={(e) => {this.onChangeRadio(e.target.value);}}>
|
||||||
|
<FormControlLabel style={{color: 'black'}}
|
||||||
|
value="picture"
|
||||||
|
control={<Radio color="primary" />}
|
||||||
|
label="Bild"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel style={{color: 'black'}}
|
||||||
|
value="youtube"
|
||||||
|
control={<Radio color="primary" />}
|
||||||
|
label="Youtube-Video"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
{this.state.radioValue === 'picture' ?
|
||||||
|
<div>
|
||||||
|
{!this.props.error ?
|
||||||
|
<div>
|
||||||
|
<FormHelperText style={{lineHeight: 'initial', marginBottom: '10px'}}>{`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.picture}' abgespeichert werden muss.`}</FormHelperText>
|
||||||
|
<img src={this.props.url ? this.props.url : `/media/tutorial/${this.props.picture}`} alt={this.props.url ? '' : `Das Bild '${this.props.picture}' konnte nicht im Ordner public/media/tutorial gefunden werden und kann daher nicht angezeigt werden.`} style={{maxHeight: '180px', maxWidth: '360px', marginBottom: '5px'}}/>
|
||||||
|
</div>
|
||||||
|
: <div
|
||||||
|
style={{height: '150px', maxWidth: '250px', marginBottom: '5px', justifyContent: "center", alignItems: "center", display:"flex", padding: '20px'}}
|
||||||
|
className={this.props.error ? this.props.classes.errorBorder : null} >
|
||||||
|
{this.state.error ?
|
||||||
|
<FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Die übergebene Datei entspricht nicht dem geforderten Bild-Format. Überprüfe, ob es sich um ein Bild handelt und versuche es nochmal.`}</FormHelperText>
|
||||||
|
: <FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Wähle ein Bild aus.`}</FormHelperText>
|
||||||
|
}
|
||||||
|
</div>}
|
||||||
|
{/*upload picture*/}
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
style={{display: 'none'}}
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => {this.uploadPicture(e.target.files[0]);}}
|
||||||
|
id={`picture ${this.props.index}`}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`picture ${this.props.index}`}>
|
||||||
|
<Button component="span" className={this.props.error ? this.props.classes.errorButton : null} style={{marginRight: '10px', marginBottom: '10px'}} variant='contained' color='primary'>Bild auswählen</Button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
/*youtube-video*/
|
||||||
|
<div>
|
||||||
|
<Textfield value={this.props.value && this.props.value.youtube} property={'media'} property2={'youtube'} label={'Youtube-ID'} index={this.props.index} error={this.props.error} errorText={`Gib eine Youtube-ID ein.`}/>
|
||||||
|
{this.props.youtube && !this.props.error ?
|
||||||
|
<div>
|
||||||
|
<FormHelperText style={{lineHeight: 'initial', margin: '0 25px 10px 25px'}}>{`Stelle sicher, dass das unten angezeigte Youtube-Video funktioniert, andernfalls überprüfe die Youtube-ID.`}</FormHelperText>
|
||||||
|
<div style={{position: 'relative', paddingBottom: '56.25%', height: 0}}>
|
||||||
|
<iframe title={this.props.youtube} style={{borderRadius: '25px', position: 'absolute', top: '0', left: '0', width: '100%', height: '100%'}} src={`https://www.youtube.com/embed/${this.props.youtube}`} frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Media.propTypes = {
|
||||||
|
changeContent: PropTypes.func.isRequired,
|
||||||
|
deleteProperty: PropTypes.func.isRequired,
|
||||||
|
setError: PropTypes.func.isRequired,
|
||||||
|
deleteError: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(null, { changeContent, deleteProperty, setError, deleteError })(withStyles(styles, {withTheme: true})(Media));
|
@ -23,7 +23,7 @@ class Requirements extends Component {
|
|||||||
else {
|
else {
|
||||||
requirements = requirements.filter(requirement => requirement !== value);
|
requirements = requirements.filter(requirement => requirement !== value);
|
||||||
}
|
}
|
||||||
this.props.changeContent(this.props.index, 'requirements', requirements);
|
this.props.changeContent(requirements, this.props.index, 'requirements');
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -10,6 +10,7 @@ import StepType from './StepType';
|
|||||||
import BlocklyExample from './BlocklyExample';
|
import BlocklyExample from './BlocklyExample';
|
||||||
import Requirements from './Requirements';
|
import Requirements from './Requirements';
|
||||||
import Hardware from './Hardware';
|
import Hardware from './Hardware';
|
||||||
|
import Media from './Media';
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
@ -103,6 +104,9 @@ class Step extends Component {
|
|||||||
<Hardware value={this.props.step.hardware ? this.props.step.hardware : []} index={index} error={this.props.error.steps[index].hardware}/>
|
<Hardware value={this.props.step.hardware ? this.props.step.hardware : []} index={index} error={this.props.error.steps[index].hardware}/>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
{this.props.step.type === 'instruction' ?
|
||||||
|
<Media value={this.props.step.media} picture={this.props.step.media && this.props.step.media.picture} youtube={this.props.step.media && this.props.step.media.youtube} url={this.props.step.url} index={index} error={this.props.error.steps[index].media} />
|
||||||
|
: null}
|
||||||
<BlocklyExample value={this.props.step.xml} index={index} task={this.props.step.type === 'task'} error={this.props.error.steps[index].xml ? true : false}/>
|
<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>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|||||||
class StepType extends Component {
|
class StepType extends Component {
|
||||||
|
|
||||||
onChange = (value) => {
|
onChange = (value) => {
|
||||||
this.props.changeContent(this.props.index, 'type', value);
|
this.props.changeContent(value, this.props.index, 'type');
|
||||||
// delete property 'xml', so that all used blocks are reset
|
// delete property 'xml', so that all used blocks are reset
|
||||||
this.props.deleteProperty(this.props.index, 'xml');
|
this.props.deleteProperty(this.props.index, 'xml');
|
||||||
if(value === 'task'){
|
if(value === 'task'){
|
||||||
|
@ -28,9 +28,11 @@ class Textfield extends Component {
|
|||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
if(this.props.error){
|
if(this.props.error){
|
||||||
|
if(this.props.property !== 'media'){
|
||||||
this.props.deleteError(this.props.index, this.props.property);
|
this.props.deleteError(this.props.index, this.props.property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleChange = (e) => {
|
handleChange = (e) => {
|
||||||
var value = e.target.value;
|
var value = e.target.value;
|
||||||
@ -41,7 +43,7 @@ class Textfield extends Component {
|
|||||||
this.props.jsonString(value);
|
this.props.jsonString(value);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.props.changeContent(this.props.index, this.props.property, value);
|
this.props.changeContent(value, this.props.index, this.props.property, this.props.property2);
|
||||||
}
|
}
|
||||||
if(value.replace(/\s/g,'') === ''){
|
if(value.replace(/\s/g,'') === ''){
|
||||||
this.props.setError(this.props.index, this.props.property);
|
this.props.setError(this.props.index, this.props.property);
|
||||||
|
@ -23,6 +23,17 @@ class Instruction extends Component {
|
|||||||
<Hardware picture={step.hardware}/> : null}
|
<Hardware picture={step.hardware}/> : null}
|
||||||
{areRequirements > 0 ?
|
{areRequirements > 0 ?
|
||||||
<Requirement tutorialIds={step.requirements}/> : null}
|
<Requirement tutorialIds={step.requirements}/> : null}
|
||||||
|
{step.media ?
|
||||||
|
step.media.picture ?
|
||||||
|
<div style={{display: 'flex', justifyContent: 'center', marginBottom: '5px'}}>
|
||||||
|
<img src={`/media/tutorial/${step.media.picture}`} alt='' style={{maxWidth: '100%'}}/>
|
||||||
|
</div>
|
||||||
|
: step.media.youtube ?
|
||||||
|
<div style={{position: 'relative', paddingBottom: '56.25%', height: 0}}>
|
||||||
|
<iframe title={step.media.youtube} style={{position: 'absolute', top: '0', left: '0', width: '100%', height: '100%'}} src={`https://www.youtube.com/embed/${step.media.youtube}`} frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
: null}
|
||||||
{step.xml ?
|
{step.xml ?
|
||||||
<Grid container spacing={2} style={{marginBottom: '5px'}}>
|
<Grid container spacing={2} style={{marginBottom: '5px'}}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
@ -57,14 +57,19 @@
|
|||||||
"type": "instruction",
|
"type": "instruction",
|
||||||
"headline": "Programmierung",
|
"headline": "Programmierung",
|
||||||
"text": "Man benötigt folgenden Block:",
|
"text": "Man benötigt folgenden Block:",
|
||||||
|
"media": {
|
||||||
|
"picture": "block_en.svg"
|
||||||
|
},
|
||||||
"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>"
|
"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,
|
"id": 3,
|
||||||
"type": "instruction",
|
"type": "instruction",
|
||||||
"headline": "Block richtig einbinden",
|
"headline": "Block richtig einbinden",
|
||||||
"text": "",
|
"text": "Dies ist ein Test.",
|
||||||
"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>"
|
"media": {
|
||||||
|
"youtube": "sf3RzXq6iVo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user