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 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({ | ||||
|     type: BUILDER_CHANGE_STEP, | ||||
|     payload: steps | ||||
| @ -96,10 +104,16 @@ export const changeContent = (index, property, content) => (dispatch, getState) | ||||
|   dispatch(changeTutorialBuilder()); | ||||
| }; | ||||
| 
 | ||||
| export const deleteProperty = (index, property) => (dispatch, getState) => { | ||||
| export const deleteProperty = (index, property1, property2) => (dispatch, getState) => { | ||||
|   var steps = getState().builder.steps; | ||||
|   var step = steps[index]; | ||||
|   delete step[property]; | ||||
|   if(property2){ | ||||
|     if(step[property1] && step[property1][property2]){ | ||||
|       delete step[property1][property2]; | ||||
|     } | ||||
|   } else { | ||||
|     delete step[property1]; | ||||
|   } | ||||
|   dispatch({ | ||||
|     type: BUILDER_DELETE_PROPERTY, | ||||
|     payload: steps | ||||
| @ -170,12 +184,14 @@ export const setSubmitError = () => (dispatch, getState) => { | ||||
|     dispatch(setError(undefined, 'title')); | ||||
|   } | ||||
|   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; | ||||
|     if(i === 0){ | ||||
|       if(step.requirements && step.requirements.length > 0){ | ||||
|         var requirements = step.requirements.filter(requirement => typeof(requirement)==='number'); | ||||
|         if(requirements.length < step.requirements.length){ | ||||
|           dispatch(changeContent(i, 'requirements', requirements)); | ||||
|           dispatch(changeContent(requirements, i, 'requirements')); | ||||
|         } | ||||
|       } | ||||
|       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 hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware)); | ||||
|         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 {};}) | ||||
|     } | ||||
|   }); | ||||
|   // 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(tutorialId(json.id)); | ||||
|   dispatch(tutorialSteps(json.steps)); | ||||
|   dispatch(tutorialSteps(steps)); | ||||
|   dispatch(setSubmitError()); | ||||
|   dispatch(progress(false)); | ||||
| }; | ||||
|  | ||||
| @ -107,7 +107,7 @@ class BlocklyExample extends Component { | ||||
| 
 | ||||
|   setXml = () => { | ||||
|     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')}); | ||||
|   } | ||||
| 
 | ||||
| @ -134,7 +134,8 @@ class BlocklyExample extends Component { | ||||
|         {this.state.checked && !this.props.task ? | ||||
|           <FormHelperText style={{lineHeight: 'initial'}}>Anmerkung: Man kann den initialen Setup()- bzw. Endlosschleifen()-Block löschen. Zusätzlich ist es möglich u.a. nur einen beliebigen Block auszuwählen, ohne dass dieser als deaktiviert dargestellt wird.</FormHelperText> | ||||
|         : null} | ||||
|         {this.state.checked ? (() => { | ||||
|         {/* ensure that the correct xml-file is displayed in the workspace */} | ||||
|         {this.state.checked && this.state.xml? (() => { | ||||
|           return( | ||||
|             <div style={{marginTop: '10px'}}> | ||||
|               <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); | ||||
|     } | ||||
|     else{ | ||||
|       // export steps without attribute 'url'
 | ||||
|       var steps = this.props.steps.map(step => { | ||||
|         if(step.url){ | ||||
|           delete step.url; | ||||
|         } | ||||
|         return step; | ||||
|       }); | ||||
|       var tutorial = { | ||||
|         id: this.props.id, | ||||
|         title: this.props.title, | ||||
|         steps: this.props.steps | ||||
|         steps: steps | ||||
|       } | ||||
|       var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' }); | ||||
|       saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`); | ||||
|  | ||||
| @ -56,7 +56,7 @@ class Requirements extends Component { | ||||
|         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){ | ||||
|       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 { | ||||
|       requirements = requirements.filter(requirement => requirement !== value); | ||||
|     } | ||||
|     this.props.changeContent(this.props.index, 'requirements', requirements); | ||||
|     this.props.changeContent(requirements, this.props.index, 'requirements'); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|  | ||||
| @ -10,6 +10,7 @@ import StepType from './StepType'; | ||||
| import BlocklyExample from './BlocklyExample'; | ||||
| import Requirements from './Requirements'; | ||||
| import Hardware from './Hardware'; | ||||
| import Media from './Media'; | ||||
| 
 | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| 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}/> | ||||
|               </div> | ||||
|             : 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}/> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
| @ -10,7 +10,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'; | ||||
| class StepType extends Component { | ||||
| 
 | ||||
|   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
 | ||||
|     this.props.deleteProperty(this.props.index, 'xml'); | ||||
|     if(value === 'task'){ | ||||
|  | ||||
| @ -28,7 +28,9 @@ class Textfield extends Component { | ||||
| 
 | ||||
|   componentDidMount(){ | ||||
|     if(this.props.error){ | ||||
|       this.props.deleteError(this.props.index, this.props.property); | ||||
|       if(this.props.property !== 'media'){ | ||||
|         this.props.deleteError(this.props.index, this.props.property); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -41,7 +43,7 @@ class Textfield extends Component { | ||||
|       this.props.jsonString(value); | ||||
|     } | ||||
|     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,'') === ''){ | ||||
|       this.props.setError(this.props.index, this.props.property); | ||||
|  | ||||
| @ -23,6 +23,17 @@ class Instruction extends Component { | ||||
|           <Hardware picture={step.hardware}/> : null} | ||||
|         {areRequirements > 0 ? | ||||
|           <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 ? | ||||
|         <Grid container spacing={2} style={{marginBottom: '5px'}}> | ||||
|           <Grid item xs={12}> | ||||
|  | ||||
| @ -57,14 +57,19 @@ | ||||
|                 "type": "instruction", | ||||
|                 "headline": "Programmierung", | ||||
|                 "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>" | ||||
|             }, | ||||
|             { | ||||
|                 "id": 3, | ||||
|                 "type": "instruction", | ||||
|                 "headline": "Block richtig einbinden", | ||||
|                 "text": "", | ||||
|                 "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>" | ||||
|                 "text": "Dies ist ein Test.", | ||||
|                 "media": { | ||||
|                   "youtube": "sf3RzXq6iVo" | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 "id": 4, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user