Added a new feature to check for and register inactivity during an exam
This commit is contained in:
@@ -9,10 +9,21 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import {useRouter} from "next/router";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
|
||||
import {
|
||||
BsArrowCounterclockwise,
|
||||
BsBook,
|
||||
BsClipboard,
|
||||
BsClipboardFill,
|
||||
BsEyeFill,
|
||||
BsHeadphones,
|
||||
BsMegaphone,
|
||||
BsPen,
|
||||
BsShareFill,
|
||||
} from "react-icons/bs";
|
||||
import {LevelScore} from "@/constants/ielts";
|
||||
import {getLevelScore} from "@/utils/score";
|
||||
import {capitalize} from "lodash";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
interface Score {
|
||||
module: Module;
|
||||
@@ -25,13 +36,18 @@ interface Props {
|
||||
user: User;
|
||||
modules: Module[];
|
||||
scores: Score[];
|
||||
information: {
|
||||
timeSpent?: number;
|
||||
inactivity?: number;
|
||||
};
|
||||
isLoading: boolean;
|
||||
onViewResults: (moduleIndex?: number) => void;
|
||||
}
|
||||
|
||||
export default function Finish({user, scores, modules, isLoading, onViewResults}: Props) {
|
||||
export default function Finish({user, scores, modules, information, isLoading, onViewResults}: Props) {
|
||||
const [selectedModule, setSelectedModule] = useState(modules[0]);
|
||||
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
||||
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
|
||||
|
||||
const exams = useExamStore((state) => state.exams);
|
||||
|
||||
@@ -86,6 +102,21 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title="Extra Information" isOpen={isExtraInformationOpen} onClose={() => setIsExtraInformationOpen(false)}>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
{!!information.timeSpent && (
|
||||
<span>
|
||||
<b>Time Spent:</b> {Math.floor(information.timeSpent / 60)} minute(s)
|
||||
</span>
|
||||
)}
|
||||
{!!information.inactivity && (
|
||||
<span>
|
||||
<b>Inactivity:</b> {Math.floor(information.inactivity / 60)} minute(s)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div className="flex h-fit min-h-full w-full flex-col items-center justify-between gap-8">
|
||||
<ModuleTitle
|
||||
module={selectedModule}
|
||||
@@ -247,6 +278,16 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
||||
</button>
|
||||
<span>Review {capitalize(selectedModule)}</span>
|
||||
</div>
|
||||
{(!!information.inactivity || !!information.timeSpent) && (
|
||||
<div className="flex w-fit cursor-pointer flex-col items-center gap-1">
|
||||
<button
|
||||
onClick={() => setIsExtraInformationOpen(true)}
|
||||
className="bg-mti-purple-light hover:bg-mti-purple flex h-11 w-11 items-center justify-center rounded-full transition duration-300 ease-in-out">
|
||||
<BsClipboardFill className="h-7 w-7 text-white" />
|
||||
</button>
|
||||
<span>Extra Information</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link href="/" className="w-full max-w-[200px] self-end">
|
||||
|
||||
@@ -89,7 +89,7 @@ function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: s
|
||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||
{part.text.content
|
||||
.split(/\n|(\\n)/g)
|
||||
.filter((x) => x && x.length > 0)
|
||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||
.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{exerciseType === "matchSentences" && (
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {Module} from ".";
|
||||
import {InstructorGender} from "./exam";
|
||||
|
||||
export type User =
|
||||
| StudentUser
|
||||
| TeacherUser
|
||||
| CorporateUser
|
||||
| AgentUser
|
||||
| AdminUser
|
||||
| DeveloperUser;
|
||||
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
|
||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||
|
||||
export interface BasicUser {
|
||||
@@ -103,15 +97,8 @@ export interface DemographicCorporateInformation {
|
||||
}
|
||||
|
||||
export type Gender = "male" | "female" | "other";
|
||||
export type EmploymentStatus =
|
||||
| "employed"
|
||||
| "student"
|
||||
| "self-employed"
|
||||
| "unemployed"
|
||||
| "retired"
|
||||
| "other";
|
||||
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
||||
[
|
||||
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
||||
{status: "student", label: "Student"},
|
||||
{status: "employed", label: "Employed"},
|
||||
{status: "unemployed", label: "Unemployed"},
|
||||
@@ -131,6 +118,7 @@ export interface Stat {
|
||||
solutions: any[];
|
||||
type: string;
|
||||
timeSpent?: number;
|
||||
inactivity?: number;
|
||||
assignment?: string;
|
||||
score: {
|
||||
correct: number;
|
||||
@@ -160,18 +148,5 @@ export interface Code {
|
||||
passport_id?: string;
|
||||
}
|
||||
|
||||
export type Type =
|
||||
| "student"
|
||||
| "teacher"
|
||||
| "corporate"
|
||||
| "admin"
|
||||
| "developer"
|
||||
| "agent";
|
||||
export const userTypes: Type[] = [
|
||||
"student",
|
||||
"teacher",
|
||||
"corporate",
|
||||
"admin",
|
||||
"developer",
|
||||
"agent",
|
||||
];
|
||||
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent";
|
||||
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent"];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ExamState {
|
||||
partIndex: number;
|
||||
exerciseIndex: number;
|
||||
questionIndex: number;
|
||||
inactivity: number;
|
||||
}
|
||||
|
||||
export interface ExamFunctions {
|
||||
@@ -33,6 +34,7 @@ export interface ExamFunctions {
|
||||
setPartIndex: (partIndex: number) => void;
|
||||
setExerciseIndex: (exerciseIndex: number) => void;
|
||||
setQuestionIndex: (questionIndex: number) => void;
|
||||
setInactivity: (inactivity: number) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -50,6 +52,7 @@ export const initialState: ExamState = {
|
||||
partIndex: -1,
|
||||
exerciseIndex: -1,
|
||||
questionIndex: 0,
|
||||
inactivity: 0,
|
||||
};
|
||||
|
||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||
@@ -68,6 +71,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||
setPartIndex: (partIndex: number) => set(() => ({partIndex})),
|
||||
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
||||
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
||||
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
||||
|
||||
reset: () => set(() => initialState),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user