Merged in feature/level-file-upload (pull request #85)
Feature/level file upload
This commit is contained in:
@@ -228,7 +228,6 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={
|
||||||
exam && exam.module === "level" &&
|
exam && exam.module === "level" &&
|
||||||
typeof exam.parts[0].intro === "string" &&
|
|
||||||
partIndex === 0 &&
|
partIndex === 0 &&
|
||||||
questionIndex === 0
|
questionIndex === 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, setAnswers])
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
if (event.over && event.over.id.toString().startsWith("droppable")) {
|
if (event.over && event.over.id.toString().startsWith("droppable")) {
|
||||||
|
|||||||
@@ -174,8 +174,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
|
|||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"
|
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"
|
||||||
disabled={
|
disabled={
|
||||||
exam && exam.module === "level" &&
|
exam && exam.module === "level" &&
|
||||||
typeof exam.parts[0].intro === "string" &&
|
|
||||||
partIndex === 0 &&
|
partIndex === 0 &&
|
||||||
questionIndex === 0
|
questionIndex === 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
const [answers, setAnswers] = useState<{id: string; solution: "true" | "false" | "not_given"}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{id: string; solution: "true" | "false" | "not_given"}[]>(userSolutions);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
@@ -28,6 +29,12 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
return {total, correct, missing};
|
return {total, correct, missing};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, setAnswers])
|
||||||
|
|
||||||
|
|
||||||
const toggleAnswer = (solution: "true" | "false" | "not_given", questionId: string) => {
|
const toggleAnswer = (solution: "true" | "false" | "not_given", questionId: string) => {
|
||||||
const answer = answers.find((x) => x.id === questionId);
|
const answer = answers.find((x) => x.id === questionId);
|
||||||
if (answer && answer.solution === solution) {
|
if (answer && answer.solution === solution) {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function Blank({
|
|||||||
export default function WriteBlanks({id, prompt, type, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
export default function WriteBlanks({id, prompt, type, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
||||||
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const {hasExamEnded, setCurrentSolution} = useExamStore((state) => state);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
@@ -70,6 +70,11 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
return {total, correct, missing};
|
return {total, correct, missing};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, setAnswers])
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-base leading-5">
|
<span className="text-base leading-5">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Fragment, ReactNode, useCallback, useState } from "react";
|
|||||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
||||||
import ProgressBar from "../Low/ProgressBar";
|
import ProgressBar from "../Low/ProgressBar";
|
||||||
import Timer from "./Timer";
|
import Timer from "./Timer";
|
||||||
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
import { Exam, Exercise, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
||||||
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
@@ -24,6 +24,7 @@ interface Props {
|
|||||||
partLabel?: string;
|
partLabel?: string;
|
||||||
showTimer?: boolean;
|
showTimer?: boolean;
|
||||||
showSolutions?: boolean;
|
showSolutions?: boolean;
|
||||||
|
currentExercise?: Exercise;
|
||||||
runOnClick?: ((questionIndex: number) => void) | undefined;
|
runOnClick?: ((questionIndex: number) => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +69,9 @@ export default function ModuleTitle({
|
|||||||
if (!isMultipleChoiceLevelExercise() && !userSolutions) return null;
|
if (!isMultipleChoiceLevelExercise() && !userSolutions) return null;
|
||||||
|
|
||||||
const currentExercise = (exam as LevelExam).parts[partIndex!].exercises[examExerciseIndex] as MultipleChoiceExercise;
|
const currentExercise = (exam as LevelExam).parts[partIndex!].exercises[examExerciseIndex] as MultipleChoiceExercise;
|
||||||
const userSolution = userSolutions!.find((x) => x.exercise == currentExercise.id)!;
|
const userSolution = userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!;
|
||||||
const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question));
|
const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question.toString()));
|
||||||
const exerciseOffset = currentExercise.questions[0].id;
|
const exerciseOffset = Number(currentExercise.questions[0].id);
|
||||||
const lastExercise = exerciseOffset + (currentExercise.questions.length - 1);
|
const lastExercise = exerciseOffset + (currentExercise.questions.length - 1);
|
||||||
|
|
||||||
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
|
const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => {
|
||||||
@@ -96,10 +97,10 @@ export default function ModuleTitle({
|
|||||||
<div className="grid grid-cols-5 gap-3 px-4 py-2">
|
<div className="grid grid-cols-5 gap-3 px-4 py-2">
|
||||||
{currentExercise.questions.map((_, index) => {
|
{currentExercise.questions.map((_, index) => {
|
||||||
const questionNumber = exerciseOffset + index;
|
const questionNumber = exerciseOffset + index;
|
||||||
const isAnswered = answeredQuestions.has(questionNumber);
|
const isAnswered = answeredQuestions.has(questionNumber.toString());
|
||||||
const solution = currentExercise.questions.find((x) => x.id == questionNumber)!.solution;
|
const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution;
|
||||||
|
|
||||||
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question == questionNumber)?.option;
|
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
|
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function FillBlanksSolutions({
|
|||||||
onBack,
|
onBack,
|
||||||
}: FillBlanksExercise & CommonProps) {
|
}: FillBlanksExercise & CommonProps) {
|
||||||
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
||||||
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
||||||
|
|
||||||
const correctUserSolutions = storeUserSolutions.find(
|
const correctUserSolutions = storeUserSolutions.find(
|
||||||
(solution) => solution.exercise === id
|
(solution) => solution.exercise === id
|
||||||
@@ -201,7 +202,14 @@ export default function FillBlanksSolutions({
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
onClick={() => onBack({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={
|
||||||
|
exam &&
|
||||||
|
typeof partIndex !== "undefined" &&
|
||||||
|
exam.module === "level" &&
|
||||||
|
questionIndex === 0 &&
|
||||||
|
partIndex === 0
|
||||||
|
}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Icon from "@mdi/react";
|
|||||||
import {Fragment} from "react";
|
import {Fragment} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import Xarrow from "react-xarrows";
|
import Xarrow from "react-xarrows";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
function QuestionSolutionArea({
|
function QuestionSolutionArea({
|
||||||
question,
|
question,
|
||||||
@@ -61,6 +62,8 @@ export default function MatchSentencesSolutions({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}: MatchSentencesExercise & CommonProps) {
|
}: MatchSentencesExercise & CommonProps) {
|
||||||
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = sentences.length;
|
const total = sentences.length;
|
||||||
const correct = userSolutions.filter(
|
const correct = userSolutions.filter(
|
||||||
@@ -112,7 +115,14 @@ export default function MatchSentencesSolutions({
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={
|
||||||
|
exam &&
|
||||||
|
typeof partIndex !== "undefined" &&
|
||||||
|
exam.module === "level" &&
|
||||||
|
questionIndex === 0 &&
|
||||||
|
partIndex === 0
|
||||||
|
}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
exam &&
|
exam &&
|
||||||
typeof partIndex !== "undefined" &&
|
typeof partIndex !== "undefined" &&
|
||||||
exam.module === "level" &&
|
exam.module === "level" &&
|
||||||
typeof exam.parts[0].intro === "string" &&
|
|
||||||
questionIndex === 0 &&
|
questionIndex === 0 &&
|
||||||
partIndex === 0
|
partIndex === 0
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import reactStringReplace from "react-string-replace";
|
|||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import {Fragment} from "react";
|
import {Fragment} from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
type Solution = "true" | "false" | "not_given";
|
type Solution = "true" | "false" | "not_given";
|
||||||
|
|
||||||
export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
||||||
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length || 0;
|
const total = questions.length || 0;
|
||||||
const correct = userSolutions.filter(
|
const correct = userSolutions.filter(
|
||||||
@@ -121,7 +124,14 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={
|
||||||
|
exam &&
|
||||||
|
typeof partIndex !== "undefined" &&
|
||||||
|
exam.module === "level" &&
|
||||||
|
questionIndex === 0 &&
|
||||||
|
partIndex === 0
|
||||||
|
}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import reactStringReplace from "react-string-replace";
|
|||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
function Blank({
|
function Blank({
|
||||||
id,
|
id,
|
||||||
@@ -71,6 +72,8 @@ export default function WriteBlanksSolutions({
|
|||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
}: WriteBlanksExercise & CommonProps) {
|
}: WriteBlanksExercise & CommonProps) {
|
||||||
|
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = userSolutions.filter(
|
const correct = userSolutions.filter(
|
||||||
@@ -142,7 +145,14 @@ export default function WriteBlanksSolutions({
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={
|
||||||
|
exam &&
|
||||||
|
typeof partIndex !== "undefined" &&
|
||||||
|
exam.module === "level" &&
|
||||||
|
questionIndex === 0 &&
|
||||||
|
partIndex === 0
|
||||||
|
}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { LevelPart, UserSolution } from "@/interfaces/exam";
|
import { LevelPart, UserSolution } from "@/interfaces/exam";
|
||||||
|
import clsx from "clsx";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
||||||
|
|
||||||
@@ -21,10 +22,10 @@ const PartDivider: React.FC<Props> = ({ partIndex, part, onNext }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-3/6 h-fit border bg-white rounded-3xl p-12 gap-8">
|
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", part.intro ? "w-3/6" : "items-center my-auto")}>
|
||||||
{/** only level for now */}
|
{/** only level for now */}
|
||||||
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{`Part ${partIndex + 1}`}</p></div>
|
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{part.intro ? `Part ${partIndex + 1}` : "Placement Test"}</p></div>
|
||||||
{part.intro!.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip">{x}</p>)}
|
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip">{x}</p>)}
|
||||||
<div className="flex items-center justify-center mt-4">
|
<div className="flex items-center justify-center mt-4">
|
||||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
||||||
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
||||||
|
|||||||
@@ -31,15 +31,23 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
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) => {
|
const lines = textContent.split(/\n/).map(line =>
|
||||||
const span = document.createElement('span');
|
line.split(/(\s+)/).map(word => {
|
||||||
span.textContent = word;
|
const span = document.createElement('span');
|
||||||
offscreenElement.appendChild(span);
|
span.textContent = word;
|
||||||
|
return span;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append all spans to offscreenElement
|
||||||
|
lines.forEach(line => {
|
||||||
|
line.forEach(span => offscreenElement.appendChild(span));
|
||||||
|
offscreenElement.appendChild(document.createElement('br'));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(offscreenElement);
|
document.body.appendChild(offscreenElement);
|
||||||
|
|
||||||
const lines: string[][] = [[]];
|
const processedLines: string[][] = [[]];
|
||||||
let currentLine = 1;
|
let currentLine = 1;
|
||||||
let currentLineTop: number | undefined;
|
let currentLineTop: number | undefined;
|
||||||
let contextWordLine: number | null = null;
|
let contextWordLine: number | null = null;
|
||||||
@@ -58,16 +66,16 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
|||||||
if (currentLineTop !== undefined && top > currentLineTop) {
|
if (currentLineTop !== undefined && top > currentLineTop) {
|
||||||
currentLine++;
|
currentLine++;
|
||||||
currentLineTop = top;
|
currentLineTop = top;
|
||||||
lines.push([]);
|
processedLines.push([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines[lines.length - 1].push(span.textContent?.trim() || '');
|
processedLines[processedLines.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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setLineNumbers(lines.map((_, index) => index + 1));
|
setLineNumbers(processedLines.map((_, index) => index + 1));
|
||||||
if (contextWordLine) {
|
if (contextWordLine) {
|
||||||
setContextWordLine(contextWordLine);
|
setContextWordLine(contextWordLine);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Button from "@/components/Low/Button";
|
|||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
import { renderSolution } from "@/components/Solutions";
|
import { renderSolution } from "@/components/Solutions";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
import { countExercises } from "@/utils/moduleUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -68,8 +68,16 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentExercise, setCurrentExercise] = useState<Exercise>(exam.parts[0].exercises[0]);
|
const [currentExercise, setCurrentExercise] = useState<Exercise | undefined>(undefined);
|
||||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
|
||||||
|
const [startNow, setStartNow] = useState<boolean>(true && !showSolutions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) {
|
||||||
|
setCurrentExercise(exam.parts[0].exercises[0]);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentExercise, partIndex, exerciseIndex]);
|
||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||||
|
|
||||||
@@ -108,10 +116,9 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
|
||||||
exercise = {
|
exercise = {
|
||||||
...exercise,
|
...exercise,
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [],
|
||||||
};
|
};
|
||||||
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles);
|
||||||
|
|
||||||
return exercise;
|
return exercise;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,7 +169,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways) {
|
if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways) {
|
||||||
modalKwargs();
|
modalKwargs();
|
||||||
setShowQuestionsModal(true);
|
setShowQuestionsModal(true);
|
||||||
}
|
}
|
||||||
@@ -219,11 +226,22 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
if (i == (partIndex - 1)) break;
|
if (i == (partIndex - 1)) break;
|
||||||
for (let j = 0; j < exam.parts[i].exercises.length; j++) {
|
for (let j = 0; j < exam.parts[i].exercises.length; j++) {
|
||||||
const exercise = exam.parts[i].exercises[j];
|
const exercise = exam.parts[i].exercises[j];
|
||||||
if (exercise.type === "multipleChoice") {
|
switch(exercise.type) {
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 })
|
case 'multipleChoice':
|
||||||
}
|
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 })
|
||||||
if (exercise.type === "fillBlanks") {
|
break;
|
||||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 })
|
case 'fillBlanks':
|
||||||
|
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 })
|
||||||
|
break;
|
||||||
|
case 'writeBlanks':
|
||||||
|
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.solutions.length - 1 })
|
||||||
|
break;
|
||||||
|
case 'matchSentences':
|
||||||
|
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.sentences.length - 1})
|
||||||
|
break;
|
||||||
|
case 'trueFalse':
|
||||||
|
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1})
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,28 +251,12 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calculateExerciseIndex = () => {
|
const calculateExerciseIndex = () => {
|
||||||
if (exam.parts[0].intro) {
|
return exam.parts.reduce((acc, curr, index) => {
|
||||||
return exam.parts.reduce((acc, curr, index) => {
|
if (index < partIndex) {
|
||||||
if (index < partIndex) {
|
return acc + countExercises(curr.exercises)
|
||||||
return acc + countExercises(curr.exercises)
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, 0) + (questionIndex + 1);
|
|
||||||
} else {
|
|
||||||
if (partIndex === 0) {
|
|
||||||
return (
|
|
||||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + questionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
|
return acc;
|
||||||
const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0);
|
}, 0) + (questionIndex + 1);
|
||||||
return (
|
|
||||||
exercisesDone +
|
|
||||||
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
|
|
||||||
questionIndex
|
|
||||||
+ multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderText = () => (
|
const renderText = () => (
|
||||||
@@ -320,11 +322,17 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
const answeredEveryQuestion = (partIndex: number) => {
|
const answeredEveryQuestion = (partIndex: number) => {
|
||||||
return exam.parts[partIndex].exercises.every((exercise) => {
|
return exam.parts[partIndex].exercises.every((exercise) => {
|
||||||
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
|
||||||
if (exercise.type === "multipleChoice") {
|
switch(exercise.type) {
|
||||||
return userSolution?.solutions.length === exercise.questions.length;
|
case 'multipleChoice':
|
||||||
}
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
if (exercise.type === "fillBlanks") {
|
case 'fillBlanks':
|
||||||
return userSolution?.solutions.length === exercise.words.length;
|
return userSolution?.solutions.length === exercise.words.length;
|
||||||
|
case 'writeBlanks':
|
||||||
|
return userSolution?.solutions.length === exercise.solutions.length;
|
||||||
|
case 'matchSentences':
|
||||||
|
return userSolution?.solutions.length === exercise.sentences.length;
|
||||||
|
case 'trueFalse':
|
||||||
|
return userSolution?.solutions.length === exercise.questions.length;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -405,8 +413,8 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
<>
|
<>
|
||||||
{exam.parts[partIndex].context && renderText()}
|
{exam.parts[partIndex].context && renderText()}
|
||||||
{(showSolutions || editing) ?
|
{(showSolutions || editing) ?
|
||||||
renderSolution(currentExercise, nextExercise, previousExercise) :
|
currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) :
|
||||||
renderExercise(currentExercise, exam.id, next, previousExercise)
|
currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise)
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -419,10 +427,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
||||||
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||||
{
|
{
|
||||||
!(partIndex === 0 && questionIndex === 0 && showPartDivider) &&
|
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
||||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
|
||||||
}
|
}
|
||||||
{exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : (
|
{(showPartDivider || startNow) ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} /> : (
|
||||||
<>
|
<>
|
||||||
{exam.parts[0].intro && (
|
{exam.parts[0].intro && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -470,33 +478,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
|||||||
)}>
|
)}>
|
||||||
{memoizedRender}
|
{memoizedRender}
|
||||||
</div>
|
</div>
|
||||||
{/*exerciseIndex === -1 && partIndex > 0 && (
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
|
|
||||||
setPartIndex(partIndex - 1);
|
|
||||||
}}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={
|
|
||||||
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
|
|
||||||
typeof exam.parts[0].intro === "string" && questionIndex === 0}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)*/}
|
|
||||||
{exerciseIndex === -1 && partIndex === 0 && (
|
|
||||||
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
|
|
||||||
Start now
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ export default function ExamPage({page, user}: Props) {
|
|||||||
}, [statsAwaitingEvaluation]);
|
}, [statsAwaitingEvaluation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (exam && exam.module === "level" && exam.parts[0].intro && !showSolutions) setBgColor("bg-ielts-level-light");
|
if (exam && exam.module === "level" && !showSolutions) setBgColor("bg-ielts-level-light");
|
||||||
}, [exam, showSolutions, setBgColor]);
|
}, [exam, showSolutions, setBgColor]);
|
||||||
|
|
||||||
const checkIfStatsHaveBeenEvaluated = (ids: string[]) => {
|
const checkIfStatsHaveBeenEvaluated = (ids: string[]) => {
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export const countExercises = (exercises: Exercise[]) => {
|
|||||||
if (e.type === "multipleChoice") return e.questions.length;
|
if (e.type === "multipleChoice") return e.questions.length;
|
||||||
if (e.type === "interactiveSpeaking") return e.prompts.length;
|
if (e.type === "interactiveSpeaking") return e.prompts.length;
|
||||||
if (e.type === "fillBlanks") return e.words.length;
|
if (e.type === "fillBlanks") return e.words.length;
|
||||||
|
if (e.type === "writeBlanks") return e.solutions.length;
|
||||||
|
if (e.type === "matchSentences") return e.sentences.length;
|
||||||
|
if (e.type === "trueFalse") return e.questions.length;
|
||||||
return 1;
|
return 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user