Merge branch 'develop'
This commit is contained in:
@@ -14,6 +14,7 @@ import {useEffect, useState} from "react";
|
|||||||
import {BsBook, BsChevronDown, BsHeadphones, BsMegaphone, BsPen, BsQuestionSquare} from "react-icons/bs";
|
import {BsBook, BsChevronDown, BsHeadphones, BsMegaphone, BsPen, BsQuestionSquare} from "react-icons/bs";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
|
import ModuleLevelSelector from "./Medium/ModuleLevelSelector";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -90,140 +91,44 @@ export default function Diagnostic({onFinish}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-center gap-8 w-full">
|
<div className="flex flex-col items-center justify-center gap-8 w-full">
|
||||||
<h2 className="font-semibold text-xl">What is your current IELTS level?</h2>
|
<h2 className="font-semibold text-xl">What is your current IELTS level?</h2>
|
||||||
<div className="flex flex-col gap-32 w-full mb-20">
|
<ModuleLevelSelector levels={levels} setLevels={setLevels} />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 mb-24">
|
</div>
|
||||||
<div className="w-full flex flex-col gap-3.5 relative">
|
|
||||||
<span className="text-sm text-mti-gray-dim">
|
<div className="flex flex-col items-center justify-center gap-8 w-full mb-44">
|
||||||
<span className="font-bold">Reading</span> level
|
<h2 className="font-semibold text-xl">What is your desired IELTS level?</h2>
|
||||||
</span>
|
<ModuleLevelSelector levels={desiredLevels} setLevels={setDesiredLevels} />
|
||||||
<Menu>
|
</div>
|
||||||
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
|
||||||
<BsBook className="text-ielts-reading" size={34} />
|
<div className="md:self-end flex -md:flex-col justify-between w-full gap-8 absolute bottom-8 left-0 px-4 md:px-8">
|
||||||
<span className="text-mti-gray-cool text-sm">
|
<div className="w-full tooltip" data-tip="Your screen size is too small to perform a diagnostic test">
|
||||||
{levels.reading === -1 ? "Select your reading level" : `Level ${levels.reading}`}
|
<Button
|
||||||
</span>
|
color="purple"
|
||||||
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
variant="outline"
|
||||||
</Menu.Button>
|
className="group flex items-center justify-center gap-6 relative md:max-w-[400px] w-full md:hidden"
|
||||||
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
disabled>
|
||||||
{Object.values(writingMarking).map((x) => (
|
<BsQuestionSquare className="text-mti-purple-light transition duration-300 ease-in-out" size={20} />
|
||||||
<Menu.Item key={x}>
|
<span>Perform diagnostic test instead</span>
|
||||||
<span
|
</Button>
|
||||||
onClick={() => setLevels((prev) => ({...prev, reading: x}))}
|
|
||||||
className="w-full py-4 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
|
||||||
Level {x}
|
|
||||||
</span>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-3.5 relative">
|
|
||||||
<span className="text-sm text-mti-gray-dim">
|
|
||||||
<span className="font-bold">Listening</span> level
|
|
||||||
</span>
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
|
||||||
<BsHeadphones className="text-ielts-listening" size={34} />
|
|
||||||
<span className="text-mti-gray-cool text-sm">
|
|
||||||
{levels.listening === -1 ? "Select your listening level" : `Level ${levels.listening}`}
|
|
||||||
</span>
|
|
||||||
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
|
||||||
{Object.values(writingMarking).map((x) => (
|
|
||||||
<Menu.Item key={x}>
|
|
||||||
<span
|
|
||||||
onClick={() => setLevels((prev) => ({...prev, listening: x}))}
|
|
||||||
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
|
||||||
Level {x}
|
|
||||||
</span>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-3.5 relative">
|
|
||||||
<span className="text-sm text-mti-gray-dim">
|
|
||||||
<span className="font-bold">Writing</span> level
|
|
||||||
</span>
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
|
||||||
<BsPen className="text-ielts-writing" size={34} />
|
|
||||||
<span className="text-mti-gray-cool text-sm">
|
|
||||||
{levels.writing === -1 ? "Select your writing level" : `Level ${levels.writing}`}
|
|
||||||
</span>
|
|
||||||
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
|
||||||
{Object.values(writingMarking).map((x) => (
|
|
||||||
<Menu.Item key={x}>
|
|
||||||
<span
|
|
||||||
onClick={() => setLevels((prev) => ({...prev, writing: x}))}
|
|
||||||
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
|
||||||
Level {x}
|
|
||||||
</span>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-3.5 relative">
|
|
||||||
<span className="text-sm text-mti-gray-dim">
|
|
||||||
<span className="font-bold">Speaking</span> level
|
|
||||||
</span>
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
|
||||||
<BsMegaphone className="text-ielts-speaking" size={34} />
|
|
||||||
<span className="text-mti-gray-cool text-sm">
|
|
||||||
{levels.speaking === -1 ? "Select your speaking level" : `Level ${levels.speaking}`}
|
|
||||||
</span>
|
|
||||||
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
|
||||||
{Object.values(writingMarking).map((x) => (
|
|
||||||
<Menu.Item key={x}>
|
|
||||||
<span
|
|
||||||
onClick={() => setLevels((prev) => ({...prev, speaking: x}))}
|
|
||||||
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
|
||||||
Level {x}
|
|
||||||
</span>
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="md:self-end flex -md:flex-col justify-between w-full gap-8 absolute bottom-8 left-0 px-4 md:px-8">
|
|
||||||
<div className="w-full tooltip" data-tip="Your screen size is too small to perform a diagnostic test">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
className="group flex items-center justify-center gap-6 relative md:max-w-[400px] w-full md:hidden"
|
|
||||||
disabled>
|
|
||||||
<BsQuestionSquare className="text-mti-purple-light transition duration-300 ease-in-out" size={20} />
|
|
||||||
<span>Perform diagnostic test instead</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => updateUser(selectExam)}
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
className="group flex items-center justify-center gap-6 relative md:max-w-[400px] w-full -md:hidden"
|
|
||||||
disabled={!focus}>
|
|
||||||
<BsQuestionSquare
|
|
||||||
className="text-mti-purple-light group-hover:text-white transition duration-300 ease-in-out"
|
|
||||||
size={20}
|
|
||||||
onClick={() => updateUser(selectExam)}
|
|
||||||
/>
|
|
||||||
<span onClick={() => updateUser(selectExam)}>Perform diagnostic test instead</span>
|
|
||||||
</Button>
|
|
||||||
<Button color="purple" className="md:max-w-[400px] w-full" onClick={() => updateUser(onFinish)} disabled={isNextDisabled()}>
|
|
||||||
Next Step
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => updateUser(selectExam)}
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
className="group flex items-center justify-center gap-6 relative md:max-w-[400px] w-full -md:hidden"
|
||||||
|
disabled={!focus}>
|
||||||
|
<BsQuestionSquare
|
||||||
|
className="text-mti-purple-light group-hover:text-white transition duration-300 ease-in-out"
|
||||||
|
size={20}
|
||||||
|
onClick={() => updateUser(selectExam)}
|
||||||
|
/>
|
||||||
|
<span onClick={() => updateUser(selectExam)}>Perform diagnostic test instead</span>
|
||||||
|
</Button>
|
||||||
|
<Button color="purple" className="md:max-w-[400px] w-full" onClick={() => updateUser(onFinish)} disabled={isNextDisabled()}>
|
||||||
|
Next Step
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ interface Props {
|
|||||||
label: string;
|
label: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
color: "red" | "rose" | "purple" | Module;
|
color: "red" | "rose" | "purple" | Module;
|
||||||
|
mark?: number;
|
||||||
|
markLabel?: string;
|
||||||
useColor?: boolean;
|
useColor?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
textClassName?: string;
|
textClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProgressBar({label, percentage, color, useColor = false, className, textClassName}: Props) {
|
export default function ProgressBar({label, percentage, color, mark, markLabel, useColor = false, className, textClassName}: Props) {
|
||||||
const progressColorClass: {[key in typeof color]: string} = {
|
const progressColorClass: {[key in typeof color]: string} = {
|
||||||
red: "bg-mti-red-light",
|
red: "bg-mti-red-light",
|
||||||
rose: "bg-mti-rose-light",
|
rose: "bg-mti-rose-light",
|
||||||
@@ -30,6 +32,9 @@ export default function ProgressBar({label, percentage, color, useColor = false,
|
|||||||
!useColor ? "bg-mti-gray-anti-flash" : progressColorClass[color],
|
!useColor ? "bg-mti-gray-anti-flash" : progressColorClass[color],
|
||||||
useColor && "bg-opacity-20",
|
useColor && "bg-opacity-20",
|
||||||
)}>
|
)}>
|
||||||
|
{mark && (
|
||||||
|
<div style={{left: `${mark}%`}} className={clsx("w-3 h-2 bg-mti-gray-davy/60 absolute -translate-x-1/2 top-0 z-20 cursor-pointer")} />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
style={{width: `${percentage}%`}}
|
style={{width: `${percentage}%`}}
|
||||||
className={clsx("absolute transition-all duration-300 ease-in-out top-0 left-0 h-full overflow-hidden", progressColorClass[color])}
|
className={clsx("absolute transition-all duration-300 ease-in-out top-0 left-0 h-full overflow-hidden", progressColorClass[color])}
|
||||||
|
|||||||
77
src/components/Medium/InviteCard.tsx
Normal file
77
src/components/Medium/InviteCard.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Invite } from "@/interfaces/invite";
|
||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
invite: Invite;
|
||||||
|
users: User[];
|
||||||
|
reload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InviteCard({ invite, users, reload }: Props) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const inviter = users.find((u) => u.id === invite.from);
|
||||||
|
const name = !inviter
|
||||||
|
? null
|
||||||
|
: inviter.type === "corporate"
|
||||||
|
? inviter.corporateInformation?.companyInformation?.name || inviter.name
|
||||||
|
: inviter.name;
|
||||||
|
|
||||||
|
const decide = (decision: "accept" | "decline") => {
|
||||||
|
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
axios
|
||||||
|
.get(`/api/invites/${decision}/${invite.id}`)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
|
||||||
|
{ toastId: "success" },
|
||||||
|
);
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.success(`Something went wrong, please try again later!`, {
|
||||||
|
toastId: "error",
|
||||||
|
});
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
src/components/Medium/ModuleLevelSelector.tsx
Normal file
121
src/components/Medium/ModuleLevelSelector.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import {writingMarking} from "@/utils/score";
|
||||||
|
import {Menu} from "@headlessui/react";
|
||||||
|
import {Dispatch, SetStateAction} from "react";
|
||||||
|
import {BsBook, BsChevronDown, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
|
||||||
|
|
||||||
|
type Levels = {[key in Module]: number};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
levels: Levels;
|
||||||
|
setLevels: Dispatch<SetStateAction<Levels>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModuleLevelSelector({levels, setLevels}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-32 w-full">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16">
|
||||||
|
<div className="w-full flex flex-col gap-3.5 relative">
|
||||||
|
<span className="text-sm text-mti-gray-dim">
|
||||||
|
<span className="font-bold">Reading</span> level
|
||||||
|
</span>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
||||||
|
<BsBook className="text-ielts-reading" size={34} />
|
||||||
|
<span className="text-mti-gray-cool text-sm">
|
||||||
|
{levels.reading === -1 ? "Select your reading level" : `Level ${levels.reading}`}
|
||||||
|
</span>
|
||||||
|
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
||||||
|
{Object.values(writingMarking).map((x) => (
|
||||||
|
<Menu.Item key={x}>
|
||||||
|
<span
|
||||||
|
onClick={() => setLevels((prev) => ({...prev, reading: x}))}
|
||||||
|
className="w-full py-4 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
||||||
|
Level {x}
|
||||||
|
</span>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col gap-3.5 relative">
|
||||||
|
<span className="text-sm text-mti-gray-dim">
|
||||||
|
<span className="font-bold">Listening</span> level
|
||||||
|
</span>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
||||||
|
<BsHeadphones className="text-ielts-listening" size={34} />
|
||||||
|
<span className="text-mti-gray-cool text-sm">
|
||||||
|
{levels.listening === -1 ? "Select your listening level" : `Level ${levels.listening}`}
|
||||||
|
</span>
|
||||||
|
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-50 drop-shadow-lg rounded-2xl">
|
||||||
|
{Object.values(writingMarking).map((x) => (
|
||||||
|
<Menu.Item key={x}>
|
||||||
|
<span
|
||||||
|
onClick={() => setLevels((prev) => ({...prev, listening: x}))}
|
||||||
|
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
||||||
|
Level {x}
|
||||||
|
</span>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col gap-3.5 relative">
|
||||||
|
<span className="text-sm text-mti-gray-dim">
|
||||||
|
<span className="font-bold">Writing</span> level
|
||||||
|
</span>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
||||||
|
<BsPen className="text-ielts-writing" size={34} />
|
||||||
|
<span className="text-mti-gray-cool text-sm">
|
||||||
|
{levels.writing === -1 ? "Select your writing level" : `Level ${levels.writing}`}
|
||||||
|
</span>
|
||||||
|
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
||||||
|
{Object.values(writingMarking).map((x) => (
|
||||||
|
<Menu.Item key={x}>
|
||||||
|
<span
|
||||||
|
onClick={() => setLevels((prev) => ({...prev, writing: x}))}
|
||||||
|
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
||||||
|
Level {x}
|
||||||
|
</span>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col gap-3.5 relative">
|
||||||
|
<span className="text-sm text-mti-gray-dim">
|
||||||
|
<span className="font-bold">Speaking</span> level
|
||||||
|
</span>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Button className="w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-between items-center gap-12 bg-white">
|
||||||
|
<BsMegaphone className="text-ielts-speaking" size={34} />
|
||||||
|
<span className="text-mti-gray-cool text-sm">
|
||||||
|
{levels.speaking === -1 ? "Select your speaking level" : `Level ${levels.speaking}`}
|
||||||
|
</span>
|
||||||
|
<BsChevronDown className="text-mti-gray-cool" size={12} />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute overflow-y-scroll scrollbar-hide max-h-[230px] origin-top top-full bg-white flex flex-col items-center w-full z-20 drop-shadow-lg rounded-2xl">
|
||||||
|
{Object.values(writingMarking).map((x) => (
|
||||||
|
<Menu.Item key={x}>
|
||||||
|
<span
|
||||||
|
onClick={() => setLevels((prev) => ({...prev, speaking: x}))}
|
||||||
|
className="w-full py-5 text-center cursor-pointer bg-white hover:bg-mti-gray-platinum transition ease-in-out duration-300">
|
||||||
|
Level {x}
|
||||||
|
</span>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"type": "service_account",
|
"type": "service_account",
|
||||||
"project_id": "mti-ielts",
|
"project_id": "storied-phalanx-349916",
|
||||||
"private_key_id": "22b783a14c760d1215a8d1f5de0fa40a33a840e7",
|
"private_key_id": "c9e05f6fe413b1031a71f981160075ff4b044444",
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoNkd7s/izUBRb\nlmJYWl0xk4X9wEVJU4LKA4HPeha8RFDse4T4suVP08oCP9ODSXF5A83+IqXNMs/N\na7PtFABBAx433JrB7I4NsAUrDSjI4LeYEIqh6YzHsQvBU53HAmPChX525S4i0IBy\ncNnyXut0nmlHz5ZwCPXgqg4eN44C+m0f7sxzivcnPth/zLupnMiDAHFZrxQolWO2\n6JfozMWGw0TmCkUxngzeGBMVYmsGiKRIxEi3MWeuwjYjGO4nR1krEUlcpjCbx4UX\nxYXicJb17HOs9LTcSh9bpDWZPHKXR48hxd2cMLr+XQzw7Otwu2p8fEUOJ+CiTyNz\nlkN9p7OhAgMBAAECggEAB5DsMZdGu1X4wdazr+AK4RCG2UKkZ0wbqvgkCMX4O2xo\n7BmmtqFCmEAk+P+KJWEVW81wTu9jUl0tWOrBVzBThUrEF2seVkL+SmshsfpI6cmr\npb5lO/sTgZau1L7kGU3GQRpvKVHUl+EODFyJt2xZFOjL8qFsjAw4sbgsw1aJT6a4\nFilm6Gapi1qSKOPSlXVmi0NJ9DUtNbKaQK8/coqEJRizeXs9MORvzyKQaV8PBmWI\noEnkxahKOD48U2kmI7rT9/YsCuaP2BlGdLxvANXLjAKcrDccVZkYEH82tPtCicED\noow3i956HPdWSXQgUOU65MfGccjOmqGaGa4zUTICyQKBgQD6zLMwL9YS+n9EKZaK\nEbzRybN2d+eKbXyDJzkDi6FnSGVre2ndShsimoOtwZDLmOF/XhN79YOLJVbI124p\npAWO+WxAfe9Xy3iFEBmL4kSREA873Sd8EN5OfYS2DsN7IbjZkoaLuM8QlyXL9ZRS\nBJDVGjx+wFKRjnClcBNbVMMXiQKBgQDtBumKZS0ZCtJuBeuwLGJ1ZJtYECykIrsD\nUtQ7zxwXJzPGqZ2c5JLpHdDm/bb9nllpLsh4SpDRqxFa2H2FF8x5KWaS7JQUsS8e\ner6x5wUt6wAJqV/ZvttVrLZCa8VYn+K7bTANnkPNJZHTqBTJbxkXMDTtkwWXUN2z\nQP3N9lodWQKBgFBHiewYw9ubV3WIImnbt6cne0ymoPUMitioi3V5Epcu81fuTzrI\nZ9sxvoi19xVUwIm2oWICerLlptvvKZImsKjNajtSlHRz6wYc2zCNowkULOwqpGLw\nO1jAkOR94VDewH7UikDbTVywJSceWvXOBFZSaZ7hDQ0OnTw3ndqUTUaRAoGAd2BG\n2PPyDa28o7sJpBYGlJdSAb1LrnLre1YJHAJIZITS99hPUEhykUP6BYx80CkjYO01\n/BeZ7m9Y80cbmJ+O1Or8BT1vqyg90f0B8/mlSyYTQ8pxQupz7ydoN/WtU+BawgjQ\n7drqzPSCCHab2YPBwEMANTMZ2sbYkcJG0aekZSkCgYBbnFJm8kUy57isxHyvrci+\nR30KQl2Y9okPytF8PpLH+yNjLDoduTOHL/hZoFC0M4Gklx4wPKpsEhImIrWmG9VC\n0UrQC6TT1WoY6/S3YehVmTXo/nBPD1XTUcbF/xxUrWDjmMjnt1IlXBbIzUPD3U4P\niRXzHnXb7yi+/iRxSDts2w==\n-----END PRIVATE KEY-----\n",
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdgavFB63nMHyb\n38ncwijTrUmqU9UyzNJ8wlZCWAWuoz25Gng988fkKNDXnHY+ap9esHyNYg9IdSA7\nAuZeHpzTZmKiWZzFWq61KWSTgIn1JwKHGHJJdmVhTYfCe9I51cFLa5q2lTFzJ0ce\nbP7/X/7kw53odgva+M8AhDTbe60akpemgZc+LFwO0Abm7erH2HiNyjoNZzNw525L\n933PCaQwhZan04s1u0oRdVlBIBwMk+J0ojgVEpUiJOzF7gkN+UpDXujalLYdlR4q\nhkGgScXQhDYJkECC3GuvOnEo1YXGNjW9D73S6sSH+Lvqta4wW1+sTn0kB6goiQBI\n7cA1G6x3AgMBAAECggEAZPMwAX/adb7XS4LWUNH8IVyccg/63kgSteErxtiu3kRv\nYOj7W+C6fPVNGLap/RBCybjNSvIh3PfkVICh1MtG1eGXmj4VAKyvaskOmVq/hQbe\nVAuEKo7W7V2UPcKIsOsGSQUlYYjlHIIOG4O5Q1HQrRmp4cPK62Txkl6uaEkZPz4u\nbvIK2BJI8aHRwxE3Phw09blwlLqQQQ8nrhK29x5puaN+ft++IlzIOVsLz+n4kTdB\n6qkG/dhenn3K8o3+NkmSN6eNRbdJd36zXTo4Oatbvqb7r0E8vYn/3Llawo2X75zn\nec7jMHrOmcwtiu9H3PsrTWtzdSjxPHy0UtEn1HWK4QKBgQD+c/V8tAvbaUGVoZf6\ntKtDSKF6IHuY2vUO33v950mVdjrTursqOG2d+SLfSnKpc+sjDlj7/S5u4uRP+qUN\ng1rb2U7oIA7tsDa2ZTSkIx6HkPUzS+fBOxELLrbgMoJ2RLzgkiPhS95YgXJ/rYG5\nWQTehzCT5roes0RvtgM0gl3EhQKBgQDe2m7PRIU4g3RJ8HTx92B4ja8W9FVCYDG5\nPOAdZB8WB6Bvu4BJHBDLr8vDi930pKj+vYObRqBDQuILW4t8wZQJ834dnoq6EpUz\nhbVEURVBP4A/nEHrQHfq0Lp+cxThy2rw7obRQOLPETtC7p3WFgSHT6PRTcpGzCCX\n+76a30yrywKBgC/5JNtyBppDaf4QDVtTHMb+tpMT9LmI7pLzR6lDJfhr5gNtPURk\nhyY1hoGaw6t3E2n0lopL3alCVdFObDfz//lbKylQggAGLQqOYjJf/K2KgvA862Df\nBgOZtxjl7PrnUsT0SJd9elotbazsxXxwcB6UVnBMG+MV4V0+b7RCr/MRAoGBAIfp\nTcVIs7roqOZjKN9dEE/VkR/9uXW2tvyS/NfP9Ql5c0ZRYwazgCbJOwsyZRZLyek6\naWYsp5b91mA435QhdwiuoI6t30tmA+qdNBTLIpxdfvjMcoNoGPpzfBmcU/L1HW58\n+mnqGalRiAPlBQvI99ASKQWAXMnaulIWrYNEhj0LAoGBALi+QZ2pp+hDeC59ezWr\nbP1zbbONceHKGgJcevChP2k1OJyIOIqmBYeTuM4cPc5ofZYQNaMC31cs8SVeSRX1\nNTxQZmvCjMyTe/WYWYNFXdgkVz4egFXbeochCGzMYo57HV1PCkPBrARRZO8OfdDD\n8sDu//ohb7nCzceEI0DnWs13\n-----END PRIVATE KEY-----\n",
|
||||||
"client_email": "firebase-adminsdk-dyg6p@mti-ielts.iam.gserviceaccount.com",
|
"client_email": "firebase-adminsdk-3ml0u@storied-phalanx-349916.iam.gserviceaccount.com",
|
||||||
"client_id": "104980563453519094431",
|
"client_id": "114163760341944984396",
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-dyg6p%40mti-ielts.iam.gserviceaccount.com",
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-3ml0u%40storied-phalanx-349916.iam.gserviceaccount.com",
|
||||||
"universe_domain": "googleapis.com"
|
"universe_domain": "googleapis.com"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,417 +1,260 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
|
import InviteCard from "@/components/Medium/InviteCard";
|
||||||
import PayPalPayment from "@/components/PayPalPayment";
|
import PayPalPayment from "@/components/PayPalPayment";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import useInvites from "@/hooks/useInvites";
|
import useInvites from "@/hooks/useInvites";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { Invite } from "@/interfaces/invite";
|
import {Invite} from "@/interfaces/invite";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import { CorporateUser, User } from "@/interfaces/user";
|
import {CorporateUser, User} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { getExamById } from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import { getUserCorporate } from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
import {
|
import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils";
|
||||||
MODULE_ARRAY,
|
import {averageScore, groupBySession} from "@/utils/stats";
|
||||||
sortByModule,
|
import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody} from "@paypal/paypal-js";
|
||||||
sortByModuleName,
|
import {PayPalButtons} from "@paypal/react-paypal-js";
|
||||||
} 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 axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize } from "lodash";
|
import {capitalize} from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {
|
import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs";
|
||||||
BsArrowRepeat,
|
import {toast} from "react-toastify";
|
||||||
BsBook,
|
|
||||||
BsClipboard,
|
|
||||||
BsFileEarmarkText,
|
|
||||||
BsHeadphones,
|
|
||||||
BsMegaphone,
|
|
||||||
BsPen,
|
|
||||||
BsPencil,
|
|
||||||
BsStar,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StudentDashboard({ user }: Props) {
|
export default function StudentDashboard({user}: Props) {
|
||||||
const [corporateUserToShow, setCorporateUserToShow] =
|
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
||||||
useState<CorporateUser>();
|
|
||||||
|
|
||||||
const { stats } = useStats(user.id);
|
const {stats} = useStats(user.id);
|
||||||
const { users } = useUsers();
|
const {users} = useUsers();
|
||||||
const {
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
||||||
assignments,
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
||||||
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 setExams = useExamStore((state) => state.setExams);
|
||||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||||
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
|
||||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||||
const setAssignment = useExamStore((state) => state.setAssignment);
|
const setAssignment = useExamStore((state) => state.setAssignment);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const startAssignment = (assignment: Assignment) => {
|
const startAssignment = (assignment: Assignment) => {
|
||||||
const examPromises = assignment.exams
|
const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id));
|
||||||
.filter((e) => e.assignee === user.id)
|
|
||||||
.map((e) => getExamById(e.module, e.id));
|
|
||||||
|
|
||||||
Promise.all(examPromises).then((exams) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
setUserSolutions([]);
|
setUserSolutions([]);
|
||||||
setShowSolutions(false);
|
setShowSolutions(false);
|
||||||
setExams(exams.map((x) => x!).sort(sortByModule));
|
setExams(exams.map((x) => x!).sort(sortByModule));
|
||||||
setSelectedModules(
|
setSelectedModules(
|
||||||
exams
|
exams
|
||||||
.map((x) => x!)
|
.map((x) => x!)
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module),
|
||||||
);
|
);
|
||||||
setAssignment(assignment);
|
setAssignment(assignment);
|
||||||
|
|
||||||
router.push("/exercises");
|
router.push("/exercises");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const InviteCard = (invite: Invite) => {
|
return (
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
<>
|
||||||
|
{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",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
const inviter = users.find((u) => u.id === invite.from);
|
{/* Bio */}
|
||||||
const name = !inviter
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
? null
|
<span className="text-lg font-bold">Bio</span>
|
||||||
: inviter.type === "corporate"
|
<span className="text-mti-gray-taupe">
|
||||||
? inviter.corporateInformation?.companyInformation?.name || inviter.name
|
{user.bio || "Your bio will appear here, you can change it by clicking on your name in the top right corner."}
|
||||||
: inviter.name;
|
</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
const decide = (decision: "accept" | "decline") => {
|
{/* Assignments */}
|
||||||
if (!confirm(`Are you sure you want to ${decision} this invite?`)) return;
|
<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>
|
||||||
|
|
||||||
setIsLoading(true);
|
{/* Invites */}
|
||||||
axios
|
{invites.length > 0 && (
|
||||||
.get(`/api/invites/${decision}/${invite.id}`)
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
.then(() => {
|
<div className="flex items-center gap-4">
|
||||||
toast.success(
|
<div
|
||||||
`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`,
|
onClick={reloadInvites}
|
||||||
{ toastId: "success" },
|
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>
|
||||||
reloadInvites();
|
<BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
|
||||||
})
|
</div>
|
||||||
.catch((e) => {
|
</div>
|
||||||
toast.success(`Something went wrong, please try again later!`, {
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
toastId: "error",
|
{invites.map((invite) => (
|
||||||
});
|
<InviteCard key={invite.id} invite={invite} users={users} reload={reloadInvites} />
|
||||||
reloadInvites();
|
))}
|
||||||
})
|
</span>
|
||||||
.finally(() => setIsLoading(false));
|
</section>
|
||||||
};
|
)}
|
||||||
|
|
||||||
return (
|
{/* Score History */}
|
||||||
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
|
<section className="flex flex-col gap-3">
|
||||||
<span>Invited by {name}</span>
|
<span className="text-lg font-bold">Score History</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="-md:grid-rows-4 grid gap-6 md:grid-cols-2">
|
||||||
<button
|
{MODULE_ARRAY.map((module) => (
|
||||||
onClick={() => decide("accept")}
|
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4" key={module}>
|
||||||
disabled={isLoading}
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
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"
|
<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" />}
|
||||||
{!isLoading && "Accept"}
|
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
|
||||||
{isLoading && (
|
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<div className="flex items-center justify-center">
|
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
|
||||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex w-full justify-between">
|
||||||
</button>
|
<span className="text-sm font-bold md:font-extrabold">{capitalize(module)}</span>
|
||||||
<button
|
<span className="text-mti-gray-dim text-sm font-normal">
|
||||||
onClick={() => decide("decline")}
|
Level {user.levels[module] || 0} / Level 9 (Desired Level: {user.desiredLevels[module] || 9})
|
||||||
disabled={isLoading}
|
</span>
|
||||||
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"
|
</div>
|
||||||
>
|
</div>
|
||||||
{!isLoading && "Decline"}
|
<div className="md:pl-14">
|
||||||
{isLoading && (
|
<ProgressBar
|
||||||
<div className="flex items-center justify-center">
|
color={module}
|
||||||
<BsArrowRepeat className="animate-spin text-white" size={25} />
|
label=""
|
||||||
</div>
|
mark={Math.round((user.desiredLevels[module] * 100) / 9)}
|
||||||
)}
|
markLabel={`Desired Level: ${user.desiredLevels[module]}`}
|
||||||
</button>
|
percentage={Math.round((user.levels[module] * 100) / 9)}
|
||||||
</div>
|
className="h-2 w-full"
|
||||||
</div>
|
/>
|
||||||
);
|
</div>
|
||||||
};
|
</div>
|
||||||
|
))}
|
||||||
return (
|
</div>
|
||||||
<>
|
</section>
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ export default function Finish({
|
|||||||
const [levelStr, grade] = getLevelScore(level);
|
const [levelStr, grade] = getLevelScore(level);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-1">
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
<span className="text-xl font-bold">{levelStr}</span>
|
<span className="text-xl font-bold">{grade}</span>
|
||||||
<span className="text-xl">{grade}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,33 +34,33 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full h-full relative flex flex-col gap-8 md:gap-16">
|
<div className="relative flex h-full w-full flex-col gap-8 md:gap-16">
|
||||||
{user && (
|
{user && (
|
||||||
<ProfileSummary
|
<ProfileSummary
|
||||||
user={user}
|
user={user}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: <BsBook className="text-ielts-reading w-6 h-6 md:w-8 md:h-8" />,
|
icon: <BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />,
|
||||||
label: "Reading",
|
label: "Reading",
|
||||||
value: totalExamsByModule(stats, "reading"),
|
value: totalExamsByModule(stats, "reading"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsHeadphones className="text-ielts-listening w-6 h-6 md:w-8 md:h-8" />,
|
icon: <BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />,
|
||||||
label: "Listening",
|
label: "Listening",
|
||||||
value: totalExamsByModule(stats, "listening"),
|
value: totalExamsByModule(stats, "listening"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsPen className="text-ielts-writing w-6 h-6 md:w-8 md:h-8" />,
|
icon: <BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />,
|
||||||
label: "Writing",
|
label: "Writing",
|
||||||
value: totalExamsByModule(stats, "writing"),
|
value: totalExamsByModule(stats, "writing"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsMegaphone className="text-ielts-speaking w-6 h-6 md:w-8 md:h-8" />,
|
icon: <BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />,
|
||||||
label: "Speaking",
|
label: "Speaking",
|
||||||
value: totalExamsByModule(stats, "speaking"),
|
value: totalExamsByModule(stats, "speaking"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsClipboard className="text-ielts-level w-6 h-6 md:w-8 md:h-8" />,
|
icon: <BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />,
|
||||||
label: "Level",
|
label: "Level",
|
||||||
value: totalExamsByModule(stats, "level"),
|
value: totalExamsByModule(stats, "level"),
|
||||||
},
|
},
|
||||||
@@ -69,7 +69,7 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<span className="font-bold text-lg">About {capitalize(page)}</span>
|
<span className="text-lg font-bold">About {capitalize(page)}</span>
|
||||||
<span className="text-mti-gray-taupe">
|
<span className="text-mti-gray-taupe">
|
||||||
{page === "exercises" && (
|
{page === "exercises" && (
|
||||||
<>
|
<>
|
||||||
@@ -94,150 +94,150 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
<section className="w-full flex -lg:flex-col -lg:items-center -lg:gap-12 justify-between gap-8 mt-8">
|
<section className="-lg:flex-col -lg:items-center -lg:gap-12 mt-8 flex w-full justify-between gap-8">
|
||||||
<div
|
<div
|
||||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("reading") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-0 -translate-y-1/2">
|
<div className="bg-ielts-reading absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsBook className="text-white w-7 h-7" />
|
<BsBook className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Reading:</span>
|
<span className="font-semibold">Reading:</span>
|
||||||
<p className="text-center text-xs">
|
<p className="text-left text-xs">
|
||||||
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
|
Expand your vocabulary, improve your reading comprehension and improve your ability to interpret texts in English.
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
|
{!selectedModules.includes("reading") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("reading") || disableSelection) && (
|
{(selectedModules.includes("reading") || disableSelection) && (
|
||||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("listening") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-0 -translate-y-1/2">
|
<div className="bg-ielts-listening absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsHeadphones className="text-white w-7 h-7" />
|
<BsHeadphones className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Listening:</span>
|
<span className="font-semibold">Listening:</span>
|
||||||
<p className="text-center text-xs">
|
<p className="text-left text-xs">
|
||||||
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
|
Improve your ability to follow conversations in English and your ability to understand different accents and intonations.
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
|
{!selectedModules.includes("listening") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("listening") || disableSelection) && (
|
{(selectedModules.includes("listening") || disableSelection) && (
|
||||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("writing") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-0 -translate-y-1/2">
|
<div className="bg-ielts-writing absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsPen className="text-white w-7 h-7" />
|
<BsPen className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Writing:</span>
|
<span className="font-semibold">Writing:</span>
|
||||||
<p className="text-center text-xs">
|
<p className="text-left text-xs">
|
||||||
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
|
Allow you to practice writing in a variety of formats, from simple paragraphs to complex essays.
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
|
{!selectedModules.includes("writing") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("writing") || disableSelection) && (
|
{(selectedModules.includes("writing") || disableSelection) && (
|
||||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
onClick={!disableSelection && !selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("speaking") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-0 -translate-y-1/2">
|
<div className="bg-ielts-speaking absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsMegaphone className="text-white w-7 h-7" />
|
<BsMegaphone className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Speaking:</span>
|
<span className="font-semibold">Speaking:</span>
|
||||||
<p className="text-center text-xs">
|
<p className="text-left text-xs">
|
||||||
You'll have access to interactive dialogs, pronunciation exercises and speech recordings.
|
You'll have access to interactive dialogs, pronunciation exercises and speech recordings.
|
||||||
</p>
|
</p>
|
||||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
|
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && !disableSelection && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("speaking") || disableSelection) && (
|
{(selectedModules.includes("speaking") || disableSelection) && (
|
||||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />}
|
||||||
</div>
|
</div>
|
||||||
{!disableSelection && (
|
{!disableSelection && (
|
||||||
<div
|
<div
|
||||||
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
|
onClick={selectedModules.length === 0 || selectedModules.includes("level") ? () => toggleModule("level") : undefined}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative w-64 max-w-xs flex flex-col items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-2 pt-12 cursor-pointer",
|
"bg-mti-white-alt relative flex w-64 max-w-xs cursor-pointer flex-col items-center gap-2 rounded-xl border p-5 pt-12 transition duration-300 ease-in-out",
|
||||||
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("level") || disableSelection ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
||||||
)}>
|
)}>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-0 -translate-y-1/2">
|
<div className="bg-ielts-level absolute top-0 flex h-16 w-16 -translate-y-1/2 items-center justify-center rounded-full">
|
||||||
<BsClipboard className="text-white w-7 h-7" />
|
<BsClipboard className="h-7 w-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold">Level:</span>
|
<span className="font-semibold">Level:</span>
|
||||||
<p className="text-center text-xs">You'll be able to test your english level with multiple choice questions.</p>
|
<p className="text-left text-xs">You'll be able to test your english level with multiple choice questions.</p>
|
||||||
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
|
{!selectedModules.includes("level") && selectedModules.length === 0 && !disableSelection && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full mt-4" />
|
<div className="border-mti-gray-platinum mt-4 h-8 w-8 rounded-full border" />
|
||||||
)}
|
)}
|
||||||
{(selectedModules.includes("level") || disableSelection) && (
|
{(selectedModules.includes("level") || disableSelection) && (
|
||||||
<BsCheckCircle className="mt-4 text-mti-purple-light w-8 h-8" />
|
<BsCheckCircle className="text-mti-purple-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
{!selectedModules.includes("level") && selectedModules.length > 0 && (
|
{!selectedModules.includes("level") && selectedModules.length > 0 && (
|
||||||
<BsXCircle className="mt-4 text-mti-red-light w-8 h-8" />
|
<BsXCircle className="text-mti-red-light mt-4 h-8 w-8" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<div className="flex w-full -md:flex-col -md:gap-4 -md:justify-center md:justify-between items-center">
|
<div className="-md:flex-col -md:gap-4 -md:justify-center flex w-full items-center md:justify-between">
|
||||||
<div className="flex flex-col gap-3 items-center w-full">
|
<div className="flex w-full flex-col items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center"
|
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
||||||
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
onClick={() => setAvoidRepeatedExams((prev) => !prev)}>
|
||||||
<input type="checkbox" className="hidden" />
|
<input type="checkbox" className="hidden" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
avoidRepeatedExams && "!bg-mti-purple-light ",
|
avoidRepeatedExams && "!bg-mti-purple-light ",
|
||||||
)}>
|
)}>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
|
<span className="tooltip" data-tip="If possible, the platform will choose exams not yet done.">
|
||||||
Avoid Repeated Questions
|
Avoid Repeated Questions
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center"
|
className="text-mti-gray-dim -md:justify-center flex w-full cursor-pointer items-center gap-3 text-sm"
|
||||||
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
onClick={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
||||||
<input type="checkbox" className="hidden" />
|
<input type="checkbox" className="hidden" />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
|
"border-mti-purple-light flex h-6 w-6 items-center justify-center rounded-md border bg-white",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
variant === "full" && "!bg-mti-purple-light ",
|
variant === "full" && "!bg-mti-purple-light ",
|
||||||
)}>
|
)}>
|
||||||
<BsCheck color="white" className="w-full h-full" />
|
<BsCheck color="white" className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<span>Full length exams</span>
|
<span>Full length exams</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
<div className="tooltip w-full" data-tip={`Your screen size is too small to do ${page}`}>
|
||||||
<Button color="purple" className="px-12 w-full max-w-xs md:hidden" disabled>
|
<Button color="purple" className="w-full max-w-xs px-12 md:hidden" disabled>
|
||||||
Start Exam
|
Start Exam
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +250,7 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
className="px-12 w-full max-w-xs md:self-end -md:hidden"
|
className="-md:hidden w-full max-w-xs px-12 md:self-end"
|
||||||
disabled={selectedModules.length === 0 && !disableSelection}>
|
disabled={selectedModules.length === 0 && !disableSelection}>
|
||||||
Start Exam
|
Start Exam
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function BatchCodeGenerator({ user }: { user: User }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && (user.type === "corporate" || user.type === "teacher")) {
|
if (user && (user.type === "corporate" || user.type === "teacher")) {
|
||||||
setExpiryDate(user.subscriptionExpirationDate || null);
|
setExpiryDate(user.subscriptionExpirationDate || null);
|
||||||
|
setIsExpiryDateEnabled(!!user.subscriptionExpirationDate);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,167 +4,272 @@ import PayPalPayment from "@/components/PayPalPayment";
|
|||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import usePackages from "@/hooks/usePackages";
|
import usePackages from "@/hooks/usePackages";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {capitalize} from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import getSymbolFromCurrency from "currency-symbol-map";
|
import getSymbolFromCurrency from "currency-symbol-map";
|
||||||
|
import useInvites from "@/hooks/useInvites";
|
||||||
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
|
import InviteCard from "@/components/Medium/InviteCard";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
hasExpired?: boolean;
|
hasExpired?: boolean;
|
||||||
clientID: string;
|
clientID: string;
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) {
|
export default function PaymentDue({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
user,
|
||||||
|
hasExpired = false,
|
||||||
|
clientID,
|
||||||
|
reload,
|
||||||
|
}: Props) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const {packages} = usePackages();
|
const router = useRouter();
|
||||||
const {users} = useUsers();
|
|
||||||
const {groups} = useGroups();
|
|
||||||
|
|
||||||
const isIndividual = () => {
|
const { packages } = usePackages();
|
||||||
if (user?.type === "developer") return true;
|
const { users } = useUsers();
|
||||||
if (user?.type !== "student") return false;
|
const { groups } = useGroups();
|
||||||
const userGroups = groups.filter((g) => g.participants.includes(user?.id));
|
const {
|
||||||
|
invites,
|
||||||
|
isLoading: isInvitesLoading,
|
||||||
|
reload: reloadInvites,
|
||||||
|
} = useInvites({ to: user?.id });
|
||||||
|
|
||||||
if (userGroups.length === 0) return true;
|
const isIndividual = () => {
|
||||||
|
if (user?.type === "developer") return true;
|
||||||
|
if (user?.type !== "student") return false;
|
||||||
|
const userGroups = groups.filter((g) => g.participants.includes(user?.id));
|
||||||
|
|
||||||
const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t);
|
if (userGroups.length === 0) return true;
|
||||||
return userGroupsAdminTypes.every((t) => t !== "corporate");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const userGroupsAdminTypes = userGroups
|
||||||
<>
|
.map((g) => users?.find((u) => u.id === g.admin)?.type)
|
||||||
{isLoading && (
|
.filter((t) => !!t);
|
||||||
<div className="w-screen h-screen absolute top-0 left-0 overflow-hidden z-[999] bg-black/60">
|
return userGroupsAdminTypes.every((t) => t !== "corporate");
|
||||||
<div className="w-fit h-fit absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 animate-pulse flex flex-col gap-8 items-center text-white">
|
};
|
||||||
<span className={clsx("loading loading-infinity w-48")} />
|
|
||||||
<span className={clsx("font-bold text-2xl")}>Completing your payment...</span>
|
return (
|
||||||
</div>
|
<>
|
||||||
</div>
|
{isLoading && (
|
||||||
)}
|
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
|
||||||
{user ? (
|
<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-8 text-white">
|
||||||
<Layout user={user} navDisabled={hasExpired}>
|
<span className={clsx("loading loading-infinity w-48")} />
|
||||||
<div className="flex flex-col items-center justify-center text-center w-full gap-4">
|
<span className={clsx("text-2xl font-bold")}>
|
||||||
{hasExpired && <span className="font-bold text-lg">You do not have time credits for your account type!</span>}
|
Completing your payment...
|
||||||
{isIndividual() && (
|
</span>
|
||||||
<div className="flex flex-col items-center w-full overflow-x-scroll scrollbar-hide gap-12">
|
</div>
|
||||||
<span className="max-w-lg">
|
</div>
|
||||||
To add to your use of EnCoach, please purchase one of the time packages available below:
|
)}
|
||||||
</span>
|
{user ? (
|
||||||
<div className="w-full flex flex-wrap justify-center gap-8">
|
<Layout user={user} navDisabled={hasExpired}>
|
||||||
{packages.map((p) => (
|
{invites.length > 0 && (
|
||||||
<div key={p.id} className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}>
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<div className="flex flex-col items-start mb-2">
|
<div className="flex items-center gap-4">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<div
|
||||||
<span className="font-semibold text-xl">
|
onClick={reloadInvites}
|
||||||
EnCoach - {p.duration}{" "}
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
|
||||||
{capitalize(
|
>
|
||||||
p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
|
<span className="text-mti-black text-lg font-bold">
|
||||||
)}
|
Invites
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<BsArrowRepeat
|
||||||
<div className="flex flex-col gap-2 items-start w-full">
|
className={clsx(
|
||||||
<span className="text-2xl">
|
"text-xl",
|
||||||
{p.price}
|
isInvitesLoading && "animate-spin",
|
||||||
{getSymbolFromCurrency(p.currency)}
|
)}
|
||||||
</span>
|
/>
|
||||||
<PayPalPayment
|
</div>
|
||||||
key={clientID}
|
</div>
|
||||||
{...p}
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
clientID={clientID}
|
{invites.map((invite) => (
|
||||||
setIsLoading={setIsLoading}
|
<InviteCard
|
||||||
onSuccess={() => {
|
key={invite.id}
|
||||||
setTimeout(reload, 500);
|
invite={invite}
|
||||||
}}
|
users={users}
|
||||||
/>
|
reload={() => {
|
||||||
</div>
|
reloadInvites();
|
||||||
<div className="flex flex-col gap-1 items-start">
|
router.reload();
|
||||||
<span>This includes:</span>
|
}}
|
||||||
<ul className="flex flex-col items-start text-sm">
|
/>
|
||||||
<li>- Train your abilities for the IELTS exam</li>
|
))}
|
||||||
<li>- Gain insights into your weaknesses and strengths</li>
|
</span>
|
||||||
<li>- Allow yourself to correctly prepare for the exam</li>
|
</section>
|
||||||
</ul>
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
||||||
))}
|
{hasExpired && (
|
||||||
</div>
|
<span className="text-lg font-bold">
|
||||||
</div>
|
You do not have time credits for your account type!
|
||||||
)}
|
</span>
|
||||||
{!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && (
|
)}
|
||||||
<div className="flex flex-col items-center">
|
{isIndividual() && (
|
||||||
<span className="max-w-lg">
|
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
||||||
To add to your use of EnCoach and that of your students and teachers, please pay your designated package below:
|
<span className="max-w-lg">
|
||||||
</span>
|
To add to your use of EnCoach, please purchase one of the time
|
||||||
<div className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}>
|
packages available below:
|
||||||
<div className="flex flex-col items-start mb-2">
|
</span>
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<div className="flex w-full flex-wrap justify-center gap-8">
|
||||||
<span className="font-semibold text-xl">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
|
{packages.map((p) => (
|
||||||
</div>
|
<div
|
||||||
<div className="flex flex-col gap-2 items-start w-full">
|
key={p.id}
|
||||||
<span className="text-2xl">
|
className={clsx(
|
||||||
{user.corporateInformation.payment.value}
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
|
||||||
{getSymbolFromCurrency(user.corporateInformation.payment.currency)}
|
)}
|
||||||
</span>
|
>
|
||||||
<PayPalPayment
|
<div className="mb-2 flex flex-col items-start">
|
||||||
key={clientID}
|
<img
|
||||||
clientID={clientID}
|
src="/logo_title.png"
|
||||||
setIsLoading={setIsLoading}
|
alt="EnCoach's Logo"
|
||||||
currency={user.corporateInformation.payment.currency}
|
className="w-32"
|
||||||
price={user.corporateInformation.payment.value}
|
/>
|
||||||
duration={user.corporateInformation.monthlyDuration}
|
<span className="text-xl font-semibold">
|
||||||
duration_unit="months"
|
EnCoach - {p.duration}{" "}
|
||||||
onSuccess={() => {
|
{capitalize(
|
||||||
setIsLoading(false);
|
p.duration === 1
|
||||||
setTimeout(reload, 500);
|
? p.duration_unit.slice(
|
||||||
}}
|
0,
|
||||||
/>
|
p.duration_unit.length - 1,
|
||||||
</div>
|
)
|
||||||
<div className="flex flex-col gap-1 items-start">
|
: p.duration_unit,
|
||||||
<span>This includes:</span>
|
)}
|
||||||
<ul className="flex flex-col items-start text-sm">
|
</span>
|
||||||
<li>
|
</div>
|
||||||
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to
|
<div className="flex w-full flex-col items-start gap-2">
|
||||||
use EnCoach
|
<span className="text-2xl">
|
||||||
</li>
|
{p.price}
|
||||||
<li>- Train their abilities for the IELTS exam</li>
|
{getSymbolFromCurrency(p.currency)}
|
||||||
<li>- Gain insights into your students' weaknesses and strengths</li>
|
</span>
|
||||||
<li>- Allow them to correctly prepare for the exam</li>
|
<PayPalPayment
|
||||||
</ul>
|
key={clientID}
|
||||||
</div>
|
{...p}
|
||||||
</div>
|
clientID={clientID}
|
||||||
</div>
|
setIsLoading={setIsLoading}
|
||||||
)}
|
onSuccess={() => {
|
||||||
{!isIndividual() && user.type !== "corporate" && (
|
setTimeout(reload, 500);
|
||||||
<div className="flex flex-col items-center">
|
}}
|
||||||
<span className="max-w-lg">
|
/>
|
||||||
You are not the person in charge of your time credits, please contact your administrator about this situation.
|
</div>
|
||||||
</span>
|
<div className="flex flex-col items-start gap-1">
|
||||||
<span className="max-w-lg">
|
<span>This includes:</span>
|
||||||
If you believe this to be a mistake, please contact the platform's administration, thank you for your
|
<ul className="flex flex-col items-start text-sm">
|
||||||
patience.
|
<li>- Train your abilities for the IELTS exam</li>
|
||||||
</span>
|
<li>
|
||||||
</div>
|
- Gain insights into your weaknesses and strengths
|
||||||
)}
|
</li>
|
||||||
{!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && (
|
<li>
|
||||||
<div className="flex flex-col items-center">
|
- Allow yourself to correctly prepare for the exam
|
||||||
<span className="max-w-lg">
|
</li>
|
||||||
An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you
|
</ul>
|
||||||
desire and your expected monthly duration.
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span className="max-w-lg">
|
))}
|
||||||
Please try again later or contact your agent or an admin, thank you for your patience.
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!isIndividual() &&
|
||||||
</div>
|
user.type === "corporate" &&
|
||||||
</Layout>
|
user?.corporateInformation.payment && (
|
||||||
) : (
|
<div className="flex flex-col items-center">
|
||||||
<div />
|
<span className="max-w-lg">
|
||||||
)}
|
To add to your use of EnCoach and that of your students and
|
||||||
</>
|
teachers, please pay your designated package below:
|
||||||
);
|
</span>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex flex-col items-start">
|
||||||
|
<img
|
||||||
|
src="/logo_title.png"
|
||||||
|
alt="EnCoach's Logo"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
<span className="text-xl font-semibold">
|
||||||
|
EnCoach - {user.corporateInformation?.monthlyDuration}{" "}
|
||||||
|
Months
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col items-start gap-2">
|
||||||
|
<span className="text-2xl">
|
||||||
|
{user.corporateInformation.payment.value}
|
||||||
|
{getSymbolFromCurrency(
|
||||||
|
user.corporateInformation.payment.currency,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<PayPalPayment
|
||||||
|
key={clientID}
|
||||||
|
clientID={clientID}
|
||||||
|
setIsLoading={setIsLoading}
|
||||||
|
currency={user.corporateInformation.payment.currency}
|
||||||
|
price={user.corporateInformation.payment.value}
|
||||||
|
duration={user.corporateInformation.monthlyDuration}
|
||||||
|
duration_unit="months"
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
setTimeout(reload, 500);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start gap-1">
|
||||||
|
<span>This includes:</span>
|
||||||
|
<ul className="flex flex-col items-start text-sm">
|
||||||
|
<li>
|
||||||
|
- Allow a total of{" "}
|
||||||
|
{
|
||||||
|
user.corporateInformation.companyInformation
|
||||||
|
.userAmount
|
||||||
|
}{" "}
|
||||||
|
students and teachers to use EnCoach
|
||||||
|
</li>
|
||||||
|
<li>- Train their abilities for the IELTS exam</li>
|
||||||
|
<li>
|
||||||
|
- Gain insights into your students' weaknesses
|
||||||
|
and strengths
|
||||||
|
</li>
|
||||||
|
<li>- Allow them to correctly prepare for the exam</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isIndividual() && user.type !== "corporate" && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="max-w-lg">
|
||||||
|
You are not the person in charge of your time credits, please
|
||||||
|
contact your administrator about this situation.
|
||||||
|
</span>
|
||||||
|
<span className="max-w-lg">
|
||||||
|
If you believe this to be a mistake, please contact the
|
||||||
|
platform's administration, thank you for your patience.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isIndividual() &&
|
||||||
|
user.type === "corporate" &&
|
||||||
|
!user.corporateInformation.payment && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="max-w-lg">
|
||||||
|
An admin nor your agent have yet set the price intended to
|
||||||
|
your requirements in terms of the amount of users you desire
|
||||||
|
and your expected monthly duration.
|
||||||
|
</span>
|
||||||
|
<span className="max-w-lg">
|
||||||
|
Please try again later or contact your agent or an admin,
|
||||||
|
thank you for your patience.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,157 +1,227 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {FormEvent, useEffect, useState} from "react";
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import {BsArrowRepeat} from "react-icons/bs";
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export function getServerSideProps({query, res}: {query: {oobCode: string; mode: string; apiKey?: string; continueUrl?: string}; res: any}) {
|
export function getServerSideProps({
|
||||||
if (!query || !query.oobCode || !query.mode) {
|
query,
|
||||||
res.setHeader("location", "/login");
|
res,
|
||||||
res.statusCode = 302;
|
}: {
|
||||||
res.end();
|
query: {
|
||||||
return {
|
oobCode: string;
|
||||||
props: {},
|
mode: string;
|
||||||
};
|
continueUrl?: string;
|
||||||
}
|
};
|
||||||
|
res: any;
|
||||||
|
}) {
|
||||||
|
if (!query || !query.oobCode || !query.mode) {
|
||||||
|
res.setHeader("location", "/login");
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.end();
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
code: query.oobCode,
|
code: query.oobCode,
|
||||||
mode: query.mode,
|
mode: query.mode,
|
||||||
apiKey: query.apiKey,
|
...(query.continueUrl ? { continueUrl: query.continueUrl } : {}),
|
||||||
...query.continueUrl ? { continueUrl: query.continueUrl } : {},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Reset({code, mode, apiKey, continueUrl}: {code: string; mode: string; apiKey?: string; continueUrl?: string}) {
|
export default function Reset({
|
||||||
const [password, setPassword] = useState("");
|
code,
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
mode,
|
||||||
|
continueUrl,
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
mode: string;
|
||||||
|
continueUrl?: string;
|
||||||
|
}) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useUser({
|
useUser({
|
||||||
redirectTo: "/",
|
redirectTo: "/",
|
||||||
redirectIfFound: true,
|
redirectIfFound: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === "signIn") {
|
if (mode === "signIn") {
|
||||||
axios
|
axios
|
||||||
.post<{ok: boolean}>("/api/reset/verify", {
|
.post<{ ok: boolean }>("/api/reset/verify", {
|
||||||
email: continueUrl?.replace("https://platform.encoach.com/", ""),
|
email: continueUrl?.replace("https://platform.encoach.com/", ""),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
toast.success("Your account has been verified!", {toastId: "verify-successful"});
|
toast.success("Your account has been verified!", {
|
||||||
setTimeout(() => {
|
toastId: "verify-successful",
|
||||||
router.reload();
|
});
|
||||||
}, 1000);
|
setTimeout(() => {
|
||||||
return;
|
router.push("/");
|
||||||
}
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.error("Something went wrong! Please make sure to click the link in your e-mail again and input the correct e-mail!", {
|
toast.error(
|
||||||
toastId: "verify-error",
|
"Something went wrong! Please make sure to click the link in your e-mail again and input the correct e-mail!",
|
||||||
});
|
{
|
||||||
})
|
toastId: "verify-error",
|
||||||
.catch(() => {
|
},
|
||||||
toast.error("Something went wrong! Please make sure to click the link in your e-mail again and input the correct e-mail!", {
|
);
|
||||||
toastId: "verify-error",
|
})
|
||||||
});
|
.catch(() => {
|
||||||
setIsLoading(false);
|
toast.error(
|
||||||
});
|
"Something went wrong! Please make sure to click the link in your e-mail again and input the correct e-mail!",
|
||||||
}
|
{
|
||||||
});
|
toastId: "verify-error",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const login = (e: FormEvent<HTMLFormElement>) => {
|
const login = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.post<{ok: boolean}>("/api/reset/confirm", {code, password})
|
.post<{ ok: boolean }>("/api/reset/confirm", { code, password })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
toast.success("Your password has been reset!", {toastId: "reset-successful"});
|
toast.success("Your password has been reset!", {
|
||||||
setTimeout(() => {
|
toastId: "reset-successful",
|
||||||
router.push("/login");
|
});
|
||||||
}, 1000);
|
setTimeout(() => {
|
||||||
return;
|
router.push("/login");
|
||||||
}
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.error("Something went wrong! Please make sure to click the link in your e-mail again!", {toastId: "reset-error"});
|
toast.error(
|
||||||
})
|
"Something went wrong! Please make sure to click the link in your e-mail again!",
|
||||||
.catch(() => {
|
{ toastId: "reset-error" },
|
||||||
toast.error("Something went wrong! Please make sure to click the link in your e-mail again!", {toastId: "reset-error"});
|
);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.catch(() => {
|
||||||
};
|
toast.error(
|
||||||
|
"Something went wrong! Please make sure to click the link in your e-mail again!",
|
||||||
|
{ toastId: "reset-error" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Reset | EnCoach</title>
|
<title>Reset | EnCoach</title>
|
||||||
<meta name="description" content="Generated by create next app" />
|
<meta name="description" content="Generated by create next app" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<main className="w-full h-[100vh] flex bg-white text-black">
|
<main className="flex h-[100vh] w-full bg-white text-black">
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<section className="h-full w-fit min-w-fit relative hidden lg:flex">
|
<section className="relative hidden h-full w-fit min-w-fit lg:flex">
|
||||||
<div className="absolute h-full w-full bg-mti-rose-light z-10 bg-opacity-50" />
|
<div className="bg-mti-rose-light absolute z-10 h-full w-full bg-opacity-50" />
|
||||||
<img src="/people-talking-tablet.png" alt="People smiling looking at a tablet" className="h-full aspect-auto" />
|
<img
|
||||||
</section>
|
src="/people-talking-tablet.png"
|
||||||
{mode === "resetPassword" && (
|
alt="People smiling looking at a tablet"
|
||||||
<section className="h-full w-full flex flex-col items-center justify-center gap-2">
|
className="aspect-auto h-full"
|
||||||
<div className="flex flex-col gap-2 items-center relative">
|
/>
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-64 absolute -top-36 lg:-top-64" />
|
</section>
|
||||||
<h1 className="font-bold text-2xl lg:text-4xl">Reset your password</h1>
|
{mode === "resetPassword" && (
|
||||||
<p className="self-start text-sm lg:text-base font-normal text-mti-gray-cool">to your registered Email Address</p>
|
<section className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
</div>
|
<div className="relative flex flex-col items-center gap-2">
|
||||||
<Divider className="max-w-xs lg:max-w-md" />
|
<img
|
||||||
<form className="flex flex-col items-center gap-6 w-full -lg:px-8 lg:w-1/2" onSubmit={login}>
|
src="/logo_title.png"
|
||||||
<Input type="password" name="password" onChange={(e) => setPassword(e)} placeholder="Password" />
|
alt="EnCoach's Logo"
|
||||||
|
className="absolute -top-36 w-36 lg:-top-64 lg:w-64"
|
||||||
|
/>
|
||||||
|
<h1 className="text-2xl font-bold lg:text-4xl">
|
||||||
|
Reset your password
|
||||||
|
</h1>
|
||||||
|
<p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">
|
||||||
|
to your registered Email Address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Divider className="max-w-xs lg:max-w-md" />
|
||||||
|
<form
|
||||||
|
className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2"
|
||||||
|
onSubmit={login}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
onChange={(e) => setPassword(e)}
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
|
||||||
<Button className="mt-8 w-full" color="purple" disabled={isLoading}>
|
<Button
|
||||||
{!isLoading && "Reset"}
|
className="mt-8 w-full"
|
||||||
{isLoading && (
|
color="purple"
|
||||||
<div className="flex items-center justify-center">
|
disabled={isLoading}
|
||||||
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
>
|
||||||
</div>
|
{!isLoading && "Reset"}
|
||||||
)}
|
{isLoading && (
|
||||||
</Button>
|
<div className="flex items-center justify-center">
|
||||||
</form>
|
<BsArrowRepeat
|
||||||
<span className="text-mti-gray-cool text-sm font-normal mt-8">
|
className="animate-spin text-white"
|
||||||
Don't have an account?{" "}
|
size={25}
|
||||||
<Link className="text-mti-purple-light" href="/register">
|
/>
|
||||||
Sign up
|
</div>
|
||||||
</Link>
|
)}
|
||||||
</span>
|
</Button>
|
||||||
</section>
|
</form>
|
||||||
)}
|
<span className="text-mti-gray-cool mt-8 text-sm font-normal">
|
||||||
{mode === "signIn" && (
|
Don't have an account?{" "}
|
||||||
<section className="h-full w-full flex flex-col items-center justify-center gap-2">
|
<Link className="text-mti-purple-light" href="/register">
|
||||||
<div className="flex flex-col gap-2 items-center relative">
|
Sign up
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-36 lg:w-64 absolute -top-36 lg:-top-64" />
|
</Link>
|
||||||
<h1 className="font-bold text-2xl lg:text-4xl">Confirm your account</h1>
|
</span>
|
||||||
<p className="self-start text-sm lg:text-base font-normal text-mti-gray-cool">to your registered Email Address</p>
|
</section>
|
||||||
</div>
|
)}
|
||||||
<Divider className="max-w-xs lg:max-w-md" />
|
{mode === "signIn" && (
|
||||||
<div className="flex flex-col items-center gap-6 w-full -lg:px-8 lg:w-1/2">
|
<section className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||||
<span className="text-center">
|
<div className="relative flex flex-col items-center gap-2">
|
||||||
Your e-mail is currently being verified, please wait a second. <br /> <br />
|
<img
|
||||||
Once it has been verified, you will be redirected to the home page.
|
src="/logo_title.png"
|
||||||
</span>
|
alt="EnCoach's Logo"
|
||||||
</div>
|
className="absolute -top-36 w-36 lg:-top-64 lg:w-64"
|
||||||
</section>
|
/>
|
||||||
)}
|
<h1 className="text-2xl font-bold lg:text-4xl">
|
||||||
</main>
|
Confirm your account
|
||||||
</>
|
</h1>
|
||||||
);
|
<p className="text-mti-gray-cool self-start text-sm font-normal lg:text-base">
|
||||||
|
to your registered Email Address
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Divider className="max-w-xs lg:max-w-md" />
|
||||||
|
<div className="-lg:px-8 flex w-full flex-col items-center gap-6 lg:w-1/2">
|
||||||
|
<span className="text-center">
|
||||||
|
Your e-mail is currently being verified, please wait a second.{" "}
|
||||||
|
<br /> <br />
|
||||||
|
Once it has been verified, you will be redirected to the home
|
||||||
|
page.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Invite } from "@/interfaces/invite";
|
|||||||
import { Group, User } from "@/interfaces/user";
|
import { Group, User } from "@/interfaces/user";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { sendEmail } from "@/email";
|
import { sendEmail } from "@/email";
|
||||||
|
import { updateExpiryDateOnGroup } from "@/utils/groups.be";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const invitedByRef = await getDoc(doc(db, "users", invite.from));
|
const invitedByRef = await getDoc(doc(db, "users", invite.from));
|
||||||
if (!invitedByRef.exists()) return res.status(404).json({ ok: false });
|
if (!invitedByRef.exists()) return res.status(404).json({ ok: false });
|
||||||
|
|
||||||
|
await updateExpiryDateOnGroup(invite.to, invite.from);
|
||||||
|
|
||||||
const invitedBy = { ...invitedByRef.data(), id: invitedByRef.id } as User;
|
const invitedBy = { ...invitedByRef.data(), id: invitedByRef.id } as User;
|
||||||
const invitedByGroupsRef = await getDocs(
|
const invitedByGroupsRef = await getDocs(
|
||||||
query(collection(db, "groups"), where("admin", "==", invitedBy.id)),
|
query(collection(db, "groups"), where("admin", "==", invitedBy.id)),
|
||||||
|
|||||||
@@ -1,61 +1,65 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import PaymentDue from "./(status)/PaymentDue";
|
import PaymentDue from "./(status)/PaymentDue";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
const envVariables: {[key: string]: string} = {};
|
const envVariables: { [key: string]: string } = {};
|
||||||
Object.keys(process.env)
|
Object.keys(process.env)
|
||||||
.filter((x) => x.startsWith("NEXT_PUBLIC"))
|
.filter((x) => x.startsWith("NEXT_PUBLIC"))
|
||||||
.forEach((x: string) => {
|
.forEach((x: string) => {
|
||||||
envVariables[x] = process.env[x]!;
|
envVariables[x] = process.env[x]!;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
res.setHeader("location", "/login");
|
res.setHeader("location", "/login");
|
||||||
res.statusCode = 302;
|
res.statusCode = 302;
|
||||||
res.end();
|
res.end();
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user: null,
|
user: null,
|
||||||
envVariables,
|
envVariables,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user, envVariables},
|
props: { user: req.session.user, envVariables },
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Home({envVariables}: {envVariables: {[key: string]: string}}) {
|
export default function Home({
|
||||||
const {user, mutateUser} = useUser({redirectTo: "/login"});
|
envVariables,
|
||||||
const router = useRouter();
|
}: {
|
||||||
|
envVariables: { [key: string]: string };
|
||||||
|
}) {
|
||||||
|
const { user } = useUser({ redirectTo: "/login" });
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>EnCoach</title>
|
<title>EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
{user && (
|
{user && (
|
||||||
<PaymentDue
|
<PaymentDue
|
||||||
key={envVariables["NEXT_PUBLIC_PAYPAL_CLIENT_ID"]}
|
key={envVariables["NEXT_PUBLIC_PAYPAL_CLIENT_ID"]}
|
||||||
clientID={envVariables["NEXT_PUBLIC_PAYPAL_CLIENT_ID"] || ""}
|
clientID={envVariables["NEXT_PUBLIC_PAYPAL_CLIENT_ID"] || ""}
|
||||||
user={user}
|
user={user}
|
||||||
reload={router.reload}
|
reload={router.reload}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {ChangeEvent, ReactNode, useEffect, useRef, useState} from "react";
|
import {ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState} from "react";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import {toast, ToastContainer} from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
@@ -25,6 +25,9 @@ import {Divider} from "primereact/divider";
|
|||||||
import GenderInput from "@/components/High/GenderInput";
|
import GenderInput from "@/components/High/GenderInput";
|
||||||
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
||||||
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
import {Module} from "@/interfaces";
|
||||||
|
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
@@ -69,6 +72,10 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
||||||
|
|
||||||
|
const [desiredLevels, setDesiredLevels] = useState<{[key in Module]: number} | undefined>(
|
||||||
|
["developer", "student"].includes(user.type) ? user.desiredLevels : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
|
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
|
||||||
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
||||||
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
||||||
@@ -138,6 +145,7 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
password,
|
password,
|
||||||
newPassword,
|
newPassword,
|
||||||
profilePicture,
|
profilePicture,
|
||||||
|
desiredLevels,
|
||||||
demographicInformation: {
|
demographicInformation: {
|
||||||
phone,
|
phone,
|
||||||
country,
|
country,
|
||||||
@@ -319,6 +327,18 @@ function UserProfile({user, mutateUser}: Props) {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
{desiredLevels && ["developer", "student"].includes(user.type) && (
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
|
||||||
|
<ModuleLevelSelector
|
||||||
|
levels={desiredLevels}
|
||||||
|
setLevels={setDesiredLevels as Dispatch<SetStateAction<{[key in Module]: number}>>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
{user.type === "corporate" && (
|
{user.type === "corporate" && (
|
||||||
<>
|
<>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user