feat: refactors + various UI changes

This commit is contained in:
swve 2023-06-22 18:26:25 +02:00
parent d5ad9e2f2f
commit f6d50627bd
69 changed files with 138 additions and 130 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);
},
});

View file

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

View 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);
},
});

View file

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

View file

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

View file

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