Made it so when the timer ends, the module ends
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {FillBlanksExercise} from "@/interfaces/exam";
|
import {FillBlanksExercise} from "@/interfaces/exam";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
@@ -79,10 +80,17 @@ export default function FillBlanks({
|
|||||||
const [currentBlankId, setCurrentBlankId] = useState<string>();
|
const [currentBlankId, setCurrentBlankId] = useState<string>();
|
||||||
const [isDrawerShowing, setIsDrawerShowing] = useState(false);
|
const [isDrawerShowing, setIsDrawerShowing] = useState(false);
|
||||||
|
|
||||||
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => setIsDrawerShowing(!!currentBlankId), 100);
|
setTimeout(() => setIsDrawerShowing(!!currentBlankId), 100);
|
||||||
}, [currentBlankId]);
|
}, [currentBlankId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = answers.filter((x) => solutions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
const correct = answers.filter((x) => solutions.find((y) => x.id === y.id)?.solution === x.solution.toLowerCase() || false).length;
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import {MatchSentencesExercise} from "@/interfaces/exam";
|
|||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import LineTo from "react-lineto";
|
import LineTo from "react-lineto";
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
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";
|
||||||
|
|
||||||
export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
||||||
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
const [selectedQuestion, setSelectedQuestion] = useState<string>();
|
||||||
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 calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = sentences.length;
|
const total = sentences.length;
|
||||||
const correct = answers.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
const correct = answers.filter((x) => sentences.find((y) => y.id === x.question)?.solution === x.option || false).length;
|
||||||
@@ -27,9 +30,11 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
setSelectedQuestion(undefined);
|
setSelectedQuestion(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSentenceColor = (id: string) => {
|
useEffect(() => {
|
||||||
return sentences.find((x) => x.id === id)?.color || "";
|
console.log({hasExamEnded});
|
||||||
};
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -44,7 +49,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex gap-8 w-full items-center justify-between bg-mti-gray-smoke rounded-xl px-24 py-6">
|
<div className="flex gap-8 w-full items-center justify-between bg-mti-gray-smoke rounded-xl px-24 py-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{sentences.map(({sentence, id, color}) => (
|
{sentences.map(({sentence, id}) => (
|
||||||
<div key={`question_${id}`} className="flex items-center justify-end gap-2 cursor-pointer">
|
<div key={`question_${id}`} className="flex items-center justify-end gap-2 cursor-pointer">
|
||||||
<span>{sentence} </span>
|
<span>{sentence} </span>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
|
import {MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
@@ -51,6 +52,13 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
||||||
const [questionIndex, setQuestionIndex] = useState(0);
|
const [questionIndex, setQuestionIndex] = useState(0);
|
||||||
|
|
||||||
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const onSelectOption = (option: string) => {
|
const onSelectOption = (option: string) => {
|
||||||
const question = questions[questionIndex];
|
const question = questions[questionIndex];
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
|
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {Fragment, useEffect, useState} from "react";
|
|||||||
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
|
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||||
@@ -15,6 +16,20 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
|
|||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
|
|
||||||
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) {
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let recordingInterval: NodeJS.Timer | undefined = undefined;
|
let recordingInterval: NodeJS.Timer | undefined = undefined;
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -48,6 +49,13 @@ 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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
const correct = answers.filter(
|
const correct = answers.filter(
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
|
||||||
import {WritingExercise} from "@/interfaces/exam";
|
import {WritingExercise} from "@/interfaces/exam";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import {CommonProps} from ".";
|
import {CommonProps} from ".";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import {Fragment, useEffect, useState} from "react";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
export default function Writing({id, prompt, info, type, wordCounter, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
export default function Writing({id, prompt, info, type, wordCounter, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [inputText, setInputText] = useState(userSolutions.length === 1 ? userSolutions[0].solution : "");
|
const [inputText, setInputText] = useState(userSolutions.length === 1 ? userSolutions[0].solution : "");
|
||||||
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
||||||
|
|
||||||
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const words = inputText.split(" ").filter((x) => x !== "");
|
const words = inputText.split(" ").filter((x) => x !== "");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
import {moduleLabels} from "@/utils/moduleUtils";
|
import {moduleLabels} from "@/utils/moduleUtils";
|
||||||
import {ReactNode, useEffect, useState} from "react";
|
import {ReactNode, useEffect, useState} from "react";
|
||||||
import {BsBook, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
import {BsBook, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
|
||||||
@@ -15,6 +16,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false}: Props) {
|
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false}: Props) {
|
||||||
const [timer, setTimer] = useState(minTimer * 60);
|
const [timer, setTimer] = useState(minTimer * 60);
|
||||||
|
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!disableTimer) {
|
if (!disableTimer) {
|
||||||
@@ -26,6 +28,10 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
|||||||
}
|
}
|
||||||
}, [disableTimer, minTimer]);
|
}, [disableTimer, minTimer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timer <= 0) setHasExamEnded(true);
|
||||||
|
}, [setHasExamEnded, timer]);
|
||||||
|
|
||||||
const moduleIcon: {[key in Module]: ReactNode} = {
|
const moduleIcon: {[key in Module]: ReactNode} = {
|
||||||
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||||
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {ListeningExam, UserSolution} from "@/interfaces/exam";
|
import {ListeningExam, UserSolution} from "@/interfaces/exam";
|
||||||
import {useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {mdiArrowRight} from "@mdi/js";
|
import {mdiArrowRight} from "@mdi/js";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -10,6 +10,8 @@ import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|||||||
import AudioPlayer from "@/components/Low/AudioPlayer";
|
import AudioPlayer from "@/components/Low/AudioPlayer";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ListeningExam;
|
exam: ListeningExam;
|
||||||
@@ -20,9 +22,11 @@ interface Props {
|
|||||||
export default function Listening({exam, showSolutions = false, onFinish}: Props) {
|
export default function Listening({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
const [timesListened, setTimesListened] = useState(0);
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
|
||||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||||
|
|
||||||
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
const confirmFinishModule = (keepGoing?: boolean) => {
|
const confirmFinishModule = (keepGoing?: boolean) => {
|
||||||
if (!keepGoing) {
|
if (!keepGoing) {
|
||||||
setShowBlankModal(false);
|
setShowBlankModal(false);
|
||||||
@@ -37,12 +41,12 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
|||||||
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
if (exerciseIndex + 1 < exam.exercises.length && !hasExamEnded) {
|
||||||
setExerciseIndex((prev) => prev + 1);
|
setExerciseIndex((prev) => prev + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userSolutions.map((x) => x.score.missing).every((x) => x === 0)) {
|
if (!userSolutions.map((x) => x.score.missing).every((x) => x === 0) && !hasExamEnded) {
|
||||||
setShowBlankModal(true);
|
setShowBlankModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|||||||
import {Divider} from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ReadingExam;
|
exam: ReadingExam;
|
||||||
@@ -80,9 +82,11 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
|
|||||||
export default function Reading({exam, showSolutions = false, onFinish}: Props) {
|
export default function Reading({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
const [exerciseIndex, setExerciseIndex] = useState(-1);
|
||||||
const [showTextModal, setShowTextModal] = useState(false);
|
const [showTextModal, setShowTextModal] = useState(false);
|
||||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
|
||||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||||
|
|
||||||
|
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]);
|
||||||
|
|
||||||
const confirmFinishModule = (keepGoing?: boolean) => {
|
const confirmFinishModule = (keepGoing?: boolean) => {
|
||||||
if (!keepGoing) {
|
if (!keepGoing) {
|
||||||
setShowBlankModal(false);
|
setShowBlankModal(false);
|
||||||
@@ -97,16 +101,18 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
|
|||||||
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
if (exerciseIndex + 1 < exam.exercises.length && !hasExamEnded) {
|
||||||
setExerciseIndex((prev) => prev + 1);
|
setExerciseIndex((prev) => prev + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userSolutions.map((x) => x.score.missing).every((x) => x === 0)) {
|
if (!userSolutions.map((x) => x.score.missing).every((x) => x === 0) && !hasExamEnded) {
|
||||||
setShowBlankModal(true);
|
setShowBlankModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHasExamEnded(false);
|
||||||
|
|
||||||
if (solution) {
|
if (solution) {
|
||||||
onFinish(
|
onFinish(
|
||||||
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "reading", exam: exam.id})),
|
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "reading", exam: exam.id})),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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 {UserSolution, SpeakingExam} from "@/interfaces/exam";
|
import {UserSolution, SpeakingExam} from "@/interfaces/exam";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
import {mdiArrowRight} from "@mdi/js";
|
import {mdiArrowRight} from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -17,7 +19,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function Speaking({exam, showSolutions = false, onFinish}: Props) {
|
export default function Speaking({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(0);
|
const [exerciseIndex, setExerciseIndex] = useState(0);
|
||||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
if (solution) {
|
if (solution) {
|
||||||
@@ -29,6 +31,8 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exerciseIndex >= exam.exercises.length) return;
|
||||||
|
|
||||||
if (solution) {
|
if (solution) {
|
||||||
onFinish(
|
onFinish(
|
||||||
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "speaking", exam: exam.id})),
|
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "speaking", exam: exam.id})),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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 {UserSolution, WritingExam} from "@/interfaces/exam";
|
import {UserSolution, WritingExam} from "@/interfaces/exam";
|
||||||
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
import {mdiArrowRight} from "@mdi/js";
|
import {mdiArrowRight} from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -17,7 +19,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function Writing({exam, showSolutions = false, onFinish}: Props) {
|
export default function Writing({exam, showSolutions = false, onFinish}: Props) {
|
||||||
const [exerciseIndex, setExerciseIndex] = useState(0);
|
const [exerciseIndex, setExerciseIndex] = useState(0);
|
||||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
if (solution) {
|
if (solution) {
|
||||||
@@ -29,6 +31,8 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exerciseIndex >= exam.exercises.length) return;
|
||||||
|
|
||||||
if (solution) {
|
if (solution) {
|
||||||
onFinish(
|
onFinish(
|
||||||
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "writing", exam: exam.id})),
|
[...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "writing", exam: exam.id})),
|
||||||
|
|||||||
@@ -32,7 +32,15 @@ async function login(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const userId = userCredentials.user.uid;
|
const userId = userCredentials.user.uid;
|
||||||
delete req.body.password;
|
delete req.body.password;
|
||||||
|
|
||||||
const user = {...req.body, desiredLevels: DEFAULT_DESIRED_LEVELS, levels: DEFAULT_LEVELS, bio: "", isFirstLogin: true};
|
const user = {
|
||||||
|
...req.body,
|
||||||
|
desiredLevels: DEFAULT_DESIRED_LEVELS,
|
||||||
|
levels: DEFAULT_LEVELS,
|
||||||
|
bio: "",
|
||||||
|
isFirstLogin: true,
|
||||||
|
focus: "academic",
|
||||||
|
type: "student",
|
||||||
|
};
|
||||||
|
|
||||||
await setDoc(doc(db, "users", userId), user);
|
await setDoc(doc(db, "users", userId), user);
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function Page() {
|
|||||||
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
|
||||||
const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]);
|
const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]);
|
||||||
const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]);
|
const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]);
|
||||||
|
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
||||||
|
|
||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ export default function Page() {
|
|||||||
|
|
||||||
const onFinish = (solutions: UserSolution[]) => {
|
const onFinish = (solutions: UserSolution[]) => {
|
||||||
const solutionIds = solutions.map((x) => x.exercise);
|
const solutionIds = solutions.map((x) => x.exercise);
|
||||||
|
const solutionExams = solutions.map((x) => x.exam);
|
||||||
|
|
||||||
|
console.log(solutionExams, exam?.id, moduleIndex, selectedModules);
|
||||||
|
|
||||||
|
if (exam && !solutionExams.includes(exam.id)) return;
|
||||||
|
|
||||||
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
|
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
|
||||||
setHasBeenUploaded(true);
|
setHasBeenUploaded(true);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import {Exam, UserSolution} from "@/interfaces/exam";
|
||||||
import {Stat} from "@/interfaces/user";
|
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
|
|
||||||
export interface ExamState {
|
export interface ExamState {
|
||||||
exams: Exam[];
|
exams: Exam[];
|
||||||
userSolutions: UserSolution[];
|
userSolutions: UserSolution[];
|
||||||
showSolutions: boolean;
|
showSolutions: boolean;
|
||||||
|
hasExamEnded: boolean;
|
||||||
selectedModules: Module[];
|
selectedModules: Module[];
|
||||||
|
setHasExamEnded: (hasExamEnded: boolean) => void;
|
||||||
setUserSolutions: (userSolutions: UserSolution[]) => void;
|
setUserSolutions: (userSolutions: UserSolution[]) => void;
|
||||||
setExams: (exams: Exam[]) => void;
|
setExams: (exams: Exam[]) => void;
|
||||||
setShowSolutions: (showSolutions: boolean) => void;
|
setShowSolutions: (showSolutions: boolean) => void;
|
||||||
@@ -20,6 +21,7 @@ export const initialState = {
|
|||||||
userSolutions: [],
|
userSolutions: [],
|
||||||
showSolutions: false,
|
showSolutions: false,
|
||||||
selectedModules: [],
|
selectedModules: [],
|
||||||
|
hasExamEnded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState>((set) => ({
|
const useExamStore = create<ExamState>((set) => ({
|
||||||
@@ -28,6 +30,7 @@ const useExamStore = create<ExamState>((set) => ({
|
|||||||
setExams: (exams: Exam[]) => set(() => ({exams})),
|
setExams: (exams: Exam[]) => set(() => ({exams})),
|
||||||
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
|
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
|
||||||
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
|
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
|
||||||
|
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})),
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import {Exam, ReadingExam, ListeningExam, WritingExam, SpeakingExam} from "@/interfaces/exam";
|
import {
|
||||||
|
Exam,
|
||||||
|
ReadingExam,
|
||||||
|
ListeningExam,
|
||||||
|
WritingExam,
|
||||||
|
SpeakingExam,
|
||||||
|
Exercise,
|
||||||
|
UserSolution,
|
||||||
|
FillBlanksExercise,
|
||||||
|
MatchSentencesExercise,
|
||||||
|
} from "@/interfaces/exam";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getExamById = async (module: Module, id: string): Promise<Exam | undefined> => {
|
export const getExamById = async (module: Module, id: string): Promise<Exam | undefined> => {
|
||||||
@@ -21,3 +31,37 @@ export const getExamById = async (module: Module, id: string): Promise<Exam | un
|
|||||||
return newExam as SpeakingExam;
|
return newExam as SpeakingExam;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => {
|
||||||
|
const defaultSettings = {
|
||||||
|
exam: exam.id,
|
||||||
|
exercise: exercise.id,
|
||||||
|
solutions: [],
|
||||||
|
module: exam.module,
|
||||||
|
type: exercise.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
switch (exercise.type) {
|
||||||
|
case "fillBlanks":
|
||||||
|
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
case "matchSentences":
|
||||||
|
total = exercise.sentences.length;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
case "multipleChoice":
|
||||||
|
total = exercise.questions.length;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
case "writeBlanks":
|
||||||
|
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
case "writing":
|
||||||
|
total = 1;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
case "speaking":
|
||||||
|
total = 1;
|
||||||
|
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
||||||
|
default:
|
||||||
|
return {...defaultSettings, score: {correct: 0, total: 0, missing: 0}};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user