261 lines
11 KiB
TypeScript
261 lines
11 KiB
TypeScript
import Button from "@/components/Low/Button";
|
|
import ProgressBar from "@/components/Low/ProgressBar";
|
|
import InviteCard from "@/components/Medium/InviteCard";
|
|
import ProfileSummary from "@/components/ProfileSummary";
|
|
import useAssignments from "@/hooks/useAssignments";
|
|
import useGradingSystem from "@/hooks/useGrading";
|
|
import useInvites from "@/hooks/useInvites";
|
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
|
import useUsers, { userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers";
|
|
import {Invite} from "@/interfaces/invite";
|
|
import {Assignment} from "@/interfaces/results";
|
|
import {CorporateUser, MasterCorporateUser, Stat, User} from "@/interfaces/user";
|
|
import useExamStore from "@/stores/examStore";
|
|
import {getExamById} from "@/utils/exams";
|
|
import {getUserCorporate} from "@/utils/groups";
|
|
import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
|
|
import {getGradingLabel, getLevelLabel, getLevelScore} from "@/utils/score";
|
|
import {averageScore, groupBySession} from "@/utils/stats";
|
|
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody} from "@paypal/paypal-js";
|
|
import {PayPalButtons} from "@paypal/react-paypal-js";
|
|
import axios from "axios";
|
|
import clsx from "clsx";
|
|
import {capitalize} from "lodash";
|
|
import moment from "moment";
|
|
import Link from "next/link";
|
|
import {useRouter} from "next/router";
|
|
import {useEffect, useMemo, useState} from "react";
|
|
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
|
import {toast} from "react-toastify";
|
|
import {activeAssignmentFilter} from "@/utils/assignments";
|
|
import ModuleBadge from "@/components/ModuleBadge";
|
|
import useSessions from "@/hooks/useSessions";
|
|
|
|
interface Props {
|
|
user: User;
|
|
linkedCorporate?: CorporateUser | MasterCorporateUser;
|
|
}
|
|
|
|
export default function StudentDashboard({user, linkedCorporate}: Props) {
|
|
const {gradingSystem} = useGradingSystem();
|
|
const {sessions} = useSessions(user.id);
|
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
|
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
|
|
|
const {users: teachers} = useUsers(userHashTeacher);
|
|
const {users: corporates} = useUsers(userHashCorporate);
|
|
|
|
const users = useMemo(() => [...teachers, ...corporates], [teachers, corporates]);
|
|
const router = useRouter();
|
|
|
|
const setExams = useExamStore((state) => state.setExams);
|
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
|
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
|
const setAssignment = useExamStore((state) => state.setAssignment);
|
|
|
|
const startAssignment = (assignment: Assignment) => {
|
|
const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id));
|
|
|
|
Promise.all(examPromises).then((exams) => {
|
|
if (exams.every((x) => !!x)) {
|
|
setUserSolutions([]);
|
|
setShowSolutions(false);
|
|
setExams(exams.map((x) => x!).sort(sortByModule));
|
|
setSelectedModules(
|
|
exams
|
|
.map((x) => x!)
|
|
.sort(sortByModule)
|
|
.map((x) => x!.module),
|
|
);
|
|
setAssignment(assignment);
|
|
|
|
router.push("/exercises");
|
|
}
|
|
});
|
|
};
|
|
|
|
const studentAssignments = assignments.filter(activeAssignmentFilter);
|
|
|
|
return (
|
|
<>
|
|
{linkedCorporate && (
|
|
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
|
Linked to: <b>{linkedCorporate?.corporateInformation?.companyInformation.name || linkedCorporate.name}</b>
|
|
</div>
|
|
)}
|
|
<ProfileSummary
|
|
user={user}
|
|
items={[
|
|
{
|
|
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
|
value: countFullExams(stats),
|
|
label: "Exams",
|
|
tooltip: "Number of all conducted completed exams",
|
|
},
|
|
{
|
|
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
|
value: countExamModules(stats),
|
|
label: "Modules",
|
|
tooltip: "Number of all exam modules performed including Level Test",
|
|
},
|
|
{
|
|
icon: <BsStar className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />,
|
|
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
|
|
label: "Average Score",
|
|
tooltip: "Average success rate for questions responded",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* Bio */}
|
|
<section className="flex flex-col gap-1 md:gap-3">
|
|
<span className="text-lg font-bold">Bio</span>
|
|
<span className="text-mti-gray-taupe">
|
|
{user.bio || "Your bio will appear here, you can change it by clicking on your name in the top right corner."}
|
|
</span>
|
|
</section>
|
|
|
|
{/* Assignments */}
|
|
<section className="flex flex-col gap-1 md:gap-3">
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
onClick={reloadAssignments}
|
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
|
<span className="text-mti-black text-lg font-bold">Assignments</span>
|
|
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
|
</div>
|
|
</div>
|
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
|
{studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."}
|
|
{studentAssignments
|
|
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
|
.map((assignment) => (
|
|
<div
|
|
className={clsx(
|
|
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
|
assignment.results.map((r) => r.user).includes(user.id) && "border-mti-green-light",
|
|
)}
|
|
key={assignment.id}>
|
|
<div className="flex flex-col gap-1">
|
|
<h3 className="text-mti-black/90 text-xl font-semibold">{assignment.name}</h3>
|
|
<span className="flex justify-between gap-1 text-lg">
|
|
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
|
<span>-</span>
|
|
<span>{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}</span>
|
|
</span>
|
|
</div>
|
|
<div className="flex w-full items-center justify-between">
|
|
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
|
|
{assignment.exams
|
|
.filter((e) => e.assignee === user.id)
|
|
.map((e) => e.module)
|
|
.sort(sortByModuleName)
|
|
.map((module) => (
|
|
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
|
))}
|
|
</div>
|
|
{!assignment.results.map((r) => r.user).includes(user.id) && (
|
|
<>
|
|
<div
|
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
|
data-tip="Your screen size is too small to perform an assignment">
|
|
<Button className="h-full w-full !rounded-xl" variant="outline">
|
|
Start
|
|
</Button>
|
|
</div>
|
|
<div
|
|
data-tip="You have already started this assignment!"
|
|
className={clsx(
|
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
|
sessions.filter((x) => x.assignment?.id === assignment.id).length > 0 && "tooltip",
|
|
)}>
|
|
<Button
|
|
className={clsx("w-full h-full !rounded-xl")}
|
|
onClick={() => startAssignment(assignment)}
|
|
variant="outline"
|
|
disabled={sessions.filter((x) => x.assignment?.id === assignment.id).length > 0}>
|
|
Start
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
{assignment.results.map((r) => r.user).includes(user.id) && (
|
|
<Button
|
|
onClick={() => router.push("/record")}
|
|
color="green"
|
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
|
variant="outline">
|
|
Submitted
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</span>
|
|
</section>
|
|
|
|
{/* Invites */}
|
|
{invites.length > 0 && (
|
|
<section className="flex flex-col gap-1 md:gap-3">
|
|
<div className="flex items-center gap-4">
|
|
<div
|
|
onClick={reloadInvites}
|
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
|
<span className="text-mti-black text-lg font-bold">Invites</span>
|
|
<BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
|
|
</div>
|
|
</div>
|
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
|
{invites.map((invite) => (
|
|
<InviteCard key={invite.id} invite={invite} users={users} reload={reloadInvites} />
|
|
))}
|
|
</span>
|
|
</section>
|
|
)}
|
|
|
|
{/* Score History */}
|
|
<section className="flex flex-col gap-3">
|
|
<span className="text-lg font-bold">Score History</span>
|
|
<div className="-md:grid-rows-4 grid gap-6 md:grid-cols-2">
|
|
{MODULE_ARRAY.map((module) => {
|
|
const desiredLevel = user.desiredLevels[module] || 9;
|
|
const level = user.levels[module] || 0;
|
|
return (
|
|
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4" key={module}>
|
|
<div className="flex items-center gap-2 md:gap-3">
|
|
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
|
|
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
|
|
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
|
|
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
|
|
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
|
|
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
|
|
</div>
|
|
<div className="flex w-full justify-between">
|
|
<span className="text-sm font-bold md:font-extrabold">{capitalize(module)}</span>
|
|
<span className="text-mti-gray-dim text-sm font-normal">
|
|
{module === "level" && !!gradingSystem && `English Level: ${getGradingLabel(level, gradingSystem.steps)}`}
|
|
{module !== "level" && `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="md:pl-14">
|
|
<ProgressBar
|
|
color={module}
|
|
label=""
|
|
mark={module === "level" ? undefined : Math.round((desiredLevel * 100) / 9)}
|
|
markLabel={`Desired Level: ${desiredLevel}`}
|
|
percentage={module === "level" ? level : Math.round((level * 100) / 9)}
|
|
className="h-2 w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|