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 Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
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 {LevelScore} from "@/constants/ielts";
|
||||||
import {getLevelScore} from "@/utils/score";
|
import {getLevelScore} from "@/utils/score";
|
||||||
import {capitalize} from "lodash";
|
import {capitalize} from "lodash";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
interface Score {
|
interface Score {
|
||||||
module: Module;
|
module: Module;
|
||||||
@@ -25,13 +36,18 @@ interface Props {
|
|||||||
user: User;
|
user: User;
|
||||||
modules: Module[];
|
modules: Module[];
|
||||||
scores: Score[];
|
scores: Score[];
|
||||||
|
information: {
|
||||||
|
timeSpent?: number;
|
||||||
|
inactivity?: number;
|
||||||
|
};
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onViewResults: (moduleIndex?: number) => void;
|
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 [selectedModule, setSelectedModule] = useState(modules[0]);
|
||||||
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === 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);
|
const exams = useExamStore((state) => state.exams);
|
||||||
|
|
||||||
@@ -86,6 +102,21 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex h-fit min-h-full w-full flex-col items-center justify-between gap-8">
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
module={selectedModule}
|
module={selectedModule}
|
||||||
@@ -247,6 +278,16 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
|
|||||||
</button>
|
</button>
|
||||||
<span>Review {capitalize(selectedModule)}</span>
|
<span>Review {capitalize(selectedModule)}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Link href="/" className="w-full max-w-[200px] self-end">
|
<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" />
|
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||||
{part.text.content
|
{part.text.content
|
||||||
.split(/\n|(\\n)/g)
|
.split(/\n|(\\n)/g)
|
||||||
.filter((x) => x && x.length > 0)
|
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||||
.map((line, index) => (
|
.map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{exerciseType === "matchSentences" && (
|
{exerciseType === "matchSentences" && (
|
||||||
|
|||||||
@@ -1,177 +1,152 @@
|
|||||||
import { Module } from ".";
|
import {Module} from ".";
|
||||||
import { InstructorGender } from "./exam";
|
import {InstructorGender} from "./exam";
|
||||||
|
|
||||||
export type User =
|
export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser;
|
||||||
| StudentUser
|
|
||||||
| TeacherUser
|
|
||||||
| CorporateUser
|
|
||||||
| AgentUser
|
|
||||||
| AdminUser
|
|
||||||
| DeveloperUser;
|
|
||||||
export type UserStatus = "active" | "disabled" | "paymentDue";
|
export type UserStatus = "active" | "disabled" | "paymentDue";
|
||||||
|
|
||||||
export interface BasicUser {
|
export interface BasicUser {
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
profilePicture: string;
|
profilePicture: string;
|
||||||
id: string;
|
id: string;
|
||||||
isFirstLogin: boolean;
|
isFirstLogin: boolean;
|
||||||
focus: "academic" | "general";
|
focus: "academic" | "general";
|
||||||
levels: { [key in Module]: number };
|
levels: {[key in Module]: number};
|
||||||
desiredLevels: { [key in Module]: number };
|
desiredLevels: {[key in Module]: number};
|
||||||
type: Type;
|
type: Type;
|
||||||
bio: string;
|
bio: string;
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
subscriptionExpirationDate?: null | Date;
|
subscriptionExpirationDate?: null | Date;
|
||||||
registrationDate?: Date;
|
registrationDate?: Date;
|
||||||
status: UserStatus;
|
status: UserStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StudentUser extends BasicUser {
|
export interface StudentUser extends BasicUser {
|
||||||
type: "student";
|
type: "student";
|
||||||
preferredGender?: InstructorGender;
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
preferredTopics?: string[];
|
preferredTopics?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeacherUser extends BasicUser {
|
export interface TeacherUser extends BasicUser {
|
||||||
type: "teacher";
|
type: "teacher";
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CorporateUser extends BasicUser {
|
export interface CorporateUser extends BasicUser {
|
||||||
type: "corporate";
|
type: "corporate";
|
||||||
corporateInformation: CorporateInformation;
|
corporateInformation: CorporateInformation;
|
||||||
demographicInformation?: DemographicCorporateInformation;
|
demographicInformation?: DemographicCorporateInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentUser extends BasicUser {
|
export interface AgentUser extends BasicUser {
|
||||||
type: "agent";
|
type: "agent";
|
||||||
agentInformation: AgentInformation;
|
agentInformation: AgentInformation;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminUser extends BasicUser {
|
export interface AdminUser extends BasicUser {
|
||||||
type: "admin";
|
type: "admin";
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeveloperUser extends BasicUser {
|
export interface DeveloperUser extends BasicUser {
|
||||||
type: "developer";
|
type: "developer";
|
||||||
preferredGender?: InstructorGender;
|
preferredGender?: InstructorGender;
|
||||||
demographicInformation?: DemographicInformation;
|
demographicInformation?: DemographicInformation;
|
||||||
preferredTopics?: string[];
|
preferredTopics?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CorporateInformation {
|
export interface CorporateInformation {
|
||||||
companyInformation: CompanyInformation;
|
companyInformation: CompanyInformation;
|
||||||
monthlyDuration: number;
|
monthlyDuration: number;
|
||||||
payment?: {
|
payment?: {
|
||||||
value: number;
|
value: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
commission: number;
|
commission: number;
|
||||||
};
|
};
|
||||||
referralAgent?: string;
|
referralAgent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentInformation {
|
export interface AgentInformation {
|
||||||
companyName: string;
|
companyName: string;
|
||||||
commercialRegistration: string;
|
commercialRegistration: string;
|
||||||
companyArabName?: string;
|
companyArabName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyInformation {
|
export interface CompanyInformation {
|
||||||
name: string;
|
name: string;
|
||||||
userAmount: number;
|
userAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemographicInformation {
|
export interface DemographicInformation {
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
employment: EmploymentStatus;
|
employment: EmploymentStatus;
|
||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemographicCorporateInformation {
|
export interface DemographicCorporateInformation {
|
||||||
country: string;
|
country: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
position: string;
|
position: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Gender = "male" | "female" | "other";
|
export type Gender = "male" | "female" | "other";
|
||||||
export type EmploymentStatus =
|
export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other";
|
||||||
| "employed"
|
export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
|
||||||
| "student"
|
{status: "student", label: "Student"},
|
||||||
| "self-employed"
|
{status: "employed", label: "Employed"},
|
||||||
| "unemployed"
|
{status: "unemployed", label: "Unemployed"},
|
||||||
| "retired"
|
{status: "self-employed", label: "Self-employed"},
|
||||||
| "other";
|
{status: "retired", label: "Retired"},
|
||||||
export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] =
|
{status: "other", label: "Other"},
|
||||||
[
|
];
|
||||||
{ status: "student", label: "Student" },
|
|
||||||
{ status: "employed", label: "Employed" },
|
|
||||||
{ status: "unemployed", label: "Unemployed" },
|
|
||||||
{ status: "self-employed", label: "Self-employed" },
|
|
||||||
{ status: "retired", label: "Retired" },
|
|
||||||
{ status: "other", label: "Other" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface Stat {
|
export interface Stat {
|
||||||
id: string;
|
id: string;
|
||||||
user: string;
|
user: string;
|
||||||
exam: string;
|
exam: string;
|
||||||
exercise: string;
|
exercise: string;
|
||||||
session: string;
|
session: string;
|
||||||
date: number;
|
date: number;
|
||||||
module: Module;
|
module: Module;
|
||||||
solutions: any[];
|
solutions: any[];
|
||||||
type: string;
|
type: string;
|
||||||
timeSpent?: number;
|
timeSpent?: number;
|
||||||
assignment?: string;
|
inactivity?: number;
|
||||||
score: {
|
assignment?: string;
|
||||||
correct: number;
|
score: {
|
||||||
total: number;
|
correct: number;
|
||||||
missing: number;
|
total: number;
|
||||||
};
|
missing: number;
|
||||||
isDisabled?: boolean;
|
};
|
||||||
|
isDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
admin: string;
|
admin: string;
|
||||||
name: string;
|
name: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
id: string;
|
id: string;
|
||||||
disableEditing?: boolean;
|
disableEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Code {
|
export interface Code {
|
||||||
code: string;
|
code: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
expiryDate: Date;
|
expiryDate: Date;
|
||||||
type: Type;
|
type: Type;
|
||||||
creationDate?: string;
|
creationDate?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
passport_id?: string;
|
passport_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Type =
|
export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent";
|
||||||
| "student"
|
export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent"];
|
||||||
| "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 [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
||||||
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
|
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
|
||||||
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
|
||||||
|
const [inactivityTimer, setInactivityTimer] = useState(0);
|
||||||
|
const [totalInactivity, setTotalInactivity] = useState(0);
|
||||||
const [timeSpent, setTimeSpent] = useState(0);
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
|
|
||||||
const resetStore = useExamStore((state) => state.reset);
|
const resetStore = useExamStore((state) => state.reset);
|
||||||
const assignment = useExamStore((state) => state.assignment);
|
const assignment = useExamStore((state) => state.assignment);
|
||||||
const initialTimeSpent = useExamStore((state) => state.timeSpent);
|
const initialTimeSpent = useExamStore((state) => state.timeSpent);
|
||||||
|
|
||||||
const examStore = useExamStore;
|
|
||||||
|
|
||||||
const {exam, setExam} = useExamStore((state) => state);
|
const {exam, setExam} = useExamStore((state) => state);
|
||||||
const {exams, setExams} = useExamStore((state) => state);
|
const {exams, setExams} = useExamStore((state) => state);
|
||||||
const {sessionId, setSessionId} = 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 {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||||
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 {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const router = useRouter();
|
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 = () => {
|
const reset = () => {
|
||||||
resetStore();
|
resetStore();
|
||||||
setVariant("full");
|
setVariant("full");
|
||||||
@@ -66,8 +76,21 @@ export default function ExamPage({page}: Props) {
|
|||||||
setIsEvaluationLoading(false);
|
setIsEvaluationLoading(false);
|
||||||
setStatsAwaitingEvaluation([]);
|
setStatsAwaitingEvaluation([]);
|
||||||
setTimeSpent(0);
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const saveSession = async () => {
|
const saveSession = async () => {
|
||||||
console.log("Saving your session...");
|
console.log("Saving your session...");
|
||||||
@@ -81,6 +104,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
selectedModules,
|
selectedModules,
|
||||||
assignment,
|
assignment,
|
||||||
timeSpent,
|
timeSpent,
|
||||||
|
inactivity: totalInactivity,
|
||||||
exams,
|
exams,
|
||||||
exam,
|
exam,
|
||||||
partIndex,
|
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(() => {
|
useEffect(() => {
|
||||||
if (userSolutions.length === 0 && exams.length > 0) {
|
if (userSolutions.length === 0 && exams.length > 0) {
|
||||||
@@ -144,7 +169,34 @@ export default function ExamPage({page}: Props) {
|
|||||||
}, [selectedModules.length]);
|
}, [selectedModules.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
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]);
|
}, [setModuleIndex, showSolutions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -190,6 +242,7 @@ export default function ExamPage({page}: Props) {
|
|||||||
...solution,
|
...solution,
|
||||||
id: solution.id || uuidv4(),
|
id: solution.id || uuidv4(),
|
||||||
timeSpent,
|
timeSpent,
|
||||||
|
inactivity: totalInactivity,
|
||||||
session: sessionId,
|
session: sessionId,
|
||||||
exam: solution.exam!,
|
exam: solution.exam!,
|
||||||
module: solution.module!,
|
module: solution.module!,
|
||||||
@@ -392,6 +445,10 @@ export default function ExamPage({page}: Props) {
|
|||||||
isLoading={isEvaluationLoading}
|
isLoading={isEvaluationLoading}
|
||||||
user={user!}
|
user={user!}
|
||||||
modules={selectedModules}
|
modules={selectedModules}
|
||||||
|
information={{
|
||||||
|
timeSpent,
|
||||||
|
inactivity: totalInactivity,
|
||||||
|
}}
|
||||||
onViewResults={(index?: number) => {
|
onViewResults={(index?: number) => {
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setModuleIndex(index || 0);
|
setModuleIndex(index || 0);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {sortByModule} from "@/utils/moduleUtils";
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {calculateBandScore} from "@/utils/score";
|
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 Select from "@/components/Low/Select";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
@@ -66,6 +66,8 @@ export default function History({user}: {user: User}) {
|
|||||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||||
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
||||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||||
|
const setInactivity = useExamStore((state) => state.setInactivity);
|
||||||
|
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const renderPdfIcon = usePDFDownload("stats");
|
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),
|
level: calculateBandScore(x.correct, x.total, x.module, user.focus),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const {timeSpent, session} = dateStats[0];
|
const {timeSpent, inactivity, session} = dateStats[0];
|
||||||
|
|
||||||
const selectExam = () => {
|
const selectExam = () => {
|
||||||
const examPromises = uniqBy(dateStats, "exam").map((stat) => {
|
const examPromises = uniqBy(dateStats, "exam").map((stat) => {
|
||||||
@@ -194,6 +196,9 @@ export default function History({user}: {user: User}) {
|
|||||||
|
|
||||||
Promise.all(examPromises).then((exams) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
|
if (!!timeSpent) setTimeSpent(timeSpent);
|
||||||
|
if (!!inactivity) setInactivity(inactivity);
|
||||||
|
|
||||||
setUserSolutions(convertToUserSolutions(dateStats));
|
setUserSolutions(convertToUserSolutions(dateStats));
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setExams(exams.map((x) => x!).sort(sortByModule));
|
setExams(exams.map((x) => x!).sort(sortByModule));
|
||||||
@@ -217,14 +222,20 @@ export default function History({user}: {user: User}) {
|
|||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
<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>
|
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
||||||
{timeSpent && (
|
<div className="flex items-center gap-2">
|
||||||
<>
|
{!!timeSpent && (
|
||||||
<span className="md:hidden 2xl:flex">• </span>
|
<span className="text-sm flex gap-2 items-center tooltip" data-tip="Time Spent">
|
||||||
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span>
|
<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>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<span className={textColor}>
|
<span className={textColor}>
|
||||||
@@ -272,7 +283,7 @@ export default function History({user}: {user: User}) {
|
|||||||
<div
|
<div
|
||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
className={clsx(
|
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",
|
isDisabled && "grayscale tooltip",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface ExamState {
|
|||||||
partIndex: number;
|
partIndex: number;
|
||||||
exerciseIndex: number;
|
exerciseIndex: number;
|
||||||
questionIndex: number;
|
questionIndex: number;
|
||||||
|
inactivity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExamFunctions {
|
export interface ExamFunctions {
|
||||||
@@ -33,6 +34,7 @@ export interface ExamFunctions {
|
|||||||
setPartIndex: (partIndex: number) => void;
|
setPartIndex: (partIndex: number) => void;
|
||||||
setExerciseIndex: (exerciseIndex: number) => void;
|
setExerciseIndex: (exerciseIndex: number) => void;
|
||||||
setQuestionIndex: (questionIndex: number) => void;
|
setQuestionIndex: (questionIndex: number) => void;
|
||||||
|
setInactivity: (inactivity: number) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ export const initialState: ExamState = {
|
|||||||
partIndex: -1,
|
partIndex: -1,
|
||||||
exerciseIndex: -1,
|
exerciseIndex: -1,
|
||||||
questionIndex: 0,
|
questionIndex: 0,
|
||||||
|
inactivity: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||||
@@ -68,6 +71,7 @@ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
|||||||
setPartIndex: (partIndex: number) => set(() => ({partIndex})),
|
setPartIndex: (partIndex: number) => set(() => ({partIndex})),
|
||||||
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
|
||||||
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
|
||||||
|
setInactivity: (inactivity: number) => set(() => ({inactivity})),
|
||||||
|
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user