diff --git a/public/blue-stock-photo.png b/public/blue-stock-photo.png new file mode 100644 index 00000000..4ac786da Binary files /dev/null and b/public/blue-stock-photo.png differ diff --git a/public/purple-stock-photo.png b/public/purple-stock-photo.png new file mode 100644 index 00000000..2ee82471 Binary files /dev/null and b/public/purple-stock-photo.png differ diff --git a/src/components/Exercises/FillBlanks/index.tsx b/src/components/Exercises/FillBlanks/index.tsx index 7dc4ea65..d138f66e 100644 --- a/src/components/Exercises/FillBlanks/index.tsx +++ b/src/components/Exercises/FillBlanks/index.tsx @@ -11,6 +11,7 @@ import MCDropdown from "./MCDropdown"; const FillBlanks: React.FC = ({ id, type, + isPractice = false, prompt, solutions, text, @@ -20,8 +21,9 @@ const FillBlanks: React.FC = ({ preview, onNext, onBack, + disableProgressButtons = false }) => { - + const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); @@ -38,7 +40,12 @@ const FillBlanks: React.FC = ({ const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; const dropdownRef = useRef(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) => { return typeof x === "string" ? x : (x as { letter: string; word: string }); }; @@ -55,16 +62,16 @@ const FillBlanks: React.FC = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setOpenDropdownId(null); - } + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setOpenDropdownId(null); + } }; - + document.addEventListener('mousedown', handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('mousedown', handleClickOutside); }; - }, []); + }, []); const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; @@ -105,18 +112,18 @@ const FillBlanks: React.FC = ({ const id = match.replaceAll(/[\{\}]/g, ""); const userSolution = answers.find((x) => x.id === id); const styles = clsx( - "rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit inline-block", + "rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit inline-block", !userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight", userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight", ); - + const currentSelection = words.find((x) => { if (typeof x !== "string" && "id" in x) { return (x as FillBlanksMCOption).id.toString() == id.toString(); } return false; }) as FillBlanksMCOption; - + return variant === "mc" ? ( = ({ className="inline-block py-2 px-1 align-middle" width={220} isOpen={openDropdownId === id} - onToggle={()=> setOpenDropdownId(prevId => prevId === id ? null : id)} + onToggle={() => setOpenDropdownId(prevId => prevId === id ? null : id)} /> ) : ( = ({ }, [variant, words, answers, openDropdownId], ); - + const memoizedLines = useMemo(() => { return text.split("\\n").map((line, index) => (

@@ -163,29 +170,33 @@ const FillBlanks: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers]); + const progressButtons = () => ( +

+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{variant !== "mc" && ( {prompt.split("\\n").map((line, index) => ( @@ -224,25 +235,8 @@ const FillBlanks: React.FC = ({
)}
-
- - -
+ {!disableProgressButtons && progressButtons()}
); }; diff --git a/src/components/Exercises/InteractiveSpeaking.tsx b/src/components/Exercises/InteractiveSpeaking.tsx index 7653fe13..a348a477 100644 --- a/src/components/Exercises/InteractiveSpeaking.tsx +++ b/src/components/Exercises/InteractiveSpeaking.tsx @@ -1,14 +1,14 @@ -import {InteractiveSpeakingExercise} from "@/interfaces/exam"; -import {CommonProps} from "."; -import {useEffect, useState} from "react"; -import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs"; +import { InteractiveSpeakingExercise } from "@/interfaces/exam"; +import { CommonProps } from "."; +import { useEffect, useState } from "react"; +import { BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill } from "react-icons/bs"; import dynamic from "next/dynamic"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; -import {downloadBlob} from "@/utils/evaluation"; +import { downloadBlob } from "@/utils/evaluation"; 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), { ssr: false, }); @@ -24,16 +24,17 @@ export default function InteractiveSpeaking({ userSolutions, onNext, onBack, + isPractice = false, preview = false }: InteractiveSpeakingExercise & CommonProps) { const [recordingDuration, setRecordingDuration] = useState(0); const [isRecording, setIsRecording] = useState(false); const [mediaBlob, setMediaBlob] = useState(); - 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 {questionIndex, setQuestionIndex} = useExamStore((state) => state); - const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state); + const { questionIndex, setQuestionIndex } = useExamStore((state) => state); + const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state); const hasExamEnded = useExamStore((state) => state.hasExamEnded); @@ -52,8 +53,9 @@ export default function InteractiveSpeaking({ onBack({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], - score: {correct: 100, total: 100, missing: 0}, + score: { correct: 100, total: 100, missing: 0 }, type, + isPractice }); }; @@ -74,8 +76,9 @@ export default function InteractiveSpeaking({ onNext({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], - score: {correct: 100, total: 100, missing: 0}, + score: { correct: 100, total: 100, missing: 0 }, type, + isPractice }); }; @@ -100,7 +103,7 @@ export default function InteractiveSpeaking({ onNext({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], - score: {correct: 100, total: 100, missing: 0}, + score: { correct: 100, total: 100, missing: 0 }, type, }); } @@ -142,10 +145,11 @@ export default function InteractiveSpeaking({ { exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], - score: {correct: 100, total: 100, missing: 0}, + score: { correct: 100, total: 100, missing: 0 }, module: "speaking", exam: examID, type, + isPractice }, ]); @@ -181,7 +185,7 @@ export default function InteractiveSpeaking({ audio key={questionIndex} onStop={(blob) => setMediaBlob(blob)} - render={({status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl}) => ( + render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (

Record your answer:

@@ -301,12 +305,12 @@ export default function InteractiveSpeaking({ {preview ? ( + {questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"} + ) : ( + {questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"} + )}
diff --git a/src/components/Exercises/MatchSentences.tsx b/src/components/Exercises/MatchSentences.tsx index 755f0292..0e00a90c 100644 --- a/src/components/Exercises/MatchSentences.tsx +++ b/src/components/Exercises/MatchSentences.tsx @@ -1,18 +1,18 @@ -import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; -import {MatchSentenceExerciseOption, MatchSentenceExerciseSentence, MatchSentencesExercise} from "@/interfaces/exam"; -import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles"; +import { MatchSentenceExerciseOption, MatchSentenceExerciseSentence, MatchSentencesExercise } from "@/interfaces/exam"; +import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import Icon from "@mdi/react"; import clsx from "clsx"; -import {Fragment, useEffect, useState} from "react"; +import { Fragment, useEffect, useState } from "react"; import LineTo from "react-lineto"; -import {CommonProps} from "."; +import { CommonProps } from "."; import Button from "../Low/Button"; import Xarrow from "react-xarrows"; import useExamStore from "@/stores/examStore"; -import {DndContext, DragEndEvent, useDraggable, useDroppable} from "@dnd-kit/core"; +import { DndContext, DragEndEvent, useDraggable, useDroppable } from "@dnd-kit/core"; -function DroppableQuestionArea({question, answer}: {question: MatchSentenceExerciseSentence; answer?: string}) { - const {isOver, setNodeRef} = useDroppable({id: `droppable_sentence_${question.id}`}); +function DroppableQuestionArea({ question, answer }: { question: MatchSentenceExerciseSentence; answer?: string }) { + const { isOver, setNodeRef } = useDroppable({ id: `droppable_sentence_${question.id}` }); return (
@@ -35,16 +35,16 @@ function DroppableQuestionArea({question, answer}: {question: MatchSentenceExerc ); } -function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) { - const {attributes, listeners, setNodeRef, transform} = useDraggable({ +function DraggableOptionArea({ option }: { option: MatchSentenceExerciseOption }) { + const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: `draggable_option_${option.id}`, }); const style = transform ? { - transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, - zIndex: 99, - } + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + zIndex: 99, + } : undefined; return ( @@ -63,15 +63,26 @@ function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) { ); } -export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) { - const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions); +export default function MatchSentences({ + id, + options, + type, + prompt, + sentences, + userSolutions, + onNext, + onBack, + isPractice = false, + disableProgressButtons = false +}: MatchSentencesExercise & CommonProps) { + const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const setCurrentSolution = useExamStore((state) => state.setCurrentSolution); 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 }, [answers, setAnswers]); @@ -80,7 +91,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us const optionID = event.active.id.toString().replace("draggable_option_", ""); const sentenceID = event.over.id.toString().replace("droppable_sentence_", ""); - setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), {question: sentenceID, option: optionID}]); + setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), { question: sentenceID, option: optionID }]); } }; @@ -91,34 +102,43 @@ export default function MatchSentences({id, options, type, prompt, sentences, us ).length; const missing = total - answers.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; useEffect(() => { - if (hasExamEnded) 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 + }, [answers, disableProgressButtons]) + + useEffect(() => { + if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasExamEnded]); + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -151,22 +171,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index 1f7bf6e0..bed48996 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -1,12 +1,12 @@ /* eslint-disable @next/next/no-img-element */ -import {MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap} from "@/interfaces/exam"; +import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; +import { CommonProps } from "."; import Button from "../Low/Button"; -import {v4} from "uuid"; +import { v4 } from "uuid"; function Question({ id, @@ -72,10 +72,20 @@ function Question({ ); } -export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { - const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions); +export default function MultipleChoice({ + id, + prompt, + type, + questions, + userSolutions, + isPractice = false, + onNext, + onBack, + disableProgressButtons = false +}: MultipleChoiceExercise & CommonProps) { + const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions || []); - const {questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution} = useExamStore( + const { questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution } = useExamStore( (state) => state, ); @@ -84,16 +94,16 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); 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 }, [hasExamEnded]); const onSelectOption = (option: string, question: MultipleChoiceQuestion) => { - 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 }]); }; 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 }, [answers, setAnswers]); @@ -127,12 +137,17 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio return isSolutionCorrect || false; }).length; const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; + useEffect(() => { + if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps, isPractice }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers, disableProgressButtons]) + const next = () => { 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 { setQuestionIndex(questionIndex + 2); } @@ -141,7 +156,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio const back = () => { 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 { if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return; setQuestionIndex(questionIndex - 2); @@ -150,72 +165,74 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio scrollToTop(); }; - return ( -
-
- + const progressButtons = () => ( +
+ - + ? "Submit" + : "Next"} + +
+ ) + + const renderAllQuestions = () => + questions.map(question => ( +
+ question.id === x.question)?.option} + onSelectOption={(option) => onSelectOption(option, question)} + />
+ )) -
-
- {/*{"Select the appropriate option."}*/} - {questionIndex < questions.length && ( - questions[questionIndex].id === x.question)?.option} - onSelectOption={(option) => onSelectOption(option, questions[questionIndex])} - /> - )} -
- - {questionIndex + 1 < questions.length && ( -
- questions[questionIndex + 1].id === x.question)?.option} - onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])} - /> -
+ const renderTwoQuestions = () => ( + <> +
+ {questionIndex < questions.length && ( + questions[questionIndex].id === x.question)?.option} + onSelectOption={(option) => onSelectOption(option, questions[questionIndex])} + /> )}
-
- + {questionIndex + 1 < questions.length && ( +
+ questions[questionIndex + 1].id === x.question)?.option} + onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])} + /> +
+ )} + + ) - + return ( +
+ {!disableProgressButtons && progressButtons()} + +
+ {disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
+ + {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Exercises/Speaking.tsx b/src/components/Exercises/Speaking.tsx index b96714c9..2614aca6 100644 --- a/src/components/Exercises/Speaking.tsx +++ b/src/components/Exercises/Speaking.tsx @@ -14,7 +14,7 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo 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 [isRecording, setIsRecording] = useState(false); const [mediaBlob, setMediaBlob] = useState(); @@ -81,7 +81,7 @@ export default function Speaking({ id, title, text, video_url, type, prompts, su exercise: id, solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], 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, solutions: mediaBlob ? [{ id, solution: mediaBlob }] : [], score: { correct: 0, total: 100, missing: 0 }, - type, + type, isPractice }); }; diff --git a/src/components/Exercises/TrueFalse.tsx b/src/components/Exercises/TrueFalse.tsx index 44e11070..4bb469ba 100644 --- a/src/components/Exercises/TrueFalse.tsx +++ b/src/components/Exercises/TrueFalse.tsx @@ -1,17 +1,28 @@ -import {TrueFalseExercise} from "@/interfaces/exam"; +import { TrueFalseExercise } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; -import {Fragment, useEffect, useState} from "react"; -import {CommonProps} from "."; +import clsx from "clsx"; +import { Fragment, useEffect, useState } from "react"; +import { CommonProps } from "."; import Button from "../Low/Button"; -export default function TrueFalse({id, type, prompt, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) { - const [answers, setAnswers] = useState<{id: string; solution: "true" | "false" | "not_given"}[]>(userSolutions); +export default function TrueFalse({ + id, + type, + prompt, + questions, + userSolutions, + isPractice = false, + onNext, + onBack, + disableProgressButtons = false +}: TrueFalseExercise & CommonProps) { + const [answers, setAnswers] = useState<{ id: string; solution: "true" | "false" | "not_given" }[]>(userSolutions); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const setCurrentSolution = useExamStore((state) => state.setCurrentSolution); 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 }, [hasExamEnded]); @@ -26,11 +37,11 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o ).length; const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; 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 }, [answers, setAnswers]); @@ -41,29 +52,38 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o return; } - setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), {id: questionId, solution}]); + setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), { id: questionId, solution }]); }; + useEffect(() => { + if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers, disableProgressButtons]) + + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -123,22 +143,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Exercises/WriteBlanks.tsx b/src/components/Exercises/WriteBlanks.tsx index 5e308fc4..4492e10f 100644 --- a/src/components/Exercises/WriteBlanks.tsx +++ b/src/components/Exercises/WriteBlanks.tsx @@ -1,12 +1,12 @@ -import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; -import {WriteBlanksExercise} from "@/interfaces/exam"; -import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles"; +import { WriteBlanksExercise } from "@/interfaces/exam"; +import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import Icon from "@mdi/react"; import clsx from "clsx"; -import {Fragment, useEffect, useState} from "react"; +import { Fragment, useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import {toast} from "react-toastify"; +import { CommonProps } from "."; +import { toast } from "react-toastify"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; @@ -29,7 +29,7 @@ function Blank({ useEffect(() => { const words = userInput.split(" "); if (words.length > maxWords) { - toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"}); + toast.warning(`You have reached your word limit of ${maxWords} words!`, { toastId: "word-limit" }); setUserInput(words.join(" ").trim()); } }, [maxWords, userInput]); @@ -46,13 +46,25 @@ function Blank({ ); } -export default function WriteBlanks({id, prompt, type, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) { - const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions); +export default function WriteBlanks({ + id, + prompt, + type, + maxWords, + solutions, + userSolutions, + isPractice = false, + text, + onNext, + onBack, + disableProgressButtons = false +}: WriteBlanksExercise & CommonProps) { + const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); - const {hasExamEnded, setCurrentSolution} = useExamStore((state) => state); + const { hasExamEnded, setCurrentSolution } = useExamStore((state) => state); 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 }, [hasExamEnded]); @@ -67,14 +79,19 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user ).length; const missing = total - answers.filter((x) => solutions.find((y) => x.id === y.id)).length; - return {total, correct, missing}; + return { total, correct, missing }; }; 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 }, [answers, setAnswers]); + useEffect(() => { + if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, isPractice }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers, disableProgressButtons]) + const renderLines = (line: string) => { return ( @@ -82,7 +99,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user const id = match.replaceAll(/[\{\}]/g, ""); const userSolution = answers.find((x) => x.id === id); const setUserSolution = (solution: string) => { - setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution}]); + setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution }]); }; return ; @@ -91,26 +108,30 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user ); }; + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -129,22 +150,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Exercises/Writing.tsx b/src/components/Exercises/Writing.tsx index e173e671..00d16d1d 100644 --- a/src/components/Exercises/Writing.tsx +++ b/src/components/Exercises/Writing.tsx @@ -1,10 +1,10 @@ /* eslint-disable @next/next/no-img-element */ -import {WritingExercise} from "@/interfaces/exam"; -import {CommonProps} from "."; -import React, {Fragment, useEffect, useRef, useState} from "react"; -import {toast} from "react-toastify"; +import { WritingExercise } from "@/interfaces/exam"; +import { CommonProps } from "."; +import React, { Fragment, useEffect, useRef, useState } from "react"; +import { toast } from "react-toastify"; 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({ @@ -16,6 +16,7 @@ export default function Writing({ wordCounter, attachment, userSolutions, + isPractice = false, onNext, onBack, enableNavigation = false @@ -25,7 +26,7 @@ export default function Writing({ const [isSubmitEnabled, setIsSubmitEnabled] = useState(false); 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); useEffect(() => { @@ -42,7 +43,7 @@ export default function Writing({ if (inputText.length > 0 && saveTimer % 10 === 0) { setUserSolutions([ ...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 @@ -66,7 +67,7 @@ export default function Writing({ useEffect(() => { 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 }, [hasExamEnded]); @@ -78,7 +79,7 @@ export default function Writing({ } else { setIsSubmitEnabled(true); 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(" ")); } } @@ -91,7 +92,7 @@ export default function Writing({ color="purple" variant="outline" 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"> Back @@ -102,10 +103,10 @@ export default function Writing({ onClick={() => onNext({ exercise: id, - solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}], - score: {correct: 100, total: 100, missing: 0}, + solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }], + score: { correct: 100, total: 100, missing: 0 }, type, - module: "writing", + module: "writing", isPractice }) } className="max-w-[200px] self-end w-full"> @@ -177,7 +178,7 @@ export default function Writing({ color="purple" variant="outline" 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"> Back @@ -188,10 +189,10 @@ export default function Writing({ onClick={() => onNext({ exercise: id, - solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}], - score: {correct: 100, total: 100, missing: 0}, + solutions: [{ id, solution: inputText.replaceAll(/\s{2,}/g, " ") }], + score: { correct: 100, total: 100, missing: 0 }, type, - module: "writing", + module: "writing", isPractice }) } className="max-w-[200px] self-end w-full"> diff --git a/src/components/Exercises/index.tsx b/src/components/Exercises/index.tsx index 8d407983..1587d6f8 100644 --- a/src/components/Exercises/index.tsx +++ b/src/components/Exercises/index.tsx @@ -19,13 +19,14 @@ import Speaking from "./Speaking"; import TrueFalse from "./TrueFalse"; import InteractiveSpeaking from "./InteractiveSpeaking"; -const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false}); +const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), { ssr: false }); export interface CommonProps { examID?: string; onNext: (userSolutions: UserSolution) => void; onBack: (userSolutions: UserSolution) => void; enableNavigation?: boolean; + disableProgressButtons?: boolean preview?: boolean; } @@ -35,21 +36,22 @@ export const renderExercise = ( onNext: (userSolutions: UserSolution) => void, onBack: (userSolutions: UserSolution) => void, enableNavigation?: boolean, + disableProgressButtons?: boolean, preview?: boolean, ) => { switch (exercise.type) { case "fillBlanks": - return ; + return ; case "trueFalse": - return ; + return ; case "matchSentences": - return ; + return ; case "multipleChoice": - return ; + return ; case "writeBlanks": - return ; + return ; case "writing": - return ; + return ; case "speaking": return ; case "interactiveSpeaking": diff --git a/src/components/High/AssignmentCard.tsx b/src/components/High/AssignmentCard.tsx index 49812254..e53bb846 100644 --- a/src/components/High/AssignmentCard.tsx +++ b/src/components/High/AssignmentCard.tsx @@ -6,6 +6,7 @@ import { sortByModuleName } from "@/utils/moduleUtils"; import clsx from "clsx"; import moment from "moment"; import { useRouter } from "next/router"; +import { useMemo } from "react"; import Button from "../Low/Button"; import ModuleBadge from "../ModuleBadge"; @@ -20,6 +21,8 @@ interface Props { export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) { const router = useRouter() + const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id]) + return (
))}
- {futureAssignmentFilter(assignment) && ( + {futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
diff --git a/src/components/Medium/StatGridItem.tsx b/src/components/Medium/StatGridItem.tsx index 82aaf0e3..be9da7b7 100644 --- a/src/components/Medium/StatGridItem.tsx +++ b/src/components/Medium/StatGridItem.tsx @@ -1,19 +1,19 @@ import React from "react"; -import {BsClock, BsXCircle} from "react-icons/bs"; +import { BsClock, BsXCircle } from "react-icons/bs"; import clsx from "clsx"; -import {Stat, User} from "@/interfaces/user"; -import {Module, Step} from "@/interfaces"; +import { Stat, User } from "@/interfaces/user"; +import { Module, Step } from "@/interfaces"; import ai_usage from "@/utils/ai.detection"; -import {calculateBandScore} from "@/utils/score"; +import { calculateBandScore } from "@/utils/score"; import moment from "moment"; -import {Assignment} from "@/interfaces/results"; -import {uuidv4} from "@firebase/util"; -import {useRouter} from "next/router"; -import {uniqBy} from "lodash"; -import {sortByModule} from "@/utils/moduleUtils"; -import {convertToUserSolutions} from "@/utils/stats"; -import {getExamById} from "@/utils/exams"; -import {Exam, UserSolution} from "@/interfaces/exam"; +import { Assignment } from "@/interfaces/results"; +import { uuidv4 } from "@firebase/util"; +import { useRouter } from "next/router"; +import { uniqBy } from "lodash"; +import { sortByModule } from "@/utils/moduleUtils"; +import { convertToUserSolutions } from "@/utils/stats"; +import { getExamById } from "@/utils/exams"; +import { Exam, UserSolution } from "@/interfaces/exam"; import ModuleBadge from "../ModuleBadge"; const formatTimestamp = (timestamp: string | number) => { @@ -23,9 +23,9 @@ const formatTimestamp = (timestamp: string | number) => { 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: { - [key in Module]: {total: number; missing: number; correct: number}; + [key in Module]: { total: number; missing: number; correct: number }; } = { reading: { 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!] = { total: scores[x.module!].total + x.score.total, correct: scores[x.module!].correct + x.score.correct, @@ -64,7 +64,7 @@ const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; return Object.keys(scores) .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 { @@ -133,7 +133,7 @@ const StatsGridItem: React.FC = ({ correct / total < 0.3 && "text-mti-rose", ); - const {timeSpent, inactivity, session} = stats[0]; + const { timeSpent, inactivity, session } = stats[0]; const selectExam = () => { if ( @@ -247,7 +247,7 @@ const StatsGridItem: React.FC = ({
{!!assignment && (assignment.released || assignment.released === undefined) && - aggregatedLevels.map(({module, level}) => )} + aggregatedLevels.map(({ module, level }) => )}
{assignment && ( @@ -270,9 +270,9 @@ const StatsGridItem: React.FC = ({ correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total < 0.3 && "hover:border-mti-rose", typeof selectedTrainingExams !== "undefined" && - typeof timestamp === "string" && - selectedTrainingExams.some((exam) => exam.includes(timestamp)) && - "border-2 border-slate-600", + typeof timestamp === "string" && + selectedTrainingExams.some((exam) => exam.includes(timestamp)) && + "border-2 border-slate-600", )} onClick={() => { if (!!assignment && !assignment.released) return; @@ -280,8 +280,8 @@ const StatsGridItem: React.FC = ({ return; }} style={{ - ...(width !== undefined && {width}), - ...(height !== undefined && {height}), + ...(width !== undefined && { width }), + ...(height !== undefined && { height }), }} data-tip={isDisabled ? "This exam is still being evaluated..." : "This exam is still locked by its assigner..."} role="button"> @@ -297,8 +297,8 @@ const StatsGridItem: React.FC = ({ )} data-tip="Your screen size is too small to view previous exams." style={{ - ...(width !== undefined && {width}), - ...(height !== undefined && {height}), + ...(width !== undefined && { width }), + ...(height !== undefined && { height }), }} role="button"> {content} diff --git a/src/components/Solutions/FillBlanks.tsx b/src/components/Solutions/FillBlanks.tsx index baa7dc04..e39b4780 100644 --- a/src/components/Solutions/FillBlanks.tsx +++ b/src/components/Solutions/FillBlanks.tsx @@ -1,15 +1,15 @@ -import {FillBlanksExercise, FillBlanksMCOption, ShuffleMap} from "@/interfaces/exam"; +import { FillBlanksExercise, FillBlanksMCOption, ShuffleMap } from "@/interfaces/exam"; import clsx from "clsx"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import {Fragment} from "react"; +import { CommonProps } from "."; +import { Fragment } from "react"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; import { typeCheckWordsMC } from "@/utils/type.check"; -export default function FillBlanksSolutions({id, type, prompt, solutions, words, text, onNext, onBack}: FillBlanksExercise & CommonProps) { +export default function FillBlanksSolutions({ id, type, prompt, solutions, words, text, onNext, onBack, disableProgressButtons = false }: FillBlanksExercise & CommonProps) { const storeUserSolutions = useExamStore((state) => state.userSolutions); - const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state); + const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const correctUserSolutions = storeUserSolutions.find((solution) => solution.exercise === id)?.solutions; @@ -42,7 +42,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words, return false; }).length; const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; const renderLines = (line: string) => { @@ -81,20 +81,20 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words, typeof w === "string" ? w.toLowerCase() === userSolution.solution.toLowerCase() : "letter" in w - ? w.letter.toLowerCase() === userSolution.solution.toLowerCase() - : "options" in w - ? w.id === userSolution.questionId - : false, + ? w.letter.toLowerCase() === userSolution.solution.toLowerCase() + : "options" in w + ? w.id === userSolution.questionId + : false, ); const userSolutionText = typeof userSolutionWord === "string" ? userSolutionWord : userSolutionWord && "letter" in userSolutionWord - ? userSolutionWord.word - : userSolutionWord && "options" in userSolutionWord - ? userSolution.solution - : userSolution.solution; + ? userSolutionWord.word + : userSolutionWord && "options" in userSolutionWord + ? userSolution.solution + : userSolution.solution; let correct; let solutionText; @@ -149,27 +149,31 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words, ); }; + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{correctUserSolutions && text.split("\\n").map((line, index) => ( @@ -195,23 +199,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Solutions/MatchSentences.tsx b/src/components/Solutions/MatchSentences.tsx index 76c0f551..ec7e43b1 100644 --- a/src/components/Solutions/MatchSentences.tsx +++ b/src/components/Solutions/MatchSentences.tsx @@ -1,11 +1,11 @@ -import {MatchSentenceExerciseSentence, MatchSentencesExercise} from "@/interfaces/exam"; +import { MatchSentenceExerciseSentence, MatchSentencesExercise } from "@/interfaces/exam"; import clsx from "clsx"; import LineTo from "react-lineto"; -import {CommonProps} from "."; -import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; -import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import { CommonProps } from "."; +import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles"; +import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import Icon from "@mdi/react"; -import {Fragment} from "react"; +import { Fragment } from "react"; import Button from "../Low/Button"; import Xarrow from "react-xarrows"; import useExamStore from "@/stores/examStore"; @@ -15,7 +15,7 @@ function QuestionSolutionArea({ userSolution, }: { question: MatchSentenceExerciseSentence; - userSolution?: {question: string; option: string}; + userSolution?: { question: string; option: string }; }) { return (
@@ -26,8 +26,8 @@ function QuestionSolutionArea({ !userSolution ? "bg-mti-gray-davy" : userSolution.option.toString() === question.solution.toString() - ? "bg-mti-purple" - : "bg-mti-rose", + ? "bg-mti-purple" + : "bg-mti-rose", "transition duration-300 ease-in-out", )}> {question.id} @@ -40,8 +40,8 @@ function QuestionSolutionArea({ !userSolution ? "border-mti-gray-davy" : userSolution.option.toString() === question.solution.toString() - ? "border-mti-purple" - : "border-mti-rose", + ? "border-mti-purple" + : "border-mti-rose", )}> {userSolution && userSolution?.option.toString() !== question.solution.toString() && `Paragraph ${userSolution.option}`} @@ -61,8 +61,9 @@ export default function MatchSentencesSolutions({ userSolutions, onNext, onBack, + disableProgressButtons = false }: MatchSentencesExercise & CommonProps) { - const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state); + const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const calculateScore = () => { const total = sentences.length; @@ -71,30 +72,34 @@ export default function MatchSentencesSolutions({ ).length; const missing = total - userSolutions.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -128,23 +133,7 @@ export default function MatchSentencesSolutions({
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Solutions/MultipleChoice.tsx b/src/components/Solutions/MultipleChoice.tsx index ecec93b4..81fbed60 100644 --- a/src/components/Solutions/MultipleChoice.tsx +++ b/src/components/Solutions/MultipleChoice.tsx @@ -1,11 +1,11 @@ /* eslint-disable @next/next/no-img-element */ -import {MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap} from "@/interfaces/exam"; +import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; +import { CommonProps } from "."; import Button from "../Low/Button"; -import {v4} from "uuid"; +import { v4 } from "uuid"; function Question({ id, @@ -14,8 +14,8 @@ function Question({ solution, options, userSolution, -}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) { - const {userSolutions} = useExamStore((state) => state); +}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) { + const { userSolutions } = useExamStore((state) => state); const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { if (foundMap) return foundMap; @@ -89,8 +89,8 @@ function Question({ ); } -export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) { - const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state); +export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: MultipleChoiceExercise & CommonProps) { + const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const stats = useExamStore((state) => state.userSolutions); @@ -107,12 +107,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio } }).length; const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; const next = () => { if (questionIndex + 1 >= questions.length - 1) { - onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type}); + onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type }); } else { setQuestionIndex(questionIndex + 2); } @@ -120,50 +120,68 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio const back = () => { if (questionIndex === 0) { - onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type}); + onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type }); } else { setQuestionIndex(questionIndex - 2); } }; - return ( -
-
- + const progressButtons = () => ( +
+ - + +
+ ) + + const renderAllQuestions = () => + questions.map(question => ( +
+ question.id === x.question)?.option} + /> +
+ )) + + const renderTwoQuestions = () => ( + <> +
+ {questionIndex < questions.length && ( + questions[questionIndex].id === x.question)?.option} + /> + )}
-
-
-
- {/*{prompt}*/} - {userSolutions && questionIndex < questions.length && ( - questions[questionIndex].id === x.question)?.option} - /> - )} -
- - {userSolutions && questionIndex + 1 < questions.length && ( -
- questions[questionIndex + 1].id === x.question)?.option} - /> -
- )} + {questionIndex + 1 < questions.length && ( +
+ questions[questionIndex + 1].id === x.question)?.option} + />
+ )} + + ) + + return ( +
+ {!disableProgressButtons && progressButtons()} + +
+ {disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
@@ -181,20 +199,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx index 31905dc6..54f3c307 100644 --- a/src/components/Solutions/TrueFalse.tsx +++ b/src/components/Solutions/TrueFalse.tsx @@ -1,15 +1,15 @@ -import {FillBlanksExercise, TrueFalseExercise} from "@/interfaces/exam"; +import { FillBlanksExercise, TrueFalseExercise } from "@/interfaces/exam"; import clsx from "clsx"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import {Fragment} from "react"; +import { CommonProps } from "."; +import { Fragment } from "react"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; type Solution = "true" | "false" | "not_given"; -export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) { - const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state); +export default function TrueFalseSolution({ prompt, type, id, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: TrueFalseExercise & CommonProps) { + const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const calculateScore = () => { const total = questions.length || 0; @@ -18,7 +18,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu ).length; const missing = total - userSolutions.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; const getButtonColor = (buttonSolution: Solution, solution: Solution, userSolution: Solution | undefined) => { @@ -39,27 +39,31 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu return "gray"; }; + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -137,23 +141,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Solutions/WriteBlanks.tsx b/src/components/Solutions/WriteBlanks.tsx index ff55f4f2..3cdc23db 100644 --- a/src/components/Solutions/WriteBlanks.tsx +++ b/src/components/Solutions/WriteBlanks.tsx @@ -1,12 +1,12 @@ -import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles"; -import {WriteBlanksExercise} from "@/interfaces/exam"; -import {mdiArrowLeft, mdiArrowRight} from "@mdi/js"; +import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles"; +import { WriteBlanksExercise } from "@/interfaces/exam"; +import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import Icon from "@mdi/react"; import clsx from "clsx"; -import {Fragment, useEffect, useState} from "react"; +import { Fragment, useEffect, useState } from "react"; import reactStringReplace from "react-string-replace"; -import {CommonProps} from "."; -import {toast} from "react-toastify"; +import { CommonProps } from "."; +import { toast } from "react-toastify"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; @@ -71,8 +71,9 @@ export default function WriteBlanksSolutions({ text, onNext, onBack, + disableProgressButtons = false }: WriteBlanksExercise & CommonProps) { - const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state); + const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state); const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; @@ -85,7 +86,7 @@ export default function WriteBlanksSolutions({ ).length; const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; - return {total, correct, missing}; + return { total, correct, missing }; }; const renderLines = (line: string) => { @@ -104,27 +105,31 @@ export default function WriteBlanksSolutions({ ); }; + const progressButtons = () => ( +
+ + + +
+ ) + return (
-
- + {!disableProgressButtons && progressButtons()} - -
- -
+
{prompt.split("\\n").map((line, index) => ( @@ -158,23 +163,7 @@ export default function WriteBlanksSolutions({
-
- - - -
+ {!disableProgressButtons && progressButtons()}
); } diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx index 6e5c243d..db457290 100644 --- a/src/components/Solutions/index.tsx +++ b/src/components/Solutions/index.tsx @@ -19,25 +19,27 @@ import TrueFalseSolution from "./TrueFalse"; import WriteBlanks from "./WriteBlanks"; import Writing from "./Writing"; -const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false}); +const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), { ssr: false }); export interface CommonProps { onNext: (userSolutions: UserSolution) => void; onBack: (userSolutions: UserSolution) => void; + disableProgressButtons?: boolean, } -export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void) => { +export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void, + disableProgressButtons?: boolean) => { switch (exercise.type) { case "fillBlanks": - return ; + return ; case "trueFalse": - return ; + return ; case "matchSentences": - return ; + return ; case "multipleChoice": - return ; + return ; case "writeBlanks": - return ; + return ; case "writing": return ; case "speaking": diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index 8c3f9a3b..eb0c333b 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -326,11 +326,9 @@ export default function Finish({ user, scores, modules, information, solutions, )}
- - - +
)} diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 1e4a9788..1998c3ea 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -1,5 +1,5 @@ -import { ListeningExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam"; -import { useEffect, useState } from "react"; +import { ListeningExam, MultipleChoiceExercise, Script, UserSolution } from "@/interfaces/exam"; +import { Fragment, useEffect, useState } from "react"; import { renderExercise } from "@/components/Exercises"; import { renderSolution } from "@/components/Solutions"; import ModuleTitle from "@/components/Medium/ModuleTitle"; @@ -9,6 +9,9 @@ import BlankQuestionsModal from "@/components/QuestionsModal"; import useExamStore, { usePersistentExamStore } from "@/stores/examStore"; import { countExercises } from "@/utils/moduleUtils"; import PartDivider from "./Navigation/SectionDivider"; +import { Dialog, Transition } from "@headlessui/react"; +import { capitalize } from "lodash"; +import { mapBy } from "@/utils"; interface Props { exam: ListeningExam; @@ -17,17 +20,76 @@ interface Props { onFinish: (userSolutions: UserSolution[]) => void; } +function ScriptModal({ isOpen, script, onClose }: { isOpen: boolean; script: Script; onClose: () => void }) { + return ( + + + +
+ + +
+
+ + +
+

+ {typeof script === "string" && script.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} + + {typeof script === "object" && script.map((line, index) => ( + + {line.name} ({capitalize(line.gender)}): {line.text} +
+
+
+ ))} +

+
+ +
+ +
+
+
+
+
+
+
+ ); +} + const INSTRUCTIONS_AUDIO_SRC = "https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82"; export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) { const listeningBgColor = "bg-ielts-listening-light"; + const [showTextModal, setShowTextModal] = useState(false); const [timesListened, setTimesListened] = useState(0); const [showBlankModal, setShowBlankModal] = useState(false); const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); - const [seenParts, setSeenParts] = useState>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [])); const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== ""); @@ -99,75 +161,35 @@ export default function Listening({ exam, showSolutions = false, preview = false }; const nextExercise = (solution?: UserSolution) => { - scrollToTop(); - if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]); - } - if (storeQuestionIndex > 0) { - const exercise = getExercise(); - setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), { id: exercise.id, amount: storeQuestionIndex }]); - } - setStoreQuestionIndex(0); + if (solution) + setUserSolutions([ + ...userSolutions.filter((x) => x.exercise !== solution.exercise), + { ...solution, module: "listening", exam: exam.id } + ]); + }; - if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { - setExerciseIndex(exerciseIndex + 1); - return; - } + const previousExercise = (solution?: UserSolution) => { }; + const nextPart = () => { if (partIndex + 1 < exam.parts.length && !hasExamEnded) { setPartIndex(partIndex + 1); - setTimesListened(0); - setExerciseIndex(showSolutions ? 0 : -1); + setExerciseIndex(0); return; } - if ( - solution && - ![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every( - (x) => x === 0, - ) && - !showSolutions && - !hasExamEnded && - !preview - ) { - setShowBlankModal(true); - return; + if (!showSolutions && !hasExamEnded) { + const exercises = partIndex < exam.parts.length ? exam.parts[partIndex].exercises : [] + const exerciseIDs = mapBy(exercises, 'id') + + const hasMissing = userSolutions.filter(x => exerciseIDs.includes(x.exercise)).map(x => x.score.missing).some(x => x > 0) + + if (hasMissing) return setShowBlankModal(true); + } setHasExamEnded(false); - - if (solution) { - onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]); - } else { - onFinish(userSolutions); - } - }; - - const previousExercise = (solution?: UserSolution) => { - scrollToTop(); - if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]); - } - setStoreQuestionIndex(0); - - setExerciseIndex(exerciseIndex - 1); - }; - - const getExercise = () => { - const exercise = exam.parts[partIndex].exercises[exerciseIndex]; - return { - ...exercise, - userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], - }; - }; - - useEffect(() => { - if (partIndex > -1 && exerciseIndex > -1) { - const exercise = getExercise(); - setMultipleChoicesDone((prev) => prev.filter((x) => x.id !== exercise.id)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [exerciseIndex, partIndex]); + onFinish(userSolutions); + } const calculateExerciseIndex = () => { if (partIndex === -1) return 0; @@ -186,6 +208,22 @@ export default function Listening({ exam, showSolutions = false, preview = false ); }; + const renderPartExercises = () => { + const exercises = partIndex > -1 ? exam.parts[partIndex].exercises : [] + const formattedExercises = exercises.map(exercise => ({ + ...exercise, + userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], + })) + + return ( +
+ {formattedExercises.map(e => showSolutions + ? renderSolution(e, nextExercise, previousExercise, undefined, true) + : renderExercise(e, exam.id, nextExercise, previousExercise, undefined, true))} +
+ ) + } + const renderAudioInstructionsPlayer = () => (
@@ -201,16 +239,28 @@ export default function Listening({ exam, showSolutions = false, preview = false
{exam?.parts[partIndex]?.audio?.source ? ( <> -
-

Please listen to the following audio attentively.

- - {(() => { - const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; - return audioRepeatTimes && audioRepeatTimes > 0 - ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` - : "You may listen to the audio as many times as you would like."; - })()} - +
+
+

Please listen to the following audio attentively.

+ + {(() => { + const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; + return audioRepeatTimes && audioRepeatTimes > 0 + ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` + : "You may listen to the audio as many times as you would like."; + })()} + +
+ {partIndex > -1 && !examState.assignment && !!exam.parts[partIndex].script && ( + + )}
); + const progressButtons = () => ( +
+ + + +
+ ) + return ( <> {showPartDivider ? @@ -244,14 +313,19 @@ export default function Listening({ exam, showSolutions = false, preview = false /> : ( <> + {partIndex > -1 && exam.parts[partIndex].script && + setShowTextModal(false)} /> + }
x.exercises))} + totalExercises={exam.parts.length} disableTimer={showSolutions || preview} + indexLabel="Part" /> + {/* Audio Player for the Instructions */} {partIndex === -1 && renderAudioInstructionsPlayer()} @@ -259,18 +333,14 @@ export default function Listening({ exam, showSolutions = false, preview = false {partIndex > -1 && renderAudioPlayer()} {/* Exercise renderer */} - {exerciseIndex > -1 && - partIndex > -1 && - exerciseIndex < exam.parts[partIndex].exercises.length && - !showSolutions && - renderExercise(getExercise(), exam.id, nextExercise, previousExercise)} - {/* Solution renderer */} - {exerciseIndex > -1 && - partIndex > -1 && - exerciseIndex < exam.parts[partIndex].exercises.length && - showSolutions && - renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)} + {exerciseIndex > -1 && partIndex > -1 && ( + <> + {progressButtons()} + {renderPartExercises()} + {progressButtons()} + + )}
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && ( @@ -295,12 +365,12 @@ export default function Listening({ exam, showSolutions = false, preview = false )} {partIndex === -1 && exam.variant !== "partial" && ( - )} {exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && ( - )} diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 38154b0a..321ba9fa 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -90,6 +90,7 @@ export interface UserSolution { exercise: string; isDisabled?: boolean; shuffleMaps?: ShuffleMap[]; + isPractice?: boolean } export interface WritingExam extends ExamBase { @@ -165,6 +166,7 @@ export interface WritingExercise extends Section { }[]; topic?: string; variant?: string; + isPractice?: boolean } export interface AIDetectionAttributes { @@ -199,6 +201,7 @@ export interface SpeakingExercise extends Section { evaluation?: SpeakingEvaluation; }[]; topic?: string; + isPractice?: boolean } export interface InteractiveSpeakingExercise extends Section { @@ -218,6 +221,7 @@ export interface InteractiveSpeakingExercise extends Section { first_topic?: string; second_topic?: string; variant?: "initial" | "final"; + isPractice?: boolean } export interface FillBlanksMCOption { @@ -246,6 +250,7 @@ export interface FillBlanksExercise { solution: string; // *EXAMPLE: "preserve" }[]; variant?: string; + isPractice?: boolean } export interface TrueFalseExercise { @@ -254,6 +259,7 @@ export interface TrueFalseExercise { prompt: string; // *EXAMPLE: "Select the appropriate option." questions: TrueFalseQuestion[]; userSolutions: { id: string; solution: "true" | "false" | "not_given" }[]; + isPractice?: boolean } export interface TrueFalseQuestion { @@ -277,6 +283,7 @@ export interface WriteBlanksExercise { solution: string; }[]; variant?: string; + isPractice?: boolean } export interface MatchSentencesExercise { @@ -288,6 +295,7 @@ export interface MatchSentencesExercise { allowRepetition: boolean; options: MatchSentenceExerciseOption[]; variant?: string; + isPractice?: boolean } export interface MatchSentenceExerciseSentence { @@ -311,7 +319,8 @@ export interface MultipleChoiceExercise { passage?: { title: string; content: string; - } + } + isPractice?: boolean } export interface MultipleChoiceQuestion { diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 54fba92a..8b5245e1 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,6 +1,6 @@ -import {Module} from "."; -import {InstructorGender, ShuffleMap} from "./exam"; -import {PermissionType} from "./permissions"; +import { Module } from "."; +import { InstructorGender, ShuffleMap } from "./exam"; +import { PermissionType } from "./permissions"; export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser; export type UserStatus = "active" | "disabled" | "paymentDue"; @@ -12,8 +12,8 @@ export interface BasicUser { id: string; isFirstLogin: boolean; focus: "academic" | "general"; - levels: {[key in Module]: number}; - desiredLevels: {[key in Module]: number}; + levels: { [key in Module]: number }; + desiredLevels: { [key in Module]: number }; type: Type; bio: string; isVerified: boolean; @@ -22,7 +22,7 @@ export interface BasicUser { status: UserStatus; permissions: PermissionType[]; lastLogin?: Date; - entities: {id: string; role: string}[]; + entities: { id: string; role: string }[]; } export interface StudentUser extends BasicUser { @@ -109,13 +109,13 @@ export interface DemographicCorporateInformation { export type Gender = "male" | "female" | "other"; export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other"; -export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [ - {status: "student", label: "Student"}, - {status: "employed", label: "Employed"}, - {status: "unemployed", label: "Unemployed"}, - {status: "self-employed", label: "Self-employed"}, - {status: "retired", label: "Retired"}, - {status: "other", label: "Other"}, +export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] = [ + { status: "student", label: "Student" }, + { status: "employed", label: "Employed" }, + { status: "unemployed", label: "Unemployed" }, + { status: "self-employed", label: "Self-employed" }, + { status: "retired", label: "Retired" }, + { status: "other", label: "Other" }, ]; export interface Stat { @@ -142,6 +142,7 @@ export interface Stat { path: string; version: string; }; + isPractice?: boolean } export interface Group { @@ -174,4 +175,4 @@ export interface Code { export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate"; export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; -export type WithUser = T extends {participants: string[]} ? Omit & {participants: User[]} : T; +export type WithUser = T extends { participants: string[] } ? Omit & { participants: User[] } : T; diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 480310ac..2cd6b108 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -11,7 +11,6 @@ import Reading from "@/exams/Reading"; import Selection from "@/exams/Selection"; import Speaking from "@/exams/Speaking"; import Writing from "@/exams/Writing"; -import useUser from "@/hooks/useUser"; import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam"; import { Stat, User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; @@ -21,12 +20,7 @@ import axios from "axios"; import { useRouter } from "next/router"; import { toast, ToastContainer } from "react-toastify"; import { v4 as uuidv4 } from "uuid"; -import useSessions from "@/hooks/useSessions"; 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 { page: "exams" | "exercises"; @@ -214,7 +208,6 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba }, [setModuleIndex, showSolutions]); useEffect(() => { - console.log(selectedModules) if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { const nextExam = exams[moduleIndex]; @@ -264,6 +257,7 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba isDisabled: solution.isDisabled, shuffleMaps: solution.shuffleMaps, ...(assignment ? { assignment: assignment.id } : {}), + isPractice: solution.isPractice })); 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 = x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); diff --git a/src/pages/login.tsx b/src/pages/login.tsx index d03ffe4a..09fee825 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,44 +1,45 @@ /* eslint-disable @next/next/no-img-element */ -import {User} from "@/interfaces/user"; -import {toast, ToastContainer} from "react-toastify"; +import { User } from "@/interfaces/user"; +import { toast, ToastContainer } from "react-toastify"; import axios from "axios"; -import {FormEvent, useEffect, useState} from "react"; +import { FormEvent, useEffect, useMemo, useState } from "react"; import Head from "next/head"; import useUser from "@/hooks/useUser"; -import {Divider} from "primereact/divider"; +import { Divider } from "primereact/divider"; import Button from "@/components/Low/Button"; -import {BsArrowRepeat, BsCheck} from "react-icons/bs"; +import { BsArrowRepeat, BsCheck } from "react-icons/bs"; import Link from "next/link"; import Input from "@/components/Low/Input"; import clsx from "clsx"; -import {useRouter} from "next/router"; +import { useRouter } from "next/router"; import EmailVerification from "./(auth)/EmailVerification"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; import { requestUser } from "@/utils/api"; import { redirect } from "@/utils"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g); -export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { +export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => { const destination = !query.destination ? "/" : Buffer.from(query.destination as string, 'base64').toString() const user = await requestUser(req, res) if (user) return redirect(destination) return { - props: {user: null, destination}, + props: { user: null, destination }, }; }, sessionOptions); -export default function Login({ destination }: { destination: string }) { +export default function Login({ destination = "/" }: { destination?: string }) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberPassword, setRememberPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); + const isOfficialExamLogin = useMemo(() => destination.startsWith("/official-exam"), [destination]) - const {user, mutateUser} = useUser({ + const { user, mutateUser } = useUser({ redirectTo: destination, redirectIfFound: true, }); @@ -56,10 +57,10 @@ export default function Login({ destination }: { destination: string }) { } axios - .post<{ok: boolean}>("/api/reset", {email}) + .post<{ ok: boolean }>("/api/reset", { email }) .then((response) => { if (response.data.ok) { - toast.success("You should receive an e-mail to reset your password!", {toastId: "forgot-success"}); + toast.success("You should receive an e-mail to reset your password!", { toastId: "forgot-success" }); return; } @@ -79,7 +80,7 @@ export default function Login({ destination }: { destination: string }) { setIsLoading(true); axios - .post("/api/login", {email, password}) + .post("/api/login", { email, password }) .then((response) => { toast.success("You have been logged in!", { toastId: "login-successful", @@ -92,7 +93,7 @@ export default function Login({ destination }: { destination: string }) { toastId: "wrong-credentials", }); } else { - toast.error("Something went wrong!", {toastId: "server-error"}); + toast.error("Something went wrong!", { toastId: "server-error" }); } setIsLoading(false); }) @@ -110,14 +111,25 @@ export default function Login({ destination }: { destination: string }) {
- {/*
*/} - People smiling looking at a tablet + {!isOfficialExamLogin && ( + People smiling looking at a tablet + )} + {isOfficialExamLogin && ( + People smiling looking at a tablet + )}
EnCoach's Logo -

Login to your account

-

with your registered Email Address

+ {!isOfficialExamLogin && ( + <> +

Login to your account

+

with your registered Email Address

+ + )} + {isOfficialExamLogin && ( +

Welcome to the Official Exams Portal

+ )}
{!user && ( diff --git a/src/pages/record.tsx b/src/pages/record.tsx index 5acf236b..f4d29152 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -1,27 +1,27 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {Stat, User} from "@/interfaces/user"; -import {useEffect, useMemo, useState} from "react"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Stat, User } from "@/interfaces/user"; +import { useEffect, useMemo, useState } from "react"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import {groupByDate} from "@/utils/stats"; +import { groupByDate } from "@/utils/stats"; import moment from "moment"; import useExamStore from "@/stores/examStore"; -import {ToastContainer} from "react-toastify"; +import { ToastContainer } from "react-toastify"; import Layout from "@/components/High/Layout"; import clsx from "clsx"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {uuidv4} from "@firebase/util"; -import {usePDFDownload} from "@/hooks/usePDFDownload"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { uuidv4 } from "@firebase/util"; +import { usePDFDownload } from "@/hooks/usePDFDownload"; import useRecordStore from "@/stores/recordStore"; import StatsGridItem from "@/components/Medium/StatGridItem"; import RecordFilter from "@/components/Medium/RecordFilter"; -import {useRouter} from "next/router"; +import { useRouter } from "next/router"; import useTrainingContentStore from "@/stores/trainingContentStore"; -import {Assignment} from "@/interfaces/results"; -import {getEntitiesUsers, getUsers} from "@/utils/users.be"; -import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be"; +import { Assignment } from "@/interfaces/results"; +import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; import useGradingSystem from "@/hooks/useGrading"; import { mapBy, redirect, serialize } from "@/utils"; import { getEntitiesWithRoles } from "@/utils/entities.be"; @@ -33,7 +33,7 @@ import { EntityWithRoles } from "@/interfaces/entity"; import CardList from "@/components/High/CardList"; 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) 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 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 gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity)) return { - props: serialize({user, users, assignments, entities, gradingSystems}), + props: serialize({ user, users, assignments, entities }), }; }, sessionOptions); @@ -58,13 +56,12 @@ interface Props { user: User; users: User[]; assignments: Assignment[]; - gradingSystems: Grading[] entities: EntityWithRoles[] } 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 [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [ state.selectedUser, @@ -75,8 +72,8 @@ export default function History({user, users, assignments, entities, gradingSyst const [filter, setFilter] = useState(); - const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser(statsUserId || user?.id); - const {gradingSystem} = useGradingSystem(); + const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser(statsUserId || user?.id); + const { gradingSystem } = useGradingSystem(); const setExams = useExamStore((state) => state.setExams); const setShowSolutions = useExamStore((state) => state.setShowSolutions); @@ -113,12 +110,12 @@ export default function History({user, users, assignments, entities, gradingSyst }; }, [router.events, setTraining]); - const filterStatsByDate = (stats: {[key: string]: Stat[]}) => { + const filterStatsByDate = (stats: { [key: string]: Stat[] }) => { if (filter && filter !== "assignments") { const filterDate = moment() - .subtract({[filter as string]: 1}) + .subtract({ [filter as string]: 1 }) .format("x"); - const filteredStats: {[key: string]: Stat[]} = {}; + const filteredStats: { [key: string]: Stat[] } = {}; Object.keys(stats).forEach((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") { - const filteredStats: {[key: string]: Stat[]} = {}; + const filteredStats: { [key: string]: Stat[] } = {}; Object.keys(stats).forEach((timestamp) => { 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; }; -const handleTrainingContentSubmission = () => { - if (groupedStats) { - const groupedStatsByDate = filterStatsByDate(groupedStats); - const allStats = Object.keys(groupedStatsByDate); - const selectedStats = selectedTrainingExams.reduce>((accumulator, moduleAndTimestamp) => { - const timestamp = moduleAndTimestamp.split("-")[1]; - if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) { - accumulator[timestamp] = groupedStatsByDate[timestamp]; - } - return accumulator; - }, {}); - setTrainingStats(Object.values(selectedStats).flat()); - router.push("/training"); - } -}; + const handleTrainingContentSubmission = () => { + if (groupedStats) { + const groupedStatsByDate = filterStatsByDate(groupedStats); + const allStats = Object.keys(groupedStatsByDate); + const selectedStats = selectedTrainingExams.reduce>((accumulator, moduleAndTimestamp) => { + const timestamp = moduleAndTimestamp.split("-")[1]; + if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) { + accumulator[timestamp] = groupedStatsByDate[timestamp]; + } + return accumulator; + }, {}); + setTrainingStats(Object.values(selectedStats).flat()); + router.push("/training"); + } + }; const filteredStats = useMemo(() => Object.keys(filterStatsByDate(groupedStats)) @@ -203,7 +200,7 @@ const handleTrainingContentSubmission = () => { {user && ( - + {training && (