feat: refactor the entire learnhouse project

This commit is contained in:
swve 2023-10-13 20:03:27 +02:00
parent f556e41dda
commit 4c215e91d5
247 changed files with 7716 additions and 1013 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,31 @@
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 className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={props.extension.options.editable}>
<AlertCircle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
</InfoCalloutWrapper>
</NodeViewWrapper>
);
}
const InfoCalloutWrapper = styled.div`
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,41 @@
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 className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={props.extension.options.editable}>
<AlertTriangle /> <NodeViewContent contentEditable={props.extension.options.editable} className="content" />
</CalloutWrapper>
</NodeViewWrapper>
);
}
const CalloutWrapper = styled.div`
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,115 @@
import { NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
import { Resizable } from 're-resizable';
import { AlertTriangle, Image, Loader } from "lucide-react";
import { uploadNewImageFile } from "../../../../../services/blocks/Image/images";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
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 fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
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,
});
};
return (
<NodeViewWrapper className="block-image">
{!blockObject && props.extension.options.editable && (
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
) : (
<>
<div>
<Image className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleImageChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
</>
)}
</BlockImageWrapper>
)}
{blockObject && (
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
handleStyles={{
right: { position: 'unset', width: 7, height: 30, borderRadius: 20, cursor: 'col-resize', backgroundColor: 'black', opacity: '0.3', margin: 'auto', marginLeft:5 },
}}
style={{ margin: "auto", display: "flex", justifyContent: "center", alignItems: "center", height: "100%" }}
maxWidth={1000}
minWidth={200}
onResizeStop={(e, direction, ref, d) => {
props.updateAttributes({
size: {
width: imageSize.width + d.width,
}
});
setImageSize({
width: imageSize.width + d.width,
});
}}
>
<img
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id,
props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'imageBlock')}`}
alt=""
className="rounded-lg shadow "
/>
</Resizable>
)}
{isLoading && (
<div>
<AlertTriangle color="#e1e0e0" size={50} />
</div>
)}
</NodeViewWrapper>
);
}
export default ImageBlockComponent;
const BlockImageWrapper = styled.div`
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;
`;

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,93 @@
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 className="flex flex-col space-y-2 bg-gray-50 shadow-inner rounded-lg py-7 px-5">
<BlockMath>{equation}</BlockMath>
{isEditing && isEditable && (
<>
<EditBar>
<input value={equation} onChange={handleEquationChange} placeholder="Insert a Math Equation (LaTeX) " type="text" />
<button className="opacity-1" 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`
`;
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,91 @@
import { NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
import { AlertCircle, AlertTriangle, FileText, Image, ImagePlus, Info, Loader } from "lucide-react";
import { getPDFFile, uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
import { getBackendUrl } from "../../../../../services/config/config";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
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 fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
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 className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
) : (
<>
<div>
<FileText className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handlePDFChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
</>
)}
</BlockPDFWrapper>
)}
{blockObject && (
<BlockPDF>
<iframe
className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id,
props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'pdfBlock')}`}
/>
</BlockPDF>
)}
{isLoading && (
<div>
<AlertTriangle color="#e1e0e0" size={50} />
</div>
)}
</NodeViewWrapper>
);
}
export default PDFBlockComponent;
const BlockPDFWrapper = styled.div`
// 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,37 @@
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,
},
questions: {
default: [],
},
};
},
parseHTML() {
return [
{
tag: "block-quiz",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(QuizBlockComponent);
},
});

View file

@ -0,0 +1,323 @@
import { NodeViewWrapper } from "@tiptap/react";
import { v4 as uuidv4 } from "uuid";
import { twJoin, twMerge } from 'tailwind-merge'
import React from "react";
import { BadgeHelp, Check, Info, Minus, MoreVertical, Plus, RefreshCcw, X } from "lucide-react";
import ReactConfetti from "react-confetti";
interface Answer {
answer_id: string;
answer: string;
correct: boolean;
}
interface Question {
question_id: string;
question: string;
type: "multiple_choice" | 'custom_answer'
answers: Answer[];
}
function QuizBlockComponent(props: any) {
const [questions, setQuestions] = React.useState(props.node.attrs.questions) as [Question[], any];
const [userAnswers, setUserAnswers] = React.useState([]) as [any[], any];
const [submitted, setSubmitted] = React.useState(false) as [boolean, any];
const [submissionMessage, setSubmissionMessage] = React.useState("") as [string, any];
const isEditable = props.extension.options.editable;
const handleAnswerClick = (question_id: string, answer_id: string) => {
// if the quiz is submitted, do nothing
if (submitted) {
return;
}
const userAnswer = {
question_id: question_id,
answer_id: answer_id
}
const newAnswers = [...userAnswers, userAnswer];
// only accept one answer per question
const filteredAnswers = newAnswers.filter((answer: any) => answer.question_id !== question_id);
setUserAnswers([...filteredAnswers, userAnswer]);
}
const refreshUserSubmission = () => {
setUserAnswers([]);
setSubmitted(false);
}
const handleUserSubmission = () => {
if (userAnswers.length === 0) {
setSubmissionMessage("Please answer at least one question!");
return;
}
setSubmitted(true);
// check if all submitted answers are correct
const correctAnswers = questions.map((question: Question) => {
const correctAnswer: any = question.answers.find((answer: Answer) => answer.correct);
const userAnswer = userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id);
if (correctAnswer.answer_id === userAnswer.answer_id) {
return true;
} else {
return false;
}
});
// check if all answers are correct
const allCorrect = correctAnswers.every((answer: boolean) => answer === true);
if (allCorrect) {
setSubmissionMessage("All answers are correct!");
console.log("All answers are correct!");
}
else {
setSubmissionMessage("Some answers are incorrect!");
console.log("Some answers are incorrect!");
}
}
const getAnswerID = (answerIndex: number, questionId : string) => {
const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode('A'.charCodeAt(0) + i));
let alphabetID = alphabet[answerIndex];
// Get question index
const questionIndex = questions.findIndex((question: Question) => question.question_id === questionId);
let questionID = questionIndex + 1;
return `${alphabetID}`;
}
const saveQuestions = (questions: any) => {
props.updateAttributes({
questions: questions,
});
setQuestions(questions);
};
const addSampleQuestion = () => {
const newQuestion = {
question_id: uuidv4(),
question: "",
type: "multiple_choice",
answers: [
{
answer_id: uuidv4(),
answer: "",
correct: false
},
]
}
setQuestions([...questions, newQuestion]);
}
const addAnswer = (question_id: string) => {
const newAnswer = {
answer_id: uuidv4(),
answer: "",
correct: false
}
// check if there is already more thqn 5 answers
const question: any = questions.find((question: Question) => question.question_id === question_id);
if (question.answers.length >= 5) {
return;
}
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.push(newAnswer);
}
return question;
});
saveQuestions(newQuestions);
}
const changeAnswerValue = (question_id: string, answer_id: string, value: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.map((answer: Answer) => {
if (answer.answer_id === answer_id) {
answer.answer = value;
}
return answer;
});
}
return question;
});
saveQuestions(newQuestions);
}
const changeQuestionValue = (question_id: string, value: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.question = value;
}
return question;
});
saveQuestions(newQuestions);
}
const deleteQuestion = (question_id: string) => {
const newQuestions = questions.filter((question: Question) => question.question_id !== question_id);
saveQuestions(newQuestions);
}
const deleteAnswer = (question_id: string, answer_id: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers = question.answers.filter((answer: Answer) => answer.answer_id !== answer_id);
}
return question;
});
saveQuestions(newQuestions);
}
const markAnswerCorrect = (question_id: string, answer_id: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.map((answer: Answer) => {
if (answer.answer_id === answer_id) {
answer.correct = true;
} else {
answer.correct = false;
}
return answer;
});
}
return question;
});
saveQuestions(newQuestions);
}
return (
<NodeViewWrapper className="block-quiz">
<div
//style={{ background: "radial-gradient(152.15% 150.08% at 56.45% -6.67%, rgba(180, 255, 250, 0.10) 5.53%, rgba(202, 201, 255, 0.10) 66.76%)" }}
className="rounded-xl px-5 py-2 bg-slate-100 transition-all ease-linear"
>
<div className="flex space-x-2 pt-1 items-center text-sm overflow-hidden">
{(submitted && submissionMessage == "All answers are correct!") &&
<ReactConfetti
numberOfPieces={submitted ? 1400 : 0}
recycle={false}
className="w-full h-screen"
/>
}
<div className="flex space-x-2 items-center text-sm">
<BadgeHelp className='text-slate-400' size={15} />
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
</div>
<div className="grow flex items-center justify-center">
</div>
{isEditable ?
<div>
<button onClick={addSampleQuestion} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Add Question</button>
</div>
:
<div className="flex space-x-1 items-center">
<div onClick={() => refreshUserSubmission()} className="cursor-pointer px-2">
<RefreshCcw className='text-slate-400 cursor-pointer' size={15} />
</div>
<button onClick={() => handleUserSubmission()} className="bg-slate-200 hover:bg-slate-300 text-slate-800 font-bold py-1 px-2 rounded-lg text-xs">Submit</button>
</div>
}
</div>
{questions.map((question: Question) => (
<div key={question.question_id} className="pt-1 space-y-2">
<div className="question">
<div className="flex space-x-2 items-center">
<div className="flex-grow">
{isEditable ?
<input value={question.question} placeholder="Your Question" onChange={(e) => changeQuestionValue(question.question_id, e.target.value)} className="text-slate-800 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-md font-bold w-full"></input>
:
<p className="text-slate-800 bg-[#00008b00] rounded-md text-md font-bold w-full">{question.question}</p>
}
</div>
{isEditable &&
<div
onClick={() => deleteQuestion(question.question_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
<Minus
className="mx-auto text-slate-400" size={12} />
</div>
}
</div>
<div className="answers flex py-2 space-x-3">
{question.answers.map((answer: Answer) => (
<div
key={answer.answer_id}
className={twMerge(
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
answer.correct && isEditable ? 'outline-lime-300' : 'outline-white',
userAnswers.find((userAnswer: any) => (userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) && !isEditable) ? 'outline-slate-300' : '',
(submitted && answer.correct) ? 'outline-lime-300 text-lime' : '',
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'outline-red-400' : '',
)
}
onClick={() => handleAnswerClick(question.question_id, answer.answer_id)}
>
<div className={twMerge(
"bg-white font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800",
answer.correct && isEditable ? 'bg-lime-300 text-lime-800 outline-none' : 'bg-white',
(submitted && answer.correct) ? 'bg-lime-300 text-lime-800 outline-none' : '',
(submitted && !answer.correct) && userAnswers.find((userAnswer: any) => userAnswer.question_id === question.question_id && userAnswer.answer_id === answer.answer_id) ? 'bg-red-400 text-red-800 outline-none' : '',
)}>
<p className="mx-auto font-bold text-sm ">{getAnswerID(question.answers.indexOf(answer),question.question_id)}</p>
</div>
{isEditable ?
<input value={answer.answer} onChange={(e) => changeAnswerValue(question.question_id, answer.answer_id, e.target.value)} placeholder="Answer" className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"></input>
:
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] rounded-md ext-sm font-bold">{answer.answer}</p>
}
{isEditable &&
<div className="flex space-x-1 items-center">
<div
onClick={() => markAnswerCorrect(question.question_id, answer.answer_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-300 hover:bg-lime-400 transition-all ease-linear text-sm cursor-pointer ">
<Check
className="mx-auto text-lime-800" size={12} />
</div>
<div
onClick={() => deleteAnswer(question.question_id, answer.answer_id)}
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer">
<Minus
className="mx-auto text-slate-400" size={12} />
</div>
</div>
}
</div>
))}
{isEditable &&
<div onClick={() => addAnswer(question.question_id)} className="outline outline-3 w-[30px] flex-none flex items-center h-[30px] outline-white hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear">
<Plus className="mx-auto text-slate-800" size={15} />
</div>
}
</div>
</div>
</div>
))}
</div>
</NodeViewWrapper>
);
}
export default QuizBlockComponent;

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,80 @@
import { NodeViewWrapper } from "@tiptap/react";
import { AlertTriangle, Image, Loader, 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";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { UploadIcon } from "@radix-ui/react-icons";
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 fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
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 className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={props.extension.options.editable}>
{isLoading ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
) : (
<>
<div>
<Video className="text-gray-200" size={50} />
</div>
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleVideoChange} type="file" name="" id="" />
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
</>
)}
</BlockVideoWrapper>
)}
{blockObject && (
<BlockVideo>
<video
controls
className="rounded-lg shadow h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id,
props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'videoBlock')}`}
></video>
</BlockVideo>
)}
</NodeViewWrapper>
);
}
const BlockVideoWrapper = styled.div`
//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;
`;
export default VideoBlockComponents;