ENCOA-233: Added the option for certain exercises to not count towards scores
This commit is contained in:
@@ -11,6 +11,7 @@ import MCDropdown from "./MCDropdown";
|
|||||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
|
isPractice = false,
|
||||||
prompt,
|
prompt,
|
||||||
solutions,
|
solutions,
|
||||||
text,
|
text,
|
||||||
@@ -40,6 +41,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
const excludeWordMCType = (x: any) => {
|
const excludeWordMCType = (x: any) => {
|
||||||
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
||||||
};
|
};
|
||||||
@@ -169,7 +175,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice })}
|
||||||
className="max-w-[200px] w-full"
|
className="max-w-[200px] w-full"
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
Previous Page
|
Previous Page
|
||||||
@@ -178,7 +184,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
|
||||||
}}
|
}}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next Page
|
Next Page
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {InteractiveSpeakingExercise} from "@/interfaces/exam";
|
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {useEffect, useState} from "react";
|
import { 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";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {downloadBlob} from "@/utils/evaluation";
|
import { downloadBlob } from "@/utils/evaluation";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
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), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
@@ -24,16 +24,17 @@ export default function InteractiveSpeaking({
|
|||||||
userSolutions,
|
userSolutions,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
isPractice = false,
|
||||||
preview = false
|
preview = false
|
||||||
}: InteractiveSpeakingExercise & CommonProps) {
|
}: InteractiveSpeakingExercise & CommonProps) {
|
||||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
const [answers, setAnswers] = useState<{prompt: string; blob: string; questionIndex: number}[]>([]);
|
const [answers, setAnswers] = useState<{ prompt: string; blob: string; questionIndex: number }[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
|
||||||
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
|
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
@@ -52,8 +53,9 @@ export default function InteractiveSpeaking({
|
|||||||
onBack({
|
onBack({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
|
isPractice
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,8 +76,9 @@ export default function InteractiveSpeaking({
|
|||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
|
isPractice
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ export default function InteractiveSpeaking({
|
|||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -142,10 +145,11 @@ export default function InteractiveSpeaking({
|
|||||||
{
|
{
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
exam: examID,
|
exam: examID,
|
||||||
type,
|
type,
|
||||||
|
isPractice
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -181,7 +185,7 @@ export default function InteractiveSpeaking({
|
|||||||
audio
|
audio
|
||||||
key={questionIndex}
|
key={questionIndex}
|
||||||
onStop={(blob) => setMediaBlob(blob)}
|
onStop={(blob) => setMediaBlob(blob)}
|
||||||
render={({status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl}) => (
|
render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (
|
||||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
<p className="text-base font-normal">Record your answer:</p>
|
<p className="text-base font-normal">Record your answer:</p>
|
||||||
<div className="flex gap-8 items-center justify-center py-8">
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
@@ -301,12 +305,12 @@ export default function InteractiveSpeaking({
|
|||||||
</Button>
|
</Button>
|
||||||
{preview ? (
|
{preview ? (
|
||||||
<Button color="purple" isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
<Button color="purple" isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button color="purple" disabled={!mediaBlob} isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
<Button color="purple" disabled={!mediaBlob} isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default function MatchSentences({
|
|||||||
userSolutions,
|
userSolutions,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
isPractice = false,
|
||||||
disableProgressButtons = false
|
disableProgressButtons = false
|
||||||
}: MatchSentencesExercise & CommonProps) {
|
}: MatchSentencesExercise & CommonProps) {
|
||||||
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
|
||||||
@@ -81,7 +82,7 @@ export default function MatchSentences({
|
|||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -105,12 +106,12 @@ export default function MatchSentences({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, disableProgressButtons])
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -119,14 +120,14 @@ export default function MatchSentences({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export default function MultipleChoice({
|
|||||||
type,
|
type,
|
||||||
questions,
|
questions,
|
||||||
userSolutions,
|
userSolutions,
|
||||||
|
isPractice = false,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
disableProgressButtons = false
|
disableProgressButtons = false
|
||||||
@@ -93,7 +94,7 @@ export default function MultipleChoice({
|
|||||||
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));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ export default function MultipleChoice({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -140,13 +141,13 @@ export default function MultipleChoice({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, disableProgressButtons])
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (questionIndex + 1 >= questions.length - 1) {
|
if (questionIndex + 1 >= questions.length - 1) {
|
||||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 2);
|
setQuestionIndex(questionIndex + 2);
|
||||||
}
|
}
|
||||||
@@ -155,7 +156,7 @@ export default function MultipleChoice({
|
|||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice });
|
||||||
} else {
|
} else {
|
||||||
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
|
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
|
||||||
setQuestionIndex(questionIndex - 2);
|
setQuestionIndex(questionIndex - 2);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, onNext, onBack, preview = false }: SpeakingExercise & CommonProps) {
|
export default function Speaking({ id, title, text, video_url, type, prompts, suffix, userSolutions, isPractice = false, onNext, onBack, preview = false }: SpeakingExercise & CommonProps) {
|
||||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
@@ -81,7 +81,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
||||||
score: { correct: 0, total: 100, missing: 0 },
|
score: { correct: 0, total: 100, missing: 0 },
|
||||||
type,
|
type, isPractice
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su
|
|||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [],
|
||||||
score: { correct: 0, total: 100, missing: 0 },
|
score: { correct: 0, total: 100, missing: 0 },
|
||||||
type,
|
type, isPractice
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default function TrueFalse({
|
|||||||
prompt,
|
prompt,
|
||||||
questions,
|
questions,
|
||||||
userSolutions,
|
userSolutions,
|
||||||
|
isPractice = false,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
disableProgressButtons = false
|
disableProgressButtons = false
|
||||||
@@ -21,7 +22,7 @@ export default function TrueFalse({
|
|||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
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, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export default function TrueFalse({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export default function TrueFalse({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, disableProgressButtons])
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
@@ -64,14 +65,14 @@ export default function TrueFalse({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function WriteBlanks({
|
|||||||
maxWords,
|
maxWords,
|
||||||
solutions,
|
solutions,
|
||||||
userSolutions,
|
userSolutions,
|
||||||
|
isPractice = false,
|
||||||
text,
|
text,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
@@ -63,7 +64,7 @@ export default function WriteBlanks({
|
|||||||
const { hasExamEnded, setCurrentSolution } = useExamStore((state) => state);
|
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, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -82,12 +83,12 @@ export default function WriteBlanks({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, disableProgressButtons])
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
@@ -112,14 +113,14 @@ export default function WriteBlanks({
|
|||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] w-full">
|
className="max-w-[200px] w-full">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice })}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {WritingExercise} from "@/interfaces/exam";
|
import { WritingExercise } from "@/interfaces/exam";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import React, {Fragment, useEffect, useRef, useState} from "react";
|
import React, { Fragment, useEffect, useRef, 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";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
export default function Writing({
|
export default function Writing({
|
||||||
@@ -16,6 +16,7 @@ export default function Writing({
|
|||||||
wordCounter,
|
wordCounter,
|
||||||
attachment,
|
attachment,
|
||||||
userSolutions,
|
userSolutions,
|
||||||
|
isPractice = false,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
enableNavigation = false
|
enableNavigation = false
|
||||||
@@ -25,7 +26,7 @@ export default function Writing({
|
|||||||
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
||||||
const [saveTimer, setSaveTimer] = useState(0);
|
const [saveTimer, setSaveTimer] = useState(0);
|
||||||
|
|
||||||
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
|
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state);
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,7 +43,7 @@ export default function Writing({
|
|||||||
if (inputText.length > 0 && saveTimer % 10 === 0) {
|
if (inputText.length > 0 && saveTimer % 10 === 0) {
|
||||||
setUserSolutions([
|
setUserSolutions([
|
||||||
...storeUserSolutions.filter((x) => x.exercise !== id),
|
...storeUserSolutions.filter((x) => x.exercise !== id),
|
||||||
{exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type, module: "writing"},
|
{ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, module: "writing" },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -66,7 +67,7 @@ export default function Writing({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded)
|
if (hasExamEnded)
|
||||||
onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type, module: "writing"});
|
onNext({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, module: "writing", isPractice });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export default function Writing({
|
|||||||
} else {
|
} else {
|
||||||
setIsSubmitEnabled(true);
|
setIsSubmitEnabled(true);
|
||||||
if (wordCounter.limit < words.length) {
|
if (wordCounter.limit < words.length) {
|
||||||
toast.warning(`You have reached your word limit of ${wordCounter.limit} words!`, {toastId: "word-limit"});
|
toast.warning(`You have reached your word limit of ${wordCounter.limit} words!`, { toastId: "word-limit" });
|
||||||
setInputText(words.slice(0, words.length - 1).join(" "));
|
setInputText(words.slice(0, words.length - 1).join(" "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,7 @@ export default function Writing({
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type})
|
onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
|
||||||
}
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Back
|
Back
|
||||||
@@ -102,10 +103,10 @@ export default function Writing({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}],
|
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
module: "writing",
|
module: "writing", isPractice
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
@@ -177,7 +178,7 @@ export default function Writing({
|
|||||||
color="purple"
|
color="purple"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onBack({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 100, total: 100, missing: 0}, type})
|
onBack({ exercise: id, solutions: [{ id, solution: inputText }], score: { correct: 100, total: 100, missing: 0 }, type, isPractice })
|
||||||
}
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
Back
|
Back
|
||||||
@@ -188,10 +189,10 @@ export default function Writing({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}],
|
solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }],
|
||||||
score: {correct: 100, total: 100, missing: 0},
|
score: { correct: 100, total: 100, missing: 0 },
|
||||||
type,
|
type,
|
||||||
module: "writing",
|
module: "writing", isPractice
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="max-w-[200px] self-end w-full">
|
className="max-w-[200px] self-end w-full">
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {BsClock, BsXCircle} from "react-icons/bs";
|
import { BsClock, BsXCircle } from "react-icons/bs";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Stat, User} from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import {Module, Step} from "@/interfaces";
|
import { Module, Step } from "@/interfaces";
|
||||||
import ai_usage from "@/utils/ai.detection";
|
import ai_usage from "@/utils/ai.detection";
|
||||||
import {calculateBandScore} from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {uniqBy} from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import {sortByModule} from "@/utils/moduleUtils";
|
import { sortByModule } from "@/utils/moduleUtils";
|
||||||
import {convertToUserSolutions} from "@/utils/stats";
|
import { convertToUserSolutions } from "@/utils/stats";
|
||||||
import {getExamById} from "@/utils/exams";
|
import { getExamById } from "@/utils/exams";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import { Exam, UserSolution } from "@/interfaces/exam";
|
||||||
import ModuleBadge from "../ModuleBadge";
|
import ModuleBadge from "../ModuleBadge";
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string | number) => {
|
const formatTimestamp = (timestamp: string | number) => {
|
||||||
@@ -23,9 +23,9 @@ const formatTimestamp = (timestamp: string | number) => {
|
|||||||
return date.format(formatter);
|
return date.format(formatter);
|
||||||
};
|
};
|
||||||
|
|
||||||
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => {
|
const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => {
|
||||||
const scores: {
|
const scores: {
|
||||||
[key in Module]: {total: number; missing: number; correct: number};
|
[key in Module]: { total: number; missing: number; correct: number };
|
||||||
} = {
|
} = {
|
||||||
reading: {
|
reading: {
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -54,7 +54,7 @@ const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number;
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
stats.forEach((x) => {
|
stats.filter(x => !x.isPractice).forEach((x) => {
|
||||||
scores[x.module!] = {
|
scores[x.module!] = {
|
||||||
total: scores[x.module!].total + x.score.total,
|
total: scores[x.module!].total + x.score.total,
|
||||||
correct: scores[x.module!].correct + x.score.correct,
|
correct: scores[x.module!].correct + x.score.correct,
|
||||||
@@ -64,7 +64,7 @@ const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number;
|
|||||||
|
|
||||||
return Object.keys(scores)
|
return Object.keys(scores)
|
||||||
.filter((x) => scores[x as Module].total > 0)
|
.filter((x) => scores[x as Module].total > 0)
|
||||||
.map((x) => ({module: x as Module, ...scores[x as Module]}));
|
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StatsGridItemProps {
|
interface StatsGridItemProps {
|
||||||
@@ -133,7 +133,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
correct / total < 0.3 && "text-mti-rose",
|
correct / total < 0.3 && "text-mti-rose",
|
||||||
);
|
);
|
||||||
|
|
||||||
const {timeSpent, inactivity, session} = stats[0];
|
const { timeSpent, inactivity, session } = stats[0];
|
||||||
|
|
||||||
const selectExam = () => {
|
const selectExam = () => {
|
||||||
if (
|
if (
|
||||||
@@ -247,7 +247,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
<div className={clsx("grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2", examNumber !== undefined && "pr-10")}>
|
<div className={clsx("grid grid-cols-4 gap-2 place-items-start w-full -md:mt-2", examNumber !== undefined && "pr-10")}>
|
||||||
{!!assignment &&
|
{!!assignment &&
|
||||||
(assignment.released || assignment.released === undefined) &&
|
(assignment.released || assignment.released === undefined) &&
|
||||||
aggregatedLevels.map(({module, level}) => <ModuleBadge key={module} module={module} level={level} />)}
|
aggregatedLevels.map(({ module, level }) => <ModuleBadge key={module} module={module} level={level} />)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assignment && (
|
{assignment && (
|
||||||
@@ -270,9 +270,9 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.3 && "hover:border-mti-rose",
|
||||||
typeof selectedTrainingExams !== "undefined" &&
|
typeof selectedTrainingExams !== "undefined" &&
|
||||||
typeof timestamp === "string" &&
|
typeof timestamp === "string" &&
|
||||||
selectedTrainingExams.some((exam) => exam.includes(timestamp)) &&
|
selectedTrainingExams.some((exam) => exam.includes(timestamp)) &&
|
||||||
"border-2 border-slate-600",
|
"border-2 border-slate-600",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!!assignment && !assignment.released) return;
|
if (!!assignment && !assignment.released) return;
|
||||||
@@ -280,8 +280,8 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
...(width !== undefined && {width}),
|
...(width !== undefined && { width }),
|
||||||
...(height !== undefined && {height}),
|
...(height !== undefined && { height }),
|
||||||
}}
|
}}
|
||||||
data-tip={isDisabled ? "This exam is still being evaluated..." : "This exam is still locked by its assigner..."}
|
data-tip={isDisabled ? "This exam is still being evaluated..." : "This exam is still locked by its assigner..."}
|
||||||
role="button">
|
role="button">
|
||||||
@@ -297,8 +297,8 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
data-tip="Your screen size is too small to view previous exams."
|
data-tip="Your screen size is too small to view previous exams."
|
||||||
style={{
|
style={{
|
||||||
...(width !== undefined && {width}),
|
...(width !== undefined && { width }),
|
||||||
...(height !== undefined && {height}),
|
...(height !== undefined && { height }),
|
||||||
}}
|
}}
|
||||||
role="button">
|
role="button">
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export interface UserSolution {
|
|||||||
exercise: string;
|
exercise: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
shuffleMaps?: ShuffleMap[];
|
shuffleMaps?: ShuffleMap[];
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WritingExam extends ExamBase {
|
export interface WritingExam extends ExamBase {
|
||||||
@@ -162,6 +163,7 @@ export interface WritingExercise extends Section {
|
|||||||
evaluation?: WritingEvaluation;
|
evaluation?: WritingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
topic?: string;
|
topic?: string;
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIDetectionAttributes {
|
export interface AIDetectionAttributes {
|
||||||
@@ -196,6 +198,7 @@ export interface SpeakingExercise extends Section {
|
|||||||
evaluation?: SpeakingEvaluation;
|
evaluation?: SpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
topic?: string;
|
topic?: string;
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InteractiveSpeakingExercise extends Section {
|
export interface InteractiveSpeakingExercise extends Section {
|
||||||
@@ -215,6 +218,7 @@ export interface InteractiveSpeakingExercise extends Section {
|
|||||||
first_topic?: string;
|
first_topic?: string;
|
||||||
second_topic?: string;
|
second_topic?: string;
|
||||||
variant?: "initial" | "final";
|
variant?: "initial" | "final";
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FillBlanksMCOption {
|
export interface FillBlanksMCOption {
|
||||||
@@ -243,6 +247,7 @@ export interface FillBlanksExercise {
|
|||||||
solution: string; // *EXAMPLE: "preserve"
|
solution: string; // *EXAMPLE: "preserve"
|
||||||
}[];
|
}[];
|
||||||
variant?: string;
|
variant?: string;
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseExercise {
|
export interface TrueFalseExercise {
|
||||||
@@ -251,6 +256,7 @@ export interface TrueFalseExercise {
|
|||||||
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
prompt: string; // *EXAMPLE: "Select the appropriate option."
|
||||||
questions: TrueFalseQuestion[];
|
questions: TrueFalseQuestion[];
|
||||||
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrueFalseQuestion {
|
export interface TrueFalseQuestion {
|
||||||
@@ -274,6 +280,7 @@ export interface WriteBlanksExercise {
|
|||||||
solution: string;
|
solution: string;
|
||||||
}[];
|
}[];
|
||||||
variant?: string;
|
variant?: string;
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchSentencesExercise {
|
export interface MatchSentencesExercise {
|
||||||
@@ -285,6 +292,7 @@ export interface MatchSentencesExercise {
|
|||||||
allowRepetition: boolean;
|
allowRepetition: boolean;
|
||||||
options: MatchSentenceExerciseOption[];
|
options: MatchSentenceExerciseOption[];
|
||||||
variant?: string;
|
variant?: string;
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatchSentenceExerciseSentence {
|
export interface MatchSentenceExerciseSentence {
|
||||||
@@ -309,6 +317,7 @@ export interface MultipleChoiceExercise {
|
|||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultipleChoiceQuestion {
|
export interface MultipleChoiceQuestion {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Module} from ".";
|
import { Module } from ".";
|
||||||
import {InstructorGender, ShuffleMap} from "./exam";
|
import { InstructorGender, ShuffleMap } from "./exam";
|
||||||
import {PermissionType} from "./permissions";
|
import { PermissionType } from "./permissions";
|
||||||
|
|
||||||
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
|
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser;
|
||||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||||
@@ -12,8 +12,8 @@ export interface BasicUser {
|
|||||||
id: string;
|
id: string;
|
||||||
isFirstLogin: boolean;
|
isFirstLogin: boolean;
|
||||||
focus: "academic" | "general";
|
focus: "academic" | "general";
|
||||||
levels: {[key in Module]: number};
|
levels: { [key in Module]: number };
|
||||||
desiredLevels: {[key in Module]: number};
|
desiredLevels: { [key in Module]: number };
|
||||||
type: Type;
|
type: Type;
|
||||||
bio: string;
|
bio: string;
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
@@ -22,7 +22,7 @@ export interface BasicUser {
|
|||||||
status: UserStatus;
|
status: UserStatus;
|
||||||
permissions: PermissionType[];
|
permissions: PermissionType[];
|
||||||
lastLogin?: Date;
|
lastLogin?: Date;
|
||||||
entities: {id: string; role: string}[];
|
entities: { id: string; role: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentUser extends BasicUser {
|
export interface StudentUser extends BasicUser {
|
||||||
@@ -109,13 +109,13 @@ export interface DemographicCorporateInformation {
|
|||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||||
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] = [
|
||||||
{status: "student", label: "Student"},
|
{ status: "student", label: "Student" },
|
||||||
{status: "employed", label: "Employed"},
|
{ status: "employed", label: "Employed" },
|
||||||
{status: "unemployed", label: "Unemployed"},
|
{ status: "unemployed", label: "Unemployed" },
|
||||||
{status: "self-employed", label: "Self-employed"},
|
{ status: "self-employed", label: "Self-employed" },
|
||||||
{status: "retired", label: "Retired"},
|
{ status: "retired", label: "Retired" },
|
||||||
{status: "other", label: "Other"},
|
{ status: "other", label: "Other" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface Stat {
|
export interface Stat {
|
||||||
@@ -142,6 +142,7 @@ export interface Stat {
|
|||||||
path: string;
|
path: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
isPractice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
@@ -174,4 +175,4 @@ export interface Code {
|
|||||||
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
|
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate";
|
||||||
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];
|
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"];
|
||||||
|
|
||||||
export type WithUser<T> = T extends {participants: string[]} ? Omit<T, "participants"> & {participants: User[]} : T;
|
export type WithUser<T> = T extends { participants: string[] } ? Omit<T, "participants"> & { participants: User[] } : T;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import Reading from "@/exams/Reading";
|
|||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Speaking from "@/exams/Speaking";
|
import Speaking from "@/exams/Speaking";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import useUser from "@/hooks/useUser";
|
|
||||||
import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
|
import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
|
||||||
import { Stat, User } from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
@@ -21,12 +20,7 @@ import axios from "axios";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import useSessions from "@/hooks/useSessions";
|
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import clsx from "clsx";
|
|
||||||
import useGradingSystem from "@/hooks/useGrading";
|
|
||||||
import { Assignment } from "@/interfaces/results";
|
|
||||||
import { mapBy } from "@/utils";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: "exams" | "exercises";
|
page: "exams" | "exercises";
|
||||||
@@ -214,7 +208,6 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
|
|||||||
}, [setModuleIndex, showSolutions]);
|
}, [setModuleIndex, showSolutions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(selectedModules)
|
|
||||||
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
|
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
|
||||||
const nextExam = exams[moduleIndex];
|
const nextExam = exams[moduleIndex];
|
||||||
|
|
||||||
@@ -264,6 +257,7 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
|
|||||||
isDisabled: solution.isDisabled,
|
isDisabled: solution.isDisabled,
|
||||||
shuffleMaps: solution.shuffleMaps,
|
shuffleMaps: solution.shuffleMaps,
|
||||||
...(assignment ? { assignment: assignment.id } : {}),
|
...(assignment ? { assignment: assignment.id } : {}),
|
||||||
|
isPractice: solution.isPractice
|
||||||
}));
|
}));
|
||||||
|
|
||||||
axios
|
axios
|
||||||
@@ -422,7 +416,7 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
userSolutions.forEach((x) => {
|
userSolutions.filter(x => !x.isPractice).forEach((x) => {
|
||||||
const examModule =
|
const examModule =
|
||||||
x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined);
|
x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {Stat, User} from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {groupByDate} from "@/utils/stats";
|
import { groupByDate } from "@/utils/stats";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {ToastContainer} from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import StatsGridItem from "@/components/Medium/StatGridItem";
|
import StatsGridItem from "@/components/Medium/StatGridItem";
|
||||||
import RecordFilter from "@/components/Medium/RecordFilter";
|
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be";
|
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import useGradingSystem from "@/hooks/useGrading";
|
import useGradingSystem from "@/hooks/useGrading";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
@@ -33,7 +33,7 @@ import { EntityWithRoles } from "@/interfaces/entity";
|
|||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -43,12 +43,10 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
|
|
||||||
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
||||||
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id')))
|
||||||
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
|
||||||
const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id')))
|
const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id')))
|
||||||
const gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({user, users, assignments, entities, gradingSystems}),
|
props: serialize({ user, users, assignments, entities }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -58,13 +56,12 @@ interface Props {
|
|||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
gradingSystems: Grading[]
|
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TRAINING_EXAMS = 10;
|
const MAX_TRAINING_EXAMS = 10;
|
||||||
|
|
||||||
export default function History({user, users, assignments, entities, gradingSystems}: Props) {
|
export default function History({ user, users, assignments, entities }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
@@ -75,8 +72,8 @@ export default function History({user, users, assignments, entities, gradingSyst
|
|||||||
|
|
||||||
const [filter, setFilter] = useState<Filter>();
|
const [filter, setFilter] = useState<Filter>();
|
||||||
|
|
||||||
const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
||||||
const {gradingSystem} = useGradingSystem();
|
const { gradingSystem } = useGradingSystem();
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||||
@@ -113,12 +110,12 @@ export default function History({user, users, assignments, entities, gradingSyst
|
|||||||
};
|
};
|
||||||
}, [router.events, setTraining]);
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
||||||
if (filter && filter !== "assignments") {
|
if (filter && filter !== "assignments") {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({[filter as string]: 1})
|
.subtract({ [filter as string]: 1 })
|
||||||
.format("x");
|
.format("x");
|
||||||
const filteredStats: {[key: string]: Stat[]} = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
||||||
@@ -127,7 +124,7 @@ export default function History({user, users, assignments, entities, gradingSyst
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filter && filter === "assignments") {
|
if (filter && filter === "assignments") {
|
||||||
const filteredStats: {[key: string]: Stat[]} = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
||||||
@@ -140,21 +137,21 @@ export default function History({user, users, assignments, entities, gradingSyst
|
|||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||||
const allStats = Object.keys(groupedStatsByDate);
|
const allStats = Object.keys(groupedStatsByDate);
|
||||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
||||||
const timestamp = moduleAndTimestamp.split("-")[1];
|
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||||
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
||||||
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, {});
|
}, {});
|
||||||
setTrainingStats(Object.values(selectedStats).flat());
|
setTrainingStats(Object.values(selectedStats).flat());
|
||||||
router.push("/training");
|
router.push("/training");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredStats = useMemo(() =>
|
const filteredStats = useMemo(() =>
|
||||||
Object.keys(filterStatsByDate(groupedStats))
|
Object.keys(filterStatsByDate(groupedStats))
|
||||||
@@ -203,7 +200,7 @@ const handleTrainingContentSubmission = () => {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<RecordFilter user={user} users={users} entities={entities} filterState={{filter: filter, setFilter: setFilter}}>
|
<RecordFilter user={user} users={users} entities={entities} filterState={{ filter: filter, setFilter: setFilter }}>
|
||||||
{training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">
|
<div className="font-semibold text-2xl mr-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user