Files
encoach_frontend/src/exams/Finish.tsx

355 lines
14 KiB
TypeScript

import Button from "@/components/Low/Button";
import ModuleTitle from "@/components/Medium/ModuleTitle";
import { moduleResultText } from "@/constants/ielts";
import { Module } from "@/interfaces";
import { User } from "@/interfaces/user";
import useExamStore from "@/stores/exam";
import { calculateBandScore, getGradingLabel } from "@/utils/score";
import clsx from "clsx";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import {
BsArrowCounterclockwise,
BsBan,
BsBook,
BsClipboard,
BsClipboardFill,
BsEyeFill,
BsHeadphones,
BsMegaphone,
BsPen,
} from "react-icons/bs";
import { capitalize } from "lodash";
import Modal from "@/components/Modal";
import { UserSolution } from "@/interfaces/exam";
import ai_usage from "@/utils/ai.detection";
import useGradingSystem from "@/hooks/useGrading";
import { Assignment } from "@/interfaces/results";
interface Score {
module: Module;
correct: number;
total: number;
missing: number;
}
interface Props {
user: User;
modules: Module[];
scores: Score[];
practiceScores: Score[]
information: {
timeSpent?: number;
inactivity?: number;
};
solutions: UserSolution[];
isLoading: boolean;
assignment?: Assignment;
onViewResults: (moduleIndex?: number) => void;
destination?: string
}
export default function Finish({ user, practiceScores, scores, modules, information, solutions, isLoading, assignment, onViewResults, destination }: Props) {
const [selectedModule, setSelectedModule] = useState(modules[0]);
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
const [selectedPracticeScore, setSelectedPracticeScore] = useState<Score | undefined>(practiceScores.find((x) => x.module === modules[0]));
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
const {selectedModules, exams, dispatch} = useExamStore((s) => s);
const aiUsage = Math.round(ai_usage(solutions) * 100);
const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities])
const { gradingSystem } = useGradingSystem(entity);
const router = useRouter()
useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]);
useEffect(() => setSelectedPracticeScore(practiceScores.find((x) => x.module === selectedModule)!), [practiceScores, selectedModule]);
const moduleColors: { [key in Module]: { progress: string; inner: string } } = {
reading: {
progress: "text-ielts-reading",
inner: "bg-ielts-reading-light",
},
listening: {
progress: "text-ielts-listening",
inner: "bg-ielts-listening-light",
},
writing: {
progress: "text-ielts-writing",
inner: "bg-ielts-writing-light",
},
speaking: {
progress: "text-ielts-speaking",
inner: "bg-ielts-speaking-light",
},
level: {
progress: "text-ielts-level",
inner: "bg-ielts-level-light",
},
};
const getTotalExercises = () => {
const exam = exams.find((x) => x.module === selectedModule)!;
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
return exam.parts.flatMap((x) => x.exercises).length;
}
return exam.exercises.length;
};
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
const showLevel = (level: number) => {
if (selectedModule === "level") {
const label = getGradingLabel(level, gradingSystem?.steps || []);
return (
<div className="flex flex-col items-center justify-center gap-1">
<span className="text-xl font-bold">{label}</span>
</div>
);
}
return <span className="text-3xl font-bold">{level}</span>;
};
const handlePlayAgain = () => {
dispatch({type: "INIT_EXAM", payload: {exams, modules: selectedModules}})
router.push(destination || "/exam")
}
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}
totalExercises={getTotalExercises()}
exerciseIndex={getTotalExercises()}
minTimer={exams.find((x) => x.module === selectedModule)!.minTimer}
preview={false}
disableTimer
/>
<div className="flex gap-4 self-start w-full">
{modules.includes("reading") && (
<div
onClick={() => setSelectedModule("reading")}
className={clsx(
"hover:bg-ielts-reading flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg",
selectedModule === "reading" ? "bg-ielts-reading text-white" : "bg-mti-gray-smoke text-ielts-reading",
)}>
<BsBook className="h-6 w-6" />
<span className="font-semibold">Reading</span>
</div>
)}
{modules.includes("listening") && (
<div
onClick={() => setSelectedModule("listening")}
className={clsx(
"hover:bg-ielts-listening flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg",
selectedModule === "listening" ? "bg-ielts-listening text-white" : "bg-mti-gray-smoke text-ielts-listening",
)}>
<BsHeadphones className="h-6 w-6" />
<span className="font-semibold">Listening</span>
</div>
)}
{modules.includes("writing") && (
<div className="flex w-full justify-between items-center">
<div
onClick={() => setSelectedModule("writing")}
className={clsx(
"hover:bg-ielts-writing flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg",
selectedModule === "writing" ? "bg-ielts-writing text-white" : "bg-mti-gray-smoke text-ielts-writing",
)}>
<BsPen className="h-6 w-6" />
<span className="font-semibold">Writing</span>
</div>
{aiUsage >= 50 && selectedModule === "writing" && user.type !== "student" && (
<div
className={clsx("flex items-center justify-center border px-3 h-full rounded", {
"bg-orange-100 border-orange-400 text-orange-700": aiUsage < 80,
"bg-red-100 border-red-400 text-red-700": aiUsage >= 80,
})}>
<span className="text-xs">AI Usage</span>
</div>
)}
</div>
)}
{modules.includes("speaking") && (
<div
onClick={() => setSelectedModule("speaking")}
className={clsx(
"hover:bg-ielts-speaking flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg",
selectedModule === "speaking" ? "bg-ielts-speaking text-white" : "bg-mti-gray-smoke text-ielts-speaking",
)}>
<BsMegaphone className="h-6 w-6" />
<span className="font-semibold">Speaking</span>
</div>
)}
{modules.includes("level") && (
<div
onClick={() => setSelectedModule("level")}
className={clsx(
"hover:bg-ielts-level flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg",
selectedModule === "level" ? "bg-ielts-level text-white" : "bg-mti-gray-smoke text-ielts-level",
)}>
<BsClipboard className="h-6 w-6" />
<span className="font-semibold">Level</span>
</div>
)}
</div>
{isLoading && (
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
<span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} />
<span className={clsx("text-center text-2xl font-bold", moduleColors[selectedModule].progress)}>
Evaluating your answers, please be patient...
<br />
You can also check it later on your records page!
</span>
</div>
)}
{assignment && !assignment.released && !isLoading && (
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-12">
{/* <span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} /> */}
<BsBan size={64} className={clsx(moduleColors[selectedModule].progress)} />
<span className={clsx("text-center text-2xl font-bold", moduleColors[selectedModule].progress)}>
This exam has not yet been released by its assigner.
<br />
You can check it later on your records page when it is released!
</span>
</div>
)}
{!isLoading && !(assignment && !assignment.released) && (
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
<div className="flex items-center gap-9 px-16">
<div
className={clsx("radial-progress overflow-hidden", moduleColors[selectedModule].progress)}
style={
{
"--value": (selectedScore.correct / selectedScore.total) * 100,
"--thickness": "12px",
"--size": "13rem",
} as any
}>
<div
className={clsx(
"flex h-48 w-48 flex-col items-center justify-center rounded-full",
moduleColors[selectedModule].inner,
)}>
<span className="text-xl">Level</span>
{showLevel(bandScore)}
</div>
</div>
{!["writing", "speaking"].includes(selectedModule) ? (
<div className="flex flex-col gap-5 w-28">
<div className="flex gap-2">
<div className="bg-mti-red-light mt-1 h-3 w-3 rounded-full" />
<div className="flex flex-col">
<span className="text-mti-red-light">
{(((selectedScore.total - selectedScore.missing) / selectedScore.total) * 100).toFixed(0)}%
</span>
<span className="text-lg">Completion</span>
</div>
</div>
<div className="flex gap-2">
<div className="bg-mti-purple-light mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
<div className="flex flex-col">
<span className="text-mti-purple-light">{selectedScore.correct.toString().padStart(2, "0")}</span>
<span className="text-lg whitespace-nowrap">Correct (Graded)</span>
</div>
</div>
<div className="flex gap-2">
<div className="bg-mti-rose-light mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
<div className="flex flex-col">
<span className="text-mti-rose-light">
{(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")}
</span>
<span className="text-lg whitespace-nowrap">Wrong (Graded)</span>
</div>
</div>
{selectedPracticeScore && (
<div className="flex gap-2">
<div className="bg-mti-green mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
<div className="flex flex-col">
<span className="text-mti-green">
{selectedPracticeScore.correct} / {selectedPracticeScore.total}
</span>
<span className="text-lg whitespace-nowrap">Practice Questions</span>
</div>
</div>
)}
</div>
) : (
<div className="w-28 h-full" />
)}
</div>
</div>
)}
</div>
{!isLoading && (
<div className="absolute bottom-8 left-0 flex w-full justify-between gap-8 self-end px-8">
<div className="flex gap-8">
<div className="flex w-fit cursor-pointer flex-col items-center gap-1">
<button
onClick={handlePlayAgain}
disabled={!!assignment}
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">
<BsArrowCounterclockwise className="h-7 w-7 text-white" />
</button>
<span>Play Again</span>
</div>
<div className="flex w-fit cursor-pointer flex-col items-center gap-1">
<button
onClick={() => onViewResults()}
disabled={assignment && !assignment.released}
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">
<BsEyeFill className="h-7 w-7 text-white" />
</button>
<span>Review All</span>
</div>
<div className="flex w-fit cursor-pointer flex-col items-center gap-1">
<button
disabled={assignment && !assignment.released}
onClick={() => onViewResults(modules.findIndex((x) => x === selectedModule))}
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">
<BsEyeFill className="h-7 w-7 text-white" />
</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>
<Button onClick={() => destination === "/exam" ? router.reload() : router.push(destination || "/")} color="purple" className="w-full max-w-[200px] self-end">
Dashboard
</Button>
</div>
)}
</>
);
}