ENCOA-182, ENCOA-185, ENCOA-177, ENCOA-168, ENCOA-186, ENCOA-176, ENCOA-189, ENCOA-167
This commit is contained in:
@@ -3,16 +3,32 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
part: LevelPart,
|
||||
contextWord: string | undefined,
|
||||
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
||||
contextWords: { match: string, originalLine: string }[] | undefined,
|
||||
setContextWordLines: React.Dispatch<React.SetStateAction<number[] | undefined>>
|
||||
setTotalLines: React.Dispatch<React.SetStateAction<number>>
|
||||
}
|
||||
|
||||
const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine }) => {
|
||||
const TextComponent: React.FC<Props> = ({ part, contextWords, setContextWordLines, setTotalLines }) => {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||
const [lineHeight, setLineHeight] = useState<number>(0);
|
||||
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
|
||||
|
||||
const getBoldTag = (context: string) => {
|
||||
const regex = /<b\s+class=['"]([^'"]+)['"]>(\d+)<\/b>/;
|
||||
const match = context.match(regex);
|
||||
if (match) {
|
||||
return {
|
||||
className: match[1],
|
||||
number: match[2],
|
||||
fullTag: match[0]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const bTag = getBoldTag(part.context!);
|
||||
|
||||
const calculateLineNumbers = () => {
|
||||
if (textRef.current) {
|
||||
const computedStyle = window.getComputedStyle(textRef.current);
|
||||
@@ -39,7 +55,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
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;
|
||||
}
|
||||
@@ -47,9 +62,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
|
||||
betweenParagraphs[lineIndex][0] = word;
|
||||
}
|
||||
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = word;
|
||||
if (wordIndex === 0 && bTag) {
|
||||
const b = document.createElement('b');
|
||||
b.classList.add(bTag.className);
|
||||
b.textContent = `${lineIndex + 1}`;
|
||||
span.appendChild(b);
|
||||
span.appendChild(document.createTextNode(word.substring(1)));
|
||||
}else {
|
||||
span.appendChild(document.createTextNode(word));
|
||||
}
|
||||
return span;
|
||||
})
|
||||
}
|
||||
@@ -67,8 +90,11 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
const processedLines: string[][] = [[]];
|
||||
let currentLine = 1;
|
||||
let currentLineTop: number | undefined;
|
||||
let contextWordLine: number | null = null;
|
||||
|
||||
let contextWordLines: number[] = [];
|
||||
if (contextWords) {
|
||||
contextWordLines = Array(contextWords.length).fill(-1);
|
||||
}
|
||||
const firstChild = offscreenElement.firstChild as HTMLElement;
|
||||
if (firstChild) {
|
||||
currentLineTop = firstChild.getBoundingClientRect().top;
|
||||
@@ -78,7 +104,7 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
|
||||
let betweenIndex = 0;
|
||||
const addBreaksTo: number[] = [];
|
||||
spans.forEach((span, index)=> {
|
||||
spans.forEach((span, index) => {
|
||||
const rect = span.getBoundingClientRect();
|
||||
const top = rect.top;
|
||||
|
||||
@@ -98,17 +124,22 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
}
|
||||
|
||||
processedLines[processedLines.length - 1].push(span.textContent?.trim() || '');
|
||||
|
||||
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
||||
contextWordLine = currentLine;
|
||||
if (contextWords && contextWordLines.some(element => element === -1)) {
|
||||
contextWords.forEach((w, index) => {
|
||||
if (span.textContent?.includes(w.match) && contextWordLines[index] == -1) {
|
||||
contextWordLines[index] = currentLine;
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
setAddBreaksTo(addBreaksTo);
|
||||
|
||||
setLineNumbers(processedLines.map((_, index) => index + 1));
|
||||
if (contextWordLine) {
|
||||
setContextWordLine(contextWordLine);
|
||||
setTotalLines(currentLine);
|
||||
|
||||
if (contextWordLines.length > 0) {
|
||||
setContextWordLines(contextWordLines);
|
||||
}
|
||||
|
||||
document.body.removeChild(offscreenElement);
|
||||
@@ -135,18 +166,18 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [part.context, contextWord]);
|
||||
}, [part.context, contextWords]);
|
||||
|
||||
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 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>
|
||||
|
||||
@@ -85,8 +85,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||
|
||||
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
|
||||
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
|
||||
const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined);
|
||||
const [contextWordLines, setContextWordLines] = useState<number[] | undefined>(undefined);
|
||||
const [totalLines, setTotalLines] = useState<number>(0);
|
||||
|
||||
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
|
||||
|
||||
@@ -206,6 +207,14 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
if (questionIndex == 0) {
|
||||
setPartIndex(partIndex - 1);
|
||||
if (!seenParts.has(partIndex - 1)) {
|
||||
setBgColor(levelBgColor);
|
||||
setShowPartDivider(true);
|
||||
setQuestionIndex(0);
|
||||
setSeenParts(prev => new Set(prev).add(partIndex - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
|
||||
const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex];
|
||||
setExerciseIndex(lastExerciseIndex);
|
||||
@@ -260,8 +269,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
{exam.parts[partIndex].context &&
|
||||
<TextComponent
|
||||
part={exam.parts[partIndex]}
|
||||
contextWord={contextWord}
|
||||
setContextWordLine={setContextWordLine}
|
||||
contextWords={contextWords}
|
||||
setContextWordLines={setContextWordLines}
|
||||
setTotalLines={setTotalLines}
|
||||
/>}
|
||||
</div>
|
||||
</>
|
||||
@@ -321,35 +331,59 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
|
||||
useEffect(() => {
|
||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||
|
||||
const findMatch = (index: number) => {
|
||||
if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) {
|
||||
const match = currentExercise!.questions[index].prompt.match(regex);
|
||||
if (match) {
|
||||
return { match: match[1], originalLine: match[2] }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if the client for some whatever random reason decides
|
||||
// to add more questions update this
|
||||
const numberOfQuestions = 2;
|
||||
|
||||
if (exam.parts[partIndex].context) {
|
||||
const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => {
|
||||
const result = findMatch(questionIndex + i);
|
||||
if (!!result) {
|
||||
acc.push(result);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (hits.length > 0) {
|
||||
setContextWords(hits)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentExercise, questionIndex, totalLines]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
exerciseIndex !== -1 && currentExercise &&
|
||||
currentExercise.type === "multipleChoice" &&
|
||||
currentExercise.questions[questionIndex] &&
|
||||
currentExercise.questions[questionIndex].prompt &&
|
||||
exam.parts[partIndex].context
|
||||
exam.parts[partIndex].context && contextWordLines
|
||||
) {
|
||||
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;
|
||||
if (contextWordLines.length > 0) {
|
||||
contextWordLines.forEach((n, i) => {
|
||||
if (contextWords && contextWords[i] && n !== -1) {
|
||||
const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace(
|
||||
`in line ${contextWords[i].originalLine}`,
|
||||
`in line ${n}`
|
||||
);
|
||||
currentExercise!.questions[questionIndex + i].prompt = updatedPrompt;
|
||||
}
|
||||
})
|
||||
setChangedPrompt(true);
|
||||
} else {
|
||||
setContextWord(undefined);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentExercise, questionIndex, contextWordLine]);
|
||||
}, [contextWordLines]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (continueAnyways) {
|
||||
@@ -419,7 +453,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
<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">
|
||||
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true) }} className="max-w-[200px] self-end w-full !text-xl">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
@@ -469,6 +503,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
</div>
|
||||
)}
|
||||
<ModuleTitle
|
||||
examLabel={exam.label}
|
||||
partLabel={partLabel()}
|
||||
minTimer={exam.minTimer}
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
|
||||
Reference in New Issue
Block a user