From 97b533bd3abf787461daa20b7fa8659099c0941c Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 20 Aug 2024 10:07:18 +0100 Subject: [PATCH] Took care of some warnings --- src/components/Medium/ModuleTitle.tsx | 48 +- .../TrainingContent/ExerciseWalkthrough.tsx | 499 +++++++------ src/exams/Level.tsx | 142 ++-- src/pages/training/[id]/index.tsx | 666 +++++++++--------- src/pages/training/index.tsx | 2 + 5 files changed, 700 insertions(+), 657 deletions(-) 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!

-
-

{tip.tipCategory}

-
-
-
- ); - } + if (tip.standalone || !tip.exercise) { + return ( +
+

The exercise for this tip is not available yet!

+
+

{tip.tipCategory}

+
+
+
+ ); + } - - return ( -
-
-

{tip.tipCategory}

-
-
-
-
- - 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0} - value={currentTime} - onChange={handleSliderChange} - onMouseDown={handleSliderMouseDown} - onMouseUp={handleSliderMouseUp} - onTouchStart={handleSliderMouseDown} - onTouchEnd={handleSliderMouseUp} - className='flex-grow' - /> -
-
-
- {/*

Question

*/} -
- -
-
-
-
- -
-
-
-
-
-
- ); + return ( +
+
+

{tip.tipCategory}

+
+
+
+
+ + 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0} + value={currentTime} + onChange={handleSliderChange} + onMouseDown={handleSliderMouseDown} + onMouseUp={handleSliderMouseUp} + onTouchStart={handleSliderMouseDown} + onTouchEnd={handleSliderMouseUp} + className="flex-grow" + /> +
+
+
+ {/*

Question

*/} +
+ +
+
+
+
+ +
+
+
+
+
+
+ ); }; 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

+
-
-
-
-
- - - - - - - - -
-

Detailed Breakdown

-
-
    - {trainingContent.exams.map((exam, index) => ( -
  • - - Exam - {index + 1} -
- } open={index == 0}> - {exam.detailed_summary} - - - ))} - -
-
-
-
-
-
-
-
- -
-
- - -
-
- -
-
- ))} - - - ); -} +
+
+
+
+ + + + + + + + +
+

Detailed Breakdown

+
+
    + {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(() => {