Merge pull request #156 from sensebox/update/tutorial-builder

update tutorial builder
This commit is contained in:
Mario Pesch 2022-07-05 21:24:43 +02:00 committed by GitHub
commit 3e9d55719b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1318 additions and 301 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

@ -1,6 +1,6 @@
import React, { Component } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import { Router } from "react-router-dom";
import { createBrowserHistory } from "history";
import { Provider } from "react-redux";

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

@ -1,34 +1,29 @@
import React, { Component } from 'react';
import React, { Component } from "react";
import { withStyles } from '@material-ui/core/styles';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from "@material-ui/core/styles";
import { alpha } from "@material-ui/core/styles";
import Typography from '@material-ui/core/Typography';
import Typography from "@material-ui/core/Typography";
const styles = (theme) => ({
alert: {
marginBottom: '20px',
marginBottom: "20px",
border: `1px solid ${theme.palette.primary.main}`,
padding: '10px 20px',
borderRadius: '4px',
background: fade(theme.palette.primary.main, 0.3),
color: 'rgb(70,70,70)'
}
padding: "10px 20px",
borderRadius: "4px",
background: alpha(theme.palette.primary.main, 0.3),
color: "rgb(70,70,70)",
},
});
export class Alert extends Component {
render(){
return(
render() {
return (
<div className={this.props.classes.alert}>
<Typography>
{this.props.children}
</Typography>
<Typography>{this.props.children}</Typography>
</div>
);
}
}
export default withStyles(styles, { withTheme: true })(Alert);

View File

@ -128,7 +128,7 @@ BlocklyWindow.propTypes = {
onChangeWorkspace: PropTypes.func.isRequired,
clearStats: PropTypes.func.isRequired,
renderer: PropTypes.string.isRequired,
sounds: PropTypes.string.isRequired,
sounds: PropTypes.bool.isRequired,
language: PropTypes.string.isRequired,
};

View File

@ -228,6 +228,12 @@ export const UI = {
builder_requirements_head: "Voraussetzungen",
builder_requirements_order:
"Beachte, dass die Reihenfolge des Anhakens maßgebend ist.",
builder_difficulty: "Schwierigkeitsgrad",
builder_public_head: "Tutorial veröffentlichen",
builder_public_label: "Tutorial für alle Nutzer:innen veröffentlichen",
builder_review_head: "Tutorial veröffentlichen",
builder_review_text:
"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.",
/**
* Login

View File

@ -222,6 +222,12 @@ export const UI = {
builder_requirements_head: "Requirements.",
builder_requirements_order:
"Note that the order of ticking is authoritative.",
builder_difficulty: "Difficulty level",
builder_public_head: "Publish tutorial",
builder_public_label: "Publish tutorial for all users",
builder_review_head: "Publish tutorial",
builder_review_text:
"You can share your tutorial with other people directly from the link. If you want to publish your tutorial for all users in the overview you can activate it here. An administrator will view your tutorial and then activate it.",
/**
* Login

View File

@ -190,7 +190,7 @@ class Compile extends Component {
className={`compileBlocks ${this.props.classes.iconButton}`}
onClick={() => this.compile()}
>
<FontAwesomeIcon icon={faClipboardCheck} size="m" />
<FontAwesomeIcon icon={faClipboardCheck} size="xs" />
</IconButton>
</Tooltip>
) : (

View File

@ -1,20 +1,21 @@
import React from "react";
import Blockly from "blockly";
import { useSelector } from "react-redux";
import Accordion from "@material-ui/core/Accordion";
import AccordionSummary from "@material-ui/core/AccordionSummary";
import AccordionDetails from "@material-ui/core/AccordionDetails";
import Typography from "@material-ui/core/Typography";
import { LibraryVersions } from "../../data/versions.js";
//import { useMonaco } from "@monaco-editor/react";
//import { Button } from "@material-ui/core";
import { useMonaco } from "@monaco-editor/react";
import { Button } from "@material-ui/core";
import Dialog from "../Dialog";
import SerialMonitor from "./SerialMonitor.js";
//import axios from "axios";
import axios from "axios";
const Sidebar = () => {
const [alert, setAlert] = React.useState(false);
//const [examples, setExamples] = React.useState([]);
const user = useSelector((state) => state.auth.user);
// useEffect(() => {
// axios
// .get("https://coelho.opensensemap.org/items/blocklysamples")
@ -22,16 +23,23 @@ const Sidebar = () => {
// setExamples(res.data.data);
// });
// }, []);
//const monaco = useMonaco();
// const loadCode = (code) => {
// monaco.editor.getModels()[0].setValue(code);
// };
const monaco = useMonaco();
const loadCode = (code) => {
monaco.editor.getModels()[0].setValue(code);
};
const toggleDialog = () => {
setAlert(false);
};
const getOsemScript = (id) => {
axios
.get(`https://api.opensensemap.org/boxes/${id}/script/`)
.then((res) => {
loadCode(res.data);
});
};
return (
<div>
{"serial" in navigator ? (
@ -50,7 +58,6 @@ const Sidebar = () => {
</AccordionDetails>
</Accordion>
) : null}
{/* <Accordion>
<AccordionSummary
expandIcon={""}
@ -67,6 +74,7 @@ const Sidebar = () => {
style={{ padding: "1rem", margin: "1rem" }}
variant="contained"
color="primary"
key={i}
onClick={() => loadCode(object.code)}
>
{object.name}
@ -76,6 +84,34 @@ const Sidebar = () => {
</Typography>
</AccordionDetails>
</Accordion> */}
{user ? (
<Accordion>
<AccordionSummary
expandIcon={""}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>Deine openSenseMap Codes</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
{user.boxes.map((box, i) => {
return (
<Button
style={{ padding: "1rem", margin: "1rem" }}
variant="contained"
color="primary"
key={i}
onClick={() => getOsemScript(box._id)}
>
{box.name}
</Button>
);
})}
</Typography>
</AccordionDetails>
</Accordion>
) : null}
<Accordion>
<AccordionSummary
expandIcon={""}

View File

@ -52,7 +52,7 @@ class SoundsSelector extends Component {
SoundsSelector.propTypes = {
setSounds: PropTypes.func.isRequired,
language: PropTypes.string.isRequired,
sounds: PropTypes.string.isRequired,
sounds: PropTypes.bool.isRequired,
};
const mapStateToProps = (state) => ({

View File

@ -1,47 +1,55 @@
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 withWidth from '@material-ui/core/withWidth';
import withWidth from "@material-ui/core/withWidth";
import { Card } from '@material-ui/core';
import * as Blockly from 'blockly'
import CardContent from '@material-ui/core/CardContent';
import { Card } from "@material-ui/core";
import * as Blockly from "blockly";
import CardContent from "@material-ui/core/CardContent";
import Typography from '@material-ui/core/Typography';
import ReactMarkdown from 'react-markdown';
import Typography from "@material-ui/core/Typography";
import ReactMarkdown from "react-markdown";
class TooltipViewer extends Component {
render() {
return (
<Card className="tooltipViewer" style={{ height: '100%', margin: '1vH 0 0 0', maxHeight: '19vH', overflow: 'auto' }} ref={this.myDiv}>
<Card
className="tooltipViewer"
style={{
height: "100%",
margin: "1vH 0 0 0",
maxHeight: "19vH",
overflow: "auto",
}}
ref={this.myDiv}
>
<CardContent>
<Typography variant="h5" component="h2">
{Blockly.Msg.tooltip_viewer}
</Typography>
<Typography variant="body2" component="p">
<ReactMarkdown linkTarget="_blank">{this.props.tooltip}</ReactMarkdown>
{this.props.helpurl !== '' ? <ReactMarkdown>{`${Blockly.Msg.tooltip_moreInformation} [${Blockly.Msg.labels_here}](${this.props.helpurl})`}</ReactMarkdown> : null}
<ReactMarkdown linkTarget="_blank">
{this.props.tooltip}
</ReactMarkdown>
</Typography>
{this.props.helpurl !== "" ? (
<ReactMarkdown>{`${Blockly.Msg.tooltip_moreInformation} [${Blockly.Msg.labels_here}](${this.props.helpurl})`}</ReactMarkdown>
) : null}
</CardContent>
</Card>
);
};
}
}
TooltipViewer.propTypes = {
tooltip: PropTypes.string.isRequired,
helpurl: PropTypes.string.isRequired
helpurl: PropTypes.string.isRequired,
};
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
tooltip: state.workspace.code.tooltip,
helpurl: state.workspace.code.helpurl
helpurl: state.workspace.code.helpurl,
});
export default connect(mapStateToProps, null)(withWidth()(TooltipViewer));

View File

@ -20,6 +20,8 @@ import ReactMarkdown from "react-markdown";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { withStyles } from "@material-ui/core/styles";
import remarkGfm from "remark-gfm";
import remarkGemoji from "remark-gemoji";
const styles = (theme) => ({
codeOn: {
@ -101,7 +103,9 @@ class Assessment extends Component {
}}
>
<Typography>
<ReactMarkdown>{currentTask.text}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm, remarkGemoji]}>
{currentTask.text}
</ReactMarkdown>
</Typography>
</Card>
<Card

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 icon={faUserCheck} />
<FontAwesomeIcon icon={faEyeSlash} />
</div>
) : tutorial.public === false ? (
<FontAwesomeIcon 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,93 @@
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">
{Blockly.Msg.builder_public_head}
</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={Blockly.Msg.builder_public_label}
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,93 @@
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">
{Blockly.Msg.builder_review_head}
</FormLabel>
{Blockly.Msg.builder_review_text}
<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,233 @@
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 { 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 clsx from "clsx";
import { withRouter, Link } from "react-router-dom";
import { alpha } from "@material-ui/core/styles";
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: alpha(theme.palette.secondary.main, 0.6),
},
outerDivError: {
stroke: fade(theme.palette.error.dark, 0.6),
color: fade(theme.palette.error.dark, 0.6)
stroke: alpha(theme.palette.error.dark, 0.6),
color: alpha(theme.palette.error.dark, 0.6),
},
outerDivSuccess: {
stroke: fade(theme.palette.primary.main, 0.6),
color: fade(theme.palette.primary.main, 0.6)
stroke: alpha(theme.palette.primary.main, 0.6),
color: alpha(theme.palette.primary.main, 0.6),
},
outerDivOther: {
stroke: fade(theme.palette.secondary.main, 0.6)
stroke: alpha(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': {
background: fade(theme.palette.secondary.main, 0.5),
borderRadius: '0 25px 25px 0 '
}
}
"&:hover": {
background: alpha(theme.palette.secondary.main, 0.5),
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

@ -63,7 +63,7 @@ class SolutionCheck extends Component {
style={{ width: "40px", height: "40px", marginRight: "5px" }}
onClick={() => this.check()}
>
<FontAwesomeIcon icon={faClipboardCheck} size="m" />
<FontAwesomeIcon icon={faClipboardCheck} size="xs" />
</IconButton>
</Tooltip>

View File

@ -7,8 +7,8 @@ import { withRouter } from "react-router-dom";
import clsx from "clsx";
// import tutorials from '../../data/tutorials';
import { alpha } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { withStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Tooltip from "@material-ui/core/Tooltip";
@ -28,13 +28,13 @@ const styles = (theme) => ({
justifyContent: "space-between",
},
stepperSuccess: {
backgroundColor: fade(theme.palette.primary.main, 0.6),
backgroundColor: alpha(theme.palette.primary.main, 0.6),
},
stepperError: {
backgroundColor: fade(theme.palette.error.dark, 0.6),
backgroundColor: alpha(theme.palette.error.dark, 0.6),
},
stepperOther: {
backgroundColor: fade(theme.palette.secondary.main, 0.6),
backgroundColor: alpha(theme.palette.secondary.main, 0.6),
},
color: {
backgroundColor: "transparent ",

View File

@ -1,36 +1,35 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { tutorialStep } from '../../actions/tutorialActions';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { tutorialStep } from "../../actions/tutorialActions";
import { withRouter } from 'react-router-dom';
import { withRouter } from "react-router-dom";
import clsx from 'clsx';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import Tooltip from '@material-ui/core/Tooltip';
import clsx from "clsx";
import { alpha } from "@material-ui/core/styles";
import { withStyles } from "@material-ui/core/styles";
import Stepper from "@material-ui/core/Stepper";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Tooltip from "@material-ui/core/Tooltip";
const styles = (theme) => ({
verticalStepper: {
padding: 0,
width: '30px',
width: "30px",
},
stepIcon: {
borderStyle: `solid`,
// borderWidth: '2px',
borderRadius: '50%',
borderRadius: "50%",
borderColor: theme.palette.secondary.main,
width: '12px',
height: '12px',
margin: '0 auto',
width: "12px",
height: "12px",
margin: "0 auto",
},
stepIconLarge: {
width: '24px',
height: '24px'
width: "24px",
height: "24px",
},
stepIconLargeSuccess: {
borderColor: theme.palette.primary.main,
@ -39,28 +38,27 @@ const styles = (theme) => ({
borderColor: theme.palette.error.dark,
},
stepIconActiveOther: {
backgroundColor: theme.palette.secondary.main
backgroundColor: theme.palette.secondary.main,
},
stepIconActiveSuccess: {
backgroundColor: fade(theme.palette.primary.main, 0.6)
backgroundColor: alpha(theme.palette.primary.main, 0.6),
},
stepIconActiveError: {
backgroundColor: fade(theme.palette.error.dark, 0.6)
backgroundColor: alpha(theme.palette.error.dark, 0.6),
},
connector: {
height: '10px',
height: "10px",
borderLeft: `2px solid black`,
margin: 'auto'
}
margin: "auto",
},
});
class StepperVertical extends Component {
componentDidMount(){
componentDidMount() {
this.props.tutorialStep(0);
}
componentDidUpdate(props){
componentDidUpdate(props) {
if (props.tutorial._id !== this.props.match.params.tutorialId) {
this.props.tutorialStep(0);
}
@ -69,60 +67,103 @@ class StepperVertical extends Component {
render() {
var steps = this.props.steps;
var activeStep = this.props.activeStep;
var tutorialStatus = this.props.status.filter(status => status._id === this.props.tutorial._id)[0];
var tutorialStatus = this.props.status.filter(
(status) => status._id === this.props.tutorial._id
)[0];
return (
<div style={{marginRight: '10px'}}>
<div style={{ marginRight: "10px" }}>
<Stepper
activeStep={activeStep}
orientation="vertical"
connector={<div className={this.props.classes.connector}></div>}
classes={{root: this.props.classes.verticalStepper}}
classes={{ root: this.props.classes.verticalStepper }}
>
{steps.map((step, i) => {
var tasksIndex = tutorialStatus.tasks.findIndex(task => task._id === step._id);
var taskType = tasksIndex > -1 ? tutorialStatus.tasks[tasksIndex].type : null;
var taskStatus = taskType === 'success' ? 'Success' : taskType === 'error' ? 'Error' : 'Other';
var tasksIndex = tutorialStatus.tasks.findIndex(
(task) => task._id === step._id
);
var taskType =
tasksIndex > -1 ? tutorialStatus.tasks[tasksIndex].type : null;
var taskStatus =
taskType === "success"
? "Success"
: taskType === "error"
? "Error"
: "Other";
return (
<Step key={i}>
<Tooltip title={step.headline} placement='right' arrow >
<div style={i === activeStep ? {padding: '5px 0'} : {padding: '5px 0', cursor: 'pointer'}} onClick={i === activeStep ? null : () => { this.props.tutorialStep(i)}}>
<Tooltip title={step.headline} placement="right" arrow>
<div
style={
i === activeStep
? { padding: "5px 0" }
: { padding: "5px 0", cursor: "pointer" }
}
onClick={
i === activeStep
? null
: () => {
this.props.tutorialStep(i);
}
}
>
<StepLabel
StepIconComponent={'div'}
StepIconComponent={"div"}
classes={{
root: step.type === 'task' ?
i === activeStep ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIconLarge'+taskStatus], this.props.classes['stepIconActive'+taskStatus])
: clsx(this.props.classes.stepIcon, this.props.classes.stepIconLarge, this.props.classes['stepIconLarge'+taskStatus])
: i === activeStep ?
clsx(this.props.classes.stepIcon, this.props.classes.stepIconActiveOther)
: clsx(this.props.classes.stepIcon)
root:
step.type === "task"
? i === activeStep
? clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconLarge,
this.props.classes[
"stepIconLarge" + taskStatus
],
this.props.classes[
"stepIconActive" + taskStatus
]
)
: clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconLarge,
this.props.classes[
"stepIconLarge" + taskStatus
]
)
: i === activeStep
? clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconActiveOther
)
: clsx(this.props.classes.stepIcon),
}}
>
</StepLabel>
></StepLabel>
</div>
</Tooltip>
</Step>
)})}
);
})}
</Stepper>
</div>
);
};
}
}
StepperVertical.propTypes = {
status: PropTypes.array.isRequired,
change: PropTypes.number.isRequired,
activeStep: PropTypes.number.isRequired,
tutorialStep: PropTypes.func.isRequired,
tutorial: PropTypes.object.isRequired
tutorial: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
const mapStateToProps = (state) => ({
change: state.tutorial.change,
status: state.tutorial.status,
activeStep: state.tutorial.activeStep,
tutorial: state.tutorial.tutorials[0]
tutorial: state.tutorial.tutorials[0],
});
export default connect(mapStateToProps, { tutorialStep })(withRouter(withStyles(styles, {withTheme: true})(StepperVertical)));
export default connect(mapStateToProps, { tutorialStep })(
withRouter(withStyles(styles, { withTheme: true })(StepperVertical))
);

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";
@ -13,16 +17,26 @@ import clsx from "clsx";
import Breadcrumbs from "../Breadcrumbs";
import { Link } from "react-router-dom";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { alpha } from "@material-ui/core/styles";
import { withStyles } from "@material-ui/core/styles";
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: {
@ -31,18 +45,18 @@ const styles = (theme) => ({
bottom: "-30px",
width: "160px",
height: "160px",
color: fade(theme.palette.secondary.main, 0.6),
color: alpha(theme.palette.secondary.main, 0.6),
},
outerDivError: {
stroke: fade(theme.palette.error.dark, 0.6),
color: fade(theme.palette.error.dark, 0.6),
stroke: alpha(theme.palette.error.dark, 0.6),
color: alpha(theme.palette.error.dark, 0.6),
},
outerDivSuccess: {
stroke: fade(theme.palette.primary.main, 0.6),
color: fade(theme.palette.primary.main, 0.6),
stroke: alpha(theme.palette.primary.main, 0.6),
color: alpha(theme.palette.primary.main, 0.6),
},
outerDivOther: {
stroke: fade(theme.palette.secondary.main, 0.6),
stroke: alpha(theme.palette.secondary.main, 0.6),
},
innerDiv: {
width: "inherit",
@ -51,26 +65,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 +158,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 +188,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 +209,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 +312,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 +432,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 +443,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

@ -190,7 +190,7 @@ class Compile extends Component {
className={`compileBlocks ${this.props.classes.iconButton}`}
onClick={() => this.compile()}
>
<FontAwesomeIcon icon={faClipboardCheck} size="m" />
<FontAwesomeIcon icon={faClipboardCheck} size="xs" />
</IconButton>
</Tooltip>
) : (

View File

@ -69,7 +69,7 @@ class CopyCode extends Component {
className={`copyCode ${this.props.classes.iconButton}`}
onClick={() => this.copyCode()}
>
<FontAwesomeIcon icon={faCopy} size="m" />
<FontAwesomeIcon icon={faCopy} size="xs" />
</IconButton>
</Tooltip>
) : (

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

@ -3,7 +3,8 @@
"id": "senseboxmcu",
"name": "senseBox MCU",
"src": "senseboxmcu.png",
"url": "https://docs.sensebox.de/hardware/allgemein-sensebox-mcu/"
"url": "https://docs.sensebox.de/hardware/allgemein-sensebox-mcu/",
"description": "test"
},
{
"id": "breadboard",

View File

@ -25,7 +25,7 @@ const initialSounds = () => {
if (window.localStorage.getItem("sounds")) {
return window.localStorage.getItem("sounds");
} else {
return "off";
return false;
}
};

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