Added a new feature to check for and register inactivity during an exam

This commit is contained in:
Tiago Ribeiro
2024-06-04 22:18:45 +01:00
parent 975f4c8285
commit 8ea97ee944
6 changed files with 232 additions and 144 deletions

View File

@@ -35,14 +35,14 @@ export default function ExamPage({page}: Props) {
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
const [inactivityTimer, setInactivityTimer] = useState(0);
const [totalInactivity, setTotalInactivity] = useState(0);
const [timeSpent, setTimeSpent] = useState(0);
const resetStore = useExamStore((state) => state.reset);
const assignment = useExamStore((state) => state.assignment);
const initialTimeSpent = useExamStore((state) => state.timeSpent);
const examStore = useExamStore;
const {exam, setExam} = useExamStore((state) => state);
const {exams, setExams} = useExamStore((state) => state);
const {sessionId, setSessionId} = useExamStore((state) => state);
@@ -53,10 +53,20 @@ export default function ExamPage({page}: Props) {
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {showSolutions, setShowSolutions} = useExamStore((state) => state);
const {selectedModules, setSelectedModules} = useExamStore((state) => state);
const {inactivity, setInactivity} = useExamStore((state) => state);
const {user} = useUser({redirectTo: "/login"});
const router = useRouter();
// eslint-disable-next-line react-hooks/exhaustive-deps
const resetInactivityTimer = () => {
setInactivityTimer((prev) => {
if (moduleIndex >= selectedModules.length || moduleIndex === -1) return 0;
if (prev >= 120) setTotalInactivity((totalPrev) => totalPrev + prev);
return 0;
});
};
const reset = () => {
resetStore();
setVariant("full");
@@ -66,8 +76,21 @@ export default function ExamPage({page}: Props) {
setIsEvaluationLoading(false);
setStatsAwaitingEvaluation([]);
setTimeSpent(0);
setInactivity(0);
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
};
useEffect(() => {
if (moduleIndex >= selectedModules.length || moduleIndex === -1 || showSolutions) {
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
}
}, [moduleIndex, resetInactivityTimer, selectedModules.length, showSolutions]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const saveSession = async () => {
console.log("Saving your session...");
@@ -81,6 +104,7 @@ export default function ExamPage({page}: Props) {
selectedModules,
assignment,
timeSpent,
inactivity: totalInactivity,
exams,
exam,
partIndex,
@@ -90,7 +114,8 @@ export default function ExamPage({page}: Props) {
});
};
useEffect(() => setTimeSpent((prev) => prev + initialTimeSpent), [initialTimeSpent]);
useEffect(() => setTimeSpent(initialTimeSpent), [initialTimeSpent]);
useEffect(() => setTotalInactivity(inactivity), [inactivity]);
useEffect(() => {
if (userSolutions.length === 0 && exams.length > 0) {
@@ -144,7 +169,34 @@ export default function ExamPage({page}: Props) {
}, [selectedModules.length]);
useEffect(() => {
if (showSolutions) setModuleIndex(-1);
if (selectedModules.length > 0 && !showSolutions && inactivityTimer === 0) {
const inactivityInterval = setInterval(() => {
setInactivityTimer((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(inactivityInterval);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules.length]);
useEffect(() => {
document.addEventListener("keydown", resetInactivityTimer);
document.addEventListener("mousemove", resetInactivityTimer);
document.addEventListener("mousedown", resetInactivityTimer);
return () => {
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
};
});
useEffect(() => {
if (showSolutions) {
setModuleIndex(-1);
}
}, [setModuleIndex, showSolutions]);
useEffect(() => {
@@ -190,6 +242,7 @@ export default function ExamPage({page}: Props) {
...solution,
id: solution.id || uuidv4(),
timeSpent,
inactivity: totalInactivity,
session: sessionId,
exam: solution.exam!,
module: solution.module!,
@@ -392,6 +445,10 @@ export default function ExamPage({page}: Props) {
isLoading={isEvaluationLoading}
user={user!}
modules={selectedModules}
information={{
timeSpent,
inactivity: totalInactivity,
}}
onViewResults={(index?: number) => {
setShowSolutions(true);
setModuleIndex(index || 0);

View File

@@ -18,7 +18,7 @@ import {sortByModule} from "@/utils/moduleUtils";
import Layout from "@/components/High/Layout";
import clsx from "clsx";
import {calculateBandScore} from "@/utils/score";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {BsBook, BsClipboard, BsClock, BsHeadphones, BsMegaphone, BsPen, BsPersonDash, BsPersonFillX, BsXCircle} from "react-icons/bs";
import Select from "@/components/Low/Select";
import useGroups from "@/hooks/useGroups";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
@@ -66,6 +66,8 @@ export default function History({user}: {user: User}) {
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const router = useRouter();
const renderPdfIcon = usePDFDownload("stats");
@@ -184,7 +186,7 @@ export default function History({user}: {user: User}) {
level: calculateBandScore(x.correct, x.total, x.module, user.focus),
}));
const {timeSpent, session} = dateStats[0];
const {timeSpent, inactivity, session} = dateStats[0];
const selectExam = () => {
const examPromises = uniqBy(dateStats, "exam").map((stat) => {
@@ -194,6 +196,9 @@ export default function History({user}: {user: User}) {
Promise.all(examPromises).then((exams) => {
if (exams.every((x) => !!x)) {
if (!!timeSpent) setTimeSpent(timeSpent);
if (!!inactivity) setInactivity(inactivity);
setUserSolutions(convertToUserSolutions(dateStats));
setShowSolutions(true);
setExams(exams.map((x) => x!).sort(sortByModule));
@@ -217,14 +222,20 @@ export default function History({user}: {user: User}) {
const content = (
<>
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
<div className="flex md:flex-col 2xl:flex-row md:gap-1 -md:gap-2 2xl:gap-2 -md:items-center 2xl:items-center">
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
<span className="font-medium">{formatTimestamp(timestamp)}</span>
{timeSpent && (
<>
<span className="md:hidden 2xl:flex"> </span>
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span>
</>
)}
<div className="flex items-center gap-2">
{!!timeSpent && (
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
<BsClock /> {Math.floor(timeSpent / 60)} minutes
</span>
)}
{!!inactivity && (
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Inactivity">
<BsXCircle /> {Math.floor(inactivity / 60)} minutes
</span>
)}
</div>
</div>
<div className="flex flex-row gap-2">
<span className={textColor}>
@@ -272,7 +283,7 @@ export default function History({user}: {user: User}) {
<div
key={uuidv4()}
className={clsx(
"flex flex-col gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
isDisabled && "grayscale tooltip",
correct / total >= 0.7 && "hover:border-mti-purple",
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",