mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: refactors + various UI changes
This commit is contained in:
parent
d5ad9e2f2f
commit
f6d50627bd
69 changed files with 138 additions and 130 deletions
|
|
@ -0,0 +1,29 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import InfoCalloutComponent from "./InfoCalloutComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "calloutInfo",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
content: "text*",
|
||||
|
||||
// TODO : multi line support
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "callout-info",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(InfoCalloutComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
function InfoCalloutComponent(props: any) {
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<InfoCalloutWrapper contentEditable={props.extension.options.editable}>
|
||||
<AlertCircle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||
</InfoCalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const InfoCalloutWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: #1f3a8a;
|
||||
background-color: #dbe9fe;
|
||||
border: 1px solid #c1d9fb;
|
||||
border-radius: 16px;
|
||||
margin: 1rem 0;
|
||||
align-items: center;
|
||||
padding-left: 15px;
|
||||
|
||||
svg{
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 5px;
|
||||
padding: 0.5rem;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
export default InfoCalloutComponent;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import WarningCalloutComponent from "./WarningCalloutComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "calloutWarning",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
content: "text*",
|
||||
marks: "",
|
||||
defining: true,
|
||||
|
||||
// TODO : multi line support
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "callout-warning",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(WarningCalloutComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
function WarningCalloutComponent(props: any) {
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<CalloutWrapper contentEditable={props.extension.options.editable}>
|
||||
<AlertTriangle/> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
|
||||
</CalloutWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const CalloutWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #fefce8;
|
||||
color: #713f11;
|
||||
border: 1px solid #fff103;
|
||||
border-radius: 16px;
|
||||
margin: 1rem 0;
|
||||
align-items: center;
|
||||
padding-left: 15px;
|
||||
|
||||
svg {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 5px;
|
||||
padding: 0.5rem;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const DragHandle = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1rem;
|
||||
height: 100%;
|
||||
cursor: move;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export default WarningCalloutComponent;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import ImageBlockComponent from "./ImageBlockComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "blockImage",
|
||||
group: "block",
|
||||
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
blockObject: {
|
||||
default: null,
|
||||
},
|
||||
size: {
|
||||
width: 300,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-image",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-image", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ImageBlockComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Resizable } from 're-resizable';
|
||||
|
||||
import * as AspectRatio from '@radix-ui/react-aspect-ratio';
|
||||
import { AlertCircle, AlertTriangle, Image, ImagePlus, Info } from "lucide-react";
|
||||
import { getImageFile, uploadNewImageFile } from "../../../../../services/blocks/Image/images";
|
||||
import { getBackendUrl } from "../../../../../services/config/config";
|
||||
|
||||
function ImageBlockComponent(props: any) {
|
||||
const [image, setImage] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 });
|
||||
|
||||
const handleImageChange = (event: React.ChangeEvent<any>) => {
|
||||
setImage(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewImageFile(image, props.extension.options.activity.activity_id);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
size: imageSize,
|
||||
});
|
||||
};
|
||||
|
||||
console.log(props.node.attrs);
|
||||
console.log(imageSize);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-image">
|
||||
{!blockObject && (
|
||||
<BlockImageWrapper contentEditable={props.extension.options.editable}>
|
||||
<div>
|
||||
<Image color="#e1e0e0" size={50} />
|
||||
<br />
|
||||
</div>
|
||||
<input onChange={handleImageChange} type="file" name="" id="" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Submit</button>
|
||||
</BlockImageWrapper>
|
||||
)}
|
||||
{blockObject && (
|
||||
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
|
||||
handleStyles={{
|
||||
right: { width: '10px', height: '100%', cursor: 'col-resize' },
|
||||
top: { width: 0 },
|
||||
bottom: { width: 0 },
|
||||
left: { width: 0 },
|
||||
topRight: { width: 0 },
|
||||
bottomRight: { width: 0 },
|
||||
bottomLeft: { width: 0 },
|
||||
topLeft: { width: 0 },
|
||||
|
||||
}}
|
||||
style={{ margin: "auto" }}
|
||||
maxWidth={850}
|
||||
minWidth={400}
|
||||
onResizeStop={(e, direction, ref, d) => {
|
||||
props.updateAttributes({
|
||||
size: {
|
||||
width: imageSize.width + d.width,
|
||||
}
|
||||
});
|
||||
setImageSize({
|
||||
width: imageSize.width + d.width,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
<BlockImage>
|
||||
<AspectRatio.Root ratio={16 / 9}>
|
||||
<img
|
||||
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/imageBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${blockObject.block_data.file_format
|
||||
}`}
|
||||
alt=""
|
||||
/>
|
||||
</AspectRatio.Root>
|
||||
|
||||
|
||||
</BlockImage>
|
||||
</Resizable>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div>
|
||||
<AlertTriangle color="#e1e0e0" size={50} />
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageBlockComponent;
|
||||
|
||||
const BlockImageWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f9f9f9;
|
||||
border-radius: 3px;
|
||||
padding: 30px;
|
||||
min-height: 74px;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const BlockImage = styled.div`
|
||||
display: flex;
|
||||
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
||||
|
||||
img{
|
||||
object-fit: "cover";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
`;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import MathEquationBlockComponent from "./MathEquationBlockComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "blockMathEquation",
|
||||
group: "block",
|
||||
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
math_equation: {
|
||||
default: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-math-equation",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-math-equation", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(MathEquationBlockComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { InlineMath, BlockMath } from "react-katex";
|
||||
import { Edit, Save } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
function MathEquationBlockComponent(props: any) {
|
||||
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
|
||||
const [isEditing, setIsEditing] = React.useState(true);
|
||||
const isEditable = props.extension.options.editable;
|
||||
|
||||
const handleEquationChange = (event: React.ChangeEvent<any>) => {
|
||||
setEquation(event.target.value);
|
||||
props.updateAttributes({
|
||||
math_equation: equation,
|
||||
});
|
||||
};
|
||||
|
||||
const saveEquation = () => {
|
||||
props.updateAttributes({
|
||||
math_equation: equation,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-math-equation">
|
||||
<MathEqWrapper>
|
||||
{isEditable && (
|
||||
<MathEqTopMenu>
|
||||
<button onClick={() => setIsEditing(true)}>
|
||||
<Edit size={15}></Edit>
|
||||
</button>
|
||||
<span className="pl-2">Edit</span>
|
||||
</MathEqTopMenu>
|
||||
)}
|
||||
<BlockMath>{equation}</BlockMath>
|
||||
{isEditing && isEditable && (
|
||||
<>
|
||||
<EditBar>
|
||||
<input value={equation} onChange={handleEquationChange} placeholder="Insert a Math Equation (LaTeX) " type="text" />
|
||||
<button onClick={() => saveEquation()}>
|
||||
<Save size={15}></Save>
|
||||
</button>
|
||||
</EditBar>
|
||||
<span className="pt-2 text-zinc-500 text-sm">Please refer to this <Link className="text-zinc-900 after:content-['↗']" href="https://katex.org/docs/supported.html" target="_blank"> guide</Link> for supported TeX functions </span>
|
||||
</>
|
||||
|
||||
)}
|
||||
</MathEqWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default MathEquationBlockComponent;
|
||||
|
||||
const MathEqWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f9f9f9a2;
|
||||
border-radius: 8px;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
min-height: 74px;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
`;
|
||||
|
||||
const MathEqTopMenu = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
button {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
color: #494949;
|
||||
}
|
||||
`;
|
||||
|
||||
const EditBar = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
color: #5252528d;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
border: solid 1px #52525224;
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
margin-right: 7px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
color: #494949;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
color: #494949;
|
||||
width: 100%;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
padding-left: 10px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #49494936;
|
||||
}
|
||||
}
|
||||
`;
|
||||
35
front/components/Objects/Editor/Extensions/PDF/PDFBlock.ts
Normal file
35
front/components/Objects/Editor/Extensions/PDF/PDFBlock.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import PDFBlockComponent from "./PDFBlockComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "blockPDF",
|
||||
group: "block",
|
||||
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
blockObject: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-pdf",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(PDFBlockComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { AlertCircle, AlertTriangle, FileText, Image, ImagePlus, Info } from "lucide-react";
|
||||
import { getPDFFile, uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
|
||||
import { getBackendUrl } from "../../../../../services/config/config";
|
||||
|
||||
function PDFBlockComponent(props: any) {
|
||||
const [pdf, setPDF] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
|
||||
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
||||
setPDF(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewPDFFile(pdf, props.extension.options.activity.activity_id);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-pdf">
|
||||
{!blockObject && (
|
||||
<BlockPDFWrapper contentEditable={props.extension.options.editable}>
|
||||
<div>
|
||||
<FileText color="#e1e0e0" size={50} />
|
||||
<br />
|
||||
</div>
|
||||
<input onChange={handlePDFChange} type="file" name="" id="" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Submit</button>
|
||||
</BlockPDFWrapper>
|
||||
)}
|
||||
{blockObject && (
|
||||
<BlockPDF>
|
||||
<iframe
|
||||
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/pdfBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${
|
||||
blockObject.block_data.file_format
|
||||
}`}
|
||||
/>
|
||||
</BlockPDF>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div>
|
||||
<AlertTriangle color="#e1e0e0" size={50} />
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default PDFBlockComponent;
|
||||
|
||||
const BlockPDFWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f9f9f9;
|
||||
border-radius: 3px;
|
||||
padding: 30px;
|
||||
min-height: 74px;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const BlockPDF = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
height: 300px;
|
||||
// cover
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
const PDFNotFound = styled.div``;
|
||||
35
front/components/Objects/Editor/Extensions/Quiz/QuizBlock.ts
Normal file
35
front/components/Objects/Editor/Extensions/Quiz/QuizBlock.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import QuizBlockComponent from "./QuizBlockComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "blockQuiz",
|
||||
group: "block",
|
||||
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
quizId: {
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-quiz",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(QuizBlockComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { submitQuizBlock } from "@services/blocks/Quiz/quiz";
|
||||
|
||||
function ImageBlockComponent(props: any) {
|
||||
const [questions, setQuestions] = React.useState([]) as any;
|
||||
const [answers, setAnswers] = React.useState([]) as any;
|
||||
|
||||
function addSampleQuestion() {
|
||||
setQuestions([
|
||||
...questions,
|
||||
{
|
||||
question_id: "question_" + uuidv4(),
|
||||
question_value: "",
|
||||
options: [
|
||||
{
|
||||
option_id: "option_" + uuidv4(),
|
||||
option_data: "",
|
||||
option_type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const deleteQuestion = (index: number) => {
|
||||
let modifiedQuestions = [...questions];
|
||||
modifiedQuestions.splice(index, 1);
|
||||
setQuestions(modifiedQuestions);
|
||||
console.log(questions);
|
||||
|
||||
// remove the answers from the answers array
|
||||
let modifiedAnswers = [...answers];
|
||||
modifiedAnswers = modifiedAnswers.filter((answer: any) => answer.question_id !== questions[index].question_id);
|
||||
setAnswers(modifiedAnswers);
|
||||
};
|
||||
|
||||
const onQuestionChange = (e: any, index: number) => {
|
||||
let modifiedQuestions = [...questions];
|
||||
modifiedQuestions[index].question_value = e.target.value;
|
||||
setQuestions(modifiedQuestions);
|
||||
};
|
||||
|
||||
const addOption = (question_id: string) => {
|
||||
// find the question index from the question_id and add the option to that question index
|
||||
let modifiedQuestions = [...questions];
|
||||
let questionIndex = modifiedQuestions.findIndex((question: any) => question.question_id === question_id);
|
||||
modifiedQuestions[questionIndex].options.push({
|
||||
option_id: "option_" + uuidv4(),
|
||||
option_data: "",
|
||||
option_type: "text",
|
||||
});
|
||||
setQuestions(modifiedQuestions);
|
||||
};
|
||||
|
||||
const deleteOption = (question_id: string, option_id: string) => {
|
||||
// find the option index from the option_id and delete the option from that option index
|
||||
let modifiedQuestions = [...questions];
|
||||
let questionIndex = modifiedQuestions.findIndex((question: any) => question.question_id === question_id);
|
||||
let optionIndex = modifiedQuestions[questionIndex].options.findIndex((option: any) => option.option_id === option_id);
|
||||
modifiedQuestions[questionIndex].options.splice(optionIndex, 1);
|
||||
setQuestions(modifiedQuestions);
|
||||
|
||||
// remove the answer from the answers array
|
||||
let answerIndex = answers.findIndex((answer: any) => answer.option_id === option_id);
|
||||
if (answerIndex !== -1) {
|
||||
let modifiedAnswers = [...answers];
|
||||
modifiedAnswers.splice(answerIndex, 1);
|
||||
setAnswers(modifiedAnswers);
|
||||
}
|
||||
};
|
||||
|
||||
const markOptionAsCorrect = (question_id: string, option_id: string) => {
|
||||
// find the option index from the option_id and mark the option as correct
|
||||
let answer = {
|
||||
question_id: question_id,
|
||||
option_id: option_id,
|
||||
};
|
||||
setAnswers([...answers, answer]);
|
||||
console.log(answers);
|
||||
};
|
||||
|
||||
const saveQuiz = async () => {
|
||||
// save the questions and answers to the backend
|
||||
console.log("saving quiz");
|
||||
console.log(questions);
|
||||
console.log(answers);
|
||||
try {
|
||||
let res = await submitQuizBlock(props.extension.options.activity.activity_id, {questions : questions , answers : answers})
|
||||
console.log(res.block_id);
|
||||
props.updateAttributes({
|
||||
quizId: {
|
||||
value : res.block_id
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const onOptionChange = (e: any, questionIndex: number, optionIndex: number) => {
|
||||
let modifiedQuestions = [...questions];
|
||||
modifiedQuestions[questionIndex].options[optionIndex].option_data = e.target.value;
|
||||
setQuestions(modifiedQuestions);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// fetch the questions and options from the backend
|
||||
console.log("fetching questions");
|
||||
console.log(questions);
|
||||
console.log(answers);
|
||||
}, [questions, answers]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-quiz">
|
||||
<QuizBlockWrapper>
|
||||
Questions <button onClick={addSampleQuestion}>Add Question</button> <button onClick={() => saveQuiz()}>Save</button>
|
||||
<hr />
|
||||
{questions.map((question: any, qIndex: number) => (
|
||||
<>
|
||||
<div key={qIndex} style={{ marginTop: "10px" }}>
|
||||
Question : <input type="text" value={question.question} onChange={(e) => onQuestionChange(e, qIndex)} />
|
||||
<button onClick={() => deleteQuestion(qIndex)}>Delete</button>
|
||||
</div>
|
||||
Answers : <br />
|
||||
{question.options.map((option: any, oIndex: number) => (
|
||||
<>
|
||||
<div key={oIndex}>
|
||||
<input type="text" value={option.option_data} onChange={(e) => onOptionChange(e, qIndex, oIndex)} />
|
||||
|
||||
<button onClick={() => deleteOption(question.question_id, option.option_id)}>Delete</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) =>
|
||||
// check if checkbox is checked or not
|
||||
// if checked then add the answer to the answers array
|
||||
// if unchecked then remove the answer from the answers array
|
||||
e.target.checked ? markOptionAsCorrect(question.question_id, option.option_id) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
<button onClick={() => addOption(question.question_id)}>Add Option</button>
|
||||
</>
|
||||
))}
|
||||
</QuizBlockWrapper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const QuizBlockWrapper = styled.div`
|
||||
background-color: #0000001d;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
`;
|
||||
export default ImageBlockComponent;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
|
||||
import VideoBlockComponent from "./VideoBlockComponent";
|
||||
|
||||
export default Node.create({
|
||||
name: "blockVideo",
|
||||
group: "block",
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
blockObject: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "block-video",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["block-video", mergeAttributes(HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(VideoBlockComponent);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import { AlertTriangle, Image, Video } from "lucide-react";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { getBackendUrl } from "../../../../../services/config/config";
|
||||
import { uploadNewVideoFile } from "../../../../../services/blocks/Video/video";
|
||||
|
||||
function VideoBlockComponents(props: any) {
|
||||
const [video, setVideo] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||
setVideo(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
let object = await uploadNewVideoFile(video, props.extension.options.activity.activity_id);
|
||||
setIsLoading(false);
|
||||
setblockObject(object);
|
||||
props.updateAttributes({
|
||||
blockObject: object,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="block-video">
|
||||
{!blockObject && (
|
||||
<BlockVideoWrapper contentEditable={props.extension.options.editable}>
|
||||
<div>
|
||||
<Video color="#e1e0e0" size={50} />
|
||||
<br />
|
||||
</div>
|
||||
<input onChange={handleVideoChange} type="file" name="" id="" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Submit</button>
|
||||
</BlockVideoWrapper>
|
||||
)}
|
||||
{blockObject && (
|
||||
<BlockVideo>
|
||||
<video
|
||||
controls
|
||||
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/videoBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${
|
||||
blockObject.block_data.file_format
|
||||
}`}
|
||||
></video>
|
||||
</BlockVideo>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div>
|
||||
<AlertTriangle color="#e1e0e0" size={50} />
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
const BlockVideoWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f9f9f9;
|
||||
border-radius: 3px;
|
||||
padding: 30px;
|
||||
min-height: 74px;
|
||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
||||
|
||||
// center
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const BlockVideo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
video {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
height: 300px;
|
||||
// cover
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
export default VideoBlockComponents;
|
||||
Loading…
Add table
Add a link
Reference in a new issue