diff --git a/package.json b/package.json index b57e05d..2799fb2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-markdown": "^8.0.0", "react-markdown-editor-lite": "^1.3.2", "react-mde": "^11.5.0", + "react-rating-stars-component": "^2.2.0", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "^5.0.0", diff --git a/src/App.css b/src/App.css index 9903a22..8457170 100644 --- a/src/App.css +++ b/src/App.css @@ -43,6 +43,20 @@ blockquote p { display: inline; } +.tutorial table, +th, +td { + border: 1px solid #ddd; +} + +.tutorial th { + padding-top: 12px; + padding-bottom: 12px; + text-align: left; + background-color: #4eaf47; + color: white; +} + .overlay { display: flex; flex-direction: column; diff --git a/src/actions/tutorialActions.js b/src/actions/tutorialActions.js index 372697a..226bba6 100644 --- a/src/actions/tutorialActions.js +++ b/src/actions/tutorialActions.js @@ -84,6 +84,77 @@ export const getTutorials = () => (dispatch, getState) => { }); }; +export const getAllTutorials = () => (dispatch, getState) => { + axios + .get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/getAllTutorials`) + .then((res) => { + var tutorials = res.data.tutorials; + existingTutorials(tutorials, getState().tutorial.status).then( + (status) => { + dispatch({ + type: TUTORIAL_SUCCESS, + payload: status, + }); + dispatch(updateStatus(status)); + dispatch({ + type: GET_TUTORIALS, + payload: tutorials, + }); + dispatch({ type: TUTORIAL_PROGRESS }); + dispatch(returnSuccess(res.data.message, res.status)); + } + ); + }) + .catch((err) => { + if (err.response) { + dispatch( + returnErrors( + err.response.data.message, + err.response.status, + "GET_TUTORIALS_FAIL" + ) + ); + } + dispatch({ type: TUTORIAL_PROGRESS }); + }); +}; + +export const getUserTutorials = () => (dispatch, getState) => { + axios + .get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/getUserTutorials`) + .then((res) => { + var tutorials = res.data.tutorials; + existingTutorials(tutorials, getState().tutorial.status).then( + (status) => { + dispatch({ + type: TUTORIAL_SUCCESS, + payload: status, + }); + dispatch(updateStatus(status)); + dispatch({ + type: GET_TUTORIALS, + payload: tutorials, + }); + dispatch({ type: TUTORIAL_PROGRESS }); + dispatch(returnSuccess(res.data.message, res.status)); + } + ); + }) + .catch((err) => { + console.log(err); + if (err.response) { + dispatch( + returnErrors( + err.response.data.message, + err.response.status, + "GET_TUTORIALS_FAIL" + ) + ); + } + dispatch({ type: TUTORIAL_PROGRESS }); + }); +}; + export const updateStatus = (status) => (dispatch, getState) => { if (getState().auth.isAuthenticated) { // update user account in database - sync with redux store diff --git a/src/actions/tutorialBuilderActions.js b/src/actions/tutorialBuilderActions.js index 72d7f3e..cb4e798 100644 --- a/src/actions/tutorialBuilderActions.js +++ b/src/actions/tutorialBuilderActions.js @@ -4,6 +4,9 @@ import { BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, + BUILDER_PUBLIC, + BUILDER_DIFFICULTY, + BUILDER_REVIEW, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, @@ -35,6 +38,30 @@ export const tutorialTitle = (title) => (dispatch) => { dispatch(changeTutorialBuilder()); }; +export const tutorialPublic = (pub) => (dispatch) => { + dispatch({ + type: BUILDER_PUBLIC, + payload: pub, + }); + dispatch(changeTutorialBuilder()); +}; + +export const tutorialDifficulty = (difficulty) => (dispatch) => { + dispatch({ + type: BUILDER_DIFFICULTY, + payload: difficulty, + }); + dispatch(changeTutorialBuilder()); +}; + +export const tutorialReview = (review) => (dispatch) => { + dispatch({ + type: BUILDER_REVIEW, + payload: review, + }); + dispatch(changeTutorialBuilder()); +}; + export const tutorialSteps = (steps) => (dispatch) => { dispatch({ type: BUILDER_ADD_STEP, @@ -320,6 +347,7 @@ export const readJSON = (json) => (dispatch, getState) => { return object; }); dispatch(tutorialTitle(json.title)); + dispatch(tutorialDifficulty(json.difficulty)); dispatch(tutorialSteps(steps)); dispatch(setSubmitError()); dispatch(progress(false)); diff --git a/src/actions/types.js b/src/actions/types.js index 6a10987..84e2580 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -21,6 +21,7 @@ export const NAME = "NAME"; export const TUTORIAL_PROGRESS = "TUTORIAL_PROGRESS"; export const GET_TUTORIAL = "GET_TUTORIAL"; export const GET_TUTORIALS = "GET_TUTORIALS"; +export const GET_USERTUTORIALS = "GET_USERTUTORIALS"; export const GET_STATUS = "GET_STATUS"; export const TUTORIAL_SUCCESS = "TUTORIAL_SUCCESS"; export const TUTORIAL_ERROR = "TUTORIAL_ERROR"; @@ -32,6 +33,9 @@ export const JSON_STRING = "JSON_STRING"; export const BUILDER_CHANGE = "BUILDER_CHANGE"; export const BUILDER_TITLE = "BUILDER_TITLE"; +export const BUILDER_DIFFICULTY = "BUILDER_DIFFICULTY"; +export const BUILDER_PUBLIC = "BUILDER_PUBLIC"; +export const BUILDER_REVIEW = "BUILDER_REVIEW"; 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/Blockly/msg/de/ui.js b/src/components/Blockly/msg/de/ui.js index 901d495..7e598ad 100644 --- a/src/components/Blockly/msg/de/ui.js +++ b/src/components/Blockly/msg/de/ui.js @@ -228,6 +228,7 @@ export const UI = { builder_requirements_head: "Voraussetzungen", builder_requirements_order: "Beachte, dass die Reihenfolge des Anhakens maßgebend ist.", + builder_difficulty: "Schwierigkeitsgrad", /** * Login diff --git a/src/components/Tutorial/Builder/Builder.js b/src/components/Tutorial/Builder/Builder.js index 26c53fd..a21738c 100644 --- a/src/components/Tutorial/Builder/Builder.js +++ b/src/components/Tutorial/Builder/Builder.js @@ -10,6 +10,8 @@ import { resetTutorial as resetTutorialBuilder, } from "../../../actions/tutorialBuilderActions"; import { + getAllTutorials, + getUserTutorials, getTutorials, resetTutorial, deleteTutorial, @@ -19,12 +21,17 @@ import { clearMessages } from "../../../actions/messageActions"; import axios from "axios"; import { withRouter } from "react-router-dom"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEyeSlash, faUserCheck } from "@fortawesome/free-solid-svg-icons"; import Breadcrumbs from "../../Breadcrumbs"; import Textfield from "./Textfield"; +import Difficulty from "./Difficulty"; import Step from "./Step"; import Dialog from "../../Dialog"; import Snackbar from "../../Snackbar"; +import Public from "./Public"; +import Review from "./Review"; import { withStyles } from "@material-ui/core/styles"; import Button from "@material-ui/core/Button"; @@ -66,6 +73,8 @@ class Builder extends Component { tutorial: "new", open: false, title: "", + public: false, + difficulty: "", content: "", string: false, snackbar: false, @@ -80,7 +89,11 @@ class Builder extends Component { // retrieve tutorials only if a potential user is loaded - authentication // is finished (success or failed) if (!this.props.authProgress) { - this.props.getTutorials(); + if (this.props.user.role === "admin") { + this.props.getAllTutorials(); + } else { + this.props.getUserTutorials(); + } } } @@ -89,8 +102,12 @@ class Builder extends Component { props.authProgress !== this.props.authProgress && !this.props.authProgress ) { - // authentication is completed - this.props.getTutorials(); + if (this.props.user.role === "admin") { + // authentication is completed + this.props.getAllTutorials(); + } else { + this.props.getUserTutorials(); + } } if (props.message !== this.props.message) { if (this.props.message.id === "GET_TUTORIALS_FAIL") { @@ -258,6 +275,9 @@ class Builder extends Component { var steps = this.props.steps; var newTutorial = new FormData(); newTutorial.append("title", this.props.title); + newTutorial.append("difficulty", this.props.difficulty); + newTutorial.append("public", this.props.public); + newTutorial.append("review", this.props.review); steps.forEach((step, i) => { if (step._id) { newTutorial.append(`steps[${i}][_id]`, step._id); @@ -284,6 +304,7 @@ class Builder extends Component { newTutorial.append(`steps[${i}][xml]`, step.xml); } }); + console.log(newTutorial); return newTutorial; } }; @@ -362,6 +383,12 @@ class Builder extends Component { (tutorial) => tutorial.creator === this.props.user.email ); } + + // } else { + // filteredTutorials = this.props.userTutorials.filter( + // (tutorial) => tutorial.creator === this.props.user.email + // ); + return (
{filteredTutorials.map((tutorial) => ( - {tutorial.title} + + {tutorial.title}{" "} + {tutorial.review && tutorial.public === false ? ( +
+ + +
+ ) : tutorial.public === false ? ( + + ) : null} +
+ /* ) : tutorial.public === false ? ( + + {tutorial.title} + + ) : ( + {tutorial.title} + )} */ ))} @@ -476,6 +520,45 @@ class Builder extends Component { label={"Titel"} error={this.props.error.title} /> +
+ + + + + + + {this.props.user.blocklyRole === "admin" ? ( + + ) : null} +
{this.props.steps.map((step, i) => ( @@ -608,6 +691,8 @@ class Builder extends Component { } Builder.propTypes = { + getAllTutorials: PropTypes.func.isRequired, + getUserTutorials: PropTypes.func.isRequired, getTutorials: PropTypes.func.isRequired, resetTutorial: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired, @@ -620,6 +705,9 @@ Builder.propTypes = { resetTutorialBuilder: PropTypes.func.isRequired, tutorialProgress: PropTypes.func.isRequired, title: PropTypes.string.isRequired, + difficulty: PropTypes.number.isRequired, + public: PropTypes.bool.isRequired, + review: PropTypes.bool.isRequired, id: PropTypes.string.isRequired, steps: PropTypes.array.isRequired, change: PropTypes.number.isRequired, @@ -634,12 +722,16 @@ Builder.propTypes = { const mapStateToProps = (state) => ({ title: state.builder.title, + difficulty: state.builder.difficulty, + review: state.builder.review, + public: state.builder.public, id: state.builder.id, steps: state.builder.steps, change: state.builder.change, error: state.builder.error, json: state.builder.json, isProgress: state.builder.progress, + userTutorials: state.tutorial.userTutorials, tutorials: state.tutorial.tutorials, message: state.message, user: state.auth.user, @@ -654,6 +746,8 @@ export default connect(mapStateToProps, { tutorialId, resetTutorialBuilder, getTutorials, + getUserTutorials, + getAllTutorials, resetTutorial, tutorialProgress, clearMessages, diff --git a/src/components/Tutorial/Builder/Difficulty.js b/src/components/Tutorial/Builder/Difficulty.js new file mode 100644 index 0000000..18111b9 --- /dev/null +++ b/src/components/Tutorial/Builder/Difficulty.js @@ -0,0 +1,101 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + tutorialDifficulty, + jsonString, + changeContent, + setError, + deleteError, +} from "../../../actions/tutorialBuilderActions"; + +import { withStyles } from "@material-ui/core/styles"; +import ReactStars from "react-rating-stars-component"; +import * as Blockly from "blockly"; +import FormGroup from "@material-ui/core/FormGroup"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormControl from "@material-ui/core/FormControl"; +import FormLabel from "@material-ui/core/FormLabel"; + +const styles = (theme) => ({ + multiline: { + padding: "18.5px 14px 18.5px 24px", + }, + errorColor: { + color: `${theme.palette.error.dark} !important`, + }, + errorColorShrink: { + color: `rgba(0, 0, 0, 0.54) !important`, + }, + errorBorder: { + borderColor: `${theme.palette.error.dark} !important`, + }, +}); + +class Difficulty extends Component { + ratingChanged = (newRating) => { + console.log(newRating); + this.handleChange(newRating); + }; + + handleChange = (e) => { + var value = e; + console.log(value); + if (this.props.property === "difficulty") { + this.props.tutorialDifficulty(value); + } else if (this.props.property === "json") { + this.props.jsonString(value); + } else { + this.props.changeContent( + value, + this.props.index, + this.props.property, + this.props.property2 + ); + } + }; + + render() { + return ( + + + {Blockly.Msg.builder_difficulty} + + + } + halfIcon={} + fullIcon={} + activeColor="#ffd700" + /> + } + label="Schwierigkeitsgrad" + labelPlacement="start" + /> + + + ); + } +} + +Difficulty.propTypes = { + tutorialDifficulty: PropTypes.func.isRequired, + jsonString: PropTypes.func.isRequired, + changeContent: PropTypes.func.isRequired, +}; + +export default connect(null, { + tutorialDifficulty, + jsonString, + changeContent, + setError, + deleteError, +})(withStyles(styles, { withTheme: true })(Difficulty)); diff --git a/src/components/Tutorial/Builder/Public.js b/src/components/Tutorial/Builder/Public.js new file mode 100644 index 0000000..ff8658e --- /dev/null +++ b/src/components/Tutorial/Builder/Public.js @@ -0,0 +1,91 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + tutorialPublic, + jsonString, + changeContent, + setError, + deleteError, +} from "../../../actions/tutorialBuilderActions"; + +import { withStyles } from "@material-ui/core/styles"; + +import * as Blockly from "blockly"; +import Checkbox from "@material-ui/core/Checkbox"; +import FormGroup from "@material-ui/core/FormGroup"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormControl from "@material-ui/core/FormControl"; +import FormLabel from "@material-ui/core/FormLabel"; + +const styles = (theme) => ({ + multiline: { + padding: "18.5px 14px 18.5px 24px", + }, + errorColor: { + color: `${theme.palette.error.dark} !important`, + }, + errorColorShrink: { + color: `rgba(0, 0, 0, 0.54) !important`, + }, + errorBorder: { + borderColor: `${theme.palette.error.dark} !important`, + }, +}); + +class Public extends Component { + handleChange = (e) => { + var value = e.target.checked; + console.log(value); + if (this.props.property === "public") { + this.props.tutorialPublic(value); + } else if (this.props.property === "json") { + this.props.jsonString(value); + } else { + this.props.changeContent( + value, + this.props.index, + this.props.property, + this.props.property2 + ); + } + }; + + render() { + return ( + + Tutorial veröffentlichen + + + } + label="Tutorial veröffentlichen" + labelPlacement="start" + /> + + + ); + } +} + +Public.propTypes = { + tutorialPublic: PropTypes.func.isRequired, + jsonString: PropTypes.func.isRequired, + changeContent: PropTypes.func.isRequired, +}; + +export default connect(null, { + tutorialPublic, + jsonString, + changeContent, + setError, + deleteError, +})(withStyles(styles, { withTheme: true })(Public)); diff --git a/src/components/Tutorial/Builder/Review.js b/src/components/Tutorial/Builder/Review.js new file mode 100644 index 0000000..1f51a1f --- /dev/null +++ b/src/components/Tutorial/Builder/Review.js @@ -0,0 +1,97 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + tutorialReview, + jsonString, + changeContent, + setError, + deleteError, +} from "../../../actions/tutorialBuilderActions"; + +import { withStyles } from "@material-ui/core/styles"; + +import * as Blockly from "blockly"; +import Checkbox from "@material-ui/core/Checkbox"; +import FormGroup from "@material-ui/core/FormGroup"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormControl from "@material-ui/core/FormControl"; +import FormLabel from "@material-ui/core/FormLabel"; + +const styles = (theme) => ({ + multiline: { + padding: "18.5px 14px 18.5px 24px", + }, + errorColor: { + color: `${theme.palette.error.dark} !important`, + }, + errorColorShrink: { + color: `rgba(0, 0, 0, 0.54) !important`, + }, + errorBorder: { + borderColor: `${theme.palette.error.dark} !important`, + }, +}); + +class Review extends Component { + handleChange = (e) => { + var value = e.target.checked; + if (this.props.property === "review") { + this.props.tutorialReview(value); + } else if (this.props.property === "json") { + this.props.jsonString(value); + } else { + this.props.changeContent( + value, + this.props.index, + this.props.property, + this.props.property2 + ); + } + }; + + render() { + return ( + + Tutorial veröffentlichen +

+ {" "} + Du kannst dein Tutorial direkt über den Link mit anderen Personen + teilen. Wenn du dein Tutorial für alle Nutzer:innen in der Überischt + veröffenltichen wollen kannst du es hier aktivieren. Ein Administrator + wird dein Tutorial ansehen und anschließend freischalten. +

+ + + } + label="Ich möchte mein Tutorial öffentlich machen" + labelPlacement="start" + /> + +
+ ); + } +} + +Review.propTypes = { + tutorialReview: PropTypes.func.isRequired, + jsonString: PropTypes.func.isRequired, + changeContent: PropTypes.func.isRequired, +}; + +export default connect(null, { + tutorialReview, + jsonString, + changeContent, + setError, + deleteError, +})(withStyles(styles, { withTheme: true })(Review)); diff --git a/src/components/Tutorial/Requirement.js b/src/components/Tutorial/Requirement.js index 87784cc..81584ad 100644 --- a/src/components/Tutorial/Requirement.js +++ b/src/components/Tutorial/Requirement.js @@ -1,124 +1,234 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; -import clsx from 'clsx'; -import { withRouter, Link } from 'react-router-dom'; +import clsx from "clsx"; +import { withRouter, Link } from "react-router-dom"; -import { fade } from '@material-ui/core/styles/colorManipulator'; -import { withStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import List from '@material-ui/core/List'; -import Tooltip from '@material-ui/core/Tooltip'; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import { withStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import List from "@material-ui/core/List"; +import Tooltip from "@material-ui/core/Tooltip"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; -import * as Blockly from 'blockly' +import * as Blockly from "blockly"; -const styles = theme => ({ +const styles = (theme) => ({ outerDiv: { - width: '50px', - height: '50px', - position: 'absolute', - color: fade(theme.palette.secondary.main, 0.6) + width: "50px", + height: "50px", + position: "absolute", + 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) + 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) + color: fade(theme.palette.primary.main, 0.6), }, outerDivOther: { - stroke: fade(theme.palette.secondary.main, 0.6) + stroke: fade(theme.palette.secondary.main, 0.6), }, innerDiv: { - width: 'inherit', - height: 'inherit', - display: 'table-cell', - verticalAlign: 'middle', - textAlign: 'center' + width: "inherit", + height: "inherit", + display: "table-cell", + verticalAlign: "middle", + textAlign: "center", }, link: { color: theme.palette.text.primary, - position: 'relative', - height: '50px', - display: 'flex', - margin: '5px 0 5px 10px', - textDecoration: 'none' + position: "relative", + height: "50px", + display: "flex", + margin: "5px 0 5px 10px", + textDecoration: "none", }, hoverLink: { - '&:hover': { + "&:hover": { background: fade(theme.palette.secondary.main, 0.5), - borderRadius: '0 25px 25px 0 ' - } - } + borderRadius: "0 25px 25px 0 ", + }, + }, }); - class Requirement extends Component { - render() { var requirements = this.props.requirements; - var tutorialIds = requirements.map(requirement => requirement._id); + var tutorialIds = requirements.map((requirement) => requirement._id); return ( -
+
{Blockly.Msg.tutorials_requirements} {tutorialIds.map((tutorialId, i) => { - var title = requirements[i].title - var status = this.props.status.filter(status => status._id === tutorialId)[0]; + 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; - var success = status.tasks.filter(task => task.type === 'success').length / tasks.length - var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other'; + 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 ( - - + +
-
- - {error || success === 1 ? - - : } - {success < 1 && !error ? - - - : null} +
+ + {error || success === 1 ? ( + + ) : ( + + )} + {success < 1 && !error ? ( + + ) : null}
-
+
- {error || success === 1 ? - - : 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success * 100)}% - } + {error || success === 1 ? ( + + ) : ( + 0 + ? this.props.classes.outerDivSuccess + : {} + } + > + {Math.round(success * 100)}% + + )}
-
- {title} +
+ + {title} +
- ) - } - )} + ); + })}
); - }; + } } Requirement.propTypes = { status: PropTypes.array.isRequired, - change: PropTypes.number.isRequired + change: PropTypes.number.isRequired, }; -const mapStateToProps = state => ({ +const mapStateToProps = (state) => ({ change: state.tutorial.change, status: state.tutorial.status, }); -export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Requirement))); +export default connect( + mapStateToProps, + null +)(withStyles(styles, { withTheme: true })(withRouter(Requirement))); diff --git a/src/components/Tutorial/TutorialHome.js b/src/components/Tutorial/TutorialHome.js index ee91c30..9df9aef 100644 --- a/src/components/Tutorial/TutorialHome.js +++ b/src/components/Tutorial/TutorialHome.js @@ -3,9 +3,13 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import { getTutorials, + getAllTutorials, + getUserTutorials, resetTutorial, tutorialProgress, } from "../../actions/tutorialActions"; +import { progress } from "../../actions/tutorialBuilderActions"; + import { clearMessages } from "../../actions/messageActions"; import clsx from "clsx"; @@ -20,9 +24,20 @@ import Grid from "@material-ui/core/Grid"; import Paper from "@material-ui/core/Paper"; import Typography from "@material-ui/core/Typography"; -import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { + faCheck, + faTimes, + faShareAlt, + faEye, + faUserCheck, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Blockly from "blockly"; +import ReactStars from "react-rating-stars-component"; +import Tooltip from "@material-ui/core/Tooltip"; +import IconButton from "@material-ui/core/IconButton"; +import Snackbar from "../Snackbar"; +import Divider from "@material-ui/core/Divider"; const styles = (theme) => ({ outerDiv: { @@ -51,26 +66,89 @@ const styles = (theme) => ({ verticalAlign: "middle", textAlign: "center", }, + 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 TutorialHome extends Component { + constructor(props) { + super(props); + this.state = { + userTutorials: [], + tutorials: [], + snackbar: false, + }; + } + componentDidMount() { this.props.tutorialProgress(); // retrieve tutorials only if a potential user is loaded - authentication // is finished (success or failed) - if (!this.props.progress) { - this.props.getTutorials(); + // if (!this.props.progress) { + // if (this.props.user) { + // if (this.props.user.blocklyRole === "admin") { + // this.props.getAllTutorials(); + // } + // if (this.props.user.blocklyRole === "creator") { + // this.props.getUserTutorials(); + // this.props.getTutorials(); + // console.log("get user tutorials"); + // console.log(this.props.userTutorials); + // } + // } else { + // this.props.getTutorials(); + // } + // } + if (!this.props.authProgress) { + if (this.props.user) { + if (this.props.user.role === "admin") { + this.props.getAllTutorials(); + } else { + this.props.getUserTutorials(); + //this.props.getTutorials(); + } + } else { + this.props.getTutorials(); + } } } componentDidUpdate(props, state) { - if (props.progress !== this.props.progress && !this.props.progress) { - // authentication is completed - this.props.getTutorials(); - } + if ( + props.authProgress !== this.props.authProgress && + !this.props.authProgress + ) + if (this.props.user) { + if (this.props.user.role === "admin") { + // authentication is completed + this.props.getAllTutorials(); + } else { + this.props.getUserTutorials(); + } + } else { + this.props.getTutorials(); + } + if (this.props.message.id === "GET_TUTORIALS_FAIL") { alert(this.props.message.msg); } + console.log(this.props.user); } componentWillUnmount() { @@ -81,13 +159,25 @@ class TutorialHome extends Component { } render() { + var userTutorials = []; + const publicTutorials = this.props.tutorials.filter( + (tutorial) => tutorial.public === true + ); + if (this.props.user && this.props.user.blocklyRole === "admin") { + userTutorials = this.props.tutorials; + } + if (this.props.user && this.props.user.blocklyRole === "creator") { + userTutorials = this.props.tutorials.filter( + (tutorial) => tutorial.creator === this.props.user.email + ); + } return this.props.isLoading ? null : (
-

{Blockly.Msg.tutorials_home_head}

+

Alle Tutorials

- {this.props.tutorials.map((tutorial, i) => { + {publicTutorials.map((tutorial, i) => { var status = this.props.status.filter( (status) => status._id === tutorial._id )[0]; @@ -99,6 +189,12 @@ class TutorialHome extends Component { tasks.length; var tutorialStatus = success === 1 ? "Success" : error ? "Error" : "Other"; + const firstExample = { + size: 30, + value: tutorial.difficulty, + edit: false, + isHalf: true, + }; return ( {tutorial.title} +
+ {this.props.user ? ( +
+

User Tutorials

+ + {userTutorials.map((tutorial, i) => { + 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"; + const firstExample = { + size: 30, + value: tutorial.difficulty, + edit: false, + isHalf: true, + }; + return ( + + + + {tutorial.title} + + + +

+ Creator:{tutorial.creator}
+

+ + { + navigator.clipboard.writeText( + `${window.location.origin}/tutorial/${tutorial._id}` + ); + this.setState({ + snackbar: true, + key: Date.now(), + message: + Blockly.Msg.messages_copylink_success, + type: "success", + }); + }} + > + + + + + + + + + {tutorial.review ? ( + + + + + + ) : null} + +
+

+
+
+ ); + })} +
+
+ ) : null}
); } @@ -223,6 +433,8 @@ class TutorialHome extends Component { TutorialHome.propTypes = { getTutorials: PropTypes.func.isRequired, + getAllTutorials: PropTypes.func.isRequired, + getUserTutorials: PropTypes.func.isRequired, resetTutorial: PropTypes.func.isRequired, tutorialProgress: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired, @@ -232,19 +444,27 @@ TutorialHome.propTypes = { isLoading: PropTypes.bool.isRequired, message: PropTypes.object.isRequired, progress: PropTypes.bool.isRequired, + user: PropTypes.object.isRequired, + authProgress: PropTypes.bool.isRequired, }; const mapStateToProps = (state) => ({ change: state.tutorial.change, status: state.tutorial.status, tutorials: state.tutorial.tutorials, + userTutorials: state.tutorial.userTutorials, isLoading: state.tutorial.progress, message: state.message, progress: state.auth.progress, + user: state.auth.user, + authProgress: state.auth.progress, }); export default connect(mapStateToProps, { getTutorials, + progress, + getUserTutorials, + getAllTutorials, resetTutorial, clearMessages, tutorialProgress, diff --git a/src/components/Workspace/ShareProject.js b/src/components/Workspace/ShareProject.js index 8b962cc..e86d817 100644 --- a/src/components/Workspace/ShareProject.js +++ b/src/components/Workspace/ShareProject.js @@ -1,72 +1,87 @@ -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 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'; +import moment from "moment"; -import Dialog from '../Dialog'; -import Snackbar from '../Snackbar'; +import Dialog from "../Dialog"; +import Snackbar from "../Snackbar"; -import { Link } from 'react-router-dom'; +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 { 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"; -import * as Blockly from 'blockly/core'; +import * as Blockly from "blockly/core"; const styles = (theme) => ({ button: { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, - width: '40px', - height: '40px', - '&:hover': { + width: "40px", + height: "40px", + "&:hover": { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, - } + }, }, link: { color: theme.palette.primary.main, - textDecoration: 'none', - '&:hover': { + textDecoration: "none", + "&:hover": { color: theme.palette.primary.main, - textDecoration: 'underline' - } - } + textDecoration: "underline", + }, + }, }); - class WorkspaceFunc extends Component { - constructor(props) { super(props); this.inputRef = React.createRef(); this.state = { snackbar: false, - type: '', - key: '', - message: '', - title: '', - content: '', + type: "", + key: "", + message: "", + title: "", + content: "", open: false, - id: '', + 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.setState({ share: true, open: true, title: Blockly.Msg.messages_SHARE_SUCCESS, 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.setState({ snackbar: true, key: Date.now(), message: Blockly.Msg.messages_SHARE_FAIL, type: 'error' }); + 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: Blockly.Msg.messages_SHARE_SUCCESS, + 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.setState({ + snackbar: true, + key: Date.now(), + message: Blockly.Msg.messages_SHARE_FAIL, + type: "error", + }); window.scrollTo(0, 0); } } @@ -77,18 +92,25 @@ class WorkspaceFunc extends Component { } toggleDialog = () => { - this.setState({ open: !this.state, title: '', content: '' }); - } + this.setState({ open: !this.state, title: "", content: "" }); + }; shareBlocks = () => { - if (this.props.projectType === 'project' && this.props.project.shared) { + if (this.props.projectType === "project" && this.props.project.shared) { // project is already shared - this.setState({ open: true, title: Blockly.Msg.messages_SHARE_SUCCESS, id: this.props.project._id }); + this.setState({ + open: true, + title: Blockly.Msg.messages_SHARE_SUCCESS, + 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 : undefined + ); } - else { - this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id : undefined); - } - } + }; render() { return ( @@ -116,43 +138,89 @@ class WorkspaceFunc extends Component { onClick={this.toggleDialog} button={Blockly.Msg.button_close} > -
- Über den folgenden Link kannst du dein Programm teilen: - this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`} - +
+ + Ü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: Blockly.Msg.messages_copylink_success, type: 'success' }); + navigator.clipboard.writeText( + `${window.location.origin}/share/${this.state.id}` + ); + this.setState({ + snackbar: true, + key: Date.now(), + message: Blockly.Msg.messages_copylink_success, + type: "success", + }); }} > - {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.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.`}} + {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.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.`} + )}
); - }; + } } WorkspaceFunc.propTypes = { shareProject: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired, name: PropTypes.string.isRequired, - message: PropTypes.object.isRequired + message: PropTypes.object.isRequired, }; -const mapStateToProps = state => ({ +const mapStateToProps = (state) => ({ name: state.workspace.name, - message: state.message + message: state.message, }); -export default connect(mapStateToProps, { shareProject, clearMessages })(withStyles(styles, { withTheme: true })(WorkspaceFunc)); +export default connect(mapStateToProps, { shareProject, clearMessages })( + withStyles(styles, { withTheme: true })(WorkspaceFunc) +); diff --git a/src/reducers/tutorialBuilderReducer.js b/src/reducers/tutorialBuilderReducer.js index 1eb80f4..cb3a9dc 100644 --- a/src/reducers/tutorialBuilderReducer.js +++ b/src/reducers/tutorialBuilderReducer.js @@ -10,6 +10,9 @@ import { BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY, + BUILDER_DIFFICULTY, + BUILDER_PUBLIC, + BUILDER_REVIEW, } from "../actions/types"; const initialState = { @@ -17,6 +20,9 @@ const initialState = { progress: false, json: "", title: "", + difficulty: 0, + public: false, + review: false, id: "", steps: [ { @@ -45,6 +51,21 @@ export default function foo(state = initialState, action) { ...state, title: action.payload, }; + case BUILDER_PUBLIC: + return { + ...state, + public: action.payload, + }; + case BUILDER_DIFFICULTY: + return { + ...state, + difficulty: action.payload, + }; + case BUILDER_REVIEW: + return { + ...state, + review: action.payload, + }; case BUILDER_ID: return { ...state, diff --git a/src/reducers/tutorialReducer.js b/src/reducers/tutorialReducer.js index a44a31f..b60f7f7 100644 --- a/src/reducers/tutorialReducer.js +++ b/src/reducers/tutorialReducer.js @@ -1,24 +1,23 @@ -import { TUTORIAL_PROGRESS, GET_TUTORIAL, GET_TUTORIALS, GET_STATUS, TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_STEP } from '../actions/types'; - - -// -// const initialStatus = () => { -// if(store.getState().auth.user){ -// return store.getState().auth.user.status || [] -// } -// else if (window.localStorage.getItem('status')) { -// var status = JSON.parse(window.localStorage.getItem('status')); -// return status; -// } -// return []; -// }; +import { + TUTORIAL_PROGRESS, + GET_TUTORIAL, + GET_TUTORIALS, + GET_USERTUTORIALS, + GET_STATUS, + TUTORIAL_SUCCESS, + TUTORIAL_ERROR, + TUTORIAL_CHANGE, + TUTORIAL_XML, + TUTORIAL_STEP, +} from "../actions/types"; const initialState = { status: [], activeStep: 0, change: 0, tutorials: [], - progress: false + userTutorials: [], + progress: false, }; export default function foo(state = initialState, action) { @@ -26,18 +25,23 @@ export default function foo(state = initialState, action) { case TUTORIAL_PROGRESS: return { ...state, - progress: !state.progress - } + progress: !state.progress, + }; case GET_TUTORIALS: return { ...state, - tutorials: action.payload + tutorials: action.payload, + }; + case GET_USERTUTORIALS: + return { + ...state, + tutorials: action.payload, }; case GET_TUTORIAL: return { ...state, - tutorials: [action.payload] - } + tutorials: [action.payload], + }; case TUTORIAL_SUCCESS: case TUTORIAL_ERROR: case TUTORIAL_XML: @@ -46,23 +50,23 @@ export default function foo(state = initialState, action) { // and 'TUTORIAL_XML' the function 'updateStatus' is called return { ...state, - status: action.payload + status: action.payload, }; case GET_STATUS: return { ...state, - status: action.payload + status: action.payload, }; case TUTORIAL_CHANGE: return { ...state, - change: state.change += 1 - } + change: (state.change += 1), + }; case TUTORIAL_STEP: return { ...state, - activeStep: action.payload - } + activeStep: action.payload, + }; default: return state; } diff --git a/src/store.js b/src/store.js index bf6460e..3203857 100644 --- a/src/store.js +++ b/src/store.js @@ -1,6 +1,6 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import rootReducer from './reducers'; +import { createStore, applyMiddleware, compose } from "redux"; +import thunk from "redux-thunk"; +import rootReducer from "./reducers"; const initialState = {}; @@ -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__() ) );