Created a simple invite system that notifies users via e-mail when a corporate uploads an Excel file with already registered students
This commit is contained in:
@@ -3,228 +3,415 @@ import ProgressBar from "@/components/Low/ProgressBar";
|
||||
import PayPalPayment from "@/components/PayPalPayment";
|
||||
import ProfileSummary from "@/components/ProfileSummary";
|
||||
import useAssignments from "@/hooks/useAssignments";
|
||||
import useInvites from "@/hooks/useInvites";
|
||||
import useStats from "@/hooks/useStats";
|
||||
import {Assignment} from "@/interfaces/results";
|
||||
import {CorporateUser, User} from "@/interfaces/user";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
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 {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: 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 top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
|
||||
Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.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: Object.keys(groupBySession(stats)).length,
|
||||
label: "Exams",
|
||||
},
|
||||
{
|
||||
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||
value: stats.length,
|
||||
label: "Exercises",
|
||||
},
|
||||
{
|
||||
icon: <BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
||||
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
|
||||
label: "Average Score",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
const InviteCard = (invite: Invite) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
<section className="flex flex-col gap-1 md:gap-3">
|
||||
<span className="font-bold text-lg">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>
|
||||
const inviter = users.find((u) => u.id === invite.from);
|
||||
const name = !inviter
|
||||
? null
|
||||
: inviter.type === "corporate"
|
||||
? inviter.corporateInformation?.companyInformation?.name || inviter.name
|
||||
: inviter.name;
|
||||
|
||||
<section className="flex flex-col gap-1 md:gap-3">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div
|
||||
onClick={reloadAssignments}
|
||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||
<span className="font-bold text-lg text-mti-black">Assignments</span>
|
||||
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-mti-gray-taupe flex gap-8 overflow-x-scroll scrollbar-hide">
|
||||
{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 border-mti-gray-anti-flash rounded-xl flex flex-col gap-6 p-4 min-w-[300px]",
|
||||
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="font-semibold text-xl text-mti-black/90">{assignment.name}</h3>
|
||||
<span className="flex gap-1 justify-between">
|
||||
<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 justify-between w-full items-center">
|
||||
<div className="grid grid-cols-2 gap-2 place-items-center justify-center w-fit min-w-[104px] -md:mt-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(
|
||||
"flex gap-2 items-center w-fit text-white -md:px-4 xl:px-4 md:px-2 py-2 rounded-xl tooltip",
|
||||
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="w-4 h-4" />}
|
||||
{module === "listening" && <BsHeadphones className="w-4 h-4" />}
|
||||
{module === "writing" && <BsPen className="w-4 h-4" />}
|
||||
{module === "speaking" && <BsMegaphone className="w-4 h-4" />}
|
||||
{module === "level" && <BsClipboard className="w-4 h-4" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<>
|
||||
<div
|
||||
className="tooltip w-full md:hidden h-full flex items-center justify-end pl-8"
|
||||
data-tip="Your screen size is too small to perform an assignment">
|
||||
<Button
|
||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
||||
className="w-full h-full !rounded-xl"
|
||||
variant="outline">
|
||||
Start
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
||||
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
||||
onClick={() => startAssignment(assignment)}
|
||||
variant="outline">
|
||||
Start
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<Button
|
||||
onClick={() => router.push("/record")}
|
||||
color="green"
|
||||
className="w-full max-w-[50%] h-full !rounded-xl -md:hidden"
|
||||
variant="outline">
|
||||
Submitted
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
</section>
|
||||
const decide = (decision: "accept" | "decline") => {
|
||||
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<span className="font-bold text-lg">Score History</span>
|
||||
<div className="grid -md:grid-rows-4 md:grid-cols-2 gap-6">
|
||||
{MODULE_ARRAY.map((module) => (
|
||||
<div className="border border-mti-gray-anti-flash rounded-xl flex flex-col gap-2 p-4" key={module}>
|
||||
<div className="flex gap-2 md:gap-3 items-center">
|
||||
<div className="w-8 h-8 md:w-12 md:h-12 bg-mti-gray-smoke flex items-center justify-center rounded-lg md:rounded-xl">
|
||||
{module === "reading" && <BsBook className="text-ielts-reading w-4 h-4 md:w-5 md:h-5" />}
|
||||
{module === "listening" && <BsHeadphones className="text-ielts-listening w-4 h-4 md:w-5 md:h-5" />}
|
||||
{module === "writing" && <BsPen className="text-ielts-writing w-4 h-4 md:w-5 md:h-5" />}
|
||||
{module === "speaking" && <BsMegaphone className="text-ielts-speaking w-4 h-4 md:w-5 md:h-5" />}
|
||||
{module === "level" && <BsClipboard className="text-ielts-level w-4 h-4 md:w-5 md:h-5" />}
|
||||
</div>
|
||||
<div className="flex justify-between w-full">
|
||||
<span className="font-bold md:font-extrabold text-sm">{capitalize(module)}</span>
|
||||
<span className="text-sm font-normal text-mti-gray-dim">
|
||||
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="w-full h-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.get(`/api/invites/${decision}/${invite.id}`)
|
||||
.then(() => {
|
||||
toast.success(
|
||||
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
|
||||
{ toastId: "success" },
|
||||
);
|
||||
reloadInvites();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.success(`Something went wrong, please try again later!`, {
|
||||
toastId: "error",
|
||||
});
|
||||
reloadInvites();
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
|
||||
<span>Invited by {name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => decide("accept")}
|
||||
disabled={isLoading}
|
||||
className="bg-mti-green-ultralight hover:bg-mti-green-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
|
||||
>
|
||||
{!isLoading && "Accept"}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => decide("decline")}
|
||||
disabled={isLoading}
|
||||
className="bg-mti-red-ultralight hover:bg-mti-red-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
|
||||
>
|
||||
{!isLoading && "Decline"}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<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} />
|
||||
))}
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user