ENCOA-149, ENCOA-150, ENCOA-152, ENCOA-153, ENCOA-155, ENCOA-156, ENCOA-157, ENCOA-158, ENCOA-161 -> Updated the mc buttons, no longer shows a context only div on parts that have context, removed line numbers on lines between paragraphs, applied bold and underline to 'not correct' in underline prompts, added another pop up to confirm submission.
This commit is contained in:
@@ -173,23 +173,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
||||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
||||||
"border-mti-purple-light",
|
"!bg-mti-purple-light !text-white",
|
||||||
)}>
|
)}>
|
||||||
<span className="font-semibold">{key}.</span>
|
<span className="font-semibold">{key}.</span>
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
/*<button
|
|
||||||
className={clsx(
|
|
||||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
|
||||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
|
||||||
"bg-mti-purple-dark text-white",
|
|
||||||
)}
|
|
||||||
key={v4()}
|
|
||||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</button>;*/
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function Question({
|
|||||||
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
|
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base select-none",
|
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base select-none",
|
||||||
userSolution === option.id.toString() && "border-mti-purple-light",
|
userSolution === option.id.toString() && "!bg-mti-purple-light !text-white",
|
||||||
)}>
|
)}>
|
||||||
<span className="font-semibold">{option.id.toString()}.</span>
|
<span className="font-semibold">{option.id.toString()}.</span>
|
||||||
<span>{option.text}</span>
|
<span>{option.text}</span>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { moduleLabels } from "@/utils/moduleUtils";
|
import { moduleLabels } from "@/utils/moduleUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Fragment, ReactNode, useCallback, useState } from "react";
|
import { ReactNode, 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 Timer from "./Timer";
|
import Timer from "./Timer";
|
||||||
import { Exam, Exercise, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
import { Exercise, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
||||||
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -145,7 +143,7 @@ export default function ModuleTitle({
|
|||||||
return (
|
return (
|
||||||
<div key={index} className="text-2xl font-semibold flex flex-col gap-2">
|
<div key={index} className="text-2xl font-semibold flex flex-col gap-2">
|
||||||
{partInstructions.split("\\n").map((line, lineIndex) => (
|
{partInstructions.split("\\n").map((line, lineIndex) => (
|
||||||
<span key={lineIndex}>{line}</span>
|
<span key={lineIndex} dangerouslySetInnerHTML={{__html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ interface Props {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
titleClassName?: string;
|
||||||
children?: ReactElement;
|
children?: ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({isOpen, title, className, onClose, children}: Props) {
|
export default function Modal({isOpen, title, className, titleClassName, onClose, children}: Props) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
||||||
@@ -41,7 +42,7 @@ export default function Modal({isOpen, title, className, onClose, children}: Pro
|
|||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
{title && (
|
{title && (
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className={clsx(titleClassName ? titleClassName : "text-lg font-medium leading-6 text-gray-900")}>
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const PartDivider: React.FC<Props> = ({ partIndex, part, onNext }) => {
|
|||||||
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", part.intro ? "w-3/6" : "items-center my-auto")}>
|
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", part.intro ? "w-3/6" : "items-center my-auto")}>
|
||||||
{/** only level for now */}
|
{/** only level for now */}
|
||||||
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{part.intro ? `Part ${partIndex + 1}` : "Placement Test"}</p></div>
|
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{part.intro ? `Part ${partIndex + 1}` : "Placement Test"}</p></div>
|
||||||
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip">{x}</p>)}
|
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip" dangerouslySetInnerHTML={{__html: x.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></p>)}
|
||||||
<div className="flex items-center justify-center mt-4">
|
<div className="flex items-center justify-center mt-4">
|
||||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
||||||
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||||
const [lineHeight, setLineHeight] = useState<number>(0);
|
const [lineHeight, setLineHeight] = useState<number>(0);
|
||||||
|
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
|
||||||
|
|
||||||
const calculateLineNumbers = () => {
|
const calculateLineNumbers = () => {
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
@@ -31,17 +32,33 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
const textContent = textRef.current.textContent || '';
|
const textContent = textRef.current.textContent || '';
|
||||||
const lines = textContent.split(/\n/).map(line =>
|
|
||||||
line.split(/(\s+)/).map(word => {
|
const paragraphs = textContent.split(/\n\n/);
|
||||||
|
const betweenParagraphs: string[][] = Array.from({ length: paragraphs.length }, () => []);
|
||||||
|
|
||||||
|
const lines = paragraphs.map((line, lineIndex) => {
|
||||||
|
const paragraphWords = line.split(/(\s+)/);
|
||||||
|
return paragraphWords.map((word, wordIndex) => {
|
||||||
|
|
||||||
|
if (lineIndex !== 0 && wordIndex == 0 && lineIndex < paragraphs.length) {
|
||||||
|
betweenParagraphs[lineIndex - 1][1] = word;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
|
||||||
|
betweenParagraphs[lineIndex][0] = word;
|
||||||
|
}
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.textContent = word;
|
span.textContent = word;
|
||||||
return span;
|
return span;
|
||||||
})
|
})
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Append all spans to offscreenElement
|
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
line.forEach(span => offscreenElement.appendChild(span));
|
line.forEach((span, index) => {
|
||||||
|
offscreenElement.appendChild(span);
|
||||||
|
});
|
||||||
offscreenElement.appendChild(document.createElement('br'));
|
offscreenElement.appendChild(document.createElement('br'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,10 +76,21 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
|
|
||||||
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
||||||
|
|
||||||
spans.forEach(span => {
|
let betweenIndex = 0;
|
||||||
|
const addBreaksTo: number[] = [];
|
||||||
|
spans.forEach((span, index)=> {
|
||||||
const rect = span.getBoundingClientRect();
|
const rect = span.getBoundingClientRect();
|
||||||
const top = rect.top;
|
const top = rect.top;
|
||||||
|
|
||||||
|
if (
|
||||||
|
betweenIndex < paragraphs.length - 1 &&
|
||||||
|
span.textContent === betweenParagraphs[betweenIndex][1] &&
|
||||||
|
spans[index - 1].textContent === betweenParagraphs[betweenIndex][0]
|
||||||
|
) {
|
||||||
|
addBreaksTo.push(currentLine);
|
||||||
|
betweenIndex = betweenIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentLineTop !== undefined && top > currentLineTop) {
|
if (currentLineTop !== undefined && top > currentLineTop) {
|
||||||
currentLine++;
|
currentLine++;
|
||||||
currentLineTop = top;
|
currentLineTop = top;
|
||||||
@@ -75,6 +103,9 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
contextWordLine = currentLine;
|
contextWordLine = currentLine;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setAddBreaksTo(addBreaksTo);
|
||||||
|
|
||||||
setLineNumbers(processedLines.map((_, index) => index + 1));
|
setLineNumbers(processedLines.map((_, index) => index + 1));
|
||||||
if (contextWordLine) {
|
if (contextWordLine) {
|
||||||
setContextWordLine(contextWordLine);
|
setContextWordLine(contextWordLine);
|
||||||
@@ -85,7 +116,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateLineNumbers();
|
calculateLineNumbers();
|
||||||
|
|
||||||
@@ -107,30 +137,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [part.context, contextWord]);
|
}, [part.context, contextWord]);
|
||||||
|
|
||||||
/*if (typeof part.showContextLines === "undefined") {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
|
||||||
{!!part.context &&
|
|
||||||
part.context
|
|
||||||
.split(/\n|(\\n)/g)
|
|
||||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
|
||||||
.map((line, index) => (
|
|
||||||
<Fragment key={index}>
|
|
||||||
<p key={index}>{line}</p>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mt-2">
|
<div className="flex mt-2">
|
||||||
<div className="flex-shrink-0 w-8 pr-2">
|
<div className="flex-shrink-0 w-8 pr-2">
|
||||||
{lineNumbers.map(num => (
|
{lineNumbers.map(num => (
|
||||||
|
<>
|
||||||
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
||||||
{num}
|
{num}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Do not delete the space between the span or else the lines get messed up */}
|
||||||
|
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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";
|
import { Tab } from "@headlessui/react";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: LevelExam;
|
exam: LevelExam;
|
||||||
@@ -51,7 +52,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
setCurrentSolution
|
setCurrentSolution
|
||||||
} = useExamStore((state) => state);
|
} = useExamStore((state) => state);
|
||||||
|
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
// In case client want to switch back
|
||||||
|
const textRenderDisabled = true;
|
||||||
|
|
||||||
|
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
||||||
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
||||||
const [continueAnyways, setContinueAnyways] = useState(false);
|
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||||
const [textRender, setTextRender] = useState(false);
|
const [textRender, setTextRender] = useState(false);
|
||||||
@@ -59,7 +63,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
||||||
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
||||||
|
|
||||||
const [seenParts, setSeenParts] = useState<number[]>(showSolutions ? exam.parts.map((_, index) => index) : [0]);
|
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
|
||||||
|
|
||||||
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
||||||
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
||||||
@@ -147,24 +151,25 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||||
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.includes(partIndex + 1)) {
|
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
||||||
modalKwargs();
|
modalKwargs();
|
||||||
setShowQuestionsModal(true);
|
setShowQuestionsModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showSolutions && exam.parts[0].intro && !seenParts.includes(partIndex + 1)) {
|
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
|
||||||
setShowPartDivider(true);
|
setShowPartDivider(true);
|
||||||
setBgColor(levelBgColor);
|
setBgColor(levelBgColor);
|
||||||
}
|
}
|
||||||
setSeenParts((prev) => [...prev, partIndex + 1])
|
|
||||||
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context) {
|
setSeenParts(prev => new Set(prev).add(partIndex + 1));
|
||||||
|
|
||||||
|
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) {
|
||||||
setTextRender(true);
|
setTextRender(true);
|
||||||
}
|
}
|
||||||
setPartIndex(partIndex + 1);
|
setPartIndex(partIndex + 1);
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : questionIndex }]);
|
|
||||||
setCurrentSolutionSet(false);
|
setCurrentSolutionSet(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -194,7 +199,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const previousExercise = (solution?: UserSolution) => {
|
const previousExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
|
|
||||||
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender) {
|
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
|
||||||
setTextRender(true);
|
setTextRender(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,31 +226,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
if (previousExercise.type === "multipleChoice") {
|
if (previousExercise.type === "multipleChoice") {
|
||||||
setQuestionIndex(previousExercise.questions.length - 1)
|
setQuestionIndex(previousExercise.questions.length - 1)
|
||||||
}
|
}
|
||||||
const multipleChoiceQuestionsDone = [];
|
|
||||||
for (let i = 0; i < exam.parts.length; i++) {
|
|
||||||
if (i == (partIndex - 1)) break;
|
|
||||||
for (let j = 0; j < exam.parts[i].exercises.length; j++) {
|
|
||||||
const exercise = exam.parts[i].exercises[j];
|
|
||||||
switch(exercise.type) {
|
|
||||||
case 'multipleChoice':
|
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 })
|
|
||||||
break;
|
|
||||||
case 'fillBlanks':
|
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 })
|
|
||||||
break;
|
|
||||||
case 'writeBlanks':
|
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.solutions.length - 1 })
|
|
||||||
break;
|
|
||||||
case 'matchSentences':
|
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.sentences.length - 1})
|
|
||||||
break;
|
|
||||||
case 'trueFalse':
|
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1})
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setMultipleChoicesDone(multipleChoiceQuestionsDone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -264,7 +244,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col w-full gap-2">
|
<div className="flex flex-col w-full gap-2">
|
||||||
{textRender ? (
|
{textRender && !textRenderDisabled ? (
|
||||||
<>
|
<>
|
||||||
<h4 className="text-xl font-semibold">
|
<h4 className="text-xl font-semibold">
|
||||||
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
||||||
@@ -286,7 +266,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
{textRender && (
|
{textRender && !textRenderDisabled && (
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
@@ -306,23 +286,24 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
);
|
);
|
||||||
|
|
||||||
const partLabel = () => {
|
const partLabel = () => {
|
||||||
|
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
|
||||||
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
||||||
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})\n\n${currentExercise.prompt}`
|
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||||
|
|
||||||
if (currentExercise?.type === "multipleChoice") {
|
if (currentExercise?.type === "multipleChoice") {
|
||||||
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})\n\n${currentExercise.prompt}`
|
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof exam.parts[partIndex].context === "string") {
|
if (typeof exam.parts[partIndex].context === "string") {
|
||||||
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
|
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
|
||||||
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})\n\n${nextExercise.prompt}`
|
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const answeredEveryQuestion = (partIndex: number) => {
|
const answeredEveryQuestion = (partIndex: number) => {
|
||||||
return 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);
|
||||||
switch(exercise.type) {
|
switch (exercise.type) {
|
||||||
case 'multipleChoice':
|
case 'multipleChoice':
|
||||||
return userSolution?.solutions.length === exercise.questions.length;
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
case 'fillBlanks':
|
case 'fillBlanks':
|
||||||
@@ -388,7 +369,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
if (partIndex === exam.parts.length - 1) {
|
if (partIndex === exam.parts.length - 1) {
|
||||||
kwargs.type = "submit"
|
kwargs.type = "submit"
|
||||||
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
||||||
kwargs.onClose = function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||||
}
|
}
|
||||||
setQuestionModalKwargs(kwargs);
|
setQuestionModalKwargs(kwargs);
|
||||||
}
|
}
|
||||||
@@ -408,7 +389,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
setChangedPrompt(false);
|
setChangedPrompt(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{textRender ?
|
{textRender && !textRenderDisabled ?
|
||||||
renderText() :
|
renderText() :
|
||||||
<>
|
<>
|
||||||
{exam.parts[partIndex].context && renderText()}
|
{exam.parts[partIndex].context && renderText()}
|
||||||
@@ -425,6 +406,25 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
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")}>
|
||||||
|
<Modal
|
||||||
|
className={"!w-2/6 !p-8"}
|
||||||
|
titleClassName={"font-bold text-3xl text-mti-rose-light"}
|
||||||
|
isOpen={showSubmissionModal}
|
||||||
|
onClose={() => { }}
|
||||||
|
title={"Confirm Submission"}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p>
|
||||||
|
<div className="w-full flex justify-between">
|
||||||
|
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true)}} className="max-w-[200px] self-end w-full !text-xl">
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||||
{
|
{
|
||||||
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
||||||
@@ -438,20 +438,27 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||||
{exam.parts.map((_, index) =>
|
{exam.parts.map((_, index) =>
|
||||||
<Tab key={index} onClick={(e) => {
|
<Tab key={index} onClick={(e) => {
|
||||||
if (!seenParts.includes(index)) {
|
/*
|
||||||
e.preventDefault();
|
// If client wants to revert uncomment and remove the added if statement
|
||||||
} else {
|
if (!seenParts.has(index)) {
|
||||||
setExerciseIndex(0);
|
e.preventDefault();
|
||||||
setQuestionIndex(0);
|
} else {
|
||||||
|
*/
|
||||||
|
setExerciseIndex(0);
|
||||||
|
setQuestionIndex(0);
|
||||||
|
if (!seenParts.has(index)) {
|
||||||
|
setShowPartDivider(true);
|
||||||
|
setBgColor(levelBgColor);
|
||||||
|
setSeenParts(prev => new Set(prev).add(index));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
clsx(
|
clsx(
|
||||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/80",
|
"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",
|
"ring-white ring-opacity-60 focus:outline-none",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out hover:bg-white/70",
|
||||||
selected && "bg-white shadow",
|
selected && "bg-white shadow",
|
||||||
seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed"
|
// seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>{`Part ${index + 1}`}</Tab>
|
>{`Part ${index + 1}`}</Tab>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface LevelExam extends ExamBase {
|
|||||||
export interface LevelPart {
|
export interface LevelPart {
|
||||||
context?: string;
|
context?: string;
|
||||||
intro?: string;
|
intro?: string;
|
||||||
|
category?: string;
|
||||||
exercises: Exercise[];
|
exercises: Exercise[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user