Merged in feature/level-file-upload (pull request #72)
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:
@@ -72,7 +72,6 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
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;
|
||||
@@ -96,6 +95,8 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
)
|
||||
return (
|
||||
variant === "mc" ? (
|
||||
<>
|
||||
{/*<span className="mr-2">{`(${id})`}</span>*/}
|
||||
<button
|
||||
className={styles}
|
||||
onClick={() => {
|
||||
@@ -114,6 +115,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
>
|
||||
{userSolution?.solution === undefined ? <span className="text-transparent select-none">placeholder</span> : <span> {userSolution.solution} </span>}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
className={styles}
|
||||
@@ -145,14 +147,14 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
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")}>
|
||||
@@ -164,11 +166,23 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
{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) &&
|
||||
@@ -178,7 +192,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
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]}
|
||||
|
||||
@@ -15,9 +15,12 @@ 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);
|
||||
@@ -86,7 +89,12 @@ 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-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">
|
||||
@@ -94,12 +102,13 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
{moduleLabels[module]} exam {label && `- ${label}`}
|
||||
</span>
|
||||
<span className="text-sm font-semibold self-end">
|
||||
Exercise {exerciseIndex}/{totalExercises}
|
||||
Question {exerciseIndex}/{totalExercises}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||
</div>
|
||||
</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;
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import clsx from "clsx";
|
||||
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
||||
import { toast } from "react-toastify";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
@@ -25,9 +26,9 @@ interface Props {
|
||||
}
|
||||
|
||||
function TextComponent({
|
||||
part, highlightPhrases, contextWord, setContextWordLine
|
||||
part, contextWord, setContextWordLine
|
||||
}: {
|
||||
part: LevelPart, highlightPhrases: string[], contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
part: LevelPart, contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
}) {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||
@@ -49,13 +50,14 @@ function TextComponent({
|
||||
offscreenElement.style.width = `${containerWidth}px`;
|
||||
offscreenElement.style.font = computedStyle.font;
|
||||
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
||||
offscreenElement.style.wordWrap = 'break-word';
|
||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||
|
||||
const textContent = textRef.current.textContent || '';
|
||||
textContent.split(/(\s+)/).forEach((word: string) => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = word;
|
||||
span.style.display = 'inline-block';
|
||||
span.style.height = `calc(1em + 16px)`;
|
||||
offscreenElement.appendChild(span);
|
||||
});
|
||||
|
||||
@@ -139,15 +141,10 @@ function TextComponent({
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
||||
<HighlightContent html={part.context!} highlightPhrases={highlightPhrases} firstOccurence={true} />
|
||||
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
|
||||
{part.context!.split('\n\n').map((line, index) => {
|
||||
return <p key={`line-${index}`}><span className="mr-6">{index + 1}</span>{line}</p>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +173,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
const [highlightPhrases, setContextHighlight] = useState<string[]>([]);
|
||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||
|
||||
@@ -304,7 +300,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
if (match) {
|
||||
const word = match[1];
|
||||
const originalLineNumber = match[2];
|
||||
setContextHighlight([word]);
|
||||
|
||||
if (word !== contextWord) {
|
||||
setContextWord(word);
|
||||
@@ -317,7 +312,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
|
||||
} else {
|
||||
setContextHighlight([]);
|
||||
setContextWord(undefined);
|
||||
}
|
||||
}
|
||||
@@ -329,8 +323,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
}
|
||||
|
||||
if (storeQuestionIndex > 0) {
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -377,8 +371,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
|
||||
}
|
||||
|
||||
if (storeQuestionIndex > 0) {
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
|
||||
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -397,7 +391,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
exercisesDone +
|
||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
||||
storeQuestionIndex +
|
||||
multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
|
||||
multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount}, 0)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -412,7 +406,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
</div>
|
||||
<TextComponent
|
||||
part={exam.parts[partIndex]}
|
||||
highlightPhrases={highlightPhrases}
|
||||
contextWord={contextWord}
|
||||
setContextWordLine={setContextWordLine}
|
||||
/>
|
||||
@@ -420,11 +413,19 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
</div>
|
||||
);
|
||||
|
||||
const partLabel = () => {
|
||||
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}`
|
||||
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 (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||
<ModuleTitle
|
||||
partLabel={partLabel()}
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
module="level"
|
||||
|
||||
@@ -26,6 +26,7 @@ export const countExercises = (exercises: Exercise[]) => {
|
||||
const lengthMap = exercises.map((e) => {
|
||||
if (e.type === "multipleChoice") return e.questions.length;
|
||||
if (e.type === "interactiveSpeaking") return e.prompts.length;
|
||||
if (e.type === "fillBlanks") return e.words.length;
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user