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:
carlos.mesquita
2024-09-04 08:10:40 +00:00
committed by Tiago Ribeiro
8 changed files with 106 additions and 94 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>
)} )}

View File

@@ -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}`}

View File

@@ -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">

View File

@@ -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&apos;ve read. Please read the following excerpt attentively, you will then be asked questions about the text you&apos;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>

View File

@@ -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[];
} }