Merge branch 'develop'

This commit is contained in:
Tiago Ribeiro
2024-02-03 15:02:49 +00:00
14 changed files with 1071 additions and 918 deletions

View File

@@ -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>
); );

View File

@@ -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])}

View 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>
);
}

View 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>
);
}

View File

@@ -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"
} }

View File

@@ -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>
</>
);
} }

View File

@@ -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>
); );
} }

View File

@@ -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&apos;ll have access to interactive dialogs, pronunciation exercises and speech recordings. You&apos;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&apos;ll be able to test your english level with multiple choice questions.</p> <p className="text-left text-xs">You&apos;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>

View File

@@ -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]);

View File

@@ -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&apos; 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&apos;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&apos; 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&apos;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 />
)}
</>
);
} }

View File

@@ -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&apos;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&apos;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>
</>
);
} }

View File

@@ -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)),

View File

@@ -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}
/> />
)} )}
</> </>
); );
} }

View File

@@ -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>