add translations and maxInstances

fixes #126
fixes #125
This commit is contained in:
Mario Pesch 2021-11-23 11:48:02 +01:00
parent 3c62635332
commit 56c39e9a9d
12 changed files with 534 additions and 291 deletions

View File

@ -11,6 +11,7 @@ import "./blocks/index";
import "./generator/index"; import "./generator/index";
import { ZoomToFitControl } from "@blockly/zoom-to-fit"; import { ZoomToFitControl } from "@blockly/zoom-to-fit";
import { initialXml } from "./initialXml.js"; import { initialXml } from "./initialXml.js";
import { getMaxInstances } from "./helpers/maxInstances";
class BlocklyWindow extends Component { class BlocklyWindow extends Component {
constructor(props) { constructor(props) {
@ -24,6 +25,7 @@ class BlocklyWindow extends Component {
this.props.clearStats(); this.props.clearStats();
workspace.addChangeListener((event) => { workspace.addChangeListener((event) => {
this.props.onChangeWorkspace(event); this.props.onChangeWorkspace(event);
// switch on that a block is displayed disabled or not depending on whether it is correctly connected // switch on that a block is displayed disabled or not depending on whether it is correctly connected
// for SVG display, a deactivated block in the display is undesirable // for SVG display, a deactivated block in the display is undesirable
if (this.props.blockDisabled) { if (this.props.blockDisabled) {
@ -37,6 +39,7 @@ class BlocklyWindow extends Component {
componentDidUpdate(props) { componentDidUpdate(props) {
const workspace = Blockly.getMainWorkspace(); const workspace = Blockly.getMainWorkspace();
var xml = this.props.initialXml; var xml = this.props.initialXml;
// if svg is true, then the update process is done in the BlocklySvg component // if svg is true, then the update process is done in the BlocklySvg component
if (props.initialXml !== xml && !this.props.svg) { if (props.initialXml !== xml && !this.props.svg) {
@ -69,6 +72,7 @@ class BlocklyWindow extends Component {
this.props.trashcan !== undefined ? this.props.trashcan : true this.props.trashcan !== undefined ? this.props.trashcan : true
} }
renderer={this.props.renderer} renderer={this.props.renderer}
maxInstances={getMaxInstances()}
zoom={{ zoom={{
// https://developers.google.com/blockly/guides/configure/web/zoom // https://developers.google.com/blockly/guides/configure/web/zoom
controls: controls:

View File

@ -0,0 +1,21 @@
/**
* To limit number of specific blocks in the workspace add block name and number of maxInstances here.
*
*/
const maxInstances = {
sensebox_wifi: 1,
sensebox_startap: 1,
sensebox_display_beginDisplay: 1,
sensebox_telegram: 1,
sensebox_telegram_do: 1,
sensebox_interval_timer: 1,
sensebox_osem_connection: 1,
sensebox_lora_initialize_otaa: 1,
sensebox_lora_initialize_abp: 1,
sensebox_phyphox_init: 1,
};
export const getMaxInstances = () => {
return maxInstances;
};

View File

@ -1,10 +1,10 @@
export const FAQ = { export const FAQ = {
/** /**
* FAQ * FAQ
*/ */
faq_q1_question: `Wie kann ich mein Programm auf die senseBox kopieren?`, faq_q1_question: `Wie kann ich mein Programm auf die senseBox kopieren?`,
faq_q1_answer: `Um Programme auf die senseBox zu kopieren wird diese mit dem Micro USB Kabel an den Computer angeschlossen. Mache anschließend auf der senseBox MCU einen Doppelklick auf den roten Reset Button. Die senseBox wird nun als Wechseldatenträger an deinem Computer erkannt und die zuvor erstellen Programm können per Drag & Drop kopiert werden. Nach jeder Änderung des Programmcodes muss das Programm neu kompiliert und übertragen werden faq_q1_answer: `Um Programme auf die senseBox zu kopieren wird diese mit dem Micro USB Kabel an den Computer angeschlossen. Mache anschließend auf der senseBox MCU einen Doppelklick auf den roten Reset Button. Die senseBox wird nun als Wechseldatenträger an deinem Computer erkannt und die zuvor erstellen Programm können per Drag & Drop kopiert werden. Nach jeder Änderung des Programmcodes muss das Programm neu kompiliert und übertragen werden
#### Lernmodus der MCU aktivieren #### Lernmodus der MCU aktivieren
<iframe width="560" height="315" src="https://www.youtube.com/embed/jzlOJ7Zuqqw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <iframe width="560" height="315" src="https://www.youtube.com/embed/jzlOJ7Zuqqw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
@ -14,14 +14,16 @@ Das Kopieren der Programme unter MacOS funktioniert nicht über den Finder, es g
- [muCommander](https://www.mucommander.com/) - [muCommander](https://www.mucommander.com/)
`, `,
faq_q2_question: `Mit welcher senseBox ist die Programmierumgebung kompatibel?`, faq_q2_question: `Mit welcher senseBox ist die Programmierumgebung kompatibel?`,
faq_q2_answer: ` faq_q2_answer: `
Grundsätzlich kann die Programmierumgebung mit jeder senseBox mit senseBox MCU verwendet werden. Grundsätzlich kann die Programmierumgebung mit jeder senseBox mit senseBox MCU verwendet werden.
`, `,
faq_q3_question: `Ich habe einen Fehler gefunden oder etwas funktioniert nicht. Wo kann ich diesen melden?`,
faq_q3_question: `Ich habe einen Fehler gefunden oder etwas funktioniert nicht. Wo kann ich diesen melden?`, faq_q3_answer: `
faq_q3_answer: `
Am besten legst du dazu ein Issue auf [Github](https://github.com/sensebox/React-Ardublockly/issues) an. Alternativ kannst du uns auch eine Email an info(at)sensebox.de senden Am besten legst du dazu ein Issue auf [Github](https://github.com/sensebox/React-Ardublockly/issues) an. Alternativ kannst du uns auch eine Email an info(at)sensebox.de senden
`, `,
}
faq_tablet_question: `Kann ich die senseBox auch über ein Tablet programmieren?`,
faq_tablet_answer: `Ja! Installiere dazu die senseBox Connect App aus dem App Store und rufe die Learn- und Programmierumgebung in deinem Browser am Tablet auf. Genaue Informationen zur Verwendung der App findest du unter [https://sensebox.de/app](https://sensebox.de/app)`,
};

View File

@ -49,7 +49,14 @@ export const UI = {
tooltip_project_title: "Titel des Projektes", tooltip_project_title: "Titel des Projektes",
tooltip_check_solution: "Lösung kontrollieren", tooltip_check_solution: "Lösung kontrollieren",
tooltip_copy_code: "Code in die Zwischenablage kopieren", tooltip_copy_code: "Code in die Zwischenablage kopieren",
tooltip_statistics_current: "Anzahl aktueller Blöcke",
tooltip_statistics_new: "Anzahl neuer Blöcke",
tooltip_statistics_changed: "Anzahl veränderter Blöcke",
tooltip_statistics_moved: "Anzahl bewegter Blöcke",
tooltip_statistics_deleted: "Anzahl gelöschter Blöcke",
tooltip_statistics_remaining: "Verbleibende Blöcke",
tooltip_statistics_show: "Statistiken anzeigen",
tooltip_start_tour: "Tour starten",
/** /**
* Messages * Messages
* *
@ -175,7 +182,7 @@ export const UI = {
/** /**
* Tutorials * Tutorials
*/ */
tutorials_home_head: "Tutorial-Übersicht",
tutorials_assessment_task: "Aufgabe", tutorials_assessment_task: "Aufgabe",
tutorials_hardware_head: "Für die Umsetzung benötigst du folgende Hardware:", tutorials_hardware_head: "Für die Umsetzung benötigst du folgende Hardware:",
tutorials_hardware_moreInformation: tutorials_hardware_moreInformation:
@ -187,7 +194,9 @@ export const UI = {
/** /**
* Tutorial Builder * Tutorial Builder
*/ */
builder_createNew: "neues Tutorial erstellen",
builder_changeExisting: "bestehendes Tutorial ändern",
builder_deleteExisting: "bestehendes Tutorial löschen",
builder_solution: "Lösung", builder_solution: "Lösung",
builder_solution_submit: "Lösung einreichen", builder_solution_submit: "Lösung einreichen",
builder_example_submit: "Beispiel einreichen", builder_example_submit: "Beispiel einreichen",
@ -215,7 +224,7 @@ export const UI = {
navbar_tutorials: "Tutorials", navbar_tutorials: "Tutorials",
navbar_tutorialbuilder: "Tutorial erstellen", navbar_tutorialbuilder: "Tutorial erstellen",
navbar_gallery: "Gallerie", navbar_gallery: "Galerie",
navbar_projects: "Projekte", navbar_projects: "Projekte",
navbar_menu: "Menü", navbar_menu: "Menü",

View File

@ -1,11 +1,10 @@
export const FAQ = { export const FAQ = {
/** /**
* FAQ * FAQ
*/ */
faq_q1_question: `How can I copy my program to the senseBox?`,
faq_q1_question: `How can I copy my program to the senseBox?`, faq_q1_answer: `To copy programs to the senseBox, connect it to the computer with the Micro USB cable. Then double click on the red reset button on the senseBox MCU. The senseBox will now be recognized as a removable disk on your computer and the previously created programs can be copied via drag & drop. After each change of the program code the program must be recompiled and transferred again.
faq_q1_answer: `To copy programs to the senseBox, connect it to the computer with the Micro USB cable. Then double click on the red reset button on the senseBox MCU. The senseBox will now be recognized as a removable disk on your computer and the previously created programs can be copied via drag & drop. After each change of the program code the program must be recompiled and transferred again.
#### Activate learning mode of the MCU #### Activate learning mode of the MCU
<iframe width="560" height="315" src="https://www.youtube.com/embed/jzlOJ7Zuqqw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <iframe width="560" height="315" src="https://www.youtube.com/embed/jzlOJ7Zuqqw" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
@ -15,15 +14,15 @@ Copying programs under MacOS does not work via the Finder, but there are still t
- [muCommander](https://www.mucommander.com/) - [muCommander](https://www.mucommander.com/)
`, `,
faq_q2_question: `With which senseBox is the programming environment compatible?`, faq_q2_question: `With which senseBox is the programming environment compatible?`,
faq_q2_answer: ` faq_q2_answer: `
Basically the programming environment can be used with any senseBox with senseBox MCU. Basically the programming environment can be used with any senseBox with senseBox MCU.
`, `,
faq_q3_question: `I found an error or something is not working. Where can I report it?`,
faq_q3_question: `I found an error or something is not working. Where can I report it?`, faq_q3_answer: `
faq_q3_answer: `
The best way to do this is to create an issue on [Github](https://github.com/sensebox/React-Ardublockly/issues). Alternatively you can send us an email to info(at)sensebox.de The best way to do this is to create an issue on [Github](https://github.com/sensebox/React-Ardublockly/issues). Alternatively you can send us an email to info(at)sensebox.de
`, `,
faq_tablet_question: `Can I program the senseBox with a tablet?`,
} faq_tablet_answer: `Yes! Install the senseBox Connect app from the App Store and call up the learning and programming environment in your browser on the tablet. Detailed information on how to use the app can be found at [https://sensebox.de/app](https://sensebox.de/app)`,
};

View File

@ -49,6 +49,14 @@ export const UI = {
tooltip_project_title: "Project title", tooltip_project_title: "Project title",
tooltip_check_solution: "Check solution", tooltip_check_solution: "Check solution",
tooltip_copy_code: "Copy Code to clipboard", tooltip_copy_code: "Copy Code to clipboard",
tooltip_statistics_current: "Number of current blocks",
tooltip_statistics_new: "Number of new blocks",
tooltip_statistics_changed: "Number of changed blocks",
tooltip_statistics_moved: "Number of moved blocks",
tooltip_statistics_deleted: "Number of deleted blocks",
tooltip_statistics_remaining: "Remaining blocks",
tooltip_statistics_show: "Show statistics",
tooltip_start_tour: "start Tour",
/** /**
* Messages * Messages
@ -169,7 +177,7 @@ export const UI = {
/** /**
* Tutorials * Tutorials
*/ */
tutorials_home_head: "Tutorials",
tutorials_assessment_task: "Task", tutorials_assessment_task: "Task",
tutorials_hardware_head: tutorials_hardware_head:
"For the implementation you need the following hardware:", "For the implementation you need the following hardware:",
@ -182,7 +190,9 @@ export const UI = {
/** /**
* Tutorial Builder * Tutorial Builder
*/ */
uilder_createNew: "create new Tutorial",
builder_changeExisting: "change existing Tutorial",
builder_deleteExisting: "remove existing Tutorial",
builder_solution: "Solution", builder_solution: "Solution",
builder_solution_submit: "Submit Solution", builder_solution_submit: "Submit Solution",
builder_example_submit: "Submit example", builder_example_submit: "Submit example",

View File

@ -1,68 +1,70 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import { clearStats, workspaceName } from '../actions/workspaceActions'; import { clearStats, workspaceName } from "../actions/workspaceActions";
import * as Blockly from 'blockly/core'; import * as Blockly from "blockly/core";
import { createNameId } from 'mnemonic-id'; import { createNameId } from "mnemonic-id";
import WorkspaceStats from './Workspace/WorkspaceStats'; import WorkspaceStats from "./Workspace/WorkspaceStats";
import WorkspaceFunc from './Workspace/WorkspaceFunc'; import WorkspaceFunc from "./Workspace/WorkspaceFunc";
import BlocklyWindow from './Blockly/BlocklyWindow'; import BlocklyWindow from "./Blockly/BlocklyWindow";
import CodeViewer from './CodeViewer'; import CodeViewer from "./CodeViewer";
import TrashcanButtons from './Workspace/TrashcanButtons'; import TrashcanButtons from "./Workspace/TrashcanButtons";
import HintTutorialExists from './Tutorial/HintTutorialExists'; import HintTutorialExists from "./Tutorial/HintTutorialExists";
import Snackbar from './Snackbar'; import Snackbar from "./Snackbar";
import Grid from '@material-ui/core/Grid'; import Grid from "@material-ui/core/Grid";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from "@material-ui/core/Tooltip";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import { faCode } from "@fortawesome/free-solid-svg-icons"; import { faCode } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import TooltipViewer from './TooltipViewer'; import TooltipViewer from "./TooltipViewer";
const styles = (theme) => ({ const styles = (theme) => ({
codeOn: { codeOn: {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
'&:hover': { "&:hover": {
backgroundColor: theme.palette.primary.contrastText, backgroundColor: theme.palette.primary.contrastText,
color: theme.palette.primary.main, color: theme.palette.primary.main,
border: `1px solid ${theme.palette.secondary.main}` border: `1px solid ${theme.palette.secondary.main}`,
} },
}, },
codeOff: { codeOff: {
backgroundColor: theme.palette.primary.contrastText, backgroundColor: theme.palette.primary.contrastText,
color: theme.palette.primary.main, color: theme.palette.primary.main,
border: `1px solid ${theme.palette.secondary.main}`, border: `1px solid ${theme.palette.secondary.main}`,
'&:hover': { "&:hover": {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
} },
} },
}); });
class Home extends Component { class Home extends Component {
state = { state = {
codeOn: true, codeOn: true,
snackbar: false, snackbar: false,
type: '', type: "",
key: '', key: "",
message: '' message: "",
} };
componentDidMount() { componentDidMount() {
this.setState({ stats: window.localStorage.getItem('stats') }); this.setState({ stats: window.localStorage.getItem("stats") });
if (!this.props.project) { if (!this.props.project) {
this.props.workspaceName(createNameId()); this.props.workspaceName(createNameId());
} }
if (this.props.message && this.props.message.id === 'GET_SHARE_FAIL') { if (this.props.message && this.props.message.id === "GET_SHARE_FAIL") {
this.setState({ snackbar: true, key: Date.now(), message: `Das angefragte geteilte Projekt konnte nicht gefunden werden.`, type: 'error' }); this.setState({
snackbar: true,
key: Date.now(),
message: `Das angefragte geteilte Projekt konnte nicht gefunden werden.`,
type: "error",
});
} }
} }
@ -87,43 +89,76 @@ class Home extends Component {
if (workspace.trashcan && workspace.trashcan.flyout) { if (workspace.trashcan && workspace.trashcan.flyout) {
workspace.trashcan.flyout.hide(); // in case of resize, the trash flyout does not reposition workspace.trashcan.flyout.hide(); // in case of resize, the trash flyout does not reposition
} }
} };
render() { render() {
return ( return (
<div> <div>
{this.props.statistics ? {this.props.statistics ? (
<div style={{ float: 'left', height: '40px', position: 'relative' }}><WorkspaceStats /></div> <div style={{ float: "left", height: "40px", position: "relative" }}>
: null <WorkspaceStats />
} </div>
<div className='workspaceFunc' style={{ float: 'right', height: '40px', marginBottom: '20px' }}> ) : null}
<WorkspaceFunc project={this.props.project} projectType={this.props.projectType} /> <div
className="workspaceFunc"
style={{ float: "right", height: "40px", marginBottom: "20px" }}
>
<WorkspaceFunc
project={this.props.project}
projectType={this.props.projectType}
/>
</div> </div>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} md={this.state.codeOn ? 8 : 12} style={{ position: 'relative' }}> <Grid
<Tooltip title={this.state.codeOn ? 'Code ausblenden' : 'Code anzeigen'} > item
xs={12}
md={this.state.codeOn ? 8 : 12}
style={{ position: "relative" }}
>
<Tooltip
title={
this.state.codeOn
? Blockly.Msg.tooltip_hide_code
: Blockly.Msg.tooltip_show_code
}
>
<IconButton <IconButton
className={`showCode ${this.state.codeOn ? this.props.classes.codeOn : this.props.classes.codeOff}`} className={`showCode ${
style={{ width: '40px', height: '40px', position: 'absolute', top: -12, right: 8, zIndex: 21 }} this.state.codeOn
? this.props.classes.codeOn
: this.props.classes.codeOff
}`}
style={{
width: "40px",
height: "40px",
position: "absolute",
top: -12,
right: 8,
zIndex: 21,
}}
onClick={() => this.onChange()} onClick={() => this.onChange()}
> >
<FontAwesomeIcon icon={faCode} size="xs" /> <FontAwesomeIcon icon={faCode} size="xs" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<TrashcanButtons /> <TrashcanButtons />
<div className='blocklyWindow'> <div className="blocklyWindow">
{this.props.project ? {this.props.project ? (
< BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.props.project.xml} /> <BlocklyWindow
: < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> blocklyCSS={{ height: "80vH" }}
} initialXml={this.props.project.xml}
/>
) : (
<BlocklyWindow blocklyCSS={{ height: "80vH" }} />
)}
</div> </div>
</Grid> </Grid>
{this.state.codeOn ? {this.state.codeOn ? (
<Grid item xs={12} md={4}> <Grid item xs={12} md={4}>
<CodeViewer /> <CodeViewer />
<TooltipViewer /> <TooltipViewer />
</Grid> </Grid>
: null} ) : null}
</Grid> </Grid>
<HintTutorialExists /> <HintTutorialExists />
<Snackbar <Snackbar
@ -134,20 +169,21 @@ class Home extends Component {
/> />
</div> </div>
); );
}; }
} }
Home.propTypes = { Home.propTypes = {
clearStats: PropTypes.func.isRequired, clearStats: PropTypes.func.isRequired,
workspaceName: PropTypes.func.isRequired, workspaceName: PropTypes.func.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
statistics: PropTypes.bool.isRequired statistics: PropTypes.bool.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
message: state.message, message: state.message,
statistics: state.general.statistics statistics: state.general.statistics,
}); });
export default connect(mapStateToProps, { clearStats, workspaceName })(
export default connect(mapStateToProps, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home)); withStyles(styles, { withTheme: true })(Home)
);

View File

@ -139,7 +139,7 @@ class Navbar extends Component {
</Link> </Link>
) : null} ) : null}
{isHome ? ( {isHome ? (
<Tooltip title="Hilfe starten" arrow> <Tooltip title={Blockly.Msg.tooltip_start_tour} arrow>
<IconButton <IconButton
color="inherit" color="inherit"
className={`openTour ${this.props.classes.button}`} className={`openTour ${this.props.classes.button}`}
@ -153,7 +153,7 @@ class Navbar extends Component {
</Tooltip> </Tooltip>
) : null} ) : null}
{isAssessment ? ( {isAssessment ? (
<Tooltip title="Hilfe starten" arrow> <Tooltip title={Blockly.Msg.tooltip_start_tour} arrow>
<IconButton <IconButton
color="inherit" color="inherit"
className={`openTour ${this.props.classes.button}`} className={`openTour ${this.props.classes.button}`}

View File

@ -39,6 +39,7 @@ import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem"; import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl"; import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select"; import Select from "@material-ui/core/Select";
import * as Blockly from "blockly";
const styles = (theme) => ({ const styles = (theme) => ({
backdrop: { backdrop: {
@ -392,7 +393,7 @@ class Builder extends Component {
style={{ color: "black" }} style={{ color: "black" }}
value="new" value="new"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label="neues Tutorial erstellen" label={Blockly.Msg.builder_createNew}
labelPlacement="end" labelPlacement="end"
/> />
{filteredTutorials.length > 0 ? ( {filteredTutorials.length > 0 ? (
@ -402,7 +403,7 @@ class Builder extends Component {
disabled={this.props.index === 0} disabled={this.props.index === 0}
value="change" value="change"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label="bestehendes Tutorial ändern" label={Blockly.Msg.builder_changeExisting}
labelPlacement="end" labelPlacement="end"
/> />
<FormControlLabel <FormControlLabel
@ -410,7 +411,7 @@ class Builder extends Component {
disabled={this.props.index === 0} disabled={this.props.index === 0}
value="delete" value="delete"
control={<Radio color="primary" />} control={<Radio color="primary" />}
label="bestehendes Tutorial löschen" label={Blockly.Msg.builder_deleteExisting}
labelPlacement="end" labelPlacement="end"
/> />
</div> </div>

View File

@ -1,129 +1,224 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import { getTutorials, resetTutorial, tutorialProgress } from '../../actions/tutorialActions'; import {
import { clearMessages } from '../../actions/messageActions'; getTutorials,
resetTutorial,
tutorialProgress,
} from "../../actions/tutorialActions";
import { clearMessages } from "../../actions/messageActions";
import clsx from 'clsx'; import clsx from "clsx";
import Breadcrumbs from '../Breadcrumbs'; import Breadcrumbs from "../Breadcrumbs";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { fade } from '@material-ui/core/styles/colorManipulator'; import { fade } from "@material-ui/core/styles/colorManipulator";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import Grid from '@material-ui/core/Grid'; import Grid from "@material-ui/core/Grid";
import Paper from '@material-ui/core/Paper'; import Paper from "@material-ui/core/Paper";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Blockly from "blockly";
const styles = (theme) => ({ const styles = (theme) => ({
outerDiv: { outerDiv: {
position: 'absolute', position: "absolute",
right: '-30px', right: "-30px",
bottom: '-30px', bottom: "-30px",
width: '160px', width: "160px",
height: '160px', height: "160px",
color: fade(theme.palette.secondary.main, 0.6) color: fade(theme.palette.secondary.main, 0.6),
}, },
outerDivError: { outerDivError: {
stroke: fade(theme.palette.error.dark, 0.6), stroke: fade(theme.palette.error.dark, 0.6),
color: fade(theme.palette.error.dark, 0.6) color: fade(theme.palette.error.dark, 0.6),
}, },
outerDivSuccess: { outerDivSuccess: {
stroke: fade(theme.palette.primary.main, 0.6), stroke: fade(theme.palette.primary.main, 0.6),
color: fade(theme.palette.primary.main, 0.6) color: fade(theme.palette.primary.main, 0.6),
}, },
outerDivOther: { outerDivOther: {
stroke: fade(theme.palette.secondary.main, 0.6) stroke: fade(theme.palette.secondary.main, 0.6),
}, },
innerDiv: { innerDiv: {
width: 'inherit', width: "inherit",
height: 'inherit', height: "inherit",
display: 'table-cell', display: "table-cell",
verticalAlign: 'middle', verticalAlign: "middle",
textAlign: 'center' textAlign: "center",
} },
}); });
class TutorialHome extends Component { class TutorialHome extends Component {
componentDidMount() { componentDidMount() {
this.props.tutorialProgress(); this.props.tutorialProgress();
// retrieve tutorials only if a potential user is loaded - authentication // retrieve tutorials only if a potential user is loaded - authentication
// is finished (success or failed) // is finished (success or failed)
if(!this.props.progress){ if (!this.props.progress) {
this.props.getTutorials(); this.props.getTutorials();
} }
} }
componentDidUpdate(props, state) { componentDidUpdate(props, state) {
if(props.progress !== this.props.progress && !this.props.progress){ if (props.progress !== this.props.progress && !this.props.progress) {
// authentication is completed // authentication is completed
this.props.getTutorials(); this.props.getTutorials();
} }
if(this.props.message.id === 'GET_TUTORIALS_FAIL'){ if (this.props.message.id === "GET_TUTORIALS_FAIL") {
alert(this.props.message.msg); alert(this.props.message.msg);
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.resetTutorial(); this.props.resetTutorial();
if(this.props.message.msg){ if (this.props.message.msg) {
this.props.clearMessages(); this.props.clearMessages();
} }
} }
render() { render() {
return ( return this.props.isLoading ? null : (
this.props.isLoading ? null :
<div> <div>
<Breadcrumbs content={[{ link: '/tutorial', title: 'Tutorial' }]} /> <Breadcrumbs content={[{ link: "/tutorial", title: "Tutorial" }]} />
<h1>Tutorial-Übersicht</h1> <h1>{Blockly.Msg.tutorials_home_head}</h1>
<Grid container spacing={2}> <Grid container spacing={2}>
{this.props.tutorials.map((tutorial, i) => { {this.props.tutorials.map((tutorial, i) => {
var status = this.props.status.filter(status => status._id === tutorial._id)[0]; var status = this.props.status.filter(
(status) => status._id === tutorial._id
)[0];
var tasks = status.tasks; var tasks = status.tasks;
var error = status.tasks.filter(task => task.type === 'error').length > 0; var error =
var success = status.tasks.filter(task => task.type === 'success').length / tasks.length status.tasks.filter((task) => task.type === "error").length > 0;
var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other'; var success =
status.tasks.filter((task) => task.type === "success").length /
tasks.length;
var tutorialStatus =
success === 1 ? "Success" : error ? "Error" : "Other";
return ( return (
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}> <Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
<Link to={`/tutorial/${tutorial._id}`} style={{ textDecoration: 'none', color: 'inherit' }}> <Link
<Paper style={{ height: '150px', padding: '10px', position: 'relative', overflow: 'hidden' }}> to={`/tutorial/${tutorial._id}`}
style={{ textDecoration: "none", color: "inherit" }}
>
<Paper
style={{
height: "150px",
padding: "10px",
position: "relative",
overflow: "hidden",
}}
>
{tutorial.title} {tutorial.title}
<div className={clsx(this.props.classes.outerDiv)} style={{ width: '160px', height: '160px', border: 0 }}> <div
<svg style={{ width: '100%', height: '100%' }}> className={clsx(this.props.classes.outerDiv)}
{error || success === 1 ? style={{ width: "160px", height: "160px", border: 0 }}
<circle className={error ? this.props.classes.outerDivError : this.props.classes.outerDivSuccess} style={{ transform: 'rotate(-44deg)', transformOrigin: "50% 50%" }} r="75" cx="50%" cy="50%" fill="none" stroke-width="10"></circle> >
: <circle className={this.props.classes.outerDivOther} style={{ transform: 'rotate(-44deg)', transformOrigin: "50% 50%" }} r="75" cx="50%" cy="50%" fill="none" stroke-width="10" stroke-dashoffset={`${(75 * 2 * Math.PI) * (1 - (50 / 100 + success / 2))}`} stroke-dasharray={`${(75 * 2 * Math.PI) * (1 - (50 / 100 - success / 2))} ${(75 * 2 * Math.PI) * (1 - (50 / 100 + success / 2))}`}></circle>} <svg style={{ width: "100%", height: "100%" }}>
{success < 1 && !error ? {error || success === 1 ? (
<circle className={this.props.classes.outerDivSuccess} style={{ transform: 'rotate(-44deg)', transformOrigin: "50% 50%" }} r="75" cx="50%" cy="50%" fill="none" stroke-width="10" stroke-dashoffset={`${(75 * 2 * Math.PI) * (1 - (50 / 100 + success / 2))}`} stroke-dasharray={`${(75 * 2 * Math.PI)}`}> <circle
</circle> className={
: null} error
? this.props.classes.outerDivError
: this.props.classes.outerDivSuccess
}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
></circle>
) : (
<circle
className={this.props.classes.outerDivOther}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
stroke-dashoffset={`${
75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
stroke-dasharray={`${
75 * 2 * Math.PI * (1 - (50 / 100 - success / 2))
} ${
75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
></circle>
)}
{success < 1 && !error ? (
<circle
className={this.props.classes.outerDivSuccess}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
stroke-dashoffset={`${
75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
stroke-dasharray={`${75 * 2 * Math.PI}`}
></circle>
) : null}
</svg> </svg>
</div> </div>
<div className={clsx(this.props.classes.outerDiv, tutorialStatus === 'Error' ? this.props.classes.outerDivError : tutorialStatus === 'Success' ? this.props.classes.outerDivSuccess : null)}> <div
className={clsx(
this.props.classes.outerDiv,
tutorialStatus === "Error"
? this.props.classes.outerDivError
: tutorialStatus === "Success"
? this.props.classes.outerDivSuccess
: null
)}
>
<div className={this.props.classes.innerDiv}> <div className={this.props.classes.innerDiv}>
{error || success === 1 ? {error || success === 1 ? (
<FontAwesomeIcon size='4x' icon={tutorialStatus === 'Success' ? faCheck : faTimes} /> <FontAwesomeIcon
: <Typography variant='h3' className={success > 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success * 100)}%</Typography> size="4x"
} icon={
tutorialStatus === "Success" ? faCheck : faTimes
}
/>
) : (
<Typography
variant="h3"
className={
success > 0
? this.props.classes.outerDivSuccess
: {}
}
>
{Math.round(success * 100)}%
</Typography>
)}
</div> </div>
</div> </div>
</Paper> </Paper>
</Link> </Link>
</Grid> </Grid>
) );
})} })}
</Grid> </Grid>
</div> </div>
); );
}; }
} }
TutorialHome.propTypes = { TutorialHome.propTypes = {
@ -136,16 +231,21 @@ TutorialHome.propTypes = {
tutorials: PropTypes.array.isRequired, tutorials: PropTypes.array.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
progress: PropTypes.bool.isRequired progress: PropTypes.bool.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
change: state.tutorial.change, change: state.tutorial.change,
status: state.tutorial.status, status: state.tutorial.status,
tutorials: state.tutorial.tutorials, tutorials: state.tutorial.tutorials,
isLoading: state.tutorial.progress, isLoading: state.tutorial.progress,
message: state.message, message: state.message,
progress: state.auth.progress progress: state.auth.progress,
}); });
export default connect(mapStateToProps, { getTutorials, resetTutorial, clearMessages, tutorialProgress })(withStyles(styles, { withTheme: true })(TutorialHome)); export default connect(mapStateToProps, {
getTutorials,
resetTutorial,
clearMessages,
tutorialProgress,
})(withStyles(styles, { withTheme: true })(TutorialHome));

View File

@ -1,147 +1,204 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import * as Blockly from 'blockly/core'; import * as Blockly from "blockly/core";
import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import withWidth, { isWidthDown } from "@material-ui/core/withWidth";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from "@material-ui/core/Tooltip";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import Chip from '@material-ui/core/Chip'; import Chip from "@material-ui/core/Chip";
import Avatar from '@material-ui/core/Avatar'; import Avatar from "@material-ui/core/Avatar";
import Popover from '@material-ui/core/Popover'; import Popover from "@material-ui/core/Popover";
import { faPuzzlePiece, faTrash, faPlus, faPen, faArrowsAlt, faEllipsisH } 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"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const styles = (theme) => ({ const styles = (theme) => ({
stats: { stats: {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
display: 'inline', display: "inline",
marginLeft: '50px', marginLeft: "50px",
padding: '3px 10px', padding: "3px 10px",
// borderRadius: '25%' // borderRadius: '25%'
}, },
menu: { menu: {
backgroundColor: theme.palette.secondary.main, backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText, color: theme.palette.secondary.contrastText,
width: '40px', width: "40px",
height: '40px', height: "40px",
'&:hover': { "&:hover": {
backgroundColor: theme.palette.secondary.main, backgroundColor: theme.palette.secondary.main,
color: theme.palette.primary.main, color: theme.palette.primary.main,
} },
} },
}); });
class WorkspaceStats extends Component { class WorkspaceStats extends Component {
state = { state = {
anchor: null anchor: null,
} };
handleClose = () => { handleClose = () => {
this.setState({ anchor: null }); this.setState({ anchor: null });
} };
handleClick = (event) => { handleClick = (event) => {
this.setState({ anchor: event.currentTarget }); this.setState({ anchor: event.currentTarget });
}; };
render() { render() {
const bigDisplay = !isWidthDown("sm", this.props.width);
const bigDisplay = !isWidthDown('sm', this.props.width);
const workspace = Blockly.getMainWorkspace(); const workspace = Blockly.getMainWorkspace();
const remainingBlocksInfinity = workspace ? workspace.remainingCapacity() !== Infinity : null; const remainingBlocksInfinity = workspace
const stats = <div style={bigDisplay ? { display: 'flex' } : { display: 'inline' }}> ? workspace.remainingCapacity() !== Infinity
<Tooltip title="Anzahl aktueller Blöcke" arrow> : null;
<Chip const stats = (
style={bigDisplay ? { marginRight: '1rem' } : { marginRight: '1rem', marginBottom: '5px' }} <div style={bigDisplay ? { display: "flex" } : { display: "inline" }}>
color="primary" <Tooltip title={Blockly.Msg.tooltip_statistics_current} arrow>
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 <Chip
style={bigDisplay ? { marginRight: '1rem' } : { marginRight: '1rem', marginBottom: '5px' }} style={
bigDisplay
? { marginRight: "1rem" }
: { marginRight: "1rem", marginBottom: "5px" }
}
color="primary" color="primary"
label={workspace.remainingCapacity()}> avatar={
</Chip> <Avatar>
</Tooltip> : null} <FontAwesomeIcon icon={faPuzzlePiece} />
</div> </Avatar>
return ( }
bigDisplay ? label={workspace ? workspace.getAllBlocks().length : 0}
<div style={{ bottom: 0, position: 'absolute' }}> ></Chip>
{stats} </Tooltip>
</div> <Tooltip title={Blockly.Msg.tooltip_statistics_new} arrow>
: <Chip
<div> style={
<Tooltip title='Statistiken anzeigen' arrow> bigDisplay
<IconButton ? { marginRight: "1rem" }
className={this.props.classes.menu} : { marginRight: "1rem", marginBottom: "5px" }
onClick={(event) => this.handleClick(event)} }
> color="primary"
<FontAwesomeIcon icon={faEllipsisH} size="xs" /> avatar={
</IconButton> <Avatar>
</Tooltip> <FontAwesomeIcon icon={faPlus} />
<Popover </Avatar>
open={Boolean(this.state.anchor)} }
anchorEl={this.state.anchor} label={this.props.create > 0 ? this.props.create : 0}
onClose={this.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
PaperProps={{
style: { margin: '5px' }
}}
> >
<div style={{ margin: '10px' }}> {" "}
{stats} {/* initialXML is created automatically, Block is not part of the statistics */}
</div> </Chip>
</Popover> </Tooltip>
</div> <Tooltip title={Blockly.Msg.tooltip_statistics_changed} 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={Blockly.Msg.tooltip_statistics_moved} 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={Blockly.Msg.tooltip_statistics_deleted} 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={Blockly.Msg.tooltip_statistics_remaining} arrow>
<Chip
style={
bigDisplay
? { marginRight: "1rem" }
: { marginRight: "1rem", marginBottom: "5px" }
}
color="primary"
label={workspace.remainingCapacity()}
></Chip>
</Tooltip>
) : null}
</div>
); );
}; return bigDisplay ? (
<div style={{ bottom: 0, position: "absolute" }}>{stats}</div>
) : (
<div>
<Tooltip title={Blockly.Msg.tooltip_statistics_show} 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>
);
}
} }
WorkspaceStats.propTypes = { WorkspaceStats.propTypes = {
@ -149,15 +206,18 @@ WorkspaceStats.propTypes = {
change: PropTypes.number.isRequired, change: PropTypes.number.isRequired,
delete: PropTypes.number.isRequired, delete: PropTypes.number.isRequired,
move: PropTypes.number.isRequired, move: PropTypes.number.isRequired,
workspaceChange: PropTypes.number.isRequired workspaceChange: PropTypes.number.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
create: state.workspace.stats.create, create: state.workspace.stats.create,
change: state.workspace.stats.change, change: state.workspace.stats.change,
delete: state.workspace.stats.delete, delete: state.workspace.stats.delete,
move: state.workspace.stats.move, move: state.workspace.stats.move,
workspaceChange: state.workspace.change workspaceChange: state.workspace.change,
}); });
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceStats))); export default connect(
mapStateToProps,
null
)(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceStats)));

View File

@ -1,21 +1,22 @@
import * as Blockly from 'blockly/core'; import * as Blockly from "blockly/core";
export const FaqQuestions = () => { export const FaqQuestions = () => {
return ( return [
[ {
{ question: `${Blockly.Msg.faq_q1_question}`,
question: `${Blockly.Msg.faq_q1_question}`, answer: `${Blockly.Msg.faq_q1_answer}`,
answer: `${Blockly.Msg.faq_q1_answer}`, },
}, {
{ question: `${Blockly.Msg.faq_q2_question}`,
question: `${Blockly.Msg.faq_q2_question}`, answer: `${Blockly.Msg.faq_q2_answer}`,
answer: `${Blockly.Msg.faq_q2_answer}`, },
}, {
{ question: `${Blockly.Msg.faq_tablet_question}`,
question: `${Blockly.Msg.faq_q3_question}`, answer: `${Blockly.Msg.faq_tablet_answer}`,
answer: `${Blockly.Msg.faq_q3_answer}`, },
}, {
question: `${Blockly.Msg.faq_q3_question}`,
]) answer: `${Blockly.Msg.faq_q3_answer}`,
} },
];
};