From 333d1610fad10e70759545f223959503288a02c6 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Thu, 10 Jul 2025 19:34:31 +0000 Subject: feat: Add invite user support --- apps/web/components/admin/InvitesList.tsx | 192 ++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 apps/web/components/admin/InvitesList.tsx (limited to 'apps/web/components/admin/InvitesList.tsx') diff --git a/apps/web/components/admin/InvitesList.tsx b/apps/web/components/admin/InvitesList.tsx new file mode 100644 index 00000000..56d47fa9 --- /dev/null +++ b/apps/web/components/admin/InvitesList.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { ActionButton } from "@/components/ui/action-button"; +import { ButtonWithTooltip } from "@/components/ui/button"; +import LoadingSpinner from "@/components/ui/spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { formatDistanceToNow } from "date-fns"; +import { Mail, MailX, UserPlus } from "lucide-react"; + +import ActionConfirmingDialog from "../ui/action-confirming-dialog"; +import CreateInviteDialog from "./CreateInviteDialog"; + +export default function InvitesList() { + const invalidateInvitesList = api.useUtils().invites.list.invalidate; + const { data: invites, isLoading } = api.invites.list.useQuery(); + + const { mutateAsync: revokeInvite, isPending: isRevokePending } = + api.invites.revoke.useMutation({ + onSuccess: () => { + toast({ + description: "Invite revoked successfully", + }); + invalidateInvitesList(); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: `Failed to revoke invite: ${e.message}`, + }); + }, + }); + + const { mutateAsync: resendInvite, isPending: isResendPending } = + api.invites.resend.useMutation({ + onSuccess: () => { + toast({ + description: "Invite resent successfully", + }); + invalidateInvitesList(); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: `Failed to resend invite: ${e.message}`, + }); + }, + }); + + if (isLoading) { + return ; + } + + const activeInvites = + invites?.invites?.filter( + (invite) => new Date(invite.expiresAt) > new Date(), + ) || []; + + const expiredInvites = + invites?.invites?.filter( + (invite) => new Date(invite.expiresAt) <= new Date(), + ) || []; + + const getStatusBadge = ( + invite: NonNullable["invites"][0], + ) => { + if (new Date(invite.expiresAt) <= new Date()) { + return ( + + Expired + + ); + } + return ( + + Active + + ); + }; + + const InviteTable = ({ + invites: inviteList, + title, + }: { + invites: NonNullable["invites"]; + title: string; + }) => ( +
+

+ {title} ({inviteList.length}) +

+ {inviteList.length === 0 ? ( +

+ No {title.toLowerCase()} invites +

+ ) : ( + + + Email + Invited By + Created + Expires + Status + Actions + + + {inviteList.map((invite) => ( + + {invite.email} + {invite.invitedBy.name} + + {formatDistanceToNow(new Date(invite.createdAt), { + addSuffix: true, + })} + + + {formatDistanceToNow(new Date(invite.expiresAt), { + addSuffix: true, + })} + + {getStatusBadge(invite)} + + {new Date(invite.expiresAt) > new Date() && ( + <> + resendInvite({ inviteId: invite.id })} + disabled={isResendPending} + > + + + ( + { + await revokeInvite({ inviteId: invite.id }); + setDialogOpen(false); + }} + > + Revoke + + )} + > + + + + + + )} + + + ))} + +
+ )} +
+ ); + + return ( +
+
+ User Invitations + + + + + +
+ + + +
+ ); +} -- cgit v1.2.3-70-g09d2