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,11 +4,15 @@ 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;
@@ -17,12 +21,24 @@ interface Props {
reload: () => void; reload: () => void;
} }
export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) { export default function PaymentDue({
user,
hasExpired = false,
clientID,
reload,
}: Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const {packages} = usePackages(); const router = useRouter();
const {users} = useUsers();
const {groups} = useGroups(); const { packages } = usePackages();
const { users } = useUsers();
const { groups } = useGroups();
const {
invites,
isLoading: isInvitesLoading,
reload: reloadInvites,
} = useInvites({ to: user.id });
const isIndividual = () => { const isIndividual = () => {
if (user?.type === "developer") return true; if (user?.type === "developer") return true;
@@ -31,42 +47,99 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
if (userGroups.length === 0) return true; if (userGroups.length === 0) return true;
const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t); const userGroupsAdminTypes = userGroups
.map((g) => users?.find((u) => u.id === g.admin)?.type)
.filter((t) => !!t);
return userGroupsAdminTypes.every((t) => t !== "corporate"); return userGroupsAdminTypes.every((t) => t !== "corporate");
}; };
return ( return (
<> <>
{isLoading && ( {isLoading && (
<div className="w-screen h-screen absolute top-0 left-0 overflow-hidden z-[999] bg-black/60"> <div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
<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"> <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">
<span className={clsx("loading loading-infinity w-48")} /> <span className={clsx("loading loading-infinity w-48")} />
<span className={clsx("font-bold text-2xl")}>Completing your payment...</span> <span className={clsx("text-2xl font-bold")}>
Completing your payment...
</span>
</div> </div>
</div> </div>
)} )}
{user ? ( {user ? (
<Layout user={user} navDisabled={hasExpired}> <Layout user={user} navDisabled={hasExpired}>
<div className="flex flex-col items-center justify-center text-center w-full gap-4"> {invites.length > 0 && (
{hasExpired && <span className="font-bold text-lg">You do not have time credits for your account type!</span>} <section className="flex flex-col gap-1 md:gap-3">
{isIndividual() && ( <div className="flex items-center gap-4">
<div className="flex flex-col items-center w-full overflow-x-scroll scrollbar-hide gap-12"> <div
<span className="max-w-lg"> onClick={reloadInvites}
To add to your use of EnCoach, please purchase one of the time packages available below: 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> </span>
<div className="w-full flex flex-wrap justify-center gap-8"> <BsArrowRepeat
className={clsx(
"text-xl",
isInvitesLoading && "animate-spin",
)}
/>
</div>
</div>
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
{invites.map((invite) => (
<InviteCard
key={invite.id}
invite={invite}
users={users}
reload={() => {
reloadInvites();
router.reload();
}}
/>
))}
</span>
</section>
)}
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
{hasExpired && (
<span className="text-lg font-bold">
You do not have time credits for your account type!
</span>
)}
{isIndividual() && (
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
<span className="max-w-lg">
To add to your use of EnCoach, please purchase one of the time
packages available below:
</span>
<div className="flex w-full flex-wrap justify-center gap-8">
{packages.map((p) => ( {packages.map((p) => (
<div key={p.id} className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}> <div
<div className="flex flex-col items-start mb-2"> key={p.id}
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" /> className={clsx(
<span className="font-semibold text-xl"> "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 - {p.duration}{" "} EnCoach - {p.duration}{" "}
{capitalize( {capitalize(
p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, p.duration === 1
? p.duration_unit.slice(
0,
p.duration_unit.length - 1,
)
: p.duration_unit,
)} )}
</span> </span>
</div> </div>
<div className="flex flex-col gap-2 items-start w-full"> <div className="flex w-full flex-col items-start gap-2">
<span className="text-2xl"> <span className="text-2xl">
{p.price} {p.price}
{getSymbolFromCurrency(p.currency)} {getSymbolFromCurrency(p.currency)}
@@ -81,12 +154,16 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col items-start gap-1">
<span>This includes:</span> <span>This includes:</span>
<ul className="flex flex-col items-start text-sm"> <ul className="flex flex-col items-start text-sm">
<li>- Train your abilities for the IELTS exam</li> <li>- Train your abilities for the IELTS exam</li>
<li>- Gain insights into your weaknesses and strengths</li> <li>
<li>- Allow yourself to correctly prepare for the exam</li> - Gain insights into your weaknesses and strengths
</li>
<li>
- Allow yourself to correctly prepare for the exam
</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -94,20 +171,36 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
</div> </div>
</div> </div>
)} )}
{!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && ( {!isIndividual() &&
user.type === "corporate" &&
user?.corporateInformation.payment && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <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: 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> </span>
<div className={clsx("p-4 bg-white rounded-xl flex flex-col gap-6 items-start")}>
<div className="flex flex-col items-start mb-2">
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
<span className="font-semibold text-xl">EnCoach - {user.corporateInformation?.monthlyDuration} Months</span>
</div> </div>
<div className="flex flex-col gap-2 items-start w-full"> <div className="flex w-full flex-col items-start gap-2">
<span className="text-2xl"> <span className="text-2xl">
{user.corporateInformation.payment.value} {user.corporateInformation.payment.value}
{getSymbolFromCurrency(user.corporateInformation.payment.currency)} {getSymbolFromCurrency(
user.corporateInformation.payment.currency,
)}
</span> </span>
<PayPalPayment <PayPalPayment
key={clientID} key={clientID}
@@ -123,15 +216,22 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
}} }}
/> />
</div> </div>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col items-start gap-1">
<span>This includes:</span> <span>This includes:</span>
<ul className="flex flex-col items-start text-sm"> <ul className="flex flex-col items-start text-sm">
<li> <li>
- Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to - Allow a total of{" "}
use EnCoach {
user.corporateInformation.companyInformation
.userAmount
}{" "}
students and teachers to use EnCoach
</li> </li>
<li>- Train their abilities for the IELTS exam</li> <li>- Train their abilities for the IELTS exam</li>
<li>- Gain insights into your students&apos; weaknesses and strengths</li> <li>
- Gain insights into your students&apos; weaknesses
and strengths
</li>
<li>- Allow them to correctly prepare for the exam</li> <li>- Allow them to correctly prepare for the exam</li>
</ul> </ul>
</div> </div>
@@ -141,22 +241,27 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
{!isIndividual() && user.type !== "corporate" && ( {!isIndividual() && user.type !== "corporate" && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <span className="max-w-lg">
You are not the person in charge of your time credits, please contact your administrator about this situation. You are not the person in charge of your time credits, please
contact your administrator about this situation.
</span> </span>
<span className="max-w-lg"> <span className="max-w-lg">
If you believe this to be a mistake, please contact the platform&apos;s administration, thank you for your If you believe this to be a mistake, please contact the
patience. platform&apos;s administration, thank you for your patience.
</span> </span>
</div> </div>
)} )}
{!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && ( {!isIndividual() &&
user.type === "corporate" &&
!user.corporateInformation.payment && (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<span className="max-w-lg"> <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 An admin nor your agent have yet set the price intended to
desire and your expected monthly duration. your requirements in terms of the amount of users you desire
and your expected monthly duration.
</span> </span>
<span className="max-w-lg"> <span className="max-w-lg">
Please try again later or contact your agent or an admin, thank you for your patience. Please try again later or contact your agent or an admin,
thank you for your patience.
</span> </span>
</div> </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,15 +1,15 @@
/* 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) => {
@@ -29,12 +29,16 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
} }
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,
}: {
envVariables: { [key: string]: string };
}) {
const { user } = useUser({ redirectTo: "/login" });
const router = useRouter(); const router = useRouter();
return ( return (