If someone else wants to join in on the fun be my guest

This commit is contained in:
Carlos Mesquita
2024-08-19 01:24:55 +01:00
parent edc9d4de2a
commit bcb1a0f914
10 changed files with 319 additions and 122 deletions

View File

@@ -5,14 +5,15 @@ import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import { renderSolution } from "@/components/Solutions";
import { infoButtonStyle } from "@/constants/buttonStyles";
import { LevelExam, LevelPart, UserSolution, WritingExam } from "@/interfaces/exam";
import { Module } from "@/interfaces";
import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import { defaultUserSolutions } from "@/utils/exams";
import { countExercises } from "@/utils/moduleUtils";
import { mdiArrowRight } from "@mdi/js";
import Icon from "@mdi/react";
import clsx from "clsx";
import { Fragment, use, useEffect, useRef, useState } from "react";
import { Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState } from "react";
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
import { toast } from "react-toastify";
@@ -39,7 +40,7 @@ function TextComponent({
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';
@@ -50,51 +51,51 @@ function TextComponent({
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;
offscreenElement.appendChild(span);
});
document.body.appendChild(offscreenElement);
const lines: string[][] = [[]];
let currentLine = 1;
let currentLineTop: number | undefined;
let contextWordLine: number | null = null;
const firstChild = offscreenElement.firstChild as HTMLElement;
if (firstChild) {
currentLineTop = firstChild.getBoundingClientRect().top;
}
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
spans.forEach(span => {
const rect = span.getBoundingClientRect();
const top = rect.top;
if (currentLineTop !== undefined && top > currentLineTop) {
currentLine++;
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);
}
document.body.removeChild(offscreenElement);
}
};
@@ -154,6 +155,12 @@ function TextComponent({
}
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every(
word => word && typeof word === 'object' && 'id' in word && 'options' in word
);
}
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
@@ -164,6 +171,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const { partIndex, setPartIndex } = useExamStore((state) => state);
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
const [currentExercise, setCurrentExercise] = useState<Exercise>();
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
@@ -171,6 +180,12 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
useEffect(() => {
if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) {
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
}
}, [showSolutions])
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
@@ -186,6 +201,128 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
onFinish(userSolutions);
};
const getExercise = () => {
if (exerciseIndex === -1) {
return undefined;
}
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
if (!exercise) return undefined;
exercise = {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
if (exam.shuffle && exercise.type === "multipleChoice") {
if (shuffleMaps.length == 0 && !showSolutions) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.questions = exercise.questions.map(question => {
const options = [...question.options];
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
const newOptions = options.map((option, index) => ({
id: option.id,
text: shuffledOptions[index].text
}));
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
if (shuffledPosition) {
acc[shuffledPosition] = originalOption.id;
}
return acc;
}, {});
newShuffleMaps.push({ id: question.id, map: optionMapping });
return { ...question, options: newOptions };
});
setShuffleMaps(newShuffleMaps);
} else {
exercise.questions = exercise.questions.map(question => {
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
if (questionShuffleMap) {
const newOptions = question.options.map(option => ({
id: option.id,
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
}));
return { ...question, options: newOptions };
}
return question;
});
}
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words)) {
if (shuffleMaps.length === 0 && !showSolutions) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.words = exercise.words.map(word => {
if ('options' in word) {
const options = { ...word.options };
const shuffledKeys = Object.keys(options).sort(() => Math.random() - 0.5);
const newOptions = shuffledKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = options[shuffledKeys[index] as keyof typeof options];
return acc;
}, {} as { [key in keyof typeof options]: string });
const optionMapping = shuffledKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = Object.keys(options)[index] as keyof typeof options;
return acc;
}, {} as { [key in keyof typeof options]: string });
newShuffleMaps.push({ id: word.id, map: optionMapping });
return { ...word, options: newOptions };
}
return word;
});
setShuffleMaps(newShuffleMaps);
}
}
return exercise;
};
useEffect(() => {
const newExercise = getExercise();
setCurrentExercise(newExercise);
}, [partIndex, exerciseIndex]);
//useShuffledMultipleChoiceOptions(currentExercise, exam.shuffle, storeQuestionIndex, shuffleMaps, setShuffleMaps, setCurrentExercise);
//useShuffledFillBlanksOptions(currentExercise, exam.shuffle, storeQuestionIndex, shuffleMaps, setShuffleMaps, setCurrentExercise);
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (currentExercise && currentExercise.type === "multipleChoice") {
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
setContextHighlight([word]);
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`
);
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else {
setContextHighlight([]);
setContextWord(undefined);
}
}
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex, shuffleMaps]);
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
@@ -193,8 +330,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}
if (storeQuestionIndex > 0) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
@@ -225,7 +361,11 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setHasExamEnded(false);
if (solution) {
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
let stat = { ...solution, module: "level" as Module, exam: exam.id }
if (exam.shuffle) {
stat.shuffleMaps = shuffleMaps
}
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]);
} else {
onFinish(userSolutions);
}
@@ -238,25 +378,13 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
}
if (storeQuestionIndex > 0) {
const exercise = getExercise();
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise!.id), { id: exercise!.id, amount: storeQuestionIndex }]);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: storeQuestionIndex }]);
}
setStoreQuestionIndex(0);
setExerciseIndex(exerciseIndex - 1);
};
const getExercise = () => {
if (exerciseIndex === -1) {
return undefined;
}
const exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
return {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
};
const calculateExerciseIndex = () => {
if (partIndex === 0)
return (
@@ -292,35 +420,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
</div>
);
const exercise = getExercise();
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (exercise && exercise.type === "multipleChoice") {
const match = exercise.questions[storeQuestionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
setContextHighlight([word]);
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = exercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`
);
exercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else {
setContextHighlight([]);
setContextWord(undefined);
}
}
}, [storeQuestionIndex, contextWordLine]);
return (
<>
<div className="flex flex-col h-full w-full gap-8 items-center">
@@ -344,7 +443,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
!editing &&
renderExercise(exercise!, exam.id, nextExercise, previousExercise)}
currentExercise &&
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&