355 lines
14 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|