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-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",

View File

@ -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;

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) => {
if (getState().auth.isAuthenticated) {
// update user account in database - sync with redux store

View File

@ -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));

View File

@ -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";

View File

@ -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

View File

@ -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,

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 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)));

View File

@ -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,

View File

@ -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)
);

View File

@ -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,

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';
//
// 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;
}

View File

@ -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__()
)
);