Compare commits

...

2 Commits

Author SHA1 Message Date
fab-scm
fd6358ea2b implement FinalPage 2023-01-21 17:48:07 +01:00
fab-scm
912440f64c added Tutorial Builder FinalPage 2022-11-24 11:45:58 +01:00
8 changed files with 690 additions and 66 deletions

View File

@ -301,12 +301,21 @@ export const resetTutorial = () => (dispatch, getState) => {
hardware: [],
requirements: [],
},
{
id: 2,
type: "finalpage",
headline: "",
text: "",
samplesolutions: false,
solutions: [],
furthertutorials: false,
},
];
dispatch(tutorialSteps(steps));
dispatch({
type: BUILDER_ERROR,
payload: {
steps: [{}],
steps: [{}, {}],
},
});
};

View File

@ -259,7 +259,7 @@ class Builder extends Component {
window.scrollTo(0, 0);
};
submit = () => {
submit = () => {;
var isError = this.props.checkError();
if (isError) {
this.setState({
@ -273,6 +273,8 @@ class Builder extends Component {
} else {
// export steps without attribute 'url'
var steps = this.props.steps;
// console.log(steps);
var length = steps.length;
var newTutorial = new FormData();
newTutorial.append("title", this.props.title);
newTutorial.append("difficulty", this.props.difficulty);
@ -299,11 +301,30 @@ class Builder extends Component {
newTutorial.append(`steps[${i}][hardware][${j}]`, hardware);
});
}
if (i === length-1 && step.type === "finalpage") {
newTutorial.append(`steps[${i}][samplesolutions]`, step.samplesolutions);
newTutorial.append(`steps[${i}][furthertutorials]`, step.furthertutorials);
if (step.samplesolutions === true) {
var solutionindex = 0;
steps.forEach((solutionstep) => {
if (solutionstep.type === "task"&& solutionstep.xml) {
newTutorial.append(`steps[${i}][solutions][${solutionindex}][type]`, solutionstep.type);
newTutorial.append(`steps[${i}][solutions][${solutionindex}][headline]`, solutionstep.headline);
newTutorial.append(`steps[${i}][solutions][${solutionindex}][xml]`, solutionstep.xml);
solutionindex++;
}
});
}
}
if (step.xml) {
// optional
newTutorial.append(`steps[${i}][xml]`, step.xml);
}
});
for (const pair of newTutorial.entries()) {
console.log(`${pair[0]}, ${pair[1]}`);
}
return newTutorial;
}
};

View File

@ -0,0 +1,107 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { changeContent } from "../../../actions/tutorialBuilderActions";
import { withStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from "@material-ui/core/FormControlLabel";
const styles = (theme) => ({
errorColor: {
color: theme.palette.error.dark,
},
errorBorder: {
border: `1px solid ${theme.palette.error.dark}`,
},
errorButton: {
marginTop: "5px",
height: "40px",
backgroundColor: theme.palette.error.dark,
"&:hover": {
backgroundColor: theme.palette.error.dark,
},
},
});
class FinalPageOptions extends Component {
constructor(props) {
super(props);
this.state = {
checkedSampleSolutions: props.checkedSampleSolutions,
checkedFurtherTutorials: props.checkedFurtherTutorials,
};
}
onChangeSampleSolutions = (value) => {
var oldValue = this.state.checked;
this.setState({ checked: value });
if (oldValue !== value) {
this.props.changeContent(value, this.props.index, "samplesolutions");
}
};
onChangeFurtherTutorials = (value) => {
var oldValue = this.state.checked;
this.setState({ checked: value });
if (oldValue !== value) {
this.props.changeContent(value, this.props.index, "furthertutorials");
}
}
render() {
var steps = this.props.steps;
return (
<div
style={{
marginBottom: "10px",
padding: "18.5px 14px",
borderRadius: "25px",
border: "1px solid lightgrey",
width: "calc(100% - 28px)",
}}
>
<FormGroup>
<FormControlLabel
labelPlacement="end"
label={
"Musterlösung(en) der Aufgabe(n) auf der Abschlussseite anzeigen"
}
control={
<Switch
checked={this.state.checked}
onChange={(e) => this.onChangeSampleSolutions(e.target.checked)}
color="primary"
/>
}
/>
<FormControlLabel
labelPlacement="end"
label={
"Vorschläge für weitere Tutorials auf der Abschlussseite anzeigen"
}
control={
<Switch
checked={this.state.checked}
onChange={(e) => this.onChangeFurtherTutorials(e.target.checked)}
color="primary"
/>
}
/>
</FormGroup>
</div>
);
}
}
FinalPageOptions.propTypes = {
changeContent: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
});
export default connect(mapStateToProps, {
changeContent,
})(withStyles(styles, { withTheme: true })(FinalPageOptions));

View File

@ -29,6 +29,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import MarkdownEditor from "./MarkdownEditor";
import FinalPageOptions from "./FinalPageOptions";
const styles = (theme) => ({
button: {
@ -55,6 +56,8 @@ class Step extends Component {
render() {
var index = this.props.index;
var steps = this.props.steps;
// console.log(this.props.steps);
// console.log(this.props.step);
return (
<div
style={{
@ -64,12 +67,21 @@ class Step extends Component {
marginBottom: "20px",
}}
>
<Typography
variant="h6"
style={{ marginBottom: "10px", marginLeft: "4px" }}
>
Schritt {index + 1}
</Typography>
{this.props.step.type !== "finalpage" ? (
<Typography
variant="h6"
style={{ marginBottom: "10px", marginLeft: "4px" }}
>
Schritt {index + 1}
</Typography>
) : (
<Typography
variant="h6"
style={{ marginBottom: "10px", marginLeft: "4px" }}
>
Abschlussseite
</Typography>
)}
<div style={{ display: "flex", position: "relative" }}>
<div
style={{
@ -80,60 +92,66 @@ class Step extends Component {
bottom: "10px",
}}
>
<Tooltip title="Schritt hinzufügen" arrow>
<IconButton
className={this.props.classes.button}
style={index === 0 ? {} : { marginBottom: "5px" }}
onClick={() => this.props.addStep(index + 1)}
>
<FontAwesomeIcon icon={faPlus} size="xs" />
</IconButton>
</Tooltip>
{index !== 0 ? (
{this.props.step.type !== "finalpage" ? (
<div>
<Tooltip
title={`Schritt ${index + 1} nach oben schieben`}
arrow
>
<Tooltip title="Schritt hinzufügen" arrow>
<IconButton
disabled={index < 2}
className={this.props.classes.button}
style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index - 1)}
style={index === 0 ? {} : { marginBottom: "5px" }}
onClick={() => this.props.addStep(index + 1)}
>
<FontAwesomeIcon icon={faAngleDoubleUp} size="xs" />
</IconButton>
</Tooltip>
<Tooltip
title={`Schritt ${index + 1} nach unten schieben`}
arrow
>
<IconButton
disabled={index === steps.length - 1}
className={this.props.classes.button}
style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index + 1)}
>
<FontAwesomeIcon icon={faAngleDoubleDown} size="xs" />
</IconButton>
</Tooltip>
<Tooltip title={`Schritt ${index + 1} löschen`} arrow>
<IconButton
disabled={index === 0}
className={clsx(
this.props.classes.button,
this.props.classes.delete
)}
onClick={() => this.props.removeStep(index)}
>
<FontAwesomeIcon icon={faTrash} size="xs" />
<FontAwesomeIcon icon={faPlus} size="xs" />
</IconButton>
</Tooltip>
{index !== 0 ? (
<div>
<Tooltip
title={`Schritt ${index + 1} nach oben schieben`}
arrow
>
<IconButton
disabled={index < 2}
className={this.props.classes.button}
style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index - 1)}
>
<FontAwesomeIcon icon={faAngleDoubleUp} size="xs" />
</IconButton>
</Tooltip>
<Tooltip
title={`Schritt ${index + 1} nach unten schieben`}
arrow
>
<IconButton
disabled={index >= this.props.steps.length - 2}
className={this.props.classes.button}
style={{ marginBottom: "5px" }}
onClick={() => this.props.changeStepIndex(index, index + 1)}
>
<FontAwesomeIcon icon={faAngleDoubleDown} size="xs" />
</IconButton>
</Tooltip>
<Tooltip title={`Schritt ${index + 1} löschen`} arrow>
<IconButton
disabled={index === 0 && index === steps.length - 1}
className={clsx(
this.props.classes.button,
this.props.classes.delete
)}
onClick={() => this.props.removeStep(index)}
>
<FontAwesomeIcon icon={faTrash} size="xs" />
</IconButton>
</Tooltip>
</div>
) : null}
</div>
) : null}
</div>
<div style={{ width: "100%", marginLeft: "54px" }}>
<StepType value={this.props.step.type} index={index} />
{this.props.step.type !== "finalpage" ? (
<StepType value={this.props.step.type} index={index} />
) : null}
<Textfield
value={this.props.step.headline}
property={"headline"}
@ -178,12 +196,21 @@ class Step extends Component {
/>
</div>
) : null}
<BlocklyExample
value={this.props.step.xml}
index={index}
task={this.props.step.type === "task"}
error={this.props.error.steps[index].xml ? true : false}
/>
{this.props.step.type !== "finalpage" ? (
<BlocklyExample
value={this.props.step.xml}
index={index}
task={this.props.step.type === "task"}
error={this.props.error.steps[index].xml ? true : false}
/>
) : (
<FinalPageOptions
steps={steps}
checkedSampleSolutions={this.props.step.samplesolutions}
checkedFurtherTutorials={this.props.step.furthertutorials}
index={index}
/>
)}
</div>
</div>
</div>

View File

@ -0,0 +1,430 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import axios from "axios";
import BlocklyWindow from "../Blockly/BlocklyWindow";
import clsx from "clsx";
import { alpha } from "@material-ui/core/styles";
import { withStyles } from "@material-ui/core/styles";
import {
faCheck,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router-dom";
import ReactStars from "react-rating-stars-component";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkGemoji from "remark-gemoji";
import Dialog from "../Dialog";
import { IconButton } from "@material-ui/core";
import { clearMessages } from "../../actions/messageActions";
import * as Blockly from "blockly/core";
// import { getTutorials,
// tutorialProgress,
// resetTutorial
// } from "../../actions/tutorialActions";
const styles = (theme) => ({
outerDiv: {
position: "absolute",
right: "-30px",
bottom: "-30px",
width: "160px",
height: "160px",
color: alpha(theme.palette.secondary.main, 0.6),
},
outerDivError: {
stroke: alpha(theme.palette.error.dark, 0.6),
color: alpha(theme.palette.error.dark, 0.6),
},
outerDivSuccess: {
stroke: alpha(theme.palette.primary.main, 0.6),
color: alpha(theme.palette.primary.main, 0.6),
},
outerDivOther: {
stroke: alpha(theme.palette.secondary.main, 0.6),
},
innerDiv: {
width: "inherit",
height: "inherit",
display: "table-cell",
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 FinalPage extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
title: "",
content: "",
tutorials: [],
randomtutorials: [],
activeXML: "",
};
}
componentWillMount() {
if (this.props.step.furthertutorials === true) {
axios
.get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial`)
.then((res) => {
this.setState({ tutorials: res.data.tutorials });
this.setState({ randomtutorials: this.getRandomTutorials(this.state.tutorials, 3) });
})
}
}
toggleDialog = (solution) => {
console.log(solution)
if (this.state.open === true) {
this.setState({ open: !this.state.open, title: "", activeXML: "" });
} else if (this.state.open === false) {
this.setState({ open: !this.state.open, title: "Task - " + solution.headline, activeXML: solution.xml });
}
};
getRandomTutorials = (tutorials, n) => {
var length = tutorials.length;
var taken = new Array(length);
var result = new Array(n);
if (n > length)
throw new RangeError("getRandom: more elements taken than available");
while (n--) {
var x = Math.floor(Math.random() * length);
result[n] = tutorials[x in taken ? taken[x] : x];
taken[x] = --length in taken ? taken[length] : length;
}
return result;
}
// toggleDialog = (solution) => {
// this.setState({ open: !this.state.open });
// // if (solution) {
// // this.setState({ open: !this.state.open, activeSolution: solution });
// // } else {
// // this.setState({ open: !this.state.open, activeSolution: null });
// // }
// };
render() {
var step = this.props.step;
var tutorials = this.state.tutorials;
var randomtutorials = this.state.randomtutorials;
// var randomtutorials = this.getRandomTutorials(tutorials, 3);
// console.log(randomtutorials)
return (
<div>
<Typography>
<ReactMarkdown
className={"tutorial"}
linkTarget={"_blank"}
skipHtml={false}
allowDangerousHtml={true}
remarkPlugins={[remarkGfm, remarkGemoji]}
>
{step.text}
</ReactMarkdown>
</Typography>
{step.media ? (
step.media.picture ? (
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "5px",
}}
>
<img
src={`${process.env.REACT_APP_BLOCKLY_API}/media/${step.media.picture.path}`}
alt=""
style={{ maxHeight: "40vH", maxWidth: "100%" }}
/>
</div>
) : step.media.youtube ? (
/*16:9; width: 800px; height: width/16*9=450px*/
<div style={{ maxWidth: "800px", margin: "auto" }}>
<div
style={{
position: "relative",
height: 0,
paddingBottom: "calc(100% / 16 * 9)",
}}
>
<iframe
title={step.media.youtube}
style={{
position: "absolute",
top: "0",
left: "0",
width: "100%",
maxWidth: "800px",
height: "100%",
maxHeight: "450px",
}}
src={`https://www.youtube.com/embed/${step.media.youtube}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
) : null
) : null}
{step.samplesolutions === true ? (
<div>
<h2 style={{ justifyContent: "center" }}>Musterlösungen</h2>
<Grid
container
spacing={2}
style={{ marginBottom: "5px", justifyContent: "space-evenly" }}
>
{step.solutions.map((solution) => {
return (
<Grid
item
xs={12}
sm={6}
md={4}
xl={3}
style={{ display: "flex", justifyContent: "center" }}
// onclick={this.toggleDialog(solution)}
>
<IconButton
onClick={() => this.toggleDialog(solution)}
>
<Paper
style={{
padding: "10px",
position: "relative",
overflow: "hidden",
}}
>
<h3>{"Task - " + solution.headline}</h3>
<BlocklyWindow
svg
blockDisabled
initialXml={solution.xml}
/>
</Paper>
</IconButton>
</Grid>
);
})}
</Grid>
<Dialog
open={this.state.open}
onClose={this.toggleDialog}
onClick={this.toggleDialog}
title={this.state.title}
button={Blockly.Msg.button_close}
>
<BlocklyWindow
svg
blockDisabled
initialXml={this.state.activeXML}
/>
</Dialog>
</div>
) : null}
{step.furthertutorials === true ? (
<div>
<h2 style={{ justifyContent: "center" }}>Weitere Tutorials</h2>
<Grid
container
spacing={2}
style={{ justifyContent: "space-evenly" }}
>
{randomtutorials.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={{}}>
<Link
to={`/tutorial/${tutorial._id}`}
style={{ textDecoration: "none", color: "inherit" }}
>
<Paper
style={{
height: "150px",
padding: "10px",
position: "relative",
overflow: "hidden",
}}
>
{tutorial.title}
<ReactStars {...firstExample} />
<div
className={clsx(this.props.classes.outerDiv)}
style={{ width: "160px", height: "160px", border: 0 }}
>
<svg style={{ width: "100%", height: "100%" }}>
{error || success === 1 ? (
<circle
className={
error
? this.props.classes.outerDivError
: this.props.classes.outerDivSuccess
}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
></circle>
) : (
<circle
className={this.props.classes.outerDivOther}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
stroke-dashoffset={`${75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
stroke-dasharray={`${75 * 2 * Math.PI * (1 - (50 / 100 - success / 2))
} ${75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
></circle>
)}
{success < 1 && !error ? (
<circle
className={this.props.classes.outerDivSuccess}
style={{
transform: "rotate(-44deg)",
transformOrigin: "50% 50%",
}}
r="75"
cx="50%"
cy="50%"
fill="none"
stroke-width="10"
stroke-dashoffset={`${75 * 2 * Math.PI * (1 - (50 / 100 + success / 2))
}`}
stroke-dasharray={`${75 * 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={this.props.classes.innerDiv}>
{error || success === 1 ? (
<FontAwesomeIcon
size="4x"
icon={
tutorialStatus === "Success" ? faCheck : faTimes
}
/>
) : (
<Typography
variant="h3"
className={
success > 0
? this.props.classes.outerDivSuccess
: {}
}
>
{Math.round(success * 100)}%
</Typography>
)}
</div>
</div>
</Paper>
</Link>
</Grid>
);
})}
</Grid>
</div>
) : null}
</div>
);
}
}
const mapStateToProps = (state) => ({
tutorials: state.tutorial.tutorials,
status: state.tutorial.status,
isLoading: state.tutorial.progress,
message: state.message,
progress: state.auth.progress,
user: state.auth.user,
authProgress: state.auth.progress,
});
export default connect(mapStateToProps, {
// getTutorials,
// resetTutorial,
// tutorialProgress,
clearMessages,
})(withStyles(styles, { withTheme: true })(FinalPage));

View File

@ -31,6 +31,9 @@ const styles = (theme) => ({
width: "24px",
height: "24px",
},
stepIconSuccess: {
borderColor: theme.palette.primary.main,
},
stepIconLargeSuccess: {
borderColor: theme.palette.primary.main,
},
@ -130,12 +133,25 @@ class StepperVertical extends Component {
"stepIconLarge" + taskStatus
]
)
: i === activeStep
? clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconActiveOther
)
: clsx(this.props.classes.stepIcon),
: step.type === "instruction"
? i === activeStep
? clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconActiveOther
)
: clsx(this.props.classes.stepIcon)
: step.type === "finalpage"
? i === activeStep
? clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconSuccess,
this.props.classes.stepIconActiveSuccess
)
: clsx(
this.props.classes.stepIcon,
this.props.classes.stepIconSuccess
)
: null,
}}
></StepLabel>
</div>

View File

@ -17,6 +17,7 @@ import StepperHorizontal from "./StepperHorizontal";
import StepperVertical from "./StepperVertical";
import Instruction from "./Instruction";
import Assessment from "./Assessment";
import FinalPage from "./FinalPage";
import NotFound from "../NotFound";
import * as Blockly from "blockly";
import { detectWhitespacesAndReturnReadableResult } from "../../helpers/whitespace";
@ -74,7 +75,9 @@ class Tutorial extends Component {
(() => {
var tutorial = this.props.tutorial;
var steps = this.props.tutorial.steps;
console.log(steps)
var step = steps[this.props.activeStep];
console.log(step)
var name = `${detectWhitespacesAndReturnReadableResult(
tutorial.title
)}_${detectWhitespacesAndReturnReadableResult(step.headline)}`;
@ -107,8 +110,10 @@ class Tutorial extends Component {
{step ? (
step.type === "instruction" ? (
<Instruction step={step} />
) : (
) : step.type === "task" ? (
<Assessment step={step} name={name} />
) : (
<FinalPage step={step} />
) // if step.type === 'assessment'
) : null}

View File

@ -33,9 +33,18 @@ const initialState = {
hardware: [],
requirements: [],
},
{
id: 2,
type: "finalpage",
headline: "",
text: "",
samplesolutions: false,
solutions: [],
furthertutorials: false,
},
],
error: {
steps: [{}],
steps: [{},{}],
},
};