Updated the use of the Desired Levels to be configurable

This commit is contained in:
Tiago Ribeiro
2024-02-03 14:46:35 +00:00
parent 846d829d10
commit 0053105dd3
6 changed files with 660 additions and 830 deletions

View File

@@ -7,352 +7,254 @@ import useAssignments from "@/hooks/useAssignments";
import useInvites from "@/hooks/useInvites";
import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers";
import { Invite } from "@/interfaces/invite";
import { Assignment } from "@/interfaces/results";
import { CorporateUser, User } from "@/interfaces/user";
import {Invite} from "@/interfaces/invite";
import {Assignment} from "@/interfaces/results";
import {CorporateUser, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import { getExamById } from "@/utils/exams";
import { getUserCorporate } from "@/utils/groups";
import {
MODULE_ARRAY,
sortByModule,
sortByModuleName,
} from "@/utils/moduleUtils";
import { averageScore, groupBySession } from "@/utils/stats";
import {
CreateOrderActions,
CreateOrderData,
OnApproveActions,
OnApproveData,
OrderResponseBody,
} from "@paypal/paypal-js";
import { PayPalButtons } from "@paypal/react-paypal-js";
import {getExamById} from "@/utils/exams";
import {getUserCorporate} from "@/utils/groups";
import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
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 {capitalize} from "lodash";
import moment from "moment";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import {
BsArrowRepeat,
BsBook,
BsClipboard,
BsFileEarmarkText,
BsHeadphones,
BsMegaphone,
BsPen,
BsPencil,
BsStar,
} from "react-icons/bs";
import { toast } from "react-toastify";
import {useRouter} from "next/router";
import {useEffect, useState} from "react";
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
import {toast} from "react-toastify";
interface Props {
user: User;
user: User;
}
export default function StudentDashboard({ user }: Props) {
const [corporateUserToShow, setCorporateUserToShow] =
useState<CorporateUser>();
export default function StudentDashboard({user}: Props) {
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
const { stats } = useStats(user.id);
const { users } = useUsers();
const {
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ assignees: user?.id });
const {
invites,
isLoading: isInvitesLoading,
reload: reloadInvites,
} = useInvites({ to: user.id });
const {stats} = useStats(user.id);
const {users} = useUsers();
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
const router = useRouter();
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 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);
useEffect(() => {
getUserCorporate(user.id).then(setCorporateUserToShow);
}, [user]);
useEffect(() => {
getUserCorporate(user.id).then(setCorporateUserToShow);
}, [user]);
const startAssignment = (assignment: Assignment) => {
const examPromises = assignment.exams
.filter((e) => e.assignee === user.id)
.map((e) => getExamById(e.module, e.id));
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);
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");
}
});
};
router.push("/exercises");
}
});
};
return (
<>
{corporateUserToShow && (
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
Linked to:{" "}
<b>
{corporateUserToShow?.corporateInformation?.companyInformation
.name || corporateUserToShow.name}
</b>
</div>
)}
<ProfileSummary
user={user}
items={[
{
icon: (
<BsFileEarmarkText className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />
),
value: Object.keys(groupBySession(stats)).length,
label: "Exams",
},
{
icon: (
<BsPencil className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />
),
value: stats.length,
label: "Exercises",
},
{
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",
},
]}
/>
return (
<>
{corporateUserToShow && (
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
</div>
)}
<ProfileSummary
user={user}
items={[
{
icon: <BsFileEarmarkText className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />,
value: Object.keys(groupBySession(stats)).length,
label: "Exams",
},
{
icon: <BsPencil className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />,
value: stats.length,
label: "Exercises",
},
{
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",
},
]}
/>
<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>
{/* 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>
<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">
{assignments.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
.length === 0 &&
"Assignments will appear here. It seems that for now there are no assignments for you."}
{assignments
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
.sort((a, b) => moment(a.startDate).diff(b.startDate))
.map((assignment) => (
<div
className={clsx(
"border-mti-gray-anti-flash flex min-w-[300px] 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">
<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-[104px] grid-cols-2 place-items-center justify-center gap-2">
{assignment.exams
.filter((e) => e.assignee === user.id)
.map((e) => e.module)
.sort(sortByModuleName)
.map((module) => (
<div
key={module}
data-tip={capitalize(module)}
className={clsx(
"-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level",
)}
>
{module === "reading" && (
<BsBook className="h-4 w-4" />
)}
{module === "listening" && (
<BsHeadphones className="h-4 w-4" />
)}
{module === "writing" && (
<BsPen className="h-4 w-4" />
)}
{module === "speaking" && (
<BsMegaphone className="h-4 w-4" />
)}
{module === "level" && (
<BsClipboard className="h-4 w-4" />
)}
</div>
))}
</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
disabled={moment(assignment.startDate).isAfter(
moment(),
)}
className="h-full w-full !rounded-xl"
variant="outline"
>
Start
</Button>
</div>
<Button
disabled={moment(assignment.startDate).isAfter(
moment(),
)}
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
onClick={() => startAssignment(assignment)}
variant="outline"
>
Start
</Button>
</>
)}
{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>
{/* 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">
{assignments.filter((a) => moment(a.endDate).isSameOrAfter(moment())).length === 0 &&
"Assignments will appear here. It seems that for now there are no assignments for you."}
{assignments
.filter((a) => moment(a.endDate).isSameOrAfter(moment()))
.sort((a, b) => moment(a.startDate).diff(b.startDate))
.map((assignment) => (
<div
className={clsx(
"border-mti-gray-anti-flash flex min-w-[300px] 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">
<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-[104px] grid-cols-2 place-items-center justify-center gap-2">
{assignment.exams
.filter((e) => e.assignee === user.id)
.map((e) => e.module)
.sort(sortByModuleName)
.map((module) => (
<div
key={module}
data-tip={capitalize(module)}
className={clsx(
"-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level",
)}>
{module === "reading" && <BsBook className="h-4 w-4" />}
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
{module === "writing" && <BsPen className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />}
</div>
))}
</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
disabled={moment(assignment.startDate).isAfter(moment())}
className="h-full w-full !rounded-xl"
variant="outline">
Start
</Button>
</div>
<Button
disabled={moment(assignment.startDate).isAfter(moment())}
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
onClick={() => startAssignment(assignment)}
variant="outline">
Start
</Button>
</>
)}
{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.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>
)}
{/* 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>
)}
<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) => (
<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">
Level {user.levels[module] || 0} / Level{" "}
{user.desiredLevels[module] || 9}
</span>
</div>
</div>
<div className="md:pl-14">
<ProgressBar
color={module}
label=""
percentage={Math.round(
(user.levels[module] * 100) / user.desiredLevels[module],
)}
className="h-2 w-full"
/>
</div>
</div>
))}
</div>
</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) => (
<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">
Level {user.levels[module] || 0} / Level 9 (Desired Level: {user.desiredLevels[module] || 9})
</span>
</div>
</div>
<div className="md:pl-14">
<ProgressBar
color={module}
label=""
mark={Math.round((user.desiredLevels[module] * 100) / 9)}
markLabel={`Desired Level: ${user.desiredLevels[module]}`}
percentage={Math.round((user.levels[module] * 100) / 9)}
className="h-2 w-full"
/>
</div>
</div>
))}
</div>
</section>
</>
);
}