diff --git a/src/components/Medium/ModuleTitle.tsx b/src/components/Medium/ModuleTitle.tsx
index 4fe6548d..bbbeec32 100644
--- a/src/components/Medium/ModuleTitle.tsx
+++ b/src/components/Medium/ModuleTitle.tsx
@@ -1,10 +1,10 @@
-import { Module } from "@/interfaces";
+import {Module} from "@/interfaces";
import useExamStore from "@/stores/examStore";
-import { moduleLabels } from "@/utils/moduleUtils";
+import {moduleLabels} from "@/utils/moduleUtils";
import clsx from "clsx";
-import { motion } from "framer-motion";
-import { ReactNode, useEffect, useState } from "react";
-import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
+import {motion} from "framer-motion";
+import {ReactNode, useEffect, useState} from "react";
+import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
import ProgressBar from "../Low/ProgressBar";
import TimerEndedModal from "../TimerEndedModal";
@@ -18,15 +18,13 @@ interface Props {
partLabel?: string;
}
-export default function ModuleTitle({
- minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel
-}: 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);
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
- const { timeSpent } = useExamStore((state) => state);
+ const {timeSpent} = useExamStore((state) => state);
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
@@ -48,7 +46,7 @@ export default function ModuleTitle({
if (timer < 300 && !warningMode) setWarningMode(true);
}, [timer, warningMode]);
- const moduleIcon: { [key in Module]: ReactNode } = {
+ const moduleIcon: {[key in Module]: ReactNode} = {
reading: ,
listening: ,
writing: ,
@@ -70,9 +68,9 @@ export default function ModuleTitle({
"absolute top-4 right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
)}
- initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
- animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
- transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
+ initial={{scale: warningMode && !disableTimer ? 0.8 : 1}}
+ animate={{scale: warningMode && !disableTimer ? 1.1 : 1}}
+ transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}>
{timer > 0 && (
@@ -90,11 +88,25 @@ export default function ModuleTitle({
- {partLabel &&
{partLabel.split('\n\n').map((line, index) => {
- if(index == 0) return
{line}
- else return
{line}
- })}
}
-
+ {partLabel && (
+
+ {partLabel.split("\n\n").map((line, index) => {
+ if (index == 0)
+ return (
+
+ {line}
+
+ );
+ else
+ return (
+
+ {line}
+
+ );
+ })}
+
+ )}
+
{moduleIcon[module]}
diff --git a/src/components/TrainingContent/ExerciseWalkthrough.tsx b/src/components/TrainingContent/ExerciseWalkthrough.tsx
index 74c7219f..75a2dbbd 100644
--- a/src/components/TrainingContent/ExerciseWalkthrough.tsx
+++ b/src/components/TrainingContent/ExerciseWalkthrough.tsx
@@ -1,287 +1,280 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import { animated } from '@react-spring/web';
-import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
-import HighlightContent from '../HighlightContent';
-import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
-
+import React, {useState, useEffect, useRef, useCallback} from "react";
+import {animated} from "@react-spring/web";
+import {FaRegCirclePlay, FaRegCircleStop} from "react-icons/fa6";
+import HighlightContent from "../HighlightContent";
+import {ITrainingTip, SegmentRef, TimelineEvent} from "./TrainingInterfaces";
const ExerciseWalkthrough: React.FC
= (tip: ITrainingTip) => {
- const [isAutoPlaying, setIsAutoPlaying] = useState(false);
- const [currentTime, setCurrentTime] = useState(0);
- const [walkthroughHtml, setWalkthroughHtml] = useState('');
- const [highlightedPhrases, setHighlightedPhrases] = useState([]);
- const [isPlaying, setIsPlaying] = useState(false);
- const timelineRef = useRef([]);
- const animationRef = useRef(null);
- const segmentsRef = useRef([]);
+ const [isAutoPlaying, setIsAutoPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [walkthroughHtml, setWalkthroughHtml] = useState("");
+ const [highlightedPhrases, setHighlightedPhrases] = useState([]);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const timelineRef = useRef([]);
+ const animationRef = useRef(null);
+ const segmentsRef = useRef([]);
- const toggleAutoPlay = useCallback(() => {
- setIsAutoPlaying((prev) => {
- if (!prev && currentTime === getMaxTime()) {
- setCurrentTime(0);
- }
- return !prev;
- });
- }, [currentTime]);
+ const toggleAutoPlay = useCallback(() => {
+ setIsAutoPlaying((prev) => {
+ if (!prev && currentTime === getMaxTime()) {
+ setCurrentTime(0);
+ }
+ return !prev;
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentTime]);
- const handleAnimationComplete = useCallback(() => {
- setIsAutoPlaying(false);
- }, []);
+ const handleAnimationComplete = useCallback(() => {
+ setIsAutoPlaying(false);
+ }, []);
- const handleResetAnimation = useCallback((newTime: number) => {
- setCurrentTime(newTime);
- }, []);
+ const handleResetAnimation = useCallback((newTime: number) => {
+ setCurrentTime(newTime);
+ }, []);
- const getMaxTime = (): number => {
- return tip.exercise?.segments.reduce((sum, segment) =>
- sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0
- ) ?? 0;
- };
+ const getMaxTime = (): number => {
+ return (
+ tip.exercise?.segments.reduce((sum, segment) => sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0) ?? 0
+ );
+ };
- useEffect(() => {
- const timeline: TimelineEvent[] = [];
- let currentTimePosition = 0;
- segmentsRef.current = [];
+ useEffect(() => {
+ const timeline: TimelineEvent[] = [];
+ let currentTimePosition = 0;
+ segmentsRef.current = [];
- tip.exercise?.segments.forEach((segment, index) => {
- const parser = new DOMParser();
- const doc = parser.parseFromString(segment.html, 'text/html');
- const words: string[] = [];
- const walkTree = (node: Node) => {
- if (node.nodeType === Node.TEXT_NODE) {
- words.push(...(node.textContent?.split(/\s+/).filter(word => word.length > 0) || []));
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- Array.from(node.childNodes).forEach(walkTree);
- }
- };
- walkTree(doc.body);
+ tip.exercise?.segments.forEach((segment, index) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(segment.html, "text/html");
+ const words: string[] = [];
+ const walkTree = (node: Node) => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ words.push(...(node.textContent?.split(/\s+/).filter((word) => word.length > 0) || []));
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ Array.from(node.childNodes).forEach(walkTree);
+ }
+ };
+ walkTree(doc.body);
- const textDuration = words.length * segment.wordDelay;
+ const textDuration = words.length * segment.wordDelay;
- segmentsRef.current.push({
- ...segment,
- words: words,
- startTime: currentTimePosition,
- endTime: currentTimePosition + textDuration
- });
+ segmentsRef.current.push({
+ ...segment,
+ words: words,
+ startTime: currentTimePosition,
+ endTime: currentTimePosition + textDuration,
+ });
- timeline.push({
- type: 'text',
- start: currentTimePosition,
- end: currentTimePosition + textDuration,
- segmentIndex: index
- });
+ timeline.push({
+ type: "text",
+ start: currentTimePosition,
+ end: currentTimePosition + textDuration,
+ segmentIndex: index,
+ });
- currentTimePosition += textDuration;
+ currentTimePosition += textDuration;
- timeline.push({
- type: 'highlight',
- start: currentTimePosition,
- end: currentTimePosition + segment.holdDelay,
- content: segment.highlight,
- segmentIndex: index
- });
+ timeline.push({
+ type: "highlight",
+ start: currentTimePosition,
+ end: currentTimePosition + segment.holdDelay,
+ content: segment.highlight,
+ segmentIndex: index,
+ });
- currentTimePosition += segment.holdDelay;
- });
+ currentTimePosition += segment.holdDelay;
+ });
- timelineRef.current = timeline;
- }, [tip.exercise?.segments]);
+ timelineRef.current = timeline;
+ }, [tip.exercise?.segments]);
- const updateText = useCallback(() => {
- const currentEvent = timelineRef.current.find(
- event => currentTime >= event.start && currentTime < event.end
- );
+ const updateText = useCallback(() => {
+ const currentEvent = timelineRef.current.find((event) => currentTime >= event.start && currentTime < event.end);
- if (currentEvent) {
- if (currentEvent.type === 'text') {
- const segment = segmentsRef.current[currentEvent.segmentIndex];
- const elapsedTime = currentTime - currentEvent.start;
- const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
+ if (currentEvent) {
+ if (currentEvent.type === "text") {
+ const segment = segmentsRef.current[currentEvent.segmentIndex];
+ const elapsedTime = currentTime - currentEvent.start;
+ const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
- const previousSegmentsHtml = segmentsRef.current
- .slice(0, currentEvent.segmentIndex)
- .map(seg => seg.html)
- .join('');
+ const previousSegmentsHtml = segmentsRef.current
+ .slice(0, currentEvent.segmentIndex)
+ .map((seg) => seg.html)
+ .join("");
- const parser = new DOMParser();
- const doc = parser.parseFromString(segment.html, 'text/html');
- let wordCount = 0;
- const walkTree = (node: Node, action: (node: Node) => void): boolean => {
- if (node.nodeType === Node.TEXT_NODE && node.textContent) {
- const words = node.textContent.split(/(\s+)/).filter(word => word.length > 0);
- if (wordCount + words.filter(w => !/\s+/.test(w)).length <= wordsToShow) {
- action(node.cloneNode(true));
- wordCount += words.filter(w => !/\s+/.test(w)).length;
- } else {
- const remainingWords = wordsToShow - wordCount;
- const newTextContent = words.reduce((acc, word) => {
- if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
- acc.text += word;
- acc.nonSpaceWords++;
- } else if (/\s+/.test(word) || acc.nonSpaceWords < remainingWords) {
- acc.text += word;
- }
- return acc;
- }, { text: '', nonSpaceWords: 0 }).text;
- const newNode = node.cloneNode(false);
- newNode.textContent = newTextContent;
- action(newNode);
- wordCount = wordsToShow;
- }
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- const clone = node.cloneNode(false);
- action(clone);
- Array.from(node.childNodes).some(child => {
- return walkTree(child, childNode => (clone as Node).appendChild(childNode));
- });
- }
- return wordCount >= wordsToShow;
- };
- const fragment = document.createDocumentFragment();
- walkTree(doc.body, node => fragment.appendChild(node));
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(segment.html, "text/html");
+ let wordCount = 0;
+ const walkTree = (node: Node, action: (node: Node) => void): boolean => {
+ if (node.nodeType === Node.TEXT_NODE && node.textContent) {
+ const words = node.textContent.split(/(\s+)/).filter((word) => word.length > 0);
+ if (wordCount + words.filter((w) => !/\s+/.test(w)).length <= wordsToShow) {
+ action(node.cloneNode(true));
+ wordCount += words.filter((w) => !/\s+/.test(w)).length;
+ } else {
+ const remainingWords = wordsToShow - wordCount;
+ const newTextContent = words.reduce(
+ (acc, word) => {
+ if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
+ acc.text += word;
+ acc.nonSpaceWords++;
+ } else if (/\s+/.test(word) || acc.nonSpaceWords < remainingWords) {
+ acc.text += word;
+ }
+ return acc;
+ },
+ {text: "", nonSpaceWords: 0},
+ ).text;
+ const newNode = node.cloneNode(false);
+ newNode.textContent = newTextContent;
+ action(newNode);
+ wordCount = wordsToShow;
+ }
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ const clone = node.cloneNode(false);
+ action(clone);
+ Array.from(node.childNodes).some((child) => {
+ return walkTree(child, (childNode) => (clone as Node).appendChild(childNode));
+ });
+ }
+ return wordCount >= wordsToShow;
+ };
+ const fragment = document.createDocumentFragment();
+ walkTree(doc.body, (node) => fragment.appendChild(node));
- const serializer = new XMLSerializer();
- const currentSegmentHtml = Array.from(fragment.childNodes)
- .map(node => serializer.serializeToString(node))
- .join('');
- const newHtml = previousSegmentsHtml + currentSegmentHtml;
+ const serializer = new XMLSerializer();
+ const currentSegmentHtml = Array.from(fragment.childNodes)
+ .map((node) => serializer.serializeToString(node))
+ .join("");
+ const newHtml = previousSegmentsHtml + currentSegmentHtml;
- setWalkthroughHtml(newHtml);
- setHighlightedPhrases([]);
- } else if (currentEvent.type === 'highlight') {
- const newHtml = segmentsRef.current
- .slice(0, currentEvent.segmentIndex + 1)
- .map(seg => seg.html)
- .join('');
- setWalkthroughHtml(newHtml);
- setHighlightedPhrases(currentEvent.content || []);
- }
- }
- }, [currentTime]);
+ setWalkthroughHtml(newHtml);
+ setHighlightedPhrases([]);
+ } else if (currentEvent.type === "highlight") {
+ const newHtml = segmentsRef.current
+ .slice(0, currentEvent.segmentIndex + 1)
+ .map((seg) => seg.html)
+ .join("");
+ setWalkthroughHtml(newHtml);
+ setHighlightedPhrases(currentEvent.content || []);
+ }
+ }
+ }, [currentTime]);
- useEffect(() => {
- updateText();
- }, [currentTime, updateText]);
+ useEffect(() => {
+ updateText();
+ }, [currentTime, updateText]);
- useEffect(() => {
- if (isAutoPlaying) {
- const lastEvent = timelineRef.current[timelineRef.current.length - 1];
- if (lastEvent && currentTime >= lastEvent.end) {
- setCurrentTime(0);
- }
- setIsPlaying(true);
- } else {
- setIsPlaying(false);
- }
- }, [isAutoPlaying, currentTime]);
+ useEffect(() => {
+ if (isAutoPlaying) {
+ const lastEvent = timelineRef.current[timelineRef.current.length - 1];
+ if (lastEvent && currentTime >= lastEvent.end) {
+ setCurrentTime(0);
+ }
+ setIsPlaying(true);
+ } else {
+ setIsPlaying(false);
+ }
+ }, [isAutoPlaying, currentTime]);
- useEffect(() => {
- const animate = () => {
- if (isPlaying) {
- setCurrentTime((prevTime) => {
- const newTime = prevTime + 50;
- const lastEvent = timelineRef.current[timelineRef.current.length - 1];
- if (lastEvent && newTime >= lastEvent.end) {
- setIsPlaying(false);
- handleAnimationComplete();
- return lastEvent.end;
- }
- return newTime;
- });
- }
- animationRef.current = requestAnimationFrame(animate);
- };
+ useEffect(() => {
+ const animate = () => {
+ if (isPlaying) {
+ setCurrentTime((prevTime) => {
+ const newTime = prevTime + 50;
+ const lastEvent = timelineRef.current[timelineRef.current.length - 1];
+ if (lastEvent && newTime >= lastEvent.end) {
+ setIsPlaying(false);
+ handleAnimationComplete();
+ return lastEvent.end;
+ }
+ return newTime;
+ });
+ }
+ animationRef.current = requestAnimationFrame(animate);
+ };
- animationRef.current = requestAnimationFrame(animate);
+ animationRef.current = requestAnimationFrame(animate);
- return () => {
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current);
- }
- };
- }, [isPlaying, handleAnimationComplete]);
+ return () => {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current);
+ }
+ };
+ }, [isPlaying, handleAnimationComplete]);
- const handleSliderChange = (e: React.ChangeEvent) => {
- const newTime = parseInt(e.target.value, 10);
- setCurrentTime(newTime);
- handleResetAnimation(newTime);
- };
+ const handleSliderChange = (e: React.ChangeEvent) => {
+ const newTime = parseInt(e.target.value, 10);
+ setCurrentTime(newTime);
+ handleResetAnimation(newTime);
+ };
- const handleSliderMouseDown = () => {
- setIsPlaying(false);
- };
+ const handleSliderMouseDown = () => {
+ setIsPlaying(false);
+ };
- const handleSliderMouseUp = () => {
- if (isAutoPlaying) {
- setIsPlaying(true);
- }
- };
+ const handleSliderMouseUp = () => {
+ if (isAutoPlaying) {
+ setIsPlaying(true);
+ }
+ };
- if (tip.standalone || !tip.exercise) {
- return (
-
-
The exercise for this tip is not available yet!
-
-
- );
- }
+ if (tip.standalone || !tip.exercise) {
+ return (
+
+
The exercise for this tip is not available yet!
+
+
+ );
+ }
-
- return (
-
-
-
-
-
- 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0}
- value={currentTime}
- onChange={handleSliderChange}
- onMouseDown={handleSliderMouseDown}
- onMouseUp={handleSliderMouseUp}
- onTouchStart={handleSliderMouseDown}
- onTouchEnd={handleSliderMouseUp}
- className='flex-grow'
- />
-
-
-
-
- );
+ return (
+
+
+
+
+
+ 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0}
+ value={currentTime}
+ onChange={handleSliderChange}
+ onMouseDown={handleSliderMouseDown}
+ onMouseUp={handleSliderMouseUp}
+ onTouchStart={handleSliderMouseDown}
+ onTouchEnd={handleSliderMouseUp}
+ className="flex-grow"
+ />
+
+
+
+
+ );
};
export default ExerciseWalkthrough;
diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx
index a5bdf478..fb4c6ee9 100644
--- a/src/exams/Level.tsx
+++ b/src/exams/Level.tsx
@@ -1,22 +1,32 @@
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
-import { renderExercise } from "@/components/Exercises";
+import {renderExercise} from "@/components/Exercises";
import HighlightContent from "@/components/HighlightContent";
import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
-import { renderSolution } from "@/components/Solutions";
-import { infoButtonStyle } from "@/constants/buttonStyles";
-import { Module } from "@/interfaces";
-import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
+import {renderSolution} from "@/components/Solutions";
+import {infoButtonStyle} from "@/constants/buttonStyles";
+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 {defaultUserSolutions} from "@/utils/exams";
+import {countExercises} from "@/utils/moduleUtils";
+import {mdiArrowRight} from "@mdi/js";
import Icon from "@mdi/react";
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";
+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;
@@ -26,9 +36,13 @@ interface Props {
}
function TextComponent({
- part, contextWord, setContextWordLine
+ part,
+ contextWord,
+ setContextWordLine,
}: {
- part: LevelPart, contextWord: string | undefined, setContextWordLine: React.Dispatch>
+ part: LevelPart;
+ contextWord: string | undefined;
+ setContextWordLine: React.Dispatch>;
}) {
const textRef = useRef(null);
const [lineNumbers, setLineNumbers] = useState([]);
@@ -42,21 +56,21 @@ function TextComponent({
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';
+ 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.textAlign = computedStyle.textAlign as CanvasTextAlign;
- const textContent = textRef.current.textContent || '';
+ const textContent = textRef.current.textContent || "";
textContent.split(/(\s+)/).forEach((word: string) => {
- const span = document.createElement('span');
+ const span = document.createElement("span");
span.textContent = word;
- span.style.display = 'inline-block';
+ span.style.display = "inline-block";
span.style.height = `calc(1em + 16px)`;
offscreenElement.appendChild(span);
});
@@ -73,9 +87,9 @@ function TextComponent({
currentLineTop = firstChild.getBoundingClientRect().top;
}
- const spans = offscreenElement.querySelectorAll('span');
+ const spans = offscreenElement.querySelectorAll("span");
- spans.forEach(span => {
+ spans.forEach((span) => {
const rect = span.getBoundingClientRect();
const top = rect.top;
@@ -85,8 +99,7 @@ function TextComponent({
lines.push([]);
}
- lines[lines.length - 1].push(span.textContent?.trim() || '');
-
+ lines[lines.length - 1].push(span.textContent?.trim() || "");
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
contextWordLine = currentLine;
@@ -115,9 +128,11 @@ function TextComponent({
return () => {
if (textRef.current) {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
resizeObserver.unobserve(textRef.current);
}
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [part.context, part.showContextLines, contextWord]);
if (typeof part.showContextLines === "undefined") {
@@ -142,8 +157,13 @@ function TextComponent({
- {part.context!.split('\n\n').map((line, index) => {
- return
{index + 1}{line}
+ {part.context!.split("\n\n").map((line, index) => {
+ return (
+
+ {index + 1}
+ {line}
+
+ );
})}
@@ -151,22 +171,18 @@ 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
- );
-}
+ 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 }[]>([]);
+export default function Level({exam, showSolutions = false, onFinish, editing = false}: Props) {
+ const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
const [showBlankModal, setShowBlankModal] = useState(false);
- const { userSolutions, setUserSolutions } = useExamStore((state) => state);
- const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state);
- const { partIndex, setPartIndex } = useExamStore((state) => state);
- const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
+ const {userSolutions, setUserSolutions} = useExamStore((state) => state);
+ const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
+ 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();
@@ -197,7 +213,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
onFinish(userSolutions);
};
-
const getExercise = () => {
if (exerciseIndex === -1) {
return undefined;
@@ -287,12 +302,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
};
useEffect(() => {
- //console.log("Getting another exercise");
- //setShuffleMaps([]);
setCurrentExercise(getExercise());
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [partIndex, exerciseIndex]);
-
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (currentExercise && currentExercise.type === "multipleChoice") {
@@ -307,7 +320,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
- `in line ${contextWordLine || originalLineNumber}`
+ `in line ${contextWordLine || originalLineNumber}`,
);
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
@@ -315,16 +328,20 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setContextWord(undefined);
}
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]);
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
- setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
+ setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
}
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 }]);
+ setMultipleChoicesDone((prev) => [
+ ...prev.filter((x) => x.id !== currentExercise!.id),
+ {id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex},
+ ]);
}
setStoreQuestionIndex(0);
@@ -355,11 +372,11 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
setHasExamEnded(false);
if (solution) {
- let stat = { ...solution, module: "level" as Module, 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 }]);
+ onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...stat}]);
} else {
onFinish(userSolutions);
}
@@ -368,11 +385,14 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
- setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
+ setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
}
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 }]);
+ setMultipleChoicesDone((prev) => [
+ ...prev.filter((x) => x.id !== currentExercise!.id),
+ {id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex},
+ ]);
}
setStoreQuestionIndex(0);
@@ -391,7 +411,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
exercisesDone +
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
storeQuestionIndex +
- multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount}, 0)
+ multipleChoicesDone.reduce((acc, curr) => {
+ return acc + curr.amount;
+ }, 0)
);
};
@@ -404,21 +426,21 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
You will be allowed to read the text while doing the exercises
-
+
>
);
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}`
+ 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 `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${
+ currentExercise.questions[currentExercise.questions.length - 1].id
+ })\n\n${currentExercise.prompt}`;
+ };
return (
<>
diff --git a/src/pages/training/[id]/index.tsx b/src/pages/training/[id]/index.tsx
index 94226995..ae902a29 100644
--- a/src/pages/training/[id]/index.tsx
+++ b/src/pages/training/[id]/index.tsx
@@ -1,357 +1,371 @@
-import { useEffect, useState } from 'react';
-import { useRouter } from 'next/router';
-import axios from 'axios';
-import { Tab } from "@headlessui/react";
-import { AiOutlineFileSearch } from "react-icons/ai";
-import { MdOutlinePlaylistAddCheckCircle, MdOutlineSelfImprovement } from "react-icons/md";
-import { BsChatLeftDots } from "react-icons/bs";
+import {useEffect, useState} from "react";
+import {useRouter} from "next/router";
+import axios from "axios";
+import {Tab} from "@headlessui/react";
+import {AiOutlineFileSearch} from "react-icons/ai";
+import {MdOutlinePlaylistAddCheckCircle, MdOutlineSelfImprovement} from "react-icons/md";
+import {BsChatLeftDots} from "react-icons/bs";
import Button from "@/components/Low/Button";
import clsx from "clsx";
import Exercise from "@/training/Exercise";
import TrainingScore from "@/training/TrainingScore";
-import { ITrainingContent, ITrainingTip } from "@/training/TrainingInterfaces";
-import { Stat, User } from '@/interfaces/user';
+import {ITrainingContent, ITrainingTip} from "@/training/TrainingInterfaces";
+import {Stat, User} from "@/interfaces/user";
import Head from "next/head";
import Layout from "@/components/High/Layout";
-import { ToastContainer } from 'react-toastify';
-import { withIronSessionSsr } from "iron-session/next";
-import { shouldRedirectHome } from "@/utils/navigation.disabled";
-import { sessionOptions } from "@/lib/session";
-import qs from 'qs';
-import StatsGridItem from '@/components/StatGridItem';
+import {ToastContainer} from "react-toastify";
+import {withIronSessionSsr} from "iron-session/next";
+import {shouldRedirectHome} from "@/utils/navigation.disabled";
+import {sessionOptions} from "@/lib/session";
+import qs from "qs";
+import StatsGridItem from "@/components/StatGridItem";
import useExamStore from "@/stores/examStore";
-import { usePDFDownload } from "@/hooks/usePDFDownload";
-import useAssignments from '@/hooks/useAssignments';
-import useUsers from '@/hooks/useUsers';
+import {usePDFDownload} from "@/hooks/usePDFDownload";
+import useAssignments from "@/hooks/useAssignments";
+import useUsers from "@/hooks/useUsers";
import Dropdown from "@/components/Dropdown";
-import InfiniteCarousel from '@/components/InfiniteCarousel';
-import { LuExternalLink } from "react-icons/lu";
-import { uniqBy } from 'lodash';
-import { getExamById } from '@/utils/exams';
-import { convertToUserSolutions } from '@/utils/stats';
-import { sortByModule } from '@/utils/moduleUtils';
+import InfiniteCarousel from "@/components/InfiniteCarousel";
+import {LuExternalLink} from "react-icons/lu";
+import {uniqBy} from "lodash";
+import {getExamById} from "@/utils/exams";
+import {convertToUserSolutions} from "@/utils/stats";
+import {sortByModule} from "@/utils/moduleUtils";
-export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
- const user = req.session.user;
+export const getServerSideProps = withIronSessionSsr(({req, res}) => {
+ const user = req.session.user;
- if (!user || !user.isVerified) {
- return {
- redirect: {
- destination: "/login",
- permanent: false,
- },
- };
- }
+ if (!user || !user.isVerified) {
+ return {
+ redirect: {
+ destination: "/login",
+ permanent: false,
+ },
+ };
+ }
- if (shouldRedirectHome(user)) {
- return {
- redirect: {
- destination: "/",
- permanent: false,
- },
- };
- }
+ if (shouldRedirectHome(user)) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
- return {
- props: { user: req.session.user },
- };
+ return {
+ props: {user: req.session.user},
+ };
}, sessionOptions);
-const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
- // Record stuff
- const setExams = useExamStore((state) => state.setExams);
- const setShowSolutions = useExamStore((state) => state.setShowSolutions);
- const setUserSolutions = useExamStore((state) => state.setUserSolutions);
- const setSelectedModules = useExamStore((state) => state.setSelectedModules);
- const setInactivity = useExamStore((state) => state.setInactivity);
- const setTimeSpent = useExamStore((state) => state.setTimeSpent);
- const renderPdfIcon = usePDFDownload("stats");
+const TrainingContent: React.FC<{user: User}> = ({user}) => {
+ // Record stuff
+ const setExams = useExamStore((state) => state.setExams);
+ const setShowSolutions = useExamStore((state) => state.setShowSolutions);
+ const setUserSolutions = useExamStore((state) => state.setUserSolutions);
+ const setSelectedModules = useExamStore((state) => state.setSelectedModules);
+ const setInactivity = useExamStore((state) => state.setInactivity);
+ const setTimeSpent = useExamStore((state) => state.setTimeSpent);
+ const renderPdfIcon = usePDFDownload("stats");
- const [trainingContent, setTrainingContent] = useState
(null);
- const [loading, setLoading] = useState(true);
- const [trainingTips, setTrainingTips] = useState([]);
- const [currentTipIndex, setCurrentTipIndex] = useState(0);
- const { assignments } = useAssignments({});
- const { users } = useUsers();
+ const [trainingContent, setTrainingContent] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [trainingTips, setTrainingTips] = useState([]);
+ const [currentTipIndex, setCurrentTipIndex] = useState(0);
+ const {assignments} = useAssignments({});
+ const {users} = useUsers();
- const router = useRouter();
- const { id } = router.query;
+ const router = useRouter();
+ const {id} = router.query;
- useEffect(() => {
- const fetchTrainingContent = async () => {
- if (!id || typeof id !== 'string') return;
+ useEffect(() => {
+ const fetchTrainingContent = async () => {
+ if (!id || typeof id !== "string") return;
- try {
- setLoading(true);
- const response = await axios.get(`/api/training/${id}`);
- const trainingContent = response.data;
+ try {
+ setLoading(true);
+ const response = await axios.get(`/api/training/${id}`);
+ const trainingContent = response.data;
- const withExamsStats = {
- ...trainingContent,
- exams: await Promise.all(trainingContent.exams.map(async (exam) => {
- const stats = await Promise.all(exam.stat_ids.map(async (statId) => {
- const statResponse = await axios.get(`/api/stats/${statId}`);
- return statResponse.data;
- }));
- return { ...exam, stats };
- }))
- };
+ const withExamsStats = {
+ ...trainingContent,
+ exams: await Promise.all(
+ trainingContent.exams.map(async (exam) => {
+ const stats = await Promise.all(
+ exam.stat_ids.map(async (statId) => {
+ const statResponse = await axios.get(`/api/stats/${statId}`);
+ return statResponse.data;
+ }),
+ );
+ return {...exam, stats};
+ }),
+ ),
+ };
- const tips = await axios.get('/api/training/walkthrough', {
- params: { ids: trainingContent.tip_ids },
- paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
- });
- setTrainingTips(tips.data);
- setTrainingContent(withExamsStats);
- } catch (error) {
- router.push('/training');
- } finally {
- setLoading(false);
- }
- };
+ const tips = await axios.get("/api/training/walkthrough", {
+ params: {ids: trainingContent.tip_ids},
+ paramsSerializer: (params) => qs.stringify(params, {arrayFormat: "repeat"}),
+ });
+ setTrainingTips(tips.data);
+ setTrainingContent(withExamsStats);
+ } catch (error) {
+ router.push("/training");
+ } finally {
+ setLoading(false);
+ }
+ };
- fetchTrainingContent();
- }, [id]);
+ fetchTrainingContent();
+ }, [id, router]);
- const handleNext = () => {
- setCurrentTipIndex((prevIndex) => (prevIndex + 1));
- };
+ const handleNext = () => {
+ setCurrentTipIndex((prevIndex) => prevIndex + 1);
+ };
- const handlePrevious = () => {
- setCurrentTipIndex((prevIndex) => (prevIndex - 1));
- };
+ const handlePrevious = () => {
+ setCurrentTipIndex((prevIndex) => prevIndex - 1);
+ };
- const goToExam = (examNumber: number) => {
- const stats = trainingContent?.exams[examNumber].stats!;
- const examPromises = uniqBy(stats, "exam").map((stat) => {
- return getExamById(stat.module, stat.exam);
- });
+ const goToExam = (examNumber: number) => {
+ const stats = trainingContent?.exams[examNumber].stats!;
+ const examPromises = uniqBy(stats, "exam").map((stat) => {
+ return getExamById(stat.module, stat.exam);
+ });
- const { timeSpent, inactivity } = stats[0];
+ const {timeSpent, inactivity} = stats[0];
- Promise.all(examPromises).then((exams) => {
- if (exams.every((x) => !!x)) {
- if (!!timeSpent) setTimeSpent(timeSpent);
- if (!!inactivity) setInactivity(inactivity);
- setUserSolutions(convertToUserSolutions(stats));
- setShowSolutions(true);
- setExams(exams.map((x) => x!).sort(sortByModule));
- setSelectedModules(
- exams
- .map((x) => x!)
- .sort(sortByModule)
- .map((x) => x!.module),
- );
- router.push("/exercises");
- }
- });
- }
+ Promise.all(examPromises).then((exams) => {
+ if (exams.every((x) => !!x)) {
+ if (!!timeSpent) setTimeSpent(timeSpent);
+ if (!!inactivity) setInactivity(inactivity);
+ setUserSolutions(convertToUserSolutions(stats));
+ setShowSolutions(true);
+ setExams(exams.map((x) => x!).sort(sortByModule));
+ setSelectedModules(
+ exams
+ .map((x) => x!)
+ .sort(sortByModule)
+ .map((x) => x!.module),
+ );
+ router.push("/exercises");
+ }
+ });
+ };
- return (
- <>
-
- Training | EnCoach
-
-
-
-
-
+ return (
+ <>
+
+ Training | EnCoach
+
+
+
+
+
-
- {loading ? (
-
-
-
- ) : (trainingContent && (
-
-
- {trainingContent.exams.length}
- Exams Selected
-
-
-
- }
- overlayFunc={goToExam}
- overlayClassName='bottom-6 right-5 cursor-pointer'
- >
- {trainingContent.exams.map((exam, examIndex) => (
-
- ))}
-
-
-
-
-
-
-
-
General Evaluation
-
-
-
-
-
-
Performance Breakdown by Exam:
-
-
- {trainingContent.exams.flatMap((exam, index) => (
- -
-
-
- Exam
- {index + 1}
-
-
{exam.score}%
-
-
-
-
{exam.performance_comment}
-
-
- ))}
-
-
-
-
-
-
-
Identified Weak Areas
-
-
-
-
-
- {trainingContent.weak_areas.map((x, index) => (
-
- clsx(
- 'text-[#53B2F9] pb-2 border-b-2',
- 'focus:outline-none',
- selected ? 'border-[#1B78BE]' : 'border-[#1B78BE0F]'
- )
- }
- >
- {x.area}
-
- ))}
-
-
-
- {trainingContent.weak_areas.map((x, index) => (
-
- {x.comment}
-
- ))}
-
-
-
-
-
-
-
-
Subjects that Need Improvement
-
+
+ {loading ? (
+
+
+
+ ) : (
+ trainingContent && (
+
+
+
+ {trainingContent.exams.length}
+
+ Exams Selected
+
+
+ }
+ overlayFunc={goToExam}
+ overlayClassName="bottom-6 right-5 cursor-pointer">
+ {trainingContent.exams.map((exam, examIndex) => (
+
+ ))}
+
+
+
+
+
+
+
+
General Evaluation
+
+
+
+
+
+
Performance Breakdown by Exam:
+
+
+ {trainingContent.exams.flatMap((exam, index) => (
+ -
+
+
+ Exam
+
+ {index + 1}
+
+
+
{exam.score}%
+
+
+
+
{exam.performance_comment}
+
+
+ ))}
+
+
+
+
+
+
+
Identified Weak Areas
+
+
+
+
+
+ {trainingContent.weak_areas.map((x, index) => (
+
+ clsx(
+ "text-[#53B2F9] pb-2 border-b-2",
+ "focus:outline-none",
+ selected ? "border-[#1B78BE]" : "border-[#1B78BE0F]",
+ )
+ }>
+ {x.area}
+
+ ))}
+
+
+
+ {trainingContent.weak_areas.map((x, index) => (
+
+ {x.comment}
+
+ ))}
+
+
+
+
+
+
+
+
Subjects that Need Improvement
+
-
-
-
-
- {trainingContent.exams.map((exam, index) => (
- -
-
- Exam
- {index + 1}
-
- } open={index == 0}>
-
{exam.detailed_summary}
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
- >
- );
-}
+
+
+
+
+ {trainingContent.exams.map((exam, index) => (
+ -
+
+ Exam
+
+ {index + 1}
+
+
+ }
+ open={index == 0}>
+
{exam.detailed_summary}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ )}
+
+ >
+ );
+};
export default TrainingContent;
-
diff --git a/src/pages/training/index.tsx b/src/pages/training/index.tsx
index c837fd16..30ab8b66 100644
--- a/src/pages/training/index.tsx
+++ b/src/pages/training/index.tsx
@@ -84,6 +84,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.events, setTrainingStats]);
useEffect(() => {
@@ -104,6 +105,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
}
});
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNewContentLoading]);
useEffect(() => {