Merged in feature/level-file-upload (pull request #90)
ENCOA-149, ENCOA-150, ENCOA-152, ENCOA-153, ENCOA-155, ENCOA-156, ENCOA-157, ENCOA-158, ENCOA-161 Approved-by: Tiago Ribeiro
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,16 +286,17 @@ 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}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)) {
|
/*
|
||||||
|
// If client wants to revert uncomment and remove the added if statement
|
||||||
|
if (!seenParts.has(index)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else {
|
} else {
|
||||||
|
*/
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(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>
|
||||||
|
|||||||
@@ -40,6 +40,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