mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement quiz logic and improve design
This commit is contained in:
parent
f93fd96bb5
commit
8d29c5cddd
6 changed files with 164 additions and 38 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from "@services/config/config";
|
||||||
import Canva from "@components/Pages/Activities/DynamicCanva/DynamicCanva";
|
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
|
||||||
import VideoActivity from "@components/Pages/Activities/Video/Video";
|
import VideoActivity from "@components/Objects/Activities/Video/Video";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { markActivityAsComplete } from "@services/courses/activity";
|
import { markActivityAsComplete } from "@services/courses/activity";
|
||||||
import DocumentPdfActivity from "@components/Pages/Activities/DocumentPdf/DocumentPdf";
|
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
|
||||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import VideoBlock from "@components/Objects/Editor/Extensions/Video/VideoBlock";
|
||||||
import MathEquationBlock from "@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock";
|
import MathEquationBlock from "@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock";
|
||||||
import PDFBlock from "@components/Objects/Editor/Extensions/PDF/PDFBlock";
|
import PDFBlock from "@components/Objects/Editor/Extensions/PDF/PDFBlock";
|
||||||
import { OrderedList } from "@tiptap/extension-ordered-list";
|
import { OrderedList } from "@tiptap/extension-ordered-list";
|
||||||
|
import QuizBlock from "@components/Objects/Editor/Extensions/Quiz/QuizBlock";
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -46,6 +47,10 @@ function Canva(props: Editor) {
|
||||||
editable: true,
|
editable: true,
|
||||||
activity: props.activity,
|
activity: props.activity,
|
||||||
}),
|
}),
|
||||||
|
QuizBlock.configure({
|
||||||
|
editable: isEditable,
|
||||||
|
activity: props.activity,
|
||||||
|
}),
|
||||||
Youtube.configure({
|
Youtube.configure({
|
||||||
controls: true,
|
controls: true,
|
||||||
modestBranding: true,
|
modestBranding: true,
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from "@tiptap/react";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { twJoin, twMerge } from 'tailwind-merge'
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BadgeHelp, Check, Minus, MoreVertical, Plus, X } from "lucide-react";
|
import { BadgeHelp, Check, Info, Minus, MoreVertical, Plus, RefreshCcw, X } from "lucide-react";
|
||||||
|
import ReactConfetti from "react-confetti";
|
||||||
|
|
||||||
interface Answer {
|
interface Answer {
|
||||||
answer_id: string;
|
answer_id: string;
|
||||||
|
|
@ -17,11 +19,80 @@ interface Question {
|
||||||
|
|
||||||
function QuizBlockComponent(props: any) {
|
function QuizBlockComponent(props: any) {
|
||||||
const [questions, setQuestions] = React.useState(props.node.attrs.questions) as [Question[], 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 isEditable = props.extension.options.editable;
|
||||||
|
|
||||||
const getAlphabetFromIndex = (index: number) => {
|
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));
|
const alphabet = Array.from({ length: 26 }, (_, i) => String.fromCharCode('A'.charCodeAt(0) + i));
|
||||||
return alphabet[index];
|
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) => {
|
const saveQuestions = (questions: any) => {
|
||||||
|
|
@ -121,6 +192,7 @@ function QuizBlockComponent(props: any) {
|
||||||
} else {
|
} else {
|
||||||
answer.correct = false;
|
answer.correct = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return answer;
|
return answer;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -132,18 +204,38 @@ function QuizBlockComponent(props: any) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="block-quiz">
|
<NodeViewWrapper className="block-quiz">
|
||||||
|
|
||||||
<div
|
<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%)" }}
|
//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"
|
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">
|
<div className="flex space-x-2 pt-1 items-center text-sm overflow-hidden">
|
||||||
<div className="grow flex space-x-2 items-center text-sm">
|
{(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} />
|
<BadgeHelp className='text-slate-400' size={15} />
|
||||||
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
|
<p className="uppercase tracking-widest text-xs font-bold py-1 text-slate-400">Quiz</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grow flex items-center justify-center">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{isEditable ?
|
||||||
<div>
|
<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>
|
<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>
|
||||||
|
:
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{questions.map((question: Question) => (
|
{questions.map((question: Question) => (
|
||||||
|
|
@ -151,44 +243,71 @@ function QuizBlockComponent(props: any) {
|
||||||
<div className="question">
|
<div className="question">
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<div className="flex-grow">
|
<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>
|
<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>
|
</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">
|
{isEditable &&
|
||||||
<Minus
|
<div
|
||||||
onClick={() => deleteQuestion(question.question_id)}
|
onClick={() => deleteQuestion(question.question_id)}
|
||||||
className="mx-auto text-red-200" size={12} />
|
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>
|
</div>
|
||||||
<div className="answers flex py-2 space-x-3">
|
<div className="answers flex py-2 space-x-3">
|
||||||
{question.answers.map((answer: Answer) => (
|
{question.answers.map((answer: Answer) => (
|
||||||
<div key={answer.answer_id}
|
<div
|
||||||
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`}>
|
key={answer.answer_id}
|
||||||
<div className="bg-white font-bold text-neutral-900 text-base flex items-center h-full w-[40px] rounded-md">
|
className={twMerge(
|
||||||
<p className="mx-auto">{getAlphabetFromIndex(question.answers.indexOf(answer))}</p>
|
'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-200' : '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",
|
||||||
|
(submitted && answer.correct) ? 'bg-lime-300 text-lime-800 ' : '',
|
||||||
|
(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>
|
</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>
|
<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 className="flex space-x-1 items-center">
|
||||||
<div
|
<div
|
||||||
onClick={() => markAnswerCorrect(question.question_id, answer.answer_id)}
|
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 ">
|
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-lime-200 hover:bg-lime-300 transition-all ease-linear text-sm cursor-pointer ">
|
||||||
<Check
|
<Check
|
||||||
className="mx-auto text-green-800" size={12} />
|
className="mx-auto text-lime-800" size={12} />
|
||||||
</div>
|
</div>
|
||||||
<MoreVertical className="text-slate-300" size={15} />
|
|
||||||
<div
|
<div
|
||||||
onClick={() => deleteAnswer(question.question_id, answer.answer_id)}
|
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">
|
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
|
<Minus
|
||||||
className="mx-auto text-red-200" size={12} />
|
className="mx-auto text-slate-400" size={12} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
{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} />
|
<Plus className="mx-auto text-slate-800" size={15} />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
"react-confetti": "^6.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-katex": "^3.0.1",
|
"react-katex": "^3.0.1",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"react-youtube": "^10.1.0",
|
"react-youtube": "^10.1.0",
|
||||||
"styled-components": "^6.0.0-beta.9",
|
"styled-components": "^6.0.0-beta.9",
|
||||||
"swr": "^2.0.1",
|
"swr": "^2.0.1",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"y-indexeddb": "^9.0.9",
|
"y-indexeddb": "^9.0.9",
|
||||||
"y-webrtc": "^10.2.3",
|
"y-webrtc": "^10.2.3",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue