Took care of some warnings
This commit is contained in:
@@ -18,9 +18,7 @@ interface Props {
|
|||||||
partLabel?: string;
|
partLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModuleTitle({
|
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel}: Props) {
|
||||||
minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel
|
|
||||||
}: Props) {
|
|
||||||
const [timer, setTimer] = useState(minTimer * 60);
|
const [timer, setTimer] = useState(minTimer * 60);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [warningMode, setWarningMode] = useState(false);
|
const [warningMode, setWarningMode] = useState(false);
|
||||||
@@ -90,10 +88,24 @@ export default function ModuleTitle({
|
|||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{partLabel && <div className="text-3xl space-y-4">{partLabel.split('\n\n').map((line, index) => {
|
{partLabel && (
|
||||||
if(index == 0) return <p className="font-bold">{line}</p>
|
<div className="text-3xl space-y-4">
|
||||||
else return <p className="text-2xl font-semibold">{line}</p>
|
{partLabel.split("\n\n").map((line, index) => {
|
||||||
})}</div>}
|
if (index == 0)
|
||||||
|
return (
|
||||||
|
<p key={index} className="font-bold">
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<p key={index} className="text-2xl font-semibold">
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={clsx("flex gap-6 w-full h-fit items-center", partLabel ? "mt-10" : "mt-5")}>
|
<div className={clsx("flex gap-6 w-full h-fit items-center", partLabel ? "mt-10" : "mt-5")}>
|
||||||
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
<div className="w-12 h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg">{moduleIcon[module]}</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, {useState, useEffect, useRef, useCallback} from "react";
|
||||||
import { animated } from '@react-spring/web';
|
import {animated} from "@react-spring/web";
|
||||||
import {FaRegCirclePlay, FaRegCircleStop} from "react-icons/fa6";
|
import {FaRegCirclePlay, FaRegCircleStop} from "react-icons/fa6";
|
||||||
import HighlightContent from '../HighlightContent';
|
import HighlightContent from "../HighlightContent";
|
||||||
import { ITrainingTip, SegmentRef, TimelineEvent } from './TrainingInterfaces';
|
import {ITrainingTip, SegmentRef, TimelineEvent} from "./TrainingInterfaces";
|
||||||
|
|
||||||
|
|
||||||
const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||||
const [isAutoPlaying, setIsAutoPlaying] = useState<boolean>(false);
|
const [isAutoPlaying, setIsAutoPlaying] = useState<boolean>(false);
|
||||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||||
const [walkthroughHtml, setWalkthroughHtml] = useState<string>('');
|
const [walkthroughHtml, setWalkthroughHtml] = useState<string>("");
|
||||||
const [highlightedPhrases, setHighlightedPhrases] = useState<string[]>([]);
|
const [highlightedPhrases, setHighlightedPhrases] = useState<string[]>([]);
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const timelineRef = useRef<TimelineEvent[]>([]);
|
const timelineRef = useRef<TimelineEvent[]>([]);
|
||||||
@@ -22,6 +21,7 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
}
|
}
|
||||||
return !prev;
|
return !prev;
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentTime]);
|
}, [currentTime]);
|
||||||
|
|
||||||
const handleAnimationComplete = useCallback(() => {
|
const handleAnimationComplete = useCallback(() => {
|
||||||
@@ -33,9 +33,9 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getMaxTime = (): number => {
|
const getMaxTime = (): number => {
|
||||||
return tip.exercise?.segments.reduce((sum, segment) =>
|
return (
|
||||||
sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0
|
tip.exercise?.segments.reduce((sum, segment) => sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0) ?? 0
|
||||||
) ?? 0;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,11 +45,11 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
|
|
||||||
tip.exercise?.segments.forEach((segment, index) => {
|
tip.exercise?.segments.forEach((segment, index) => {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
const doc = parser.parseFromString(segment.html, "text/html");
|
||||||
const words: string[] = [];
|
const words: string[] = [];
|
||||||
const walkTree = (node: Node) => {
|
const walkTree = (node: Node) => {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
words.push(...(node.textContent?.split(/\s+/).filter(word => word.length > 0) || []));
|
words.push(...(node.textContent?.split(/\s+/).filter((word) => word.length > 0) || []));
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
Array.from(node.childNodes).forEach(walkTree);
|
Array.from(node.childNodes).forEach(walkTree);
|
||||||
}
|
}
|
||||||
@@ -62,24 +62,24 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
...segment,
|
...segment,
|
||||||
words: words,
|
words: words,
|
||||||
startTime: currentTimePosition,
|
startTime: currentTimePosition,
|
||||||
endTime: currentTimePosition + textDuration
|
endTime: currentTimePosition + textDuration,
|
||||||
});
|
});
|
||||||
|
|
||||||
timeline.push({
|
timeline.push({
|
||||||
type: 'text',
|
type: "text",
|
||||||
start: currentTimePosition,
|
start: currentTimePosition,
|
||||||
end: currentTimePosition + textDuration,
|
end: currentTimePosition + textDuration,
|
||||||
segmentIndex: index
|
segmentIndex: index,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentTimePosition += textDuration;
|
currentTimePosition += textDuration;
|
||||||
|
|
||||||
timeline.push({
|
timeline.push({
|
||||||
type: 'highlight',
|
type: "highlight",
|
||||||
start: currentTimePosition,
|
start: currentTimePosition,
|
||||||
end: currentTimePosition + segment.holdDelay,
|
end: currentTimePosition + segment.holdDelay,
|
||||||
content: segment.highlight,
|
content: segment.highlight,
|
||||||
segmentIndex: index
|
segmentIndex: index,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentTimePosition += segment.holdDelay;
|
currentTimePosition += segment.holdDelay;
|
||||||
@@ -89,33 +89,32 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
}, [tip.exercise?.segments]);
|
}, [tip.exercise?.segments]);
|
||||||
|
|
||||||
const updateText = useCallback(() => {
|
const updateText = useCallback(() => {
|
||||||
const currentEvent = timelineRef.current.find(
|
const currentEvent = timelineRef.current.find((event) => currentTime >= event.start && currentTime < event.end);
|
||||||
event => currentTime >= event.start && currentTime < event.end
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentEvent) {
|
if (currentEvent) {
|
||||||
if (currentEvent.type === 'text') {
|
if (currentEvent.type === "text") {
|
||||||
const segment = segmentsRef.current[currentEvent.segmentIndex];
|
const segment = segmentsRef.current[currentEvent.segmentIndex];
|
||||||
const elapsedTime = currentTime - currentEvent.start;
|
const elapsedTime = currentTime - currentEvent.start;
|
||||||
const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
|
const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
|
||||||
|
|
||||||
const previousSegmentsHtml = segmentsRef.current
|
const previousSegmentsHtml = segmentsRef.current
|
||||||
.slice(0, currentEvent.segmentIndex)
|
.slice(0, currentEvent.segmentIndex)
|
||||||
.map(seg => seg.html)
|
.map((seg) => seg.html)
|
||||||
.join('');
|
.join("");
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
const doc = parser.parseFromString(segment.html, "text/html");
|
||||||
let wordCount = 0;
|
let wordCount = 0;
|
||||||
const walkTree = (node: Node, action: (node: Node) => void): boolean => {
|
const walkTree = (node: Node, action: (node: Node) => void): boolean => {
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
||||||
const words = node.textContent.split(/(\s+)/).filter(word => word.length > 0);
|
const words = node.textContent.split(/(\s+)/).filter((word) => word.length > 0);
|
||||||
if (wordCount + words.filter(w => !/\s+/.test(w)).length <= wordsToShow) {
|
if (wordCount + words.filter((w) => !/\s+/.test(w)).length <= wordsToShow) {
|
||||||
action(node.cloneNode(true));
|
action(node.cloneNode(true));
|
||||||
wordCount += words.filter(w => !/\s+/.test(w)).length;
|
wordCount += words.filter((w) => !/\s+/.test(w)).length;
|
||||||
} else {
|
} else {
|
||||||
const remainingWords = wordsToShow - wordCount;
|
const remainingWords = wordsToShow - wordCount;
|
||||||
const newTextContent = words.reduce((acc, word) => {
|
const newTextContent = words.reduce(
|
||||||
|
(acc, word) => {
|
||||||
if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
|
if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
|
||||||
acc.text += word;
|
acc.text += word;
|
||||||
acc.nonSpaceWords++;
|
acc.nonSpaceWords++;
|
||||||
@@ -123,7 +122,9 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
acc.text += word;
|
acc.text += word;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, { text: '', nonSpaceWords: 0 }).text;
|
},
|
||||||
|
{text: "", nonSpaceWords: 0},
|
||||||
|
).text;
|
||||||
const newNode = node.cloneNode(false);
|
const newNode = node.cloneNode(false);
|
||||||
newNode.textContent = newTextContent;
|
newNode.textContent = newTextContent;
|
||||||
action(newNode);
|
action(newNode);
|
||||||
@@ -132,28 +133,28 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
const clone = node.cloneNode(false);
|
const clone = node.cloneNode(false);
|
||||||
action(clone);
|
action(clone);
|
||||||
Array.from(node.childNodes).some(child => {
|
Array.from(node.childNodes).some((child) => {
|
||||||
return walkTree(child, childNode => (clone as Node).appendChild(childNode));
|
return walkTree(child, (childNode) => (clone as Node).appendChild(childNode));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return wordCount >= wordsToShow;
|
return wordCount >= wordsToShow;
|
||||||
};
|
};
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
walkTree(doc.body, node => fragment.appendChild(node));
|
walkTree(doc.body, (node) => fragment.appendChild(node));
|
||||||
|
|
||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
const currentSegmentHtml = Array.from(fragment.childNodes)
|
const currentSegmentHtml = Array.from(fragment.childNodes)
|
||||||
.map(node => serializer.serializeToString(node))
|
.map((node) => serializer.serializeToString(node))
|
||||||
.join('');
|
.join("");
|
||||||
const newHtml = previousSegmentsHtml + currentSegmentHtml;
|
const newHtml = previousSegmentsHtml + currentSegmentHtml;
|
||||||
|
|
||||||
setWalkthroughHtml(newHtml);
|
setWalkthroughHtml(newHtml);
|
||||||
setHighlightedPhrases([]);
|
setHighlightedPhrases([]);
|
||||||
} else if (currentEvent.type === 'highlight') {
|
} else if (currentEvent.type === "highlight") {
|
||||||
const newHtml = segmentsRef.current
|
const newHtml = segmentsRef.current
|
||||||
.slice(0, currentEvent.segmentIndex + 1)
|
.slice(0, currentEvent.segmentIndex + 1)
|
||||||
.map(seg => seg.html)
|
.map((seg) => seg.html)
|
||||||
.join('');
|
.join("");
|
||||||
setWalkthroughHtml(newHtml);
|
setWalkthroughHtml(newHtml);
|
||||||
setHighlightedPhrases(currentEvent.content || []);
|
setHighlightedPhrases(currentEvent.content || []);
|
||||||
}
|
}
|
||||||
@@ -221,7 +222,7 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
if (tip.standalone || !tip.exercise) {
|
if (tip.standalone || !tip.exercise) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<h1 className='text-xl font-bold text-red-600'>The exercise for this tip is not available yet!</h1>
|
<h1 className="text-xl font-bold text-red-600">The exercise for this tip is not available yet!</h1>
|
||||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4 mt-10">
|
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4 mt-10">
|
||||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||||
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
||||||
@@ -230,25 +231,19 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4">
|
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4">
|
||||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||||
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col space-y-4'>
|
<div className="flex flex-col space-y-4">
|
||||||
<div className='flex flex-row items-center space-x-4 py-4'>
|
<div className="flex flex-row items-center space-x-4 py-4">
|
||||||
<button
|
<button
|
||||||
onClick={toggleAutoPlay}
|
onClick={toggleAutoPlay}
|
||||||
className="p-2 bg-blue-500 text-white rounded-full transition-colors duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
className="p-2 bg-blue-500 text-white rounded-full transition-colors duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
||||||
aria-label={isAutoPlaying ? 'Pause' : 'Play'}
|
aria-label={isAutoPlaying ? "Pause" : "Play"}>
|
||||||
>
|
{isAutoPlaying ? <FaRegCircleStop className="w-6 h-6" /> : <FaRegCirclePlay className="w-6 h-6" />}
|
||||||
{isAutoPlaying ? (
|
|
||||||
<FaRegCircleStop className="w-6 h-6" />
|
|
||||||
) : (
|
|
||||||
<FaRegCirclePlay className="w-6 h-6" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -260,21 +255,19 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
|||||||
onMouseUp={handleSliderMouseUp}
|
onMouseUp={handleSliderMouseUp}
|
||||||
onTouchStart={handleSliderMouseDown}
|
onTouchStart={handleSliderMouseDown}
|
||||||
onTouchEnd={handleSliderMouseUp}
|
onTouchEnd={handleSliderMouseUp}
|
||||||
className='flex-grow'
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4'>
|
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||||
<div className='flex-1 bg-white p-6 rounded-lg shadow'>
|
<div className="flex-1 bg-white p-6 rounded-lg shadow">
|
||||||
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
||||||
<div className="mb-4" dangerouslySetInnerHTML={{__html: tip.exercise.question}} />
|
<div className="mb-4" dangerouslySetInnerHTML={{__html: tip.exercise.question}} />
|
||||||
<HighlightContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
<HighlightContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1'>
|
<div className="flex-1">
|
||||||
<div className='bg-gray-50 rounded-lg shadow'>
|
<div className="bg-gray-50 rounded-lg shadow">
|
||||||
<div className='p-6 space-y-4'>
|
<div className="p-6 space-y-4">
|
||||||
<animated.div
|
<animated.div dangerouslySetInnerHTML={{__html: walkthroughHtml}} />
|
||||||
dangerouslySetInnerHTML={{ __html: walkthroughHtml }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|||||||
import {renderSolution} from "@/components/Solutions";
|
import {renderSolution} from "@/components/Solutions";
|
||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import { Exercise, FillBlanksExercise, FillBlanksMCOption, LevelExam, LevelPart, MultipleChoiceExercise, ShuffleMap, UserSolution, WritingExam } from "@/interfaces/exam";
|
import {
|
||||||
|
Exercise,
|
||||||
|
FillBlanksExercise,
|
||||||
|
FillBlanksMCOption,
|
||||||
|
LevelExam,
|
||||||
|
LevelPart,
|
||||||
|
MultipleChoiceExercise,
|
||||||
|
ShuffleMap,
|
||||||
|
UserSolution,
|
||||||
|
WritingExam,
|
||||||
|
} from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {defaultUserSolutions} from "@/utils/exams";
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
import {countExercises} from "@/utils/moduleUtils";
|
import {countExercises} from "@/utils/moduleUtils";
|
||||||
@@ -26,9 +36,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TextComponent({
|
function TextComponent({
|
||||||
part, contextWord, setContextWordLine
|
part,
|
||||||
|
contextWord,
|
||||||
|
setContextWordLine,
|
||||||
}: {
|
}: {
|
||||||
part: LevelPart, contextWord: string | undefined, setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
|
part: LevelPart;
|
||||||
|
contextWord: string | undefined;
|
||||||
|
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>;
|
||||||
}) {
|
}) {
|
||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||||
@@ -42,21 +56,21 @@ function TextComponent({
|
|||||||
const containerWidth = textRef.current.clientWidth;
|
const containerWidth = textRef.current.clientWidth;
|
||||||
setLineHeight(lineHeightValue);
|
setLineHeight(lineHeightValue);
|
||||||
|
|
||||||
const offscreenElement = document.createElement('div');
|
const offscreenElement = document.createElement("div");
|
||||||
offscreenElement.style.position = 'absolute';
|
offscreenElement.style.position = "absolute";
|
||||||
offscreenElement.style.top = '-9999px';
|
offscreenElement.style.top = "-9999px";
|
||||||
offscreenElement.style.left = '-9999px';
|
offscreenElement.style.left = "-9999px";
|
||||||
offscreenElement.style.whiteSpace = 'pre-wrap';
|
offscreenElement.style.whiteSpace = "pre-wrap";
|
||||||
offscreenElement.style.width = `${containerWidth}px`;
|
offscreenElement.style.width = `${containerWidth}px`;
|
||||||
offscreenElement.style.font = computedStyle.font;
|
offscreenElement.style.font = computedStyle.font;
|
||||||
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
offscreenElement.style.lineHeight = computedStyle.lineHeight;
|
||||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
const textContent = textRef.current.textContent || '';
|
const textContent = textRef.current.textContent || "";
|
||||||
textContent.split(/(\s+)/).forEach((word: string) => {
|
textContent.split(/(\s+)/).forEach((word: string) => {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement("span");
|
||||||
span.textContent = word;
|
span.textContent = word;
|
||||||
span.style.display = 'inline-block';
|
span.style.display = "inline-block";
|
||||||
span.style.height = `calc(1em + 16px)`;
|
span.style.height = `calc(1em + 16px)`;
|
||||||
offscreenElement.appendChild(span);
|
offscreenElement.appendChild(span);
|
||||||
});
|
});
|
||||||
@@ -73,9 +87,9 @@ function TextComponent({
|
|||||||
currentLineTop = firstChild.getBoundingClientRect().top;
|
currentLineTop = firstChild.getBoundingClientRect().top;
|
||||||
}
|
}
|
||||||
|
|
||||||
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>("span");
|
||||||
|
|
||||||
spans.forEach(span => {
|
spans.forEach((span) => {
|
||||||
const rect = span.getBoundingClientRect();
|
const rect = span.getBoundingClientRect();
|
||||||
const top = rect.top;
|
const top = rect.top;
|
||||||
|
|
||||||
@@ -85,8 +99,7 @@ function TextComponent({
|
|||||||
lines.push([]);
|
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)) {
|
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
|
||||||
contextWordLine = currentLine;
|
contextWordLine = currentLine;
|
||||||
@@ -115,9 +128,11 @@ function TextComponent({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (textRef.current) {
|
if (textRef.current) {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
resizeObserver.unobserve(textRef.current);
|
resizeObserver.unobserve(textRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [part.context, part.showContextLines, contextWord]);
|
}, [part.context, part.showContextLines, contextWord]);
|
||||||
|
|
||||||
if (typeof part.showContextLines === "undefined") {
|
if (typeof part.showContextLines === "undefined") {
|
||||||
@@ -142,8 +157,13 @@ function TextComponent({
|
|||||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||||
<div className="flex mt-2">
|
<div className="flex mt-2">
|
||||||
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
|
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
|
||||||
{part.context!.split('\n\n').map((line, index) => {
|
{part.context!.split("\n\n").map((line, index) => {
|
||||||
return <p key={`line-${index}`}><span className="mr-6">{index + 1}</span>{line}</p>
|
return (
|
||||||
|
<p key={`line-${index}`}>
|
||||||
|
<span className="mr-6">{index + 1}</span>
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,13 +171,9 @@ function TextComponent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
|
||||||
return Array.isArray(words) && words.every(
|
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
|
||||||
word => word && typeof word === 'object' && 'id' in word && 'options' in word
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function Level({exam, showSolutions = false, onFinish, editing = false}: Props) {
|
export default function Level({exam, showSolutions = false, onFinish, editing = false}: Props) {
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
||||||
@@ -197,7 +213,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getExercise = () => {
|
const getExercise = () => {
|
||||||
if (exerciseIndex === -1) {
|
if (exerciseIndex === -1) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -287,12 +302,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//console.log("Getting another exercise");
|
|
||||||
//setShuffleMaps([]);
|
|
||||||
setCurrentExercise(getExercise());
|
setCurrentExercise(getExercise());
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [partIndex, exerciseIndex]);
|
}, [partIndex, exerciseIndex]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
|
||||||
if (currentExercise && currentExercise.type === "multipleChoice") {
|
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(
|
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
|
||||||
`in line ${originalLineNumber}`,
|
`in line ${originalLineNumber}`,
|
||||||
`in line ${contextWordLine || originalLineNumber}`
|
`in line ${contextWordLine || originalLineNumber}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
|
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
|
||||||
@@ -315,6 +328,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
setContextWord(undefined);
|
setContextWord(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]);
|
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]);
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
@@ -324,7 +338,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
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);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -355,7 +372,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
|
||||||
if (solution) {
|
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) {
|
/*if (exam.shuffle) {
|
||||||
stat.shuffleMaps = shuffleMaps
|
stat.shuffleMaps = shuffleMaps
|
||||||
}*/
|
}*/
|
||||||
@@ -372,7 +389,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
|
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);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -391,7 +411,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
exercisesDone +
|
exercisesDone +
|
||||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
||||||
storeQuestionIndex +
|
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 =
|
|||||||
</h4>
|
</h4>
|
||||||
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
|
||||||
</div>
|
</div>
|
||||||
<TextComponent
|
<TextComponent part={exam.parts[partIndex]} contextWord={contextWord} setContextWordLine={setContextWordLine} />
|
||||||
part={exam.parts[partIndex]}
|
|
||||||
contextWord={contextWord}
|
|
||||||
setContextWordLine={setContextWordLine}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const partLabel = () => {
|
const partLabel = () => {
|
||||||
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
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")
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import {useEffect, useState} from "react";
|
||||||
import { useRouter } from 'next/router';
|
import {useRouter} from "next/router";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import {Tab} from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import {AiOutlineFileSearch} from "react-icons/ai";
|
import {AiOutlineFileSearch} from "react-icons/ai";
|
||||||
import {MdOutlinePlaylistAddCheckCircle, MdOutlineSelfImprovement} from "react-icons/md";
|
import {MdOutlinePlaylistAddCheckCircle, MdOutlineSelfImprovement} from "react-icons/md";
|
||||||
@@ -10,26 +10,26 @@ import clsx from "clsx";
|
|||||||
import Exercise from "@/training/Exercise";
|
import Exercise from "@/training/Exercise";
|
||||||
import TrainingScore from "@/training/TrainingScore";
|
import TrainingScore from "@/training/TrainingScore";
|
||||||
import {ITrainingContent, ITrainingTip} from "@/training/TrainingInterfaces";
|
import {ITrainingContent, ITrainingTip} from "@/training/TrainingInterfaces";
|
||||||
import { Stat, User } from '@/interfaces/user';
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import { ToastContainer } from 'react-toastify';
|
import {ToastContainer} from "react-toastify";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import qs from 'qs';
|
import qs from "qs";
|
||||||
import StatsGridItem from '@/components/StatGridItem';
|
import StatsGridItem from "@/components/StatGridItem";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
||||||
import useAssignments from '@/hooks/useAssignments';
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import useUsers from '@/hooks/useUsers';
|
import useUsers from "@/hooks/useUsers";
|
||||||
import Dropdown from "@/components/Dropdown";
|
import Dropdown from "@/components/Dropdown";
|
||||||
import InfiniteCarousel from '@/components/InfiniteCarousel';
|
import InfiniteCarousel from "@/components/InfiniteCarousel";
|
||||||
import {LuExternalLink} from "react-icons/lu";
|
import {LuExternalLink} from "react-icons/lu";
|
||||||
import { uniqBy } from 'lodash';
|
import {uniqBy} from "lodash";
|
||||||
import { getExamById } from '@/utils/exams';
|
import {getExamById} from "@/utils/exams";
|
||||||
import { convertToUserSolutions } from '@/utils/stats';
|
import {convertToUserSolutions} from "@/utils/stats";
|
||||||
import { sortByModule } from '@/utils/moduleUtils';
|
import {sortByModule} from "@/utils/moduleUtils";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
@@ -79,7 +79,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTrainingContent = async () => {
|
const fetchTrainingContent = async () => {
|
||||||
if (!id || typeof id !== 'string') return;
|
if (!id || typeof id !== "string") return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -88,37 +88,41 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
|
|
||||||
const withExamsStats = {
|
const withExamsStats = {
|
||||||
...trainingContent,
|
...trainingContent,
|
||||||
exams: await Promise.all(trainingContent.exams.map(async (exam) => {
|
exams: await Promise.all(
|
||||||
const stats = await Promise.all(exam.stat_ids.map(async (statId) => {
|
trainingContent.exams.map(async (exam) => {
|
||||||
|
const stats = await Promise.all(
|
||||||
|
exam.stat_ids.map(async (statId) => {
|
||||||
const statResponse = await axios.get<Stat>(`/api/stats/${statId}`);
|
const statResponse = await axios.get<Stat>(`/api/stats/${statId}`);
|
||||||
return statResponse.data;
|
return statResponse.data;
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
return {...exam, stats};
|
return {...exam, stats};
|
||||||
}))
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tips = await axios.get<ITrainingTip[]>('/api/training/walkthrough', {
|
const tips = await axios.get<ITrainingTip[]>("/api/training/walkthrough", {
|
||||||
params: {ids: trainingContent.tip_ids},
|
params: {ids: trainingContent.tip_ids},
|
||||||
paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
|
paramsSerializer: (params) => qs.stringify(params, {arrayFormat: "repeat"}),
|
||||||
});
|
});
|
||||||
setTrainingTips(tips.data);
|
setTrainingTips(tips.data);
|
||||||
setTrainingContent(withExamsStats);
|
setTrainingContent(withExamsStats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
router.push('/training');
|
router.push("/training");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTrainingContent();
|
fetchTrainingContent();
|
||||||
}, [id]);
|
}, [id, router]);
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setCurrentTipIndex((prevIndex) => (prevIndex + 1));
|
setCurrentTipIndex((prevIndex) => prevIndex + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = () => {
|
const handlePrevious = () => {
|
||||||
setCurrentTipIndex((prevIndex) => (prevIndex - 1));
|
setCurrentTipIndex((prevIndex) => prevIndex - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToExam = (examNumber: number) => {
|
const goToExam = (examNumber: number) => {
|
||||||
@@ -145,7 +149,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
router.push("/exercises");
|
router.push("/exercises");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -165,25 +169,26 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
</div>
|
</div>
|
||||||
) : (trainingContent && (
|
) : (
|
||||||
|
trainingContent && (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<span className="bg-gray-200 text-gray-800 px-3 py-0.5 rounded-full font-semibold text-lg mr-2">{trainingContent.exams.length}</span>
|
<span className="bg-gray-200 text-gray-800 px-3 py-0.5 rounded-full font-semibold text-lg mr-2">
|
||||||
|
{trainingContent.exams.length}
|
||||||
|
</span>
|
||||||
<span>Exams Selected</span>
|
<span>Exams Selected</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-[15vh] mb-4'>
|
<div className="h-[15vh] mb-4">
|
||||||
<InfiniteCarousel height="150px"
|
<InfiniteCarousel
|
||||||
overlay={
|
height="150px"
|
||||||
<LuExternalLink size={20} />
|
overlay={<LuExternalLink size={20} />}
|
||||||
}
|
|
||||||
overlayFunc={goToExam}
|
overlayFunc={goToExam}
|
||||||
overlayClassName='bottom-6 right-5 cursor-pointer'
|
overlayClassName="bottom-6 right-5 cursor-pointer">
|
||||||
>
|
|
||||||
{trainingContent.exams.map((exam, examIndex) => (
|
{trainingContent.exams.map((exam, examIndex) => (
|
||||||
<StatsGridItem
|
<StatsGridItem
|
||||||
key={`exam-${examIndex}`}
|
key={`exam-${examIndex}`}
|
||||||
width='380px'
|
width="380px"
|
||||||
height='150px'
|
height="150px"
|
||||||
examNumber={examIndex + 1}
|
examNumber={examIndex + 1}
|
||||||
stats={exam.stats || []}
|
stats={exam.stats || []}
|
||||||
timestamp={exam.date}
|
timestamp={exam.date}
|
||||||
@@ -201,17 +206,14 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
))}
|
))}
|
||||||
</InfiniteCarousel>
|
</InfiniteCarousel>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className='flex flex-row gap-10 -md:flex-col h-full'>
|
<div className="flex flex-row gap-10 -md:flex-col h-full">
|
||||||
<div className="flex flex-col rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full max-h-full">
|
<div className="flex flex-col rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full max-h-full">
|
||||||
<div className="flex flex-row items-center mb-6 gap-1">
|
<div className="flex flex-row items-center mb-6 gap-1">
|
||||||
<MdOutlinePlaylistAddCheckCircle color={"#40A1EA"} size={26} />
|
<MdOutlinePlaylistAddCheckCircle color={"#40A1EA"} size={26} />
|
||||||
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>General Evaluation</h2>
|
<h2 className={`text-xl font-semibold text-[#40A1EA]`}>General Evaluation</h2>
|
||||||
</div>
|
</div>
|
||||||
<TrainingScore
|
<TrainingScore trainingContent={trainingContent} gridView={false} />
|
||||||
trainingContent={trainingContent}
|
|
||||||
gridView={false}
|
|
||||||
/>
|
|
||||||
<div className="w-full h-px bg-[#D9D9D929] my-6"></div>
|
<div className="w-full h-px bg-[#D9D9D929] my-6"></div>
|
||||||
<div className="flex flex-row gap-2 items-center mb-6">
|
<div className="flex flex-row gap-2 items-center mb-6">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -219,18 +221,23 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
<rect width="24" height="24" fill="#D9D9D9" />
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_112_168)">
|
<g mask="url(#mask0_112_168)">
|
||||||
<path d="M4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V7H4V19H19V21H4ZM8 17C7.45 17 6.97917 16.8042 6.5875 16.4125C6.19583 16.0208 6 15.55 6 15V3H23V15C23 15.55 22.8042 16.0208 22.4125 16.4125C22.0208 16.8042 21.55 17 21 17H8ZM8 15H21V5H8V15ZM10 12H14V7H10V12ZM15 12H19V10H15V12ZM15 9H19V7H15V9Z" fill="#53B2F9" />
|
<path
|
||||||
|
d="M4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V7H4V19H19V21H4ZM8 17C7.45 17 6.97917 16.8042 6.5875 16.4125C6.19583 16.0208 6 15.55 6 15V3H23V15C23 15.55 22.8042 16.0208 22.4125 16.4125C22.0208 16.8042 21.55 17 21 17H8ZM8 15H21V5H8V15ZM10 12H14V7H10V12ZM15 12H19V10H15V12ZM15 9H19V7H15V9Z"
|
||||||
|
fill="#53B2F9"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Performance Breakdown by Exam:</h3>
|
<h3 className="text-xl font-semibold text-[#40A1EA]">Performance Breakdown by Exam:</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className='overflow-auto scrollbar-hide flex-grow'>
|
<ul className="overflow-auto scrollbar-hide flex-grow">
|
||||||
{trainingContent.exams.flatMap((exam, index) => (
|
{trainingContent.exams.flatMap((exam, index) => (
|
||||||
<li key={index} className="flex flex-col mb-2 bg-[#22E1B30F] p-4 rounded-xl border">
|
<li key={index} className="flex flex-col mb-2 bg-[#22E1B30F] p-4 rounded-xl border">
|
||||||
<div className="flex flex-row font-semibold border-b-2 border-[#D9D9D929] text-[#22E1B3] mb-2">
|
<div className="flex flex-row font-semibold border-b-2 border-[#D9D9D929] text-[#22E1B3] mb-2">
|
||||||
<div className='flex items-center border-r-2 border-[#D9D9D929] pr-2'>
|
<div className="flex items-center border-r-2 border-[#D9D9D929] pr-2">
|
||||||
<span className='mr-1'>Exam</span>
|
<span className="mr-1">Exam</span>
|
||||||
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm">{index + 1}</span>
|
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="pl-2">{exam.score}%</span>
|
<span className="pl-2">{exam.score}%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,7 +250,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full">
|
<div className="flex flex-col rounded-3xl p-6 w-1/2 shadow-training-inset -md:w-full">
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row items-center mb-4 gap-1">
|
<div className="flex flex-row items-center mb-4 gap-1">
|
||||||
<AiOutlineFileSearch color="#40A1EA" size={24} />
|
<AiOutlineFileSearch color="#40A1EA" size={24} />
|
||||||
<h3 className="text-xl font-semibold text-[#40A1EA]">Identified Weak Areas</h3>
|
<h3 className="text-xl font-semibold text-[#40A1EA]">Identified Weak Areas</h3>
|
||||||
@@ -257,12 +264,11 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
key={index}
|
key={index}
|
||||||
className={({selected}) =>
|
className={({selected}) =>
|
||||||
clsx(
|
clsx(
|
||||||
'text-[#53B2F9] pb-2 border-b-2',
|
"text-[#53B2F9] pb-2 border-b-2",
|
||||||
'focus:outline-none',
|
"focus:outline-none",
|
||||||
selected ? 'border-[#1B78BE]' : 'border-[#1B78BE0F]'
|
selected ? "border-[#1B78BE]" : "border-[#1B78BE0F]",
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{x.area}
|
{x.area}
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
@@ -270,10 +276,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
{trainingContent.weak_areas.map((x, index) => (
|
{trainingContent.weak_areas.map((x, index) => (
|
||||||
<Tab.Panel
|
<Tab.Panel key={index} className="p-3 bg-[#FBFBFB] rounded-lg border border-[#0000000F]">
|
||||||
key={index}
|
|
||||||
className="p-3 bg-[#FBFBFB] rounded-lg border border-[#0000000F]"
|
|
||||||
>
|
|
||||||
<p>{x.comment}</p>
|
<p>{x.comment}</p>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
))}
|
))}
|
||||||
@@ -288,15 +291,23 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-grow bg-[#FBFBFB] border rounded-xl p-4">
|
<div className="flex flex-grow bg-[#FBFBFB] border rounded-xl p-4">
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row items-center gap-1 mb-4">
|
<div className="flex flex-row items-center gap-1 mb-4">
|
||||||
<div className="flex items-center justify-center w-[48px] h-[48px]">
|
<div className="flex items-center justify-center w-[48px] h-[48px]">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<mask id="mask0_112_445" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
<mask id="mask0_112_445" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
<rect width="24" height="24" fill="#D9D9D9" />
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_112_445)">
|
<g mask="url(#mask0_112_445)">
|
||||||
<path d="M6 17H11V15H6V17ZM16 17H18V15H16V17ZM6 13H11V11H6V13ZM16 13H18V7H16V13ZM6 9H11V7H6V9ZM4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V5C2 4.45 2.19583 3.97917 2.5875 3.5875C2.97917 3.19583 3.45 3 4 3H20C20.55 3 21.0208 3.19583 21.4125 3.5875C21.8042 3.97917 22 4.45 22 5V19C22 19.55 21.8042 20.0208 21.4125 20.4125C21.0208 20.8042 20.55 21 20 21H4ZM4 19H20V5H4V19Z" fill="#1C1B1F" />
|
<path
|
||||||
|
d="M6 17H11V15H6V17ZM16 17H18V15H16V17ZM6 13H11V11H6V13ZM16 13H18V7H16V13ZM6 9H11V7H6V9ZM4 21C3.45 21 2.97917 20.8042 2.5875 20.4125C2.19583 20.0208 2 19.55 2 19V5C2 4.45 2.19583 3.97917 2.5875 3.5875C2.97917 3.19583 3.45 3 4 3H20C20.55 3 21.0208 3.19583 21.4125 3.5875C21.8042 3.97917 22 4.45 22 5V19C22 19.55 21.8042 20.0208 21.4125 20.4125C21.0208 20.8042 20.55 21 20 21H4ZM4 19H20V5H4V19Z"
|
||||||
|
fill="#1C1B1F"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,12 +316,16 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
<ul className="flex flex-col flex-grow space-y-4 pb-2 overflow-y-auto scrollbar-hide">
|
<ul className="flex flex-col flex-grow space-y-4 pb-2 overflow-y-auto scrollbar-hide">
|
||||||
{trainingContent.exams.map((exam, index) => (
|
{trainingContent.exams.map((exam, index) => (
|
||||||
<li key={index} className="border rounded-lg bg-white">
|
<li key={index} className="border rounded-lg bg-white">
|
||||||
<Dropdown title={
|
<Dropdown
|
||||||
<div className='flex flex-row items-center'>
|
title={
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
<span className="mr-1">Exam</span>
|
<span className="mr-1">Exam</span>
|
||||||
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm mt-0.5">{index + 1}</span>
|
<span className="font-semibold bg-gray-200 text-gray-800 px-2 rounded-full text-sm mt-0.5">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
} open={index == 0}>
|
}
|
||||||
|
open={index == 0}>
|
||||||
<span>{exam.detailed_summary}</span>
|
<span>{exam.detailed_summary}</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</li>
|
</li>
|
||||||
@@ -337,21 +352,20 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
disabled={currentTipIndex == (trainingTips.length - 1)}
|
disabled={currentTipIndex == trainingTips.length - 1}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TrainingContent;
|
export default TrainingContent;
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", handleRouteChange);
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [router.events, setTrainingStats]);
|
}, [router.events, setTrainingStats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -104,6 +105,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isNewContentLoading]);
|
}, [isNewContentLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user