update tutorial builder

- update tutorial modell
- add difficulty closes #82
- add review system
- make tutorials private by default
- add admin overview
This commit is contained in:
Mario Pesch 2022-02-28 19:56:19 +01:00
parent 39c8fd504f
commit 0414d2043d
16 changed files with 1097 additions and 172 deletions

View File

@ -33,6 +33,7 @@
"react-markdown": "^8.0.0", "react-markdown": "^8.0.0",
"react-markdown-editor-lite": "^1.3.2", "react-markdown-editor-lite": "^1.3.2",
"react-mde": "^11.5.0", "react-mde": "^11.5.0",
"react-rating-stars-component": "^2.2.0",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^5.0.0", "react-scripts": "^5.0.0",

View File

@ -43,6 +43,20 @@ blockquote p {
display: inline; 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 { .overlay {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -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) => { export const updateStatus = (status) => (dispatch, getState) => {
if (getState().auth.isAuthenticated) { if (getState().auth.isAuthenticated) {
// update user account in database - sync with redux store // update user account in database - sync with redux store

View File

@ -4,6 +4,9 @@ import {
BUILDER_CHANGE, BUILDER_CHANGE,
BUILDER_ERROR, BUILDER_ERROR,
BUILDER_TITLE, BUILDER_TITLE,
BUILDER_PUBLIC,
BUILDER_DIFFICULTY,
BUILDER_REVIEW,
BUILDER_ID, BUILDER_ID,
BUILDER_ADD_STEP, BUILDER_ADD_STEP,
BUILDER_DELETE_STEP, BUILDER_DELETE_STEP,
@ -35,6 +38,30 @@ export const tutorialTitle = (title) => (dispatch) => {
dispatch(changeTutorialBuilder()); 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) => { export const tutorialSteps = (steps) => (dispatch) => {
dispatch({ dispatch({
type: BUILDER_ADD_STEP, type: BUILDER_ADD_STEP,
@ -320,6 +347,7 @@ export const readJSON = (json) => (dispatch, getState) => {
return object; return object;
}); });
dispatch(tutorialTitle(json.title)); dispatch(tutorialTitle(json.title));
dispatch(tutorialDifficulty(json.difficulty));
dispatch(tutorialSteps(steps)); dispatch(tutorialSteps(steps));
dispatch(setSubmitError()); dispatch(setSubmitError());
dispatch(progress(false)); dispatch(progress(false));

View File

@ -21,6 +21,7 @@ export const NAME = "NAME";
export const TUTORIAL_PROGRESS = "TUTORIAL_PROGRESS"; export const TUTORIAL_PROGRESS = "TUTORIAL_PROGRESS";
export const GET_TUTORIAL = "GET_TUTORIAL"; export const GET_TUTORIAL = "GET_TUTORIAL";
export const GET_TUTORIALS = "GET_TUTORIALS"; export const GET_TUTORIALS = "GET_TUTORIALS";
export const GET_USERTUTORIALS = "GET_USERTUTORIALS";
export const GET_STATUS = "GET_STATUS"; export const GET_STATUS = "GET_STATUS";
export const TUTORIAL_SUCCESS = "TUTORIAL_SUCCESS"; export const TUTORIAL_SUCCESS = "TUTORIAL_SUCCESS";
export const TUTORIAL_ERROR = "TUTORIAL_ERROR"; 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_CHANGE = "BUILDER_CHANGE";
export const BUILDER_TITLE = "BUILDER_TITLE"; 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_ID = "BUILDER_ID";
export const BUILDER_ADD_STEP = "BUILDER_ADD_STEP"; export const BUILDER_ADD_STEP = "BUILDER_ADD_STEP";
export const BUILDER_DELETE_STEP = "BUILDER_DELETE_STEP"; export const BUILDER_DELETE_STEP = "BUILDER_DELETE_STEP";

View File

@ -228,6 +228,7 @@ export const UI = {
builder_requirements_head: "Voraussetzungen", builder_requirements_head: "Voraussetzungen",
builder_requirements_order: builder_requirements_order:
"Beachte, dass die Reihenfolge des Anhakens maßgebend ist.", "Beachte, dass die Reihenfolge des Anhakens maßgebend ist.",
builder_difficulty: "Schwierigkeitsgrad",
/** /**
* Login * Login

View File

@ -10,6 +10,8 @@ import {
resetTutorial as resetTutorialBuilder, resetTutorial as resetTutorialBuilder,
} from "../../../actions/tutorialBuilderActions"; } from "../../../actions/tutorialBuilderActions";
import { import {
getAllTutorials,
getUserTutorials,
getTutorials, getTutorials,
resetTutorial, resetTutorial,
deleteTutorial, deleteTutorial,
@ -19,12 +21,17 @@ import { clearMessages } from "../../../actions/messageActions";
import axios from "axios"; import axios from "axios";
import { withRouter } from "react-router-dom"; 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 Breadcrumbs from "../../Breadcrumbs";
import Textfield from "./Textfield"; import Textfield from "./Textfield";
import Difficulty from "./Difficulty";
import Step from "./Step"; import Step from "./Step";
import Dialog from "../../Dialog"; import Dialog from "../../Dialog";
import Snackbar from "../../Snackbar"; import Snackbar from "../../Snackbar";
import Public from "./Public";
import Review from "./Review";
import { withStyles } from "@material-ui/core/styles"; import { withStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
@ -66,6 +73,8 @@ class Builder extends Component {
tutorial: "new", tutorial: "new",
open: false, open: false,
title: "", title: "",
public: false,
difficulty: "",
content: "", content: "",
string: false, string: false,
snackbar: false, snackbar: false,
@ -80,7 +89,11 @@ class Builder extends Component {
// retrieve tutorials only if a potential user is loaded - authentication // retrieve tutorials only if a potential user is loaded - authentication
// is finished (success or failed) // is finished (success or failed)
if (!this.props.authProgress) { 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 && props.authProgress !== this.props.authProgress &&
!this.props.authProgress !this.props.authProgress
) { ) {
if (this.props.user.role === "admin") {
// authentication is completed // authentication is completed
this.props.getTutorials(); this.props.getAllTutorials();
} else {
this.props.getUserTutorials();
}
} }
if (props.message !== this.props.message) { if (props.message !== this.props.message) {
if (this.props.message.id === "GET_TUTORIALS_FAIL") { if (this.props.message.id === "GET_TUTORIALS_FAIL") {
@ -258,6 +275,9 @@ class Builder extends Component {
var steps = this.props.steps; var steps = this.props.steps;
var newTutorial = new FormData(); var newTutorial = new FormData();
newTutorial.append("title", this.props.title); 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) => { steps.forEach((step, i) => {
if (step._id) { if (step._id) {
newTutorial.append(`steps[${i}][_id]`, step._id); newTutorial.append(`steps[${i}][_id]`, step._id);
@ -284,6 +304,7 @@ class Builder extends Component {
newTutorial.append(`steps[${i}][xml]`, step.xml); newTutorial.append(`steps[${i}][xml]`, step.xml);
} }
}); });
console.log(newTutorial);
return newTutorial; return newTutorial;
} }
}; };
@ -362,6 +383,12 @@ class Builder extends Component {
(tutorial) => tutorial.creator === this.props.user.email (tutorial) => tutorial.creator === this.props.user.email
); );
} }
// } else {
// filteredTutorials = this.props.userTutorials.filter(
// (tutorial) => tutorial.creator === this.props.user.email
// );
return ( return (
<div> <div>
<Breadcrumbs <Breadcrumbs
@ -451,7 +478,24 @@ class Builder extends Component {
label="Tutorial" label="Tutorial"
> >
{filteredTutorials.map((tutorial) => ( {filteredTutorials.map((tutorial) => (
<MenuItem value={tutorial._id}>
{tutorial.title}{" "}
{tutorial.review && tutorial.public === false ? (
<div>
<FontAwesomeIcon mx={2} icon={faUserCheck} />
<FontAwesomeIcon mx={2} icon={faEyeSlash} />
</div>
) : tutorial.public === false ? (
<FontAwesomeIcon mx={2} icon={faEyeSlash} />
) : null}
</MenuItem>
/* ) : tutorial.public === false ? (
<MenuItem value={tutorial._id}>
{tutorial.title} <FontAwesomeIcon icon={faEyeSlash} />
</MenuItem>
) : (
<MenuItem value={tutorial._id}>{tutorial.title}</MenuItem> <MenuItem value={tutorial._id}>{tutorial.title}</MenuItem>
)} */
))} ))}
</Select> </Select>
</FormControl> </FormControl>
@ -476,6 +520,45 @@ class Builder extends Component {
label={"Titel"} label={"Titel"}
error={this.props.error.title} error={this.props.error.title}
/> />
<div
style={{
borderRadius: "25px",
border: "1px solid lightgrey",
padding: "10px 14px 10px 10px",
marginBottom: "20px",
}}
>
<Difficulty
value={this.props.difficulty}
property={"difficulty"}
label={"difficulty"}
error={this.props.error.difficulty}
/>
<Divider
variant="fullWidth"
style={{ margin: "30px 0 10px 0" }}
/>
<Review
value={this.props.review}
property={"review"}
label={"review"}
error={this.props.error.review}
/>
<Divider
variant="fullWidth"
style={{ margin: "30px 0 10px 0" }}
/>
{this.props.user.blocklyRole === "admin" ? (
<Public
value={this.props.public}
property={"public"}
label={"public"}
error={this.props.error.public}
/>
) : null}
</div>
{this.props.steps.map((step, i) => ( {this.props.steps.map((step, i) => (
<Step step={step} index={i} key={i} /> <Step step={step} index={i} key={i} />
@ -608,6 +691,8 @@ class Builder extends Component {
} }
Builder.propTypes = { Builder.propTypes = {
getAllTutorials: PropTypes.func.isRequired,
getUserTutorials: PropTypes.func.isRequired,
getTutorials: PropTypes.func.isRequired, getTutorials: PropTypes.func.isRequired,
resetTutorial: PropTypes.func.isRequired, resetTutorial: PropTypes.func.isRequired,
clearMessages: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired,
@ -620,6 +705,9 @@ Builder.propTypes = {
resetTutorialBuilder: PropTypes.func.isRequired, resetTutorialBuilder: PropTypes.func.isRequired,
tutorialProgress: PropTypes.func.isRequired, tutorialProgress: PropTypes.func.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
difficulty: PropTypes.number.isRequired,
public: PropTypes.bool.isRequired,
review: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
steps: PropTypes.array.isRequired, steps: PropTypes.array.isRequired,
change: PropTypes.number.isRequired, change: PropTypes.number.isRequired,
@ -634,12 +722,16 @@ Builder.propTypes = {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
title: state.builder.title, title: state.builder.title,
difficulty: state.builder.difficulty,
review: state.builder.review,
public: state.builder.public,
id: state.builder.id, id: state.builder.id,
steps: state.builder.steps, steps: state.builder.steps,
change: state.builder.change, change: state.builder.change,
error: state.builder.error, error: state.builder.error,
json: state.builder.json, json: state.builder.json,
isProgress: state.builder.progress, isProgress: state.builder.progress,
userTutorials: state.tutorial.userTutorials,
tutorials: state.tutorial.tutorials, tutorials: state.tutorial.tutorials,
message: state.message, message: state.message,
user: state.auth.user, user: state.auth.user,
@ -654,6 +746,8 @@ export default connect(mapStateToProps, {
tutorialId, tutorialId,
resetTutorialBuilder, resetTutorialBuilder,
getTutorials, getTutorials,
getUserTutorials,
getAllTutorials,
resetTutorial, resetTutorial,
tutorialProgress, tutorialProgress,
clearMessages, clearMessages,

View File

@ -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 (
<FormControl component="fieldset">
<FormLabel component="legend">
{Blockly.Msg.builder_difficulty}
</FormLabel>
<FormGroup aria-label="position" row>
<FormControlLabel
value="Tutorial veröffentlichen"
control={
<ReactStars
count={5}
onChange={this.handleChange}
value={this.props.value}
size={30}
isHalf={true}
emptyIcon={<i className="fa fa-star"></i>}
halfIcon={<i className="fa fa-star-half-alt"></i>}
fullIcon={<i className="fa fa-star"></i>}
activeColor="#ffd700"
/>
}
label="Schwierigkeitsgrad"
labelPlacement="start"
/>
</FormGroup>
</FormControl>
);
}
}
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));

View File

@ -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 (
<FormControl component="fieldset">
<FormLabel component="legend">Tutorial veröffentlichen</FormLabel>
<FormGroup aria-label="position" row>
<FormControlLabel
value="Tutorial veröffentlichen"
control={
<Checkbox
checked={this.props.value}
onChange={this.handleChange}
color="primary"
name="checkedA"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
}
label="Tutorial veröffentlichen"
labelPlacement="start"
/>
</FormGroup>
</FormControl>
);
}
}
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));

View File

@ -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 (
<FormControl component="fieldset">
<FormLabel component="legend">Tutorial veröffentlichen</FormLabel>
<p>
{" "}
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.
</p>
<FormGroup aria-label="position" row>
<FormControlLabel
value="Tutorial veröffentlichen"
control={
<Checkbox
checked={this.props.value}
onChange={this.handleChange}
color="primary"
name="checkedA"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
}
label="Ich möchte mein Tutorial öffentlich machen"
labelPlacement="start"
/>
</FormGroup>
</FormControl>
);
}
}
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));

View File

@ -1,124 +1,234 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import clsx from 'clsx'; import clsx from "clsx";
import { withRouter, Link } from 'react-router-dom'; import { withRouter, Link } from "react-router-dom";
import { fade } from '@material-ui/core/styles/colorManipulator'; import { fade } from "@material-ui/core/styles/colorManipulator";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import List from '@material-ui/core/List'; import List from "@material-ui/core/List";
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from "@material-ui/core/Tooltip";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; 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: { outerDiv: {
width: '50px', width: "50px",
height: '50px', height: "50px",
position: 'absolute', position: "absolute",
color: fade(theme.palette.secondary.main, 0.6) color: fade(theme.palette.secondary.main, 0.6),
}, },
outerDivError: { outerDivError: {
stroke: fade(theme.palette.error.dark, 0.6), stroke: fade(theme.palette.error.dark, 0.6),
color: fade(theme.palette.error.dark, 0.6) color: fade(theme.palette.error.dark, 0.6),
}, },
outerDivSuccess: { outerDivSuccess: {
stroke: fade(theme.palette.primary.main, 0.6), stroke: fade(theme.palette.primary.main, 0.6),
color: fade(theme.palette.primary.main, 0.6) color: fade(theme.palette.primary.main, 0.6),
}, },
outerDivOther: { outerDivOther: {
stroke: fade(theme.palette.secondary.main, 0.6) stroke: fade(theme.palette.secondary.main, 0.6),
}, },
innerDiv: { innerDiv: {
width: 'inherit', width: "inherit",
height: 'inherit', height: "inherit",
display: 'table-cell', display: "table-cell",
verticalAlign: 'middle', verticalAlign: "middle",
textAlign: 'center' textAlign: "center",
}, },
link: { link: {
color: theme.palette.text.primary, color: theme.palette.text.primary,
position: 'relative', position: "relative",
height: '50px', height: "50px",
display: 'flex', display: "flex",
margin: '5px 0 5px 10px', margin: "5px 0 5px 10px",
textDecoration: 'none' textDecoration: "none",
}, },
hoverLink: { hoverLink: {
'&:hover': { "&:hover": {
background: fade(theme.palette.secondary.main, 0.5), background: fade(theme.palette.secondary.main, 0.5),
borderRadius: '0 25px 25px 0 ' borderRadius: "0 25px 25px 0 ",
} },
} },
}); });
class Requirement extends Component { class Requirement extends Component {
render() { render() {
var requirements = this.props.requirements; var requirements = this.props.requirements;
var tutorialIds = requirements.map(requirement => requirement._id); var tutorialIds = requirements.map((requirement) => requirement._id);
return ( return (
<div style={{ marginTop: '20px', marginBottom: '5px' }}> <div style={{ marginTop: "20px", marginBottom: "5px" }}>
<Typography>{Blockly.Msg.tutorials_requirements}</Typography> <Typography>{Blockly.Msg.tutorials_requirements}</Typography>
<List component="div"> <List component="div">
{tutorialIds.map((tutorialId, i) => { {tutorialIds.map((tutorialId, i) => {
var title = requirements[i].title var title = requirements[i].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 tasks = status.tasks;
var error = status.tasks.filter(task => task.type === 'error').length > 0; var error =
var success = status.tasks.filter(task => task.type === 'success').length / tasks.length status.tasks.filter((task) => task.type === "error").length > 0;
var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other'; var success =
status.tasks.filter((task) => task.type === "success").length /
tasks.length;
var tutorialStatus =
success === 1 ? "Success" : error ? "Error" : "Other";
return ( return (
<Link to={`/tutorial/${tutorialId}`} className={this.props.classes.link} key={i}> <Link
<Tooltip style={{ height: '50px', width: '50px', position: 'absolute', background: 'white', zIndex: 1, borderRadius: '25px' }} title={error ? `Mind. eine Aufgabe im Tutorial wurde nicht richtig gelöst.` : success === 1 ? `Das Tutorial wurde bereits erfolgreich abgeschlossen.` : `Das Tutorial ist zu ${success * 100}% abgeschlossen.`} arrow> target={"_blank"}
to={`/tutorial/${tutorialId}`}
className={this.props.classes.link}
key={i}
>
<Tooltip
style={{
height: "50px",
width: "50px",
position: "absolute",
background: "white",
zIndex: 1,
borderRadius: "25px",
}}
title={
error
? `Mind. eine Aufgabe im Tutorial wurde nicht richtig gelöst.`
: success === 1
? `Das Tutorial wurde bereits erfolgreich abgeschlossen.`
: `Das Tutorial ist zu ${success * 100}% abgeschlossen.`
}
arrow
>
<div> <div>
<div className={clsx(this.props.classes.outerDiv)} style={{ width: '50px', height: '50px', border: 0 }}> <div
<svg style={{ width: '100%', height: '100%' }}> className={clsx(this.props.classes.outerDiv)}
{error || success === 1 ? style={{ width: "50px", height: "50px", border: 0 }}
<circle className={error ? this.props.classes.outerDivError : this.props.classes.outerDivSuccess} r="22.5" cx="50%" cy="50%" fill="none" strokeWidth="5"></circle> >
: <circle className={this.props.classes.outerDivOther} r="22.5" cx="50%" cy="50%" fill="none" strokeWidth="5"></circle>} <svg style={{ width: "100%", height: "100%" }}>
{success < 1 && !error ? {error || success === 1 ? (
<circle className={this.props.classes.outerDivSuccess} style={{ transform: 'rotate(-90deg)', transformOrigin: "50% 50%" }} r="22.5" cx="50%" cy="50%" fill="none" strokeWidth="5" strokeDashoffset={`${(22.5 * 2 * Math.PI) * (1 - success)}`} strokeDasharray={`${(22.5 * 2 * Math.PI)}`}> <circle
</circle> className={
: null} error
? this.props.classes.outerDivError
: this.props.classes.outerDivSuccess
}
r="22.5"
cx="50%"
cy="50%"
fill="none"
strokeWidth="5"
></circle>
) : (
<circle
className={this.props.classes.outerDivOther}
r="22.5"
cx="50%"
cy="50%"
fill="none"
strokeWidth="5"
></circle>
)}
{success < 1 && !error ? (
<circle
className={this.props.classes.outerDivSuccess}
style={{
transform: "rotate(-90deg)",
transformOrigin: "50% 50%",
}}
r="22.5"
cx="50%"
cy="50%"
fill="none"
strokeWidth="5"
strokeDashoffset={`${
22.5 * 2 * Math.PI * (1 - success)
}`}
strokeDasharray={`${22.5 * 2 * Math.PI}`}
></circle>
) : null}
</svg> </svg>
</div> </div>
<div className={clsx(this.props.classes.outerDiv, tutorialStatus === 'Error' ? this.props.classes.outerDivError : tutorialStatus === 'Success' ? this.props.classes.outerDivSuccess : null)}> <div
className={clsx(
this.props.classes.outerDiv,
tutorialStatus === "Error"
? this.props.classes.outerDivError
: tutorialStatus === "Success"
? this.props.classes.outerDivSuccess
: null
)}
>
<div className={this.props.classes.innerDiv}> <div className={this.props.classes.innerDiv}>
{error || success === 1 ? {error || success === 1 ? (
<FontAwesomeIcon icon={tutorialStatus === 'Success' ? faCheck : faTimes} /> <FontAwesomeIcon
: <Typography variant='h7' className={success > 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success * 100)}%</Typography> icon={
tutorialStatus === "Success" ? faCheck : faTimes
} }
/>
) : (
<Typography
variant="h7"
className={
success > 0
? this.props.classes.outerDivSuccess
: {}
}
>
{Math.round(success * 100)}%
</Typography>
)}
</div> </div>
</div> </div>
</div> </div>
</Tooltip> </Tooltip>
<div style={{ height: '50px', width: 'calc(100% - 25px)', transform: 'translate(25px)' }} className={this.props.classes.hoverLink}> <div
<Typography style={{ margin: 0, position: 'absolute', top: '50%', transform: 'translate(45px, -50%)', maxHeight: '50px', overflow: 'hidden', maxWidth: 'calc(100% - 45px)', textOverflow: 'ellipsis', whiteSpace: 'pre-line', overflowWrap: 'anywhere' }}>{title}</Typography> style={{
height: "50px",
width: "calc(100% - 25px)",
transform: "translate(25px)",
}}
className={this.props.classes.hoverLink}
>
<Typography
style={{
margin: 0,
position: "absolute",
top: "50%",
transform: "translate(45px, -50%)",
maxHeight: "50px",
overflow: "hidden",
maxWidth: "calc(100% - 45px)",
textOverflow: "ellipsis",
whiteSpace: "pre-line",
overflowWrap: "anywhere",
}}
>
{title}
</Typography>
</div> </div>
</Link> </Link>
) );
} })}
)}
</List> </List>
</div> </div>
); );
}; }
} }
Requirement.propTypes = { Requirement.propTypes = {
status: PropTypes.array.isRequired, status: PropTypes.array.isRequired,
change: PropTypes.number.isRequired change: PropTypes.number.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
change: state.tutorial.change, change: state.tutorial.change,
status: state.tutorial.status, 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)));

View File

@ -3,9 +3,13 @@ import PropTypes from "prop-types";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
getTutorials, getTutorials,
getAllTutorials,
getUserTutorials,
resetTutorial, resetTutorial,
tutorialProgress, tutorialProgress,
} from "../../actions/tutorialActions"; } from "../../actions/tutorialActions";
import { progress } from "../../actions/tutorialBuilderActions";
import { clearMessages } from "../../actions/messageActions"; import { clearMessages } from "../../actions/messageActions";
import clsx from "clsx"; import clsx from "clsx";
@ -20,9 +24,20 @@ import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import {
faCheck,
faTimes,
faShareAlt,
faEye,
faUserCheck,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Blockly from "blockly"; 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) => ({ const styles = (theme) => ({
outerDiv: { outerDiv: {
@ -51,26 +66,89 @@ const styles = (theme) => ({
verticalAlign: "middle", verticalAlign: "middle",
textAlign: "center", 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 { class TutorialHome extends Component {
constructor(props) {
super(props);
this.state = {
userTutorials: [],
tutorials: [],
snackbar: false,
};
}
componentDidMount() { componentDidMount() {
this.props.tutorialProgress(); this.props.tutorialProgress();
// retrieve tutorials only if a potential user is loaded - authentication // retrieve tutorials only if a potential user is loaded - authentication
// is finished (success or failed) // is finished (success or failed)
if (!this.props.progress) { // if (!this.props.progress) {
// 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(); this.props.getTutorials();
} }
} }
}
componentDidUpdate(props, state) { componentDidUpdate(props, state) {
if (props.progress !== this.props.progress && !this.props.progress) { if (
props.authProgress !== this.props.authProgress &&
!this.props.authProgress
)
if (this.props.user) {
if (this.props.user.role === "admin") {
// authentication is completed // authentication is completed
this.props.getAllTutorials();
} else {
this.props.getUserTutorials();
}
} else {
this.props.getTutorials(); this.props.getTutorials();
} }
if (this.props.message.id === "GET_TUTORIALS_FAIL") { if (this.props.message.id === "GET_TUTORIALS_FAIL") {
alert(this.props.message.msg); alert(this.props.message.msg);
} }
console.log(this.props.user);
} }
componentWillUnmount() { componentWillUnmount() {
@ -81,13 +159,25 @@ class TutorialHome extends Component {
} }
render() { 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 : ( return this.props.isLoading ? null : (
<div> <div>
<Breadcrumbs content={[{ link: "/tutorial", title: "Tutorial" }]} /> <Breadcrumbs content={[{ link: "/tutorial", title: "Tutorial" }]} />
<h1>{Blockly.Msg.tutorials_home_head}</h1> <h1>{Blockly.Msg.tutorials_home_head}</h1>
<h2>Alle Tutorials</h2>
<Grid container spacing={2}> <Grid container spacing={2}>
{this.props.tutorials.map((tutorial, i) => { {publicTutorials.map((tutorial, i) => {
var status = this.props.status.filter( var status = this.props.status.filter(
(status) => status._id === tutorial._id (status) => status._id === tutorial._id
)[0]; )[0];
@ -99,6 +189,12 @@ class TutorialHome extends Component {
tasks.length; tasks.length;
var tutorialStatus = var tutorialStatus =
success === 1 ? "Success" : error ? "Error" : "Other"; success === 1 ? "Success" : error ? "Error" : "Other";
const firstExample = {
size: 30,
value: tutorial.difficulty,
edit: false,
isHalf: true,
};
return ( return (
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}> <Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
<Link <Link
@ -114,6 +210,7 @@ class TutorialHome extends Component {
}} }}
> >
{tutorial.title} {tutorial.title}
<ReactStars {...firstExample} />
<div <div
className={clsx(this.props.classes.outerDiv)} className={clsx(this.props.classes.outerDiv)}
style={{ width: "160px", height: "160px", border: 0 }} style={{ width: "160px", height: "160px", border: 0 }}
@ -216,6 +313,119 @@ class TutorialHome extends Component {
); );
})} })}
</Grid> </Grid>
{this.props.user ? (
<div>
<h2>User Tutorials</h2>
<Grid container spacing={2}>
{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 (
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
<Paper
style={{
height: "150",
padding: "10px",
position: "relative",
overflow: "hidden",
backgroundColor: tutorial.review
? "lightyellow"
: "white",
}}
>
<Link
to={`/tutorial/${tutorial._id}`}
style={{ textDecoration: "none", color: "inherit" }}
>
{tutorial.title}
<ReactStars {...firstExample} />
</Link>
<Divider
style={{
marginTop: "10px",
marginBottom: "10px",
}}
/>
<p>
Creator:{tutorial.creator} <br />
<div style={this.props.style}>
<Tooltip
title={Blockly.Msg.tooltip_share_tutorial}
arrow
>
<IconButton
className={`shareTutorial ${this.props.classes.button}`}
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/tutorial/${tutorial._id}`
);
this.setState({
snackbar: true,
key: Date.now(),
message:
Blockly.Msg.messages_copylink_success,
type: "success",
});
}}
>
<FontAwesomeIcon icon={faShareAlt} size="xs" />
</IconButton>
</Tooltip>
<Tooltip
title={Blockly.Msg.tooltip_share_tutorial}
arrow
>
<IconButton
className={`publicTutorial ${this.props.classes.button}`}
disabled={!tutorial.public}
>
<FontAwesomeIcon icon={faEye} size="xs" />
</IconButton>
</Tooltip>
{tutorial.review ? (
<Tooltip
title={Blockly.Msg.tooltip_share_tutorial}
arrow
>
<IconButton
className={`publicTutorial ${this.props.classes.button}`}
disabled={!tutorial.review}
>
<FontAwesomeIcon icon={faUserCheck} size="xs" />
</IconButton>
</Tooltip>
) : null}
<Snackbar
open={this.state.snackbar}
message={Blockly.Msg.messages_copylink_success}
type={this.state.type}
key={this.state.key}
/>
</div>
</p>
</Paper>
</Grid>
);
})}
</Grid>
</div>
) : null}
</div> </div>
); );
} }
@ -223,6 +433,8 @@ class TutorialHome extends Component {
TutorialHome.propTypes = { TutorialHome.propTypes = {
getTutorials: PropTypes.func.isRequired, getTutorials: PropTypes.func.isRequired,
getAllTutorials: PropTypes.func.isRequired,
getUserTutorials: PropTypes.func.isRequired,
resetTutorial: PropTypes.func.isRequired, resetTutorial: PropTypes.func.isRequired,
tutorialProgress: PropTypes.func.isRequired, tutorialProgress: PropTypes.func.isRequired,
clearMessages: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired,
@ -232,19 +444,27 @@ TutorialHome.propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
progress: PropTypes.bool.isRequired, progress: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired,
authProgress: PropTypes.bool.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
change: state.tutorial.change, change: state.tutorial.change,
status: state.tutorial.status, status: state.tutorial.status,
tutorials: state.tutorial.tutorials, tutorials: state.tutorial.tutorials,
userTutorials: state.tutorial.userTutorials,
isLoading: state.tutorial.progress, isLoading: state.tutorial.progress,
message: state.message, message: state.message,
progress: state.auth.progress, progress: state.auth.progress,
user: state.auth.user,
authProgress: state.auth.progress,
}); });
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
getTutorials, getTutorials,
progress,
getUserTutorials,
getAllTutorials,
resetTutorial, resetTutorial,
clearMessages, clearMessages,
tutorialProgress, tutorialProgress,

View File

@ -1,72 +1,87 @@
import React, { Component } from 'react'; import React, { Component } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import { shareProject } from '../../actions/projectActions'; import { shareProject } from "../../actions/projectActions";
import { clearMessages } from '../../actions/messageActions'; import { clearMessages } from "../../actions/messageActions";
import moment from 'moment'; import moment from "moment";
import Dialog from '../Dialog'; import Dialog from "../Dialog";
import Snackbar from '../Snackbar'; import Snackbar from "../Snackbar";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from "@material-ui/core/styles";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import Tooltip from '@material-ui/core/Tooltip'; import Tooltip from "@material-ui/core/Tooltip";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import { faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons"; import { faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Blockly from 'blockly/core'; import * as Blockly from "blockly/core";
const styles = (theme) => ({ const styles = (theme) => ({
button: { button: {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
width: '40px', width: "40px",
height: '40px', height: "40px",
'&:hover': { "&:hover": {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
} },
}, },
link: { link: {
color: theme.palette.primary.main, color: theme.palette.primary.main,
textDecoration: 'none', textDecoration: "none",
'&:hover': { "&:hover": {
color: theme.palette.primary.main, color: theme.palette.primary.main,
textDecoration: 'underline' textDecoration: "underline",
} },
} },
}); });
class WorkspaceFunc extends Component { class WorkspaceFunc extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.inputRef = React.createRef(); this.inputRef = React.createRef();
this.state = { this.state = {
snackbar: false, snackbar: false,
type: '', type: "",
key: '', key: "",
message: '', message: "",
title: '', title: "",
content: '', content: "",
open: false, open: false,
id: '', id: "",
}; };
} }
componentDidUpdate(props) { componentDidUpdate(props) {
if (this.props.message !== props.message) { if (this.props.message !== props.message) {
if (this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || this.props.message.status === this.props.project._id)) { if (
this.setState({ share: true, open: true, title: Blockly.Msg.messages_SHARE_SUCCESS, id: this.props.message.status }); this.props.message.id === "SHARE_SUCCESS" &&
} (!this.props.multiple ||
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)
this.setState({ snackbar: true, key: Date.now(), message: Blockly.Msg.messages_SHARE_FAIL, type: 'error' }); ) {
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); window.scrollTo(0, 0);
} }
} }
@ -77,18 +92,25 @@ class WorkspaceFunc extends Component {
} }
toggleDialog = () => { toggleDialog = () => {
this.setState({ open: !this.state, title: '', content: '' }); this.setState({ open: !this.state, title: "", content: "" });
} };
shareBlocks = () => { shareBlocks = () => {
if (this.props.projectType === 'project' && this.props.project.shared) { if (this.props.projectType === "project" && this.props.project.shared) {
// project is already shared // project is already shared
this.setState({ open: true, title: Blockly.Msg.messages_SHARE_SUCCESS, id: this.props.project._id }); this.setState({
} open: true,
else { title: Blockly.Msg.messages_SHARE_SUCCESS,
this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id : undefined); 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
);
} }
};
render() { render() {
return ( return (
@ -116,43 +138,89 @@ class WorkspaceFunc extends Component {
onClick={this.toggleDialog} onClick={this.toggleDialog}
button={Blockly.Msg.button_close} button={Blockly.Msg.button_close}
> >
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: "10px" }}>
<Typography>Über den folgenden Link kannst du dein Programm teilen:</Typography> <Typography>
<Link to={`/share/${this.state.id}`} onClick={() => this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`}</Link> Über den folgenden Link kannst du dein Programm teilen:
<Tooltip title={Blockly.Msg.tooltip_copy_link} arrow style={{ marginRight: '5px' }}> </Typography>
<Link
to={`/share/${this.state.id}`}
onClick={() => this.toggleDialog()}
className={this.props.classes.link}
>{`${window.location.origin}/share/${this.state.id}`}</Link>
<Tooltip
title={Blockly.Msg.tooltip_copy_link}
arrow
style={{ marginRight: "5px" }}
>
<IconButton <IconButton
onClick={() => { onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/share/${this.state.id}`); navigator.clipboard.writeText(
this.setState({ snackbar: true, key: Date.now(), message: Blockly.Msg.messages_copylink_success, type: 'success' }); `${window.location.origin}/share/${this.state.id}`
);
this.setState({
snackbar: true,
key: Date.now(),
message: Blockly.Msg.messages_copylink_success,
type: "success",
});
}} }}
> >
<FontAwesomeIcon icon={faCopy} size="xs" /> <FontAwesomeIcon icon={faCopy} size="xs" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{this.props.project && this.props.project.shared && this.props.message.id !== 'SHARE_SUCCESS' ? {this.props.project &&
<Typography variant='body2' style={{ marginTop: '20px' }}>{`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${moment(this.props.project.shared).diff(moment().utc(), 'days') === 0 ? this.props.project.shared &&
moment(this.props.project.shared).diff(moment().utc(), 'hours') === 0 ? this.props.message.id !== "SHARE_SUCCESS" ? (
`${moment(this.props.project.shared).diff(moment().utc(), 'minutes')} Minuten` <Typography
: `${moment(this.props.project.shared).diff(moment().utc(), 'hours')} Stunden` variant="body2"
: `${moment(this.props.project.shared).diff(moment().utc(), 'days')} Tage`} gültig.`}</Typography> style={{ marginTop: "20px" }}
: <Typography variant='body2' style={{ marginTop: '20px' }}>{`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}</Typography>} >{`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.`}</Typography>
) : (
<Typography
variant="body2"
style={{ marginTop: "20px" }}
>{`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}</Typography>
)}
</div> </div>
</Dialog> </Dialog>
</div> </div>
); );
}; }
} }
WorkspaceFunc.propTypes = { WorkspaceFunc.propTypes = {
shareProject: PropTypes.func.isRequired, shareProject: PropTypes.func.isRequired,
clearMessages: PropTypes.func.isRequired, clearMessages: PropTypes.func.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
message: PropTypes.object.isRequired message: PropTypes.object.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = (state) => ({
name: state.workspace.name, 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)
);

View File

@ -10,6 +10,9 @@ import {
BUILDER_CHANGE_STEP, BUILDER_CHANGE_STEP,
BUILDER_CHANGE_ORDER, BUILDER_CHANGE_ORDER,
BUILDER_DELETE_PROPERTY, BUILDER_DELETE_PROPERTY,
BUILDER_DIFFICULTY,
BUILDER_PUBLIC,
BUILDER_REVIEW,
} from "../actions/types"; } from "../actions/types";
const initialState = { const initialState = {
@ -17,6 +20,9 @@ const initialState = {
progress: false, progress: false,
json: "", json: "",
title: "", title: "",
difficulty: 0,
public: false,
review: false,
id: "", id: "",
steps: [ steps: [
{ {
@ -45,6 +51,21 @@ export default function foo(state = initialState, action) {
...state, ...state,
title: action.payload, 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: case BUILDER_ID:
return { return {
...state, ...state,

View File

@ -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'; import {
TUTORIAL_PROGRESS,
GET_TUTORIAL,
// GET_TUTORIALS,
// const initialStatus = () => { GET_USERTUTORIALS,
// if(store.getState().auth.user){ GET_STATUS,
// return store.getState().auth.user.status || [] TUTORIAL_SUCCESS,
// } TUTORIAL_ERROR,
// else if (window.localStorage.getItem('status')) { TUTORIAL_CHANGE,
// var status = JSON.parse(window.localStorage.getItem('status')); TUTORIAL_XML,
// return status; TUTORIAL_STEP,
// } } from "../actions/types";
// return [];
// };
const initialState = { const initialState = {
status: [], status: [],
activeStep: 0, activeStep: 0,
change: 0, change: 0,
tutorials: [], tutorials: [],
progress: false userTutorials: [],
progress: false,
}; };
export default function foo(state = initialState, action) { export default function foo(state = initialState, action) {
@ -26,18 +25,23 @@ export default function foo(state = initialState, action) {
case TUTORIAL_PROGRESS: case TUTORIAL_PROGRESS:
return { return {
...state, ...state,
progress: !state.progress progress: !state.progress,
} };
case GET_TUTORIALS: case GET_TUTORIALS:
return { return {
...state, ...state,
tutorials: action.payload tutorials: action.payload,
};
case GET_USERTUTORIALS:
return {
...state,
tutorials: action.payload,
}; };
case GET_TUTORIAL: case GET_TUTORIAL:
return { return {
...state, ...state,
tutorials: [action.payload] tutorials: [action.payload],
} };
case TUTORIAL_SUCCESS: case TUTORIAL_SUCCESS:
case TUTORIAL_ERROR: case TUTORIAL_ERROR:
case TUTORIAL_XML: case TUTORIAL_XML:
@ -46,23 +50,23 @@ export default function foo(state = initialState, action) {
// and 'TUTORIAL_XML' the function 'updateStatus' is called // and 'TUTORIAL_XML' the function 'updateStatus' is called
return { return {
...state, ...state,
status: action.payload status: action.payload,
}; };
case GET_STATUS: case GET_STATUS:
return { return {
...state, ...state,
status: action.payload status: action.payload,
}; };
case TUTORIAL_CHANGE: case TUTORIAL_CHANGE:
return { return {
...state, ...state,
change: state.change += 1 change: (state.change += 1),
} };
case TUTORIAL_STEP: case TUTORIAL_STEP:
return { return {
...state, ...state,
activeStep: action.payload activeStep: action.payload,
} };
default: default:
return state; return state;
} }

View File

@ -1,6 +1,6 @@
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from "redux";
import thunk from 'redux-thunk'; import thunk from "redux-thunk";
import rootReducer from './reducers'; import rootReducer from "./reducers";
const initialState = {}; const initialState = {};
@ -11,7 +11,7 @@ const store = createStore(
initialState, initialState,
compose( compose(
applyMiddleware(...middleware), applyMiddleware(...middleware),
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
) )
); );