mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: init editable quiz
This commit is contained in:
parent
a01a0afea7
commit
f93fd96bb5
3 changed files with 187 additions and 146 deletions
|
|
@ -6,7 +6,6 @@ import QuizBlockComponent from "./QuizBlockComponent";
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "blockQuiz",
|
name: "blockQuiz",
|
||||||
group: "block",
|
group: "block",
|
||||||
|
|
||||||
atom: true,
|
atom: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
|
@ -14,6 +13,9 @@ export default Node.create({
|
||||||
quizId: {
|
quizId: {
|
||||||
value: null,
|
value: null,
|
||||||
},
|
},
|
||||||
|
questions: {
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +1,203 @@
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import { BadgeHelp, Check, Minus, MoreVertical, Plus, X } from "lucide-react";
|
||||||
import { submitQuizBlock } from "@services/blocks/Quiz/quiz";
|
|
||||||
|
|
||||||
function ImageBlockComponent(props: any) {
|
interface Answer {
|
||||||
const [questions, setQuestions] = React.useState([]) as any;
|
answer_id: string;
|
||||||
const [answers, setAnswers] = React.useState([]) as any;
|
answer: string;
|
||||||
|
correct: boolean;
|
||||||
|
}
|
||||||
|
interface Question {
|
||||||
|
question_id: string;
|
||||||
|
question: string;
|
||||||
|
type: "multiple_choice" | 'custom_answer'
|
||||||
|
answers: Answer[];
|
||||||
|
}
|
||||||
|
|
||||||
function addSampleQuestion() {
|
function QuizBlockComponent(props: any) {
|
||||||
setQuestions([
|
const [questions, setQuestions] = React.useState(props.node.attrs.questions) as [Question[], any];
|
||||||
...questions,
|
const isEditable = props.extension.options.editable;
|
||||||
{
|
|
||||||
question_id: "question_" + uuidv4(),
|
const getAlphabetFromIndex = (index: number) => {
|
||||||
question_value: "",
|
const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode('A'.charCodeAt(0) + i));
|
||||||
options: [
|
return alphabet[index];
|
||||||
{
|
|
||||||
option_id: "option_" + uuidv4(),
|
|
||||||
option_data: "",
|
|
||||||
option_type: "text",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteQuestion = (index: number) => {
|
const saveQuestions = (questions: any) => {
|
||||||
let modifiedQuestions = [...questions];
|
props.updateAttributes({
|
||||||
modifiedQuestions.splice(index, 1);
|
questions: questions,
|
||||||
setQuestions(modifiedQuestions);
|
|
||||||
|
|
||||||
|
|
||||||
// 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);
|
setQuestions(questions);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
const addSampleQuestion = () => {
|
||||||
const deleteOption = (question_id: string, option_id: string) => {
|
const newQuestion = {
|
||||||
// find the option index from the option_id and delete the option from that option index
|
question_id: uuidv4(),
|
||||||
let modifiedQuestions = [...questions];
|
question: "",
|
||||||
let questionIndex = modifiedQuestions.findIndex((question: any) => question.question_id === question_id);
|
type: "multiple_choice",
|
||||||
let optionIndex = modifiedQuestions[questionIndex].options.findIndex((option: any) => option.option_id === option_id);
|
answers: [
|
||||||
modifiedQuestions[questionIndex].options.splice(optionIndex, 1);
|
{
|
||||||
setQuestions(modifiedQuestions);
|
answer_id: uuidv4(),
|
||||||
|
answer: "",
|
||||||
// remove the answer from the answers array
|
correct: false
|
||||||
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]);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveQuiz = async () => {
|
|
||||||
// save the questions and answers to the backend
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
let res = await submitQuizBlock(props.extension.options.activity.activity_id, {questions : questions , answers : answers})
|
|
||||||
|
|
||||||
props.updateAttributes({
|
|
||||||
quizId: {
|
|
||||||
value : res.block_id
|
|
||||||
},
|
},
|
||||||
});
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
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 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
|
const newQuestions = questions.map((question: Question) => {
|
||||||
|
if (question.question_id === question_id) {
|
||||||
|
question.answers.push(newAnswer);
|
||||||
|
}
|
||||||
}, [questions, answers]);
|
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 (
|
return (
|
||||||
<NodeViewWrapper className="block-quiz">
|
<NodeViewWrapper className="block-quiz">
|
||||||
<QuizBlockWrapper>
|
<div
|
||||||
Questions <button onClick={addSampleQuestion}>Add Question</button> <button onClick={() => saveQuiz()}>Save</button>
|
//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%)" }}
|
||||||
<hr />
|
className="rounded-xl px-5 py-2 bg-slate-100 transition-all ease-linear"
|
||||||
{questions.map((question: any, qIndex: number) => (
|
>
|
||||||
<>
|
<div className="flex space-x-2 pt-1 items-center text-sm">
|
||||||
<div key={qIndex} style={{ marginTop: "10px" }}>
|
<div className="grow flex space-x-2 items-center text-sm">
|
||||||
Question : <input type="text" value={question.question} onChange={(e) => onQuestionChange(e, qIndex)} />
|
<BadgeHelp className='text-slate-400' size={15} />
|
||||||
<button onClick={() => deleteQuestion(qIndex)}>Delete</button>
|
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
|
||||||
</div>
|
</div>
|
||||||
Answers : <br />
|
<div>
|
||||||
{question.options.map((option: any, oIndex: number) => (
|
<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 key={oIndex}>
|
</div>
|
||||||
<input type="text" value={option.option_data} onChange={(e) => onOptionChange(e, qIndex, oIndex)} />
|
|
||||||
|
|
||||||
<button onClick={() => deleteOption(question.question_id, option.option_id)}>Delete</button>
|
{questions.map((question: Question) => (
|
||||||
<input
|
<div key={question.question_id} className="pt-1 space-y-2">
|
||||||
type="checkbox"
|
<div className="question">
|
||||||
onChange={(e) =>
|
<div className="flex space-x-2 items-center">
|
||||||
// check if checkbox is checked or not
|
<div className="flex-grow">
|
||||||
// if checked then add the answer to the answers array
|
<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>
|
||||||
// if unchecked then remove the answer from the answers array
|
|
||||||
e.target.checked ? markOptionAsCorrect(question.question_id, option.option_id) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-red-600 hover:bg-red-700 text-sm transition-all ease-linear cursor-pointer">
|
||||||
))}
|
<Minus
|
||||||
<button onClick={() => addOption(question.question_id)}>Add Option</button>
|
onClick={() => deleteQuestion(question.question_id)}
|
||||||
</>
|
className="mx-auto text-red-200" size={12} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="answers flex py-2 space-x-3">
|
||||||
|
{question.answers.map((answer: Answer) => (
|
||||||
|
<div key={answer.answer_id}
|
||||||
|
className={`outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] ${answer.correct ? 'outline-green-200' : 'outline-white'} bg-opacity-50 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`}>
|
||||||
|
<div className="bg-white font-bold text-neutral-900 text-base flex items-center h-full w-[40px] rounded-md">
|
||||||
|
<p className="mx-auto">{getAlphabetFromIndex(question.answers.indexOf(answer))}</p>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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-green-200 hover:bg-green-300 transition-all ease-linear text-sm cursor-pointer ">
|
||||||
|
<Check
|
||||||
|
className="mx-auto text-green-800" size={12} />
|
||||||
|
</div>
|
||||||
|
<MoreVertical className="text-slate-300" size={15} />
|
||||||
|
<div
|
||||||
|
onClick={() => deleteAnswer(question.question_id, answer.answer_id)}
|
||||||
|
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-red-600 hover:bg-red-700 text-sm transition-all ease-linear cursor-pointer">
|
||||||
|
<Minus
|
||||||
|
className="mx-auto text-red-200" size={12} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div onClick={() => addAnswer(question.question_id)} className="outline outline-3 shadow 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>
|
||||||
))}
|
))}
|
||||||
</QuizBlockWrapper>
|
|
||||||
|
</div>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuizBlockWrapper = styled.div`
|
|
||||||
background-color: #0000001d;
|
export default QuizBlockComponent;
|
||||||
border-radius: 5px;
|
|
||||||
padding: 20px;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
export default ImageBlockComponent;
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon, DividerVerticalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
||||||
import { AlertCircle, AlertTriangle, FileText, GraduationCap, ImagePlus, Info, Sigma, Video, Youtube } from "lucide-react";
|
import { AlertCircle, AlertTriangle, FileText, GraduationCap, HelpCircle, ImagePlus, Info, Sigma, Video, Youtube } from "lucide-react";
|
||||||
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
|
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
|
||||||
|
|
||||||
export const ToolbarButtons = ({ editor, props }: any) => {
|
export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
|
|
@ -59,7 +59,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
<option value="6">Heading 6</option>
|
<option value="6">Heading 6</option>
|
||||||
</ToolSelect>
|
</ToolSelect>
|
||||||
{/* TODO: fix this : toggling only works one-way */}
|
{/* TODO: fix this : toggling only works one-way */}
|
||||||
<DividerVerticalIcon style={{marginTop:"auto", marginBottom:"auto", color : "grey"}}/>
|
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey" }} />
|
||||||
<ToolTip content={"Info Callout"}>
|
<ToolTip content={"Info Callout"}>
|
||||||
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
|
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
|
||||||
<AlertCircle size={15} />
|
<AlertCircle size={15} />
|
||||||
|
|
@ -113,7 +113,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.insertContent({
|
.insertContent({
|
||||||
type: "blockMathEquation",
|
type: "blockMathEquation",
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
<FileText size={15} />
|
<FileText size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
{/* <ToolTip content={"Interactive Quiz"}>
|
<ToolTip content={"Interactive Quiz"}>
|
||||||
<ToolBtn
|
<ToolBtn
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
editor
|
editor
|
||||||
|
|
@ -148,9 +148,9 @@ export const ToolbarButtons = ({ editor, props }: any) => {
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GraduationCap size={15} />
|
<HelpCircle size={15} />
|
||||||
</ToolBtn>
|
</ToolBtn>
|
||||||
</ToolTip> */}
|
</ToolTip>
|
||||||
</ToolButtonsWrapper>
|
</ToolButtonsWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue