diff --git a/apps/web/src/components/pending-invites.tsx b/apps/web/src/components/pending-invites.tsx
index 01c2196..7fcc184 100644
--- a/apps/web/src/components/pending-invites.tsx
+++ b/apps/web/src/components/pending-invites.tsx
@@ -9,14 +9,32 @@ import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
-import { Mail } from "lucide-react";
+import { Copy, Mail, RefreshCw } from "lucide-react";
const label = (role: string) => role.charAt(0).toUpperCase() + role.slice(1);
export function PendingInvites({ groupId, eventId, canRevoke = false }: { groupId: string; eventId?: string; canRevoke?: boolean }) {
const utils = api.useUtils();
const confirmation = useConfirmAction();
- const resend = api.group.resendInvite.useMutation({ onSuccess: () => toast.success("Invitation resent. Previous link replaced."), onError: error => toast.error(error.message) });
+ const copyLink = api.group.copyInviteLink.useMutation();
+ async function copyInvite(inviteId: string) {
+ try {
+ // Start the clipboard operation in the click gesture, including on Safari.
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
+ const content = copyLink.mutateAsync({ groupId, inviteId }).then(({ url }) => new Blob([url], { type: "text/plain" }));
+ await navigator.clipboard.write([new ClipboardItem({ "text/plain": content })]);
+ } else {
+ const { url } = await copyLink.mutateAsync({ groupId, inviteId });
+ await navigator.clipboard.writeText(url);
+ }
+ toast.success("Invitation link copied");
+ } catch {
+ toast.error("Could not copy the link. Check clipboard permissions and try again.");
+ } finally {
+ copyLink.reset();
+ }
+ }
+ const resend = api.group.resendInvite.useMutation({ onSuccess: async (_, input) => { toast.success(input.regenerate ? "New invitation link emailed. Previous link disabled." : "Invitation resent with the same link."); await utils.group.pendingInvites.invalidate(); }, onError: error => toast.error(error.message) });
const revoke = api.group.revokeInvite.useMutation({
onSuccess: async () => { toast.success("Invite revoked"); await utils.group.pendingInvites.invalidate(); },
onError: error => toast.error(error.message),
@@ -42,11 +60,13 @@ export function PendingInvites({ groupId, eventId, canRevoke = false }: { groupI
{invite.groupRole ? Workspace {label(invite.groupRole)} : null}
{invite.eventRole ? Event {label(invite.eventRole)} : null}
{invite.kind === "email" ? "Awaiting acceptance" : "Active code"}
- {canRevoke ? : null}
- {canRevoke && invite.kind === "email" ?