Merge pull request #153 from sensebox/markdown_file_upload

file upload with multer
This commit is contained in:
Mario Pesch 2022-02-18 12:21:00 +01:00 committed by GitHub
commit 8fd9eb01ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 548 additions and 412 deletions

View File

@ -23,6 +23,7 @@
"axios": "^0.22.0", "axios": "^0.22.0",
"blockly": "^7.20211209.4", "blockly": "^7.20211209.4",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"markdown-it": "^12.3.2",
"mnemonic-id": "^3.2.7", "mnemonic-id": "^3.2.7",
"moment": "^2.28.0", "moment": "^2.28.0",
"prismjs": "^1.25.0", "prismjs": "^1.25.0",
@ -30,6 +31,7 @@
"react-cookie-consent": "^7.2.1", "react-cookie-consent": "^7.2.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-markdown": "^5.0.2", "react-markdown": "^5.0.2",
"react-markdown-editor-lite": "^1.3.2",
"react-mde": "^11.5.0", "react-mde": "^11.5.0",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
@ -46,7 +48,7 @@
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "node_modules/react-scripts/bin/react-scripts.js start",
"dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:8080\" && npm start", "dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:8080\" && npm start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",

View File

@ -9,11 +9,11 @@ import { loadUser } from "./actions/authActions";
import "./App.css"; import "./App.css";
import { ThemeProvider, createMuiTheme } from "@material-ui/core/styles"; import { ThemeProvider, createTheme } from "@material-ui/core/styles";
import Content from "./components/Content"; import Content from "./components/Content";
const theme = createMuiTheme({ const theme = createTheme({
palette: { palette: {
primary: { primary: {
main: "#4EAF47", main: "#4EAF47",

View File

@ -13,10 +13,10 @@ import SaveIcon from "./SaveIcon";
import store from "../../store"; import store from "../../store";
const CodeEditor = (props) => { const CodeEditor = (props) => {
const [fileHandle, setFileHandle] = useState(); //const [filehandle, setFileHandle] = useState();
const [fileContent, setFileContent] = useState(""); const [fileContent, setFileContent] = useState("");
const [progress, setProgress] = useState(false); const [progress, setProgress] = useState(false);
const [id, setId] = useState(""); // const [id, setId] = useState("");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const editorRef = useRef(null); const editorRef = useRef(null);
@ -24,20 +24,6 @@ const CodeEditor = (props) => {
const [time, setTime] = useState(null); const [time, setTime] = useState(null);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [resetDialog, setResetDialog] = useState(false); const [resetDialog, setResetDialog] = useState(false);
const [defaultValue, setDefaultValue] = useState(
localStorage.getItem("ArduinoCode")
? localStorage.getItem("ArduinoCode")
: `
#include <senseBoxIO.h> //needs to be always included
void setup () {
}
void loop() {
}`
);
const compile = () => { const compile = () => {
setProgress(true); setProgress(true);
@ -60,7 +46,7 @@ void loop() {
} }
setProgress(false); setProgress(false);
const result = data.data.id; const result = data.data.id;
setId(result); //setId(result);
const filename = "sketch"; const filename = "sketch";
window.open( window.open(
`${process.env.REACT_APP_COMPILER_URL}/download?id=${result}&board=${process.env.REACT_APP_BOARD}&filename=${filename}`, `${process.env.REACT_APP_COMPILER_URL}/download?id=${result}&board=${process.env.REACT_APP_BOARD}&filename=${filename}`,
@ -83,7 +69,7 @@ void loop() {
const openIno = async () => { const openIno = async () => {
const [myFileHandle] = await window.showOpenFilePicker(); const [myFileHandle] = await window.showOpenFilePicker();
setFileHandle(myFileHandle); //setFileHandle(myFileHandle);
const file = await myFileHandle.getFile(); const file = await myFileHandle.getFile();
const contents = await file.text(); const contents = await file.text();
@ -187,7 +173,20 @@ void loop() {
editValue(value); editValue(value);
}} }}
defaultLanguage="cpp" defaultLanguage="cpp"
defaultValue={defaultValue} defaultValue={
localStorage.getItem("ArduinoCode")
? localStorage.getItem("ArduinoCode")
: `
#include <senseBoxIO.h> //needs to be always included
void setup () {
}
void loop() {
}`
}
value={fileContent} value={fileContent}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
editorRef.current = editor; editorRef.current = editor;

View File

@ -1,39 +1,36 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import Breadcrumbs from './Breadcrumbs'; import Breadcrumbs from "./Breadcrumbs";
import { withRouter } from 'react-router-dom'; import { withRouter } from "react-router-dom";
import Button from '@material-ui/core/Button'; import Button from "@material-ui/core/Button";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import * as Blockly from 'blockly' import * as Blockly from "blockly";
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from "react-markdown";
import Container from '@material-ui/core/Container'; import Container from "@material-ui/core/Container";
import ExpansionPanel from '@material-ui/core/ExpansionPanel'; import ExpansionPanel from "@material-ui/core/ExpansionPanel";
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'; import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FaqQuestions } from '../data/faq' import { FaqQuestions } from "../data/faq";
class Faq extends Component { class Faq extends Component {
state = { state = {
panel: '', panel: "",
expanded: false expanded: false,
} };
handleChange = (panel) => { handleChange = (panel) => {
this.setState({ panel: this.state.panel === panel ? '' : panel }); this.setState({ panel: this.state.panel === panel ? "" : panel });
}; };
componentDidMount() { componentDidMount() {
// Ensure that Blockly.setLocale is adopted in the component. // Ensure that Blockly.setLocale is adopted in the component.
// Otherwise, the text will not be displayed until the next update of the component. // Otherwise, the text will not be displayed until the next update of the component.
window.scrollTo(0, 0) window.scrollTo(0, 0);
this.forceUpdate(); this.forceUpdate();
} }
@ -41,139 +38,63 @@ class Faq extends Component {
const { panel } = this.state; const { panel } = this.state;
return ( return (
<div> <div>
<Breadcrumbs content={[{ link: this.props.location.pathname, title: 'FAQ' }]} /> <Breadcrumbs
content={[{ link: this.props.location.pathname, title: "FAQ" }]}
/>
<Container fixed> <Container fixed>
<div style={{ margin: '0px 24px 0px 24px' }}> <div style={{ margin: "0px 24px 0px 24px" }}>
<h1>FAQ</h1> <h1>FAQ</h1>
{FaqQuestions().map((object, i) => { {FaqQuestions().map((object, i) => {
return ( return (
<ExpansionPanel expanded={panel === `panel${i}`} onChange={() => this.handleChange(`panel${i}`)}> <ExpansionPanel
expanded={panel === `panel${i}`}
onChange={() => this.handleChange(`panel${i}`)}
>
<ExpansionPanelSummary <ExpansionPanelSummary
expandIcon={ expandIcon={<FontAwesomeIcon icon={faChevronDown} />}
<FontAwesomeIcon icon={faChevronDown} />
}
> >
<Typography variant="h6">{object.question}</Typography> <Typography variant="h6">{object.question}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails> <ExpansionPanelDetails>
<Typography> <Typography>
<ReactMarkdown className="news" allowDangerousHtml="true" children={object.answer}> <ReactMarkdown
</ReactMarkdown> className="news"
allowDangerousHtml="true"
children={object.answer}
></ReactMarkdown>
</Typography> </Typography>
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
) );
})} })}
{ {this.props.button ? (
this.props.button ?
<Button <Button
style={{ marginTop: '20px' }} style={{ marginTop: "20px" }}
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => { this.props.history.push(this.props.button.link) }} onClick={() => {
this.props.history.push(this.props.button.link);
}}
> >
{this.props.button.title} {this.props.button.title}
</Button> </Button>
: ) : (
<Button <Button
style={{ marginTop: '20px' }} style={{ marginTop: "20px" }}
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => { this.props.history.push('/') }} onClick={() => {
this.props.history.push("/");
}}
> >
{Blockly.Msg.button_back} {Blockly.Msg.button_back}
</Button> </Button>
} )}
</div> </div>
</Container> </Container>
</div> </div>
); );
}; }
} }
export default withRouter(Faq); export default withRouter(Faq);
/*
<ExpansionPanel expanded={panel === 'panel1'} onChange={() => this.handleChange('panel1')}>
<ExpansionPanelSummary
expandIcon={
<FontAwesomeIcon icon={faChevronDown} />
}
>
<Typography variant="h6">{Blockly.Msg.faq_q1_question}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>
<ReactMarkdown className="news" allowDangerousHtml="true" children={Blockly.Msg.faq_q1_answer}>
</ReactMarkdown>
</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel expanded={panel === 'panel2'} onChange={() => this.handleChange('panel2')}>
<ExpansionPanelSummary
expandIcon={
<FontAwesomeIcon icon={faChevronDown} />
}
>
<Typography>Frage 2</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>
Donec placerat, lectus sed mattis semper, neque lectus feugiat lectus, varius pulvinar
diam eros in elit. Pellentesque convallis laoreet laoreet.
</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel expanded={panel === 'panel3'} onChange={() => this.handleChange('panel3')}>
<ExpansionPanelSummary
expandIcon={
<FontAwesomeIcon icon={faChevronDown} />
}
>
<Typography>Frage 3</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>
Nunc vitae orci ultricies, auctor nunc in, volutpat nisl. Integer sit amet egestas eros,
vitae egestas augue. Duis vel est augue.
</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel expanded={panel === 'panel4'} onChange={() => this.handleChange('panel4')}>
<ExpansionPanelSummary
expandIcon={
<FontAwesomeIcon icon={faChevronDown} />
}
>
<Typography>Frage 4</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>
Nunc vitae orci ultricies, auctor nunc in, volutpat nisl. Integer sit amet egestas eros,
vitae egestas augue. Duis vel est augue.
</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
*/
// {{
// this.props.button ?
// <Button
// style={{ marginTop: '20px' }}
// variant="contained"
// color="primary"
// onClick={() => { this.props.history.push(this.props.button.link) }}
// >
// {this.props.button.title}
// </Button>
// :
// <Button
// style={{ marginTop: '20px' }}
// variant="contained"
// color="primary"
// onClick={() => { this.props.history.push('/') }}
// >
// {Blockly.Msg.button_back}
// </Button>
// }}

View File

@ -1,44 +1,41 @@
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 { Route, Redirect, withRouter } from 'react-router-dom';
import { Route, Redirect, withRouter } from "react-router-dom";
class PrivateRoute extends Component { class PrivateRoute extends Component {
render() { render() {
return ( return !this.props.progress ? (
!this.props.progress ?
<Route <Route
{...this.props.exact} {...this.props.exact}
render={({ location }) => render={({ location }) =>
this.props.isAuthenticated ? ( this.props.isAuthenticated
this.props.children ? this.props.children
) : (()=>{ : (() => {
return ( return (
<Redirect <Redirect
to={{ to={{
pathname: "/user/login", pathname: "/user/login",
state: { from: location } state: { from: location },
}} }}
/> />
) );
})() })()
} }
/> : null />
); ) : null;
} }
} }
PrivateRoute.propTypes = { PrivateRoute.propTypes = {
isAuthenticated: PropTypes.bool.isRequired, isAuthenticated: PropTypes.bool,
progress: PropTypes.bool.isRequired progress: PropTypes.bool.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated, isAuthenticated: state.auth.isAuthenticated,
progress: state.auth.progress progress: state.auth.progress,
}); });
export default connect(mapStateToProps, null)(withRouter(PrivateRoute)); export default connect(mapStateToProps, null)(withRouter(PrivateRoute));

View File

@ -74,7 +74,7 @@ class Assessment extends Component {
(task) => task._id === currentTask._id (task) => task._id === currentTask._id
); );
var statusTask = status.tasks[taskIndex]; var statusTask = status.tasks[taskIndex];
console.log(statusTask);
return ( return (
<div className="assessmentDiv" style={{ width: "100%" }}> <div className="assessmentDiv" style={{ width: "100%" }}>
<div style={{ float: "right", height: "40px" }}> <div style={{ float: "right", height: "40px" }}>

View File

@ -283,21 +283,6 @@ class Builder extends Component {
// optional // optional
newTutorial.append(`steps[${i}][xml]`, step.xml); newTutorial.append(`steps[${i}][xml]`, step.xml);
} }
if (step.media) {
// optional
if (step.media.youtube) {
newTutorial.append(
`steps[${i}][media][youtube]`,
step.media.youtube
);
}
if (step.media.picture) {
newTutorial.append(
`steps[${i}][media][picture]`,
step.media.picture
);
}
}
}); });
return newTutorial; return newTutorial;
} }

View File

@ -1,107 +1,159 @@
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 { changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions'; import {
changeContent,
setError,
deleteError,
} from "../../../actions/tutorialBuilderActions";
import hardware from '../../../data/hardware.json'; import hardware from "../../../data/hardware.json";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import withWidth, { isWidthDown } from "@material-ui/core/withWidth";
import GridList from '@material-ui/core/GridList'; import ImageList from "@material-ui/core/ImageList";
import GridListTile from '@material-ui/core/GridListTile'; import ImageListTile from "@material-ui/core/ImageListItem";
import GridListTileBar from '@material-ui/core/GridListTileBar'; import ImageListTileBar from "@material-ui/core/ImageListItemBar";
import FormHelperText from '@material-ui/core/FormHelperText'; import FormHelperText from "@material-ui/core/FormHelperText";
import FormLabel from '@material-ui/core/FormLabel'; import FormLabel from "@material-ui/core/FormLabel";
import * as Blockly from 'blockly' import * as Blockly from "blockly";
const styles = (theme) => ({
const styles = theme => ({ multiImageListTile: {
multiGridListTile: {
background: theme.palette.primary.main, background: theme.palette.primary.main,
opacity: 0.9, opacity: 0.9,
height: '30px' height: "30px",
}, },
multiGridListTileTitle: { multiImageListTileTitle: {
color: theme.palette.text.primary color: theme.palette.text.primary,
}, },
border: { border: {
cursor: 'pointer', cursor: "pointer",
'&:hover': { "&:hover": {
width: 'calc(100% - 4px)', width: "calc(100% - 4px)",
height: 'calc(100% - 4px)', height: "calc(100% - 4px)",
border: `2px solid ${theme.palette.primary.main}` border: `2px solid ${theme.palette.primary.main}`,
} },
}, },
active: { active: {
cursor: 'pointer', cursor: "pointer",
width: 'calc(100% - 4px)', width: "calc(100% - 4px)",
height: 'calc(100% - 4px)', height: "calc(100% - 4px)",
border: `2px solid ${theme.palette.primary.main}` border: `2px solid ${theme.palette.primary.main}`,
}, },
errorColor: { errorColor: {
color: theme.palette.error.dark, color: theme.palette.error.dark,
lineHeight: 'initial', lineHeight: "initial",
marginBottom: '10px' marginBottom: "10px",
} },
}); });
class Requirements extends Component { class Requirements extends Component {
onChange = (hardware) => { onChange = (hardware) => {
var hardwareArray = this.props.value; var hardwareArray = this.props.value;
if (hardwareArray.filter(value => value === hardware).length > 0) { if (hardwareArray.filter((value) => value === hardware).length > 0) {
hardwareArray = hardwareArray.filter(value => value !== hardware); hardwareArray = hardwareArray.filter((value) => value !== hardware);
} } else {
else {
hardwareArray.push(hardware); hardwareArray.push(hardware);
if (this.props.error) { if (this.props.error) {
this.props.deleteError(this.props.index, 'hardware'); this.props.deleteError(this.props.index, "hardware");
} }
} }
this.props.changeContent(hardwareArray, this.props.index, 'hardware'); this.props.changeContent(hardwareArray, this.props.index, "hardware");
if (hardwareArray.length === 0) { if (hardwareArray.length === 0) {
this.props.setError(this.props.index, 'hardware'); this.props.setError(this.props.index, "hardware");
}
} }
};
render() { render() {
var cols = isWidthDown('md', this.props.width) ? isWidthDown('sm', this.props.width) ? isWidthDown('xs', this.props.width) ? 2 : 3 : 4 : 6; var cols = isWidthDown("md", this.props.width)
? isWidthDown("sm", this.props.width)
? isWidthDown("xs", this.props.width)
? 2
: 3
: 4
: 6;
return ( return (
<div style={{ marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)' }}> <div
<FormLabel style={{ color: 'black' }}>Hardware</FormLabel> style={{
<FormHelperText style={this.props.error ? { lineHeight: 'initial', marginTop: '5px' } : { marginTop: '5px', lineHeight: 'initial', marginBottom: '10px' }}>{Blockly.Msg.builder_hardware_order}</FormHelperText> marginBottom: "10px",
{this.props.error ? <FormHelperText className={this.props.classes.errorColor}>{Blockly.Msg.builder_hardware_helper}</FormHelperText> : null} padding: "18.5px 14px",
<GridList cellHeight={100} cols={cols} spacing={10}> borderRadius: "25px",
border: "1px solid lightgrey",
width: "calc(100% - 28px)",
}}
>
<FormLabel style={{ color: "black" }}>Hardware</FormLabel>
<FormHelperText
style={
this.props.error
? { lineHeight: "initial", marginTop: "5px" }
: {
marginTop: "5px",
lineHeight: "initial",
marginBottom: "10px",
}
}
>
{Blockly.Msg.builder_hardware_order}
</FormHelperText>
{this.props.error ? (
<FormHelperText className={this.props.classes.errorColor}>
{Blockly.Msg.builder_hardware_helper}
</FormHelperText>
) : null}
<ImageList rowHeight={100} cols={cols} gap={10}>
{hardware.map((picture, i) => ( {hardware.map((picture, i) => (
<GridListTile key={i} onClick={() => this.onChange(picture.id)} classes={{ tile: this.props.value.filter(value => value === picture.id).length > 0 ? this.props.classes.active : this.props.classes.border }}> <ImageListTile
<div style={{ margin: 'auto', width: 'max-content' }}> key={i}
<img src={`/media/hardware/${picture.src}`} alt={picture.name} height={100} /> onClick={() => this.onChange(picture.id)}
classes={{
item:
this.props.value.filter((value) => value === picture.id)
.length > 0
? this.props.classes.active
: this.props.classes.border,
}}
>
<div style={{ margin: "auto", width: "max-content" }}>
<img
src={`/media/hardware/${picture.src}`}
alt={picture.name}
height={100}
/>
</div> </div>
<GridListTileBar <ImageListTileBar
classes={{ root: this.props.classes.multiGridListTile }} classes={{ root: this.props.classes.multiImageListTile }}
title={ title={
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} className={this.props.classes.multiGridListTileTitle}> <div
style={{ overflow: "hidden", textOverflow: "ellipsis" }}
className={this.props.classes.multiImageListTileTitle}
>
{picture.name} {picture.name}
</div> </div>
} }
/> />
</GridListTile> </ImageListTile>
))} ))}
</GridList> </ImageList>
</div> </div>
); );
}; }
} }
Requirements.propTypes = { Requirements.propTypes = {
changeContent: PropTypes.func.isRequired, changeContent: PropTypes.func.isRequired,
setError: PropTypes.func.isRequired, setError: PropTypes.func.isRequired,
deleteError: PropTypes.func.isRequired, deleteError: PropTypes.func.isRequired,
change: PropTypes.number.isRequired change: PropTypes.number.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
change: state.builder.change change: state.builder.change,
}); });
export default connect(mapStateToProps, { changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(withWidth()(Requirements))); export default connect(mapStateToProps, {
changeContent,
setError,
deleteError,
})(withStyles(styles, { withTheme: true })(withWidth()(Requirements)));

View File

@ -0,0 +1,81 @@
import React from "react";
import { connect } from "react-redux";
import {
tutorialTitle,
jsonString,
changeContent,
setError,
deleteError,
} from "../../../actions/tutorialBuilderActions";
import FormControl from "@material-ui/core/FormControl";
import MarkdownIt from "markdown-it";
import Editor from "react-markdown-editor-lite";
import "react-markdown-editor-lite/lib/index.css";
import axios from "axios";
const mdParser = new MarkdownIt(/* Markdown-it options */);
const MarkdownEditor = (props) => {
//const [value, setValue] = React.useState(props.value);
const mdEditor = React.useRef(null);
function handleChange({ html, text }) {
//setValue(text);
var value = text;
props.changeContent(value, props.index, props.property, props.property2);
if (value.replace(/\s/g, "") === "") {
props.setError(props.index, props.property);
} else {
props.deleteError(props.index, props.property);
}
}
async function uploadImage(files) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("files", files);
axios({
method: "post",
url: `${process.env.REACT_APP_BLOCKLY_API}/upload/uploadImage`,
data: formData,
headers: { "Content-Type": "multipart/form-data" },
})
.then((res) => {
console.log(res);
resolve(
`${process.env.REACT_APP_BLOCKLY_API}/upload/` + res.data.filename
);
})
.catch((err) => {
reject(new Error("error"));
});
});
}
return (
<FormControl variant="outlined" fullWidth style={{ marginBottom: "10px" }}>
<Editor
ref={mdEditor}
style={{ height: "500px" }}
renderHTML={(text) => mdParser.render(text)}
onChange={handleChange}
//value={value}
id={props.property}
label={props.label}
property={props.property}
onImageUpload={uploadImage}
/>
</FormControl>
);
};
export default connect(null, {
tutorialTitle,
jsonString,
changeContent,
setError,
deleteError,
})(MarkdownEditor);

View File

@ -1,82 +1,117 @@
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 { addStep, removeStep, changeStepIndex } from '../../../actions/tutorialBuilderActions'; import {
addStep,
removeStep,
changeStepIndex,
} from "../../../actions/tutorialBuilderActions";
import clsx from 'clsx'; import clsx from "clsx";
import Textfield from './Textfield'; import Textfield from "./Textfield";
import StepType from './StepType'; import StepType from "./StepType";
import BlocklyExample from './BlocklyExample'; import BlocklyExample from "./BlocklyExample";
import Requirements from './Requirements'; import Requirements from "./Requirements";
import Hardware from './Hardware'; import Hardware from "./Hardware";
import Media from './Media';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
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 { faPlus, faAngleDoubleUp, faAngleDoubleDown, faTrash } from "@fortawesome/free-solid-svg-icons"; import {
faPlus,
faAngleDoubleUp,
faAngleDoubleDown,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import MarkdownEditor from "./MarkdownEditor";
const styles = (theme) => ({ const styles = (theme) => ({
button: { button: {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
width: '40px', width: "40px",
height: '40px', height: "40px",
'&:hover': { "&:hover": {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
} },
}, },
delete: { delete: {
backgroundColor: theme.palette.error.dark, backgroundColor: theme.palette.error.dark,
color: theme.palette.error.contrastText, color: theme.palette.error.contrastText,
'&:hover': { "&:hover": {
backgroundColor: theme.palette.error.dark, backgroundColor: theme.palette.error.dark,
color: theme.palette.error.contrastText, color: theme.palette.error.contrastText,
} },
} },
}); });
class Step extends Component { class Step extends Component {
render() { render() {
var index = this.props.index; var index = this.props.index;
var steps = this.props.steps; var steps = this.props.steps;
return ( return (
<div style={{borderRadius: '25px', border: '1px solid lightgrey', padding: '10px 14px 10px 10px', marginBottom: '20px'}}> <div
<Typography variant='h6' style={{marginBottom: '10px', marginLeft: '4px'}}>Schritt {index+1}</Typography> style={{
<div style={{display: 'flex', position: 'relative'}}> borderRadius: "25px",
<div style={{width: '40px', marginRight: '10px', position: 'absolute', left: '4px', bottom: '10px'}}> border: "1px solid lightgrey",
<Tooltip title='Schritt hinzufügen' arrow> padding: "10px 14px 10px 10px",
marginBottom: "20px",
}}
>
<Typography
variant="h6"
style={{ marginBottom: "10px", marginLeft: "4px" }}
>
Schritt {index + 1}
</Typography>
<div style={{ display: "flex", position: "relative" }}>
<div
style={{
width: "40px",
marginRight: "10px",
position: "absolute",
left: "4px",
bottom: "10px",
}}
>
<Tooltip title="Schritt hinzufügen" arrow>
<IconButton <IconButton
className={this.props.classes.button} className={this.props.classes.button}
style={index === 0 ? {} : {marginBottom: '5px'}} style={index === 0 ? {} : { marginBottom: "5px" }}
onClick={() => this.props.addStep(index + 1)} onClick={() => this.props.addStep(index + 1)}
> >
<FontAwesomeIcon icon={faPlus} size="xs" /> <FontAwesomeIcon icon={faPlus} size="xs" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{index !== 0 ? {index !== 0 ? (
<div> <div>
<Tooltip title={`Schritt ${index+1} nach oben schieben`} arrow> <Tooltip
title={`Schritt ${index + 1} nach oben schieben`}
arrow
>
<IconButton <IconButton
disabled={index < 2} disabled={index < 2}
className={this.props.classes.button} className={this.props.classes.button}
style={{marginBottom: '5px'}} style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index - 1)} onClick={() => this.props.changeStepIndex(index, index - 1)}
> >
<FontAwesomeIcon icon={faAngleDoubleUp} size="xs" /> <FontAwesomeIcon icon={faAngleDoubleUp} size="xs" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={`Schritt ${index+1} nach unten schieben`} arrow> <Tooltip
title={`Schritt ${index + 1} nach unten schieben`}
arrow
>
<IconButton <IconButton
disabled={index === steps.length - 1} disabled={index === steps.length - 1}
className={this.props.classes.button} className={this.props.classes.button}
style={{marginBottom: '5px'}} style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index + 1)} onClick={() => this.props.changeStepIndex(index, index + 1)}
> >
<FontAwesomeIcon icon={faAngleDoubleDown} size="xs" /> <FontAwesomeIcon icon={faAngleDoubleDown} size="xs" />
@ -85,34 +120,75 @@ class Step extends Component {
<Tooltip title={`Schritt ${index + 1} löschen`} arrow> <Tooltip title={`Schritt ${index + 1} löschen`} arrow>
<IconButton <IconButton
disabled={index === 0} disabled={index === 0}
className={clsx(this.props.classes.button, this.props.classes.delete)} className={clsx(
this.props.classes.button,
this.props.classes.delete
)}
onClick={() => this.props.removeStep(index)} onClick={() => this.props.removeStep(index)}
> >
<FontAwesomeIcon icon={faTrash} size="xs" /> <FontAwesomeIcon icon={faTrash} size="xs" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
: null} ) : null}
</div> </div>
<div style={{width: '100%', marginLeft: '54px'}}> <div style={{ width: "100%", marginLeft: "54px" }}>
<StepType value={this.props.step.type} index={index} /> <StepType value={this.props.step.type} index={index} />
<Textfield value={this.props.step.headline} property={'headline'} label={'Überschrift'} index={index} error={this.props.error.steps[index].headline} errorText={`Gib eine Überschrift für die ${this.props.step.type === 'task' ? 'Aufgabe' : 'Anleitung'} ein.`} /> <Textfield
<Textfield value={this.props.step.text} property={'text'} label={this.props.step.type === 'task' ? 'Aufgabenstellung' : 'Instruktionen'} index={index} multiline error={this.props.error.steps[index].text} errorText={`Gib Instruktionen für die ${this.props.step.type === 'task' ? 'Aufgabe' : 'Anleitung'} ein.`}/> value={this.props.step.headline}
{index === 0 ? property={"headline"}
label={"Überschrift"}
index={index}
error={this.props.error.steps[index].headline}
errorText={`Gib eine Überschrift für die ${
this.props.step.type === "task" ? "Aufgabe" : "Anleitung"
} ein.`}
/>
<MarkdownEditor
value={this.props.step.text}
property={"text"}
label={
this.props.step.type === "task"
? "Aufgabenstellung"
: "Instruktionen"
}
index={index}
multiline
error={this.props.error.steps[index].text}
errorText={`Gib Instruktionen für die ${
this.props.step.type === "task" ? "Aufgabe" : "Anleitung"
} ein.`}
/>
{index === 0 ? (
<div> <div>
<Requirements value={this.props.step.requirements ? this.props.step.requirements : []} index={index}/> <Requirements
<Hardware value={this.props.step.hardware ? this.props.step.hardware : []} index={index} error={this.props.error.steps[index].hardware}/> value={
this.props.step.requirements
? this.props.step.requirements
: []
}
index={index}
/>
<Hardware
value={
this.props.step.hardware ? this.props.step.hardware : []
}
index={index}
error={this.props.error.steps[index].hardware}
/>
</div> </div>
: null} ) : null}
{this.props.step.type === 'instruction' ? <BlocklyExample
<Media value={this.props.step.media} picture={this.props.step.media && this.props.step.media.picture} youtube={this.props.step.media && this.props.step.media.youtube} url={this.props.step.url} index={index} error={this.props.error.steps[index].media} /> value={this.props.step.xml}
: null} index={index}
<BlocklyExample value={this.props.step.xml} index={index} task={this.props.step.type === 'task'} error={this.props.error.steps[index].xml ? true : false}/> task={this.props.step.type === "task"}
error={this.props.error.steps[index].xml ? true : false}
/>
</div> </div>
</div> </div>
</div> </div>
); );
}; }
} }
Step.propTypes = { Step.propTypes = {
@ -124,10 +200,14 @@ Step.propTypes = {
error: PropTypes.object.isRequired, error: PropTypes.object.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
steps: state.builder.steps, steps: state.builder.steps,
change: state.builder.change, change: state.builder.change,
error: state.builder.error error: state.builder.error,
}); });
export default connect(mapStateToProps, { addStep, removeStep, changeStepIndex })(withStyles(styles, {withTheme: true})(Step)); export default connect(mapStateToProps, {
addStep,
removeStep,
changeStepIndex,
})(withStyles(styles, { withTheme: true })(Step));

View File

@ -31,14 +31,6 @@ const styles = (theme) => ({
}); });
class Textfield extends Component { class Textfield extends Component {
componentDidMount() {
if (this.props.error) {
if (this.props.property !== "media") {
this.props.deleteError(this.props.index, this.props.property);
}
}
}
handleChange = (e) => { handleChange = (e) => {
var value = e.target.value; var value = e.target.value;
if (this.props.property === "title") { if (this.props.property === "title") {

View File

@ -1,47 +1,45 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import Dialog from '../Dialog'; import Dialog from "../Dialog";
import hardware from '../../data/hardware.json'; import hardware from "../../data/hardware.json";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import withWidth, { isWidthDown } from "@material-ui/core/withWidth";
import Link from '@material-ui/core/Link'; import Link from "@material-ui/core/Link";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import GridList from '@material-ui/core/GridList'; import ImageList from "@material-ui/core/ImageList";
import GridListTile from '@material-ui/core/GridListTile'; import ImageListTile from "@material-ui/core/ImageList";
import GridListTileBar from '@material-ui/core/GridListTileBar'; import ImageListTileBar from "@material-ui/core/ImageListItemBar";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExpandAlt } from "@fortawesome/free-solid-svg-icons"; import { faExpandAlt } from "@fortawesome/free-solid-svg-icons";
import * as Blockly from 'blockly' import * as Blockly from "blockly";
const styles = theme => ({ const styles = (theme) => ({
expand: { expand: {
'&:hover': { "&:hover": {
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
'&:active': { "&:active": {
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
color: theme.palette.text.primary color: theme.palette.text.primary,
}, },
multiGridListTile: { multiImageListTile: {
background: theme.palette.primary.main, background: theme.palette.primary.main,
opacity: 0.9, opacity: 0.9,
height: '30px' height: "30px",
},
multiImageListTileTitle: {
color: theme.palette.text.primary,
}, },
multiGridListTileTitle: {
color: theme.palette.text.primary
}
}); });
class Hardware extends Component { class Hardware extends Component {
state = { state = {
open: false, open: false,
hardwareInfo: {} hardwareInfo: {},
}; };
handleClickOpen = (hardwareInfo) => { handleClickOpen = (hardwareInfo) => {
@ -53,36 +51,57 @@ class Hardware extends Component {
}; };
render() { render() {
var cols = isWidthDown('md', this.props.width) ? isWidthDown('sm', this.props.width) ? isWidthDown('xs', this.props.width) ? 2 : 3 : 4 : 6; var cols = isWidthDown("md", this.props.width)
? isWidthDown("sm", this.props.width)
? isWidthDown("xs", this.props.width)
? 2
: 3
: 4
: 6;
return ( return (
<div style={{ marginTop: '10px', marginBottom: '5px' }}> <div style={{ marginTop: "10px", marginBottom: "5px" }}>
<Typography>{Blockly.Msg.tutorials_hardware_head}</Typography> <Typography>{Blockly.Msg.tutorials_hardware_head}</Typography>
<GridList cellHeight={100} cols={cols} spacing={10}> <ImageList rowHeight={100} cols={cols} spacing={10}>
{this.props.picture.map((picture, i) => { {this.props.picture.map((picture, i) => {
var hardwareInfo = hardware.filter(hardware => hardware.id === picture)[0]; var hardwareInfo = hardware.filter(
(hardware) => hardware.id === picture
)[0];
return ( return (
<GridListTile key={i}> <ImageListTile key={i}>
<div style={{ margin: 'auto', width: 'max-content' }}> <div style={{ margin: "auto", width: "max-content" }}>
<img src={`/media/hardware/${hardwareInfo.src}`} alt={hardwareInfo.name} height={100} style={{ cursor: 'pointer' }} onClick={() => this.handleClickOpen(hardwareInfo)} /> <img
src={`/media/hardware/${hardwareInfo.src}`}
alt={hardwareInfo.name}
height={100}
style={{ cursor: "pointer" }}
onClick={() => this.handleClickOpen(hardwareInfo)}
/>
</div> </div>
<GridListTileBar <ImageListTileBar
classes={{ root: this.props.classes.multiGridListTile }} classes={{ root: this.props.classes.multiImageListTile }}
title={ title={
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} className={this.props.classes.multiGridListTileTitle}> <div
style={{ overflow: "hidden", textOverflow: "ellipsis" }}
className={this.props.classes.multiImageListTileTitle}
>
{hardwareInfo.name} {hardwareInfo.name}
</div> </div>
} }
actionIcon={ actionIcon={
<IconButton className={this.props.classes.expand} aria-label='Vollbild' onClick={() => this.handleClickOpen(hardwareInfo)}> <IconButton
className={this.props.classes.expand}
aria-label="Vollbild"
onClick={() => this.handleClickOpen(hardwareInfo)}
>
<FontAwesomeIcon icon={faExpandAlt} size="xs" /> <FontAwesomeIcon icon={faExpandAlt} size="xs" />
</IconButton> </IconButton>
} }
/> />
</GridListTile> </ImageListTile>
) );
})} })}
</GridList> </ImageList>
<Dialog <Dialog
style={{ zIndex: 1500 }} style={{ zIndex: 1500 }}
@ -94,14 +113,26 @@ class Hardware extends Component {
button={Blockly.Msg.button_close} button={Blockly.Msg.button_close}
> >
<div> <div>
<img src={`/media/hardware/${this.state.hardwareInfo.src}`} width="100%" alt={this.state.hardwareInfo.name} /> <img
{Blockly.Msg.tutorials_hardware_moreInformation} <Link rel="noreferrer" target="_blank" href={this.state.hardwareInfo.url} color="primary">{Blockly.Msg.tutorials_hardware_here}</Link>. src={`/media/hardware/${this.state.hardwareInfo.src}`}
width="100%"
alt={this.state.hardwareInfo.name}
/>
{Blockly.Msg.tutorials_hardware_moreInformation}{" "}
<Link
rel="noreferrer"
target="_blank"
href={this.state.hardwareInfo.url}
color="primary"
>
{Blockly.Msg.tutorials_hardware_here}
</Link>
.
</div> </div>
</Dialog> </Dialog>
</div> </div>
); );
}; }
} }
export default withWidth()(withStyles(styles, { withTheme: true })(Hardware)); export default withWidth()(withStyles(styles, { withTheme: true })(Hardware));

View File

@ -235,7 +235,7 @@ WorkspaceName.propTypes = {
workspaceName: PropTypes.func.isRequired, workspaceName: PropTypes.func.isRequired,
setDescription: PropTypes.func.isRequired, setDescription: PropTypes.func.isRequired,
updateProject: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string,
description: PropTypes.string.isRequired, description: PropTypes.string.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
}; };

View File

@ -1,30 +1,26 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import './index.css'; import "./index.css";
import App from './App'; import App from "./App";
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from "./serviceWorker";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing"; import { Integrations } from "@sentry/tracing";
Sentry.init({ Sentry.init({
dsn: "https://ffe5d54461f64c46b4bed5d77c130d6f@o507523.ingest.sentry.io/5598758", dsn: "https://ffe5d54461f64c46b4bed5d77c130d6f@o507523.ingest.sentry.io/5598758",
autoSessionTracking: true, autoSessionTracking: true,
integrations: [ integrations: [new Integrations.BrowserTracing()],
new Integrations.BrowserTracing(),
],
// We recommend adjusting this value in production, or using tracesSampler // We recommend adjusting this value in production, or using tracesSampler
// for finer control // for finer control
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
}); });
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change