Part and MC question grid jump to, has a bug on next going to refactor the whole thing
This commit is contained in:
@@ -23,6 +23,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state);
|
const { shuffles, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state);
|
||||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
|
|
||||||
const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>();
|
const [currentMCSelection, setCurrentMCSelection] = useState<{ id: string, selection: FillBlanksMCOption }>();
|
||||||
@@ -122,20 +123,9 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelection = (id: string, value: string) => {
|
const onSelection = (questionID: string, value: string) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
||||||
}
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
|
|
||||||
const getShuffles = () => {
|
|
||||||
let shuffle = {};
|
|
||||||
if (shuffleMaps) {
|
|
||||||
shuffle = {
|
|
||||||
shuffleMaps: shuffleMaps.filter((map) =>
|
|
||||||
answers.some(answer => answer.id === map.questionID)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shuffle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -221,7 +211,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={
|
||||||
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
|
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
|
||||||
@@ -232,7 +222,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
|
onClick={() => {onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
|
|||||||
hasExamEnded,
|
hasExamEnded,
|
||||||
userSolutions: storeUserSolutions,
|
userSolutions: storeUserSolutions,
|
||||||
setQuestionIndex,
|
setQuestionIndex,
|
||||||
setUserSolutions
|
setUserSolutions,
|
||||||
|
setCurrentSolution
|
||||||
} = useExamStore((state) => state);
|
} = useExamStore((state) => state);
|
||||||
|
|
||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
@@ -106,6 +107,11 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
|
|||||||
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
|
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers])
|
||||||
|
|
||||||
const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => {
|
const getShuffledSolution = (originalSolution: string, questionShuffleMap: ShuffleMap) => {
|
||||||
for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) {
|
for (const [newPosition, originalPosition] of Object.entries(questionShuffleMap.map)) {
|
||||||
if (originalPosition === originalSolution) {
|
if (originalPosition === originalSolution) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
padding?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
type?: "button" | "reset" | "submit";
|
type?: "button" | "reset" | "submit";
|
||||||
}
|
}
|
||||||
@@ -21,6 +22,7 @@ export default function Button({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
|
padding = "py-4 px-6",
|
||||||
onClick,
|
onClick,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
|
const colorClassNames: {[key in typeof color]: {[key in typeof variant]: string}} = {
|
||||||
@@ -61,7 +63,8 @@ export default function Button({
|
|||||||
type={type}
|
type={type}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"py-4 px-6 rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer",
|
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer",
|
||||||
|
padding,
|
||||||
colorClassNames[color][variant],
|
colorClassNames[color][variant],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import useExamStore from "@/stores/examStore";
|
import { moduleLabels } from "@/utils/moduleUtils";
|
||||||
import {moduleLabels} from "@/utils/moduleUtils";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {motion} from "framer-motion";
|
import { Fragment, ReactNode, useCallback, useState } from "react";
|
||||||
import {ReactNode, useEffect, useState} from "react";
|
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
||||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
|
||||||
import ProgressBar from "../Low/ProgressBar";
|
import ProgressBar from "../Low/ProgressBar";
|
||||||
import TimerEndedModal from "../TimerEndedModal";
|
|
||||||
import Timer from "./Timer";
|
import Timer from "./Timer";
|
||||||
|
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
||||||
|
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import Button from "../Low/Button";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
@@ -18,13 +22,32 @@ interface Props {
|
|||||||
disableTimer?: boolean;
|
disableTimer?: boolean;
|
||||||
partLabel?: string;
|
partLabel?: string;
|
||||||
showTimer?: boolean;
|
showTimer?: boolean;
|
||||||
|
showSolutions?: boolean;
|
||||||
|
runOnClick?: ((questionIndex: number) => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModuleTitle({
|
export default function ModuleTitle({
|
||||||
minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel, showTimer = true
|
minTimer,
|
||||||
|
module,
|
||||||
|
label,
|
||||||
|
exerciseIndex,
|
||||||
|
totalExercises,
|
||||||
|
disableTimer = false,
|
||||||
|
partLabel,
|
||||||
|
showTimer = true,
|
||||||
|
showSolutions = false,
|
||||||
|
runOnClick = undefined
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const {
|
||||||
|
userSolutions,
|
||||||
|
partIndex,
|
||||||
|
exam
|
||||||
|
} = useExamStore((state) => state);
|
||||||
|
const examExerciseIndex = useExamStore((state) => state.exerciseIndex)
|
||||||
|
|
||||||
const moduleIcon: {[key in Module]: ReactNode} = {
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const moduleIcon: { [key in Module]: ReactNode } = {
|
||||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||||
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
||||||
@@ -32,6 +55,78 @@ export default function ModuleTitle({
|
|||||||
level: <BsClipboard className="text-ielts-level w-6 h-6" />,
|
level: <BsClipboard className="text-ielts-level w-6 h-6" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMultipleChoiceLevelExercise = () => {
|
||||||
|
if (exam?.module === 'level' && typeof partIndex === "number" && partIndex > -1) {
|
||||||
|
const currentExercise = (exam as LevelExam).parts[partIndex].exercises[examExerciseIndex];
|
||||||
|
return currentExercise && currentExercise.type === 'multipleChoice';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMCQuestionGrid = () => {
|
||||||
|
if (!isMultipleChoiceLevelExercise() && !userSolutions) return null;
|
||||||
|
|
||||||
|
const currentExercise = (exam as LevelExam).parts[partIndex!].exercises[examExerciseIndex] as MultipleChoiceExercise;
|
||||||
|
const userSolution = userSolutions!.find((x) => x.exercise == currentExercise.id)!;
|
||||||
|
const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question));
|
||||||
|
const exerciseOffset = currentExercise.questions[0].id;
|
||||||
|
const lastExercise = exerciseOffset + (currentExercise.questions.length - 1);
|
||||||
|
|
||||||
|
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
|
||||||
|
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
||||||
|
if (foundMap) return foundMap;
|
||||||
|
return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null;
|
||||||
|
}, null as ShuffleMap | null);
|
||||||
|
const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution;
|
||||||
|
|
||||||
|
if (!userSolutions) return "";
|
||||||
|
|
||||||
|
if (!userQuestionSolution) {
|
||||||
|
return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700";
|
||||||
|
}
|
||||||
|
|
||||||
|
return userQuestionSolution === newSolution ?
|
||||||
|
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" :
|
||||||
|
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${partIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3>
|
||||||
|
<div className="grid grid-cols-5 gap-3 px-4 py-2">
|
||||||
|
{currentExercise.questions.map((_, index) => {
|
||||||
|
const questionNumber = exerciseOffset + index;
|
||||||
|
const isAnswered = answeredQuestions.has(questionNumber);
|
||||||
|
const solution = currentExercise.questions.find((x) => x.id == questionNumber)!.solution;
|
||||||
|
|
||||||
|
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question == questionNumber)?.option;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
|
||||||
|
key={index}
|
||||||
|
className={clsx(
|
||||||
|
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
|
||||||
|
(showSolutions ?
|
||||||
|
getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
|
||||||
|
(isAnswered ?
|
||||||
|
"bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark":
|
||||||
|
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
|
||||||
|
>
|
||||||
|
{questionNumber}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-gray-600 text-center">
|
||||||
|
Click a question number to jump to that question
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
|
{showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
|
||||||
@@ -67,6 +162,22 @@ export default function ModuleTitle({
|
|||||||
</div>
|
</div>
|
||||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
{isMultipleChoiceLevelExercise() && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg">
|
||||||
|
<BsFillGrid3X3GapFill size={24} />
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{renderMCQuestionGrid()}
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) =>
|
|||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
|
"absolute right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
|
||||||
standalone ? "top-6" : "top-4",
|
standalone ? "top-10" : "top-4",
|
||||||
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
|
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
|
||||||
)}
|
)}
|
||||||
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
|
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import Button from "./Low/Button";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
blankQuestions?: boolean;
|
type?: "module" | "blankQuestions" | "submit";
|
||||||
finishingWhat? : string;
|
unanswered?: boolean;
|
||||||
onClose: (next?: boolean) => void;
|
onClose: (next?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuestionsModal({ isOpen, onClose, blankQuestions = true, finishingWhat = "module" }: Props) {
|
export default function QuestionsModal({ isOpen, onClose, type = "module", unanswered = false }: Props) {
|
||||||
return (
|
return (
|
||||||
<Transition show={isOpen} as={Fragment}>
|
<Transition show={isOpen} as={Fragment}>
|
||||||
<Dialog onClose={() => onClose(false)} className="relative z-50">
|
<Dialog onClose={() => onClose(false)} className="relative z-50">
|
||||||
@@ -34,11 +34,11 @@ export default function QuestionsModal({ isOpen, onClose, blankQuestions = true,
|
|||||||
leaveTo="opacity-0 scale-95">
|
leaveTo="opacity-0 scale-95">
|
||||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
<Dialog.Panel className="w-full max-w-2xl h-fit p-8 rounded-xl bg-white flex flex-col gap-4">
|
<Dialog.Panel className="w-full max-w-2xl h-fit p-8 rounded-xl bg-white flex flex-col gap-4">
|
||||||
{blankQuestions ? (
|
{type === "module" && (
|
||||||
<>
|
<>
|
||||||
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
|
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
|
||||||
<span>
|
<span>
|
||||||
Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be
|
Please note that you are finishing the current module and once you proceed to the next module, you will no longer be
|
||||||
able to change the answers of the current one, including your unanswered questions. <br />
|
able to change the answers of the current one, including your unanswered questions. <br />
|
||||||
<br />
|
<br />
|
||||||
Are you sure you want to continue without completing those questions?
|
Are you sure you want to continue without completing those questions?
|
||||||
@@ -52,14 +52,16 @@ export default function QuestionsModal({ isOpen, onClose, blankQuestions = true,
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
): (
|
)}
|
||||||
|
{type === "blankQuestions" && (
|
||||||
<>
|
<>
|
||||||
<Dialog.Title className="font-bold text-xl">Confirm Submission</Dialog.Title>
|
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
|
||||||
<span>
|
<span>
|
||||||
Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be
|
You have left some questions unanswered in the current part. <br />
|
||||||
able to review the answers of the current one. <br />
|
|
||||||
<br />
|
<br />
|
||||||
Are you sure you want to continue?
|
If you wish to continue, you can still access this part later using the navigation bar at the top. <br />
|
||||||
|
<br />
|
||||||
|
Do you want to proceed to the next part, or would you like to go back and complete the unanswered questions in the current part?
|
||||||
</span>
|
</span>
|
||||||
<div className="w-full flex justify-between mt-8">
|
<div className="w-full flex justify-between mt-8">
|
||||||
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
|
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
@@ -71,6 +73,34 @@ export default function QuestionsModal({ isOpen, onClose, blankQuestions = true,
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{type === "submit" && (
|
||||||
|
<>
|
||||||
|
<Dialog.Title className="font-bold text-xl">Confirm Submission</Dialog.Title>
|
||||||
|
<span>
|
||||||
|
{unanswered ? (
|
||||||
|
<>
|
||||||
|
By clicking "Submit," you are finalizing your exam with some <b>questions left unanswered</b>. Once you submit, you will not be able to review or change any of your answers, including the unanswered ones. <br />
|
||||||
|
<br />
|
||||||
|
Are you sure you want to submit and complete the exam with unanswered questions?
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
By clicking "Submit," you are finalizing your exam. Once you submit, you will not be able to review or change any of your answers. <br />
|
||||||
|
<br />
|
||||||
|
Are you sure you want to submit and complete the exam?
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="w-full flex justify-between mt-8">
|
||||||
|
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption, ShuffleMap } from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { CommonProps } from ".";
|
import { CommonProps } from ".";
|
||||||
@@ -16,14 +16,14 @@ export default function FillBlanksSolutions({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}: FillBlanksExercise & CommonProps) {
|
}: FillBlanksExercise & CommonProps) {
|
||||||
|
|
||||||
// next and back was all messed up and still don't know why, anyways
|
|
||||||
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
||||||
|
|
||||||
const correctUserSolutions = storeUserSolutions.find(
|
const correctUserSolutions = storeUserSolutions.find(
|
||||||
(solution) => solution.exercise === id
|
(solution) => solution.exercise === id
|
||||||
)?.solutions;
|
)?.solutions;
|
||||||
|
|
||||||
|
const shuffles = useExamStore((state) => state.shuffles);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = correctUserSolutions!.filter((x) => {
|
const correct = correctUserSolutions!.filter((x) => {
|
||||||
@@ -65,16 +65,18 @@ export default function FillBlanksSolutions({
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
const id = match.replaceAll(/[\{\}]/g, "");
|
const questionId = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = correctUserSolutions!.find((x) => x.id.toString() === id.toString());
|
const userSolution = correctUserSolutions!.find((x) => x.id.toString() === questionId.toString());
|
||||||
const answerSolution = solutions.find(sol => sol.id.toString() === id.toString())!.solution;
|
const answerSolution = solutions.find(sol => sol.id.toString() === questionId.toString())!.solution;
|
||||||
|
const questionShuffleMap = shuffles.find((x) => x.exerciseID == id)?.shuffles.find((y) => y.questionID == questionId);
|
||||||
|
const newAnswerSolution = questionShuffleMap ? questionShuffleMap.map[answerSolution].toLowerCase() : answerSolution.toLowerCase();
|
||||||
|
|
||||||
if (!userSolution) {
|
if (!userSolution) {
|
||||||
let answerText;
|
let answerText;
|
||||||
if (typeCheckWordsMC(words)) {
|
if (typeCheckWordsMC(words)) {
|
||||||
const options = words.find((x) => x.id.toString() === id.toString());
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
||||||
const correctKey = Object.keys(options!.options).find(key =>
|
const correctKey = Object.keys(options!.options).find(key =>
|
||||||
key.toLowerCase() === answerSolution.toLowerCase()
|
key.toLowerCase() === newAnswerSolution
|
||||||
);
|
);
|
||||||
answerText = options!.options[correctKey as keyof typeof options];
|
answerText = options!.options[correctKey as keyof typeof options];
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +99,7 @@ export default function FillBlanksSolutions({
|
|||||||
: 'letter' in w
|
: 'letter' in w
|
||||||
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
||||||
: 'options' in w
|
: 'options' in w
|
||||||
? w.id === userSolution.id
|
? w.id === userSolution.questionId
|
||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -113,10 +115,10 @@ export default function FillBlanksSolutions({
|
|||||||
let correct;
|
let correct;
|
||||||
let solutionText;
|
let solutionText;
|
||||||
if (typeCheckWordsMC(words)) {
|
if (typeCheckWordsMC(words)) {
|
||||||
const options = words.find((x) => x.id.toString() === id.toString());
|
const options = words.find((x) => x.id.toString() === questionId.toString());
|
||||||
if (options) {
|
if (options) {
|
||||||
const correctKey = Object.keys(options.options).find(key =>
|
const correctKey = Object.keys(options.options).find(key =>
|
||||||
key.toLowerCase() === answerSolution.toLowerCase()
|
key.toLowerCase() === newAnswerSolution
|
||||||
);
|
);
|
||||||
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
|
||||||
solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
|
solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import TextComponent from "./TextComponent";
|
|||||||
import PartDivider from "./PartDivider";
|
import PartDivider from "./PartDivider";
|
||||||
import Timer from "@/components/Medium/Timer";
|
import Timer from "@/components/Medium/Timer";
|
||||||
import shuffleExamExercise from "./Shuffle";
|
import shuffleExamExercise from "./Shuffle";
|
||||||
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: LevelExam;
|
exam: LevelExam;
|
||||||
@@ -33,6 +34,17 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const levelBgColor = "bg-ielts-level-light";
|
const levelBgColor = "bg-ielts-level-light";
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||||
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
||||||
|
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||||
|
|
||||||
|
const [seenParts, setSeenParts] = useState<number[]>(showSolutions ? exam.parts.map((_, index) => index) : [0]);
|
||||||
|
const [lastSolution, setLastSolution] = useState<UserSolution | undefined>(undefined);
|
||||||
|
|
||||||
|
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
||||||
|
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
||||||
|
}>({
|
||||||
|
type: "blankQuestions",
|
||||||
|
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
||||||
|
});
|
||||||
|
|
||||||
const { setBgColor } = useExamStore((state) => state);
|
const { setBgColor } = useExamStore((state) => state);
|
||||||
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
|
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
@@ -48,7 +60,28 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
|
|
||||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||||
|
const [currentSolution, setCurrentSolution] = useExamStore((state) => [state.currentSolution, state.setCurrentSolution])
|
||||||
|
|
||||||
|
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof currentSolution !== "undefined") {
|
||||||
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
||||||
|
setCurrentSolution(undefined);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentSolution])
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if (showSolutions) {
|
||||||
|
const solutionShuffles = userSolutions.map(solution => ({
|
||||||
|
exerciseID: solution.exercise,
|
||||||
|
shuffles: solution.shuffleMaps || []
|
||||||
|
}));
|
||||||
|
setShuffles(solutionShuffles);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
},[]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded && exerciseIndex === -1) {
|
if (hasExamEnded && exerciseIndex === -1) {
|
||||||
@@ -56,27 +89,21 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
|
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
|
||||||
|
|
||||||
const getExercise = () => {
|
const getExercise = () => {;
|
||||||
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||||
if (!exercise) return undefined;
|
if (!exercise) return undefined;
|
||||||
|
|
||||||
exercise = {
|
exercise = {
|
||||||
...exercise,
|
...exercise,
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||||
};
|
};
|
||||||
if (showSolutions) {
|
|
||||||
setShuffles([...shuffles.filter((x) => x.exerciseID !== exercise.id), { exerciseID: exercise.id, shuffles: userSolutions.find((x) => x.exercise === exercise.id)!.shuffleMaps!}]);
|
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
||||||
} else {
|
|
||||||
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
|
||||||
}
|
|
||||||
return exercise;
|
return exercise;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setCurrentExercise(getExercise());
|
||||||
if (exerciseIndex !== -1) {
|
|
||||||
setCurrentExercise(getExercise());
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [partIndex, exerciseIndex, exam.parts[partIndex].context]);
|
}, [partIndex, exerciseIndex, exam.parts[partIndex].context]);
|
||||||
|
|
||||||
@@ -108,24 +135,31 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
setExerciseIndex(exerciseIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded && (showQuestionsModal || showSolutions)) {
|
if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) {
|
||||||
|
setLastSolution(solution);
|
||||||
|
modalKwargs();
|
||||||
|
setShowQuestionsModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||||
|
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions) {
|
||||||
|
modalKwargs();
|
||||||
|
setShowQuestionsModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!showSolutions && exam.parts[0].intro) {
|
if (!showSolutions && exam.parts[0].intro) {
|
||||||
setShowPartDivider(true);
|
setShowPartDivider(true);
|
||||||
setBgColor(levelBgColor);
|
setBgColor(levelBgColor);
|
||||||
}
|
}
|
||||||
|
setSeenParts((prev) => [...prev, partIndex + 1])
|
||||||
setPartIndex(partIndex + 1);
|
setPartIndex(partIndex + 1);
|
||||||
setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0);
|
setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0);
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
@@ -133,28 +167,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions) {
|
|
||||||
setShowQuestionsModal(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
solution &&
|
|
||||||
![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every(
|
|
||||||
(x) => x === 0,
|
|
||||||
) &&
|
|
||||||
!showSolutions &&
|
|
||||||
!editing &&
|
|
||||||
!hasExamEnded
|
|
||||||
) {
|
|
||||||
setShowQuestionsModal(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
if (typeof showSolutionsSave !== "undefined") {
|
||||||
if (solution) {
|
onFinish(showSolutionsSave);
|
||||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);
|
|
||||||
} else {
|
} else {
|
||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
}
|
}
|
||||||
@@ -200,19 +215,28 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}, [exerciseIndex])
|
}, [exerciseIndex])
|
||||||
|
|
||||||
const calculateExerciseIndex = () => {
|
const calculateExerciseIndex = () => {
|
||||||
if (partIndex === 0) {
|
if (exam.parts[0].intro) {
|
||||||
|
return exam.parts.reduce((acc, curr, index) => {
|
||||||
|
if (index < partIndex) {
|
||||||
|
return acc + countExercises(curr.exercises)
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0) + (storeQuestionIndex + 1);
|
||||||
|
} else {
|
||||||
|
if (partIndex === 0) {
|
||||||
|
return (
|
||||||
|
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
|
||||||
|
const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0);
|
||||||
return (
|
return (
|
||||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
|
exercisesDone +
|
||||||
|
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
||||||
|
storeQuestionIndex
|
||||||
|
+ multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
|
|
||||||
const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0);
|
|
||||||
return (
|
|
||||||
exercisesDone +
|
|
||||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
|
||||||
storeQuestionIndex
|
|
||||||
+ multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderText = () => (
|
const renderText = () => (
|
||||||
@@ -247,8 +271,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modalKwargs = () => {
|
const answeredEveryQuestion = (partIndex: number) => {
|
||||||
const allSolutionsCorrectLength = exam.parts[partIndex].exercises.every((exercise) => {
|
return exam.parts[partIndex].exercises.every((exercise) => {
|
||||||
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
||||||
if (exercise.type === "multipleChoice") {
|
if (exercise.type === "multipleChoice") {
|
||||||
return userSolution?.solutions.length === exercise.questions.length;
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
@@ -258,27 +282,81 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
blankQuestions: !allSolutionsCorrectLength,
|
|
||||||
finishingWhat: "part",
|
|
||||||
onClose: partIndex !== exam.parts.length - 1 ? (
|
|
||||||
function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
|
||||||
) : function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); onFinish(userSolutions); } else { setShowQuestionsModal(false) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (continueAnyways) {
|
||||||
|
nextExercise(lastSolution);
|
||||||
|
setContinueAnyways(false);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [continueAnyways]);
|
||||||
|
|
||||||
|
const modalKwargs = () => {
|
||||||
|
const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = {
|
||||||
|
type: "blankQuestions",
|
||||||
|
unanswered: false,
|
||||||
|
onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (partIndex === exam.parts.length - 1) {
|
||||||
|
kwargs.type = "submit"
|
||||||
|
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
||||||
|
kwargs.onClose = function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||||
|
}
|
||||||
|
setQuestionModalKwargs(kwargs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcNavKwargs = {
|
||||||
|
userSolutions: userSolutions,
|
||||||
|
exam: exam,
|
||||||
|
partIndex: partIndex,
|
||||||
|
showSolutions: showSolutions,
|
||||||
|
"setExerciseIndex": setExerciseIndex,
|
||||||
|
"setPartIndex": setPartIndex,
|
||||||
|
"runOnClick": setStoreQuestionIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
||||||
<QuestionsModal isOpen={showQuestionsModal} {...modalKwargs()} />
|
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||||
{
|
{
|
||||||
!(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) &&
|
!(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) &&
|
||||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
||||||
}
|
}
|
||||||
{exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : (
|
{exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : (
|
||||||
<>
|
<>
|
||||||
|
{exam.parts[0].intro && (
|
||||||
|
<div className="w-full">
|
||||||
|
<Tab.Group className="w-[90%]" selectedIndex={partIndex} onChange={setPartIndex}>
|
||||||
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||||
|
{exam.parts.map((_, index) =>
|
||||||
|
<Tab key={index} onClick={(e) => {
|
||||||
|
if (!seenParts.includes(index)) {
|
||||||
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
setExerciseIndex(0);
|
||||||
|
setStoreQuestionIndex(0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={({ selected }) =>
|
||||||
|
clsx(
|
||||||
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/80",
|
||||||
|
"ring-white ring-opacity-60 focus:outline-none",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
selected && "bg-white shadow",
|
||||||
|
seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>{`Part ${index + 1}`}</Tab>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Tab.List>
|
||||||
|
</Tab.Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
partLabel={partLabel()}
|
partLabel={partLabel()}
|
||||||
minTimer={exam.minTimer}
|
minTimer={exam.minTimer}
|
||||||
@@ -287,6 +365,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||||
disableTimer={showSolutions || editing}
|
disableTimer={showSolutions || editing}
|
||||||
showTimer={false}
|
showTimer={false}
|
||||||
|
{...mcNavKwargs}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface ExamState {
|
|||||||
inactivity: number;
|
inactivity: number;
|
||||||
shuffles: Shuffles[];
|
shuffles: Shuffles[];
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
|
currentSolution?: UserSolution | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExamFunctions {
|
export interface ExamFunctions {
|
||||||
@@ -39,6 +40,7 @@ export interface ExamFunctions {
|
|||||||
setInactivity: (inactivity: number) => void;
|
setInactivity: (inactivity: number) => void;
|
||||||
setShuffles: (shuffles: Shuffles[]) => void;
|
setShuffles: (shuffles: Shuffles[]) => void;
|
||||||
setBgColor: (bgColor: string) => void;
|
setBgColor: (bgColor: string) => void;
|
||||||
|
setCurrentSolution: (currentSolution: UserSolution | undefined) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +60,8 @@ export const initialState: ExamState = {
|
|||||||
questionIndex: 0,
|
questionIndex: 0,
|
||||||
inactivity: 0,
|
inactivity: 0,
|
||||||
shuffles: [],
|
shuffles: [],
|
||||||
bgColor: "bg-white"
|
bgColor: "bg-white",
|
||||||
|
currentSolution: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||||
@@ -80,6 +83,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
|||||||
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
||||||
setShuffles: (shuffles: Shuffles[]) => set(() => ({shuffles})),
|
setShuffles: (shuffles: Shuffles[]) => set(() => ({shuffles})),
|
||||||
setBgColor: (bgColor) => set(() => ({bgColor})),
|
setBgColor: (bgColor) => set(() => ({bgColor})),
|
||||||
|
setCurrentSolution: (currentSolution: UserSolution | undefined) => set(() => ({currentSolution})),
|
||||||
|
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user