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:
parent
39c8fd504f
commit
0414d2043d
@ -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",
|
||||
|
14
src/App.css
14
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;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<Breadcrumbs
|
||||
@ -451,7 +478,24 @@ class Builder extends Component {
|
||||
label="Tutorial"
|
||||
>
|
||||
{filteredTutorials.map((tutorial) => (
|
||||
<MenuItem value={tutorial._id}>{tutorial.title}</MenuItem>
|
||||
<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>
|
||||
)} */
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
@ -476,6 +520,45 @@ class Builder extends Component {
|
||||
label={"Titel"}
|
||||
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) => (
|
||||
<Step step={step} index={i} key={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,
|
||||
|
101
src/components/Tutorial/Builder/Difficulty.js
Normal file
101
src/components/Tutorial/Builder/Difficulty.js
Normal 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));
|
91
src/components/Tutorial/Builder/Public.js
Normal file
91
src/components/Tutorial/Builder/Public.js
Normal 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));
|
97
src/components/Tutorial/Builder/Review.js
Normal file
97
src/components/Tutorial/Builder/Review.js
Normal 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));
|
@ -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 (
|
||||
<div style={{ marginTop: '20px', marginBottom: '5px' }}>
|
||||
<div style={{ marginTop: "20px", marginBottom: "5px" }}>
|
||||
<Typography>{Blockly.Msg.tutorials_requirements}</Typography>
|
||||
<List component="div">
|
||||
{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 (
|
||||
<Link 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>
|
||||
<Link
|
||||
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 className={clsx(this.props.classes.outerDiv)} style={{ width: '50px', height: '50px', border: 0 }}>
|
||||
<svg style={{ width: '100%', height: '100%' }}>
|
||||
{error || success === 1 ?
|
||||
<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>}
|
||||
{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}
|
||||
<div
|
||||
className={clsx(this.props.classes.outerDiv)}
|
||||
style={{ width: "50px", height: "50px", border: 0 }}
|
||||
>
|
||||
<svg style={{ width: "100%", height: "100%" }}>
|
||||
{error || success === 1 ? (
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
</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}>
|
||||
{error || success === 1 ?
|
||||
<FontAwesomeIcon icon={tutorialStatus === 'Success' ? faCheck : faTimes} />
|
||||
: <Typography variant='h7' className={success > 0 ? this.props.classes.outerDivSuccess : {}}>{Math.round(success * 100)}%</Typography>
|
||||
}
|
||||
{error || success === 1 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
tutorialStatus === "Success" ? faCheck : faTimes
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
variant="h7"
|
||||
className={
|
||||
success > 0
|
||||
? this.props.classes.outerDivSuccess
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{Math.round(success * 100)}%
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div 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
|
||||
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>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
|
@ -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 : (
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: "/tutorial", title: "Tutorial" }]} />
|
||||
|
||||
<h1>{Blockly.Msg.tutorials_home_head}</h1>
|
||||
<h2>Alle Tutorials</h2>
|
||||
<Grid container spacing={2}>
|
||||
{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 (
|
||||
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
|
||||
<Link
|
||||
@ -114,6 +210,7 @@ class TutorialHome extends Component {
|
||||
}}
|
||||
>
|
||||
{tutorial.title}
|
||||
<ReactStars {...firstExample} />
|
||||
<div
|
||||
className={clsx(this.props.classes.outerDiv)}
|
||||
style={{ width: "160px", height: "160px", border: 0 }}
|
||||
@ -216,6 +313,119 @@ class TutorialHome extends Component {
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
@ -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}
|
||||
>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<Typography>Über den folgenden Link kannst du dein Programm teilen:</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' }}>
|
||||
<div style={{ marginTop: "10px" }}>
|
||||
<Typography>
|
||||
Über den folgenden Link kannst du dein Programm teilen:
|
||||
</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
|
||||
onClick={() => {
|
||||
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",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{this.props.project && this.props.project.shared && this.props.message.id !== 'SHARE_SUCCESS' ?
|
||||
<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 ?
|
||||
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>}
|
||||
{this.props.project &&
|
||||
this.props.project.shared &&
|
||||
this.props.message.id !== "SHARE_SUCCESS" ? (
|
||||
<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
|
||||
? 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>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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__()
|
||||
)
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user