Merged develop into ENCOA-83_MasterStatistical

This commit is contained in:
João Ramos
2024-08-20 23:39:57 +00:00
28 changed files with 5976 additions and 3637 deletions

2943
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
import {Dialog, Transition} from "@headlessui/react";
import {Fragment} from "react";
import Button from "./Low/Button";
interface Props {
isOpen: boolean;
onClose: (next?: boolean) => void;
}
export default function BlankQuestionsModal({isOpen, onClose}: Props) {
return (
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={() => onClose(false)} className="relative z-50">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-2xl h-fit p-8 rounded-xl bg-white flex flex-col gap-4">
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
<span>
Please note that you are finishing the current module and once you proceed to the next module, you will no longer be
able to change the answers in the current one, including your unanswered questions. <br />
<br />
Are you sure you want to continue without completing those questions?
</span>
<div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back
</Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full">
Continue
</Button>
</div>
</Dialog.Panel>
</div>
</Transition.Child>
</Dialog>
</Transition>
);
}

View File

@@ -20,7 +20,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
onNext, onNext,
onBack, onBack,
}) => { }) => {
//const { shuffleMaps } = useExamStore((state) => state); const { shuffleMaps, exam, partIndex, questionIndex, exerciseIndex } = useExamStore((state) => state);
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions); const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
const hasExamEnded = useExamStore((state) => state.hasExamEnded); const hasExamEnded = useExamStore((state) => state.hasExamEnded);
@@ -41,19 +41,24 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]); }, [hasExamEnded]);
let correctWords: any;
if (exam && exam.module === "level" && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") {
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
}
const calculateScore = () => { const calculateScore = () => {
const total = text.match(/({{\d+}})/g)?.length || 0; const total = text.match(/({{\d+}})/g)?.length || 0;
const correct = userSolutions.filter((x) => { const correct = answers!.filter((x) => {
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
if (!solution) return false; if (!solution) return false;
const option = correctWords!.find((w: any) => {
const option = words.find((w) => {
if (typeof w === "string") { if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase(); return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) { } else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase(); return w.word.toLowerCase() === x.solution.toLowerCase();
} else { } else {
return w.id === x.id; return w.id.toString() === x.id.toString();
} }
}); });
if (!option) return false; if (!option) return false;
@@ -63,25 +68,13 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
} else if ('letter' in option) { } else if ('letter' in option) {
return solution.toLowerCase() === option.word.toLowerCase(); return solution.toLowerCase() === option.word.toLowerCase();
} else if ('options' in option) { } else if ('options' in option) {
/* return option.options[solution as keyof typeof option.options] == x.solution;
if (shuffleMaps.length !== 0) {
const shuffleMap = shuffleMaps.find((map) => map.id == x.id)
if (!shuffleMap) {
return false;
}
const original = shuffleMap[x.solution as keyof typeof shuffleMap];
return solution.toLowerCase() === (option.options[original as keyof typeof option.options] || '').toLowerCase();
}*/
return solution.toLowerCase() === (option.options[x.solution as keyof typeof option.options] || '').toLowerCase();
} }
return false; return false;
}).length; }).length;
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).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) => { const renderLines = (line: string) => {
return ( return (
<div className="text-base leading-5"> <div className="text-base leading-5">
@@ -132,7 +125,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]); setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id: id, solution: value }]);
} }
/*const getShuffles = () => { const getShuffles = () => {
let shuffle = {}; let shuffle = {};
if (shuffleMaps.length !== 0) { if (shuffleMaps.length !== 0) {
shuffle = { shuffle = {
@@ -142,7 +135,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
} }
} }
return shuffle; return shuffle;
}*/ }
return ( return (
<> <>
@@ -227,14 +220,18 @@ 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, })}//...getShuffles() })} onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full"
disabled={
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
typeof exam.parts[0].intro === "string" && questionIndex === 0}
>
Back Back
</Button> </Button>
<Button <Button
color="purple" color="purple"
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, })}//...getShuffles() })} onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() })}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>

View File

@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { CommonProps } from "."; import { CommonProps } from ".";
import Button from "../Low/Button"; import Button from "../Low/Button";
import { v4 } from "uuid";
function Question({ function Question({
id, id,
@@ -30,9 +31,9 @@ function Question({
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{isNaN(Number(id)) ? ( {isNaN(Number(id)) ? (
<span className={clsx(true ? "text-lg" : "text-base")}>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span> <span className="text-lg">{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
) : ( ) : (
<span className={clsx(true ? "text-lg" : "text-base")}> <span className="text-lg">
<> <>
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span> {id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
</> </>
@@ -42,10 +43,10 @@ function Question({
{variant === "image" && {variant === "image" &&
options.map((option) => ( options.map((option) => (
<div <div
key={option.id.toString()} key={v4()}
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)} onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
className={clsx( className={clsx(
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative", "flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative select-none",
userSolution === option.id.toString() && "border-mti-purple-light", userSolution === option.id.toString() && "border-mti-purple-light",
)}> )}>
<span className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>{option.id.toString()}</span> <span className={clsx("text-sm", userSolution !== option.id.toString() && "opacity-50")}>{option.id.toString()}</span>
@@ -55,10 +56,10 @@ function Question({
{variant === "text" && {variant === "text" &&
options.map((option) => ( options.map((option) => (
<div <div
key={option.id.toString()} key={v4()}
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)} onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
className={clsx( className={clsx(
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base", "flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base select-none",
userSolution === option.id.toString() && "border-mti-purple-light", userSolution === option.id.toString() && "border-mti-purple-light",
)}> )}>
<span className="font-semibold">{option.id.toString()}.</span> <span className="font-semibold">{option.id.toString()}.</span>
@@ -73,10 +74,15 @@ function Question({
export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { export default function MultipleChoice({ id, prompt, type, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions); const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
//const { shuffleMaps } = useExamStore((state) => state); const {
const { questionIndex, setQuestionIndex } = useExamStore((state) => state); questionIndex,
const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state); exam,
const hasExamEnded = useExamStore((state) => state.hasExamEnded); shuffleMaps,
hasExamEnded,
userSolutions: storeUserSolutions,
setQuestionIndex,
setUserSolutions
} = useExamStore((state) => state);
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));
@@ -106,12 +112,12 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
}); });
let isSolutionCorrect; let isSolutionCorrect;
//if (shuffleMaps.length == 0) { if (shuffleMaps.length == 0) {
isSolutionCorrect = matchingQuestion?.solution === x.option; isSolutionCorrect = matchingQuestion?.solution === x.option;
//} else { } else {
// const shuffleMap = shuffleMaps.find((map) => map.id == x.question) const shuffleMap = shuffleMaps.find((map) => map.id == x.question)
// isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution; isSolutionCorrect = shuffleMap?.map[x.option] == matchingQuestion?.solution;
//} }
return isSolutionCorrect || false; return isSolutionCorrect || false;
}).length; }).length;
const missing = total - correct; const missing = total - correct;
@@ -119,7 +125,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
return { total, correct, missing }; return { total, correct, missing };
}; };
/*const getShuffles = () => { const getShuffles = () => {
let shuffle = {}; let shuffle = {};
if (shuffleMaps.length !== 0) { if (shuffleMaps.length !== 0) {
shuffle = { shuffle = {
@@ -129,11 +135,11 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
} }
} }
return shuffle; return shuffle;
}*/ }
const next = () => { const next = () => {
if (questionIndex === questions.length - 1) { if (questionIndex === questions.length - 1) {
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, });//...getShuffles() }); onNext({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
} else { } else {
setQuestionIndex(questionIndex + 1); setQuestionIndex(questionIndex + 1);
} }
@@ -142,7 +148,7 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
const back = () => { const back = () => {
if (questionIndex === 0) { if (questionIndex === 0) {
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, });// ...getShuffles() }); onBack({ exercise: id, solutions: answers, score: calculateScore(), type, ...getShuffles() });
} else { } else {
setQuestionIndex(questionIndex - 1); setQuestionIndex(questionIndex - 1);
} }
@@ -164,7 +170,10 @@ export default function MultipleChoice({ id, prompt, type, questions, userSoluti
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"
disabled={
exam && exam.module === "level" && typeof exam.parts[0].intro === "string" && questionIndex === 0}
>
Back Back
</Button> </Button>

View File

@@ -11,14 +11,15 @@ interface Props {
className?: string; className?: string;
navDisabled?: boolean; navDisabled?: boolean;
focusMode?: boolean; focusMode?: boolean;
bgColor?: string;
onFocusLayerMouseEnter?: () => void; onFocusLayerMouseEnter?: () => void;
} }
export default function Layout({user, children, className, navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) { export default function Layout({user, children, className, bgColor="bg-white", navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) {
const router = useRouter(); const router = useRouter();
return ( return (
<main className="w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative"> <main className={clsx("w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative")}>
<Navbar <Navbar
path={router.pathname} path={router.pathname}
user={user} user={user}
@@ -37,7 +38,8 @@ export default function Layout({user, children, className, navDisabled = false,
/> />
<div <div
className={clsx( className={clsx(
"w-full min-h-full h-fit md:mr-8 bg-white shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2", `w-full min-h-full md:mr-8 ${bgColor} shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2`,
bgColor !== "bg-white" ? "justify-center" : "h-fit",
className, className,
)}> )}>
{children} {children}

View File

@@ -7,6 +7,7 @@ import {ReactNode, useEffect, useState} from "react";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs"; import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch} from "react-icons/bs";
import ProgressBar from "../Low/ProgressBar"; import ProgressBar from "../Low/ProgressBar";
import TimerEndedModal from "../TimerEndedModal"; import TimerEndedModal from "../TimerEndedModal";
import Timer from "./Timer";
interface Props { interface Props {
minTimer: number; minTimer: number;
@@ -16,35 +17,12 @@ interface Props {
totalExercises: number; totalExercises: number;
disableTimer?: boolean; disableTimer?: boolean;
partLabel?: string; partLabel?: string;
showTimer?: boolean;
} }
export default function ModuleTitle({minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel}: Props) { export default function ModuleTitle({
const [timer, setTimer] = useState(minTimer * 60); minTimer, module, label, exerciseIndex, totalExercises, disableTimer = false, partLabel, showTimer = true
const [showModal, setShowModal] = useState(false); }: Props) {
const [warningMode, setWarningMode] = useState(false);
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
const {timeSpent} = useExamStore((state) => state);
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
useEffect(() => {
if (!disableTimer) {
const timerInterval = setInterval(() => setTimer((prev) => prev - 1), 1000);
return () => {
clearInterval(timerInterval);
};
}
}, [disableTimer, minTimer]);
useEffect(() => {
if (timer <= 0) setShowModal(true);
}, [timer]);
useEffect(() => {
if (timer < 300 && !warningMode) setWarningMode(true);
}, [timer, warningMode]);
const moduleIcon: {[key in Module]: ReactNode} = { const moduleIcon: {[key in Module]: ReactNode} = {
reading: <BsBook className="text-ielts-reading w-6 h-6" />, reading: <BsBook className="text-ielts-reading w-6 h-6" />,
@@ -56,37 +34,7 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
return ( return (
<> <>
<TimerEndedModal {showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
isOpen={showModal}
onClose={() => {
setHasExamEnded(true);
setShowModal(false);
}}
/>
<motion.div
className={clsx(
"absolute top-4 right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
)}
initial={{scale: warningMode && !disableTimer ? 0.8 : 1}}
animate={{scale: warningMode && !disableTimer ? 1.1 : 1}}
transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}>
<BsStopwatch className="w-6 h-6" />
<span className="text-base font-semibold w-12">
{timer > 0 && (
<>
{Math.floor(timer / 60)
.toString(10)
.padStart(2, "0")}
:
{Math.floor(timer % 60)
.toString(10)
.padStart(2, "0")}
</>
)}
{timer <= 0 && <>00:00</>}
</span>
</motion.div>
<div className="w-full"> <div className="w-full">
{partLabel && ( {partLabel && (
<div className="text-3xl space-y-4"> <div className="text-3xl space-y-4">

View File

@@ -0,0 +1,80 @@
import useExamStore from "@/stores/examStore";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import TimerEndedModal from "../TimerEndedModal";
import clsx from "clsx";
import { BsStopwatch } from "react-icons/bs";
interface Props {
minTimer: number;
disableTimer?: boolean;
standalone?: boolean;
}
const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) => {
const [timer, setTimer] = useState(minTimer * 60);
const [showModal, setShowModal] = useState(false);
const [warningMode, setWarningMode] = useState(false);
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
const { timeSpent } = useExamStore((state) => state);
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
useEffect(() => {
if (!disableTimer) {
const timerInterval = setInterval(() => setTimer((prev) => prev - 1), 1000);
return () => {
clearInterval(timerInterval);
};
}
}, [disableTimer, minTimer]);
useEffect(() => {
if (timer <= 0) setShowModal(true);
}, [timer]);
useEffect(() => {
if (timer < 300 && !warningMode) setWarningMode(true);
}, [timer, warningMode]);
return (
<>
<TimerEndedModal
isOpen={showModal}
onClose={() => {
setHasExamEnded(true);
setShowModal(false);
}}
/>
<motion.div
className={clsx(
"absolute right-6 bg-mti-gray-seasalt px-4 py-3 flex items-center gap-2 rounded-full text-mti-gray-davy",
standalone ? "top-6" : "top-4",
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
)}
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
<BsStopwatch className="w-6 h-6" />
<span className="text-base font-semibold w-12">
{timer > 0 && (
<>
{Math.floor(timer / 60)
.toString(10)
.padStart(2, "0")}
:
{Math.floor(timer % 60)
.toString(10)
.padStart(2, "0")}
</>
)}
{timer <= 0 && <>00:00</>}
</span>
</motion.div>
</>
);
}
export default Timer;

View File

@@ -0,0 +1,80 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react";
import Button from "./Low/Button";
interface Props {
isOpen: boolean;
blankQuestions?: boolean;
finishingWhat? : string;
onClose: (next?: boolean) => void;
}
export default function QuestionsModal({ isOpen, onClose, blankQuestions = true, finishingWhat = "module" }: Props) {
return (
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={() => onClose(false)} className="relative z-50">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-2xl h-fit p-8 rounded-xl bg-white flex flex-col gap-4">
{blankQuestions ? (
<>
<Dialog.Title className="font-bold text-xl">Questions Unanswered</Dialog.Title>
<span>
Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be
able to change the answers of the current one, including your unanswered questions. <br />
<br />
Are you sure you want to continue without completing those questions?
</span>
<div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back
</Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full">
Continue
</Button>
</div>
</>
): (
<>
<Dialog.Title className="font-bold text-xl">Confirm Submission</Dialog.Title>
<span>
Please note that you are finishing the current {finishingWhat} and once you proceed to the next {finishingWhat}, you will no longer be
able to review the answers of the current one. <br />
<br />
Are you sure you want to continue?
</span>
<div className="w-full flex justify-between mt-8">
<Button color="purple" onClick={() => onClose(false)} variant="outline" className="max-w-[200px] self-end w-full">
Go Back
</Button>
<Button color="purple" onClick={() => onClose(true)} className="max-w-[200px] self-end w-full">
Continue
</Button>
</div>
</>
)}
</Dialog.Panel>
</div>
</Transition.Child>
</Dialog>
</Transition>
);
}

View File

@@ -4,6 +4,7 @@ import reactStringReplace from "react-string-replace";
import { CommonProps } from "."; import { CommonProps } from ".";
import { Fragment } from "react"; import { Fragment } from "react";
import Button from "../Low/Button"; import Button from "../Low/Button";
import useExamStore from "@/stores/examStore";
export default function FillBlanksSolutions({ export default function FillBlanksSolutions({
id, id,
@@ -12,14 +13,22 @@ export default function FillBlanksSolutions({
solutions, solutions,
words, words,
text, text,
userSolutions,
onNext, onNext,
onBack, onBack,
}: FillBlanksExercise & CommonProps) { }: FillBlanksExercise & CommonProps) {
// next and back was all messed up and still don't know why, anyways
const storeUserSolutions = useExamStore((state) => state.userSolutions);
const correctUserSolutions = storeUserSolutions.find(
(solution) => solution.exercise === id
)?.solutions;
const calculateScore = () => { const calculateScore = () => {
const total = text.match(/({{\d+}})/g)?.length || 0; const total = text.match(/({{\d+}})/g)?.length || 0;
const correct = userSolutions.filter((x) => { const correct = correctUserSolutions!.filter((x) => {
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
console.log(solution);
if (!solution) return false; if (!solution) return false;
const option = words.find((w) => { const option = words.find((w) => {
@@ -28,7 +37,7 @@ export default function FillBlanksSolutions({
} else if ('letter' in w) { } else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase(); return w.word.toLowerCase() === x.solution.toLowerCase();
} else { } else {
return w.id === x.id; return w.id.toString() === x.id.toString();
} }
}); });
if (!option) return false; if (!option) return false;
@@ -42,9 +51,7 @@ export default function FillBlanksSolutions({
} }
return false; return false;
}).length; }).length;
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).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 };
}; };
@@ -60,16 +67,27 @@ export default function FillBlanksSolutions({
<span> <span>
{reactStringReplace(line, /({{\d+}})/g, (match) => { {reactStringReplace(line, /({{\d+}})/g, (match) => {
const id = match.replaceAll(/[\{\}]/g, ""); const id = match.replaceAll(/[\{\}]/g, "");
const userSolution = userSolutions.find((x) => x.id === id); const userSolution = correctUserSolutions!.find((x) => x.id.toString() === id.toString());
const solution = solutions.find((x) => x.id === id)!; const answerSolution = solutions.find(sol => sol.id.toString() === id.toString())!.solution;
if (!userSolution) { if (!userSolution) {
let answerText;
if (typeCheckWordsMC(words)) {
const options = words.find((x) => x.id.toString() === id.toString());
const correctKey = Object.keys(options!.options).find(key =>
key.toLowerCase() === answerSolution.toLowerCase()
);
answerText = options!.options[correctKey as keyof typeof options];
} else {
answerText = answerSolution;
}
return ( return (
<button <button
className={clsx( className={clsx(
"rounded-full hover:text-white hover:bg-mti-gray-davy transition duration-300 ease-in-out my-1 px-5 py-2 text-center text-white bg-mti-gray-davy", "rounded-full hover:text-white hover:bg-mti-gray-davy transition duration-300 ease-in-out my-1 px-5 py-2 text-center text-white bg-mti-gray-davy",
)}> )}>
{solution?.solution} {answerText}
</button> </button>
); );
} }
@@ -96,21 +114,21 @@ export default function FillBlanksSolutions({
let correct; let correct;
let solutionText; let solutionText;
if (typeCheckWordsMC(words)) { if (typeCheckWordsMC(words)) {
const options = words.find((x) => x.id === id); const options = words.find((x) => x.id.toString() === id.toString());
if (options) { if (options) {
const correctKey = Object.keys(options.options).find(key => const correctKey = Object.keys(options.options).find(key =>
key.toLowerCase() === solution.solution.toLowerCase() key.toLowerCase() === answerSolution.toLowerCase()
); );
correct = userSolution.solution == options.options[correctKey as keyof typeof options.options]; correct = userSolution.solution == options.options[correctKey as keyof typeof options.options];
solutionText = options.options[correctKey as keyof typeof options.options] || solution.solution; solutionText = options.options[correctKey as keyof typeof options.options] || answerSolution;
} else { } else {
correct = false; correct = false;
solutionText = solution?.solution; solutionText = answerSolution;
} }
} else { } else {
correct = userSolutionText === solution.solution; correct = userSolutionText === answerSolution;
solutionText = solution.solution; solutionText = answerSolution;
} }
if (correct) { if (correct) {
@@ -152,16 +170,8 @@ export default function FillBlanksSolutions({
return ( return (
<> <>
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20"> <div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
<span className="text-sm w-full leading-6">
{prompt.split("\\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</span>
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6"> <span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{userSolutions && {correctUserSolutions &&
text.split("\\n").map((line, index) => ( text.split("\\n").map((line, index) => (
<p key={index}> <p key={index}>
{renderLines(line)} {renderLines(line)}
@@ -189,14 +199,14 @@ export default function FillBlanksSolutions({
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })} onClick={() => onBack({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
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: userSolutions, score: calculateScore(), type })} onClick={() => onNext({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
className="max-w-[200px] self-end w-full"> className="max-w-[200px] self-end w-full">
Next Next
</Button> </Button>

View File

@@ -17,7 +17,6 @@ function Question({
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) { }: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) {
const { userSolutions } = useExamStore((state) => state); const { userSolutions } = useExamStore((state) => state);
/*
const getShuffledOptions = (options: { id: string, text: string }[], questionShuffleMap: ShuffleMap) => { const getShuffledOptions = (options: { id: string, text: string }[], questionShuffleMap: ShuffleMap) => {
const shuffledOptions = ['A', 'B', 'C', 'D'].map(newId => { const shuffledOptions = ['A', 'B', 'C', 'D'].map(newId => {
const originalId = questionShuffleMap.map[newId]; const originalId = questionShuffleMap.map[newId];
@@ -43,10 +42,9 @@ function Question({
if (foundMap) return foundMap; if (foundMap) return foundMap;
return userSolution.shuffleMaps?.find(map => map.id === id) || null; return userSolution.shuffleMaps?.find(map => map.id === id) || null;
}, null as ShuffleMap | null); }, null as ShuffleMap | null);
*/
const questionOptions = options; // questionShuffleMap ? getShuffledOptions(options as {id: string, text: string}[], questionShuffleMap) : options; const questionOptions = questionShuffleMap ? getShuffledOptions(options as { id: string, text: string }[], questionShuffleMap) : options;
const newSolution = solution; //questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution; const newSolution = questionShuffleMap ? getShuffledSolution(solution, questionShuffleMap) : solution;
const renderPrompt = (prompt: string) => { const renderPrompt = (prompt: string) => {
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => { return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
@@ -68,23 +66,23 @@ function Question({
}; };
return ( return (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col gap-4">
{isNaN(Number(id)) ? ( {isNaN(Number(id)) ? (
<span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span> <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
) : ( ) : (
<span className=""> <span className="text-lg">
<> <>
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span> {id} - <span className="text-lg">{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
</> </>
</span> </span>
)} )}
<div className="grid grid-cols-4 gap-4 place-items-center"> <div className="flex flex-wrap gap-4 justify-between">
{variant === "image" && {variant === "image" &&
questionOptions.map((option) => ( questionOptions.map((option) => (
<div <div
key={option?.id} key={option?.id}
className={clsx( className={clsx(
"flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative", "flex flex-col items-center border border-mti-gray-platinum p-4 px-8 rounded-xl gap-4 cursor-pointer bg-white relative select-none",
optionColor(option!.id), optionColor(option!.id),
)}> )}>
<span className={clsx("text-sm", newSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span> <span className={clsx("text-sm", newSolution !== option?.id && userSolution !== option?.id && "opacity-50")}>{option?.id}</span>
@@ -95,7 +93,7 @@ function Question({
questionOptions.map((option) => ( questionOptions.map((option) => (
<div <div
key={option?.id} key={option?.id}
className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-sm", optionColor(option!.id))}> className={clsx("flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base select-none", optionColor(option!.id))}>
<span className="font-semibold">{option?.id}.</span> <span className="font-semibold">{option?.id}.</span>
<span>{option?.text}</span> <span>{option?.text}</span>
</div> </div>
@@ -106,7 +104,8 @@ function Question({
} }
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) { export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack }: MultipleChoiceExercise & CommonProps) {
const { questionIndex, setQuestionIndex } = useExamStore((state) => state); const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
const calculateScore = () => { const calculateScore = () => {
const total = questions.length; const total = questions.length;
@@ -138,7 +137,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
<> <>
<div className="flex flex-col gap-4 w-full h-full mb-20"> <div className="flex flex-col gap-4 w-full h-full mb-20">
<div className="flex flex-col gap-2 mt-4 h-full bg-mti-gray-smoke rounded-xl px-16 py-8"> <div className="flex flex-col gap-2 mt-4 h-full bg-mti-gray-smoke rounded-xl px-16 py-8">
<span className="text-xl font-semibold">{prompt}</span> {/*<span className="text-xl font-semibold">{prompt}</span>*/}
{userSolutions && questionIndex < questions.length && ( {userSolutions && questionIndex < questions.length && (
<Question <Question
{...questions[questionIndex]} {...questions[questionIndex]}
@@ -163,7 +162,11 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
</div> </div>
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8"> <div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"> <Button color="purple" variant="outline" onClick={back} className="max-w-[200px] w-full"
disabled={
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
typeof exam.parts[0].intro === "string" && questionIndex === 0 && partIndex === 0}
>
Back Back
</Button> </Button>

View File

@@ -22,7 +22,7 @@ import {
BsPlus, BsPlus,
BsPersonFillGear, BsPersonFillGear,
BsFilter, BsFilter,
BsDatabase BsDatabase,
} from "react-icons/bs"; } from "react-icons/bs";
import UserCard from "@/components/UserCard"; import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
@@ -309,21 +309,11 @@ export default function MasterCorporateDashboard({user}: Props) {
const {codes} = useCodes(user.id); const {codes} = useCodes(user.id);
const {groups} = useGroups({admin: user.id, userType: user.type}); const {groups} = useGroups({admin: user.id, userType: user.type});
const masterCorporateUserGroups = [ const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
...new Set(
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [ const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
...new Set(groups.flatMap((g) => g.participants)),
];
const { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ corporate: user.id });
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -350,13 +340,8 @@ export default function MasterCorporateDashboard({user}: Props) {
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
onClick={() => setSelectedUser(displayUser)} onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
> <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -366,10 +351,7 @@ export default function MasterCorporateDashboard({user}: Props) {
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -379,8 +361,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -393,10 +374,7 @@ export default function MasterCorporateDashboard({user}: Props) {
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -406,8 +384,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -419,10 +396,7 @@ export default function MasterCorporateDashboard({user}: Props) {
}; };
const corporateUserFilter = (x: User) => const corporateUserFilter = (x: User) =>
x.type === "corporate" && x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
(!!selectedUser
? masterCorporateUserGroups.includes(x.id) || false
: masterCorporateUserGroups.includes(x.id));
const CorporateList = () => { const CorporateList = () => {
return ( return (
@@ -433,8 +407,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -451,8 +424,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -646,22 +618,18 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<h2 className="text-2xl font-semibold">Master Statistical</h2> <h2 className="text-2xl font-semibold">Master Statistical</h2>
</div> </div>
<MasterStatistical <MasterStatistical
users={masterCorporateUserGroups.reduce( users={masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => {
(accm: CorporateUser[], id) => {
const user = users.find((u) => u.id === id) as CorporateUser; const user = users.find((u) => u.id === id) as CorporateUser;
if (user) return [...accm, user]; if (user) return [...accm, user];
return accm; return accm;
}, }, [])}
[]
)}
/> />
</> </>
); );
@@ -726,13 +694,13 @@ export default function MasterCorporateDashboard({user}: Props) {
color="purple" color="purple"
onClick={() => setPage("studentsPerformance")} onClick={() => setPage("studentsPerformance")}
/> />
<IconCard {/* <IconCard
Icon={BsDatabase} Icon={BsDatabase}
label="Master Statistical" label="Master Statistical"
// value={masterCorporateUserGroups.length} // value={masterCorporateUserGroups.length}
color="purple" color="purple"
onClick={() => setPage("statistical")} onClick={() => setPage("statistical")}
/> /> */}
<button <button
disabled={isAssignmentsLoading} disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")} onClick={() => setPage("assignments")}
@@ -775,11 +743,7 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort( .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -792,8 +756,7 @@ export default function MasterCorporateDashboard({user}: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -817,8 +780,7 @@ export default function MasterCorporateDashboard({user}: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "teacher"
selectedUser.type === "teacher"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-students", id: "view-students",
@@ -828,11 +790,7 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });
@@ -842,8 +800,7 @@ export default function MasterCorporateDashboard({user}: Props) {
: undefined : undefined
} }
onViewTeachers={ onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "student"
selectedUser.type === "student"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-teachers", id: "view-teachers",
@@ -853,11 +810,7 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });

View File

@@ -2,7 +2,7 @@ import React from "react";
import {CorporateUser} from "@/interfaces/user"; import {CorporateUser} from "@/interfaces/user";
import {BsBank, BsPersonFill} from "react-icons/bs"; import {BsBank, BsPersonFill} from "react-icons/bs";
import IconCard from "./IconCard"; import IconCard from "./IconCard";
import useAssignmentsCorporates from '@/hooks/useAssignmentCorporates'; import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates";
interface Props { interface Props {
users: CorporateUser[]; users: CorporateUser[];
} }
@@ -13,16 +13,9 @@ const MasterStatistical = (props: Props) => {
const {assignments} = useAssignmentsCorporates({corporates: usersList}); const {assignments} = useAssignmentsCorporates({corporates: usersList});
console.log('Assignments', assignments);
return ( return (
<div className="flex flex-wrap gap-2 items-center text-center"> <div className="flex flex-wrap gap-2 items-center text-center">
<IconCard <IconCard Icon={BsBank} label="Consolidate" value={0} color="purple" onClick={() => console.log("clicked")} />
Icon={BsBank}
label="Consolidate"
value={0}
color="purple"
onClick={() => console.log("clicked")}
/>
{users.map((group) => ( {users.map((group) => (
<IconCard <IconCard
key={group.id} key={group.id}
@@ -33,12 +26,7 @@ const MasterStatistical = (props: Props) => {
onClick={() => console.log("clicked", group)} onClick={() => console.log("clicked", group)}
/> />
))} ))}
<IconCard <IconCard onClick={() => console.log("clicked")} Icon={BsPersonFill} label="Consolidate Highest Student" color="purple" />
onClick={() => console.log("clicked")}
Icon={BsPersonFill}
label="Consolidate Highest Student"
color="purple"
/>
</div> </div>
); );
}; };

View File

@@ -1,504 +0,0 @@
import BlankQuestionsModal from "@/components/BlankQuestionsModal";
import {renderExercise} from "@/components/Exercises";
import HighlightContent from "@/components/HighlightContent";
import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import {renderSolution} from "@/components/Solutions";
import {infoButtonStyle} from "@/constants/buttonStyles";
import {Module} from "@/interfaces";
import {
Exercise,
FillBlanksExercise,
FillBlanksMCOption,
LevelExam,
LevelPart,
MultipleChoiceExercise,
ShuffleMap,
UserSolution,
WritingExam,
} from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import {defaultUserSolutions} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils";
import {mdiArrowRight} from "@mdi/js";
import Icon from "@mdi/react";
import clsx from "clsx";
import {Dispatch, Fragment, SetStateAction, use, useEffect, useMemo, useRef, useState} from "react";
import {BsChevronDown, BsChevronUp} from "react-icons/bs";
import {toast} from "react-toastify";
import {v4} from "uuid";
interface Props {
exam: LevelExam;
showSolutions?: boolean;
onFinish: (userSolutions: UserSolution[]) => void;
editing?: boolean;
}
function TextComponent({
part,
contextWord,
setContextWordLine,
}: {
part: LevelPart;
contextWord: string | undefined;
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>;
}) {
const textRef = useRef<HTMLDivElement>(null);
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
const [lineHeight, setLineHeight] = useState<number>(0);
part.showContextLines = true;
const calculateLineNumbers = () => {
if (textRef.current) {
const computedStyle = window.getComputedStyle(textRef.current);
const lineHeightValue = parseFloat(computedStyle.lineHeight);
const containerWidth = textRef.current.clientWidth;
setLineHeight(lineHeightValue);
const offscreenElement = document.createElement("div");
offscreenElement.style.position = "absolute";
offscreenElement.style.top = "-9999px";
offscreenElement.style.left = "-9999px";
offscreenElement.style.whiteSpace = "pre-wrap";
offscreenElement.style.width = `${containerWidth}px`;
offscreenElement.style.font = computedStyle.font;
offscreenElement.style.lineHeight = computedStyle.lineHeight;
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
const textContent = textRef.current.textContent || "";
textContent.split(/(\s+)/).forEach((word: string) => {
const span = document.createElement("span");
span.textContent = word;
span.style.display = "inline-block";
span.style.height = `calc(1em + 16px)`;
offscreenElement.appendChild(span);
});
document.body.appendChild(offscreenElement);
const lines: string[][] = [[]];
let currentLine = 1;
let currentLineTop: number | undefined;
let contextWordLine: number | null = null;
const firstChild = offscreenElement.firstChild as HTMLElement;
if (firstChild) {
currentLineTop = firstChild.getBoundingClientRect().top;
}
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>("span");
spans.forEach((span) => {
const rect = span.getBoundingClientRect();
const top = rect.top;
if (currentLineTop !== undefined && top > currentLineTop) {
currentLine++;
currentLineTop = top;
lines.push([]);
}
lines[lines.length - 1].push(span.textContent?.trim() || "");
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
contextWordLine = currentLine;
}
});
setLineNumbers(lines.map((_, index) => index + 1));
if (contextWordLine) {
setContextWordLine(contextWordLine);
}
document.body.removeChild(offscreenElement);
}
};
useEffect(() => {
calculateLineNumbers();
const resizeObserver = new ResizeObserver(() => {
calculateLineNumbers();
});
if (textRef.current) {
resizeObserver.observe(textRef.current);
}
return () => {
if (textRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
resizeObserver.unobserve(textRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [part.context, part.showContextLines, contextWord]);
if (typeof part.showContextLines === "undefined") {
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
{!!part.context &&
part.context
.split(/\n|(\\n)/g)
.filter((x) => x && x.length > 0 && x !== "\\n")
.map((line, index) => (
<Fragment key={index}>
<p key={index}>{line}</p>
</Fragment>
))}
</div>
);
}
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
<div className="flex mt-2">
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
{part.context!.split("\n\n").map((line, index) => {
return (
<p key={`line-${index}`}>
<span className="mr-6">{index + 1}</span>
{line}
</p>
);
})}
</div>
</div>
</div>
);
}
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word);
};
export default function Level({exam, showSolutions = false, onFinish, editing = false}: Props) {
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
const [showBlankModal, setShowBlankModal] = useState(false);
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
//const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
const [currentExercise, setCurrentExercise] = useState<Exercise>();
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
/*useEffect(() => {
if (showSolutions && userSolutions[exerciseIndex].shuffleMaps) {
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
}
}, [showSolutions])*/
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
}
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const confirmFinishModule = (keepGoing?: boolean) => {
if (!keepGoing) {
setShowBlankModal(false);
return;
}
onFinish(userSolutions);
};
const getExercise = () => {
if (exerciseIndex === -1) {
return undefined;
}
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
if (!exercise) return undefined;
exercise = {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
/*if (exam.shuffle && exercise.type === "multipleChoice") {
if (shuffleMaps.length == 0 && !showSolutions) {
console.log("Shuffling answers");
const newShuffleMaps: ShuffleMap[] = [];
exercise.questions = exercise.questions.map(question => {
const options = [...question.options];
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
const newOptions = options.map((option, index) => ({
id: option.id,
text: shuffledOptions[index].text
}));
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
if (shuffledPosition) {
acc[shuffledPosition] = originalOption.id;
}
return acc;
}, {});
newShuffleMaps.push({ id: question.id, map: optionMapping });
return { ...question, options: newOptions };
});
setShuffleMaps(newShuffleMaps);
} else {
console.log("Retrieving shuffles");
exercise.questions = exercise.questions.map(question => {
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
if (questionShuffleMap) {
const newOptions = question.options.map(option => ({
id: option.id,
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
}));
return { ...question, options: newOptions };
}
return question;
});
}
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words)) {
if (shuffleMaps.length === 0 && !showSolutions) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.words = exercise.words.map(word => {
if ('options' in word) {
const options = { ...word.options };
const originalKeys = Object.keys(options);
const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5);
const newOptions = shuffledKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
return acc;
}, {} as { [key in keyof typeof options]: string });
const optionMapping = originalKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = shuffledKeys[index];
return acc;
}, {} as { [key in keyof typeof options]: string });
newShuffleMaps.push({ id: word.id, map: optionMapping });
return { ...word, options: newOptions };
}
return word;
});
setShuffleMaps(newShuffleMaps);
}
}
*/
return exercise;
};
useEffect(() => {
setCurrentExercise(getExercise());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [partIndex, exerciseIndex]);
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (currentExercise && currentExercise.type === "multipleChoice") {
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`,
);
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else {
setContextWord(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [storeQuestionIndex, contextWordLine, exerciseIndex, partIndex]); //, shuffleMaps]);
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
}
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
setMultipleChoicesDone((prev) => [
...prev.filter((x) => x.id !== currentExercise!.id),
{id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex},
]);
}
setStoreQuestionIndex(0);
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
setExerciseIndex(exerciseIndex + 1);
return;
}
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
setPartIndex(partIndex + 1);
setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0);
return;
}
if (
solution &&
![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every(
(x) => x === 0,
) &&
!showSolutions &&
!editing &&
!hasExamEnded
) {
setShowBlankModal(true);
return;
}
setHasExamEnded(false);
if (solution) {
let stat = {...solution, module: "level" as Module, exam: exam.id};
/*if (exam.shuffle) {
stat.shuffleMaps = shuffleMaps
}*/
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...stat}]);
} else {
onFinish(userSolutions);
}
};
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]);
}
if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
setMultipleChoicesDone((prev) => [
...prev.filter((x) => x.id !== currentExercise!.id),
{id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex},
]);
}
setStoreQuestionIndex(0);
setExerciseIndex(exerciseIndex - 1);
};
const calculateExerciseIndex = () => {
if (partIndex === 0)
return (
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex + multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
);
const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0);
return (
exercisesDone +
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
storeQuestionIndex +
multipleChoicesDone.reduce((acc, curr) => {
return acc + curr.amount;
}, 0)
);
};
const renderText = () => (
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
<>
<div className="flex flex-col w-full gap-2">
<h4 className="text-xl font-semibold">
Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read.
</h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
</div>
<TextComponent part={exam.parts[partIndex]} contextWord={contextWord} setContextWordLine={setContextWordLine} />
</>
</div>
);
const partLabel = () => {
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${
currentExercise.words[currentExercise.words.length - 1].id
})\n\n${currentExercise.prompt}`;
if (currentExercise?.type === "multipleChoice")
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${
currentExercise.questions[currentExercise.questions.length - 1].id
})\n\n${currentExercise.prompt}`;
};
return (
<>
<div className="flex flex-col h-full w-full gap-8 items-center">
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
<ModuleTitle
partLabel={partLabel()}
minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()}
module="level"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
disableTimer={showSolutions || editing}
/>
<div
className={clsx(
"mb-20 w-full",
partIndex > -1 && exerciseIndex > -1 && !!exam.parts[partIndex].context && "grid grid-cols-2 gap-4",
)}>
{partIndex > -1 && !!exam.parts[partIndex].context && renderText()}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
!editing &&
currentExercise &&
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
(showSolutions || editing) &&
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
</div>
{exerciseIndex === -1 && partIndex > 0 && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => {
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex(partIndex - 1);
}}
className="max-w-[200px] w-full">
Back
</Button>
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)}
{exerciseIndex === -1 && partIndex === 0 && (
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Start now
</Button>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,37 @@
import Button from "@/components/Low/Button";
import { Module } from "@/interfaces";
import { LevelPart, UserSolution } from "@/interfaces/exam";
import { ReactNode } from "react";
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
interface Props {
partIndex: number;
part: LevelPart // for now
onNext: () => void;
}
const PartDivider: React.FC<Props> = ({ partIndex, part, onNext }) => {
const moduleIcon: { [key in Module]: ReactNode } = {
reading: <BsBook className="text-ielts-reading w-6 h-6" />,
listening: <BsHeadphones className="text-ielts-listening w-6 h-6" />,
writing: <BsPen className="text-ielts-writing w-6 h-6" />,
speaking: <BsMegaphone className="text-ielts-speaking w-6 h-6" />,
level: <BsClipboard className="text-white w-6 h-6" />,
};
return (
<div className="flex flex-col w-3/6 h-fit border bg-white rounded-3xl p-12 gap-8">
{/** only level for now */}
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{`Part ${partIndex + 1}`}</p></div>
{part.intro!.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip">{x}</p>)}
<div className="flex items-center justify-center mt-4">
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
</Button>
</div>
</div>
)
}
export default PartDivider;

View File

@@ -0,0 +1,146 @@
import { LevelPart } from "@/interfaces/exam";
import { useEffect, useRef } from "react";
interface Props {
part: LevelPart,
contextWord: string | undefined,
setContextWordLine: React.Dispatch<React.SetStateAction<number | undefined>>
}
const TextComponent: React.FC<Props> = ({part, contextWord, setContextWordLine}) => {
const textRef = useRef<HTMLDivElement>(null);
const calculateLineNumbers = () => {
if (textRef.current) {
const computedStyle = window.getComputedStyle(textRef.current);
const containerWidth = textRef.current.clientWidth;
const offscreenElement = document.createElement('div');
offscreenElement.style.position = 'absolute';
offscreenElement.style.top = '-9999px';
offscreenElement.style.left = '-9999px';
offscreenElement.style.width = `${containerWidth}px`;
offscreenElement.style.font = computedStyle.font;
offscreenElement.style.lineHeight = computedStyle.lineHeight;
offscreenElement.style.whiteSpace = 'pre-wrap';
offscreenElement.style.wordWrap = 'break-word';
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
const paragraphs = part.context!.split('\n\n');
let currentLine = 1;
let contextWordLine: number | null = null;
const paragraphLineStarts: number[] = [];
paragraphs.forEach((paragraph, pIndex) => {
const p = document.createElement('p');
p.style.margin = '0';
p.style.padding = '0';
paragraph.split(/(\s+)/).forEach((word: string) => {
const span = document.createElement('span');
span.textContent = word;
p.appendChild(span);
});
offscreenElement.appendChild(p);
if (pIndex < paragraphs.length - 1) {
const gap = document.createElement('div');
gap.style.height = '16px'; // gap-4
offscreenElement.appendChild(gap);
}
});
document.body.appendChild(offscreenElement);
let currentLineTop: number | undefined;
const elements = offscreenElement.querySelectorAll('p, div');
elements.forEach((element) => {
if (element.tagName === 'P') {
const spans = element.querySelectorAll<HTMLSpanElement>('span');
paragraphLineStarts.push(currentLine);
spans.forEach(span => {
const rect = span.getBoundingClientRect();
const top = rect.top;
if (currentLineTop === undefined || top > currentLineTop) {
if (currentLineTop !== undefined) {
currentLine++;
}
currentLineTop = top;
}
if (contextWord && contextWordLine === null && span.textContent?.includes(contextWord)) {
contextWordLine = currentLine;
}
});
} else if (element.tagName === 'DIV') { // Gap
currentLine++;
currentLineTop = undefined;
}
});
if (contextWordLine) {
setContextWordLine(contextWordLine);
}
document.body.removeChild(offscreenElement);
}
};
useEffect(() => {
calculateLineNumbers();
const resizeObserver = new ResizeObserver(() => {
calculateLineNumbers();
});
if (textRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
resizeObserver.observe(textRef.current);
}
return () => {
if (textRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
resizeObserver.unobserve(textRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [part.context, contextWord]);
/*if (typeof part.showContextLines === "undefined") {
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
{!!part.context &&
part.context
.split(/\n|(\\n)/g)
.filter((x) => x && x.length > 0 && x !== "\\n")
.map((line, index) => (
<Fragment key={index}>
<p key={index}>{line}</p>
</Fragment>
))}
</div>
);
}*/
return (
<div className="flex flex-col gap-2 w-full">
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
<div className="flex mt-2">
<div ref={textRef} className="h-fit ml-2 flex flex-col gap-4">
{part.context!.split('\n\n').map((line, index) => {
return <p key={`line-${index}`}><span className="mr-6">{index + 1}</span>{line}</p>
})}
</div>
</div>
</div>
);
}
export default TextComponent;

422
src/exams/Level/index.tsx Normal file
View File

@@ -0,0 +1,422 @@
import QuestionsModal from "@/components/QuestionsModal";
import { renderExercise } from "@/components/Exercises";
import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import { renderSolution } from "@/components/Solutions";
import { Module } from "@/interfaces";
import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap, UserSolution } from "@/interfaces/exam";
import useExamStore from "@/stores/examStore";
import { countExercises } from "@/utils/moduleUtils";
import clsx from "clsx";
import { use, useEffect, useState } from "react";
import TextComponent from "./TextComponent";
import PartDivider from "./PartDivider";
import Timer from "@/components/Medium/Timer";
import { Stat } from "@/interfaces/user";
interface Props {
exam: LevelExam;
showSolutions?: boolean;
onFinish: (userSolutions: UserSolution[]) => void;
editing?: boolean;
partDividers?: boolean;
}
const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => {
return Array.isArray(words) && words.every(
word => word && typeof word === 'object' && 'id' in word && 'options' in word
);
}
export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) {
const levelBgColor = "bg-ielts-level-light";
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
const { setBgColor } = useExamStore((state) => state);
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state);
const { partIndex, setPartIndex } = useExamStore((state) => state);
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps])
const [currentExercise, setCurrentExercise] = useState<Exercise>();
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && !showSolutions);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const [contextWord, setContextWord] = useState<string | undefined>(undefined);
const [contextWordLine, setContextWordLine] = useState<number | undefined>(undefined);
useEffect(() => {
if (showSolutions && exerciseIndex && userSolutions[exerciseIndex].shuffleMaps) {
setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[])
}
}, [showSolutions, exerciseIndex, setShuffleMaps, userSolutions])
useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex(exerciseIndex + 1);
}
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const getExercise = () => {
let exercise = exam.parts[partIndex]?.exercises[exerciseIndex];
if (!exercise) return undefined;
exercise = {
...exercise,
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
};
if (exam.shuffle && exercise.type === "multipleChoice" && !showSolutions) {
console.log("Shuffling");
const exerciseShuffles = userSolutions[exerciseIndex].shuffleMaps;
if (exerciseShuffles && exerciseShuffles.length == 0) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.questions = exercise.questions.map(question => {
const options = [...question.options];
let shuffledOptions = [...options].sort(() => Math.random() - 0.5);
const newOptions = options.map((option, index) => ({
id: option.id,
text: shuffledOptions[index].text
}));
const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => {
const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id;
if (shuffledPosition) {
acc[shuffledPosition] = originalOption.id;
}
return acc;
}, {});
newShuffleMaps.push({ id: question.id, map: optionMapping });
return { ...question, options: newOptions };
});
setShuffleMaps(newShuffleMaps);
} else {
console.log("retrieving shuffles");
exercise.questions = exercise.questions.map(question => {
const questionShuffleMap = shuffleMaps.find(map => map.id === question.id);
if (questionShuffleMap) {
const newOptions = question.options.map(option => ({
id: option.id,
text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text
}));
return { ...question, options: newOptions };
}
return question;
});
}
} else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words) && !showSolutions) {
if (shuffleMaps.length === 0 && !showSolutions) {
const newShuffleMaps: ShuffleMap[] = [];
exercise.words = exercise.words.map(word => {
if ('options' in word) {
const options = { ...word.options };
const originalKeys = Object.keys(options);
const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5);
const newOptions = shuffledKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options];
return acc;
}, {} as { [key in keyof typeof options]: string });
const optionMapping = originalKeys.reduce((acc, key, index) => {
acc[key as keyof typeof options] = shuffledKeys[index];
return acc;
}, {} as { [key in keyof typeof options]: string });
newShuffleMaps.push({ id: word.id, map: optionMapping });
return { ...word, options: newOptions };
}
return word;
});
setShuffleMaps(newShuffleMaps);
}
}
return exercise;
};
useEffect(() => {
if (exerciseIndex !== -1) {
setCurrentExercise(getExercise());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [partIndex, exerciseIndex, shuffleMaps, exam.parts[partIndex].context]);
useEffect(() => {
const regex = /.*?['"](.*?)['"] in line (\d+)\?$/;
if (exerciseIndex !== -1 && currentExercise && currentExercise.type === "multipleChoice" && currentExercise.questions[storeQuestionIndex].prompt) {
const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex);
if (match) {
const word = match[1];
const originalLineNumber = match[2];
if (word !== contextWord) {
setContextWord(word);
}
const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace(
`in line ${originalLineNumber}`,
`in line ${contextWordLine || originalLineNumber}`
);
currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt;
} else {
setContextWord(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentExercise, storeQuestionIndex]);
const nextExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
}
/*if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") {
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
}*/
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
setExerciseIndex(exerciseIndex + 1);
return;
}
if (partIndex + 1 < exam.parts.length && !hasExamEnded && (showQuestionsModal || showSolutions)) {
if (!showSolutions && exam.parts[0].intro) {
setShowPartDivider(true);
setBgColor(levelBgColor);
}
setPartIndex(partIndex + 1);
setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0);
setStoreQuestionIndex(0);
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]);
return;
}
if (partIndex + 1 < exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions) {
setShowQuestionsModal(true);
return;
}
if (
solution &&
![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every(
(x) => x === 0,
) &&
!showSolutions &&
!editing &&
!hasExamEnded
) {
setShowQuestionsModal(true);
return;
}
setHasExamEnded(false);
if (solution) {
let stat = { ...solution, module: "level" as Module, exam: exam.id }
if (exam.shuffle) {
stat.shuffleMaps = shuffleMaps
}
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]);
} else {
onFinish(userSolutions);
}
};
const previousExercise = (solution?: UserSolution) => {
scrollToTop();
if (solution) {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]);
}
setExerciseIndex(exerciseIndex - 1);
if (exerciseIndex - 1 === -1) {
setPartIndex(partIndex - 1);
const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1;
const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex];
if (previousExercise.type === "multipleChoice") {
setStoreQuestionIndex(previousExercise.questions.length - 1)
}
const multipleChoiceQuestionsDone = [];
for (let i = 0; i < exam.parts.length; i++) {
if (i == (partIndex - 1)) break;
for (let j = 0; j < exam.parts[i].exercises.length; j++) {
const exercise = exam.parts[i].exercises[j];
if (exercise.type === "multipleChoice") {
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 })
}
if (exercise.type === "fillBlanks") {
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 })
}
}
}
setMultipleChoicesDone(multipleChoiceQuestionsDone);
}
};
useEffect(() => {
if (exerciseIndex === -1) {
nextExercise()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exerciseIndex])
const calculateExerciseIndex = () => {
if (partIndex === 0) {
return (
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0)
);
}
const exercisesPerPart = exam.parts.map((x) => x.exercises.length);
const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0);
return (
exercisesDone +
(exerciseIndex === -1 ? 0 : exerciseIndex + 1) +
storeQuestionIndex
+ multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0)
);
};
const renderText = () => (
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
<>
<div className="flex flex-col w-full gap-2">
<h4 className="text-xl font-semibold">
Please read the following excerpt attentively, you will then be asked questions about the text you&apos;ve read.
</h4>
<span className="text-base">You will be allowed to read the text while doing the exercises</span>
</div>
<TextComponent
part={exam.parts[partIndex]}
contextWord={contextWord}
setContextWordLine={setContextWordLine}
/>
</>
</div>
);
const partLabel = () => {
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})\n\n${currentExercise.prompt}`
if (currentExercise?.type === "multipleChoice") {
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})\n\n${currentExercise.prompt}`
}
if (typeof exam.parts[partIndex].context === "string") {
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})\n\n${nextExercise.prompt}`
}
}
const modalKwargs = () => {
const allSolutionsCorrectLength = exam.parts[partIndex].exercises.every((exercise) => {
const userSolution = userSolutions.find(x => x.exercise === exercise.id);
if (exercise.type === "multipleChoice") {
return userSolution?.solutions.length === exercise.questions.length;
}
if (exercise.type === "fillBlanks") {
return userSolution?.solutions.length === exercise.words.length;
}
return false;
});
return {
blankQuestions: !allSolutionsCorrectLength,
finishingWhat: "part",
onClose: partIndex !== exam.parts.length - 1 ? (
function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } }
) : function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); onFinish(userSolutions); } else { setShowQuestionsModal(false) } }
}
}
return (
<>
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
<QuestionsModal isOpen={showQuestionsModal} {...modalKwargs()} />
{
!(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) &&
<Timer minTimer={exam.minTimer} disableTimer={showSolutions} standalone={true} />
}
{exam.parts[0].intro && showPartDivider ? <PartDivider part={exam.parts[partIndex]} partIndex={partIndex} onNext={() => { setShowPartDivider(false); setBgColor("bg-white") }} /> : (
<>
<ModuleTitle
partLabel={partLabel()}
minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()}
module="level"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
disableTimer={showSolutions || editing}
showTimer={typeof exam.parts[0].intro === "undefined"}
/>
<div
className={clsx(
"mb-20 w-full",
partIndex > -1 && exerciseIndex > -1 && !!exam.parts[partIndex].context && "grid grid-cols-2 gap-4",
)}>
{partIndex > -1 && !!exam.parts[partIndex].context && renderText()}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions &&
!editing &&
currentExercise &&
renderExercise(currentExercise, exam.id, nextExercise, previousExercise)}
{exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length &&
(showSolutions || editing) &&
currentExercise &&
renderSolution(currentExercise, nextExercise, previousExercise)}
</div>
{/*exerciseIndex === -1 && partIndex > 0 && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => {
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex(partIndex - 1);
}}
className="max-w-[200px] w-full"
disabled={
exam && typeof partIndex !== "undefined" && exam.module === "level" &&
typeof exam.parts[0].intro === "string" && storeQuestionIndex === 0}
>
Back
</Button>
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)*/}
{exerciseIndex === -1 && partIndex === 0 && (
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Start now
</Button>
)}
</>
)}
</div>
</>
);
}

View File

@@ -5,7 +5,7 @@ import {renderSolution} from "@/components/Solutions";
import ModuleTitle from "@/components/Medium/ModuleTitle"; import ModuleTitle from "@/components/Medium/ModuleTitle";
import AudioPlayer from "@/components/Low/AudioPlayer"; import AudioPlayer from "@/components/Low/AudioPlayer";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import BlankQuestionsModal from "@/components/BlankQuestionsModal"; import BlankQuestionsModal from "@/components/QuestionsModal";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {countExercises} from "@/utils/moduleUtils"; import {countExercises} from "@/utils/moduleUtils";

View File

@@ -15,7 +15,7 @@ import ProgressBar from "@/components/Low/ProgressBar";
import ModuleTitle from "@/components/Medium/ModuleTitle"; import ModuleTitle from "@/components/Medium/ModuleTitle";
import {Divider} from "primereact/divider"; import {Divider} from "primereact/divider";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import BlankQuestionsModal from "@/components/BlankQuestionsModal"; import BlankQuestionsModal from "@/components/QuestionsModal";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {defaultUserSolutions} from "@/utils/exams"; import {defaultUserSolutions} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils"; import {countExercises} from "@/utils/moduleUtils";

View File

@@ -133,7 +133,7 @@ export default function Selection({user, page, onStart, disableSelection = false
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} /> <BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
</div> </div>
</div> </div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll"> <span className="text-mti-gray-taupe flex gap-8 overflow-x-auto pb-2">
{sessions {sessions
.sort((a, b) => moment(b.date).diff(moment(a.date))) .sort((a, b) => moment(b.date).diff(moment(a.date)))
.map((session) => ( .map((session) => (

View File

@@ -37,7 +37,7 @@ export interface LevelExam extends ExamBase {
export interface LevelPart { export interface LevelPart {
context?: string; context?: string;
showContextLines?: boolean; intro?: string;
exercises: Exercise[]; exercises: Exercise[];
} }

View File

@@ -14,7 +14,7 @@ import {toast} from "react-toastify";
import readXlsxFile from "read-excel-file"; import readXlsxFile from "read-excel-file";
import {useFilePicker} from "use-file-picker"; import {useFilePicker} from "use-file-picker";
import {getUserCorporate} from "@/utils/groups"; import {getUserCorporate} from "@/utils/groups";
import {isAgentUser, isCorporateUser} from "@/resources/user"; import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user";
import {checkAccess} from "@/utils/permissions"; import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
@@ -109,7 +109,7 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
const submit = () => { const submit = () => {
setIsLoading(true); setIsLoading(true);
if (name !== group?.name && (name === "Students" || name === "Teachers")) { if (name !== group?.name && (name?.trim() === "Students" || name?.trim() === "Teachers" || name?.trim() === "Corporate")) {
toast.error("That group name is reserved and cannot be used, please enter another one."); toast.error("That group name is reserved and cannot be used, please enter another one.");
setIsLoading(false); setIsLoading(false);
return; return;
@@ -202,7 +202,6 @@ const filterTypes = ["corporate", "teacher", "mastercorporate"];
export default function GroupList({user}: {user: User}) { export default function GroupList({user}: {user: User}) {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [editingGroup, setEditingGroup] = useState<Group>(); const [editingGroup, setEditingGroup] = useState<Group>();
const [filterByUser, setFilterByUser] = useState(false);
const {permissions} = usePermissions(user?.id || ""); const {permissions} = usePermissions(user?.id || "");
@@ -218,12 +217,6 @@ export default function GroupList({user}: {user: User}) {
adminAdmins: user?.id, adminAdmins: user?.id,
}); });
useEffect(() => {
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {
setFilterByUser(true);
}
}, [user]);
const deleteGroup = (group: Group) => { const deleteGroup = (group: Group) => {
if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return; if (!confirm(`Are you sure you want to delete "${group.name}"?`)) return;
@@ -246,7 +239,7 @@ export default function GroupList({user}: {user: User}) {
columnHelper.accessor("admin", { columnHelper.accessor("admin", {
header: "Admin", header: "Admin",
cell: (info) => ( cell: (info) => (
<div className="tooltip" data-tip={capitalize(users.find((x) => x.id === info.getValue())?.type)}> <div className="tooltip" data-tip={USER_TYPE_LABELS[users.find((x) => x.id === info.getValue())?.type || "student"]}>
{users.find((x) => x.id === info.getValue())?.name} {users.find((x) => x.id === info.getValue())?.name}
</div> </div>
), ),
@@ -309,7 +302,7 @@ export default function GroupList({user}: {user: User}) {
user={user} user={user}
onClose={closeModal} onClose={closeModal}
users={ users={
user?.type === "corporate" || user?.type === "teacher" checkAccess(user, ["corporate", "teacher", "mastercorporate"])
? users.filter( ? users.filter(
(u) => (u) =>
groups groups

View File

@@ -58,7 +58,10 @@ export default function UserList({
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {permissions} = usePermissions(user?.id || ""); const {permissions} = usePermissions(user?.id || "");
const {groups} = useGroups({admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined}); const {groups} = useGroups({
admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined,
userType: user?.type,
});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -76,8 +79,7 @@ export default function UserList({
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (user && users) { if (user && users) {
const filterUsers = const filterUsers = ["corporate", "teacher", "mastercorporate"].includes(user.type)
user.type === "corporate" || user.type === "teacher"
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id)) ? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
: users; : users;

View File

@@ -23,6 +23,7 @@ import {toast, ToastContainer} from "react-toastify";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import useSessions from "@/hooks/useSessions"; import useSessions from "@/hooks/useSessions";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import clsx from "clsx";
interface Props { interface Props {
page: "exams" | "exercises"; page: "exams" | "exercises";
@@ -54,6 +55,7 @@ export default function ExamPage({page}: Props) {
const {showSolutions, setShowSolutions} = useExamStore((state) => state); const {showSolutions, setShowSolutions} = useExamStore((state) => state);
const {selectedModules, setSelectedModules} = useExamStore((state) => state); const {selectedModules, setSelectedModules} = useExamStore((state) => state);
const {inactivity, setInactivity} = useExamStore((state) => state); const {inactivity, setInactivity} = useExamStore((state) => state);
const {bgColor, setBgColor} = useExamStore((state) => state);
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
const router = useRouter(); const router = useRouter();
@@ -280,6 +282,13 @@ export default function ExamPage({page}: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [statsAwaitingEvaluation]); }, [statsAwaitingEvaluation]);
useEffect(()=> {
if(exam && exam.module === "level" && exam.parts[0].intro && !showSolutions) {
setBgColor("bg-ielts-level-light");
}
}, [exam, showSolutions, setBgColor])
const checkIfStatsHaveBeenEvaluated = (ids: string[]) => { const checkIfStatsHaveBeenEvaluated = (ids: string[]) => {
setTimeout(async () => { setTimeout(async () => {
try { try {
@@ -513,6 +522,7 @@ export default function ExamPage({page}: Props) {
{user && ( {user && (
<Layout <Layout
user={user} user={user}
bgColor={bgColor}
className="justify-between" className="justify-between"
focusMode={selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length} focusMode={selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length}
onFocusLayerMouseEnter={() => setShowAbandonPopup(true)}> onFocusLayerMouseEnter={() => setShowAbandonPopup(true)}>

View File

@@ -1,20 +1,13 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase"; import {app} from "@/firebase";
import { import {getFirestore, collection, getDocs, setDoc, doc, query, where} from "firebase/firestore";
getFirestore,
collection,
getDocs,
setDoc,
doc,
query,
where,
} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user"; import {Group} from "@/interfaces/user";
import {v4} from "uuid"; import {v4} from "uuid";
import {updateExpiryDateOnGroup, getGroupsForUser} from "@/utils/groups.be"; import {updateExpiryDateOnGroup, getGroupsForUser} from "@/utils/groups.be";
import {uniqBy} from "lodash";
const db = getFirestore(app); const db = getFirestore(app);
@@ -39,32 +32,17 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
if (req.session?.user?.type === "mastercorporate") { if (req.session?.user?.type === "mastercorporate") {
try { try {
const masterCorporateGroups = await getGroupsForUser(admin, participant); const masterCorporateGroups = await getGroupsForUser(admin, participant);
const corporatesFromMaster = masterCorporateGroups const corporatesFromMaster = masterCorporateGroups.filter((g) => g.name.trim() === "Corporate").flatMap((g) => g.participants);
.filter((g) => g.name === "Corporate")
.flatMap((g) => g.participants);
if (corporatesFromMaster.length === 0) { if (corporatesFromMaster.length === 0) return res.status(200).json(masterCorporateGroups);
res.status(200).json([]);
return; const groups = await Promise.all(corporatesFromMaster.map((c) => getGroupsForUser(c, participant)));
} return res.status(200).json([...masterCorporateGroups, ...uniqBy(groups.flat(), "id")]);
Promise.all(
corporatesFromMaster.map((c) => getGroupsForUser(c, participant))
)
.then((groups) => {
res.status(200).json([...masterCorporateGroups, ...groups.flat()]);
return;
})
.catch((e) => {
console.error(e);
res.status(500).json({ ok: false });
return;
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
res.status(500).json({ok: false}); res.status(500).json({ok: false});
return; return;
} }
return;
} }
try { try {
@@ -79,11 +57,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Group; const body = req.body as Group;
await Promise.all( await Promise.all(body.participants.map(async (p) => await updateExpiryDateOnGroup(p, body.admin)));
body.participants.map(
async (p) => await updateExpiryDateOnGroup(p, body.admin)
)
);
await setDoc(doc(db, "groups", v4()), { await setDoc(doc(db, "groups", v4()), {
name: body.name, name: body.name,

View File

@@ -61,13 +61,14 @@ export default function History({user}: {user: User}) {
state.training, state.training,
state.setTraining, state.setTraining,
]); ]);
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id); // const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>(); const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">(); const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
const {assignments} = useAssignments({}); const {assignments} = useAssignments({});
const {users} = useUsers(); const {users} = useUsers();
const {stats, isLoading: isStatsLoading} = useStats(statsUserId); const {stats, isLoading: isStatsLoading} = useStats(user?.type === "student" ? user?.id : statsUserId);
const {groups: allGroups} = useGroups({}); const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id); const groups = allGroups.filter((x) => x.admin === user.id);

View File

@@ -19,6 +19,7 @@ export interface ExamState {
questionIndex: number; questionIndex: number;
inactivity: number; inactivity: number;
shuffleMaps: ShuffleMap[]; shuffleMaps: ShuffleMap[];
bgColor: string;
} }
export interface ExamFunctions { export interface ExamFunctions {
@@ -37,6 +38,7 @@ export interface ExamFunctions {
setQuestionIndex: (questionIndex: number) => void; setQuestionIndex: (questionIndex: number) => void;
setInactivity: (inactivity: number) => void; setInactivity: (inactivity: number) => void;
setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void; setShuffleMaps: (shuffleMaps: ShuffleMap[]) => void;
setBgColor: (bgColor: string) => void;
reset: () => void; reset: () => void;
} }
@@ -55,7 +57,8 @@ export const initialState: ExamState = {
exerciseIndex: -1, exerciseIndex: -1,
questionIndex: 0, questionIndex: 0,
inactivity: 0, inactivity: 0,
shuffleMaps: [] shuffleMaps: [],
bgColor: "bg-white"
}; };
const useExamStore = create<ExamState & ExamFunctions>((set) => ({ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
@@ -76,6 +79,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})), setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
setInactivity: (inactivity: number) => set(() => ({inactivity})), setInactivity: (inactivity: number) => set(() => ({inactivity})),
setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})), setShuffleMaps: (shuffleMaps) => set(() => ({shuffleMaps})),
setBgColor: (bgColor) => set(()=> ({bgColor})),
reset: () => set(() => initialState), reset: () => set(() => initialState),
})); }));

View File

@@ -35,6 +35,7 @@ module.exports = {
boxShadow: { boxShadow: {
"training-inset": "inset 0px 2px 18px 0px #00000029", "training-inset": "inset 0px 2px 18px 0px #00000029",
}, },
safelist: ["bg-ielts-level"],
colors: { colors: {
mti: { mti: {
orange: {DEFAULT: "#FF6000", dark: "#cc4402", light: "#ff790a", ultralight: "#ffdaa5"}, orange: {DEFAULT: "#FF6000", dark: "#cc4402", light: "#ff790a", ultralight: "#ffdaa5"},

4272
yarn.lock

File diff suppressed because it is too large Load Diff