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 && (