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:
Carlos Mesquita
2024-09-04 02:26:22 +01:00
parent 60554d8e16
commit 5168e70edc
8 changed files with 106 additions and 94 deletions

View File

@@ -173,23 +173,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
className={clsx(
"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) &&
"border-mti-purple-light",
"!bg-mti-purple-light !text-white",
)}>
<span className="font-semibold">{key}.</span>
<span>{value}</span>
</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>

View File

@@ -60,7 +60,7 @@ function Question({
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
className={clsx(
"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>{option.text}</span>

View File

@@ -1,15 +1,13 @@
import { Module } from "@/interfaces";
import { moduleLabels } from "@/utils/moduleUtils";
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 ProgressBar from "../Low/ProgressBar";
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 { 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";
import React from "react";
@@ -145,7 +143,7 @@ export default function ModuleTitle({
return (
<div key={index} className="text-2xl font-semibold flex flex-col gap-2">
{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>
);

View File

@@ -7,10 +7,11 @@ interface Props {
onClose: () => void;
title?: string;
className?: string;
titleClassName?: string;
children?: ReactElement;
}
export default function Modal({isOpen, title, className, onClose, children}: Props) {
export default function Modal({isOpen, title, className, titleClassName, onClose, children}: Props) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
@@ -41,7 +42,7 @@ export default function Modal({isOpen, title, className, onClose, children}: Pro
className,
)}>
{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}
</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")}>
{/** 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>
{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">
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
{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 [lineNumbers, setLineNumbers] = useState<number[]>([]);
const [lineHeight, setLineHeight] = useState<number>(0);
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
const calculateLineNumbers = () => {
if (textRef.current) {
@@ -31,17 +32,33 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
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');
span.textContent = word;
return span;
})
}
);
// Append all spans to offscreenElement
lines.forEach(line => {
line.forEach(span => offscreenElement.appendChild(span));
line.forEach((span, index) => {
offscreenElement.appendChild(span);
});
offscreenElement.appendChild(document.createElement('br'));
});
@@ -59,10 +76,21 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
spans.forEach(span => {
let betweenIndex = 0;
const addBreaksTo: number[] = [];
spans.forEach((span, index)=> {
const rect = span.getBoundingClientRect();
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) {
currentLine++;
currentLineTop = top;
@@ -75,6 +103,9 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
contextWordLine = currentLine;
}
});
setAddBreaksTo(addBreaksTo);
setLineNumbers(processedLines.map((_, index) => index + 1));
if (contextWordLine) {
setContextWordLine(contextWordLine);
@@ -85,7 +116,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
};
useEffect(() => {
calculateLineNumbers();
@@ -107,30 +137,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 (
<div className="flex mt-2">
<div className="flex-shrink-0 w-8 pr-2">
{lineNumbers.map(num => (
<>
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
{num}
</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 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 shuffleExamExercise from "./Shuffle";
import { Tab } from "@headlessui/react";
import Modal from "@/components/Modal";
interface Props {
exam: LevelExam;
@@ -51,7 +52,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setCurrentSolution
} = 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 [continueAnyways, setContinueAnyways] = 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 [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<{
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 (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.includes(partIndex + 1)) {
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
modalKwargs();
setShowQuestionsModal(true);
return;
}
if (!showSolutions && exam.parts[0].intro && !seenParts.includes(partIndex + 1)) {
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
setShowPartDivider(true);
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);
}
setPartIndex(partIndex + 1);
setExerciseIndex(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);
return;
}
@@ -194,7 +199,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender) {
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
setTextRender(true);
return;
}
@@ -221,31 +226,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
if (previousExercise.type === "multipleChoice") {
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="flex flex-col w-full gap-2">
{textRender ? (
{textRender && !textRenderDisabled ? (
<>
<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.
@@ -286,7 +266,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
</div>
</>
</div>
{textRender && (
{textRender && !textRenderDisabled && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
@@ -306,23 +286,24 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
);
const partLabel = () => {
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
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") {
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") {
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) => {
return exam.parts[partIndex].exercises.every((exercise) => {
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
switch(exercise.type) {
switch (exercise.type) {
case 'multipleChoice':
return userSolution?.solutions.length === exercise.questions.length;
case 'fillBlanks':
@@ -388,7 +369,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
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) } };
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
}
setQuestionModalKwargs(kwargs);
}
@@ -408,7 +389,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setChangedPrompt(false);
return (
<>
{textRender ?
{textRender && !textRenderDisabled ?
renderText() :
<>
{exam.parts[partIndex].context && renderText()}
@@ -425,6 +406,25 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
return (
<>
<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} />
{
!(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">
{exam.parts.map((_, index) =>
<Tab key={index} onClick={(e) => {
if (!seenParts.includes(index)) {
e.preventDefault();
} else {
setExerciseIndex(0);
setQuestionIndex(0);
/*
// If client wants to revert uncomment and remove the added if statement
if (!seenParts.has(index)) {
e.preventDefault();
} else {
*/
setExerciseIndex(0);
setQuestionIndex(0);
if (!seenParts.has(index)) {
setShowPartDivider(true);
setBgColor(levelBgColor);
setSeenParts(prev => new Set(prev).add(index));
}
}}
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",
"transition duration-300 ease-in-out hover:bg-white/70",
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>

View File

@@ -39,6 +39,7 @@ export interface LevelExam extends ExamBase {
export interface LevelPart {
context?: string;
intro?: string;
category?: string;
exercises: Exercise[];
}