Fixed question numbers for fillBlanks exercises, reverted multipleChoice underline prompt, added part label to module title, and changed some styles
This commit is contained in:
@@ -44,44 +44,43 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
const calculateScore = () => {
|
||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||
const correct = userSolutions.filter((x) => {
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||
if (!solution) return false;
|
||||
|
||||
const option = words.find((w) => {
|
||||
if (typeof w === "string") {
|
||||
return w.toLowerCase() === x.solution.toLowerCase();
|
||||
} else if ('letter' in w) {
|
||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
||||
} else {
|
||||
return w.id === x.id;
|
||||
}
|
||||
});
|
||||
if (!option) return false;
|
||||
|
||||
if (typeof option === "string") {
|
||||
return solution.toLowerCase() === option.toLowerCase();
|
||||
} else if ('letter' in option) {
|
||||
return solution.toLowerCase() === option.word.toLowerCase();
|
||||
} else if ('options' in option) {
|
||||
/*
|
||||
if (shuffleMaps.length !== 0) {
|
||||
const shuffleMap = shuffleMaps.find((map) => map.id == x.id)
|
||||
if (!shuffleMap) {
|
||||
return false;
|
||||
}
|
||||
const original = shuffleMap[x.solution as keyof typeof shuffleMap];
|
||||
return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase();
|
||||
}*/
|
||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||
if (!solution) return false;
|
||||
|
||||
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
||||
}
|
||||
return false;
|
||||
const option = words.find((w) => {
|
||||
if (typeof w === "string") {
|
||||
return w.toLowerCase() === x.solution.toLowerCase();
|
||||
} else if ('letter' in w) {
|
||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
||||
} else {
|
||||
return w.id === x.id;
|
||||
}
|
||||
});
|
||||
if (!option) return false;
|
||||
|
||||
if (typeof option === "string") {
|
||||
return solution.toLowerCase() === option.toLowerCase();
|
||||
} else if ('letter' in option) {
|
||||
return solution.toLowerCase() === option.word.toLowerCase();
|
||||
} else if ('options' in option) {
|
||||
/*
|
||||
if (shuffleMaps.length !== 0) {
|
||||
const shuffleMap = shuffleMaps.find((map) => map.id == x.id)
|
||||
if (!shuffleMap) {
|
||||
return false;
|
||||
}
|
||||
const original = shuffleMap[x.solution as keyof typeof shuffleMap];
|
||||
return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase();
|
||||
}*/
|
||||
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
|
||||
}
|
||||
return false;
|
||||
}).length;
|
||||
|
||||
|
||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||
|
||||
|
||||
return { total, correct, missing };
|
||||
};
|
||||
};
|
||||
|
||||
const renderLines = (line: string) => {
|
||||
return (
|
||||
@@ -96,24 +95,27 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
)
|
||||
return (
|
||||
variant === "mc" ? (
|
||||
<button
|
||||
className={styles}
|
||||
onClick={() => {
|
||||
setCurrentMCSelection(
|
||||
{
|
||||
id: id,
|
||||
selection: words.find((x) => {
|
||||
if (typeof x !== "string" && 'id' in x) {
|
||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||
}
|
||||
return false;
|
||||
}) as FillBlanksMCOption
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
||||
</button>
|
||||
<>
|
||||
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
||||
<button
|
||||
className={styles}
|
||||
onClick={() => {
|
||||
setCurrentMCSelection(
|
||||
{
|
||||
id: id,
|
||||
selection: words.find((x) => {
|
||||
if (typeof x !== "string" && 'id' in x) {
|
||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||
}
|
||||
return false;
|
||||
}) as FillBlanksMCOption
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
className={styles}
|
||||
@@ -131,28 +133,28 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
}
|
||||
|
||||
/*const getShuffles = () => {
|
||||
let shuffle = {};
|
||||
if (shuffleMaps.length !== 0) {
|
||||
shuffle = {
|
||||
shuffleMaps: shuffleMaps.filter((map) =>
|
||||
answers.some(answer => answer.id === map.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
return shuffle;
|
||||
}*/
|
||||
let shuffle = {};
|
||||
if (shuffleMaps.length !== 0) {
|
||||
shuffle = {
|
||||
shuffleMaps: shuffleMaps.filter((map) =>
|
||||
answers.some(answer => answer.id === map.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
return shuffle;
|
||||
}*/
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||
<span className="text-sm w-full leading-6">
|
||||
{false && <span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
</span>}
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
||||
@@ -162,23 +164,35 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
))}
|
||||
</span>
|
||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
||||
<>
|
||||
<>
|
||||
{currentMCSelection && (
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
<span className="font-medium text-mti-purple-dark">Options</span>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
<div className="bg-mti-gray-smoke rounded-xl flex flex-col gap-4 px-16 py-8">
|
||||
<span className="font-medium text-lg text-mti-purple-dark mb-4 px-2">{`${currentMCSelection.id} - Select the appropriate word.`}</span>
|
||||
<div className="flex gap-4 flex-wrap justify-between">
|
||||
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => {
|
||||
return <button
|
||||
return <div
|
||||
key={v4()}
|
||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||
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",
|
||||
)}>
|
||||
<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 ) &&
|
||||
!!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>;
|
||||
</button>;*/
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,24 +20,21 @@ function Question({
|
||||
showSolution?: boolean,
|
||||
}) {
|
||||
|
||||
/*
|
||||
const renderPrompt = (prompt: string) => {
|
||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
|
||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||
return word.length > 0 ? <u>{word}</u> : null;
|
||||
});
|
||||
};
|
||||
*/
|
||||
|
||||
return (
|
||||
// {renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-8">
|
||||
{isNaN(Number(id)) ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||
<span className={clsx(true ? "text-lg" : "text-base")}>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
|
||||
) : (
|
||||
<span className="">
|
||||
<span className={clsx(true ? "text-lg" : "text-base")}>
|
||||
<>
|
||||
{id} - <span dangerouslySetInnerHTML={{ __html: prompt }} />
|
||||
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
|
||||
</>
|
||||
</span>
|
||||
)}
|
||||
@@ -61,7 +58,7 @@ function Question({
|
||||
key={option.id.toString()}
|
||||
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
|
||||
className={clsx(
|
||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm",
|
||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
||||
userSolution === option.id.toString() && "border-mti-purple-light",
|
||||
)}>
|
||||
<span className="font-semibold">{option.id.toString()}.</span>
|
||||
@@ -156,7 +153,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 mt-4 h-fit w-full mb-20 bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||
<span className="text-xl font-semibold">{prompt}</span>
|
||||
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
|
||||
{questionIndex < questions.length && (
|
||||
<Question
|
||||
{...questions[questionIndex]}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Module} from "@/interfaces";
|
||||
import { Module } from "@/interfaces";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import {moduleLabels} from "@/utils/moduleUtils";
|
||||
import { moduleLabels } from "@/utils/moduleUtils";
|
||||
import clsx from "clsx";
|
||||
import {motion} from "framer-motion";
|
||||
import {ReactNode, useEffect, useState} from "react";
|
||||
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
||||
import { motion } from "framer-motion";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
||||
import ProgressBar from "../Low/ProgressBar";
|
||||
import TimerEndedModal from "../TimerEndedModal";
|
||||
|
||||
@@ -15,15 +15,18 @@ interface Props {
|
||||
exerciseIndex: number;
|
||||
totalExercises: number;
|
||||
disableTimer?: boolean;
|
||||
partLabel?: string;
|
||||
}
|
||||
|
||||
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false}: Props) {
|
||||
export default function ModuleTitle({
|
||||
minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel
|
||||
}: Props) {
|
||||
const [timer, setTimer] = useState(minTimer * 60);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [warningMode, setWarningMode] = useState(false);
|
||||
|
||||
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
||||
const {timeSpent} = useExamStore((state) => state);
|
||||
const { timeSpent } = useExamStore((state) => state);
|
||||
|
||||
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
|
||||
|
||||
@@ -45,7 +48,7 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
if (timer < 300 && !warningMode) setWarningMode(true);
|
||||
}, [timer, warningMode]);
|
||||
|
||||
const moduleIcon: {[key in Module]: ReactNode} = {
|
||||
const moduleIcon: { [key in Module]: ReactNode } = {
|
||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
|
||||
@@ -67,9 +70,9 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
"absolute top-4 right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
|
||||
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
|
||||
)}
|
||||
initial={{scale: warningMode && !disableTimer ? 0.8 : 1}}
|
||||
animate={{scale: warningMode && !disableTimer ? 1.1 : 1}}
|
||||
transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}>
|
||||
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
|
||||
animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
|
||||
transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
|
||||
<BsStopwatch className="w-6 h-6" />
|
||||
<span className="text-base font-semibold w-12">
|
||||
{timer > 0 && (
|
||||
@@ -86,18 +89,24 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
{timer <= 0 && <>00:00</>}
|
||||
</span>
|
||||
</motion.div>
|
||||
<div className="flex gap-6 w-full h-fit items-center mt-5">
|
||||
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<div className="w-full flex justify-between">
|
||||
<span className="text-base font-semibold">
|
||||
{moduleLabels[module]} exam {label && `- ${label}`}
|
||||
</span>
|
||||
<span className="text-sm font-semibold self-end">
|
||||
Exercise {exerciseIndex}/{totalExercises}
|
||||
</span>
|
||||
<div className="w-full">
|
||||
{partLabel && <div className="text-3xl space-y-4">{partLabel.split('\n\n').map((line, index) => {
|
||||
if(index == 0) return <p className="font-bold">{line}</p>
|
||||
else return <p className="text-2xl font-semibold">{line}</p>
|
||||
})}</div>}
|
||||
<div className={clsx("flex gap-6 w-full h-fit items-center", partLabel ? "mt-10" : "mt-5" )}>
|
||||
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<div className="w-full flex justify-between">
|
||||
<span className="text-base font-semibold">
|
||||
{moduleLabels[module]} exam {label && `- ${label}`}
|
||||
</span>
|
||||
<span className="text-sm font-semibold self-end">
|
||||
Question {exerciseIndex}/{totalExercises}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||
</div>
|
||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -49,7 +49,7 @@ function Question({
|
||||
const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution;
|
||||
|
||||
const renderPrompt = (prompt: string) => {
|
||||
return reactStringReplace(prompt, /((<u>)[\w\s']+(<\/u>))/g, (match) => {
|
||||
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
|
||||
const word = match.replaceAll("<u>", "").replaceAll("</u>", "");
|
||||
return word.length > 0 ? <u>{word}</u> : null;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user