Merge branch 'master' into new-blocks
| @ -13,6 +13,7 @@ | ||||
|     "@testing-library/react": "^9.5.0", | ||||
|     "@testing-library/user-event": "^7.2.1", | ||||
|     "blockly": "^3.20200625.2", | ||||
|     "file-saver": "^2.0.2", | ||||
|     "prismjs": "^1.20.0", | ||||
|     "react": "^16.13.1", | ||||
|     "react-dom": "^16.13.1", | ||||
|  | ||||
| Before Width: | Height: | Size: 43 B After Width: | Height: | Size: 43 B | 
| Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 569 B | 
| Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B | 
| Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 766 B | 
| Before Width: | Height: | Size: 198 B After Width: | Height: | Size: 198 B | 
| Before Width: | Height: | Size: 1010 B After Width: | Height: | Size: 1010 B | 
| Before Width: | Height: | Size: 771 B After Width: | Height: | Size: 771 B | 
| Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 738 B | 
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB | 
| @ -5,6 +5,7 @@ export const MOVE_BLOCK = 'MOVE_BLOCK'; | ||||
| export const CHANGE_BLOCK = 'CHANGE_BLOCK'; | ||||
| export const DELETE_BLOCK = 'DELETE_BLOCK'; | ||||
| export const CLEAR_STATS = 'CLEAR_STATS'; | ||||
| export const NAME = 'NAME'; | ||||
| 
 | ||||
| 
 | ||||
| export const TUTORIAL_SUCCESS = 'TUTORIAL_SUCCESS'; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { NEW_CODE, CHANGE_WORKSPACE, CREATE_BLOCK, MOVE_BLOCK, CHANGE_BLOCK, DELETE_BLOCK, CLEAR_STATS } from './types'; | ||||
| import { NEW_CODE, CHANGE_WORKSPACE, CREATE_BLOCK, MOVE_BLOCK, CHANGE_BLOCK, DELETE_BLOCK, CLEAR_STATS, NAME } from './types'; | ||||
| 
 | ||||
| import * as Blockly from 'blockly/core'; | ||||
| 
 | ||||
| @ -72,3 +72,10 @@ export const clearStats = () => (dispatch) => { | ||||
|     payload: stats | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export const workspaceName = (name) => (dispatch) => { | ||||
|   dispatch({ | ||||
|     type: NAME, | ||||
|     payload: name | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @ -63,8 +63,8 @@ class BlocklyWindow extends Component { | ||||
|             length: 1, | ||||
|             colour: '#4EAF47', // senseBox-green
 | ||||
|             snap: false | ||||
|           }} | ||||
|         media={'/media/'} | ||||
|         }} | ||||
|         media={'/media/blockly/'} | ||||
|         move={this.props.move !== undefined && !this.props.move ? {} : | ||||
|           { // https://developers.google.com/blockly/guides/configure/web/move
 | ||||
|             scrollbars: true, | ||||
|  | ||||
| @ -1,27 +1,49 @@ | ||||
| import React, { Component } from 'react'; | ||||
| 
 | ||||
| import { Link } from 'react-router-dom'; | ||||
| import clsx from 'clsx'; | ||||
| 
 | ||||
| import Breadcrumbs from '@material-ui/core/Breadcrumbs'; | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import MaterialUIBreadcrumbs from '@material-ui/core/Breadcrumbs'; | ||||
| import Typography from '@material-ui/core/Typography'; | ||||
| 
 | ||||
| class MyBreadcrumbs extends Component { | ||||
| import { faHome } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| 
 | ||||
| const styles = (theme) => ({ | ||||
|   home: { | ||||
|     color: theme.palette.secondary.main, | ||||
|     width: '20px !important', | ||||
|     height: '20px', | ||||
|     marginTop: '2px' | ||||
|   }, | ||||
|   hover: { | ||||
|     '&:hover': { | ||||
|       color: theme.palette.primary.main | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| class Breadcrumbs extends Component { | ||||
|   render() { | ||||
|     return ( | ||||
|       this.props.content && this.props.content.length > 1 ? | ||||
|         <Breadcrumbs separator="›" style={{marginBottom: '20px'}}> | ||||
|       this.props.content && this.props.content.length > 0 ? | ||||
|         <MaterialUIBreadcrumbs separator="›" style={{marginBottom: '20px'}}> | ||||
|           <Link to={'/'} style={{textDecoration: 'none'}}> | ||||
|             <FontAwesomeIcon className={clsx(this.props.classes.home, this.props.classes.hover)} icon={faHome} size="xs"/> | ||||
|           </Link> | ||||
|           {this.props.content.splice(0, this.props.content.length-1).map((content, i) => ( | ||||
|             <Link to={content.link} style={{textDecoration: 'none'}} key={i}> | ||||
|               <Typography color="secondary">{content.title}</Typography> | ||||
|               <Typography className={this.props.classes.hover} color="secondary">{content.title}</Typography> | ||||
|             </Link> | ||||
|           ))} | ||||
|           <Typography color="textPrimary" style={{overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '300px'}}> | ||||
|             {this.props.content.slice(-1)[0].title} | ||||
|           </Typography> | ||||
|         </Breadcrumbs> | ||||
|         </MaterialUIBreadcrumbs> | ||||
|       : null | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export default MyBreadcrumbs; | ||||
| export default withStyles(styles, {withTheme: true})(Breadcrumbs); | ||||
|  | ||||
| @ -1,46 +0,0 @@ | ||||
| import React, {Component} from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { clearStats, onChangeCode } from '../actions/workspaceActions'; | ||||
| import { initialXml } from './Blockly/initialXml.js'; | ||||
| 
 | ||||
| import * as Blockly from 'blockly/core'; | ||||
| 
 | ||||
| import ListItem from '@material-ui/core/ListItem'; | ||||
| import ListItemIcon from '@material-ui/core/ListItemIcon'; | ||||
| import ListItemText from '@material-ui/core/ListItemText'; | ||||
| 
 | ||||
| import { faTrashRestore } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| 
 | ||||
| class ClearWorkspace extends Component { | ||||
| 
 | ||||
|   clearWorkspace = () => { | ||||
|     const workspace = Blockly.getMainWorkspace(); | ||||
|     Blockly.Events.disable(); // https://groups.google.com/forum/#!topic/blockly/m7e3g0TC75Y
 | ||||
|     // if events are disabled, then the workspace will be cleared AND the blocks are not in the trashcan
 | ||||
|     const xmlDom = Blockly.Xml.textToDom(initialXml) | ||||
|     Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom, workspace); | ||||
|     Blockly.Events.enable(); | ||||
|     workspace.options.maxBlocks = Infinity; | ||||
|     this.props.onChangeCode(); | ||||
|     this.props.clearStats(); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return ( | ||||
|       <ListItem button onClick={() => {this.clearWorkspace(); this.props.onClick();}}> | ||||
|         <ListItemIcon><FontAwesomeIcon icon={faTrashRestore} /></ListItemIcon> | ||||
|         <ListItemText primary='Zurücksetzen' /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| ClearWorkspace.propTypes = { | ||||
|   clearStats: PropTypes.func.isRequired, | ||||
|   onChangeCode: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| export default connect(null, { clearStats, onChangeCode })(ClearWorkspace); | ||||
| @ -7,6 +7,7 @@ import "prismjs/themes/prism.css"; | ||||
| import "prismjs/plugins/line-numbers/prism-line-numbers"; | ||||
| import "prismjs/plugins/line-numbers/prism-line-numbers.css"; | ||||
| 
 | ||||
| import withWidth from '@material-ui/core/withWidth'; | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import MuiAccordion from '@material-ui/core/Accordion'; | ||||
| import MuiAccordionSummary from '@material-ui/core/AccordionSummary'; | ||||
| @ -136,4 +137,4 @@ const mapStateToProps = state => ({ | ||||
|   xml: state.workspace.code.xml | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(CodeViewer); | ||||
| export default connect(mapStateToProps, null)(withWidth()(CodeViewer)); | ||||
|  | ||||
| @ -1,6 +1,9 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { workspaceName } from '../actions/workspaceActions'; | ||||
| 
 | ||||
| import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; | ||||
| 
 | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import Button from '@material-ui/core/Button'; | ||||
| @ -10,71 +13,136 @@ import DialogTitle from '@material-ui/core/DialogTitle'; | ||||
| import DialogContent from '@material-ui/core/DialogContent'; | ||||
| import DialogActions from '@material-ui/core/DialogActions'; | ||||
| import Dialog from '@material-ui/core/Dialog'; | ||||
| import IconButton from '@material-ui/core/IconButton'; | ||||
| import Tooltip from '@material-ui/core/Tooltip'; | ||||
| import TextField from '@material-ui/core/TextField'; | ||||
| 
 | ||||
| import { faCogs } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| 
 | ||||
| const styles = (theme) => ({ | ||||
|   backdrop: { | ||||
|     zIndex: theme.zIndex.drawer + 1, | ||||
|     color: '#fff', | ||||
|   }, | ||||
|   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 Compile extends Component { | ||||
| 
 | ||||
|   state = { | ||||
|     progress: false, | ||||
|     open: false | ||||
|   constructor(props){ | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       progress: false, | ||||
|       open: false, | ||||
|       file: false, | ||||
|       title: '', | ||||
|       content: '', | ||||
|       name: props.name | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   componentDidUpdate(props){ | ||||
|     if(props.name !== this.props.name){ | ||||
|       this.setState({name: this.props.name}); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   compile = () => { | ||||
|     this.setState({ progress: true }); | ||||
|     const data = { | ||||
|       "board": process.env.REACT_APP_BOARD, | ||||
|       "sketch": this.props.arduino | ||||
|     }; | ||||
|     this.setState({ progress: true }); | ||||
|     fetch(`${process.env.REACT_APP_COMPILER_URL}/compile`, { | ||||
|       method: "POST", | ||||
|       headers: { 'Content-Type': 'application/json' }, | ||||
|       body: JSON.stringify(data) | ||||
|     }) | ||||
|       .then(response => response.json()) | ||||
|       .then(data => { | ||||
|         console.log(data) | ||||
|         this.download(data.data.id) | ||||
|       }) | ||||
|       .catch(err => { | ||||
|         console.log(err); | ||||
|         this.setState({ progress: false, open: true }); | ||||
|     .then(response => response.json()) | ||||
|     .then(data => { | ||||
|       console.log(data); | ||||
|       this.setState({id: data.data.id}, () => { | ||||
|         this.createFileName(); | ||||
|       }); | ||||
|     }) | ||||
|     .catch(err => { | ||||
|       console.log(err); | ||||
|       this.setState({ progress: false, file: false, open: true, title: 'Fehler', content: 'Etwas ist beim Kompilieren schief gelaufen. Versuche es nochmal.' }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   download = (id) => { | ||||
|     const filename = 'sketch' | ||||
|   download = () => { | ||||
|     const id = this.state.id; | ||||
|     const filename = detectWhitespacesAndReturnReadableResult(this.state.name); | ||||
|     this.toggleDialog(); | ||||
|     this.props.workspaceName(this.state.name); | ||||
|     window.open(`${process.env.REACT_APP_COMPILER_URL}/download?id=${id}&board=${process.env.REACT_APP_BOARD}&filename=${filename}`, '_self'); | ||||
|     this.setState({ progress: false }); | ||||
|   } | ||||
| 
 | ||||
|   toggleDialog = () => { | ||||
|     this.setState({ open: !this.state }); | ||||
|     this.setState({ open: !this.state, progress: false }); | ||||
|   } | ||||
| 
 | ||||
|   createFileName = () => { | ||||
|     if(this.state.name){ | ||||
|       this.download(); | ||||
|     } | ||||
|     else{ | ||||
|       this.setState({ file: true, open: true, title: 'Blöcke kompilieren', content: 'Bitte gib einen Namen für die Bennenung des zu kompilierenden Programms ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setFileName = (e) => { | ||||
|     this.setState({name: e.target.value}); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return ( | ||||
|       <div style={{ display: 'inline' }}> | ||||
|         <Button style={{ float: 'right', color: 'white' }} variant="contained" color="primary" onClick={() => this.compile()}> | ||||
|           Kompilieren | ||||
|         </Button> | ||||
|       <div style={{}}> | ||||
|         {this.props.iconButton ? | ||||
|           <Tooltip title='Blöcke kompilieren' arrow style={{marginRight: '5px'}}> | ||||
|             <IconButton | ||||
|               className={this.props.classes.button} | ||||
|               onClick={() => this.compile()} | ||||
|             > | ||||
|               <FontAwesomeIcon icon={faCogs} size="xs"/> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|          : | ||||
|           <Button style={{ float: 'right', color: 'white' }} variant="contained" color="primary" onClick={() => this.compile()}> | ||||
|             <FontAwesomeIcon icon={faCogs} style={{marginRight: '5px'}}/> Kompilieren | ||||
|           </Button> | ||||
|         } | ||||
|         <Backdrop className={this.props.classes.backdrop} open={this.state.progress}> | ||||
|           <CircularProgress color="inherit" /> | ||||
|         </Backdrop> | ||||
|         <Dialog onClose={this.toggleDialog} open={this.state.open}> | ||||
|           <DialogTitle>Fehler</DialogTitle> | ||||
|           <DialogTitle>{this.state.title}</DialogTitle> | ||||
|           <DialogContent dividers> | ||||
|             Etwas ist beim Kompilieren schief gelaufen. Versuche es nochmal. | ||||
|             {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.toggleDialog} color="primary"> | ||||
|               Schließen | ||||
|             <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> | ||||
| @ -84,11 +152,15 @@ class Compile extends Component { | ||||
| } | ||||
| 
 | ||||
| Compile.propTypes = { | ||||
|   arduino: PropTypes.string.isRequired | ||||
|   arduino: PropTypes.string.isRequired, | ||||
|   name: PropTypes.string, | ||||
|   workspaceName: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   arduino: state.workspace.code.arduino | ||||
|   arduino: state.workspace.code.arduino, | ||||
|   name: state.workspace.name | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(Compile)); | ||||
| 
 | ||||
| export default connect(mapStateToProps, { workspaceName })(withStyles(styles, {withTheme: true})(Compile)); | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { clearStats } from '../actions/workspaceActions'; | ||||
| import { clearStats, workspaceName } from '../actions/workspaceActions'; | ||||
| 
 | ||||
| import * as Blockly from 'blockly/core'; | ||||
| 
 | ||||
| @ -58,6 +58,7 @@ class Home extends Component { | ||||
| 
 | ||||
|   componentWillUnmount(){ | ||||
|     this.props.clearStats(); | ||||
|     this.props.workspaceName(null); | ||||
|   } | ||||
| 
 | ||||
|   onChange = () => { | ||||
| @ -72,7 +73,8 @@ class Home extends Component { | ||||
|   render() { | ||||
|     return ( | ||||
|       <div> | ||||
|         <WorkspaceStats /> | ||||
|         <div style={{float: 'right', height: '40px', marginBottom: '20px'}}><WorkspaceFunc /></div> | ||||
|         <div style={{float: 'left', height: '40px', position: 'relative'}}><WorkspaceStats /></div> | ||||
|         <Grid container spacing={2}> | ||||
|           <Grid item xs={12} md={this.state.codeOn ? 6 : 12} style={{ position: 'relative' }}> | ||||
|             <Tooltip title={this.state.codeOn ? 'Code ausblenden' : 'Code anzeigen'} > | ||||
| @ -93,15 +95,15 @@ class Home extends Component { | ||||
|             </Grid> | ||||
|             : null} | ||||
|         </Grid> | ||||
|         <WorkspaceFunc /> | ||||
|       </div> | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| Home.propTypes = { | ||||
|   clearStats: PropTypes.func.isRequired | ||||
|   clearStats: PropTypes.func.isRequired, | ||||
|   workspaceName: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| export default connect(null, { clearStats })(withStyles(styles, { withTheme: true })(Home)); | ||||
| export default connect(null, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home)); | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| 
 | ||||
| import ClearWorkspace from './ClearWorkspace'; | ||||
| import senseboxLogo from './sensebox_logo.svg'; | ||||
| 
 | ||||
| import { withRouter } from 'react-router-dom'; | ||||
| @ -105,7 +104,6 @@ class Navbar extends Component { | ||||
|                 </ListItem> | ||||
|               </Link> | ||||
|             ))} | ||||
|             <ClearWorkspace onClick={this.toggleDrawer}/> | ||||
|           </List> | ||||
|           <Divider classes={{root: this.props.classes.appBarColor}} style={{marginTop: 'auto'}}/> | ||||
|           <List> | ||||
|  | ||||
| @ -11,7 +11,7 @@ class NotFound extends Component { | ||||
|   render() { | ||||
|     return ( | ||||
|       <div> | ||||
|         <Breadcrumbs content={[{link: '/', title: 'Home'}, {link: this.props.location.pathname, title: 'Error'}]}/> | ||||
|         <Breadcrumbs content={[{link: this.props.location.pathname, title: 'Error'}]}/> | ||||
|         <Typography variant='h4' style={{marginBottom: '5px'}}>Die von Ihnen angeforderte Seite kann nicht gefunden werden.</Typography> | ||||
|         <Typography variant='body1'>Die gesuchte Seite wurde möglicherweise entfernt, ihr Name wurde geändert oder sie ist vorübergehend nicht verfügbar.</Typography> | ||||
|         {this.props.button ? | ||||
|  | ||||
| @ -1,17 +1,32 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 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'; | ||||
| 
 | ||||
| import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; | ||||
| import Grid from '@material-ui/core/Grid'; | ||||
| import Card from '@material-ui/core/Card'; | ||||
| import Typography from '@material-ui/core/Typography'; | ||||
| 
 | ||||
| class Assessment extends Component { | ||||
| 
 | ||||
|   componentDidMount(){ | ||||
|     // alert(this.props.name);
 | ||||
|     this.props.workspaceName(this.props.name); | ||||
|   } | ||||
| 
 | ||||
|   componentDidUpdate(props){ | ||||
|     if(props.name !== this.props.name){ | ||||
|       // alert(this.props.name);
 | ||||
|       this.props.workspaceName(this.props.name); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     var tutorialId = this.props.currentTutorialId; | ||||
|     var currentTask = this.props.step; | ||||
| @ -21,18 +36,18 @@ class Assessment extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <div style={{width: '100%'}}> | ||||
|         <Typography variant='h4' style={{marginBottom: '5px'}}>{currentTask.headline}</Typography> | ||||
|         <Typography variant='h4' style={{float: 'left', marginBottom: '5px', height: '40px', display: 'table'}}>{currentTask.headline}</Typography> | ||||
|         <div style={{float: 'right', height: '40px'}}><WorkspaceFunc solutionCheck/></div> | ||||
|         <Grid container spacing={2} style={{marginBottom: '5px'}}> | ||||
|           <Grid item xs={12} md={6} lg={8} style={{ position: 'relative' }}> | ||||
|             <SolutionCheck /> | ||||
|           <Grid item xs={12} md={6} lg={8}> | ||||
|             <BlocklyWindow initialXml={statusTask ? statusTask.xml ? statusTask.xml : null : null}/> | ||||
|           </Grid> | ||||
|           <Grid item xs={12} md={6} lg={4}> | ||||
|           <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> | ||||
|             </Card> | ||||
|             <div style={{height: '50%'}}> | ||||
|             <div style={isWidthDown('sm', this.props.width) ? {height: '500px'} : {height: '50%'}}> | ||||
|               <CodeViewer /> | ||||
|             </div> | ||||
|           </Grid> | ||||
| @ -45,7 +60,8 @@ class Assessment extends Component { | ||||
| Assessment.propTypes = { | ||||
|   currentTutorialId: PropTypes.number, | ||||
|   status: PropTypes.array.isRequired, | ||||
|   change: PropTypes.number.isRequired | ||||
|   change: PropTypes.number.isRequired, | ||||
|   workspaceName: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
| @ -54,4 +70,4 @@ const mapStateToProps = state => ({ | ||||
|   currentTutorialId: state.tutorial.currentId | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(Assessment); | ||||
| export default connect(mapStateToProps, { workspaceName })(withWidth()(Assessment)); | ||||
|  | ||||
| @ -24,15 +24,15 @@ const styles = theme => ({ | ||||
|     color: fade(theme.palette.secondary.main, 0.6) | ||||
|   }, | ||||
|   outerDivError: { | ||||
|     stroke: fade(theme.palette.error.dark, 0.2), | ||||
|     color: fade(theme.palette.error.dark, 0.2) | ||||
|     stroke: fade(theme.palette.error.dark, 0.6), | ||||
|     color: fade(theme.palette.error.dark, 0.6) | ||||
|   }, | ||||
|   outerDivSuccess: { | ||||
|     stroke: fade(theme.palette.primary.main, 0.2), | ||||
|     color: fade(theme.palette.primary.main, 0.2) | ||||
|     stroke: fade(theme.palette.primary.main, 0.6), | ||||
|     color: fade(theme.palette.primary.main, 0.6) | ||||
|   }, | ||||
|   outerDivOther: { | ||||
|     stroke: fade(theme.palette.secondary.main, 0.2) | ||||
|     stroke: fade(theme.palette.secondary.main, 0.6) | ||||
|   }, | ||||
|   innerDiv: { | ||||
|     width: 'inherit', | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { withRouter } from 'react-router-dom'; | ||||
| import Compile from '../Compile'; | ||||
| 
 | ||||
| import tutorials from './tutorials.json'; | ||||
| import { checkXml } from './compareXml'; | ||||
| import { checkXml } from '../../helpers/compareXml'; | ||||
| 
 | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import IconButton from '@material-ui/core/IconButton'; | ||||
| @ -60,10 +60,10 @@ class SolutionCheck extends Component { | ||||
|     const steps = tutorials.filter(tutorial => tutorial.id === this.props.currentTutorialId)[0].steps; | ||||
|     return ( | ||||
|       <div> | ||||
|         <Tooltip title='Lösung kontrollieren'> | ||||
|         <Tooltip title='Lösung kontrollieren' arrow> | ||||
|           <IconButton | ||||
|             className={this.props.classes.compile} | ||||
|             style={{width: '40px', height: '40px', position: 'absolute', top: 8, right: 8, zIndex: 21 }} | ||||
|             style={{width: '40px', height: '40px', marginRight: '5px'}} | ||||
|             onClick={() => this.check()} | ||||
|           > | ||||
|             <FontAwesomeIcon icon={faPlay} size="xs"/> | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { workspaceName } from '../../actions/workspaceActions'; | ||||
| import { tutorialId, tutorialStep } from '../../actions/tutorialActions'; | ||||
| 
 | ||||
| import Breadcrumbs from '../Breadcrumbs'; | ||||
| @ -10,6 +11,8 @@ import Instruction from './Instruction'; | ||||
| import Assessment from './Assessment'; | ||||
| import NotFound from '../NotFound'; | ||||
| 
 | ||||
| import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; | ||||
| 
 | ||||
| import tutorials from './tutorials.json'; | ||||
| 
 | ||||
| import Card from '@material-ui/core/Card'; | ||||
| @ -29,6 +32,7 @@ class Tutorial extends Component { | ||||
| 
 | ||||
|   componentWillUnmount(){ | ||||
|     this.props.tutorialId(null); | ||||
|     this.props.workspaceName(null); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
| @ -36,12 +40,13 @@ class Tutorial extends Component { | ||||
|     var tutorial = tutorials.filter(tutorial => tutorial.id === currentTutorialId)[0]; | ||||
|     var steps = tutorial ? tutorial.steps : null; | ||||
|     var step = steps ? steps[this.props.activeStep] : null; | ||||
|     var name = step ? `${detectWhitespacesAndReturnReadableResult(tutorial.title)}_${detectWhitespacesAndReturnReadableResult(step.headline)}` : null; | ||||
|     return ( | ||||
|       !Number.isInteger(currentTutorialId) || currentTutorialId < 1 || currentTutorialId > tutorials.length ? | ||||
|         <NotFound button={{title: 'Zurück zur Tutorials-Übersicht', link: '/tutorial'}}/> | ||||
|       : | ||||
|       <div> | ||||
|         <Breadcrumbs content={[{link: '/', title: 'Home'},{link: '/tutorial', title: 'Tutorial'}, {link: `/tutorial/${currentTutorialId}`, title: tutorial.title}]}/> | ||||
|         <Breadcrumbs content={[{link: '/tutorial', title: 'Tutorial'}, {link: `/tutorial/${currentTutorialId}`, title: tutorial.title}]}/> | ||||
| 
 | ||||
|         <StepperHorizontal /> | ||||
| 
 | ||||
| @ -52,7 +57,7 @@ class Tutorial extends Component { | ||||
|             {step ? | ||||
|               step.type === 'instruction' ? | ||||
|                 <Instruction step={step}/> | ||||
|               : <Assessment step={step}/> // if step.type === 'assessment'
 | ||||
|               : <Assessment step={step} name={name}/> // if step.type === 'assessment'
 | ||||
|              : null} | ||||
| 
 | ||||
|             <div style={{marginTop: '20px', position: 'absolute', bottom: '10px'}}> | ||||
| @ -69,6 +74,7 @@ class Tutorial extends Component { | ||||
| Tutorial.propTypes = { | ||||
|   tutorialId: PropTypes.func.isRequired, | ||||
|   tutorialStep: PropTypes.func.isRequired, | ||||
|   workspaceName: PropTypes.func.isRequired, | ||||
|   currentTutorialId: PropTypes.number, | ||||
|   status: PropTypes.array.isRequired, | ||||
|   change: PropTypes.number.isRequired, | ||||
| @ -82,4 +88,4 @@ const mapStateToProps = state => ({ | ||||
|   activeStep: state.tutorial.activeStep | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, { tutorialId, tutorialStep })(Tutorial); | ||||
| export default connect(mapStateToProps, { tutorialId, tutorialStep, workspaceName })(Tutorial); | ||||
|  | ||||
| @ -29,15 +29,15 @@ const styles = (theme) => ({ | ||||
|     color: fade(theme.palette.secondary.main, 0.6) | ||||
|   }, | ||||
|   outerDivError: { | ||||
|     stroke: fade(theme.palette.error.dark, 0.2), | ||||
|     color: fade(theme.palette.error.dark, 0.2) | ||||
|     stroke: fade(theme.palette.error.dark, 0.6), | ||||
|     color: fade(theme.palette.error.dark, 0.6) | ||||
|   }, | ||||
|   outerDivSuccess: { | ||||
|     stroke: fade(theme.palette.primary.main, 0.2), | ||||
|     color: fade(theme.palette.primary.main, 0.2) | ||||
|     stroke: fade(theme.palette.primary.main, 0.6), | ||||
|     color: fade(theme.palette.primary.main, 0.6) | ||||
|   }, | ||||
|   outerDivOther: { | ||||
|     stroke: fade(theme.palette.secondary.main, 0.2) | ||||
|     stroke: fade(theme.palette.secondary.main, 0.6) | ||||
|   }, | ||||
|   innerDiv: { | ||||
|     width: 'inherit', | ||||
| @ -54,7 +54,7 @@ class TutorialHome extends Component { | ||||
|   render() { | ||||
|     return ( | ||||
|       <div> | ||||
|         <Breadcrumbs content={[{link: '/', title: 'Home'},{link: '/tutorial', title: 'Tutorial'}]}/> | ||||
|         <Breadcrumbs content={[{link: '/tutorial', title: 'Tutorial'}]}/> | ||||
| 
 | ||||
|         <h1>Tutorial-Übersicht</h1> | ||||
|         <Grid container spacing={2}> | ||||
|  | ||||
| @ -1,58 +1,217 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions'; | ||||
| 
 | ||||
| import * as Blockly from 'blockly/core'; | ||||
| 
 | ||||
| import { saveAs } from 'file-saver'; | ||||
| 
 | ||||
| import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; | ||||
| import { initialXml } from './Blockly/initialXml.js'; | ||||
| 
 | ||||
| import MaxBlocks from './MaxBlocks'; | ||||
| import Compile from './Compile'; | ||||
| import SolutionCheck from './Tutorial/SolutionCheck'; | ||||
| 
 | ||||
| import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import Button from '@material-ui/core/Button'; | ||||
| import DialogTitle from '@material-ui/core/DialogTitle'; | ||||
| import DialogContent from '@material-ui/core/DialogContent'; | ||||
| import DialogActions from '@material-ui/core/DialogActions'; | ||||
| import Dialog from '@material-ui/core/Dialog'; | ||||
| import IconButton from '@material-ui/core/IconButton'; | ||||
| import Tooltip from '@material-ui/core/Tooltip'; | ||||
| import TextField from '@material-ui/core/TextField'; | ||||
| import Typography from '@material-ui/core/Typography'; | ||||
| 
 | ||||
| import { faPen, faSave, faUpload, faShare } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { 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, | ||||
|     } | ||||
|   }, | ||||
|   workspaceName: { | ||||
|     backgroundColor: theme.palette.secondary.main, | ||||
|     borderRadius: '25px', | ||||
|     display: 'inline-flex', | ||||
|     cursor: 'pointer', | ||||
|     '&:hover': { | ||||
|       color: theme.palette.primary.main, | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| class WorkspaceFunc extends Component { | ||||
| 
 | ||||
|   state = { | ||||
|     title: '', | ||||
|     content: '', | ||||
|     open: false | ||||
|   constructor(props){ | ||||
|     super(props); | ||||
|     this.inputRef = React.createRef(); | ||||
|     this.state = { | ||||
|       title: '', | ||||
|       content: '', | ||||
|       open: false, | ||||
|       file: false, | ||||
|       saveXml: false, | ||||
|       name: props.name | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getArduinoCode = () => { | ||||
|     this.setState({ title: 'Adurino Code', content: this.props.arduino, open: true }); | ||||
|   } | ||||
| 
 | ||||
|   getXMLCode = () => { | ||||
|     this.setState({ title: 'XML Code', content: this.props.xml, open: true }); | ||||
|   componentDidUpdate(props){ | ||||
|     if(props.name !== this.props.name){ | ||||
|       this.setState({name: this.props.name}); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   toggleDialog = () => { | ||||
|     this.setState({ open: !this.state }); | ||||
|   } | ||||
| 
 | ||||
|   saveXmlFile = () => { | ||||
|     var code = this.props.xml; | ||||
|     this.toggleDialog(); | ||||
|     var fileName = detectWhitespacesAndReturnReadableResult(this.state.name); | ||||
|     this.props.workspaceName(this.state.name); | ||||
|     fileName = `${fileName}.xml` | ||||
|     var blob = new Blob([code], { type: 'text/xml' }); | ||||
|     saveAs(blob, fileName); | ||||
|   } | ||||
| 
 | ||||
|   createFileName = () => { | ||||
|     if(this.state.name){ | ||||
|       this.saveXmlFile(); | ||||
|     } | ||||
|     else{ | ||||
|       this.setState({ file: true, saveXml: true, open: true, title: 'Blöcke speichern', content: 'Bitte gib einen Namen für die Bennenung der XML-Datei ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setFileName = (e) => { | ||||
|     this.setState({name: e.target.value}); | ||||
|   } | ||||
| 
 | ||||
|   uploadXmlFile = (xmlFile) => { | ||||
|     if(xmlFile.type !== 'text/xml'){ | ||||
|       this.setState({ open: true, file: false, title: 'Unzulässiger Dateityp', content: 'Die übergebene Datei entsprach nicht dem geforderten Format. Es sind nur XML-Dateien zulässig.' }); | ||||
|     } | ||||
|     else { | ||||
|       var reader = new FileReader(); | ||||
|       reader.readAsText(xmlFile); | ||||
|       reader.onloadend = () => { | ||||
|         var xmlDom = null; | ||||
|         try { | ||||
|           xmlDom = Blockly.Xml.textToDom(reader.result); | ||||
|           const workspace = Blockly.getMainWorkspace(); | ||||
|           var xmlBefore = this.props.xml; | ||||
|           workspace.clear(); | ||||
|           this.props.clearStats(); | ||||
|           Blockly.Xml.domToWorkspace(xmlDom, workspace); | ||||
|           if(workspace.getAllBlocks().length < 1){ | ||||
|             Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xmlBefore), workspace) | ||||
|             this.setState({ open: true, file: false, title: 'Keine Blöcke', content: 'Es wurden keine Blöcke detektiert. Bitte überprüfe den XML-Code und versuche es erneut.' }); | ||||
|           } | ||||
|           else { | ||||
|             if(!this.props.solutionCheck){ | ||||
|               var extensionPosition = xmlFile.name.lastIndexOf('.'); | ||||
|               this.props.workspaceName(xmlFile.name.substr(0, extensionPosition)); | ||||
|             } | ||||
|           } | ||||
|         } catch(err){ | ||||
|           this.setState({ open: true, file: false, title: 'Ungültige XML', content: 'Die XML-Datei konnte nicht in Blöcke zerlegt werden. Bitte überprüfe den XML-Code und versuche es erneut.' }); | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   resetWorkspace = () => { | ||||
|     const workspace = Blockly.getMainWorkspace(); | ||||
|     Blockly.Events.disable(); // https://groups.google.com/forum/#!topic/blockly/m7e3g0TC75Y
 | ||||
|     // if events are disabled, then the workspace will be cleared AND the blocks are not in the trashcan
 | ||||
|     const xmlDom = Blockly.Xml.textToDom(initialXml) | ||||
|     Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom, workspace); | ||||
|     Blockly.Events.enable(); | ||||
|     workspace.options.maxBlocks = Infinity; | ||||
|     this.props.onChangeCode(); | ||||
|     this.props.clearStats(); | ||||
|     if(!this.props.solutionCheck){ | ||||
|       this.props.workspaceName(null); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return ( | ||||
|       <div style={{ marginTop: '20px' }}> | ||||
|       <div style={{width: 'max-content', display: 'flex'}}> | ||||
|         {!this.props.solutionCheck ? | ||||
|           <Tooltip title={`Name des Projekts${this.props.name ? `: ${this.props.name}` : ''}`} arrow style={{marginRight: '5px'}}> | ||||
|           <div className={this.props.classes.workspaceName} onClick={() => {this.setState({file: true, open: true, saveXml: false, title: 'Projekt benennen', content: 'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.'})}}> | ||||
|             {this.props.name && !isWidthDown('xs', this.props.width) ? <Typography style={{margin: 'auto -3px auto 12px'}}>{this.props.name}</Typography> : null} | ||||
|             <div style={{width: '40px', display: 'flex'}}> | ||||
|               <FontAwesomeIcon icon={faPen} style={{height: '18px', width: '18px', margin: 'auto'}}/> | ||||
|             </div> | ||||
|           </div> | ||||
|           </Tooltip> | ||||
|         : null} | ||||
|         {this.props.solutionCheck ? <SolutionCheck /> : <Compile iconButton />} | ||||
|         <Tooltip title='Blöcke speichern' arrow style={{marginRight: '5px'}}> | ||||
|           <IconButton | ||||
|             className={this.props.classes.button} | ||||
|             onClick={() => this.createFileName()} | ||||
|           > | ||||
|             <FontAwesomeIcon icon={faSave} size="xs"/> | ||||
|           </IconButton> | ||||
|         </Tooltip> | ||||
|         <div ref={this.inputRef} style={{width: 'max-content', height: '40px', marginRight: '5px'}}> | ||||
|           <input | ||||
|             style={{display: 'none'}} | ||||
|             accept="text/xml" | ||||
|             onChange={(e) => {this.uploadXmlFile(e.target.files[0])}} | ||||
|             id="open-blocks" | ||||
|             type="file" | ||||
|           /> | ||||
|           <label htmlFor="open-blocks"> | ||||
|             <Tooltip title='Blöcke öffnen' arrow style={{marginRight: '5px'}}> | ||||
|               <div className={this.props.classes.button} style={{borderRadius: '50%', cursor: 'pointer', display: 'table-cell', | ||||
|               verticalAlign: 'middle', | ||||
|               textAlign: 'center'}}> | ||||
|                 <FontAwesomeIcon icon={faUpload} style={{width: '18px', height: '18px'}}/> | ||||
|               </div> | ||||
|             </Tooltip> | ||||
|           </label> | ||||
|         </div> | ||||
|         <Tooltip title='Workspace zurücksetzen' arrow> | ||||
|           <IconButton | ||||
|             className={this.props.classes.button} | ||||
|             onClick={() => this.resetWorkspace()} | ||||
|           > | ||||
|             <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.toggleDialog} color="primary"> | ||||
|               Schließen | ||||
|             <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> | ||||
|         <Button style={{ marginRight: '10px', color: 'white' }} variant="contained" color="primary" onClick={() => this.getArduinoCode()}> | ||||
|           Get Adurino Code | ||||
|         </Button> | ||||
|         <Button style={{ marginRight: '10px', color: 'white' }} variant="contained" color="primary" onClick={() => this.getXMLCode()}> | ||||
|           Get XML Code | ||||
|         </Button> | ||||
|         <MaxBlocks /> | ||||
|         <Compile /> | ||||
|       </div> | ||||
|     ); | ||||
|   }; | ||||
| @ -60,12 +219,17 @@ class WorkspaceFunc extends Component { | ||||
| 
 | ||||
| WorkspaceFunc.propTypes = { | ||||
|   arduino: PropTypes.string.isRequired, | ||||
|   xml: PropTypes.string.isRequired | ||||
|   xml: PropTypes.string.isRequired, | ||||
|   name: PropTypes.string, | ||||
|   clearStats: PropTypes.func.isRequired, | ||||
|   onChangeCode: PropTypes.func.isRequired, | ||||
|   workspaceName: PropTypes.func.isRequired | ||||
| }; | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   arduino: state.workspace.code.arduino, | ||||
|   xml: state.workspace.code.xml | ||||
|   xml: state.workspace.code.xml, | ||||
|   name: state.workspace.name | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(WorkspaceFunc); | ||||
| export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName })(withStyles(styles, {withTheme: true})(withWidth()(WorkspaceFunc))); | ||||
|  | ||||
| @ -4,12 +4,15 @@ import { connect } from 'react-redux'; | ||||
| 
 | ||||
| import * as Blockly from 'blockly/core'; | ||||
| 
 | ||||
| import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import Tooltip from '@material-ui/core/Tooltip'; | ||||
| import IconButton from '@material-ui/core/IconButton'; | ||||
| import Chip from '@material-ui/core/Chip'; | ||||
| import Avatar from '@material-ui/core/Avatar'; | ||||
| import Popover from '@material-ui/core/Popover'; | ||||
| 
 | ||||
| import { faPuzzlePiece, faTrash, faPlus, faPen, faArrowsAlt } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { faPuzzlePiece, faTrash, faPlus, faPen, faArrowsAlt, faEllipsisH } from "@fortawesome/free-solid-svg-icons"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| 
 | ||||
| const styles = (theme) => ({ | ||||
| @ -19,65 +22,123 @@ const styles = (theme) => ({ | ||||
|     marginLeft: '50px', | ||||
|     padding: '3px 10px', | ||||
|     // borderRadius: '25%'
 | ||||
|   }, | ||||
|   menu: { | ||||
|     backgroundColor: theme.palette.secondary.main, | ||||
|     color: theme.palette.secondary.contrastText, | ||||
|     width: '40px', | ||||
|     height: '40px', | ||||
|     '&:hover': { | ||||
|       backgroundColor: theme.palette.secondary.main, | ||||
|       color: theme.palette.primary.main, | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| class WorkspaceStats extends Component { | ||||
| 
 | ||||
|   state={ | ||||
|     anchor: null | ||||
|   } | ||||
| 
 | ||||
|   handleClose = () => { | ||||
|     this.setState({ anchor: null }); | ||||
|   } | ||||
| 
 | ||||
|   handleClick = (event) => { | ||||
|     this.setState({ anchor: event.currentTarget }); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const bigDisplay = !isWidthDown('sm', this.props.width); | ||||
|     const workspace = Blockly.getMainWorkspace(); | ||||
|     const remainingBlocksInfinity = workspace ? workspace.remainingCapacity() !== Infinity : null; | ||||
|     const stats =  <div style={bigDisplay ? {display: 'flex'} : {display: 'inline'}}> | ||||
|                     <Tooltip title="Anzahl aktueller Blöcke" arrow> | ||||
|                       <Chip | ||||
|                         style={bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'}} | ||||
|                         color="primary" | ||||
|                         avatar={<Avatar><FontAwesomeIcon icon={faPuzzlePiece} /></Avatar>} | ||||
|                         label={workspace ? workspace.getAllBlocks().length : 0}> | ||||
|                       </Chip> | ||||
|                     </Tooltip> | ||||
|                     <Tooltip title="Anzahl neuer Blöcke" arrow> | ||||
|                       <Chip | ||||
|                         style={bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'}} | ||||
|                         color="primary" | ||||
|                         avatar={<Avatar><FontAwesomeIcon icon={faPlus} /></Avatar>} | ||||
|                         label={this.props.create > 0 ? this.props.create : 0}> {/* initialXML is created automatically, Block is not part of the statistics */} | ||||
|                       </Chip> | ||||
|                     </Tooltip> | ||||
|                     <Tooltip title="Anzahl veränderter Blöcke" arrow> | ||||
|                       <Chip | ||||
|                         style={bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'}} | ||||
|                         color="primary" | ||||
|                         avatar={<Avatar><FontAwesomeIcon icon={faPen} /></Avatar>} | ||||
|                         label={this.props.change}> | ||||
|                       </Chip> | ||||
|                     </Tooltip> | ||||
|                     <Tooltip title="Anzahl bewegter Blöcke" arrow> | ||||
|                       <Chip | ||||
|                         style={bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'}} | ||||
|                         color="primary" | ||||
|                         avatar={<Avatar><FontAwesomeIcon icon={faArrowsAlt} /></Avatar>} | ||||
|                         label={this.props.move > 0 ? this.props.move : 0}> {/* initialXML is moved automatically, Block is not part of the statistics */} | ||||
|                       </Chip> | ||||
|                     </Tooltip> | ||||
|                     <Tooltip title="Anzahl gelöschter Blöcke" arrow> | ||||
|                       <Chip | ||||
|                         style={remainingBlocksInfinity ? bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'} : {}} | ||||
|                         color="primary" | ||||
|                         avatar={<Avatar><FontAwesomeIcon icon={faTrash} /></Avatar>} | ||||
|                         label={this.props.delete}> | ||||
|                       </Chip> | ||||
|                     </Tooltip> | ||||
|                     {remainingBlocksInfinity ? | ||||
|                       <Tooltip title="Verbleibende Blöcke" arrow> | ||||
|                         <Chip | ||||
|                           style={bigDisplay ? {marginRight: '1rem'} : {marginRight: '1rem', marginBottom: '5px'}} | ||||
|                           color="primary" | ||||
|                           label={workspace.remainingCapacity()}> | ||||
|                         </Chip> | ||||
|                       </Tooltip> : null} | ||||
|                     </div> | ||||
|     return ( | ||||
|       <div style={{ marginBottom: '20px' }}> | ||||
|         <Tooltip title="Anzahl aktueller Blöcke" > | ||||
|           <Chip | ||||
|             style={{ marginRight: '1rem' }} | ||||
|             color="primary" | ||||
|             avatar={<Avatar><FontAwesomeIcon icon={faPuzzlePiece} /></Avatar>} | ||||
|             label={workspace ? workspace.getAllBlocks().length : 0}> | ||||
|           </Chip> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Anzahl neuer Blöcke" > | ||||
|           <Chip | ||||
|             style={{ marginRight: '1rem' }} | ||||
|             color="primary" | ||||
|             avatar={<Avatar><FontAwesomeIcon icon={faPlus} /></Avatar>} | ||||
|             label={this.props.create > 0 ? this.props.create : 0}> {/* initialXML is created automatically, Block is not part of the statistics */} | ||||
|           </Chip> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Anzahl veränderter Blöcke" > | ||||
|           <Chip | ||||
|             style={{ marginRight: '1rem' }} | ||||
|             color="primary" | ||||
|             avatar={<Avatar><FontAwesomeIcon icon={faPen} /></Avatar>} | ||||
|             label={this.props.change}> | ||||
|           </Chip> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Anzahl bewegter Blöcke" > | ||||
|           <Chip | ||||
|             style={{ marginRight: '1rem' }} | ||||
|             color="primary" | ||||
|             avatar={<Avatar><FontAwesomeIcon icon={faArrowsAlt} /></Avatar>} | ||||
|             label={this.props.move > 0 ? this.props.move : 0}> {/* initialXML is moved automatically, Block is not part of the statistics */} | ||||
|           </Chip> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Anzahl gelöschter Blöcke" > | ||||
|           <Chip | ||||
|             style={{ marginRight: '1rem' }} | ||||
|             color="primary" | ||||
|             avatar={<Avatar><FontAwesomeIcon icon={faTrash} /></Avatar>} | ||||
|             label={this.props.delete}> | ||||
|           </Chip> | ||||
|         </Tooltip> | ||||
|         {remainingBlocksInfinity ? | ||||
|           <Tooltip title="Verbleibende Blöcke" > | ||||
|             <Chip | ||||
|               style={{ marginRight: '1rem' }} | ||||
|               color="primary" | ||||
|               label={workspace.remainingCapacity()}> | ||||
|             </Chip> | ||||
|           </Tooltip> : null} | ||||
|       </div> | ||||
|       bigDisplay ? | ||||
|         <div style={{bottom: 0, position: 'absolute'}}> | ||||
|           {stats} | ||||
|         </div> | ||||
|       : | ||||
|         <div> | ||||
|           <Tooltip title='Statistiken anzeigen' arrow> | ||||
|             <IconButton | ||||
|               className={this.props.classes.menu} | ||||
|               onClick={(event) => this.handleClick(event)} | ||||
|             > | ||||
|               <FontAwesomeIcon icon={faEllipsisH} size="xs"/> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|           <Popover | ||||
|             open={Boolean(this.state.anchor)} | ||||
|             anchorEl={this.state.anchor} | ||||
|             onClose={this.handleClose} | ||||
|             anchorOrigin={{ | ||||
|               vertical: 'bottom', | ||||
|               horizontal: 'center', | ||||
|             }} | ||||
|             transformOrigin={{ | ||||
|               vertical: 'top', | ||||
|               horizontal: 'center', | ||||
|             }} | ||||
|             PaperProps={{ | ||||
|               style: {margin: '5px'} | ||||
|             }} | ||||
|           > | ||||
|             <div style={{margin: '10px'}}> | ||||
|               {stats} | ||||
|             </div> | ||||
|           </Popover> | ||||
|         </div> | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
| @ -98,4 +159,4 @@ const mapStateToProps = state => ({ | ||||
|   workspaceChange: state.workspace.change | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(WorkspaceStats)); | ||||
| export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceStats))); | ||||
|  | ||||
| @ -12,11 +12,14 @@ const parseXml = (xmlString) => { | ||||
| 
 | ||||
| const compareNumberOfBlocks = (originalBlocks, userBlocks) => { | ||||
|   if(originalBlocks.length !== userBlocks.length){ | ||||
|     var blocks; | ||||
|     if(originalBlocks.length > userBlocks.length){ | ||||
|       return {text: 'Es wurden zu wenig Blöcke verwendet.', type: 'error'}; | ||||
|       blocks = originalBlocks.length-userBlocks.length; | ||||
|       return {text: `Es wurde${blocks === 1 ? '' : 'n'} ${blocks} Bl${blocks === 1 ? 'ock' : 'öcke'} zu wenig verwendet.`, type: 'error'}; | ||||
|     } | ||||
|     else { | ||||
|       return {text: 'Es wurden zu viele Blöcke verwendet.', type: 'error'}; | ||||
|       blocks = userBlocks.length-originalBlocks.length; | ||||
|       return {text: `Es wurde${blocks === 1 ? '' : 'n'} ${blocks} Bl${blocks === 1 ? 'ock' : 'öcke'} zu viel verwendet.`, type: 'error'}; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @ -74,5 +77,5 @@ const compareXml = (originalXml, userXml) => { | ||||
|     if(parent){return parent;} | ||||
|   } | ||||
| 
 | ||||
|   return {text: 'Super. Alles richtig!', type: 'success'}; | ||||
|   return {text: 'Super, alles richtig! Kompiliere nun die benutzen Blöcke, um eine BIN-Datei zu erhalten und damit das Programm auf die senseBox zu spielen und ausführen zu können.', type: 'success'}; | ||||
| }; | ||||
							
								
								
									
										15
									
								
								src/helpers/whitespace.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,15 @@ | ||||
| export const detectWhitespacesAndReturnReadableResult = (word) => { | ||||
|   var readableResult = ''; | ||||
|   var space = false; | ||||
|   for(var i = 0; i < word.length; i++){ | ||||
|     var letter = word[i]; | ||||
|     if(/\s/g.test(letter)){ | ||||
|       space = true; | ||||
|     } | ||||
|     else { | ||||
|       readableResult += space ? letter.toUpperCase() : letter; | ||||
|       space = false; | ||||
|     } | ||||
|   } | ||||
|   return readableResult; | ||||
| }; | ||||
| @ -1,4 +1,4 @@ | ||||
| import { CHANGE_WORKSPACE, NEW_CODE, CREATE_BLOCK, MOVE_BLOCK, CHANGE_BLOCK, DELETE_BLOCK, CLEAR_STATS } from '../actions/types'; | ||||
| import { CHANGE_WORKSPACE, NEW_CODE, CREATE_BLOCK, MOVE_BLOCK, CHANGE_BLOCK, DELETE_BLOCK, CLEAR_STATS, NAME } from '../actions/types'; | ||||
| 
 | ||||
| 
 | ||||
| const initialState = { | ||||
| @ -12,7 +12,8 @@ const initialState = { | ||||
|     delete: 0, | ||||
|     move: -1 // initialXML is moved automatically, Block is not part of the statistics
 | ||||
|   }, | ||||
|   change: 0 | ||||
|   change: 0, | ||||
|   name: null | ||||
| }; | ||||
| 
 | ||||
| export default function(state = initialState, action){ | ||||
| @ -36,6 +37,11 @@ export default function(state = initialState, action){ | ||||
|         ...state, | ||||
|         stats: action.payload | ||||
|       }; | ||||
|     case NAME: | ||||
|       return { | ||||
|         ...state, | ||||
|         name: action.payload | ||||
|       } | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  | ||||
| @ -11,7 +11,7 @@ const store = createStore( | ||||
|   initialState, | ||||
|   compose( | ||||
|     applyMiddleware(...middleware), | ||||
|     window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() | ||||
|     // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
|  | ||||