From a2c571c1e76bc0ea64b3208b388c747277d12f34 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Fri, 27 Nov 2020 17:21:42 +0100 Subject: [PATCH 01/32] adjustments do mongoDB output --- package.json | 1 + src/actions/tutorialActions.js | 39 ++++++++++---------- src/components/Tutorial/Assessment.js | 6 +-- src/components/Tutorial/Requirement.js | 2 +- src/components/Tutorial/StepperHorizontal.js | 4 +- src/components/Tutorial/StepperVertical.js | 8 ++-- src/components/Tutorial/Tutorial.js | 5 +-- src/components/Tutorial/TutorialHome.js | 4 +- src/store.js | 2 +- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 44ff8d2..2c51390 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "scripts": { "start": "react-scripts start", + "dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:3001\" && npm start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index 58e67d4..c6efc3e 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -5,9 +5,9 @@ import { returnErrors, returnSuccess } from './messageActions'; export const getTutorial = (id) => (dispatch, getState) => { dispatch({type: TUTORIAL_PROGRESS}); - axios.get(`https://api.blockly.sensebox.de/tutorial/${id}`) + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${id}`) .then(res => { - var tutorial = res.data; + var tutorial = res.data.tutorial; existingTutorial(tutorial, getState().tutorial.status).then(status => { dispatch({ type: TUTORIAL_SUCCESS, @@ -30,9 +30,10 @@ export const getTutorial = (id) => (dispatch, getState) => { export const getTutorials = () => (dispatch, getState) => { dispatch({type: TUTORIAL_PROGRESS}); - axios.get(`https://api.blockly.sensebox.de/tutorial`) + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial`) .then(res => { - var tutorials = res.data; + var tutorials = res.data.tutorials; + console.log(tutorials); existingTutorials(tutorials, getState().tutorial.status).then(status => { dispatch({ type: TUTORIAL_SUCCESS, @@ -72,9 +73,9 @@ export const tutorialChange = () => (dispatch) => { export const tutorialCheck = (status, step) => (dispatch, getState) => { var tutorialsStatus = getState().tutorial.status; - var id = getState().tutorial.tutorials[0].id; - var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id); - var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === step.id); + var id = getState().tutorial.tutorials[0]._id; + var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus._id === id); + var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task._id === step._id); tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = { ...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex], type: status @@ -89,13 +90,13 @@ export const tutorialCheck = (status, step) => (dispatch, getState) => { export const storeTutorialXml = (code) => (dispatch, getState) => { var tutorial = getState().tutorial.tutorials[0]; if (tutorial) { - var id = tutorial.id; + var id = tutorial._id; var activeStep = getState().tutorial.activeStep; var steps = tutorial.steps; if (steps && steps[activeStep].type === 'task') { var tutorialsStatus = getState().tutorial.status; - var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id); - var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === steps[activeStep].id); + var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus._id === id); + var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task._id === steps[activeStep]._id); tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = { ...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex], xml: code @@ -123,38 +124,38 @@ const existingTutorials = (tutorials, status) => new Promise(function(resolve, r existingTutorial(tutorial, status).then(status => { newstatus = status; }); - return tutorial.id; + return tutorial._id; }); resolve(existingTutorialIds) }).then(existingTutorialIds => { // deleting old tutorials which do not longer exist if (existingTutorialIds.length > 0) { - status = newstatus.filter(status => existingTutorialIds.indexOf(status.id) > -1); + status = newstatus.filter(status => existingTutorialIds.indexOf(status._id) > -1); } resolve(status); }); }); const existingTutorial = (tutorial, status) => new Promise(function(resolve, reject){ - var tutorialsId = tutorial.id; - var statusIndex = status.findIndex(status => status.id === tutorialsId); + var tutorialsId = tutorial._id; + var statusIndex = status.findIndex(status => status._id === tutorialsId); if (statusIndex > -1) { var tasks = tutorial.steps.filter(step => step.type === 'task'); var existingTaskIds = tasks.map((task, j) => { - var tasksId = task.id; - if (status[statusIndex].tasks.findIndex(task => task.id === tasksId) === -1) { + var tasksId = task._id; + if (status[statusIndex].tasks.findIndex(task => task._id === tasksId) === -1) { // task does not exist - status[statusIndex].tasks.push({ id: tasksId }); + status[statusIndex].tasks.push({ _id: tasksId }); } return tasksId; }); // deleting old tasks which do not longer exist if (existingTaskIds.length > 0) { - status[statusIndex].tasks = status[statusIndex].tasks.filter(task => existingTaskIds.indexOf(task.id) > -1); + status[statusIndex].tasks = status[statusIndex].tasks.filter(task => existingTaskIds.indexOf(task._id) > -1); } } else { - status.push({ id: tutorialsId, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => { return { id: task.id }; }) }); + status.push({ _id: tutorialsId, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => { return { _id: task._id }; }) }); } resolve(status); }); diff --git a/src/components/Tutorial/Assessment.js b/src/components/Tutorial/Assessment.js index 3f62941..14ad23b 100644 --- a/src/components/Tutorial/Assessment.js +++ b/src/components/Tutorial/Assessment.js @@ -25,10 +25,10 @@ class Assessment extends Component { } render() { - var tutorialId = this.props.tutorial.id; + var tutorialId = this.props.tutorial._id; var currentTask = this.props.step; - var status = this.props.status.filter(status => status.id === tutorialId)[0]; - var taskIndex = status.tasks.findIndex(task => task.id === currentTask.id); + var status = this.props.status.filter(status => status._id === tutorialId)[0]; + var taskIndex = status.tasks.findIndex(task => task._id === currentTask._id); var statusTask = status.tasks[taskIndex]; return ( diff --git a/src/components/Tutorial/Requirement.js b/src/components/Tutorial/Requirement.js index 53d2211..8415a78 100644 --- a/src/components/Tutorial/Requirement.js +++ b/src/components/Tutorial/Requirement.js @@ -67,7 +67,7 @@ class Requirement extends Component { {tutorialIds.map((tutorialId, i) => { // title must be provided together with ids // var title = tutorials.filter(tutorial => tutorial.id === tutorialId)[0].title; - var status = this.props.status.filter(status => status.id === tutorialId)[0]; + var status = this.props.status.filter(status => status._id === tutorialId)[0]; var tasks = status.tasks; var error = status.tasks.filter(task => task.type === 'error').length > 0; var success = status.tasks.filter(task => task.type === 'success').length / tasks.length diff --git a/src/components/Tutorial/StepperHorizontal.js b/src/components/Tutorial/StepperHorizontal.js index acfccca..9886336 100644 --- a/src/components/Tutorial/StepperHorizontal.js +++ b/src/components/Tutorial/StepperHorizontal.js @@ -50,9 +50,9 @@ const styles = (theme) => ({ class StepperHorizontal extends Component { render() { - var tutorialId = this.props.tutorial.id; + var tutorialId = this.props.tutorial._id; var tutorialIndex = this.props.currentTutorialIndex; - var status = this.props.status.filter(status => status.id === tutorialId)[0]; + var status = this.props.status.filter(status => status._id === tutorialId)[0]; var tasks = status.tasks; var error = tasks.filter(task => task.type === 'error').length > 0; var success = tasks.filter(task => task.type === 'success').length / tasks.length; diff --git a/src/components/Tutorial/StepperVertical.js b/src/components/Tutorial/StepperVertical.js index 1535e20..19387a4 100644 --- a/src/components/Tutorial/StepperVertical.js +++ b/src/components/Tutorial/StepperVertical.js @@ -61,7 +61,7 @@ class StepperVertical extends Component { } componentDidUpdate(props){ - if (props.tutorial.id !== Number(this.props.match.params.tutorialId)) { + if (props.tutorial._id !== this.props.match.params.tutorialId) { this.props.tutorialStep(0); } } @@ -69,7 +69,7 @@ class StepperVertical extends Component { render() { var steps = this.props.steps; var activeStep = this.props.activeStep; - var tutorialStatus = this.props.status.filter(status => status.id === this.props.tutorial.id)[0]; + var tutorialStatus = this.props.status.filter(status => status._id === this.props.tutorial._id)[0]; return (
{steps.map((step, i) => { - var tasksIndex = tutorialStatus.tasks.findIndex(task => task.id === step.id); + var tasksIndex = tutorialStatus.tasks.findIndex(task => task._id === step._id); var taskType = tasksIndex > -1 ? tutorialStatus.tasks[tasksIndex].type : null; var taskStatus = taskType === 'success' ? 'Success' : taskType === 'error' ? 'Error' : 'Other'; return ( -
{this.props.tutorialStep(i)}}> +
{console.log(i); this.props.tutorialStep(i)}}> {this.props.isLoading ? null : @@ -57,7 +56,7 @@ class Tutorial extends Component { var name = `${detectWhitespacesAndReturnReadableResult(tutorial.title)}_${detectWhitespacesAndReturnReadableResult(step.headline)}`; return(
- + diff --git a/src/components/Tutorial/TutorialHome.js b/src/components/Tutorial/TutorialHome.js index b010e2f..f6bb7b9 100644 --- a/src/components/Tutorial/TutorialHome.js +++ b/src/components/Tutorial/TutorialHome.js @@ -78,14 +78,14 @@ class TutorialHome extends Component {

Tutorial-Übersicht

{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 error = status.tasks.filter(task => task.type === 'error').length > 0; var success = status.tasks.filter(task => task.type === 'success').length / tasks.length var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other'; return ( - + {tutorial.title}
diff --git a/src/store.js b/src/store.js index bf6460e..bd28ae1 100644 --- a/src/store.js +++ b/src/store.js @@ -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__() ) ); From c8f01f8273a0343e2e9fac8dec4b1606cc824b90 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 30 Nov 2020 13:44:05 +0100 Subject: [PATCH 02/32] load pictures from the api server --- src/actions/tutorialBuilderActions.js | 2 +- src/components/Tutorial/Builder/Builder.js | 58 +++++++++++++------ src/components/Tutorial/Builder/Media.js | 2 +- .../Tutorial/Builder/Requirements.js | 6 +- src/components/Tutorial/Instruction.js | 4 +- src/components/Tutorial/Requirement.js | 12 ++-- 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js index 328c7fa..f72e09a 100644 --- a/src/actions/tutorialBuilderActions.js +++ b/src/actions/tutorialBuilderActions.js @@ -189,7 +189,7 @@ export const setSubmitError = () => (dispatch, getState) => { step.id = i + 1; if (i === 0) { if (step.requirements && step.requirements.length > 0) { - var requirements = step.requirements.filter(requirement => typeof (requirement) === 'number'); + var requirements = step.requirements.filter(requirement => /^[0-9a-fA-F]{24}$/.test(requirement)); if (requirements.length < step.requirements.length) { dispatch(changeContent(requirements, i, 'requirements')); } diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 4c88177..788cde9 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -3,8 +3,9 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { checkError, readJSON, jsonString, progress, resetTutorial } from '../../../actions/tutorialBuilderActions'; -import { saveAs } from 'file-saver'; +import axios from 'axios'; +import { saveAs } from 'file-saver'; import { detectWhitespacesAndReturnReadableResult } from '../../../helpers/whitespace'; import Breadcrumbs from '../../Breadcrumbs'; @@ -51,12 +52,6 @@ class Builder extends Component { } submit = () => { - if (this.props.id === null) { - var randomID = Date.now(); - } else { - randomID = this.props.id; - } - var isError = this.props.checkError(); if (isError) { this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error' }); @@ -64,19 +59,46 @@ class Builder extends Component { } else { // export steps without attribute 'url' - var steps = this.props.steps.map(step => { - if (step.url) { - delete step.url; + var steps = this.props.steps; + var newTutorial = new FormData(); + newTutorial.append('title', this.props.title); + steps.forEach((step, i) => { + newTutorial.append(`steps[${i}][type]`, step.type); + newTutorial.append(`steps[${i}][headline]`, step.headline); + newTutorial.append(`steps[${i}][text]`, step.text); + if(i === 0 && step.type === 'instruction'){ + if(step.requirements){ // optional + step.requirements.forEach((requirement, j) => { + newTutorial.append(`steps[${i}][requirements][${j}]`, requirement); + }); + } + step.hardware.forEach((hardware, j) => { + newTutorial.append(`steps[${i}][hardware][${j}]`, hardware); + }); + } + if(step.xml){ // optional + 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 step; }); - var tutorial = { - id: randomID, - title: this.props.title, - steps: steps - } - var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' }); - saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`); + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/`, newTutorial) + .then(res => { + var tutorial = res.data.tutorial; + this.props.history.push(`/tutorial/${tutorial._id}`); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen des Tutorials. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + // var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' }); + // saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`); } } diff --git a/src/components/Tutorial/Builder/Media.js b/src/components/Tutorial/Builder/Media.js index 923aee6..9daea48 100644 --- a/src/components/Tutorial/Builder/Media.js +++ b/src/components/Tutorial/Builder/Media.js @@ -84,7 +84,7 @@ class Media extends Component { this.setState({ error: false }); this.props.changeContent(URL.createObjectURL(pic), this.props.index, 'url'); } - this.props.changeContent(pic.name, this.props.index, 'media', 'picture'); + this.props.changeContent(pic, this.props.index, 'media', 'picture'); } render() { diff --git a/src/components/Tutorial/Builder/Requirements.js b/src/components/Tutorial/Builder/Requirements.js index ede201a..2474fb7 100644 --- a/src/components/Tutorial/Builder/Requirements.js +++ b/src/components/Tutorial/Builder/Requirements.js @@ -34,7 +34,7 @@ class Requirements extends Component { onChange = (e) => { var requirements = this.props.value; - var value = parseInt(e.target.value) + var value = e.target.value; if (e.target.checked) { requirements.push(value); } @@ -55,8 +55,8 @@ class Requirements extends Component { key={i} control={ id === tutorial.id).length > 0} + value={tutorial._id} + checked={this.props.value.filter(id => id === tutorial._id).length > 0} onChange={(e) => this.onChange(e)} name="requirements" color="primary" diff --git a/src/components/Tutorial/Instruction.js b/src/components/Tutorial/Instruction.js index d0ecf02..0cb6aba 100644 --- a/src/components/Tutorial/Instruction.js +++ b/src/components/Tutorial/Instruction.js @@ -22,11 +22,11 @@ class Instruction extends Component { {isHardware ? : null} {areRequirements > 0 ? - : null} + : null} {step.media ? step.media.picture ?
- +
: step.media.youtube ? /*16:9; width: 800px; height: width/16*9=450px*/ diff --git a/src/components/Tutorial/Requirement.js b/src/components/Tutorial/Requirement.js index 8415a78..a3c0c86 100644 --- a/src/components/Tutorial/Requirement.js +++ b/src/components/Tutorial/Requirement.js @@ -59,14 +59,14 @@ const styles = theme => ({ class Requirement extends Component { render() { - var tutorialIds = this.props.tutorialIds; + var requirements = this.props.requirements; + var tutorialIds = requirements.map(requirement => requirement._id); return (
Bevor du mit diesem Tutorial fortfährst solltest du folgende Tutorials erfolgreich abgeschlossen haben: {tutorialIds.map((tutorialId, i) => { - // title must be provided together with ids - // var title = tutorials.filter(tutorial => tutorial.id === tutorialId)[0].title; + var title = requirements[i].title var status = this.props.status.filter(status => status._id === tutorialId)[0]; var tasks = status.tasks; var error = status.tasks.filter(task => task.type === 'error').length > 0; @@ -98,7 +98,7 @@ class Requirement extends Component {
- {/*title*/}Name hinzufügen über Datenbankeintrag + {title}
) @@ -112,12 +112,12 @@ class Requirement extends Component { Requirement.propTypes = { status: PropTypes.array.isRequired, - change: PropTypes.number.isRequired, + change: PropTypes.number.isRequired }; const mapStateToProps = state => ({ change: state.tutorial.change, - status: state.tutorial.status + status: state.tutorial.status, }); export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Requirement))); From d0dda4038e223ff3ba8a6805c13873d1543219f8 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 30 Nov 2020 17:19:32 +0100 Subject: [PATCH 03/32] change existing tutorial with Tutorial-Builder --- src/actions/tutorialBuilderActions.js | 8 +- src/components/Tutorial/Builder/Builder.js | 304 ++++++++++++------ src/components/Tutorial/Builder/Media.js | 3 +- .../Tutorial/Builder/Requirements.js | 33 +- 4 files changed, 225 insertions(+), 123 deletions(-) diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js index f72e09a..b2d6c5c 100644 --- a/src/actions/tutorialBuilderActions.js +++ b/src/actions/tutorialBuilderActions.js @@ -180,7 +180,7 @@ export const setSubmitError = () => (dispatch, getState) => { // if(builder.id === undefined || builder.id === ''){ // dispatch(setError(undefined, 'id')); // } - if (builder.id === undefined || builder.title === '') { + if (builder.title === '') { dispatch(setError(undefined, 'title')); } var type = builder.steps.map((step, i) => { @@ -243,7 +243,7 @@ export const progress = (inProgress) => (dispatch) => { export const resetTutorial = () => (dispatch, getState) => { dispatch(jsonString('')); dispatch(tutorialTitle('')); - dispatch(tutorialId('')); + // dispatch(tutorialId('')); var steps = [ { id: 1, @@ -274,7 +274,7 @@ export const readJSON = (json) => (dispatch, getState) => { // accept only valid attributes var steps = json.steps.map((step, i) => { var object = { - id: step.id, + // id: step.id, type: step.type, headline: step.headline, text: step.text @@ -298,7 +298,7 @@ export const readJSON = (json) => (dispatch, getState) => { return object; }); dispatch(tutorialTitle(json.title)); - dispatch(tutorialId(json.id)); + // dispatch(tutorialId(json.id)); dispatch(tutorialSteps(steps)); dispatch(setSubmitError()); dispatch(progress(false)); diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 788cde9..398c79f 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -1,7 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { checkError, readJSON, jsonString, progress, resetTutorial } from '../../../actions/tutorialBuilderActions'; +import { checkError, readJSON, jsonString, progress, tutorialId, resetTutorial as resetTutorialBuilder} from '../../../actions/tutorialBuilderActions'; +import { getTutorials, resetTutorial} from '../../../actions/tutorialActions'; +import { clearMessages } from '../../../actions/messageActions'; import axios from 'axios'; @@ -20,6 +22,13 @@ import Backdrop from '@material-ui/core/Backdrop'; import CircularProgress from '@material-ui/core/CircularProgress'; import Divider from '@material-ui/core/Divider'; import FormHelperText from '@material-ui/core/FormHelperText'; +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import FormControl from '@material-ui/core/FormControl'; +import Select from '@material-ui/core/Select'; const styles = (theme) => ({ backdrop: { @@ -36,6 +45,7 @@ class Builder extends Component { constructor(props) { super(props); this.state = { + tutorial: 'new', open: false, title: '', content: '', @@ -47,65 +57,23 @@ class Builder extends Component { this.inputRef = React.createRef(); } + componentDidMount() { + this.props.getTutorials(); + } + + componentDidUpdate(props, state) { + if(this.props.message.id === 'GET_TUTORIALS_FAIL'){ + alert(this.props.message.msg); + this.props.clearMessages(); + } + } + componentWillUnmount() { - this.reset(); - } - - submit = () => { - var isError = this.props.checkError(); - if (isError) { - this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error' }); - window.scrollTo(0, 0); - } - else { - // export steps without attribute 'url' - var steps = this.props.steps; - var newTutorial = new FormData(); - newTutorial.append('title', this.props.title); - steps.forEach((step, i) => { - newTutorial.append(`steps[${i}][type]`, step.type); - newTutorial.append(`steps[${i}][headline]`, step.headline); - newTutorial.append(`steps[${i}][text]`, step.text); - if(i === 0 && step.type === 'instruction'){ - if(step.requirements){ // optional - step.requirements.forEach((requirement, j) => { - newTutorial.append(`steps[${i}][requirements][${j}]`, requirement); - }); - } - step.hardware.forEach((hardware, j) => { - newTutorial.append(`steps[${i}][hardware][${j}]`, hardware); - }); - } - if(step.xml){ // optional - 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); - } - } - }); - axios.post(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/`, newTutorial) - .then(res => { - var tutorial = res.data.tutorial; - this.props.history.push(`/tutorial/${tutorial._id}`); - }) - .catch(err => { - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen des Tutorials. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - }); - // var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' }); - // saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`); - } - } - - reset = () => { + this.resetFull(); this.props.resetTutorial(); - this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich zurückgesetzt.`, type: 'success' }); - window.scrollTo(0, 0); + if(this.props.message.msg){ + this.props.clearMessages(); + } } uploadJsonFile = (jsonFile) => { @@ -153,6 +121,104 @@ class Builder extends Component { this.setState({ open: !this.state }); } + onChange = (value) => { + this.props.resetTutorialBuilder(); + this.props.tutorialId(''); + this.setState({tutorial: value}); + } + + onChangeId = (value) => { + this.props.tutorialId(value); + this.props.progress(true); + var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === value)[0]; + this.props.readJSON(tutorial); + this.setState({ snackbar: true, key: Date.now(), message: `Das ausgewählte Tutorial "${tutorial.title}" wurde erfolgreich übernommen.`, type: 'success' }); + } + + resetFull = () => { + this.props.resetTutorialBuilder(); + this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich zurückgesetzt.`, type: 'success' }); + window.scrollTo(0, 0); + } + + resetTutorial = () => { + var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === this.props.id)[0]; + this.props.readJSON(tutorial); + this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial ${tutorial.title} wurde erfolgreich auf den ursprünglichen Stand zurückgesetzt.`, type: 'success' }); + window.scrollTo(0, 0); + } + + submit = () => { + var isError = this.props.checkError(); + if (isError) { + this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error' }); + window.scrollTo(0, 0); + return false; + } + else { + // export steps without attribute 'url' + var steps = this.props.steps; + var newTutorial = new FormData(); + newTutorial.append('title', this.props.title); + steps.forEach((step, i) => { + newTutorial.append(`steps[${i}][type]`, step.type); + newTutorial.append(`steps[${i}][headline]`, step.headline); + newTutorial.append(`steps[${i}][text]`, step.text); + if(i === 0 && step.type === 'instruction'){ + if(step.requirements){ // optional + step.requirements.forEach((requirement, j) => { + newTutorial.append(`steps[${i}][requirements][${j}]`, requirement); + }); + } + step.hardware.forEach((hardware, j) => { + newTutorial.append(`steps[${i}][hardware][${j}]`, hardware); + }); + } + if(step.xml){ // optional + 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; + } + } + + submitNew = () => { + var newTutorial = this.submit(); + if(newTutorial){ + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/`, newTutorial) + .then(res => { + var tutorial = res.data.tutorial; + this.props.history.push(`/tutorial/${tutorial._id}`); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen des Tutorials. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + } + + submitUpdate = () => { + var updatedTutorial = this.submit(); + if(updatedTutorial){ + axios.put(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${this.props.id}`, updatedTutorial) + .then(res => { + var tutorial = res.data.tutorial; + this.props.history.push(`/tutorial/${tutorial._id}`); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Ändern des Tutorials. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + } render() { return ( @@ -161,41 +227,88 @@ class Builder extends Component {

Tutorial-Builder

- {/*upload JSON*/} -
- { this.uploadJsonFile(e.target.files[0]) }} - id="open-json" - type="file" + this.onChange(e.target.value)}> + } + label="neues Tutorial erstellen" + labelPlacement="end" /> - - -
+ } + label="bestehendes Tutorial ändern" + labelPlacement="end" + /> + + - {/*Tutorial-Builder-Form*/} - {this.props.error.type ? - {`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`} - : null} - {/* */} - + {this.state.tutorial === 'new' ? + /*upload JSON*/ +
+ { this.uploadJsonFile(e.target.files[0]) }} + id="open-json" + type="file" + /> + + +
+ : + Tutorial + + + } - {this.props.steps.map((step, i) => - - )} + - {/*submit or reset*/} - - - + {this.state.tutorial === 'new' || (this.state.tutorial === 'change' && this.props.id !== '') ? + /*Tutorial-Builder-Form*/ +
+ {this.props.error.type ? + {`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`} + : null} + {/* */} + - - - + {this.props.steps.map((step, i) => + + )} + + {/*submit or reset*/} + + {this.state.tutorial === 'new' ? +
+ + +
+ :
+ + +
+ } + + + + +
+ : null} ({ @@ -253,7 +373,9 @@ const mapStateToProps = state => ({ change: state.builder.change, error: state.builder.error, json: state.builder.json, - isProgress: state.builder.progress + isProgress: state.builder.progress, + tutorials: state.tutorial.tutorials, + message: state.message }); -export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, resetTutorial })(withStyles(styles, { withTheme: true })(Builder)); +export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages })(withStyles(styles, { withTheme: true })(Builder)); diff --git a/src/components/Tutorial/Builder/Media.js b/src/components/Tutorial/Builder/Media.js index 9daea48..999ece4 100644 --- a/src/components/Tutorial/Builder/Media.js +++ b/src/components/Tutorial/Builder/Media.js @@ -121,8 +121,7 @@ class Media extends Component {
{!this.props.error ?
- {`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.picture}' abgespeichert werden muss.`} - {this.props.url + {''}
:
{ var requirements = this.props.value; var value = e.target.value; @@ -50,7 +32,7 @@ class Requirements extends Component { Voraussetzungen Beachte, dass die Reihenfolge des Anhakens maßgebend ist. - {this.props.tutorials.map((tutorial, i) => + {this.props.tutorials.filter(tutorial => tutorial._id !== this.props.id).map((tutorial, i) => ({ change: state.builder.change, - tutorials: state.tutorial.tutorials, - message: state.message + id: state.builder.id, + tutorials: state.tutorial.tutorials }); -export default connect(mapStateToProps, { changeContent, getTutorials, resetTutorial, clearMessages })(Requirements); +export default connect(mapStateToProps, { changeContent })(Requirements); From 6db8c094ea21270449e97a048fd375fd232166bc Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 1 Dec 2020 13:29:52 +0100 Subject: [PATCH 04/32] create and display content to share --- src/components/Home.js | 90 ++++++++++++++++--------- src/components/WorkspaceFunc.js | 114 ++++++++++++++------------------ 2 files changed, 106 insertions(+), 98 deletions(-) diff --git a/src/components/Home.js b/src/components/Home.js index 7729aad..09a5248 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -5,6 +5,8 @@ import { clearStats, workspaceName } from '../actions/workspaceActions'; import * as Blockly from 'blockly/core'; +import axios from 'axios'; + import WorkspaceStats from './WorkspaceStats'; import WorkspaceFunc from './WorkspaceFunc'; import BlocklyWindow from './Blockly/BlocklyWindow'; @@ -17,6 +19,8 @@ import Grid from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import { withStyles } from '@material-ui/core/styles'; +import Backdrop from '@material-ui/core/Backdrop'; +import CircularProgress from '@material-ui/core/CircularProgress'; import { faCode } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -50,17 +54,28 @@ class Home extends Component { gallery: [], share: [], projectToLoad: undefined, + progress: false, stats: window.localStorage.getItem('stats'), } componentDidMount() { this.setState({ stats: window.localStorage.getItem('stats') }) - this.props.workspaceName(createNameId()); - fetch(process.env.REACT_APP_BLOCKLY_API + this.props.location.pathname) - .then(res => res.json()) - .then((data) => { - this.setState({ projectToLoad: data }) - }) + if(this.props.match.params.shareId || this.props.match.params.galleryId){ + this.setState({progress: true}); + axios.get(`${process.env.REACT_APP_BLOCKLY_API}${this.props.location.pathname}`) + .then(res => { + var shareContent = res.data.content; + this.props.workspaceName(res.data.content.name); + this.setState({ projectToLoad: res.data.content, progress: false }); + }) + .catch(err => { + this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + else { + this.props.workspaceName(createNameId()); + } } @@ -91,35 +106,44 @@ class Home extends Component { render() { return (
- {this.state.stats ? -
- : null - } -
- - - - this.onChange()} - > - - - - - {this.state.projectToLoad ? - < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.state.projectToLoad.xml} /> : < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> - } + {this.state.progress ? + + + + : +
+ {this.state.stats ? +
+ : null + } +
+ + + + this.onChange()} + > + + + + + {this.state.projectToLoad ? + < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.state.projectToLoad.xml} /> + : < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> + } - - {this.state.codeOn ? - - - : null} - - + {this.state.codeOn ? + + + + : null} + + +
+ }
); }; diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index 253f9b4..c78c4f8 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -5,7 +5,9 @@ import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceAct import * as Blockly from 'blockly/core'; +import axios from 'axios'; import { saveAs } from 'file-saver'; +import { createId } from 'mnemonic-id'; import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; import { initialXml } from './Blockly/initialXml.js'; @@ -14,6 +16,8 @@ import Compile from './Compile'; import SolutionCheck from './Tutorial/SolutionCheck'; import Snackbar from './Snackbar'; +import { Link } from 'react-router-dom'; + import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import { withStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; @@ -21,7 +25,6 @@ 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 { createId } from 'mnemonic-id'; import Dialog from './Dialog'; @@ -33,7 +36,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'; -import { faPen, faSave, faUpload, faCamera, faShare, faShareAlt } from "@fortawesome/free-solid-svg-icons"; +import { faPen, faSave, faUpload, faCamera, faShare, faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -55,6 +58,14 @@ const styles = (theme) => ({ '&:hover': { color: theme.palette.primary.main, } + }, + link: { + color: theme.palette.primary.main, + textDecoration: 'none', + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline' + } } }); @@ -74,14 +85,13 @@ class WorkspaceFunc extends Component { share: false, name: props.name, snackbar: false, + type: '', key: '', message: '', id: '' }; } - - componentDidUpdate(props) { if (props.name !== this.props.name) { this.setState({ name: this.props.name }); @@ -89,7 +99,7 @@ class WorkspaceFunc extends Component { } toggleDialog = () => { - this.setState({ open: !this.state, share: false }); + this.setState({ open: !this.state, share: false, file: false, saveFile: false, title: '', content: '' }); } saveXmlFile = () => { @@ -103,38 +113,20 @@ class WorkspaceFunc extends Component { } shareBlocks = () => { - let code = this.props.xml; - let requestOptions = ''; - let id = ''; - if (this.state.id !== '') { - requestOptions = { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: this.state.id, - name: this.state.name, - xml: code - }) - }; - fetch(process.env.REACT_APP_BLOCKLY_API + '/share' + this.state.id, requestOptions) - .then(response => response.json()) - .then(data => this.setState({ share: true })); - } - else { - id = createId(10); - requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id: id, - name: this.state.name, - xml: code - }) - }; - fetch(process.env.REACT_APP_BLOCKLY_API + '/share', requestOptions) - .then(response => response.json()) - .then(data => this.setState({ id: data.id, share: true })); - } + var body = { + _id: createId(10), + name: this.state.name, + xml: this.props.xml + }; + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) + .then(res => { + var shareContent = res.data.content; + this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent._id }); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); } getSvg = () => { @@ -220,7 +212,7 @@ class WorkspaceFunc extends Component { var extensionPosition = xmlFile.name.lastIndexOf('.'); this.props.workspaceName(xmlFile.name.substr(0, extensionPosition)); } - this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' }); + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' }); } } 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.' }); @@ -232,7 +224,7 @@ class WorkspaceFunc extends Component { renameWorkspace = () => { this.props.workspaceName(this.state.name); this.toggleDialog(); - this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); } resetWorkspace = () => { @@ -248,7 +240,7 @@ class WorkspaceFunc extends Component { if (!this.props.solutionCheck) { this.props.workspaceName(null); } - this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' }); + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' }); } @@ -320,29 +312,6 @@ class WorkspaceFunc extends Component { - - Dein Link wurde erstellt. - - - Über den folgenden Link kannst du dein Programm teilen. - - - - - - - -
- : null} + : this.state.share ? +
+ Über den folgenden Link kannst du dein Programm teilen: + {`${window.location.origin}/share/${this.state.id}`} + + { + navigator.clipboard.writeText(`${window.location.origin}/share/${this.state.id}`); + this.setState({ snackbar: true, key: Date.now(), message: 'Link erfolgreich in Zwischenablage gespeichert.', type: 'success' }); + }} + > + + + +
+ : null}
From 2dfd146d1854b7853ef0f28ad8198e796fa7cf1d Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 1 Dec 2020 18:01:52 +0100 Subject: [PATCH 05/32] display gallery content --- package.json | 1 - src/components/Blockly/BlocklySvg.js | 3 +- src/components/Gallery/GalleryHome.js | 148 ++++++++----------------- src/components/Home.js | 45 ++++---- src/components/Tutorial/Instruction.js | 2 +- src/components/WorkspaceFunc.js | 6 +- 6 files changed, 76 insertions(+), 129 deletions(-) diff --git a/package.json b/package.json index 2c51390..e3e4f1c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "axios": "^0.21.0", "blockly": "^3.20200924.0", "file-saver": "^2.0.2", - "mnemonic-id": "^3.2.7", "moment": "^2.28.0", "prismjs": "^1.20.0", "react": "^16.13.1", diff --git a/src/components/Blockly/BlocklySvg.js b/src/components/Blockly/BlocklySvg.js index 84a4c00..739b052 100644 --- a/src/components/Blockly/BlocklySvg.js +++ b/src/components/Blockly/BlocklySvg.js @@ -53,7 +53,6 @@ class BlocklySvg extends Component { var css = ''; var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox(); var content = new XMLSerializer().serializeToString(canvas); - var xml = ` ${css}">${content}`; @@ -65,7 +64,7 @@ class BlocklySvg extends Component { render() { return (
); diff --git a/src/components/Gallery/GalleryHome.js b/src/components/Gallery/GalleryHome.js index 6e6772a..ea142e6 100644 --- a/src/components/Gallery/GalleryHome.js +++ b/src/components/Gallery/GalleryHome.js @@ -1,118 +1,62 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import clsx from 'clsx'; - -import Breadcrumbs from '../Breadcrumbs'; +import axios from 'axios'; import { Link } from 'react-router-dom'; -import { fade } from '@material-ui/core/styles/colorManipulator'; -import { withStyles } from '@material-ui/core/styles'; +import Breadcrumbs from '../Breadcrumbs'; +import BlocklyWindow from '../Blockly/BlocklyWindow'; + import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; -import BlocklyWindow from '../Blockly/BlocklyWindow'; import Divider from '@material-ui/core/Divider'; - - -const styles = (theme) => ({ - outerDiv: { - position: 'absolute', - right: '-30px', - bottom: '-30px', - width: '160px', - height: '160px', - color: fade(theme.palette.secondary.main, 0.6) - }, - outerDivError: { - stroke: fade(theme.palette.error.dark, 0.6), - color: fade(theme.palette.error.dark, 0.6) - }, - outerDivSuccess: { - stroke: fade(theme.palette.primary.main, 0.6), - color: fade(theme.palette.primary.main, 0.6) - }, - outerDivOther: { - stroke: fade(theme.palette.secondary.main, 0.6) - }, - innerDiv: { - width: 'inherit', - height: 'inherit', - display: 'table-cell', - verticalAlign: 'middle', - textAlign: 'center' - } -}); - - +import Typography from '@material-ui/core/Typography'; class GalleryHome extends Component { - state = { - gallery: [] - } + state = { + gallery: [] + } - componentDidMount() { - fetch(process.env.REACT_APP_BLOCKLY_API + this.props.location.pathname) - .then(res => res.json()) - .then((data) => { - this.setState({ gallery: data }) - }) - } + componentDidMount() { + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/gallery`) + .then(res => { + this.setState({ gallery: res.data.galleries }); + }) + .catch(err => { + // TODO: + }); + } + render() { + return ( +
+ - render() { - return ( -
- - -

Gallery

- - {this.state.gallery.map((gallery, i) => { - return ( - - - -

{gallery.title}

- - - - - - -

{gallery.text}

- - -
-
-
- -
- ) - })} -
-
- ); - }; +

Gallery

+ + {this.state.gallery.map((gallery, i) => { + return ( + + + +

{gallery.title}

+ + + {gallery.description} +
+ +
+ ) + })} +
+
+ ); + }; } -GalleryHome.propTypes = { - status: PropTypes.array.isRequired, - change: PropTypes.number.isRequired, -}; - -const mapStateToProps = state => ({ - change: state.tutorial.change, - status: state.tutorial.status -}); - -export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(GalleryHome)); +export default GalleryHome; diff --git a/src/components/Home.js b/src/components/Home.js index 09a5248..a28618e 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -60,31 +60,17 @@ class Home extends Component { componentDidMount() { this.setState({ stats: window.localStorage.getItem('stats') }) - if(this.props.match.params.shareId || this.props.match.params.galleryId){ - this.setState({progress: true}); - axios.get(`${process.env.REACT_APP_BLOCKLY_API}${this.props.location.pathname}`) - .then(res => { - var shareContent = res.data.content; - this.props.workspaceName(res.data.content.name); - this.setState({ projectToLoad: res.data.content, progress: false }); - }) - .catch(err => { - this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - }); - } - else { - this.props.workspaceName(createNameId()); - } + this.getProject(); } - - componentDidUpdate() { + componentDidUpdate(props) { + if(props.location.path !== this.props.location.path){ + this.getProject(); + } /* Resize and reposition all of the workspace chrome (toolbox, trash, scrollbars etc.) This should be called when something changes that requires recalculating dimensions and positions of the trash, zoom, toolbox, etc. (e.g. window resize). */ - const workspace = Blockly.getMainWorkspace(); Blockly.svgResize(workspace); } @@ -94,6 +80,27 @@ class Home extends Component { this.props.workspaceName(null); } + getProject = () => { + if(this.props.match.params.shareId || this.props.match.params.galleryId){ + var param = this.props.match.params.shareId ? 'share' : 'gallery'; + var id = this.props.match.params[`${param}Id`]; + this.setState({progress: true}); + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${param}/${id}`) + .then(res => { + this.props.workspaceName(res.data[param].name ? res.data[param].name : res.data[param].title); + this.setState({ projectToLoad: res.data[param], progress: false }); + }) + .catch(err => { + // TODO: + this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Aufrufen des angeforderten Programms. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + else { + this.props.workspaceName(createNameId()); + } + } + onChange = () => { this.setState({ codeOn: !this.state.codeOn }); const workspace = Blockly.getMainWorkspace(); diff --git a/src/components/Tutorial/Instruction.js b/src/components/Tutorial/Instruction.js index 0cb6aba..ade6dfb 100644 --- a/src/components/Tutorial/Instruction.js +++ b/src/components/Tutorial/Instruction.js @@ -39,7 +39,7 @@ class Instruction extends Component { : null} {step.xml ? - + { var body = { - _id: createId(10), name: this.state.name, xml: this.props.xml }; axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) .then(res => { var shareContent = res.data.content; - this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent._id }); + this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent.link }); }) .catch(err => { this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); @@ -299,7 +297,7 @@ class WorkspaceFunc extends Component { : null} - + this.resetWorkspace()} From 0d4072ab176dd360e1a72537c941ed83f1dc641e Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 1 Dec 2020 19:05:14 +0100 Subject: [PATCH 06/32] redirection to home if no share or gallery content is available --- src/components/Home.js | 14 ++++++++++++-- src/components/WorkspaceFunc.js | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Home.js b/src/components/Home.js index a28618e..432ec06 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -87,12 +87,22 @@ class Home extends Component { this.setState({progress: true}); axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${param}/${id}`) .then(res => { - this.props.workspaceName(res.data[param].name ? res.data[param].name : res.data[param].title); - this.setState({ projectToLoad: res.data[param], progress: false }); + var data = param === 'share' ? 'content' : param; + if(res.data[data]){ + this.props.workspaceName(res.data[data].name ? res.data[data].name : res.data[data].title); + this.setState({ projectToLoad: res.data[data], progress: false }); + } + else { + this.props.workspaceName(createNameId()); + this.setState({ progress: false }); + this.props.history.push('/'); + } }) .catch(err => { // TODO: this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Aufrufen des angeforderten Programms. Versuche es noch einmal.`, type: 'error' }); + this.props.workspaceName(createNameId()); + this.props.history.push('/'); window.scrollTo(0, 0); }); } diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index 6417191..ae3af26 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -332,7 +332,7 @@ class WorkspaceFunc extends Component { : this.state.share ?
Über den folgenden Link kannst du dein Programm teilen: - {`${window.location.origin}/share/${this.state.id}`} + this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`} { From e600c196799d14523d5bd0c09dd985f079e7d873 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 1 Dec 2020 19:35:53 +0100 Subject: [PATCH 07/32] new script: npm run dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3e4f1c..ad72591 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "scripts": { "start": "react-scripts start", - "dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:3001\" && npm start", + "dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:8080\" && npm start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" From a8e36a4f069e967723a6e858fbf17f8f96d94609 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 2 Dec 2020 12:42:45 +0100 Subject: [PATCH 08/32] create and display projects --- src/components/Compile.js | 4 +- src/components/Gallery/gallery.json | 37 ------ src/components/Home.js | 117 +++++------------- src/components/Navbar.js | 8 +- src/components/Project/Project.js | 83 +++++++++++++ .../GalleryHome.js => Project/ProjectHome.js} | 37 ++++-- src/components/Routes.js | 22 +++- src/components/WorkspaceFunc.js | 58 +++++---- 8 files changed, 203 insertions(+), 163 deletions(-) delete mode 100644 src/components/Gallery/gallery.json create mode 100644 src/components/Project/Project.js rename src/components/{Gallery/GalleryHome.js => Project/ProjectHome.js} (53%) diff --git a/src/components/Compile.js b/src/components/Compile.js index 946466f..7fe5932 100644 --- a/src/components/Compile.js +++ b/src/components/Compile.js @@ -99,7 +99,7 @@ class Compile extends Component { 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\'.' }); + this.setState({ file: true, open: true, title: 'Projekt kompilieren', content: 'Bitte gib einen Namen für die Bennenung des zu kompilierenden Programms ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }); } } @@ -111,7 +111,7 @@ class Compile extends Component { return (
{this.props.iconButton ? - + this.compile()} diff --git a/src/components/Gallery/gallery.json b/src/components/Gallery/gallery.json deleted file mode 100644 index 0ff72ae..0000000 --- a/src/components/Gallery/gallery.json +++ /dev/null @@ -1,37 +0,0 @@ -[ - { - "id": 15212, - "title": "Das senseBox Buch Kapitel 1", - "name": "Mario", - "text": "Die Blöcke findest du in der Kategorie \"Schleifen\". Die einfachste Schleife, die du Verwenden kannst, ist der Block \"Wiederhole 10 mal\". Bei diesem Block kannst du die Blöcke, die eine bestimmte Zahl wiederholt werden soll einfach in den offenen Block abschnitt ziehen. ", - "xml": "\n \n \n \n 10\n \n \n \n" - }, - { - "id": 25451, - "title": "Das senseBox Buch Kapitel 2", - "name": "Mario", - "text": "", - "xml": "\n \n \n \n 1\n HIGH\n \n \n \n \n 1000\n \n \n \n \n 1\n LOW\n \n \n \n \n 1000\n \n \n \n \n \n \n \n \n \n \n \n" - }, - { - "id": 3541512, - "title": "Das senseBox Buch Kapitel 3", - "name": "Mario", - "text": "", - "xml": "\n \n \n \n \n \n \n \n \n WHITE,BLACK\n \n \n 1\n \n \n \n \n 0\n \n \n \n \n 0\n \n \n \n \n \n \n \n Helligkeit:\n \n \n \n \n Illuminance\n \n \n \n \n \n \n \n \n \n" - }, - { - "id": 7487454, - "title": "Das senseBox Buch Kapitel 4", - "name": "Mario", - "text": "", - "xml": "\n \n \n \n \n \n \n \n \n WHITE,BLACK\n \n \n 1\n \n \n \n \n 0\n \n \n \n \n 0\n \n \n \n \n \n \n \n Helligkeit:\n \n \n \n \n Illuminance\n \n \n \n \n \n \n \n \n \n" - }, - { - "id": 54541251, - "title": "Das senseBox Buch Kapitel 5", - "name": "Mario", - "text": "", - "xml": "" - } -] \ No newline at end of file diff --git a/src/components/Home.js b/src/components/Home.js index 432ec06..3bce82c 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -4,23 +4,19 @@ import { connect } from 'react-redux'; import { clearStats, workspaceName } from '../actions/workspaceActions'; import * as Blockly from 'blockly/core'; - -import axios from 'axios'; +import { createNameId } from 'mnemonic-id'; import WorkspaceStats from './WorkspaceStats'; import WorkspaceFunc from './WorkspaceFunc'; import BlocklyWindow from './Blockly/BlocklyWindow'; import CodeViewer from './CodeViewer'; import TrashcanButtons from './TrashcanButtons'; -import { createNameId } from 'mnemonic-id'; import HintTutorialExists from './Tutorial/HintTutorialExists'; import Grid from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import { withStyles } from '@material-ui/core/styles'; -import Backdrop from '@material-ui/core/Backdrop'; -import CircularProgress from '@material-ui/core/CircularProgress'; import { faCode } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -51,22 +47,17 @@ class Home extends Component { state = { codeOn: false, - gallery: [], - share: [], - projectToLoad: undefined, - progress: false, - stats: window.localStorage.getItem('stats'), + stats: window.localStorage.getItem('stats') } componentDidMount() { - this.setState({ stats: window.localStorage.getItem('stats') }) - this.getProject(); + this.setState({ stats: window.localStorage.getItem('stats') }); + if(!this.props.project){ + this.props.workspaceName(createNameId()); + } } componentDidUpdate(props) { - if(props.location.path !== this.props.location.path){ - this.getProject(); - } /* Resize and reposition all of the workspace chrome (toolbox, trash, scrollbars etc.) This should be called when something changes that requires recalculating dimensions and positions of the trash, zoom, toolbox, etc. @@ -80,37 +71,6 @@ class Home extends Component { this.props.workspaceName(null); } - getProject = () => { - if(this.props.match.params.shareId || this.props.match.params.galleryId){ - var param = this.props.match.params.shareId ? 'share' : 'gallery'; - var id = this.props.match.params[`${param}Id`]; - this.setState({progress: true}); - axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${param}/${id}`) - .then(res => { - var data = param === 'share' ? 'content' : param; - if(res.data[data]){ - this.props.workspaceName(res.data[data].name ? res.data[data].name : res.data[data].title); - this.setState({ projectToLoad: res.data[data], progress: false }); - } - else { - this.props.workspaceName(createNameId()); - this.setState({ progress: false }); - this.props.history.push('/'); - } - }) - .catch(err => { - // TODO: - this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Aufrufen des angeforderten Programms. Versuche es noch einmal.`, type: 'error' }); - this.props.workspaceName(createNameId()); - this.props.history.push('/'); - window.scrollTo(0, 0); - }); - } - else { - this.props.workspaceName(createNameId()); - } - } - onChange = () => { this.setState({ codeOn: !this.state.codeOn }); const workspace = Blockly.getMainWorkspace(); @@ -123,44 +83,35 @@ class Home extends Component { render() { return (
- {this.state.progress ? - - - - : -
- {this.state.stats ? -
- : null - } -
- - - - this.onChange()} - > - - - - - {this.state.projectToLoad ? - < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.state.projectToLoad.xml} /> - : < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> - } - - - {this.state.codeOn ? - - - - : null} + {this.state.stats ? +
+ : null + } +
+ + + + this.onChange()} + > + + + + + {this.props.project ? + < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.props.project} /> + : < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> + } - -
- } + {this.state.codeOn ? + + + + : null} + +
); }; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index a7137ed..f1d037b 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -20,7 +20,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import LinearProgress from '@material-ui/core/LinearProgress'; -import { faBars, faChevronLeft, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons"; +import { faBars, faChevronLeft, faLayerGroup, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -99,7 +99,11 @@ class Navbar extends Component {
- {[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" }, { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" }, { text: 'Gallery', icon: faLightbulb, link: "/gallery" }, { text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => ( + {[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" }, + { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" }, + { text: 'Galerie', icon: faLightbulb, link: "/gallery" }, + { text: 'Projekte', icon: faLayerGroup, link: "/project" }, + { text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => ( diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js new file mode 100644 index 0000000..da813e6 --- /dev/null +++ b/src/components/Project/Project.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { workspaceName } from '../../actions/workspaceActions'; + +import axios from 'axios'; +import { createNameId } from 'mnemonic-id'; + +import Home from '../Home'; +import Breadcrumbs from '../Breadcrumbs'; + +import Backdrop from '@material-ui/core/Backdrop'; +import CircularProgress from '@material-ui/core/CircularProgress'; + + +class Project extends Component { + + state = { + project: {}, + progress: false, + type: '' + } + + componentDidMount() { + this.getProject(); + } + + componentDidUpdate(props) { + if(props.location.path !== this.props.location.path){ + this.getProject(); + } + } + + getProject = () => { + var param = this.props.match.params.shareId ? 'share' : this.props.match.params.galleryId ? 'gallery' : 'project'; + this.setState({ type: param, progress: true }); + var id = this.props.match.params[`${param}Id`]; + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${param}/${id}`) + .then(res => { + var data = param === 'share' ? 'content' : param; + if(res.data[data]){ + this.props.workspaceName(res.data[data].title); + this.setState({ project: res.data[data], progress: false }); + } + else { + this.props.workspaceName(createNameId()); + this.setState({ progress: false }); + this.props.history.push('/'); + } + }) + .catch(err => { + // TODO: + this.setState({ progress: false, snackbar: true, key: Date.now(), message: `Fehler beim Aufrufen des angeforderten Programms. Versuche es noch einmal.`, type: 'error' }); + this.props.workspaceName(createNameId()); + this.props.history.push('/'); + window.scrollTo(0, 0); + }); + } + + render() { + var data = this.state.type === 'project' ? 'Projekte' : 'Galerie'; + return ( + this.state.progress ? + + + + : +
+ {this.state.type !== 'share' ? + + : null} + +
+ ); + }; +} + +Project.propTypes = { + workspaceName: PropTypes.func.isRequired +}; + + +export default connect(null, { workspaceName })(Project); diff --git a/src/components/Gallery/GalleryHome.js b/src/components/Project/ProjectHome.js similarity index 53% rename from src/components/Gallery/GalleryHome.js rename to src/components/Project/ProjectHome.js index ea142e6..1dd7708 100644 --- a/src/components/Gallery/GalleryHome.js +++ b/src/components/Project/ProjectHome.js @@ -12,16 +12,28 @@ import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; -class GalleryHome extends Component { +class ProjectHome extends Component { state = { - gallery: [] + projects: [] } componentDidMount() { - axios.get(`${process.env.REACT_APP_BLOCKLY_API}/gallery`) + this.getProjects(); + } + + componentDidUpdate(props) { + if(props.match.path !== this.props.match.path){ + this.setState({projects: []}); + this.getProjects(); + } + } + + getProjects = () => { + var data = this.props.match.path === '/project' ? 'projects' : 'galleries'; + axios.get(`${process.env.REACT_APP_BLOCKLY_API}${this.props.match.path}`) .then(res => { - this.setState({ gallery: res.data.galleries }); + this.setState({ projects: res.data[data] }); }) .catch(err => { // TODO: @@ -29,25 +41,26 @@ class GalleryHome extends Component { } render() { + var data = this.props.match.path === '/project' ? 'Projekte' : 'Galerie'; return (
- + -

Gallery

+

{data}

- {this.state.gallery.map((gallery, i) => { + {this.state.projects.map((project, i) => { return ( - + -

{gallery.title}

+

{project.title}

- {gallery.description} + {project.description}
@@ -59,4 +72,4 @@ class GalleryHome extends Component { }; } -export default GalleryHome; +export default ProjectHome; diff --git a/src/components/Routes.js b/src/components/Routes.js index cf6b559..4dd5acc 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -10,7 +10,8 @@ import Tutorial from './Tutorial/Tutorial'; import TutorialHome from './Tutorial/TutorialHome'; import Builder from './Tutorial/Builder/Builder'; import NotFound from './NotFound'; -import GalleryHome from './Gallery/GalleryHome'; +import ProjectHome from './Project/ProjectHome'; +import Project from './Project/Project'; import Settings from './Settings/Settings'; import Impressum from './Impressum'; import Privacy from './Privacy'; @@ -27,15 +28,24 @@ class Routes extends Component {
+ // Tutorials + + + // Sharing + + // Gallery-Projects + + + // User-Projects + + + // settings - - + // privacy - - - + // Not Found
diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index ae3af26..86b3a55 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -5,6 +5,7 @@ import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceAct import * as Blockly from 'blockly/core'; +import { withRouter } from 'react-router-dom'; import axios from 'axios'; import { saveAs } from 'file-saver'; @@ -14,6 +15,7 @@ import { initialXml } from './Blockly/initialXml.js'; import Compile from './Compile'; import SolutionCheck from './Tutorial/SolutionCheck'; import Snackbar from './Snackbar'; +import Dialog from './Dialog'; import { Link } from 'react-router-dom'; @@ -25,17 +27,7 @@ import Tooltip from '@material-ui/core/Tooltip'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; - -import Dialog from './Dialog'; -// import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; - - - -import { faPen, faSave, faUpload, faCamera, faShare, faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faPen, faSave, faUpload, faFileDownload, faCamera, faShare, faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -101,7 +93,23 @@ class WorkspaceFunc extends Component { this.setState({ open: !this.state, share: false, file: false, saveFile: false, title: '', content: '' }); } - saveXmlFile = () => { + saveProject = () => { + var body = { + xml: this.props.xml, + title: this.props.name + }; + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/project`, body) + .then(res => { + var project = res.data.project; + this.props.history.push(`/project/${project._id}`); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Speichern des Projektes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + + downloadXmlFile = () => { var code = this.props.xml; this.toggleDialog(); var fileName = detectWhitespacesAndReturnReadableResult(this.state.name); @@ -173,10 +181,10 @@ class WorkspaceFunc extends Component { createFileName = (filetype) => { this.setState({ file: filetype }, () => { if (this.state.name) { - this.state.file === 'xml' ? this.saveXmlFile() : this.getSvg() + this.state.file === 'xml' ? this.downloadXmlFile() : this.getSvg() } else { - this.setState({ saveFile: true, file: filetype, open: true, title: this.state.file === 'xml' ? 'Blöcke speichern' : 'Screenshot erstellen', content: `Bitte gib einen Namen für die Bennenung der ${this.state.file === 'xml' ? 'XML' : 'SVG'}-Datei ein und bestätige diesen mit einem Klick auf 'Eingabe'.` }); + this.setState({ saveFile: true, file: filetype, open: true, title: this.state.file === 'xml' ? 'Projekt herunterladen' : 'Screenshot erstellen', content: `Bitte gib einen Namen für die Bennenung der ${this.state.file === 'xml' ? 'XML' : 'SVG'}-Datei ein und bestätige diesen mit einem Klick auf 'Eingabe'.` }); } }); } @@ -247,7 +255,7 @@ class WorkspaceFunc extends Component { return (
{!this.props.assessment ? - +
{ this.setState({ file: true, open: true, saveFile: 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) ? {this.props.name} : null}
@@ -257,12 +265,20 @@ class WorkspaceFunc extends Component { : null} {this.props.assessment ? : } - + + this.saveProject()} + > + + + + { this.createFileName('xml'); }} > - + {!this.props.assessment? @@ -275,7 +291,7 @@ class WorkspaceFunc extends Component { type="file" />
); }; @@ -119,8 +133,13 @@ class Home extends Component { Home.propTypes = { clearStats: PropTypes.func.isRequired, - workspaceName: PropTypes.func.isRequired + workspaceName: PropTypes.func.isRequired, + message: PropTypes.object.isRequired }; +const mapStateToProps = state => ({ + message: state.message +}); -export default connect(null, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home)); + +export default connect(mapStateToProps, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home)); diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 3e7244d..8a72713 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { workspaceName } from '../../actions/workspaceActions'; import { getProject, resetProject } from '../../actions/projectActions'; -import { clearMessages } from '../../actions/messageActions'; +import { clearMessages, returnErrors } from '../../actions/messageActions'; import axios from 'axios'; import { createNameId } from 'mnemonic-id'; @@ -18,6 +18,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; class Project extends Component { componentDidMount() { + this.props.resetProject(); this.getProject(); } @@ -31,8 +32,13 @@ class Project extends Component { } if(this.props.message !== props.message){ if(this.props.message.id === 'PROJECT_EMPTY' || this.props.message.id === 'GET_PROJECT_FAIL'){ - this.props.workspaceName(createNameId()); - this.props.history.push('/'); + if(this.props.type!=='share'){ + this.props.returnErrors('', 404, 'GET_PROJECT_FAIL'); + this.props.history.push(`/${this.props.type}`); + } else { + this.props.history.push('/'); + this.props.returnErrors('', 404, 'GET_SHARE_FAIL'); + } } if(this.props.message.id === 'GET_PROJECT_SUCCESS'){ this.props.workspaceName(this.props.project.title); @@ -43,9 +49,6 @@ class Project extends Component { componentWillUnmount() { this.props.resetProject(); this.props.workspaceName(null); - if(this.props.message.msg){ - this.props.clearMessages(); - } } getProject = () => { @@ -77,6 +80,7 @@ Project.propTypes = { getProject: PropTypes.func.isRequired, resetProject: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired, + returnErrors: PropTypes.func.isRequired, project: PropTypes.object.isRequired, type: PropTypes.string.isRequired, message: PropTypes.object.isRequired, @@ -90,4 +94,4 @@ const mapStateToProps = state => ({ message: state.message }); -export default connect(mapStateToProps, { workspaceName, getProject, resetProject, clearMessages })(Project); +export default connect(mapStateToProps, { workspaceName, getProject, resetProject, clearMessages, returnErrors })(Project); diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index 907b846..2cd32c9 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -2,36 +2,60 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { getProjects, resetProject } from '../../actions/projectActions'; +import { clearMessages } from '../../actions/messageActions'; import axios from 'axios'; import { Link } from 'react-router-dom'; import Breadcrumbs from '../Breadcrumbs'; import BlocklyWindow from '../Blockly/BlocklyWindow'; +import Snackbar from '../Snackbar'; import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import Divider from '@material-ui/core/Divider'; import Typography from '@material-ui/core/Typography'; +import Backdrop from '@material-ui/core/Backdrop'; +import CircularProgress from '@material-ui/core/CircularProgress'; class ProjectHome extends Component { + state = { + snackbar: false, + type: '', + key: '', + message: '' + } + componentDidMount() { - this.props.getProjects(this.props.match.path.replace('/','')); + var type = this.props.match.path.replace('/',''); + this.props.getProjects(type); + if(this.props.message){ + if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Dein Projekt wurde erfolgreich gelöscht.`, type: 'success' }); + } + else if(this.props.message.id === 'GALLERY_DELETE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Dein Galerie-Projekt wurde erfolgreich gelöscht.`, type: 'success' }); + } + else if(this.props.message.id === 'GET_PROJECT_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Dein angefragtes ${type === 'gallery' ? 'Galerie-':''}Projekt konnte nicht gefunden werden.`, type: 'error' }); + } + } } componentDidUpdate(props) { if(props.match.path !== this.props.match.path){ + this.setState({snackbar: false}); this.props.getProjects(this.props.match.path.replace('/','')); } } componentWillUnmount() { this.props.resetProject(); + this.props.clearMessages(); } - render() { var data = this.props.match.path === '/project' ? 'Projekte' : 'Galerie'; return ( @@ -39,7 +63,11 @@ class ProjectHome extends Component {

{data}

- {this.props.progress ? null : + {this.props.progress ? + + + + : {this.props.projects.map((project, i) => { return ( @@ -60,6 +88,12 @@ class ProjectHome extends Component { ) })} } +
); }; @@ -68,14 +102,17 @@ class ProjectHome extends Component { ProjectHome.propTypes = { getProjects: PropTypes.func.isRequired, resetProject: PropTypes.func.isRequired, + clearMessages: PropTypes.func.isRequired, projects: PropTypes.array.isRequired, - progress: PropTypes.bool.isRequired + progress: PropTypes.bool.isRequired, + message: PropTypes.object.isRequired }; const mapStateToProps = state => ({ projects: state.project.projects, - progress: state.project.progress + progress: state.project.progress, + message: state.message }); -export default connect(mapStateToProps, { getProjects, resetProject })(ProjectHome); +export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(ProjectHome); diff --git a/src/components/Routes.js b/src/components/Routes.js index 4dd5acc..54b4ac7 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -30,8 +30,8 @@ class Routes extends Component { // Tutorials - + // Sharing // Gallery-Projects diff --git a/src/components/Snackbar.js b/src/components/Snackbar.js index 16fd4c2..55e7429 100644 --- a/src/components/Snackbar.js +++ b/src/components/Snackbar.js @@ -35,6 +35,12 @@ class Snackbar extends Component { } } + componentDidUpdate(){ + if(!this.state.open){ + clearTimeout(this.timeout); + } + } + componentWillUnmount(){ if(this.state.open){ clearTimeout(this.timeout); diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index bc0879e..54013d8 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions'; -import { updateProject } from '../actions/projectActions'; +import { updateProject, deleteProject } from '../actions/projectActions'; import * as Blockly from 'blockly/core'; @@ -28,7 +28,7 @@ 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, faFileDownload, faCamera, faShare, faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { faPen, faSave, faUpload, faFileDownload, faTrashAlt, faCamera, faShare, faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -51,6 +51,16 @@ const styles = (theme) => ({ color: theme.palette.primary.main, } }, + buttonTrash: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.primary.contrastText, + width: '40px', + height: '40px', + '&:hover': { + backgroundColor: theme.palette.error.dark, + color: theme.palette.primary.contrastText, + } + }, link: { color: theme.palette.primary.main, textDecoration: 'none', @@ -92,9 +102,15 @@ class WorkspaceFunc extends Component { if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){ this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); } - else if (this.props.message.id === 'PROJECT_UPDATE_FAIL'){ + else if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ + this.props.history.push(`/${this.props.projectType}`); + } + else if(this.props.message.id === 'PROJECT_UPDATE_FAIL'){ this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Projektes. Versuche es noch einmal.`, type: 'error' }); } + else if(this.props.message.id === 'PROJECT_DELETE_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Projektes. Versuche es noch einmal.`, type: 'error' }); + } } } @@ -239,7 +255,11 @@ class WorkspaceFunc extends Component { renameWorkspace = () => { this.props.workspaceName(this.state.name); this.toggleDialog(); - this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); + if(this.props.projectType === 'project'){ + this.props.updateProject(); + } else { + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); + } } resetWorkspace = () => { @@ -264,9 +284,9 @@ class WorkspaceFunc extends Component { return (
{!this.props.assessment ? - +
{ this.setState({ file: true, open: true, saveFile: 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) ? {this.props.name} : null} + {this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ? {this.props.name} : null}
@@ -322,7 +342,17 @@ class WorkspaceFunc extends Component { : null} - + {!this.props.assessment? + + this.shareBlocks()} + > + + + + :null} + this.resetWorkspace()} @@ -330,13 +360,13 @@ class WorkspaceFunc extends Component { - {!this.props.assessment? - + {!this.props.assessment && (this.props.projectType === 'project' || this.props.projectType === 'gallery') ? + this.shareBlocks()} + className={this.props.classes.buttonTrash} + onClick={() => this.props.deleteProject()} > - + :null} @@ -351,7 +381,7 @@ class WorkspaceFunc extends Component { > {this.state.file ?
- +
: this.state.share ? @@ -389,6 +419,7 @@ WorkspaceFunc.propTypes = { onChangeCode: PropTypes.func.isRequired, workspaceName: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired, + deleteProject: PropTypes.func.isRequired, arduino: PropTypes.string.isRequired, xml: PropTypes.string.isRequired, name: PropTypes.string, @@ -404,4 +435,4 @@ const mapStateToProps = state => ({ message: state.message }); -export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); +export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, deleteProject })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); From e2915e8cd002cb2bb6d3c6e7c564540437113645 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:04:03 +0100 Subject: [PATCH 12/32] update gallery-project --- src/actions/projectActions.js | 31 ++++++++++++++++++++++++++----- src/actions/types.js | 1 + src/components/Project/Project.js | 5 ++++- src/components/WorkspaceFunc.js | 31 +++++++++++++++++++++++++------ src/reducers/projectReducer.js | 8 +++++++- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index 7709ab1..28c0484 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -1,4 +1,4 @@ -import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE } from './types'; +import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE, PROJECT_DESCRIPTION } from './types'; import axios from 'axios'; import { workspaceName } from './workspaceActions'; @@ -11,6 +11,13 @@ export const setType = (type) => (dispatch) => { }); }; +export const setDescription = (description) => (dispatch) => { + dispatch({ + type: PROJECT_DESCRIPTION, + payload: description + }); +}; + export const getProject = (type, id) => (dispatch) => { dispatch({type: PROJECT_PROGRESS}); dispatch(setType(type)); @@ -23,6 +30,10 @@ export const getProject = (type, id) => (dispatch) => { type: GET_PROJECT, payload: project }); + dispatch({ + type: PROJECT_DESCRIPTION, + payload: project.description + }); dispatch({type: PROJECT_PROGRESS}); dispatch(returnSuccess(res.data.message, res.status, 'GET_PROJECT_SUCCESS')); } @@ -66,15 +77,24 @@ export const updateProject = () => (dispatch, getState) => { xml: workspace.code.xml, title: workspace.name } - var id = getState().project.projects[0]._id; - axios.put(`${process.env.REACT_APP_BLOCKLY_API}/project/${id}`, body) + var project = getState().project; + var id = project.projects[0]._id; + var type = project.type; + if(type==='gallery'){ + body.description = project.description; + } + axios.put(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`, body) .then(res => { - var project = res.data.project; + var project = res.data[type]; dispatch({ type: GET_PROJECT, payload: project }); - dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_UPDATE_SUCCESS')); + if(type === 'project'){ + dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_UPDATE_SUCCESS')); + } else { + dispatch(returnSuccess(res.data.message, res.status, 'GALLERY_UPDATE_SUCCESS')); + } }) .catch(err => { if(err.response){ @@ -110,4 +130,5 @@ export const resetProject = () => (dispatch) => { payload: [] }); dispatch(setType('')); + dispatch(setDescription('')); }; diff --git a/src/actions/types.js b/src/actions/types.js index 1afb071..32d1056 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -44,3 +44,4 @@ export const PROJECT_PROGRESS = 'PROJECT_PROGRESS'; export const GET_PROJECT = 'GET_PROJECT'; export const GET_PROJECTS = 'GET_PROJECTS'; export const PROJECT_TYPE = 'PROJECT_TYPE'; +export const PROJECT_DESCRIPTION = 'PROJECT_DESCRIPTION'; diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 8a72713..6d467a8 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -40,9 +40,12 @@ class Project extends Component { this.props.returnErrors('', 404, 'GET_SHARE_FAIL'); } } - if(this.props.message.id === 'GET_PROJECT_SUCCESS'){ + else if(this.props.message.id === 'GET_PROJECT_SUCCESS'){ this.props.workspaceName(this.props.project.title); } + else if(this.props.message.id === 'PROJECT_DELETE_SUCCESS' || this.props.message.id === 'GALLERY_DELETE_SUCCESS'){ + this.props.history.push(`/${this.props.type}`); + } } } diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index 54013d8..ef856ac 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions'; -import { updateProject, deleteProject } from '../actions/projectActions'; +import { updateProject, deleteProject, setDescription } from '../actions/projectActions'; import * as Blockly from 'blockly/core'; @@ -86,6 +86,7 @@ class WorkspaceFunc extends Component { saveFile: false, share: false, name: props.name, + description: props.description, snackbar: false, type: '', key: '', @@ -102,6 +103,9 @@ class WorkspaceFunc extends Component { if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){ this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); } + if(this.props.message.id === 'GALLERY_UPDATE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Das Galerie-Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); + } else if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ this.props.history.push(`/${this.props.projectType}`); } @@ -218,6 +222,10 @@ class WorkspaceFunc extends Component { this.setState({ name: e.target.value }); } + setDescription = (e) => { + this.setState({ description: 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.' }); @@ -255,7 +263,10 @@ class WorkspaceFunc extends Component { renameWorkspace = () => { this.props.workspaceName(this.state.name); this.toggleDialog(); - if(this.props.projectType === 'project'){ + if(this.props.projectType === 'project' || this.props.projectType === 'gallery'){ + if(this.props.projectType === 'gallery'){ + this.props.setDescription(this.state.description); + } this.props.updateProject(); } else { this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); @@ -285,7 +296,7 @@ class WorkspaceFunc extends Component {
{!this.props.assessment ? -
{ this.setState({ file: true, open: true, saveFile: false, title: 'Projekt benennen', content: 'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}> +
{ this.setState({ file: true, open: true, saveFile: false, title: this.props.projectType === 'gallery' ? 'Projektdaten eintragen':'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}> {this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ? {this.props.name} : null}
@@ -381,7 +392,12 @@ class WorkspaceFunc extends Component { > {this.state.file ?
- + {this.props.projectType === 'gallery' ? +
+ + +
+ : }
: this.state.share ? @@ -420,9 +436,11 @@ WorkspaceFunc.propTypes = { workspaceName: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired, deleteProject: PropTypes.func.isRequired, + setDescription: PropTypes.func.isRequired, arduino: PropTypes.string.isRequired, xml: PropTypes.string.isRequired, - name: PropTypes.string, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, projectType: PropTypes.string.isRequired, message: PropTypes.object.isRequired }; @@ -431,8 +449,9 @@ const mapStateToProps = state => ({ arduino: state.workspace.code.arduino, xml: state.workspace.code.xml, name: state.workspace.name, + description: state.project.description, projectType: state.project.type, message: state.message }); -export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, deleteProject })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); +export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, deleteProject, setDescription })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); diff --git a/src/reducers/projectReducer.js b/src/reducers/projectReducer.js index c34a760..c97d298 100644 --- a/src/reducers/projectReducer.js +++ b/src/reducers/projectReducer.js @@ -1,8 +1,9 @@ -import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE } from '../actions/types'; +import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE, PROJECT_DESCRIPTION } from '../actions/types'; const initialState = { projects: [], type: '', + description: '', progress: false }; @@ -28,6 +29,11 @@ export default function (state = initialState, action) { ...state, type: action.payload } + case PROJECT_DESCRIPTION: + return { + ...state, + description: action.payload + } default: return state; } From 4f57e7fd321b24fa271a30071cbda06e06cecc24 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:49:08 +0100 Subject: [PATCH 13/32] note, if no data are available in database --- src/components/Project/ProjectHome.js | 63 ++++++++++++++++++--------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index 2cd32c9..c04b219 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -11,6 +11,7 @@ import Breadcrumbs from '../Breadcrumbs'; import BlocklyWindow from '../Blockly/BlocklyWindow'; import Snackbar from '../Snackbar'; +import { withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; import Divider from '@material-ui/core/Divider'; @@ -18,6 +19,17 @@ import Typography from '@material-ui/core/Typography'; import Backdrop from '@material-ui/core/Backdrop'; import CircularProgress from '@material-ui/core/CircularProgress'; +const styles = (theme) => ({ + link: { + color: theme.palette.primary.main, + textDecoration: 'none', + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline' + } + } +}); + class ProjectHome extends Component { @@ -68,26 +80,37 @@ class ProjectHome extends Component { : - - {this.props.projects.map((project, i) => { - return ( - - - -

{project.title}

- - - {project.description} -
- +
+ {this.props.projects.length > 0 ? + + {this.props.projects.map((project, i) => { + return ( + + + +

{project.title}

+ + + {project.description} +
+ +
+ ) + })}
- ) - })} - } + :
+ Es sind aktuell keine Projekte vorhanden. + {this.props.match.path.replace('/','') === 'project' ? + Erstelle jetzt dein eigenes Projekt oder lasse dich von Projektbeispielen in der Galerie inspirieren. + : null} +
+ } +
+ } ({ }); -export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(ProjectHome); +export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(withStyles(styles, { withTheme: true })(ProjectHome)); From 5d9bfa97af3f320a170ea14a80670ed6370549f7 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 3 Dec 2020 11:17:39 +0100 Subject: [PATCH 14/32] login --- src/actions/authActions.js | 184 +++++++++++++++++++++++++++++++++++ src/actions/types.js | 11 +++ src/components/Navbar.js | 41 +++++--- src/components/Routes.js | 3 + src/components/User/Login.js | 157 ++++++++++++++++++++++++++++++ src/reducers/authReducer.js | 56 +++++++++++ src/reducers/index.js | 2 + 7 files changed, 440 insertions(+), 14 deletions(-) create mode 100644 src/actions/authActions.js create mode 100644 src/components/User/Login.js create mode 100644 src/reducers/authReducer.js diff --git a/src/actions/authActions.js b/src/actions/authActions.js new file mode 100644 index 0000000..cce0e43 --- /dev/null +++ b/src/actions/authActions.js @@ -0,0 +1,184 @@ +import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; + +import axios from 'axios'; +import { returnErrors, returnSuccess } from './messageActions' + + +// // Check token & load user +// export const loadUser = () => (dispatch) => { +// // user loading +// dispatch({ +// type: USER_LOADING +// }); +// const config = { +// success: res => { +// dispatch({ +// type: USER_LOADED, +// payload: res.data.user +// }); +// }, +// error: err => { +// if(err.response){ +// dispatch(returnErrors(err.response.data.message, err.response.status)); +// } +// dispatch({ +// type: AUTH_ERROR +// }); +// } +// }; +// axios.get('/api/v1/user/me', config, dispatch(authInterceptor())) +// .then(res => { +// res.config.success(res); +// }) +// .catch(err => { +// err.config.error(err); +// }); +// }; + + +var logoutTimerId; +const timeToLogout = 14.9*60*1000; // nearly 15 minutes corresponding to the API + +// Login user +export const login = ({ email, password }) => (dispatch) => { + // Headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + }; + // Request Body + const body = JSON.stringify({ email, password }); + axios.post('https://api.opensensemap.org/users/sign-in', body, config) + .then(res => { + // Logout automatically if refreshToken "expired" + const logoutTimer = () => setTimeout( + () => dispatch(logout()), + timeToLogout + ); + logoutTimerId = logoutTimer(); + dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS')); + dispatch({ + type: LOGIN_SUCCESS, + payload: res.data + }); + }) + .catch(err => { + console.log('hier'); + console.log(err); + dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGIN_FAIL')); + dispatch({ + type: LOGIN_FAIL + }); + }); +}; + + +// Logout User +export const logout = () => (dispatch) => { + const config = { + success: res => { + dispatch({ + type: LOGOUT_SUCCESS + }); + dispatch(returnSuccess(res.data.message, res.status, 'LOGOUT_SUCCESS')); + clearTimeout(logoutTimerId); + }, + error: err => { + dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGOUT_FAIL')); + dispatch({ + type: LOGOUT_FAIL + }); + clearTimeout(logoutTimerId); + } + }; + axios.post('https://api.opensensemap.org/users/sign-out', {}, config, dispatch(authInterceptor())) + .then(res => { + res.config.success(res); + }) + .catch(err => { + if(err.response.status !== 401){ + err.config.error(err); + } + }); +}; + + +export const authInterceptor = () => (dispatch, getState) => { + // Add a request interceptor + axios.interceptors.request.use( + config => { + config.headers['Content-Type'] = 'application/json'; + const token = getState().auth.token; + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + error => { + Promise.reject(error); + } + ); + + // Add a response interceptor + axios.interceptors.response.use( + response => { + // request was successfull + return response; + }, + error => { + const originalRequest = error.config; + const refreshToken = getState().auth.refreshToken; + if(refreshToken){ + // try to refresh the token failed + if (error.response.status === 401 && originalRequest._retry) { + // router.push('/login'); + return Promise.reject(error); + } + // token was not valid and 1st try to refresh the token + if (error.response.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + const refreshToken = getState().auth.refreshToken; + // request to refresh the token, in request-body is the refreshToken + axios.post('/api/v1/user/token/refresh', {"refreshToken": refreshToken}) + .then(res => { + if (res.status === 200) { + clearTimeout(logoutTimerId); + const logoutTimer = () => setTimeout( + () => dispatch(logout()), + timeToLogout + ); + logoutTimerId = logoutTimer(); + dispatch({ + type: REFRESH_TOKEN_SUCCESS, + payload: res.data + }); + axios.defaults.headers.common['Authorization'] = 'Bearer ' + getState().auth.token; + // request was successfull, new request with the old parameters and the refreshed token + return axios(originalRequest) + .then(res => { + originalRequest.success(res); + }) + .catch(err => { + originalRequest.error(err); + }); + } + return Promise.reject(error); + }) + .catch(err => { + // request failed, token could not be refreshed + if(err.response){ + dispatch(returnErrors(err.response.data.message, err.response.status)); + } + dispatch({ + type: AUTH_ERROR + }); + return Promise.reject(error); + }); + } + } + // request status was unequal to 401, no possibility to refresh the token + return Promise.reject(error); + } + ); +}; diff --git a/src/actions/types.js b/src/actions/types.js index 32d1056..34d5889 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -1,3 +1,14 @@ +// authentication +export const USER_LOADING = 'USER_LOADING'; +export const USER_LOADED = 'USER_LOADED'; +export const AUTH_ERROR = 'AUTH_ERROR'; +export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; +export const LOGIN_FAIL = 'LOGIN_FAIL'; +export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; +export const LOGOUT_FAIL = 'LOGOUT_FAIL'; +export const REFRESH_TOKEN_FAIL = 'REFRESH_TOKEN_FAIL'; +export const REFRESH_TOKEN_SUCCESS = 'REFRESH_TOKEN_SUCCESS'; + export const NEW_CODE = 'NEW_CODE'; export const CHANGE_WORKSPACE = 'CHANGE_WORKSPACE'; export const CREATE_BLOCK = 'CREATE_BLOCK'; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 3bb1b51..dfd5e6a 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; +import { logout } from '../actions/authActions'; import senseboxLogo from './sensebox_logo.svg'; @@ -20,7 +21,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import LinearProgress from '@material-ui/core/LinearProgress'; -import { faBars, faChevronLeft, faLayerGroup, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons"; +import { faBars, faChevronLeft, faLayerGroup, faSignInAlt, faSignOutAlt, faCertificate, faUserCircle, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -102,8 +103,7 @@ class Navbar extends Component { {[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" }, { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" }, { text: 'Galerie', icon: faLightbulb, link: "/gallery" }, - { text: 'Projekte', icon: faLayerGroup, link: "/project" }, - { text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => ( + { text: 'Projekte', icon: faLayerGroup, link: "/project" }].map((item, index) => ( @@ -113,14 +113,25 @@ class Navbar extends Component { ))} - {/* - {[{ text: 'Über uns', icon: faBuilding }, { text: 'Kontakt', icon: faEnvelope }, { text: 'Impressum', icon: faIdCard }].map((item, index) => ( - - - - - ))} - */} + + {[{ text: 'Anmelden', icon: faSignInAlt, link: '/user/login', restriction: !this.props.isAuthenticated }, + { text: 'Konto', icon: faUserCircle, link: '/user', restriction: this.props.isAuthenticated }, + { text: 'MyBadges', icon: faCertificate, link: '/user/badge', restriction: this.props.isAuthenticated }, + { text: 'Abmelden', icon: faSignOutAlt, function: this.props.logout, restriction: this.props.isAuthenticated }, + { text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => { + if(item.restriction || Object.keys(item).filter(attribute => attribute === 'restriction').length === 0){ + return( + + {item.function(); this.toggleDrawer();} : this.toggleDrawer}> + + + + + ); + } + } + )} + {this.props.tutorialIsLoading || this.props.projectIsLoading ? @@ -132,12 +143,14 @@ class Navbar extends Component { Navbar.propTypes = { tutorialIsLoading: PropTypes.bool.isRequired, - projectIsLoading: PropTypes.bool.isRequired + projectIsLoading: PropTypes.bool.isRequired, + isAuthenticated: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ tutorialIsLoading: state.tutorial.progress, - projectIsLoading: state.project.progress + projectIsLoading: state.project.progress, + isAuthenticated: state.auth.isAuthenticated }); -export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Navbar))); +export default connect(mapStateToProps, { logout })(withStyles(styles, { withTheme: true })(withRouter(Navbar))); diff --git a/src/components/Routes.js b/src/components/Routes.js index 54b4ac7..70bee87 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -15,6 +15,7 @@ import Project from './Project/Project'; import Settings from './Settings/Settings'; import Impressum from './Impressum'; import Privacy from './Privacy'; +import Login from './User/Login'; class Routes extends Component { @@ -40,6 +41,8 @@ class Routes extends Component { // User-Projects + // User + // settings // privacy diff --git a/src/components/User/Login.js b/src/components/User/Login.js new file mode 100644 index 0000000..81e8963 --- /dev/null +++ b/src/components/User/Login.js @@ -0,0 +1,157 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { login } from '../../actions/authActions' +import { clearMessages } from '../../actions/messageActions' + +import { withRouter } from 'react-router-dom'; + +import Snackbar from '../Snackbar'; +import Breadcrumbs from '../Breadcrumbs'; + +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +import TextField from '@material-ui/core/TextField'; +import Divider from '@material-ui/core/Divider'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Link from '@material-ui/core/Link'; + + +export class Login extends Component { + + constructor(props) { + super(props); + this.state = { + email: '', + password: '', + snackbar: false, + type: '', + key: '', + message: '', + showPassword: false + }; + } + + componentDidUpdate(props){ + const { message } = this.props; + if (message !== props.message) { + if(message.id === 'LOGIN_SUCCESS'){ + this.props.history.goBack(); + } + // Check for login error + else if(message.id === 'LOGIN_FAIL'){ + this.setState({ email: '', password: '', snackbar: true, key: Date.now(), message: 'Der Benutzername oder das Passwort ist nicht korrekt.', type: 'error' }); + } + } + } + + onChange = e => { + this.setState({ [e.target.name]: e.target.value }); + }; + + onSubmit = e => { + e.preventDefault(); + const {email, password} = this.state; + if(email !== '' && password !== ''){ + // create user object + const user = { + email, + password + }; + this.props.login(user); + } else { + this.setState({ snackbar: true, key: Date.now(), message: 'Gib sowohl ein Benutzername als auch ein Passwort ein.', type: 'error' }); + } + }; + + handleClickShowPassword = () => { + this.setState({ showPassword: !this.state.showPassword }); + }; + + handleMouseDownPassword = (e) => { + e.preventDefault(); + }; + + render(){ + return( +
+ + +
+

Anmelden

+ + + + + + + + }} + onChange={this.onChange} + fullWidth={true} + /> +

+ +

+

+ Passwort vergessen? +

+ +

+ Du hast noch kein Konto? Registrieren +

+
+
+ ); + } +} + +Login.propTypes = { + message: PropTypes.object.isRequired, + login: PropTypes.func.isRequired, + clearMessages: PropTypes.func.isRequired, + message: PropTypes.object.isRequired, + progress: PropTypes.bool.isRequired +}; + +const mapStateToProps = state => ({ + message: state.message, + progress: state.auth.progress +}); + +export default connect(mapStateToProps, { login, clearMessages })(withRouter(Login)); diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js new file mode 100644 index 0000000..cc48b84 --- /dev/null +++ b/src/reducers/authReducer.js @@ -0,0 +1,56 @@ +import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; + + +const initialState = { + token: localStorage.getItem('token'), + refreshToken: localStorage.getItem('refreshToken'), + isAuthenticated: null, + progress: false, + user: null +}; + +export default function(state = initialState, action){ + switch(action.type){ + case USER_LOADING: + return { + ...state, + progress: true + }; + case USER_LOADED: + return { + ...state, + isAuthenticated: true, + progress: false, + user: action.payload + }; + case LOGIN_SUCCESS: + case REFRESH_TOKEN_SUCCESS: + localStorage.setItem('token', action.payload.token); + localStorage.setItem('refreshToken', action.payload.refreshToken); + console.log(action.payload); + return { + ...state, + user: action.payload.data.user, + token: action.payload.token, + refreshToken: action.payload.refreshToken, + isAuthenticated: true, + progress: false + }; + case AUTH_ERROR: + case LOGIN_FAIL: + case LOGOUT_SUCCESS: + case LOGOUT_FAIL: + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + return { + ...state, + token: null, + refreshToken: null, + user: null, + isAuthenticated: false, + progress: false + }; + default: + return state; + } +} diff --git a/src/reducers/index.js b/src/reducers/index.js index 5ff8fed..cdedf04 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -5,8 +5,10 @@ import tutorialBuilderReducer from './tutorialBuilderReducer'; import generalReducer from './generalReducer'; import projectReducer from './projectReducer'; import messageReducer from './messageReducer'; +import authReducer from './authReducer'; export default combineReducers({ + auth: authReducer, workspace: workspaceReducer, tutorial: tutorialReducer, builder: tutorialBuilderReducer, From 80fd1126587a09488292395bbc0693a3f9c4bed4 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 3 Dec 2020 12:09:01 +0100 Subject: [PATCH 15/32] private routes and redirecting --- src/App.js | 2 +- src/actions/authActions.js | 2 +- src/components/Project/Project.js | 14 +++++---- src/components/Project/ProjectHome.js | 16 +++++------ src/components/Route/PrivateRoute.js | 41 +++++++++++++++++++++++++++ src/components/{ => Route}/Routes.js | 37 ++++++++++++++---------- src/components/User/Login.js | 9 +++++- 7 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 src/components/Route/PrivateRoute.js rename src/components/{ => Route}/Routes.js (61%) diff --git a/src/App.js b/src/App.js index cf38679..6f21ed6 100644 --- a/src/App.js +++ b/src/App.js @@ -12,7 +12,7 @@ import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles'; import Navbar from './components/Navbar'; import Footer from './components/Footer'; -import Routes from './components/Routes'; +import Routes from './components/Route/Routes'; import Cookies from './components/Cookies'; const theme = createMuiTheme({ diff --git a/src/actions/authActions.js b/src/actions/authActions.js index cce0e43..5b142af 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -57,11 +57,11 @@ export const login = ({ email, password }) => (dispatch) => { timeToLogout ); logoutTimerId = logoutTimer(); - dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS')); dispatch({ type: LOGIN_SUCCESS, payload: res.data }); + dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS')); }) .catch(err => { console.log('hier'); diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 6d467a8..14ba164 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -6,6 +6,7 @@ import { getProject, resetProject } from '../../actions/projectActions'; import { clearMessages, returnErrors } from '../../actions/messageActions'; import axios from 'axios'; +import { withRouter } from 'react-router-dom'; import { createNameId } from 'mnemonic-id'; import Home from '../Home'; @@ -23,7 +24,8 @@ class Project extends Component { } componentDidUpdate(props) { - if(props.location.path !== this.props.location.path || + console.log(this.props); + if(props.location.pathname !== this.props.location.pathname || props.match.params[`${this.props.type}Id`] !== this.props.match.params[`${this.props.type}Id`]){ if(this.props.message.msg){ this.props.clearMessages(); @@ -55,8 +57,10 @@ class Project extends Component { } getProject = () => { - var param = this.props.match.params.shareId ? 'share' : this.props.match.params.galleryId ? 'gallery' : 'project'; - var id = this.props.match.params[`${param}Id`]; + var id = this.props.location.pathname.replace(/\/[a-z]{1,}\//,''); + var param = this.props.location.pathname.replace(`/${id}`,'').replace('/',''); + console.log('param', param); + console.log(id); this.props.getProject(param, id); } @@ -70,7 +74,7 @@ class Project extends Component { : this.props.project ?
{this.props.type !== 'share' ? - + : null}
: null @@ -97,4 +101,4 @@ const mapStateToProps = state => ({ message: state.message }); -export default connect(mapStateToProps, { workspaceName, getProject, resetProject, clearMessages, returnErrors })(Project); +export default connect(mapStateToProps, { workspaceName, getProject, resetProject, clearMessages, returnErrors })(withRouter(Project)); diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index c04b219..668d5d9 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -5,7 +5,7 @@ import { getProjects, resetProject } from '../../actions/projectActions'; import { clearMessages } from '../../actions/messageActions'; import axios from 'axios'; -import { Link } from 'react-router-dom'; +import { Link, withRouter } from 'react-router-dom'; import Breadcrumbs from '../Breadcrumbs'; import BlocklyWindow from '../Blockly/BlocklyWindow'; @@ -41,7 +41,7 @@ class ProjectHome extends Component { } componentDidMount() { - var type = this.props.match.path.replace('/',''); + var type = this.props.location.pathname.replace('/',''); this.props.getProjects(type); if(this.props.message){ if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ @@ -57,9 +57,9 @@ class ProjectHome extends Component { } componentDidUpdate(props) { - if(props.match.path !== this.props.match.path){ + if(props.location.pathname !== this.props.location.pathname){ this.setState({snackbar: false}); - this.props.getProjects(this.props.match.path.replace('/','')); + this.props.getProjects(this.props.location.pathname.replace('/','')); } } @@ -69,10 +69,10 @@ class ProjectHome extends Component { } render() { - var data = this.props.match.path === '/project' ? 'Projekte' : 'Galerie'; + var data = this.props.location.pathname === '/project' ? 'Projekte' : 'Galerie'; return (
- +

{data}

{this.props.progress ? @@ -104,7 +104,7 @@ class ProjectHome extends Component { :
Es sind aktuell keine Projekte vorhanden. - {this.props.match.path.replace('/','') === 'project' ? + {this.props.location.pathname.replace('/','') === 'project' ? Erstelle jetzt dein eigenes Projekt oder lasse dich von Projektbeispielen in der Galerie inspirieren. : null}
@@ -138,4 +138,4 @@ const mapStateToProps = state => ({ }); -export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(withStyles(styles, { withTheme: true })(ProjectHome)); +export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(withStyles(styles, { withTheme: true })(withRouter(ProjectHome))); diff --git a/src/components/Route/PrivateRoute.js b/src/components/Route/PrivateRoute.js new file mode 100644 index 0000000..4de5bc5 --- /dev/null +++ b/src/components/Route/PrivateRoute.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Route, Redirect, withRouter } from 'react-router-dom'; + + +class PrivateRoute extends Component { + + render() { + return ( + + this.props.isAuthenticated ? ( + this.props.children + ) : (()=>{ + return ( + + ) + })() + } + /> + ); + } +} + +PrivateRoute.propTypes = { + isAuthenticated: PropTypes.bool +}; + +const mapStateToProps = state => ({ + isAuthenticated: state.auth.isAuthenticated +}); + +export default connect(mapStateToProps, null)(withRouter(PrivateRoute)); diff --git a/src/components/Routes.js b/src/components/Route/Routes.js similarity index 61% rename from src/components/Routes.js rename to src/components/Route/Routes.js index 70bee87..79d1e01 100644 --- a/src/components/Routes.js +++ b/src/components/Route/Routes.js @@ -1,21 +1,22 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { visitPage } from '../actions/generalActions'; +import { visitPage } from '../../actions/generalActions'; import { Route, Switch, withRouter } from 'react-router-dom'; -import Home from './Home'; -import Tutorial from './Tutorial/Tutorial'; -import TutorialHome from './Tutorial/TutorialHome'; -import Builder from './Tutorial/Builder/Builder'; -import NotFound from './NotFound'; -import ProjectHome from './Project/ProjectHome'; -import Project from './Project/Project'; -import Settings from './Settings/Settings'; -import Impressum from './Impressum'; -import Privacy from './Privacy'; -import Login from './User/Login'; +import PrivateRoute from './PrivateRoute'; +import Home from '../Home'; +import Tutorial from '../Tutorial/Tutorial'; +import TutorialHome from '../Tutorial/TutorialHome'; +import Builder from '../Tutorial/Builder/Builder'; +import NotFound from '../NotFound'; +import ProjectHome from '../Project/ProjectHome'; +import Project from '../Project/Project'; +import Settings from '../Settings/Settings'; +import Impressum from '../Impressum'; +import Privacy from '../Privacy'; +import Login from '../User/Login'; class Routes extends Component { @@ -31,7 +32,9 @@ class Routes extends Component { // Tutorials - + + + // Sharing @@ -39,8 +42,12 @@ class Routes extends Component { // User-Projects - - + + + + + + // User // settings diff --git a/src/components/User/Login.js b/src/components/User/Login.js index 81e8963..d8dfc32 100644 --- a/src/components/User/Login.js +++ b/src/components/User/Login.js @@ -25,6 +25,7 @@ export class Login extends Component { constructor(props) { super(props); this.state = { + redirect: props.location.state ? props.location.state.from.pathname : null, email: '', password: '', snackbar: false, @@ -36,10 +37,16 @@ export class Login extends Component { } componentDidUpdate(props){ + console.log(this.state.redirect); const { message } = this.props; if (message !== props.message) { if(message.id === 'LOGIN_SUCCESS'){ - this.props.history.goBack(); + if(this.state.redirect){ + this.props.history.push(this.state.redirect); + } + else{ + this.props.history.goBack(); + } } // Check for login error else if(message.id === 'LOGIN_FAIL'){ From 31dbda57df33c78efbae2a4afc1e088b4b313c59 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 3 Dec 2020 15:38:47 +0100 Subject: [PATCH 16/32] load user, if valid token exists --- src/App.js | 40 ++++++++++++---------- src/actions/authActions.js | 66 ++++++++++++++++++------------------- src/reducers/authReducer.js | 2 +- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/App.js b/src/App.js index 6f21ed6..db72ff0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { Component } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { createBrowserHistory } from "history"; import { Provider } from 'react-redux'; import store from './store'; +import { loadUser } from './actions/authActions'; import './App.css'; @@ -27,24 +28,29 @@ const theme = createMuiTheme({ } }); -const customHistory = createBrowserHistory(); +class App extends Component { + componentDidMount(){ + store.dispatch(loadUser()); + } -function App() { - return ( - - - -
- - - -
-
-
-
-
- ); + render() { + const customHistory = createBrowserHistory(); + return ( + + + +
+ + + +
+
+
+
+
+ ); + } } export default App; diff --git a/src/actions/authActions.js b/src/actions/authActions.js index 5b142af..c29f7cf 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -4,36 +4,36 @@ import axios from 'axios'; import { returnErrors, returnSuccess } from './messageActions' -// // Check token & load user -// export const loadUser = () => (dispatch) => { -// // user loading -// dispatch({ -// type: USER_LOADING -// }); -// const config = { -// success: res => { -// dispatch({ -// type: USER_LOADED, -// payload: res.data.user -// }); -// }, -// error: err => { -// if(err.response){ -// dispatch(returnErrors(err.response.data.message, err.response.status)); -// } -// dispatch({ -// type: AUTH_ERROR -// }); -// } -// }; -// axios.get('/api/v1/user/me', config, dispatch(authInterceptor())) -// .then(res => { -// res.config.success(res); -// }) -// .catch(err => { -// err.config.error(err); -// }); -// }; +// Check token & load user +export const loadUser = () => (dispatch) => { + // user loading + dispatch({ + type: USER_LOADING + }); + const config = { + success: res => { + dispatch({ + type: USER_LOADED, + payload: res.data.user + }); + }, + error: err => { + if(err.response){ + dispatch(returnErrors(err.response.data.message, err.response.status)); + } + dispatch({ + type: AUTH_ERROR + }); + } + }; + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/user`, config, dispatch(authInterceptor())) + .then(res => { + res.config.success(res); + }) + .catch(err => { + err.config.error(err); + }); +}; var logoutTimerId; @@ -49,7 +49,7 @@ export const login = ({ email, password }) => (dispatch) => { }; // Request Body const body = JSON.stringify({ email, password }); - axios.post('https://api.opensensemap.org/users/sign-in', body, config) + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/user`, body, config) .then(res => { // Logout automatically if refreshToken "expired" const logoutTimer = () => setTimeout( @@ -92,7 +92,7 @@ export const logout = () => (dispatch) => { clearTimeout(logoutTimerId); } }; - axios.post('https://api.opensensemap.org/users/sign-out', {}, config, dispatch(authInterceptor())) + axios.post('https://api.opensensemap.org/users/sign-out', {}, config) .then(res => { res.config.success(res); }) @@ -140,7 +140,7 @@ export const authInterceptor = () => (dispatch, getState) => { originalRequest._retry = true; const refreshToken = getState().auth.refreshToken; // request to refresh the token, in request-body is the refreshToken - axios.post('/api/v1/user/token/refresh', {"refreshToken": refreshToken}) + axios.post('https://api.opensensemap.org/users/refresh-auth', {"token": refreshToken}) .then(res => { if (res.status === 200) { clearTimeout(logoutTimerId); diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index cc48b84..653ecb0 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -30,7 +30,7 @@ export default function(state = initialState, action){ console.log(action.payload); return { ...state, - user: action.payload.data.user, + user: action.payload.user, token: action.payload.token, refreshToken: action.payload.refreshToken, isAuthenticated: true, From 6a8036e07538d9b0b96b23a01a0f443bc9cd92ae Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 3 Dec 2020 16:56:04 +0100 Subject: [PATCH 17/32] restrictions in the accessibility of URLs --- src/components/Navbar.js | 29 ++++++++----- src/components/Route/IsLoggedRoute.js | 39 ++++++++++++++++++ src/components/Route/PrivateRoute.js | 2 +- src/components/Route/PrivateRouteCreator.js | 45 +++++++++++++++++++++ src/components/Route/Routes.js | 11 +++-- 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 src/components/Route/IsLoggedRoute.js create mode 100644 src/components/Route/PrivateRouteCreator.js diff --git a/src/components/Navbar.js b/src/components/Navbar.js index dfd5e6a..b7fa021 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -101,16 +101,21 @@ class Navbar extends Component {
{[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" }, - { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" }, + { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder", restriction: this.props.user && this.props.user.blocklyRole !== 'user' && this.props.isAuthenticated}, { text: 'Galerie', icon: faLightbulb, link: "/gallery" }, - { text: 'Projekte', icon: faLayerGroup, link: "/project" }].map((item, index) => ( - - - - - - - ))} + { text: 'Projekte', icon: faLayerGroup, link: "/project", restriction: this.props.isAuthenticated }].map((item, index) => { + if(item.restriction || Object.keys(item).filter(attribute => attribute === 'restriction').length === 0){ + return( + + + + + + + ); + } + } + )} @@ -144,13 +149,15 @@ class Navbar extends Component { Navbar.propTypes = { tutorialIsLoading: PropTypes.bool.isRequired, projectIsLoading: PropTypes.bool.isRequired, - isAuthenticated: PropTypes.bool.isRequired + isAuthenticated: PropTypes.bool.isRequired, + user: PropTypes.object }; const mapStateToProps = state => ({ tutorialIsLoading: state.tutorial.progress, projectIsLoading: state.project.progress, - isAuthenticated: state.auth.isAuthenticated + isAuthenticated: state.auth.isAuthenticated, + userRole: state.auth.user }); export default connect(mapStateToProps, { logout })(withStyles(styles, { withTheme: true })(withRouter(Navbar))); diff --git a/src/components/Route/IsLoggedRoute.js b/src/components/Route/IsLoggedRoute.js new file mode 100644 index 0000000..f071b2b --- /dev/null +++ b/src/components/Route/IsLoggedRoute.js @@ -0,0 +1,39 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Route, Redirect } from 'react-router-dom'; + + +class IsLoggedRoute extends Component { + + render() { + return ( + + !this.props.isAuthenticated ? ( + this.props.children + ) : ( + + ) + } + /> + ); + } +} + +IsLoggedRoute.propTypes = { + isAuthenticated: PropTypes.bool.isRequired +}; + +const mapStateToProps = state => ({ + isAuthenticated: state.auth.isAuthenticated, +}); + +export default connect(mapStateToProps, null)(IsLoggedRoute); diff --git a/src/components/Route/PrivateRoute.js b/src/components/Route/PrivateRoute.js index 4de5bc5..ffd321a 100644 --- a/src/components/Route/PrivateRoute.js +++ b/src/components/Route/PrivateRoute.js @@ -31,7 +31,7 @@ class PrivateRoute extends Component { } PrivateRoute.propTypes = { - isAuthenticated: PropTypes.bool + isAuthenticated: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ diff --git a/src/components/Route/PrivateRouteCreator.js b/src/components/Route/PrivateRouteCreator.js new file mode 100644 index 0000000..b4734da --- /dev/null +++ b/src/components/Route/PrivateRouteCreator.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Route, Redirect, withRouter } from 'react-router-dom'; + + +class PrivateRoute extends Component { + + render() { + return ( + + this.props.isAuthenticated && + this.props.user && + this.props.user.blocklyRole !== 'user' ? ( + this.props.children + ) : (()=>{ + return ( + + ) + })() + } + /> + ); + } +} + +PrivateRoute.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + user: PropTypes.object +}; + +const mapStateToProps = state => ({ + isAuthenticated: state.auth.isAuthenticated, + userRole: state.auth.user +}); + +export default connect(mapStateToProps, null)(withRouter(PrivateRoute)); diff --git a/src/components/Route/Routes.js b/src/components/Route/Routes.js index 79d1e01..8304dc3 100644 --- a/src/components/Route/Routes.js +++ b/src/components/Route/Routes.js @@ -6,6 +6,9 @@ import { visitPage } from '../../actions/generalActions'; import { Route, Switch, withRouter } from 'react-router-dom'; import PrivateRoute from './PrivateRoute'; +import PrivateRouteCreator from './PrivateRouteCreator'; +import IsLoggedRoute from './IsLoggedRoute'; + import Home from '../Home'; import Tutorial from '../Tutorial/Tutorial'; import TutorialHome from '../Tutorial/TutorialHome'; @@ -32,9 +35,9 @@ class Routes extends Component { // Tutorials - + - + // Sharing @@ -49,7 +52,9 @@ class Routes extends Component { // User - + + + // settings // privacy From dddb00bd793080845d399c0763b61d729d4fa379 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:33:47 +0100 Subject: [PATCH 18/32] osem boxes dropdown blockly menu --- .../Blockly/blocks/sensebox-osem.js | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/src/components/Blockly/blocks/sensebox-osem.js b/src/components/Blockly/blocks/sensebox-osem.js index ec8dd5a..918098f 100644 --- a/src/components/Blockly/blocks/sensebox-osem.js +++ b/src/components/Blockly/blocks/sensebox-osem.js @@ -1,7 +1,14 @@ import * as Blockly from 'blockly/core'; import { getColour } from '../helpers/colour'; -var apiData = '[{"_id":"5e6073fe57703e001bb99453","createdAt":"2020-03-05T03:37:34.151Z","updatedAt":"2020-10-17T10:49:51.636Z","name":"Vtuzgorodok","currentLocation":{"timestamp":"2020-03-05T03:37:34.146Z","coordinates":[60.658676,56.833041,51],"type":"Point"},"exposure":"outdoor","sensors":[{"title":"PM10","unit":"µg/m³","sensorType":"SDS 011","icon":"osem-cloud","_id":"5e6073fe57703e001bb99458","lastMeasurement":{"value":"3.30","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"PM2.5","unit":"µg/m³","sensorType":"SDS 011","icon":"osem-cloud","_id":"5e6073fe57703e001bb99457","lastMeasurement":{"value":"0.90","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"Temperatur","unit":"°C","sensorType":"BME280","icon":"osem-thermometer","_id":"5e6073fe57703e001bb99456","lastMeasurement":{"value":"6.58","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"rel. Luftfeuchte","unit":"%","sensorType":"BME280","icon":"osem-humidity","_id":"5e6073fe57703e001bb99455","lastMeasurement":{"value":"53.76","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"Luftdruck","unit":"Pa","sensorType":"BME280","icon":"osem-barometer","_id":"5e6073fe57703e001bb99454","lastMeasurement":{"value":"96937.66","createdAt":"2020-10-17T10:49:51.627Z"}}],"model":"luftdaten_sds011_bme280","lastMeasurementAt":"2020-10-17T10:49:51.627Z","loc":[{"geometry":{"timestamp":"2020-03-05T03:37:34.146Z","coordinates":[60.658676,56.833041,51],"type":"Point"},"type":"Feature"}]}]'; +import store from '../../../store'; + +var boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null; +store.subscribe(() => { + boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null; +}); +var selectedBox = ''; + Blockly.Blocks['sensebox_osem_connection'] = { init: function () { @@ -17,10 +24,21 @@ Blockly.Blocks['sensebox_osem_connection'] = { .setAlign(Blockly.ALIGN_LEFT) .appendField(Blockly.Msg.senseBox_osem_exposure) .appendField(new Blockly.FieldDropdown([[Blockly.Msg.senseBox_osem_stationary, 'Stationary'], [Blockly.Msg.senseBox_osem_mobile, 'Mobile']]), "type"); - this.appendDummyInput() - .setAlign(Blockly.ALIGN_LEFT) - .appendField("senseBox ID") - .appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID"); + if(!boxes){ + this.appendDummyInput() + .setAlign(Blockly.ALIGN_LEFT) + .appendField("senseBox ID") + .appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID"); + } else { + var dropdown = [] + for (var i = 0; i < boxes.length; i++) { + dropdown.push([boxes[i].name, boxes[i]._id]) + } + this.appendDummyInput() + .setAlign(Blockly.ALIGN_LEFT) + .appendField("senseBox ID") + .appendField(new Blockly.FieldDropdown(dropdown), 'BoxID'); + } this.appendDummyInput() .setAlign(Blockly.ALIGN_LEFT) .appendField(Blockly.Msg.senseBox_osem_access_token) @@ -32,14 +50,7 @@ Blockly.Blocks['sensebox_osem_connection'] = { this.setNextStatement(true, null); }, onchange: function (e) { - let boxID = this.getFieldValue('BoxID'); - if (boxID !== 'senseBox ID') { - fetch('https://api.opensensemap.org/boxes/ ' + boxID) - .then(res => res.json()) - .then((data) => { - apiData = data; - }) - } + selectedBox = this.getFieldValue('BoxID'); }, mutationToDom: function () { var container = document.createElement('mutation'); @@ -83,35 +94,42 @@ Blockly.Blocks['sensebox_osem_connection'] = { }; Blockly.Blocks['sensebox_send_to_osem'] = { init: function () { - this.setTooltip(Blockly.Msg.senseBox_send_to_osem_tip); this.setHelpUrl(''); this.setColour(getColour().sensebox); this.appendDummyInput() .appendField(Blockly.Msg.senseBox_send_to_osem); - this.appendValueInput('Value') - .appendField('Phänomen') - .appendField(new Blockly.FieldDropdown( - this.generateOptions), 'SensorID'); + if(boxes){ + this.appendValueInput('Value') + .appendField('Phänomen') + .appendField(new Blockly.FieldDropdown( + this.generateOptions), 'SensorID'); + } else { + this.appendValueInput('Value') + .setAlign(Blockly.ALIGN_LEFT) + .appendField('Phänomen') + .appendField(new Blockly.FieldTextInput( + 'sensorID'), 'SensorID') + } + this.setPreviousStatement(true, null); this.setNextStatement(true, null); }, generateOptions: function () { - var options = [['', '']]; - if (apiData.sensors != undefined) { - for (var i = 0; i < apiData.sensors.length; i++) { - options.push([apiData.sensors[i].title, apiData.sensors[i]._id]); - } + var dropdown = [['', '']]; + var boxID = selectedBox; + if(boxID !== '' && boxes){ + + let box = boxes.find(el => el._id === boxID); + if (box !== undefined) { + for (var i = 0; i < box.sensors.length; i++) { + dropdown.push([box.sensors[i].title, box.sensors[i]._id]) + } + console.log(dropdown) + } } - if (options.length > 1) { - - var dropdown = options.slice(1) - return dropdown; - } else - return options; - - + return dropdown }, /** * Called whenever anything on the workspace changes. @@ -143,4 +161,4 @@ Blockly.Blocks['sensebox_send_to_osem'] = { * Blockly.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop'); */ LOOP_TYPES: ['sensebox_osem_connection'] -}; \ No newline at end of file +}; From b2f6b1d012e12914da6244cbc131836af936e277 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 7 Dec 2020 14:15:08 +0100 Subject: [PATCH 19/32] display of the sharing link validity --- .env | 3 ++ src/actions/projectActions.js | 4 +-- src/components/Project/Project.js | 1 - src/components/Project/ProjectHome.js | 2 +- src/components/WorkspaceFunc.js | 47 +++++++++++++++++++-------- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/.env b/.env index f3793fa..9af8f29 100644 --- a/.env +++ b/.env @@ -1,3 +1,6 @@ REACT_APP_COMPILER_URL=https://compiler.sensebox.de REACT_APP_BOARD=sensebox-mcu REACT_APP_BLOCKLY_API=https://api.blockly.sensebox.de + +# in days +REACT_APP_SHARE_LINK_EXPIRES=30 diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index 28c0484..94909ef 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -78,7 +78,7 @@ export const updateProject = () => (dispatch, getState) => { title: workspace.name } var project = getState().project; - var id = project.projects[0]._id; + var id = project.projects[0]._id._id ? project.projects[0]._id._id : project.projects[0]._id; var type = project.type; if(type==='gallery'){ body.description = project.description; @@ -105,7 +105,7 @@ export const updateProject = () => (dispatch, getState) => { export const deleteProject = () => (dispatch, getState) => { var project = getState().project; - var id = project.projects[0]._id; + var id = project.projects[0]._id._id ? project.projects[0]._id._id : project.projects[0]._id; var type = project.type; axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`) .then(res => { diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 14ba164..80eb59d 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -24,7 +24,6 @@ class Project extends Component { } componentDidUpdate(props) { - console.log(this.props); if(props.location.pathname !== this.props.location.pathname || props.match.params[`${this.props.type}Id`] !== this.props.match.params[`${this.props.type}Id`]){ if(this.props.message.msg){ diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index 668d5d9..5ccd49b 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -86,7 +86,7 @@ class ProjectHome extends Component { {this.props.projects.map((project, i) => { return ( - +

{project.title}

diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index ef856ac..a0a7600 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -8,6 +8,7 @@ import * as Blockly from 'blockly/core'; import { withRouter } from 'react-router-dom'; import axios from 'axios'; +import moment from 'moment'; import { saveAs } from 'file-saver'; import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; @@ -149,19 +150,29 @@ class WorkspaceFunc extends Component { } shareBlocks = () => { - var body = { - name: this.state.name, - xml: this.props.xml - }; - axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) - .then(res => { - var shareContent = res.data.content; - this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent.link }); - }) - .catch(err => { - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - }); + if(this.props.projectType === 'project' && this.props.project._id._id){ + // project is already shared + this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.project._id._id }); + } + else { + var body = { + title: this.state.name + }; + if(this.props.projectType === 'project'){ + body.projectId = this.props.project._id._id ? this.props.project._id._id : this.props.project._id + } else { + body.xml = this.props.xml; + } + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) + .then(res => { + var shareContent = res.data.content; + this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent._id }); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } } getSvg = () => { @@ -414,6 +425,14 @@ class WorkspaceFunc extends Component { + {this.props.project && this.props.project._id._id ? + {`Das Projekt wurde bereits geteilt. Der Link ist noch ${ + moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days') === 0 ? + moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours') === 0 ? + `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'minutes')} Minuten` + : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours')} Stunden` + : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days')} Tage`} gültig.`} + : {`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}}
: null} @@ -442,6 +461,7 @@ WorkspaceFunc.propTypes = { name: PropTypes.string.isRequired, description: PropTypes.string.isRequired, projectType: PropTypes.string.isRequired, + project: PropTypes.object.isRequired, message: PropTypes.object.isRequired }; @@ -451,6 +471,7 @@ const mapStateToProps = state => ({ name: state.workspace.name, description: state.project.description, projectType: state.project.type, + project: state.project.projects[0], message: state.message }); From 49fb0187039352f2f7ba283f21af0843f457e5a9 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 7 Dec 2020 15:44:56 +0100 Subject: [PATCH 20/32] fix build process --- package-lock.json | 8 ++++++++ package.json | 1 + 2 files changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2382348..1a11fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2581,6 +2581,14 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", diff --git a/package.json b/package.json index ad72591..60a336a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^0.21.0", "blockly": "^3.20200924.0", "file-saver": "^2.0.2", + "mnemonic-id": "^3.2.7", "moment": "^2.28.0", "prismjs": "^1.20.0", "react": "^16.13.1", From 0e05cdca5c164788ccff56b1548b88d0fa480df4 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 7 Dec 2020 16:26:18 +0100 Subject: [PATCH 21/32] add access-token --- .../Blockly/blocks/sensebox-osem.js | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/src/components/Blockly/blocks/sensebox-osem.js b/src/components/Blockly/blocks/sensebox-osem.js index 918098f..144d00d 100644 --- a/src/components/Blockly/blocks/sensebox-osem.js +++ b/src/components/Blockly/blocks/sensebox-osem.js @@ -5,7 +5,7 @@ import store from '../../../store'; var boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null; store.subscribe(() => { - boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null; + boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null; }); var selectedBox = ''; @@ -24,20 +24,20 @@ Blockly.Blocks['sensebox_osem_connection'] = { .setAlign(Blockly.ALIGN_LEFT) .appendField(Blockly.Msg.senseBox_osem_exposure) .appendField(new Blockly.FieldDropdown([[Blockly.Msg.senseBox_osem_stationary, 'Stationary'], [Blockly.Msg.senseBox_osem_mobile, 'Mobile']]), "type"); - if(!boxes){ - this.appendDummyInput() - .setAlign(Blockly.ALIGN_LEFT) - .appendField("senseBox ID") - .appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID"); + if (!boxes) { + this.appendDummyInput() + .setAlign(Blockly.ALIGN_LEFT) + .appendField("senseBox ID") + .appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID"); } else { - var dropdown = [] - for (var i = 0; i < boxes.length; i++) { - dropdown.push([boxes[i].name, boxes[i]._id]) - } - this.appendDummyInput() - .setAlign(Blockly.ALIGN_LEFT) - .appendField("senseBox ID") - .appendField(new Blockly.FieldDropdown(dropdown), 'BoxID'); + var dropdown = [] + for (var i = 0; i < boxes.length; i++) { + dropdown.push([boxes[i].name, boxes[i]._id]) + } + this.appendDummyInput() + .setAlign(Blockly.ALIGN_LEFT) + .appendField("senseBox ID") + .appendField(new Blockly.FieldDropdown(dropdown), 'BoxID'); } this.appendDummyInput() .setAlign(Blockly.ALIGN_LEFT) @@ -51,6 +51,16 @@ Blockly.Blocks['sensebox_osem_connection'] = { }, onchange: function (e) { selectedBox = this.getFieldValue('BoxID'); + console.log(selectedBox) + if (selectedBox !== '' && boxes) { + var accessToken = boxes.find(element => element._id === selectedBox).access_token + if (accessToken !== undefined) { + this.getField('access_token').setValue(accessToken) + } else { + this.getField('access_token').setValue('access_token') + } + } + }, mutationToDom: function () { var container = document.createElement('mutation'); @@ -99,17 +109,17 @@ Blockly.Blocks['sensebox_send_to_osem'] = { this.setColour(getColour().sensebox); this.appendDummyInput() .appendField(Blockly.Msg.senseBox_send_to_osem); - if(boxes){ - this.appendValueInput('Value') - .appendField('Phänomen') - .appendField(new Blockly.FieldDropdown( - this.generateOptions), 'SensorID'); + if (boxes) { + this.appendValueInput('Value') + .appendField('Phänomen') + .appendField(new Blockly.FieldDropdown( + this.generateOptions), 'SensorID'); } else { - this.appendValueInput('Value') - .setAlign(Blockly.ALIGN_LEFT) - .appendField('Phänomen') - .appendField(new Blockly.FieldTextInput( - 'sensorID'), 'SensorID') + this.appendValueInput('Value') + .setAlign(Blockly.ALIGN_LEFT) + .appendField('Phänomen') + .appendField(new Blockly.FieldTextInput( + 'sensorID'), 'SensorID') } this.setPreviousStatement(true, null); @@ -119,15 +129,15 @@ Blockly.Blocks['sensebox_send_to_osem'] = { generateOptions: function () { var dropdown = [['', '']]; var boxID = selectedBox; - if(boxID !== '' && boxes){ + if (boxID !== '' && boxes) { - let box = boxes.find(el => el._id === boxID); - if (box !== undefined) { - for (var i = 0; i < box.sensors.length; i++) { - dropdown.push([box.sensors[i].title, box.sensors[i]._id]) - } - console.log(dropdown) - } + let box = boxes.find(el => el._id === boxID); + if (box !== undefined) { + for (var i = 0; i < box.sensors.length; i++) { + dropdown.push([box.sensors[i].title, box.sensors[i]._id]) + } + console.log(dropdown) + } } return dropdown }, From 50e22fdf92383accb28dff3e6d03deb88306fe73 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 7 Dec 2020 18:18:08 +0100 Subject: [PATCH 22/32] possiblilty to change title of project, get share link, delete project for every project via button --- src/actions/projectActions.js | 59 ++++++++-- src/components/Home.js | 6 +- src/components/Navbar.js | 2 +- src/components/Project/Project.js | 2 +- src/components/Project/ProjectHome.js | 31 ++++- src/components/Route/PrivateRouteCreator.js | 2 +- src/components/Tutorial/Builder/Builder.js | 3 +- src/components/WorkspaceFunc.js | 118 ++++++++++---------- 8 files changed, 145 insertions(+), 78 deletions(-) diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index 94909ef..a41e685 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -71,24 +71,25 @@ export const getProjects = (type) => (dispatch) => { }); }; -export const updateProject = () => (dispatch, getState) => { +export const updateProject = (type, id) => (dispatch, getState) => { var workspace = getState().workspace; var body = { xml: workspace.code.xml, title: workspace.name } var project = getState().project; - var id = project.projects[0]._id._id ? project.projects[0]._id._id : project.projects[0]._id; - var type = project.type; if(type==='gallery'){ body.description = project.description; } axios.put(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`, body) .then(res => { var project = res.data[type]; + var projects = getState().project.projects; + var index = projects.findIndex(res => res._id === project._id); + projects[index] = project; dispatch({ - type: GET_PROJECT, - payload: project + type: GET_PROJECTS, + payload: projects }); if(type === 'project'){ dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_UPDATE_SUCCESS')); @@ -103,13 +104,17 @@ export const updateProject = () => (dispatch, getState) => { }); } -export const deleteProject = () => (dispatch, getState) => { +export const deleteProject = (type, id) => (dispatch, getState) => { var project = getState().project; - var id = project.projects[0]._id._id ? project.projects[0]._id._id : project.projects[0]._id; - var type = project.type; axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`) .then(res => { - dispatch({type: GET_PROJECTS, payload: []}); + var projects = getState().project.projects; + var index = projects.findIndex(res => res._id === id || res._id._id === id); + projects.splice(index, 1) + dispatch({ + type: GET_PROJECTS, + payload: projects + }); if(type === 'project'){ dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_DELETE_SUCCESS')); } else { @@ -121,7 +126,41 @@ export const deleteProject = () => (dispatch, getState) => { dispatch(returnErrors(err.response.data.message, err.response.status, 'PROJECT_DELETE_FAIL')); } }); -} +}; + + +export const shareProject = (title, type, id) => (dispatch, getState) => { + var body = { + title: title + }; + if(type === 'project'){ + body.projectId = id; + } else { + body.xml = getState().workspace.code.xml; + } + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) + .then(res => { + var shareContent = res.data.content; + if(body.projectId){ + var projects = getState().project.projects; + var index = projects.findIndex(res => res._id === id); + projects[index]._id = { + _id: shareContent._id, + expiresAt: shareContent.expiresAt + }; + dispatch({ + type: GET_PROJECTS, + payload: projects + }); + } + dispatch(returnSuccess(res.data.message, shareContent._id, 'SHARE_SUCCESS')); + }) + .catch(err => { + if(err.response){ + dispatch(returnErrors(err.response.data.message, err.response.status, 'SHARE_FAIL')); + } + }); +}; export const resetProject = () => (dispatch) => { diff --git a/src/components/Home.js b/src/components/Home.js index 38368d8..bade15d 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -95,7 +95,9 @@ class Home extends Component {
: null } -
+
+ +
@@ -109,7 +111,7 @@ class Home extends Component { {this.props.project ? - < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.props.project} /> + < BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.props.project.xml} /> : < BlocklyWindow blocklyCSS={{ height: '80vH' }} /> } diff --git a/src/components/Navbar.js b/src/components/Navbar.js index b7fa021..2bc7b62 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -157,7 +157,7 @@ const mapStateToProps = state => ({ tutorialIsLoading: state.tutorial.progress, projectIsLoading: state.project.progress, isAuthenticated: state.auth.isAuthenticated, - userRole: state.auth.user + user: state.auth.user }); export default connect(mapStateToProps, { logout })(withStyles(styles, { withTheme: true })(withRouter(Navbar))); diff --git a/src/components/Project/Project.js b/src/components/Project/Project.js index 80eb59d..a3a2403 100644 --- a/src/components/Project/Project.js +++ b/src/components/Project/Project.js @@ -75,7 +75,7 @@ class Project extends Component { {this.props.type !== 'share' ? : null} - +
: null ); }; diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index 5ccd49b..b373f02 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -10,6 +10,7 @@ import { Link, withRouter } from 'react-router-dom'; import Breadcrumbs from '../Breadcrumbs'; import BlocklyWindow from '../Blockly/BlocklyWindow'; import Snackbar from '../Snackbar'; +import WorkspaceFunc from '../WorkspaceFunc'; import { withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; @@ -61,6 +62,14 @@ class ProjectHome extends Component { this.setState({snackbar: false}); this.props.getProjects(this.props.location.pathname.replace('/','')); } + if(props.message !== this.props.message){ + if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Dein Projekt wurde erfolgreich gelöscht.`, type: 'success' }); + } + else if(this.props.message.id === 'GALLERY_DELETE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Dein Galerie-Projekt wurde erfolgreich gelöscht.`, type: 'success' }); + } + } } componentWillUnmount() { @@ -86,8 +95,8 @@ class ProjectHome extends Component { {this.props.projects.map((project, i) => { return ( - - + +

{project.title}

{project.description} -
- + + {this.props.user && this.props.user.email === project.creator ? +
+ +
+ +
+
+ : null} +
) })} @@ -128,12 +149,14 @@ ProjectHome.propTypes = { clearMessages: PropTypes.func.isRequired, projects: PropTypes.array.isRequired, progress: PropTypes.bool.isRequired, + user: PropTypes.object, message: PropTypes.object.isRequired }; const mapStateToProps = state => ({ projects: state.project.projects, progress: state.project.progress, + user: state.auth.user, message: state.message }); diff --git a/src/components/Route/PrivateRouteCreator.js b/src/components/Route/PrivateRouteCreator.js index b4734da..0efd48c 100644 --- a/src/components/Route/PrivateRouteCreator.js +++ b/src/components/Route/PrivateRouteCreator.js @@ -39,7 +39,7 @@ PrivateRoute.propTypes = { const mapStateToProps = state => ({ isAuthenticated: state.auth.isAuthenticated, - userRole: state.auth.user + user: state.auth.user }); export default connect(mapStateToProps, null)(withRouter(PrivateRoute)); diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 398c79f..753edd0 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -6,6 +6,7 @@ import { getTutorials, resetTutorial} from '../../../actions/tutorialActions'; import { clearMessages } from '../../../actions/messageActions'; import axios from 'axios'; +import { withRouter } from 'react-router-dom'; import { saveAs } from 'file-saver'; import { detectWhitespacesAndReturnReadableResult } from '../../../helpers/whitespace'; @@ -378,4 +379,4 @@ const mapStateToProps = state => ({ message: state.message }); -export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages })(withStyles(styles, { withTheme: true })(Builder)); +export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages })(withStyles(styles, { withTheme: true })(withRouter(Builder))); diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js index a0a7600..f11e620 100644 --- a/src/components/WorkspaceFunc.js +++ b/src/components/WorkspaceFunc.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions'; -import { updateProject, deleteProject, setDescription } from '../actions/projectActions'; +import { updateProject, deleteProject, shareProject, setDescription } from '../actions/projectActions'; import * as Blockly from 'blockly/core'; @@ -100,6 +100,9 @@ class WorkspaceFunc extends Component { if (props.name !== this.props.name) { this.setState({ name: this.props.name }); } + if (props.description !== this.props.description) { + this.setState({ description: this.props.description }); + } if(this.props.message !== props.message){ if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){ this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); @@ -116,6 +119,15 @@ class WorkspaceFunc extends Component { else if(this.props.message.id === 'PROJECT_DELETE_FAIL'){ this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Projektes. Versuche es noch einmal.`, type: 'error' }); } + else if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || + (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.message.status }); + } + else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || + (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + } } } @@ -155,23 +167,7 @@ class WorkspaceFunc extends Component { this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.project._id._id }); } else { - var body = { - title: this.state.name - }; - if(this.props.projectType === 'project'){ - body.projectId = this.props.project._id._id ? this.props.project._id._id : this.props.project._id - } else { - body.xml = this.props.xml; - } - axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body) - .then(res => { - var shareContent = res.data.content; - this.setState({ share: true, open: true, title: 'Programm teilen', id: shareContent._id }); - }) - .catch(err => { - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - }); + this.props.shareProject(this.state.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id._id ? this.props.project._id._id : this.props.project._id : undefined); } } @@ -274,11 +270,12 @@ class WorkspaceFunc extends Component { renameWorkspace = () => { this.props.workspaceName(this.state.name); this.toggleDialog(); + console.log(this.props.projectType); if(this.props.projectType === 'project' || this.props.projectType === 'gallery'){ if(this.props.projectType === 'gallery'){ this.props.setDescription(this.state.description); } - this.props.updateProject(); + this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id); } else { this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); } @@ -307,7 +304,7 @@ class WorkspaceFunc extends Component {
{!this.props.assessment ? -
{ this.setState({ file: true, open: true, saveFile: false, title: this.props.projectType === 'gallery' ? 'Projektdaten eintragen':'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}> +
{if(this.props.multiple){this.props.workspaceName(this.props.project.title);if(this.props.projectType === 'gallery'){this.props.setDescription(this.props.project.description);}} this.setState({ file: true, open: true, saveFile: false, title: this.props.projectType === 'gallery' ? 'Projektdaten eintragen':'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}> {this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ? {this.props.name} : null}
@@ -315,24 +312,28 @@ class WorkspaceFunc extends Component {
: null} - {this.props.assessment ? : } - - this.props.updateProject() : () => this.saveProject()} - > - - - - - { this.createFileName('xml'); }} - > - - - - {!this.props.assessment? + {this.props.assessment ? : !this.props.multiple ? : null} + {this.props.user && !this.props.multiple? + + this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id) : () => this.saveProject()} + > + + + + : null} + {!this.props.multiple ? + + { this.createFileName('xml'); }} + > + + + + : null} + {!this.props.assessment && !this.props.multiple?
: null} - {!this.props.assessment? + {!this.props.assessment && !this.props.multiple? : null} - {!this.props.assessment? + {this.props.projectType !== 'gallery' && !this.props.assessment ? :null} - - this.resetWorkspace()} - > - - - + {!this.props.multiple ? + + this.resetWorkspace()} + > + + + + : null} {!this.props.assessment && (this.props.projectType === 'project' || this.props.projectType === 'gallery') ? this.props.deleteProject()} + onClick={() => this.props.deleteProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} > @@ -406,7 +409,7 @@ class WorkspaceFunc extends Component { {this.props.projectType === 'gallery' ?
- +
: } @@ -426,7 +429,7 @@ class WorkspaceFunc extends Component {
{this.props.project && this.props.project._id._id ? - {`Das Projekt wurde bereits geteilt. Der Link ist noch ${ + {`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${ moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days') === 0 ? moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours') === 0 ? `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'minutes')} Minuten` @@ -454,15 +457,15 @@ WorkspaceFunc.propTypes = { onChangeCode: PropTypes.func.isRequired, workspaceName: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired, + shareProject: PropTypes.func.isRequired, deleteProject: PropTypes.func.isRequired, setDescription: PropTypes.func.isRequired, arduino: PropTypes.string.isRequired, xml: PropTypes.string.isRequired, name: PropTypes.string.isRequired, description: PropTypes.string.isRequired, - projectType: PropTypes.string.isRequired, - project: PropTypes.object.isRequired, - message: PropTypes.object.isRequired + message: PropTypes.object.isRequired, + user: PropTypes.object }; const mapStateToProps = state => ({ @@ -470,9 +473,8 @@ const mapStateToProps = state => ({ xml: state.workspace.code.xml, name: state.workspace.name, description: state.project.description, - projectType: state.project.type, - project: state.project.projects[0], - message: state.message + message: state.message, + user: state.auth.user }); -export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, deleteProject, setDescription })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); +export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, shareProject, deleteProject, setDescription })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); From 646254017c8b900b5f6d56a16423c57ebaef3d37 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Mon, 7 Dec 2020 20:12:33 +0100 Subject: [PATCH 23/32] display account information --- src/components/Alert.js | 34 +++++++++ src/components/Route/Routes.js | 20 +++-- src/components/User/Account.js | 129 +++++++++++++++++++++++++++++++++ src/components/User/Login.js | 6 +- 4 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 src/components/Alert.js create mode 100644 src/components/User/Account.js diff --git a/src/components/Alert.js b/src/components/Alert.js new file mode 100644 index 0000000..ce10c87 --- /dev/null +++ b/src/components/Alert.js @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; + +import { withStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; + +import Typography from '@material-ui/core/Typography'; + +const styles = (theme) => ({ + alert: { + marginBottom: '20px', + border: `1px solid ${theme.palette.primary.main}`, + padding: '10px 20px', + borderRadius: '4px', + background: fade(theme.palette.primary.main, 0.3), + color: 'rgb(70,70,70)' + } +}); + + +export class Alert extends Component { + + render(){ + return( +
+ + {this.props.children} + +
+ ); + } +} + + +export default withStyles(styles, { withTheme: true })(Alert); diff --git a/src/components/Route/Routes.js b/src/components/Route/Routes.js index 8304dc3..25e9aff 100644 --- a/src/components/Route/Routes.js +++ b/src/components/Route/Routes.js @@ -20,6 +20,7 @@ import Settings from '../Settings/Settings'; import Impressum from '../Impressum'; import Privacy from '../Privacy'; import Login from '../User/Login'; +import Account from '../User/Account'; class Routes extends Component { @@ -33,34 +34,37 @@ class Routes extends Component {
- // Tutorials + {/* Tutorials */} - // Sharing + {/* Sharing */} - // Gallery-Projects + {/* Gallery-Projects */} - // User-Projects + {/* User-Projects */} - // User + {/* User */} - // settings + + + + {/* settings */} - // privacy + {/* privacy */} - // Not Found + {/* Not Found */}
diff --git a/src/components/User/Account.js b/src/components/User/Account.js new file mode 100644 index 0000000..09a9545 --- /dev/null +++ b/src/components/User/Account.js @@ -0,0 +1,129 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import Breadcrumbs from '../Breadcrumbs'; +import Alert from '../Alert'; + +import Grid from '@material-ui/core/Grid'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Divider from '@material-ui/core/Divider'; +import Paper from '@material-ui/core/Paper'; +import Link from '@material-ui/core/Link'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { faUser, faAt, faMapMarkerAlt, faCloudSunRain, faBox } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + + +export class Account extends Component { + + render(){ + const {user} = this.props; + return( +
+ + +

Account

+ + Alle Angaben stammen von openSenseMap und können dort verwaltet werden. + + + + + + + + + + + + + + + + + + + + + + +
+ {this.props.user.boxes.length < 1 ? + + Du hast noch keine senseBox registriert. Besuche openSenseMap um eine senseBox zu registrieren. + + : + Du hast {this.props.user.boxes.length} {this.props.user.boxes.length === 1 ? 'senseBox' : 'senseBoxen'} registriert: + } +
+ + {this.props.user.boxes.map((box, i) => { + var sensors = box.sensors.map(sensor => sensor.title ); + return ( + + + + + + {box.name} + + + + + + + +
+ Modell: + {box.model} +
+
+ + + + + + +
+ Standort: + {`${box.exposure} (lon: ${box.currentLocation.coordinates[0]}, lat: ${box.currentLocation.coordinates[1]})`} +
+
+ + + + + + +
+ Sensoren: + {sensors.join(', ')} +
+
+
+
+ +
+ ) + })} +
+
+ ); + } +} + +Account.propTypes = { + user: PropTypes.object.isRequired +}; + +const mapStateToProps = state => ({ + user: state.auth.user +}); + +export default connect(mapStateToProps, null)(Account); diff --git a/src/components/User/Login.js b/src/components/User/Login.js index d8dfc32..13d4497 100644 --- a/src/components/User/Login.js +++ b/src/components/User/Login.js @@ -7,6 +7,7 @@ import { clearMessages } from '../../actions/messageActions' import { withRouter } from 'react-router-dom'; import Snackbar from '../Snackbar'; +import Alert from '../Alert'; import Breadcrumbs from '../Breadcrumbs'; import Button from '@material-ui/core/Button'; @@ -89,6 +90,9 @@ export class Login extends Component {

Anmelden

+ + Zur Anmeldung ist ein Konto auf openSenseMap Voraussetzung. +

- Du hast noch kein Konto? Registrieren + Du hast noch kein Konto? Registriere dich auf openSenseMap.

From a1834875a0d8adc7051247774bc39bcd0c8b10a1 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 8 Dec 2020 10:34:11 +0100 Subject: [PATCH 24/32] Update store.js --- src/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store.js b/src/store.js index bd28ae1..bf6460e 100644 --- a/src/store.js +++ b/src/store.js @@ -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__() ) ); From e6813ba2d31a227755abc2ef04baff36b3c6eff7 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 8 Dec 2020 13:53:43 +0100 Subject: [PATCH 25/32] create modular structure of WorkspaceFunc component --- src/actions/projectActions.js | 10 +- src/components/Home.js | 6 +- src/components/Project/ProjectHome.js | 4 +- src/components/Tutorial/Assessment.js | 2 +- src/components/Tutorial/SolutionCheck.js | 2 +- src/components/{ => Workspace}/Compile.js | 6 +- src/components/Workspace/DeleteProject.js | 89 ++++ src/components/Workspace/DownloadProject.js | 66 +++ src/components/Workspace/OpenProject.js | 143 ++++++ src/components/Workspace/ResetWorkspace.js | 95 ++++ src/components/Workspace/SaveProject.js | 200 ++++++++ src/components/Workspace/Screenshot.js | 97 ++++ src/components/Workspace/ShareProject.js | 154 ++++++ .../{ => Workspace}/TrashcanButtons.js | 0 src/components/Workspace/WorkspaceFunc.js | 94 ++++ src/components/Workspace/WorkspaceName.js | 150 ++++++ .../{ => Workspace}/WorkspaceStats.js | 0 src/components/WorkspaceFunc.js | 480 ------------------ 18 files changed, 1105 insertions(+), 493 deletions(-) rename src/components/{ => Workspace}/Compile.js (96%) create mode 100644 src/components/Workspace/DeleteProject.js create mode 100644 src/components/Workspace/DownloadProject.js create mode 100644 src/components/Workspace/OpenProject.js create mode 100644 src/components/Workspace/ResetWorkspace.js create mode 100644 src/components/Workspace/SaveProject.js create mode 100644 src/components/Workspace/Screenshot.js create mode 100644 src/components/Workspace/ShareProject.js rename src/components/{ => Workspace}/TrashcanButtons.js (100%) create mode 100644 src/components/Workspace/WorkspaceFunc.js create mode 100644 src/components/Workspace/WorkspaceName.js rename src/components/{ => Workspace}/WorkspaceStats.js (100%) delete mode 100644 src/components/WorkspaceFunc.js diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index a41e685..344140b 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -76,7 +76,7 @@ export const updateProject = (type, id) => (dispatch, getState) => { var body = { xml: workspace.code.xml, title: workspace.name - } + }; var project = getState().project; if(type==='gallery'){ body.description = project.description; @@ -99,10 +99,14 @@ export const updateProject = (type, id) => (dispatch, getState) => { }) .catch(err => { if(err.response){ - dispatch(returnErrors(err.response.data.message, err.response.status, 'PROJECT_UPDATE_FAIL')); + if(type === 'project'){ + dispatch(returnErrors(err.response.data.message, err.response.status, 'PROJECT_UPDATE_FAIL')); + } else { + dispatch(returnErrors(err.response.data.message, err.response.status, 'GALLERY_UPDATE_FAIL')); + } } }); -} +}; export const deleteProject = (type, id) => (dispatch, getState) => { var project = getState().project; diff --git a/src/components/Home.js b/src/components/Home.js index bade15d..7c385ac 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -6,11 +6,11 @@ import { clearStats, workspaceName } from '../actions/workspaceActions'; import * as Blockly from 'blockly/core'; import { createNameId } from 'mnemonic-id'; -import WorkspaceStats from './WorkspaceStats'; -import WorkspaceFunc from './WorkspaceFunc'; +import WorkspaceStats from './Workspace/WorkspaceStats'; +import WorkspaceFunc from './Workspace/WorkspaceFunc'; import BlocklyWindow from './Blockly/BlocklyWindow'; import CodeViewer from './CodeViewer'; -import TrashcanButtons from './TrashcanButtons'; +import TrashcanButtons from './Workspace/TrashcanButtons'; import HintTutorialExists from './Tutorial/HintTutorialExists'; import Snackbar from './Snackbar'; diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index b373f02..66f8f0a 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -10,7 +10,7 @@ import { Link, withRouter } from 'react-router-dom'; import Breadcrumbs from '../Breadcrumbs'; import BlocklyWindow from '../Blockly/BlocklyWindow'; import Snackbar from '../Snackbar'; -import WorkspaceFunc from '../WorkspaceFunc'; +import WorkspaceFunc from '../Workspace/WorkspaceFunc'; import { withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; @@ -104,7 +104,7 @@ class ProjectHome extends Component { blockDisabled initialXml={project.xml} /> - {project.description} + {project.description} {this.props.user && this.props.user.email === project.creator ?
diff --git a/src/components/Tutorial/Assessment.js b/src/components/Tutorial/Assessment.js index be845ee..75e180d 100644 --- a/src/components/Tutorial/Assessment.js +++ b/src/components/Tutorial/Assessment.js @@ -5,7 +5,7 @@ import { workspaceName } from '../../actions/workspaceActions'; import BlocklyWindow from '../Blockly/BlocklyWindow'; import CodeViewer from '../CodeViewer'; -import WorkspaceFunc from '../WorkspaceFunc'; +import WorkspaceFunc from '../Workspace/WorkspaceFunc'; import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; import Grid from '@material-ui/core/Grid'; diff --git a/src/components/Tutorial/SolutionCheck.js b/src/components/Tutorial/SolutionCheck.js index 37cafa7..5728ef1 100644 --- a/src/components/Tutorial/SolutionCheck.js +++ b/src/components/Tutorial/SolutionCheck.js @@ -5,7 +5,7 @@ import { tutorialCheck, tutorialStep } from '../../actions/tutorialActions'; import { withRouter } from 'react-router-dom'; -import Compile from '../Compile'; +import Compile from '../Workspace/Compile'; import Dialog from '../Dialog'; import { checkXml } from '../../helpers/compareXml'; diff --git a/src/components/Compile.js b/src/components/Workspace/Compile.js similarity index 96% rename from src/components/Compile.js rename to src/components/Workspace/Compile.js index 7fe5932..89b081e 100644 --- a/src/components/Compile.js +++ b/src/components/Workspace/Compile.js @@ -1,11 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { workspaceName } from '../actions/workspaceActions'; +import { workspaceName } from '../../actions/workspaceActions'; -import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; +import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; -import Dialog from './Dialog'; +import Dialog from '../Dialog'; import { withStyles } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; diff --git a/src/components/Workspace/DeleteProject.js b/src/components/Workspace/DeleteProject.js new file mode 100644 index 0000000..70b617a --- /dev/null +++ b/src/components/Workspace/DeleteProject.js @@ -0,0 +1,89 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { deleteProject } from '../../actions/projectActions'; + +import { withRouter } from 'react-router-dom'; + +import Snackbar from '../Snackbar'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const styles = (theme) => ({ + buttonTrash: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.primary.contrastText, + width: '40px', + height: '40px', + '&:hover': { + backgroundColor: theme.palette.error.dark, + color: theme.palette.primary.contrastText, + } + } +}); + + + +class DeleteProject extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + snackbar: false, + type: '', + key: '', + message: '' + }; + } + + componentDidUpdate(props) { + if(this.props.message !== props.message){ + if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ + this.props.history.push(`/${this.props.projectType}`); + } + else if(this.props.message.id === 'PROJECT_DELETE_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Projektes. Versuche es noch einmal.`, type: 'error' }); + } + } + } + + render() { + return ( +
+ + this.props.deleteProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} + > + + + + + +
+ ); + }; +} + +DeleteProject.propTypes = { + deleteProject: PropTypes.func.isRequired, + message: PropTypes.string.isRequired +}; + +const mapStateToProps = state => ({ + message: state.message, +}); + + +export default connect(mapStateToProps, { deleteProject })(withStyles(styles, { withTheme: true })(withRouter(DeleteProject))); diff --git a/src/components/Workspace/DownloadProject.js b/src/components/Workspace/DownloadProject.js new file mode 100644 index 0000000..9b30273 --- /dev/null +++ b/src/components/Workspace/DownloadProject.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { saveAs } from 'file-saver'; + +import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { faFileDownload } 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, + } + } +}); + + +class DownloadProject extends Component { + + downloadXmlFile = () => { + var code = this.props.xml; + var fileName = detectWhitespacesAndReturnReadableResult(this.props.name); + fileName = `${fileName}.xml` + var blob = new Blob([code], { type: 'text/xml' }); + saveAs(blob, fileName); + } + + render() { + return ( +
+ + this.downloadXmlFile()} + > + + + +
+ ); + }; +} + +DownloadProject.propTypes = { + xml: PropTypes.string.isRequired, + name: PropTypes.string.isRequired +}; + +const mapStateToProps = state => ({ + xml: state.workspace.code.xml, + name: state.workspace.name +}); + +export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(DownloadProject)); diff --git a/src/components/Workspace/OpenProject.js b/src/components/Workspace/OpenProject.js new file mode 100644 index 0000000..6038a49 --- /dev/null +++ b/src/components/Workspace/OpenProject.js @@ -0,0 +1,143 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { clearStats, workspaceName } from '../../actions/workspaceActions'; + +import * as Blockly from 'blockly/core'; + +import Snackbar from '../Snackbar'; +import Dialog from '../Dialog'; + +import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; + +import { faUpload } 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, + } + } +}); + + +class OpenProject extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + title: '', + content: '', + open: false, + snackbar: false, + type: '', + key: '', + message: '' + }; + } + + toggleDialog = () => { + this.setState({ open: !this.state, title: '', content: '' }); + } + + uploadXmlFile = (xmlFile) => { + if (xmlFile.type !== 'text/xml') { + this.setState({ open: true, 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, 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.assessment) { + var extensionPosition = xmlFile.name.lastIndexOf('.'); + this.props.workspaceName(xmlFile.name.substr(0, extensionPosition)); + } + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' }); + } + } catch (err) { + this.setState({ open: true, 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.' }); + } + }; + } + } + + render() { + return ( +
+
+ { this.uploadXmlFile(e.target.files[0]) }} + id="open-blocks" + type="file" + /> + +
+ + + +
+ ); + }; +} + +OpenProject.propTypes = { + clearStats: PropTypes.func.isRequired, + workspaceName: PropTypes.func.isRequired, + xml: PropTypes.string.isRequired, + name: PropTypes.string.isRequired +}; + +const mapStateToProps = state => ({ + xml: state.workspace.code.xml, + name: state.workspace.name +}); + +export default connect(mapStateToProps, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(OpenProject)); diff --git a/src/components/Workspace/ResetWorkspace.js b/src/components/Workspace/ResetWorkspace.js new file mode 100644 index 0000000..0c36299 --- /dev/null +++ b/src/components/Workspace/ResetWorkspace.js @@ -0,0 +1,95 @@ +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 { createNameId } from 'mnemonic-id'; +import { initialXml } from '../Blockly/initialXml.js'; + +import Snackbar from '../Snackbar'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { 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, + } + } +}); + + + +class ResetWorkspace extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + snackbar: false, + type: '', + key: '', + message: '', + }; + } + + 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.assessment) { + this.props.workspaceName(createNameId()); + } + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' }); + } + + + + render() { + return ( +
+ + this.resetWorkspace()} + > + + + + + +
+ ); + }; +} + +ResetWorkspace.propTypes = { + clearStats: PropTypes.func.isRequired, + onChangeCode: PropTypes.func.isRequired, + workspaceName: PropTypes.func.isRequired +}; + +export default connect(null, { clearStats, onChangeCode, workspaceName })(withStyles(styles, { withTheme: true })(ResetWorkspace)); diff --git a/src/components/Workspace/SaveProject.js b/src/components/Workspace/SaveProject.js new file mode 100644 index 0000000..91bc17a --- /dev/null +++ b/src/components/Workspace/SaveProject.js @@ -0,0 +1,200 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { updateProject, setDescription } from '../../actions/projectActions'; + +import axios from 'axios'; +import { withRouter } from 'react-router-dom'; + +import Snackbar from '../Snackbar'; +import Dialog from '../Dialog'; + +import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +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 Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; + +import { faSave } 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, + } + } +}); + + + +class SaveProject extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + title: '', + content: '', + open: false, + description: props.description, + snackbar: false, + type: '', + key: '', + message: '', + menuOpen: false, + anchor: '', + projectType: props.projectType + }; + } + + componentDidUpdate(props) { + if (props.projectType !== this.props.projectType) { + this.setState({ projectType: this.props.projectType }); + } + if (props.description !== this.props.description) { + this.setState({ description: this.props.description }); + } + if(this.props.message !== props.message){ + if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); + } + else if(this.props.message.id === 'GALLERY_UPDATE_SUCCESS'){ + this.setState({ snackbar: true, key: Date.now(), message: `Das Galerie-Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); + } + else if(this.props.message.id === 'PROJECT_UPDATE_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Projektes. Versuche es noch einmal.`, type: 'error' }); + } + else if(this.props.message.id === 'GALLERY_UPDATE_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Galerie-Projektes. Versuche es noch einmal.`, type: 'error' }); + } + } + } + + toggleMenu = (e) => { + this.setState({ menuOpen: !this.state.menuOpen, anchor: e.currentTarget }); + }; + + toggleDialog = () => { + this.setState({ open: !this.state, title: '', content: '' }); + } + + saveProject = () => { + var body = { + xml: this.props.xml, + title: this.props.name + }; + if(this.state.projectType === 'gallery'){ + body.description = this.state.description; + } + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/${this.state.projectType}`, body) + .then(res => { + var project = res.data[this.state.projectType]; + this.props.history.push(`/${this.state.projectType}/${project._id}`); + }) + .catch(err => { + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Speichern des ${this.state.projectType === 'gallery' ? 'Galerie-':''}Projektes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + }); + } + + setDescription = (e) => { + this.setState({ description: e.target.value }); + } + + workspaceDescription = () => { + this.props.setDescription(this.state.description); + this.setState({projectType: 'gallery'}, + () => this.saveProject() + ); + } + + render() { + console.log(1, this.props); + return ( +
+ + this.toggleMenu(e) : this.state.projectType === 'project' ? () => this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id) : () => {this.setState({projectType: 'project'}, () => this.saveProject())}} + > + + + + + {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({projectType: 'project'}, () => this.saveProject())}} + > + {this.state.projectType === 'project' ? 'Projekt aktualisieren' : 'Projekt erstellen'} + + {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({ open: true, title: 'Projekbeschreibung ergänzen', content: 'Bitte gib eine Beschreibung für das Galerie-Projekt ein und bestätige deine Angabe mit einem Klick auf \'Eingabe\'.'});}} + > + {this.state.projectType === 'gallery' ? 'Galerie-Projekt aktualisieren' : 'Galerie-Projekt erstellen'} + + + + + {this.toggleDialog(); this.setState({ description: this.props.description });}} + onClick={() => {this.toggleDialog(); this.setState({ description: this.props.description });}} + button={'Abbrechen'} + > +
+ + +
+
+
+ ); + }; +} + +SaveProject.propTypes = { + updateProject: PropTypes.func.isRequired, + setDescription: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + xml: PropTypes.string.isRequired, + message: PropTypes.object.isRequired, + user: PropTypes.object +}; + +const mapStateToProps = state => ({ + name: state.workspace.name, + description: state.project.description, + xml: state.workspace.code.xml, + message: state.message, + user: state.auth.user +}); + +export default connect(mapStateToProps, { updateProject, setDescription })(withStyles(styles, { withTheme: true })(withRouter(SaveProject))); diff --git a/src/components/Workspace/Screenshot.js b/src/components/Workspace/Screenshot.js new file mode 100644 index 0000000..c150db2 --- /dev/null +++ b/src/components/Workspace/Screenshot.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import * as Blockly from 'blockly/core'; + +import { saveAs } from 'file-saver'; + +import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import { faCamera } 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, + } + } +}); + + +class Screenshot extends Component { + + getSvg = () => { + const workspace = Blockly.getMainWorkspace(); + var canvas = workspace.svgBlockCanvas_.cloneNode(true); + + if (canvas.children[0] !== undefined) { + canvas.removeAttribute("transform"); + // does not work in react + // var cssContent = Blockly.Css.CONTENT.join(''); + var cssContent = ''; + for (var i = 0; i < document.getElementsByTagName('style').length; i++) { + if (/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)) { + cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.'); + } + } + // ensure that fill-opacity is 1, because there cannot be a replacing + // https://github.com/google/blockly/pull/3431/files#diff-00254795773903d3c0430915a68c9521R328 + cssContent += `.blocklyPath { + fill-opacity: 1; + } + .blocklyPathDark { + display: flex; + } + .blocklyPathLight { + display: flex; + } `; + var css = ''; + var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox(); + var content = new XMLSerializer().serializeToString(canvas); + var xml = ` + ${css}">${content}`; + var fileName = detectWhitespacesAndReturnReadableResult(this.props.name); + // this.props.workspaceName(this.state.name); + fileName = `${fileName}.svg` + var blob = new Blob([xml], { type: 'image/svg+xml;base64' }); + saveAs(blob, fileName); + } + } + + render() { + return ( +
+ + this.getSvg()} + > + + + +
+ ); + }; +} + +Screenshot.propTypes = { + name: PropTypes.string.isRequired, +}; + +const mapStateToProps = state => ({ + name: state.workspace.name, +}); + +export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(Screenshot)); diff --git a/src/components/Workspace/ShareProject.js b/src/components/Workspace/ShareProject.js new file mode 100644 index 0000000..f4ca60d --- /dev/null +++ b/src/components/Workspace/ShareProject.js @@ -0,0 +1,154 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { shareProject } from '../../actions/projectActions'; + +import moment from 'moment'; + +import Dialog from '../Dialog'; +import Snackbar from '../Snackbar'; + +import { Link } from 'react-router-dom'; + +import { withStyles } from '@material-ui/core/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; + +import { faShareAlt, faCopy } 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, + } + }, + link: { + color: theme.palette.primary.main, + textDecoration: 'none', + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline' + } + } +}); + + +class WorkspaceFunc extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + snackbar: false, + type: '', + key: '', + message: '', + title: '', + content: '', + open: false, + id: '', + }; + } + + componentDidUpdate(props) { + if(this.props.message !== props.message){ + if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || + (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.message.status }); + } + else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || + (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); + window.scrollTo(0, 0); + } + } + } + + toggleDialog = () => { + this.setState({ open: !this.state, title: '', content: '' }); + } + + + shareBlocks = () => { + if(this.props.projectType === 'project' && this.props.project._id._id){ + // project is already shared + this.setState({ open: true, title: 'Programm teilen', id: this.props.project._id._id }); + } + else { + this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id._id ? this.props.project._id._id : this.props.project._id : undefined); + } + } + + render() { + return ( +
+ + this.shareBlocks()} + > + + + + + + +
+ Über den folgenden Link kannst du dein Programm teilen: + this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`} + + { + navigator.clipboard.writeText(`${window.location.origin}/share/${this.state.id}`); + this.setState({ snackbar: true, key: Date.now(), message: 'Link erfolgreich in Zwischenablage gespeichert.', type: 'success' }); + }} + > + + + + {this.props.project && this.props.project._id._id ? + {`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${ + moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days') === 0 ? + moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours') === 0 ? + `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'minutes')} Minuten` + : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours')} Stunden` + : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days')} Tage`} gültig.`} + : {`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}} +
+
+
+ ); + }; +} + +WorkspaceFunc.propTypes = { + shareProject: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + message: PropTypes.object.isRequired +}; + +const mapStateToProps = state => ({ + name: state.workspace.name, + message: state.message +}); + +export default connect(mapStateToProps, { shareProject })(withStyles(styles, { withTheme: true })(WorkspaceFunc)); diff --git a/src/components/TrashcanButtons.js b/src/components/Workspace/TrashcanButtons.js similarity index 100% rename from src/components/TrashcanButtons.js rename to src/components/Workspace/TrashcanButtons.js diff --git a/src/components/Workspace/WorkspaceFunc.js b/src/components/Workspace/WorkspaceFunc.js new file mode 100644 index 0000000..8c8ae31 --- /dev/null +++ b/src/components/Workspace/WorkspaceFunc.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import WorkspaceName from './WorkspaceName'; +import SaveProject from './SaveProject'; +import Compile from './Compile'; +import SolutionCheck from '../Tutorial/SolutionCheck'; +import DownloadProject from './DownloadProject'; +import OpenProject from './OpenProject'; +import Screenshot from './Screenshot'; +import ShareProject from './ShareProject'; +import ResetWorkspace from './ResetWorkspace'; +import DeleteProject from './DeleteProject'; + +class WorkspaceFunc extends Component { + + render() { + return ( +
+ + {!this.props.assessment ? + + : null} + + {this.props.assessment ? + + : !this.props.multiple ? + + : null} + + {this.props.user && !this.props.multiple? + + : null} + + {!this.props.multiple ? + + : null} + + {!this.props.assessment && !this.props.multiple? + + : null} + + {!this.props.assessment && !this.props.multiple? + + : null} + + {this.props.projectType !== 'gallery' && !this.props.assessment ? + + :null} + + {!this.props.multiple ? + + : null} + + {!this.props.assessment && (this.props.projectType === 'project' || this.props.projectType === 'gallery') && this.props.user && this.props.user.email === this.props.project.creator ? + + :null} + +
+ ); + }; +} + +WorkspaceFunc.propTypes = { + user: PropTypes.object +}; + +const mapStateToProps = state => ({ + user: state.auth.user +}); + +export default connect(mapStateToProps, null)(WorkspaceFunc); diff --git a/src/components/Workspace/WorkspaceName.js b/src/components/Workspace/WorkspaceName.js new file mode 100644 index 0000000..33d94ab --- /dev/null +++ b/src/components/Workspace/WorkspaceName.js @@ -0,0 +1,150 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { workspaceName } from '../../actions/workspaceActions'; +import { setDescription, updateProject } from '../../actions/projectActions'; + +import Snackbar from '../Snackbar'; +import Dialog from '../Dialog'; + +import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; +import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Tooltip from '@material-ui/core/Tooltip'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; + +import { faPen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const styles = (theme) => ({ + workspaceName: { + backgroundColor: theme.palette.secondary.main, + borderRadius: '25px', + display: 'inline-flex', + cursor: 'pointer', + '&:hover': { + color: theme.palette.primary.main, + } + } +}); + + +class WorkspaceName extends Component { + + constructor(props) { + super(props); + this.inputRef = React.createRef(); + this.state = { + title: '', + content: '', + open: false, + name: props.name, + description: props.description, + snackbar: false, + type: '', + key: '', + message: '' + }; + } + + componentDidUpdate(props) { + if (props.name !== this.props.name) { + this.setState({ name: this.props.name }); + } + if (props.description !== this.props.description) { + this.setState({ description: this.props.description }); + } + } + + toggleDialog = () => { + this.setState({ open: !this.state, title: '', content: '' }); + } + + setFileName = (e) => { + this.setState({ name: e.target.value }); + } + + setDescription = (e) => { + this.setState({ description: e.target.value }); + } + + renameWorkspace = () => { + this.props.workspaceName(this.state.name); + this.toggleDialog(); + if(this.props.projectType === 'project' || this.props.projectType === 'gallery' || this.state.projectType === 'gallery'){ + if(this.props.projectType === 'gallery' || this.state.projectType === 'gallery'){ + this.props.setDescription(this.state.description); + } + if(this.state.projectType === 'gallery'){ + this.saveGallery(); + } else { + this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id); + } + } else { + this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); + } + } + + render() { + return ( +
+ +
{if(this.props.multiple){this.props.workspaceName(this.props.project.title);if(this.props.projectType === 'gallery'){this.props.setDescription(this.props.project.description);}} this.setState({ open: true, title: this.props.projectType === 'gallery' ? 'Projektdaten ändern': this.props.projectType === 'project' ? 'Projekt umbenennen' : 'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }} + > + {this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ? + {this.props.name} + : null} +
+ +
+
+
+ + + {this.toggleDialog(); this.setState({ name: this.props.name, description: this.props.description });}} + onClick={() => {this.toggleDialog(); this.setState({ name: this.props.name, description: this.props.description });}} + button={'Abbrechen'} + > +
+ {this.props.projectType === 'gallery' || this.state.projectType === 'gallery' ? +
+ + +
+ : } + +
+
+
+ ); + }; +} + +WorkspaceName.propTypes = { + workspaceName: PropTypes.func.isRequired, + setDescription: PropTypes.func.isRequired, + updateProject: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + message: PropTypes.object.isRequired, +}; + +const mapStateToProps = state => ({ + name: state.workspace.name, + description: state.project.description, + message: state.message, +}); + +export default connect(mapStateToProps, { workspaceName, setDescription, updateProject })(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceName))); diff --git a/src/components/WorkspaceStats.js b/src/components/Workspace/WorkspaceStats.js similarity index 100% rename from src/components/WorkspaceStats.js rename to src/components/Workspace/WorkspaceStats.js diff --git a/src/components/WorkspaceFunc.js b/src/components/WorkspaceFunc.js deleted file mode 100644 index f11e620..0000000 --- a/src/components/WorkspaceFunc.js +++ /dev/null @@ -1,480 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions'; -import { updateProject, deleteProject, shareProject, setDescription } from '../actions/projectActions'; - -import * as Blockly from 'blockly/core'; - -import { withRouter } from 'react-router-dom'; -import axios from 'axios'; -import moment from 'moment'; -import { saveAs } from 'file-saver'; - -import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace'; -import { initialXml } from './Blockly/initialXml.js'; - -import Compile from './Compile'; -import SolutionCheck from './Tutorial/SolutionCheck'; -import Snackbar from './Snackbar'; -import Dialog from './Dialog'; - -import { Link } from 'react-router-dom'; - -import withWidth, { isWidthDown } from '@material-ui/core/withWidth'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -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, faFileDownload, faTrashAlt, faCamera, faShare, faShareAlt, faCopy } 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, - } - }, - buttonTrash: { - backgroundColor: theme.palette.error.dark, - color: theme.palette.primary.contrastText, - width: '40px', - height: '40px', - '&:hover': { - backgroundColor: theme.palette.error.dark, - color: theme.palette.primary.contrastText, - } - }, - link: { - color: theme.palette.primary.main, - textDecoration: 'none', - '&:hover': { - color: theme.palette.primary.main, - textDecoration: 'underline' - } - } -}); - - - -class WorkspaceFunc extends Component { - - constructor(props) { - super(props); - this.inputRef = React.createRef(); - this.state = { - title: '', - content: '', - open: false, - file: false, - saveFile: false, - share: false, - name: props.name, - description: props.description, - snackbar: false, - type: '', - key: '', - message: '', - id: '' - }; - } - - componentDidUpdate(props) { - if (props.name !== this.props.name) { - this.setState({ name: this.props.name }); - } - if (props.description !== this.props.description) { - this.setState({ description: this.props.description }); - } - if(this.props.message !== props.message){ - if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){ - this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); - } - if(this.props.message.id === 'GALLERY_UPDATE_SUCCESS'){ - this.setState({ snackbar: true, key: Date.now(), message: `Das Galerie-Projekt wurde erfolgreich aktualisiert.`, type: 'success' }); - } - else if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){ - this.props.history.push(`/${this.props.projectType}`); - } - else if(this.props.message.id === 'PROJECT_UPDATE_FAIL'){ - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Projektes. Versuche es noch einmal.`, type: 'error' }); - } - else if(this.props.message.id === 'PROJECT_DELETE_FAIL'){ - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Projektes. Versuche es noch einmal.`, type: 'error' }); - } - else if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || - (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ - this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.message.status }); - } - else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || - (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - } - } - } - - toggleDialog = () => { - this.setState({ open: !this.state, share: false, file: false, saveFile: false, title: '', content: '' }); - } - - saveProject = () => { - var body = { - xml: this.props.xml, - title: this.props.name - }; - axios.post(`${process.env.REACT_APP_BLOCKLY_API}/project`, body) - .then(res => { - var project = res.data.project; - this.props.history.push(`/project/${project._id}`); - }) - .catch(err => { - this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Speichern des Projektes. Versuche es noch einmal.`, type: 'error' }); - window.scrollTo(0, 0); - }); - } - - downloadXmlFile = () => { - 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); - } - - shareBlocks = () => { - if(this.props.projectType === 'project' && this.props.project._id._id){ - // project is already shared - this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.project._id._id }); - } - else { - this.props.shareProject(this.state.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id._id ? this.props.project._id._id : this.props.project._id : undefined); - } - } - - getSvg = () => { - const workspace = Blockly.getMainWorkspace(); - var canvas = workspace.svgBlockCanvas_.cloneNode(true); - - if (canvas.children[0] !== undefined) { - canvas.removeAttribute("transform"); - - // does not work in react - // var cssContent = Blockly.Css.CONTENT.join(''); - var cssContent = ''; - for (var i = 0; i < document.getElementsByTagName('style').length; i++) { - if (/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)) { - cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.'); - } - } - // ensure that fill-opacity is 1, because there cannot be a replacing - // https://github.com/google/blockly/pull/3431/files#diff-00254795773903d3c0430915a68c9521R328 - cssContent += `.blocklyPath { - fill-opacity: 1; - } - .blocklyPathDark { - display: flex; - } - .blocklyPathLight { - display: flex; - } `; - - var css = ''; - - var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox(); - var content = new XMLSerializer().serializeToString(canvas); - - var xml = ` - ${css}">${content}`; - var fileName = detectWhitespacesAndReturnReadableResult(this.state.name); - this.props.workspaceName(this.state.name); - fileName = `${fileName}.svg` - var blob = new Blob([xml], { type: 'image/svg+xml;base64' }); - saveAs(blob, fileName); - } - } - - createFileName = (filetype) => { - this.setState({ file: filetype }, () => { - if (this.state.name) { - this.state.file === 'xml' ? this.downloadXmlFile() : this.getSvg() - } - else { - this.setState({ saveFile: true, file: filetype, open: true, title: this.state.file === 'xml' ? 'Projekt herunterladen' : 'Screenshot erstellen', content: `Bitte gib einen Namen für die Bennenung der ${this.state.file === 'xml' ? 'XML' : 'SVG'}-Datei ein und bestätige diesen mit einem Klick auf 'Eingabe'.` }); - } - }); - } - - setFileName = (e) => { - this.setState({ name: e.target.value }); - } - - setDescription = (e) => { - this.setState({ description: 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.assessment) { - var extensionPosition = xmlFile.name.lastIndexOf('.'); - this.props.workspaceName(xmlFile.name.substr(0, extensionPosition)); - } - this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' }); - } - } 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.' }); - } - }; - } - } - - renameWorkspace = () => { - this.props.workspaceName(this.state.name); - this.toggleDialog(); - console.log(this.props.projectType); - if(this.props.projectType === 'project' || this.props.projectType === 'gallery'){ - if(this.props.projectType === 'gallery'){ - this.props.setDescription(this.state.description); - } - this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id); - } else { - this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); - } - } - - 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.assessment) { - this.props.workspaceName(null); - } - this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' }); - } - - - - render() { - return ( -
- {!this.props.assessment ? - -
{if(this.props.multiple){this.props.workspaceName(this.props.project.title);if(this.props.projectType === 'gallery'){this.props.setDescription(this.props.project.description);}} this.setState({ file: true, open: true, saveFile: false, title: this.props.projectType === 'gallery' ? 'Projektdaten eintragen':'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}> - {this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ? {this.props.name} : null} -
- -
-
-
- : null} - {this.props.assessment ? : !this.props.multiple ? : null} - {this.props.user && !this.props.multiple? - - this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id) : () => this.saveProject()} - > - - - - : null} - {!this.props.multiple ? - - { this.createFileName('xml'); }} - > - - - - : null} - {!this.props.assessment && !this.props.multiple? -
- { this.uploadXmlFile(e.target.files[0]) }} - id="open-blocks" - type="file" - /> - -
- : null} - {!this.props.assessment && !this.props.multiple? - - { this.createFileName('svg'); }} - > - - - - : null} - {this.props.projectType !== 'gallery' && !this.props.assessment ? - - this.shareBlocks()} - > - - - - :null} - {!this.props.multiple ? - - this.resetWorkspace()} - > - - - - : null} - {!this.props.assessment && (this.props.projectType === 'project' || this.props.projectType === 'gallery') ? - - this.props.deleteProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} - > - - - - :null} - - { this.toggleDialog(); this.setState({ name: this.props.name }) } : this.toggleDialog} - button={this.state.file ? 'Abbrechen' : 'Schließen'} - > - {this.state.file ? -
- {this.props.projectType === 'gallery' ? -
- - -
- : } - -
- : this.state.share ? -
- Über den folgenden Link kannst du dein Programm teilen: - this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`} - - { - navigator.clipboard.writeText(`${window.location.origin}/share/${this.state.id}`); - this.setState({ snackbar: true, key: Date.now(), message: 'Link erfolgreich in Zwischenablage gespeichert.', type: 'success' }); - }} - > - - - - {this.props.project && this.props.project._id._id ? - {`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${ - moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days') === 0 ? - moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours') === 0 ? - `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'minutes')} Minuten` - : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours')} Stunden` - : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days')} Tage`} gültig.`} - : {`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}} -
- : null} -
- - - -
- ); - }; -} - -WorkspaceFunc.propTypes = { - clearStats: PropTypes.func.isRequired, - onChangeCode: PropTypes.func.isRequired, - workspaceName: PropTypes.func.isRequired, - updateProject: PropTypes.func.isRequired, - shareProject: PropTypes.func.isRequired, - deleteProject: PropTypes.func.isRequired, - setDescription: PropTypes.func.isRequired, - arduino: PropTypes.string.isRequired, - xml: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - message: PropTypes.object.isRequired, - user: PropTypes.object -}; - -const mapStateToProps = state => ({ - arduino: state.workspace.code.arduino, - xml: state.workspace.code.xml, - name: state.workspace.name, - description: state.project.description, - message: state.message, - user: state.auth.user -}); - -export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName, updateProject, shareProject, deleteProject, setDescription })(withStyles(styles, { withTheme: true })(withWidth()(withRouter(WorkspaceFunc)))); From 3cf7f10d34d44a50d206b70d7d8e37f15eb34624 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 8 Dec 2020 15:33:03 +0100 Subject: [PATCH 26/32] delete tutorial button at tutorial builder --- src/actions/tutorialActions.js | 22 ++++++ src/components/Tutorial/Builder/Builder.js | 83 ++++++++++++++++------ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index ec0e67b..bffabfa 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -56,6 +56,28 @@ export const getTutorials = () => (dispatch, getState) => { }); }; +export const deleteTutorial = (id) => (dispatch, getState) => { + var tutorial = getState().tutorial; + var id = getState().builder.id; + axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${id}`) + .then(res => { + var tutorials = tutorial.tutorials; + var index = tutorials.findIndex(res => res._id === id); + tutorials.splice(index, 1) + dispatch({ + type: GET_TUTORIALS, + payload: tutorials + }); + dispatch(returnSuccess(res.data.message, res.status, 'TUTORIAL_DELETE_SUCCESS')); + }) + .catch(err => { + if(err.response){ + dispatch(returnErrors(err.response.data.message, err.response.status, 'TUTORIAL_DELETE_FAIL')); + } + }); +}; + + export const resetTutorial = () => (dispatch) => { dispatch({ type: GET_TUTORIALS, diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 753edd0..9656d00 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { checkError, readJSON, jsonString, progress, tutorialId, resetTutorial as resetTutorialBuilder} from '../../../actions/tutorialBuilderActions'; -import { getTutorials, resetTutorial} from '../../../actions/tutorialActions'; +import { getTutorials, resetTutorial, deleteTutorial } from '../../../actions/tutorialActions'; import { clearMessages } from '../../../actions/messageActions'; import axios from 'axios'; @@ -38,9 +38,18 @@ const styles = (theme) => ({ }, errorColor: { color: theme.palette.error.dark + }, + errorButton: { + marginTop: '5px', + height: '40px', + backgroundColor: theme.palette.error.dark, + '&:hover':{ + backgroundColor: theme.palette.error.dark + } } }); + class Builder extends Component { constructor(props) { @@ -63,9 +72,18 @@ class Builder extends Component { } componentDidUpdate(props, state) { - if(this.props.message.id === 'GET_TUTORIALS_FAIL'){ - alert(this.props.message.msg); - this.props.clearMessages(); + if(props.message !== this.props.message){ + if(this.props.message.id === 'GET_TUTORIALS_FAIL'){ + // alert(this.props.message.msg); + this.props.clearMessages(); + } + else if(this.props.message.id === 'TUTORIAL_DELETE_SUCCESS'){ + this.onChange('new'); + this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich gelöscht.`, type: 'success' }); + } + else if(this.props.message.id === 'TUTORIAL_DELETE_FAIL'){ + this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Tutorials. Versuche es noch einmal.`, type: 'error' }); + } } } @@ -130,10 +148,12 @@ class Builder extends Component { onChangeId = (value) => { this.props.tutorialId(value); - this.props.progress(true); - var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === value)[0]; - this.props.readJSON(tutorial); - this.setState({ snackbar: true, key: Date.now(), message: `Das ausgewählte Tutorial "${tutorial.title}" wurde erfolgreich übernommen.`, type: 'success' }); + if(this.state.tutorial === 'change'){ + this.props.progress(true); + var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === value)[0]; + this.props.readJSON(tutorial); + this.setState({ snackbar: true, key: Date.now(), message: `Das ausgewählte Tutorial "${tutorial.title}" wurde erfolgreich übernommen.`, type: 'success' }); + } } resetFull = () => { @@ -222,6 +242,7 @@ class Builder extends Component { } render() { + var filteredTutorials = this.props.tutorials.filter(tutorial => tutorial.creator === this.props.user.email); return (
@@ -235,13 +256,24 @@ class Builder extends Component { label="neues Tutorial erstellen" labelPlacement="end" /> - } - label="bestehendes Tutorial ändern" - labelPlacement="end" - /> + {filteredTutorials.length > 0 ? +
+ } + label="bestehendes Tutorial ändern" + labelPlacement="end" + /> + } + label="bestehendes Tutorial löschen" + labelPlacement="end" + /> +
+ : null} @@ -266,11 +298,11 @@ class Builder extends Component { @@ -311,6 +343,14 @@ class Builder extends Component {
: null} + {this.state.tutorial === 'delete' && this.props.id !== '' ? + + : null} + ({ @@ -376,7 +418,8 @@ const mapStateToProps = state => ({ json: state.builder.json, isProgress: state.builder.progress, tutorials: state.tutorial.tutorials, - message: state.message + message: state.message, + user: state.auth.user, }); -export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages })(withStyles(styles, { withTheme: true })(withRouter(Builder))); +export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages, deleteTutorial })(withStyles(styles, { withTheme: true })(withRouter(Builder))); From f53baf25360b07035c8438c89700bc75969dadbb Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 8 Dec 2020 20:06:51 +0100 Subject: [PATCH 27/32] update compile button color --- src/App.js | 5 ++++- src/components/Tutorial/SolutionCheck.js | 8 ++++---- src/components/Workspace/Compile.js | 10 +++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/App.js b/src/App.js index db72ff0..1eccac4 100644 --- a/src/App.js +++ b/src/App.js @@ -24,13 +24,16 @@ const theme = createMuiTheme({ }, secondary: { main: '#DDDDDD' + }, + button: { + compile: '#e27136' } } }); class App extends Component { - componentDidMount(){ + componentDidMount() { store.dispatch(loadUser()); } diff --git a/src/components/Tutorial/SolutionCheck.js b/src/components/Tutorial/SolutionCheck.js index 5728ef1..f8c1383 100644 --- a/src/components/Tutorial/SolutionCheck.js +++ b/src/components/Tutorial/SolutionCheck.js @@ -15,15 +15,15 @@ import IconButton from '@material-ui/core/IconButton'; import Tooltip from '@material-ui/core/Tooltip'; import Button from '@material-ui/core/Button'; -import { faPlay } from "@fortawesome/free-solid-svg-icons"; +import { faClipboardCheck } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ compile: { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, '&:hover': { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, } } @@ -63,7 +63,7 @@ class SolutionCheck extends Component { style={{ width: '40px', height: '40px', marginRight: '5px' }} onClick={() => this.check()} > - + diff --git a/src/components/Workspace/Compile.js b/src/components/Workspace/Compile.js index 89b081e..e2e4768 100644 --- a/src/components/Workspace/Compile.js +++ b/src/components/Workspace/Compile.js @@ -15,7 +15,7 @@ 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 { faClipboardCheck } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const styles = (theme) => ({ @@ -24,12 +24,12 @@ const styles = (theme) => ({ color: '#fff', }, button: { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, width: '40px', height: '40px', '&:hover': { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, } } @@ -116,12 +116,12 @@ class Compile extends Component { className={this.props.classes.button} onClick={() => this.compile()} > - + : } From a4cf0d32b07d1cde6457032c79245b22f7dc96ac Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Tue, 8 Dec 2020 20:15:54 +0100 Subject: [PATCH 28/32] adjustment of the project object property 'shared' --- src/actions/projectActions.js | 7 ++--- src/components/Project/ProjectHome.js | 2 +- src/components/Workspace/DeleteProject.js | 2 +- src/components/Workspace/SaveProject.js | 6 ++--- src/components/Workspace/ShareProject.js | 33 ++++++++++++----------- src/components/Workspace/WorkspaceName.js | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/actions/projectActions.js b/src/actions/projectActions.js index 344140b..8ab01cf 100644 --- a/src/actions/projectActions.js +++ b/src/actions/projectActions.js @@ -113,7 +113,7 @@ export const deleteProject = (type, id) => (dispatch, getState) => { axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`) .then(res => { var projects = getState().project.projects; - var index = projects.findIndex(res => res._id === id || res._id._id === id); + var index = projects.findIndex(res => res._id === id); projects.splice(index, 1) dispatch({ type: GET_PROJECTS, @@ -148,10 +148,7 @@ export const shareProject = (title, type, id) => (dispatch, getState) => { if(body.projectId){ var projects = getState().project.projects; var index = projects.findIndex(res => res._id === id); - projects[index]._id = { - _id: shareContent._id, - expiresAt: shareContent.expiresAt - }; + projects[index].shared = shareContent.expiresAt; dispatch({ type: GET_PROJECTS, payload: projects diff --git a/src/components/Project/ProjectHome.js b/src/components/Project/ProjectHome.js index 66f8f0a..07e559b 100644 --- a/src/components/Project/ProjectHome.js +++ b/src/components/Project/ProjectHome.js @@ -96,7 +96,7 @@ class ProjectHome extends Component { return ( - +

{project.title}

this.props.deleteProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} + onClick={() => this.props.deleteProject(this.props.projectType, this.props.project._id)} > diff --git a/src/components/Workspace/SaveProject.js b/src/components/Workspace/SaveProject.js index 91bc17a..0a46cf0 100644 --- a/src/components/Workspace/SaveProject.js +++ b/src/components/Workspace/SaveProject.js @@ -124,7 +124,7 @@ class SaveProject extends Component { this.toggleMenu(e) : this.state.projectType === 'project' ? () => this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id) : () => {this.setState({projectType: 'project'}, () => this.saveProject())}} + onClick={this.props.user.blocklyRole !== 'user' && (!this.props.project || this.props.user.email === this.props.project.creator) ? (e) => this.toggleMenu(e) : this.state.projectType === 'project' ? () => this.props.updateProject(this.state.projectType, this.props.project._id) : () => {this.setState({projectType: 'project'}, () => this.saveProject())}} > @@ -144,12 +144,12 @@ class SaveProject extends Component { onClose={this.toggleMenu} > {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({projectType: 'project'}, () => this.saveProject())}} + onClick={this.state.projectType === 'project' ? (e) => {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({projectType: 'project'}, () => this.saveProject())}} > {this.state.projectType === 'project' ? 'Projekt aktualisieren' : 'Projekt erstellen'} {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({ open: true, title: 'Projekbeschreibung ergänzen', content: 'Bitte gib eine Beschreibung für das Galerie-Projekt ein und bestätige deine Angabe mit einem Klick auf \'Eingabe\'.'});}} + onClick={this.state.projectType === 'gallery' ? (e) => {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({ open: true, title: 'Projekbeschreibung ergänzen', content: 'Bitte gib eine Beschreibung für das Galerie-Projekt ein und bestätige deine Angabe mit einem Klick auf \'Eingabe\'.'});}} > {this.state.projectType === 'gallery' ? 'Galerie-Projekt aktualisieren' : 'Galerie-Projekt erstellen'} diff --git a/src/components/Workspace/ShareProject.js b/src/components/Workspace/ShareProject.js index f4ca60d..060e0e9 100644 --- a/src/components/Workspace/ShareProject.js +++ b/src/components/Workspace/ShareProject.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { shareProject } from '../../actions/projectActions'; +import { clearMessages } from '../../actions/messageActions'; import moment from 'moment'; @@ -59,30 +60,31 @@ class WorkspaceFunc extends Component { componentDidUpdate(props) { if(this.props.message !== props.message){ - if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || - (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || this.props.message.status === this.props.project._id)){ this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.message.status }); } - else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || - (this.props.message.status === this.props.project._id || this.props.message.status === this.props.project._id._id))){ + else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || this.props.message.status === this.props.project._id)){ this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' }); window.scrollTo(0, 0); } } } + componentWillUnmount(){ + this.props.clearMessages(); + } + toggleDialog = () => { this.setState({ open: !this.state, title: '', content: '' }); } - shareBlocks = () => { - if(this.props.projectType === 'project' && this.props.project._id._id){ + if(this.props.projectType === 'project' && this.props.project.shared){ // project is already shared - this.setState({ open: true, title: 'Programm teilen', id: this.props.project._id._id }); + this.setState({ open: true, title: 'Programm teilen', id: this.props.project._id }); } else { - this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id._id ? this.props.project._id._id : this.props.project._id : undefined); + this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id : undefined); } } @@ -125,13 +127,13 @@ class WorkspaceFunc extends Component { - {this.props.project && this.props.project._id._id ? + {this.props.project && this.props.project.shared && this.props.message.id !== 'SHARE_SUCCESS' ? {`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${ - moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days') === 0 ? - moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours') === 0 ? - `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'minutes')} Minuten` - : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'hours')} Stunden` - : `${moment(this.props.project._id.expiresAt).diff(moment().utc(), 'days')} Tage`} gültig.`} + moment(this.props.project.shared).diff(moment().utc(), 'days') === 0 ? + moment(this.props.project.shared).diff(moment().utc(), 'hours') === 0 ? + `${moment(this.props.project.shared).diff(moment().utc(), 'minutes')} Minuten` + : `${moment(this.props.project.shared).diff(moment().utc(), 'hours')} Stunden` + : `${moment(this.props.project.shared).diff(moment().utc(), 'days')} Tage`} gültig.`} : {`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}}
@@ -142,6 +144,7 @@ class WorkspaceFunc extends Component { WorkspaceFunc.propTypes = { shareProject: PropTypes.func.isRequired, + clearMessages: PropTypes.func.isRequired, name: PropTypes.string.isRequired, message: PropTypes.object.isRequired }; @@ -151,4 +154,4 @@ const mapStateToProps = state => ({ message: state.message }); -export default connect(mapStateToProps, { shareProject })(withStyles(styles, { withTheme: true })(WorkspaceFunc)); +export default connect(mapStateToProps, { shareProject, clearMessages })(withStyles(styles, { withTheme: true })(WorkspaceFunc)); diff --git a/src/components/Workspace/WorkspaceName.js b/src/components/Workspace/WorkspaceName.js index 33d94ab..9efe915 100644 --- a/src/components/Workspace/WorkspaceName.js +++ b/src/components/Workspace/WorkspaceName.js @@ -79,7 +79,7 @@ class WorkspaceName extends Component { if(this.state.projectType === 'gallery'){ this.saveGallery(); } else { - this.props.updateProject(this.props.projectType, this.props.project._id._id ? this.props.project._id._id : this.props.project._id); + this.props.updateProject(this.props.projectType, this.props.project._id); } } else { this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` }); From c47d98d2ebfc47dcf9c173e610773532ab188207 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 9 Dec 2020 09:34:33 +0100 Subject: [PATCH 29/32] compile button --- src/components/Workspace/Compile.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/Workspace/Compile.js b/src/components/Workspace/Compile.js index e2e4768..f8a204a 100644 --- a/src/components/Workspace/Compile.js +++ b/src/components/Workspace/Compile.js @@ -23,7 +23,7 @@ const styles = (theme) => ({ zIndex: theme.zIndex.drawer + 1, color: '#fff', }, - button: { + iconButton: { backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, width: '40px', @@ -32,6 +32,14 @@ const styles = (theme) => ({ backgroundColor: theme.palette.button.compile, color: theme.palette.primary.contrastText, } + }, + button: { + backgroundColor: theme.palette.button.compile, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.button.compile, + color: theme.palette.primary.contrastText, + } } }); @@ -113,14 +121,14 @@ class Compile extends Component { {this.props.iconButton ? this.compile()} > : - } From f9caeb619e07a6e2b4b492cfefc6be88411f44a3 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 9 Dec 2020 18:10:45 +0100 Subject: [PATCH 30/32] connect to MyBadges-account --- .env | 2 + src/actions/authActions.js | 53 ++++++- src/actions/types.js | 2 + src/components/Route/Routes.js | 6 +- src/components/User/MyBadges.js | 260 ++++++++++++++++++++++++++++++++ src/reducers/authReducer.js | 9 +- 6 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 src/components/User/MyBadges.js diff --git a/.env b/.env index 9af8f29..3ba890f 100644 --- a/.env +++ b/.env @@ -2,5 +2,7 @@ REACT_APP_COMPILER_URL=https://compiler.sensebox.de REACT_APP_BOARD=sensebox-mcu REACT_APP_BLOCKLY_API=https://api.blockly.sensebox.de +REACT_APP_MYBADGES=https://mybadges.org + # in days REACT_APP_SHARE_LINK_EXPIRES=30 diff --git a/src/actions/authActions.js b/src/actions/authActions.js index c29f7cf..cb06139 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -1,4 +1,4 @@ -import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; +import { MYBADGES_CONNECT, MYBADGES_DISCONNECT, USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; import axios from 'axios'; import { returnErrors, returnSuccess } from './messageActions' @@ -64,8 +64,6 @@ export const login = ({ email, password }) => (dispatch) => { dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS')); }) .catch(err => { - console.log('hier'); - console.log(err); dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGIN_FAIL')); dispatch({ type: LOGIN_FAIL @@ -74,6 +72,55 @@ export const login = ({ email, password }) => (dispatch) => { }; +// Connect to MyBadges-Account +export const connectMyBadges = ({ username, password }) => (dispatch, getState) => { + // Headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + }; + // Request Body + const body = JSON.stringify({ username, password }); + axios.post(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`, body, config) + .then(res => { + var user = getState().auth.user; + user.badge = res.data.account; + dispatch({ + type: MYBADGES_CONNECT, + payload: user + }); + dispatch(returnSuccess(res.data.message, res.status, 'MYBADGES_CONNECT_SUCCESS')); + }) + .catch(err => { + dispatch(returnErrors(err.response.data.message, err.response.status, 'MYBADGES_CONNECT_FAIL')); + }); +}; + +// Disconnect MyBadges-Account +export const disconnectMyBadges = () => (dispatch, getState) => { + // Headers + const config = { + headers: { + 'Content-Type': 'application/json' + } + }; + axios.put(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`, config) + .then(res => { + var user = getState().auth.user; + user.badge = null; + dispatch({ + type: MYBADGES_DISCONNECT, + payload: user + }); + dispatch(returnSuccess(res.data.message, res.status, 'MYBADGES_DISCONNECT_SUCCESS')); + }) + .catch(err => { + dispatch(returnErrors(err.response.data.message, err.response.status, 'MYBADGES_DISCONNECT_FAIL')); + }); +}; + + // Logout User export const logout = () => (dispatch) => { const config = { diff --git a/src/actions/types.js b/src/actions/types.js index 34d5889..9172b4a 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -8,6 +8,8 @@ export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; export const LOGOUT_FAIL = 'LOGOUT_FAIL'; export const REFRESH_TOKEN_FAIL = 'REFRESH_TOKEN_FAIL'; export const REFRESH_TOKEN_SUCCESS = 'REFRESH_TOKEN_SUCCESS'; +export const MYBADGES_CONNECT = 'MYBADGES_CONNECT'; +export const MYBADGES_DISCONNECT = 'MYBADGES_DISCONNECT'; export const NEW_CODE = 'NEW_CODE'; export const CHANGE_WORKSPACE = 'CHANGE_WORKSPACE'; diff --git a/src/components/Route/Routes.js b/src/components/Route/Routes.js index 25e9aff..3121edf 100644 --- a/src/components/Route/Routes.js +++ b/src/components/Route/Routes.js @@ -21,6 +21,7 @@ import Impressum from '../Impressum'; import Privacy from '../Privacy'; import Login from '../User/Login'; import Account from '../User/Account'; +import MyBadges from '../User/MyBadges'; class Routes extends Component { @@ -57,7 +58,10 @@ class Routes extends Component { - + + + + {/* settings */} diff --git a/src/components/User/MyBadges.js b/src/components/User/MyBadges.js new file mode 100644 index 0000000..4d7dc47 --- /dev/null +++ b/src/components/User/MyBadges.js @@ -0,0 +1,260 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { connectMyBadges, disconnectMyBadges } from '../../actions/authActions'; + +import axios from 'axios'; +import { withRouter } from 'react-router-dom'; + +import Breadcrumbs from '../Breadcrumbs'; +import Alert from '../Alert'; + +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Divider from '@material-ui/core/Divider'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import Link from '@material-ui/core/Link'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import Avatar from '@material-ui/core/Avatar'; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; + +const styles = (theme) => ({ + root: { + '& label.Mui-focused': { + color: '#aed9c8' + }, + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderColor: '#aed9c8' + }, + borderRadius: '0.75rem' + } + }, + text: { + fontFamily: [ + '"Open Sans"', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + ].join(','), + fontSize: 16 + } +}); + +export class MyBadges extends Component { + + constructor(props) { + super(props); + this.state = { + username: '', + password: '', + showPassword: false, + msg: '', + badges: [], + progress: false + }; + } + + componentDidMount(){ + if(this.props.user.badge){ + this.getBadges(); + } + } + + componentDidUpdate(props){ + const { message } = this.props; + if (message !== props.message) { + // Check for login error + if(message.id === 'MYBADGES_CONNECT_FAIL'){ + this.setState({msg: 'Der Benutzername oder das Passwort ist nicht korrekt.', username: '', password: '', showPassword: false}); + } + else if(message.id === 'MYBADGES_CONNECT_SUCCESS'){ + this.getBadges(); + } + else if(message.id === 'MYBADGES_DISCONNECT_SUCCESS' || message.id === 'MYBADGES_DISCONNECT_FAIL'){ + this.setState({progress: false}); + } + else { + this.setState({msg: null}); + } + } + } + + getBadges = () => { + this.setState({progress: true}); + axios.get(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`) + .then(res => { + this.setState({badges: res.data.badges, progress: false}); + }) + .catch(err => { + this.setState({progress: false}); + console.log(err); + }); + }; + + onChange = e => { + this.setState({ [e.target.name]: e.target.value, msg: '' }); + }; + + onSubmit = e => { + e.preventDefault(); + const {username, password} = this.state; + // create user object + const user = { + username, + password + }; + this.props.connectMyBadges(user); + }; + + handleClickShowPassword = () => { + this.setState({ showPassword: !this.state.showPassword }); + }; + + handleMouseDownPassword = (e) => { + e.preventDefault(); + }; + + render(){ + return( +
+ + + + + {!this.props.user.badge ? + + Du kannst dein Blockly-Konto mit deinem MyBadges-Konto verknüpfen, um Badges erwerben zu können. + + : null} + +
+
+ My Badges +
+ {!this.props.user.badge ? +
+ {this.state.msg ? +
+ {this.state.msg} +
: null + } + + + + + + + }} + onChange={this.onChange} + fullWidth={true} + /> +

+ +

+

+ Passwort vergessen? +

+ +

+ Du hast noch kein Konto? Registrieren +

+
+ :
+ MyBadges-Konto ist erfolgreich verknüpft. + +
} +
+
+
+ + {this.props.user.badge && !this.state.progress ? + + + {this.state.badges && this.state.badges.length > 0 ? + + Du hast {this.state.badges.length} {this.state.badges.length === 1 ? 'Badge' : 'Badges'} im Kontext Blockly for senseBox erreicht. + + : null} + + + {this.state.badges && this.state.badges.length > 0 ? + this.state.badges.map(badge => ( + + + {badge.image && badge.image.path ? + + : } + +
{badge.name}
+
+
+
+ )) + : + + + Du hast noch keine Badges im Kontext senseBox for Blockly erreicht. + + } +
+
+ : null} +
+
+ ); + } +} + +MyBadges.propTypes = { + connectMyBadges: PropTypes.func.isRequired, + disconnectMyBadges: PropTypes.func.isRequired, + message: PropTypes.object.isRequired, + user: PropTypes.object.isRequired +}; + +const mapStateToProps = state => ({ + message: state.message, + user: state.auth.user +}); + +export default connect(mapStateToProps, { connectMyBadges, disconnectMyBadges })(withStyles(styles, { withTheme: true })(withRouter(MyBadges))); diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js index 653ecb0..482af0c 100644 --- a/src/reducers/authReducer.js +++ b/src/reducers/authReducer.js @@ -1,4 +1,4 @@ -import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; +import { MYBADGES_CONNECT, MYBADGES_DISCONNECT, USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types'; const initialState = { @@ -27,7 +27,6 @@ export default function(state = initialState, action){ case REFRESH_TOKEN_SUCCESS: localStorage.setItem('token', action.payload.token); localStorage.setItem('refreshToken', action.payload.refreshToken); - console.log(action.payload); return { ...state, user: action.payload.user, @@ -36,6 +35,12 @@ export default function(state = initialState, action){ isAuthenticated: true, progress: false }; + case MYBADGES_CONNECT: + case MYBADGES_DISCONNECT: + return { + ...state, + user: action.payload + }; case AUTH_ERROR: case LOGIN_FAIL: case LOGOUT_SUCCESS: From 101dfc7692d4b319a322d675aef127c348c45bb4 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Wed, 9 Dec 2020 19:34:44 +0100 Subject: [PATCH 31/32] assigne badge after tutorial-success --- src/actions/tutorialActions.js | 13 +++ src/actions/tutorialBuilderActions.js | 14 +++- src/actions/types.js | 1 + src/components/Tutorial/Badge.js | 85 ++++++++++++++++++++ src/components/Tutorial/Builder/Builder.js | 9 ++- src/components/Tutorial/Builder/Textfield.js | 8 +- src/components/Tutorial/Tutorial.js | 2 + src/reducers/tutorialBuilderReducer.js | 8 +- 8 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/components/Tutorial/Badge.js diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index bffabfa..a02c37d 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -56,6 +56,19 @@ export const getTutorials = () => (dispatch, getState) => { }); }; +export const assigneBadge = (id) => (dispatch, getState) => { + axios.put(`${process.env.REACT_APP_BLOCKLY_API}/user/badge/${id}`) + .then(res => { + var badge = res.data.badge; + dispatch(returnSuccess(badge, res.status, 'ASSIGNE_BADGE_SUCCESS')); + }) + .catch(err => { + if(err.response){ + dispatch(returnErrors(err.response.data.message, err.response.status, 'ASSIGNE_BADGE_FAIL')); + } + }); +}; + export const deleteTutorial = (id) => (dispatch, getState) => { var tutorial = getState().tutorial; var id = getState().builder.id; diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js index b2d6c5c..356e8fa 100644 --- a/src/actions/tutorialBuilderActions.js +++ b/src/actions/tutorialBuilderActions.js @@ -1,4 +1,4 @@ -import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from './types'; +import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_BADGE, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from './types'; import data from '../data/hardware.json'; @@ -39,6 +39,14 @@ export const tutorialId = (id) => (dispatch) => { dispatch(changeTutorialBuilder()); }; +export const tutorialBadge = (badge) => (dispatch) => { + dispatch({ + type: BUILDER_BADGE, + payload: badge + }); + dispatch(changeTutorialBuilder()); +}; + export const addStep = (index) => (dispatch, getState) => { var steps = getState().builder.steps; var step = { @@ -243,7 +251,7 @@ export const progress = (inProgress) => (dispatch) => { export const resetTutorial = () => (dispatch, getState) => { dispatch(jsonString('')); dispatch(tutorialTitle('')); - // dispatch(tutorialId('')); + dispatch(tutorialBadge('')); var steps = [ { id: 1, @@ -298,7 +306,7 @@ export const readJSON = (json) => (dispatch, getState) => { return object; }); dispatch(tutorialTitle(json.title)); - // dispatch(tutorialId(json.id)); + dispatch(tutorialBadge(json.badge)); dispatch(tutorialSteps(steps)); dispatch(setSubmitError()); dispatch(progress(false)); diff --git a/src/actions/types.js b/src/actions/types.js index 9172b4a..b3bff75 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -34,6 +34,7 @@ export const JSON_STRING = 'JSON_STRING'; export const BUILDER_CHANGE = 'BUILDER_CHANGE'; export const BUILDER_TITLE = 'BUILDER_TITLE'; +export const BUILDER_BADGE = 'BUILDER_BADGE'; export const BUILDER_ID = 'BUILDER_ID'; export const BUILDER_ADD_STEP = 'BUILDER_ADD_STEP'; export const BUILDER_DELETE_STEP = 'BUILDER_DELETE_STEP'; diff --git a/src/components/Tutorial/Badge.js b/src/components/Tutorial/Badge.js new file mode 100644 index 0000000..1a7d625 --- /dev/null +++ b/src/components/Tutorial/Badge.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { assigneBadge } from '../../actions/tutorialActions'; + +import Dialog from '../Dialog'; + +class Badge extends Component { + + state = { + open: false, + title: '', + content: '' + }; + + componentDidUpdate(props){ + if(this.props.tutorial.badge){ + if(this.isSuccess()){ + // is connected to MyBadges? + if(this.props.isAuthenticated && this.props.user && this.props.user.badge){ + // if(!this.props.user.badges.include(this.props.tutorial.badge)){ + this.props.assigneBadge(this.props.tutorial.badge); + } + // } + } + } + if(props.message !== this.props.message){ + if(this.props.message.id === 'ASSIGNE_BADGE_SUCCESS'){ + alert('Badge '+props.message.msg.name); + this.setState({title: '', content: '', open: true}); + } + } + } + + isSuccess = () => { + var tutorialId = this.props.tutorial._id; + var status = this.props.status.filter(status => status._id === tutorialId)[0]; + var tasks = status.tasks; + var success = tasks.filter(task => task.type === 'success').length / tasks.length; + if(success===1){ + return true; + } + return false; + } + + toggleDialog = () => { + this.setState({ open: !this.state, title: '', content: '' }); + } + + render() { + return ( + {this.toggleDialog();}} + onClick={() => {this.toggleDialog();}} + button={'Schließen'} + > +
+ +
+
+ ); + }; +} + +Badge.propTypes = { + assigneBadge: PropTypes.func.isRequired, + status: PropTypes.array.isRequired, + change: PropTypes.number.isRequired, + tutorial: PropTypes.object.isRequired, + user: PropTypes.object, + isAuthenticated: PropTypes.bool.isRequired +}; + +const mapStateToProps = state => ({ + change: state.tutorial.change, + status: state.tutorial.status, + tutorial: state.tutorial.tutorials[0], + user: state.auth.user, + isAuthenticated: state.auth.isAuthenticated +}); + +export default connect(mapStateToProps, { assigneBadge })(Badge); diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 9656d00..78c7657 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -181,6 +181,7 @@ class Builder extends Component { var steps = this.props.steps; var newTutorial = new FormData(); newTutorial.append('title', this.props.title); + newTutorial.append('badge', this.props.badge); steps.forEach((step, i) => { newTutorial.append(`steps[${i}][type]`, step.type); newTutorial.append(`steps[${i}][headline]`, step.headline); @@ -319,6 +320,7 @@ class Builder extends Component { : null} {/* */} + {this.props.steps.map((step, i) => @@ -390,19 +392,21 @@ Builder.propTypes = { getTutorials: PropTypes.func.isRequired, resetTutorial: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired, - checkError: PropTypes.func.isRequired, tutorialId: PropTypes.func.isRequired, + checkError: PropTypes.func.isRequired, readJSON: PropTypes.func.isRequired, jsonString: PropTypes.func.isRequired, progress: PropTypes.func.isRequired, deleteTutorial: PropTypes.func.isRequired, resetTutorialBuilder: PropTypes.func.isRequired, title: PropTypes.string.isRequired, + badge: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, steps: PropTypes.array.isRequired, change: PropTypes.number.isRequired, error: PropTypes.object.isRequired, json: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, + badge: PropTypes.string.isRequired, isProgress: PropTypes.bool.isRequired, tutorials: PropTypes.array.isRequired, message: PropTypes.object.isRequired, @@ -411,6 +415,7 @@ Builder.propTypes = { const mapStateToProps = state => ({ title: state.builder.title, + badge: state.builder.badge, id: state.builder.id, steps: state.builder.steps, change: state.builder.change, diff --git a/src/components/Tutorial/Builder/Textfield.js b/src/components/Tutorial/Builder/Textfield.js index fa39547..74a57f1 100644 --- a/src/components/Tutorial/Builder/Textfield.js +++ b/src/components/Tutorial/Builder/Textfield.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { tutorialTitle, jsonString, changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions'; +import { tutorialTitle, tutorialBadge, jsonString, changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions'; import { withStyles } from '@material-ui/core/styles'; import OutlinedInput from '@material-ui/core/OutlinedInput'; @@ -42,6 +42,9 @@ class Textfield extends Component { else if(this.props.property === 'json'){ this.props.jsonString(value); } + else if(this.props.property === 'badge'){ + this.props.tutorialBadge(value); + } else { this.props.changeContent(value, this.props.index, this.props.property, this.props.property2); } @@ -86,8 +89,9 @@ class Textfield extends Component { Textfield.propTypes = { tutorialTitle: PropTypes.func.isRequired, + tutorialBadge: PropTypes.func.isRequired, jsonString: PropTypes.func.isRequired, changeContent: PropTypes.func.isRequired, }; -export default connect(null, { tutorialTitle, jsonString, changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(Textfield)); +export default connect(null, { tutorialTitle, tutorialBadge, jsonString, changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(Textfield)); diff --git a/src/components/Tutorial/Tutorial.js b/src/components/Tutorial/Tutorial.js index 47f6a7b..d86a958 100644 --- a/src/components/Tutorial/Tutorial.js +++ b/src/components/Tutorial/Tutorial.js @@ -10,6 +10,7 @@ import StepperHorizontal from './StepperHorizontal'; import StepperVertical from './StepperVertical'; import Instruction from './Instruction'; import Assessment from './Assessment'; +import Badge from './Badge'; import NotFound from '../NotFound'; import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace'; @@ -57,6 +58,7 @@ class Tutorial extends Component { +
diff --git a/src/reducers/tutorialBuilderReducer.js b/src/reducers/tutorialBuilderReducer.js index 132de0a..cad4c2e 100644 --- a/src/reducers/tutorialBuilderReducer.js +++ b/src/reducers/tutorialBuilderReducer.js @@ -1,10 +1,11 @@ -import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP,BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from '../actions/types'; +import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_BADGE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP,BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from '../actions/types'; const initialState = { change: 0, progress: false, json: '', title: '', + badge: '', id: '', steps: [ { @@ -33,6 +34,11 @@ export default function(state = initialState, action){ ...state, title: action.payload }; + case BUILDER_BADGE: + return { + ...state, + badge: action.payload + }; case BUILDER_ID: return { ...state, From 5430e783ccd214523687c8e5d2367b8d85a57df3 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 10 Dec 2020 10:31:43 +0100 Subject: [PATCH 32/32] show assigned badge --- src/actions/authActions.js | 2 ++ src/actions/tutorialActions.js | 9 ++++- src/components/Tutorial/Badge.js | 57 +++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/actions/authActions.js b/src/actions/authActions.js index cb06139..7cdd1c0 100644 --- a/src/actions/authActions.js +++ b/src/actions/authActions.js @@ -86,6 +86,7 @@ export const connectMyBadges = ({ username, password }) => (dispatch, getState) .then(res => { var user = getState().auth.user; user.badge = res.data.account; + user.badges = res.data.badges; dispatch({ type: MYBADGES_CONNECT, payload: user @@ -109,6 +110,7 @@ export const disconnectMyBadges = () => (dispatch, getState) => { .then(res => { var user = getState().auth.user; user.badge = null; + user.badges = null; dispatch({ type: MYBADGES_DISCONNECT, payload: user diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index a02c37d..8983da9 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -1,4 +1,4 @@ -import { TUTORIAL_PROGRESS, GET_TUTORIAL, GET_TUTORIALS, TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types'; +import { MYBADGES_DISCONNECT, TUTORIAL_PROGRESS, GET_TUTORIAL, GET_TUTORIALS, TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types'; import axios from 'axios'; import { returnErrors, returnSuccess } from './messageActions'; @@ -60,6 +60,12 @@ export const assigneBadge = (id) => (dispatch, getState) => { axios.put(`${process.env.REACT_APP_BLOCKLY_API}/user/badge/${id}`) .then(res => { var badge = res.data.badge; + var user = getState().auth.user; + user.badges.push(badge._id); + dispatch({ + type: MYBADGES_DISCONNECT, + payload: user + }); dispatch(returnSuccess(badge, res.status, 'ASSIGNE_BADGE_SUCCESS')); }) .catch(err => { @@ -122,6 +128,7 @@ export const tutorialCheck = (status, step) => (dispatch, getState) => { payload: tutorialsStatus }); dispatch(tutorialChange()); + dispatch(returnSuccess('','','TUTORIAL_CHECK_SUCCESS')); }; export const storeTutorialXml = (code) => (dispatch, getState) => { diff --git a/src/components/Tutorial/Badge.js b/src/components/Tutorial/Badge.js index 1a7d625..97103fa 100644 --- a/src/components/Tutorial/Badge.js +++ b/src/components/Tutorial/Badge.js @@ -5,6 +5,25 @@ import { assigneBadge } from '../../actions/tutorialActions'; import Dialog from '../Dialog'; +import { Link } from 'react-router-dom'; + +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import Avatar from '@material-ui/core/Avatar'; + +const styles = (theme) => ({ + link: { + color: theme.palette.primary.main, + textDecoration: 'none', + '&:hover': { + color: theme.palette.primary.main, + textDecoration: 'underline' + } + } +}); + + class Badge extends Component { state = { @@ -14,20 +33,21 @@ class Badge extends Component { }; componentDidUpdate(props){ - if(this.props.tutorial.badge){ - if(this.isSuccess()){ - // is connected to MyBadges? + if(this.props.message.id === 'TUTORIAL_CHECK_SUCCESS'){ + if(this.props.tutorial.badge){ + // is connected to MyBadges? if(this.props.isAuthenticated && this.props.user && this.props.user.badge){ - // if(!this.props.user.badges.include(this.props.tutorial.badge)){ - this.props.assigneBadge(this.props.tutorial.badge); + if(this.props.user.badges && !this.props.user.badges.includes(this.props.tutorial.badge)){ + if(this.isSuccess()){ + this.props.assigneBadge(this.props.tutorial.badge); + } } - // } + } } } if(props.message !== this.props.message){ if(this.props.message.id === 'ASSIGNE_BADGE_SUCCESS'){ - alert('Badge '+props.message.msg.name); - this.setState({title: '', content: '', open: true}); + this.setState({title: `Badge: ${this.props.message.msg.name}`, content: `Herzlichen Glückwunsch! Du hast den Badge ${this.props.message.msg.name} erhalten.`, open: true}); } } } @@ -50,6 +70,7 @@ class Badge extends Component { render() { return (
- + + {this.props.message.msg.image && this.props.message.msg.image.path ? + + : } + +
{this.props.message.msg.name}
+
+
+ + Eine Übersicht über alle erhaltenen Badges im Kontext Blockly for senseBox findest du hier. +
); @@ -71,7 +102,8 @@ Badge.propTypes = { change: PropTypes.number.isRequired, tutorial: PropTypes.object.isRequired, user: PropTypes.object, - isAuthenticated: PropTypes.bool.isRequired + isAuthenticated: PropTypes.bool.isRequired, + message: PropTypes.object.isRequired }; const mapStateToProps = state => ({ @@ -79,7 +111,8 @@ const mapStateToProps = state => ({ status: state.tutorial.status, tutorial: state.tutorial.tutorials[0], user: state.auth.user, - isAuthenticated: state.auth.isAuthenticated + isAuthenticated: state.auth.isAuthenticated, + message: state.message }); -export default connect(mapStateToProps, { assigneBadge })(Badge); +export default connect(mapStateToProps, { assigneBadge })(withStyles(styles, { withTheme: true })(Badge));