108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useCallback, useEffect, useState } from 'react';
|
||
|
|
import {
|
||
|
|
organizationApi,
|
||
|
|
type OrganizationInvitationHistoryItemDto,
|
||
|
|
} from '@/lib/api/organization';
|
||
|
|
import {
|
||
|
|
canShareOrganizationInviteLink,
|
||
|
|
readOrganizationInviteLinks,
|
||
|
|
type StoredOrganizationInviteLink,
|
||
|
|
writeOrganizationInviteLinks,
|
||
|
|
} from '@/components/invitations/organizationInviteLinks';
|
||
|
|
|
||
|
|
export function useOrganizationInviteLinkCopy(organizationId: string | undefined) {
|
||
|
|
const [pendingInviteLinks, setPendingInviteLinks] = useState<
|
||
|
|
Record<string, StoredOrganizationInviteLink>
|
||
|
|
>({});
|
||
|
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||
|
|
const [copyingInvitationId, setCopyingInvitationId] = useState<string | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!organizationId) return;
|
||
|
|
setPendingInviteLinks(readOrganizationInviteLinks(organizationId));
|
||
|
|
}, [organizationId]);
|
||
|
|
|
||
|
|
const storeInviteLink = useCallback(
|
||
|
|
(invitationId: string, ownerEmail: string, invitationUrl: string) => {
|
||
|
|
if (!organizationId) return;
|
||
|
|
setPendingInviteLinks((prev) => {
|
||
|
|
const next = {
|
||
|
|
...prev,
|
||
|
|
[invitationId]: {
|
||
|
|
invitationId,
|
||
|
|
ownerEmail: ownerEmail.trim().toLowerCase(),
|
||
|
|
invitationUrl,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
writeOrganizationInviteLinks(organizationId, next);
|
||
|
|
return next;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[organizationId],
|
||
|
|
);
|
||
|
|
|
||
|
|
const pruneAcceptedLinks = useCallback(
|
||
|
|
(items: OrganizationInvitationHistoryItemDto[]) => {
|
||
|
|
if (!organizationId) return;
|
||
|
|
const acceptedIds = new Set(
|
||
|
|
items.filter((item) => item.acceptedAt || item.status === 'ACTIVE').map((item) => item.id),
|
||
|
|
);
|
||
|
|
|
||
|
|
setPendingInviteLinks((prev) => {
|
||
|
|
let changed = false;
|
||
|
|
const next = { ...prev };
|
||
|
|
for (const id of Object.keys(next)) {
|
||
|
|
if (acceptedIds.has(id)) {
|
||
|
|
delete next[id];
|
||
|
|
changed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (changed) {
|
||
|
|
writeOrganizationInviteLinks(organizationId, next);
|
||
|
|
}
|
||
|
|
return changed ? next : prev;
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[organizationId],
|
||
|
|
);
|
||
|
|
|
||
|
|
const copyInvitationLink = useCallback(
|
||
|
|
async (
|
||
|
|
invitation: OrganizationInvitationHistoryItemDto,
|
||
|
|
options?: { onRegenerated?: () => void | Promise<void> },
|
||
|
|
): Promise<string | null> => {
|
||
|
|
if (!canShareOrganizationInviteLink(invitation)) return null;
|
||
|
|
|
||
|
|
setCopyingInvitationId(invitation.id);
|
||
|
|
try {
|
||
|
|
let invitationUrl = pendingInviteLinks[invitation.id]?.invitationUrl;
|
||
|
|
if (!invitationUrl || invitation.status === 'EXPIRED') {
|
||
|
|
const res = await organizationApi.getInvitationLink(invitation.id);
|
||
|
|
invitationUrl = res.data.invitationUrl;
|
||
|
|
storeInviteLink(invitation.id, invitation.ownerEmail, invitationUrl);
|
||
|
|
await options?.onRegenerated?.();
|
||
|
|
}
|
||
|
|
|
||
|
|
await navigator.clipboard.writeText(invitationUrl);
|
||
|
|
setCopiedId(invitation.id);
|
||
|
|
setTimeout(() => setCopiedId(null), 1500);
|
||
|
|
return invitationUrl;
|
||
|
|
} finally {
|
||
|
|
setCopyingInvitationId(null);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[pendingInviteLinks, storeInviteLink],
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
pendingInviteLinks,
|
||
|
|
copiedId,
|
||
|
|
copyingInvitationId,
|
||
|
|
storeInviteLink,
|
||
|
|
copyInvitationLink,
|
||
|
|
pruneAcceptedLinks,
|
||
|
|
};
|
||
|
|
}
|