From 845bccbe2a236cb70321cc694d177ec9efd74141 Mon Sep 17 00:00:00 2001
From: Carlos Mesquita
Date: Tue, 27 Aug 2024 17:08:33 +0100
Subject: [PATCH] ENCOA-107, ENCOA-115 Fixed completion percentage, brought
back the line numbers, 'Level Exam' was replaced by 'Placement Test',
Next/Back instructions with quotes, 'Submit' on last level question
---
next.config.js | 2 +-
src/components/Exercises/FillBlanks/index.tsx | 13 ++-
src/components/Exercises/MultipleChoice.tsx | 10 +-
src/components/Medium/ModuleTitle.tsx | 31 +++---
src/components/Solutions/MultipleChoice.tsx | 3 +-
src/exams/Level/TextComponent.tsx | 90 ++++++++---------
src/exams/Level/index.tsx | 99 ++++++++++---------
7 files changed, 128 insertions(+), 120 deletions(-)
diff --git a/next.config.js b/next.config.js
index cdba31d8..2203b000 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const websiteUrl = process.env.NODE_ENV === 'production' ? "https://encoach.com" : "http://localhost:3000";
const nextConfig = {
- reactStrictMode: true,
+ reactStrictMode: false,
output: "standalone",
async headers() {
return [
diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx
index f8419b7d..96b3ae7e 100644
--- a/src/components/Exercises/FillBlanks/index.tsx
+++ b/src/components/Exercises/FillBlanks/index.tsx
@@ -55,7 +55,6 @@ const FillBlanks: React.FC = ({
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
if (!solution) return false;
const option = correctWords!.find((w: any) => {
- console.log(w);
if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) {
@@ -85,7 +84,8 @@ const FillBlanks: React.FC = ({
const id = match.replaceAll(/[\{\}]/g, "");
const userSolution = answers.find((x) => x.id === id);
const styles = clsx(
- "rounded-full hover:text-white focus:ring-0 focus:outline-none focus:!text-white focus:bg-mti-purple transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
+ "rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center",
+ currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0",
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
)
@@ -122,7 +122,7 @@ const FillBlanks: React.FC = ({
})}
);
- }, [variant, words, setCurrentMCSelection, answers]);
+ }, [variant, words, setCurrentMCSelection, answers, currentMCSelection]);
const memoizedLines = useMemo(() => {
return text.split("\\n").map((line, index) => (
@@ -131,7 +131,7 @@ const FillBlanks: React.FC = ({
));
- }, [text, variant, renderLines]);
+ }, [text, variant, renderLines, currentMCSelection]);
const onSelection = (questionID: string, value: string) => {
@@ -139,10 +139,9 @@ const FillBlanks: React.FC = ({
}
useEffect(() => {
- //if (variant === "mc") {
- console.log(answers);
+ if (variant === "mc") {
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
- //}
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers])
diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx
index e158643b..e03487ac 100644
--- a/src/components/Exercises/MultipleChoice.tsx
+++ b/src/components/Exercises/MultipleChoice.tsx
@@ -76,6 +76,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
const {
questionIndex,
+ exerciseIndex,
exam,
shuffles,
hasExamEnded,
@@ -143,7 +144,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
}
return isSolutionCorrect || false;
}).length;
- const missing = total - correct;
+ const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length;
return { total, correct, missing };
};
@@ -193,7 +194,12 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
>
diff --git a/src/components/Medium/ModuleTitle.tsx b/src/components/Medium/ModuleTitle.tsx
index 04dc84e9..7eb569d8 100644
--- a/src/components/Medium/ModuleTitle.tsx
+++ b/src/components/Medium/ModuleTitle.tsx
@@ -12,6 +12,7 @@ import Button from "../Low/Button";
import { Dialog, Transition } from "@headlessui/react";
import useExamStore from "@/stores/examStore";
import Modal from "../Modal";
+import React from "react";
interface Props {
minTimer: number;
@@ -85,7 +86,7 @@ export default function ModuleTitle({
return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700";
}
- return userQuestionSolution === newSolution ?
+ return userQuestionSolution === newSolution ?
"!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" :
"!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark";
}
@@ -105,10 +106,10 @@ export default function ModuleTitle({
key={index}
className={clsx(
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
- (showSolutions ?
- getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
- (isAnswered ?
- "bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark":
+ (showSolutions ?
+ getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
+ (isAnswered ?
+ "bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
)
)
@@ -126,25 +127,26 @@ export default function ModuleTitle({
>
);
};
-
return (
<>
{showTimer && }
{partLabel && (
- {partLabel.split("\n\n").map((line, index) => {
- if (index == 0)
+ {partLabel.split("\n\n").map((partInstructions, index) => {
+ if (index === 0)
return (
- {line}
+ {partInstructions}
);
else
return (
-
- {line}
-
+
+ {partInstructions.split("\\n").map((line, lineIndex) => (
+ {line}
+ ))}
+
);
})}
@@ -154,7 +156,10 @@ export default function ModuleTitle({
- {moduleLabels[module]} exam {label && `- ${label}`}
+ {module === "level"
+ ? "Placement Test"
+ : `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`
+ }
Question {exerciseIndex}/{totalExercises}
diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx
index 6f29e9ab..5a589336 100644
--- a/src/components/Solutions/MultipleChoice.tsx
+++ b/src/components/Solutions/MultipleChoice.tsx
@@ -2,7 +2,6 @@
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import clsx from "clsx";
-import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace";
import { CommonProps } from ".";
import Button from "../Low/Button";
@@ -101,7 +100,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
}
},
).length;
- const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
+ const missing = total - userSolutions.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length;
return { total, correct, missing };
};
diff --git a/src/exams/Level/TextComponent.tsx b/src/exams/Level/TextComponent.tsx
index fc3fa216..4b3c6f3a 100644
--- a/src/exams/Level/TextComponent.tsx
+++ b/src/exams/Level/TextComponent.tsx
@@ -1,5 +1,5 @@
import { LevelPart } from "@/interfaces/exam";
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
interface Props {
part: LevelPart,
@@ -9,78 +9,65 @@ interface Props {
const TextComponent: React.FC = ({ part, contextWord, setContextWordLine }) => {
const textRef = useRef(null);
+ const [lineNumbers, setLineNumbers] = useState([]);
+ const [lineHeight, setLineHeight] = useState(0);
const calculateLineNumbers = () => {
if (textRef.current) {
const computedStyle = window.getComputedStyle(textRef.current);
+ const lineHeightValue = parseFloat(computedStyle.lineHeight);
const containerWidth = textRef.current.clientWidth;
+ setLineHeight(lineHeightValue);
const offscreenElement = document.createElement('div');
offscreenElement.style.position = 'absolute';
offscreenElement.style.top = '-9999px';
offscreenElement.style.left = '-9999px';
+ offscreenElement.style.whiteSpace = 'pre-wrap';
offscreenElement.style.width = `${containerWidth}px`;
offscreenElement.style.font = computedStyle.font;
offscreenElement.style.lineHeight = computedStyle.lineHeight;
- offscreenElement.style.whiteSpace = 'pre-wrap';
offscreenElement.style.wordWrap = 'break-word';
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
- const paragraphs = part.context!.split('\n\n');
- let currentLine = 1;
- let contextWordLine: number | null = null;
- const paragraphLineStarts: number[] = [];
-
- paragraphs.forEach((paragraph, pIndex) => {
- const p = document.createElement('p');
- p.style.margin = '0';
- p.style.padding = '0';
-
- paragraph.split(/(\s+)/).forEach((word: string) => {
- const span = document.createElement('span');
- span.textContent = word;
- p.appendChild(span);
- });
-
- offscreenElement.appendChild(p);
-
- if (pIndex < paragraphs.length - 1) {
- const gap = document.createElement('div');
- gap.style.height = '16px'; // gap-4
- offscreenElement.appendChild(gap);
- }
+ const textContent = textRef.current.textContent || '';
+ textContent.split(/(\s+)/).forEach((word: string) => {
+ const span = document.createElement('span');
+ span.textContent = word;
+ offscreenElement.appendChild(span);
});
document.body.appendChild(offscreenElement);
+ const lines: string[][] = [[]];
+ let currentLine = 1;
let currentLineTop: number | undefined;
- const elements = offscreenElement.querySelectorAll('p, div');
+ let contextWordLine: number | null = null;
- elements.forEach((element) => {
- if (element.tagName === 'P') {
- const spans = element.querySelectorAll('span');
- paragraphLineStarts.push(currentLine);
+ const firstChild = offscreenElement.firstChild as HTMLElement;
+ if (firstChild) {
+ currentLineTop = firstChild.getBoundingClientRect().top;
+ }
- spans.forEach(span => {
- const rect = span.getBoundingClientRect();
- const top = rect.top;
+ const spans = offscreenElement.querySelectorAll('span');
- if (currentLineTop === undefined || top > currentLineTop) {
- if (currentLineTop !== undefined) {
- currentLine++;
- }
- currentLineTop = top;
- }
+ spans.forEach(span => {
+ const rect = span.getBoundingClientRect();
+ const top = rect.top;
- if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
- contextWordLine = currentLine;
- }
- });
- } else if (element.tagName === 'DIV') { // Gap
+ if (currentLineTop !== undefined && top > currentLineTop) {
currentLine++;
- currentLineTop = undefined;
+ currentLineTop = top;
+ lines.push([]);
+ }
+
+ lines[lines.length - 1].push(span.textContent?.trim() || '');
+
+ if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
+ contextWordLine = currentLine;
}
});
+ setLineNumbers(lines.map((_, index) => index + 1));
if (contextWordLine) {
setContextWordLine(contextWordLine);
}
@@ -131,10 +118,15 @@ const TextComponent: React.FC = ({ part, contextWord, setContextWordLine
return (
-
- {part.context!.split('\n\n').map((line, index) => {
- return
{index + 1}{line}
- })}
+
+ {lineNumbers.map(num => (
+
+ {num}
+
+ ))}
+
+
);
diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx
index b88aa109..587c07ff 100644
--- a/src/exams/Level/index.tsx
+++ b/src/exams/Level/index.tsx
@@ -8,7 +8,7 @@ import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, Multip
import useExamStore from "@/stores/examStore";
import { countExercises } from "@/utils/moduleUtils";
import clsx from "clsx";
-import { use, useEffect, useState } from "react";
+import { use, useEffect, useMemo, useState } from "react";
import TextComponent from "./TextComponent";
import PartDivider from "./PartDivider";
import Timer from "@/components/Medium/Timer";
@@ -56,6 +56,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
const [continueAnyways, setContinueAnyways] = useState(false);
const [textRender, setTextRender] = useState(false);
+ const [changedPrompt, setChangedPrompt] = useState(false);
const [seenParts, setSeenParts] = useState
(showSolutions ? exam.parts.map((_, index) => index) : [0]);
@@ -66,8 +67,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
});
-
-
const [currentExercise, setCurrentExercise] = useState(exam.parts[0].exercises[0]);
const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions);
@@ -120,37 +119,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}, [partIndex, exerciseIndex, questionIndex]);
- useEffect(() => {
- const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
- if (
- exerciseIndex !== -1 && currentExercise &&
- currentExercise.type === "multipleChoice" &&
- currentExercise.questions[questionIndex] &&
- currentExercise.questions[questionIndex].prompt &&
- exam.parts[partIndex].context
- ) {
- const match = currentExercise.questions[questionIndex].prompt.match(regex);
- if (match) {
- const word = match[1];
- const originalLineNumber = match[2];
-
- if (word !== contextWord) {
- setContextWord(word);
- }
-
- const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace(
- `in line ${originalLineNumber}`,
- `in line ${contextWordLine || originalLineNumber}`
- );
-
- currentExercise.questions[questionIndex].prompt = updatedPrompt;
- } else {
- setContextWord(undefined);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentExercise, questionIndex]);
-
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
@@ -341,6 +309,38 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
});
}
+ useEffect(() => {
+ const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
+ if (
+ exerciseIndex !== -1 && currentExercise &&
+ currentExercise.type === "multipleChoice" &&
+ currentExercise.questions[questionIndex] &&
+ currentExercise.questions[questionIndex].prompt &&
+ exam.parts[partIndex].context
+ ) {
+ const match = currentExercise.questions[questionIndex].prompt.match(regex);
+ if (match) {
+ const word = match[1];
+ const originalLineNumber = match[2];
+
+ if (word !== contextWord) {
+ setContextWord(word);
+ }
+
+ const updatedPrompt = currentExercise.questions[questionIndex].prompt.replace(
+ `in line ${originalLineNumber}`,
+ `in line ${contextWordLine || originalLineNumber}`
+ );
+
+ currentExercise.questions[questionIndex].prompt = updatedPrompt;
+ setChangedPrompt(true);
+ } else {
+ setContextWord(undefined);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentExercise, questionIndex, contextWordLine]);
+
useEffect(() => {
if (continueAnyways) {
setContinueAnyways(false);
@@ -375,6 +375,24 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}
+ const memoizedRender = useMemo(() => {
+ setChangedPrompt(false);
+ return (
+ <>
+ {textRender ?
+ renderText() :
+ <>
+ {exam.parts[partIndex].context && renderText()}
+ {(showSolutions || editing) ?
+ renderSolution(currentExercise, nextExercise, previousExercise)
+ :
+ renderExercise(currentExercise, exam.id, nextExercise, previousExercise)
+ }
+ >
+ }
+ >)
+ }, [textRender, currentExercise, changedPrompt]);
+
return (
<>
@@ -429,18 +447,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
"mb-20 w-full",
!!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4",
)}>
-
- {textRender ?
- renderText() :
- <>
- {exam.parts[partIndex].context && renderText()}
- {(showSolutions || editing) ?
- renderSolution(currentExercise, nextExercise, previousExercise)
- :
- renderExercise(currentExercise, exam.id, nextExercise, previousExercise)
- }
- >
- }
+ {memoizedRender}
{/*exerciseIndex === -1 && partIndex > 0 && (