Added an Invite list to the payment due page as well

This commit is contained in:
Tiago Ribeiro
2024-01-31 23:42:46 +00:00
parent 0ec62c107c
commit 908ce5b5b9
5 changed files with 397 additions and 267 deletions

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

@@ -1,5 +1,6 @@
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";
@@ -102,71 +103,6 @@ export default function StudentDashboard({ user }: Props) {
}); });
}; };
const InviteCard = (invite: Invite) => {
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" },
);
reloadInvites();
})
.catch((e) => {
toast.success(`Something went wrong, please try again later!`, {
toastId: "error",
});
reloadInvites();
})
.finally(() => setIsLoading(false));
};
return (
<div className="border-mti-gray-anti-flash flex min-w-[200px] flex-col gap-6 rounded-xl border p-4 text-black">
<span>Invited by {name}</span>
<div className="flex items-center gap-2">
<button
onClick={() => decide("accept")}
disabled={isLoading}
className="bg-mti-green-ultralight hover:bg-mti-green-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Accept"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
<button
onClick={() => decide("decline")}
disabled={isLoading}
className="bg-mti-red-ultralight hover:bg-mti-red-light w-24 rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"
>
{!isLoading && "Decline"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
</div>
</div>
);
};
return ( return (
<> <>
{corporateUserToShow && ( {corporateUserToShow && (
@@ -356,7 +292,12 @@ export default function StudentDashboard({ user }: Props) {
</div> </div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll"> <span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{invites.map((invite) => ( {invites.map((invite) => (
<InviteCard key={invite.id} {...invite} /> <InviteCard
key={invite.id}
invite={invite}
users={users}
reload={reloadInvites}
/>
))} ))}
</span> </span>
</section> </section>

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

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