From 821a4009f080dff650a4f6e1143d77ee8afcd789 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 5 May 2026 16:05:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=9B=D0=A3=D0=A7=D0=A8=D0=95=D0=9D?= =?UTF-8?q?=D0=98=D0=95=20-=20NODEDC=20LAUNCHER:=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B2=D0=B0=D0=B9=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/globals.css | 10 +- src/widgets/admin-overlay/AdminOverlay.tsx | 175 ++++++++++++++------- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index 5c63a3b..c9360d4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2904,7 +2904,7 @@ code { .invite-link-cell { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 0.45rem; } @@ -2917,6 +2917,14 @@ code { white-space: nowrap; } +.invite-link-cell__state { + color: var(--accent-lime); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.02em; + white-space: nowrap; +} + .admin-helper-note { max-width: 38rem; margin: 0.22rem 0 0; diff --git a/src/widgets/admin-overlay/AdminOverlay.tsx b/src/widgets/admin-overlay/AdminOverlay.tsx index c5788b1..f4efcb5 100644 --- a/src/widgets/admin-overlay/AdminOverlay.tsx +++ b/src/widgets/admin-overlay/AdminOverlay.tsx @@ -2090,6 +2090,7 @@ function InvitesSection({ const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const [deleteInviteId, setDeleteInviteId] = useState(null); + const [copiedInviteId, setCopiedInviteId] = useState(null); const invites = data.invites.filter((invite) => invite.clientId === clientId); const deletingInvite = invites.find((invite) => invite.id === deleteInviteId) ?? null; const actor = getUser(data, actorUserId); @@ -2102,6 +2103,26 @@ function InvitesSection({ setRole("member"); } + async function handleCopyInvite(invite: Invite) { + const inviteUrl = buildInviteUrl(invite.token); + + try { + await copyToClipboard(inviteUrl); + } catch { + return; + } + + setCopiedInviteId(invite.id); + + if (invite.status === "created") { + onUpdateInvite(invite.id, { status: "sent" }); + } + + window.setTimeout(() => { + setCopiedInviteId((currentInviteId) => (currentInviteId === invite.id ? null : currentInviteId)); + }, 1800); + } + return (
@@ -2149,69 +2170,75 @@ function InvitesSection({ - {invites.map((invite) => ( - - - onUpdateInvite(invite.id, { email: event.target.value })} - aria-label={`Email инвайта ${invite.email}`} - /> - - - onUpdateInvite(invite.id, { role: nextRole })} - /> - - - onUpdateInvite(invite.id, { status })} - /> - - -
- {`/invite/${invite.token}`} + {invites.map((invite) => { + const inviteUrl = buildInviteUrl(invite.token); + const isCopied = copiedInviteId === invite.id; + + return ( + + + onUpdateInvite(invite.id, { email: event.target.value })} + aria-label={`Email инвайта ${invite.email}`} + /> + + + onUpdateInvite(invite.id, { role: nextRole })} + /> + + + onUpdateInvite(invite.id, { status })} + /> + + +
+ {inviteUrl} + {isCopied ? Скопировано : null} + void handleCopyInvite(invite)} + > + + +
+ + + { + if (value) onUpdateInvite(invite.id, { expiresAt: value }); + }} + /> + + void navigator.clipboard?.writeText(`/invite/${invite.token}`)} + onClick={() => setDeleteInviteId(invite.id)} > - + -
- - - { - if (value) onUpdateInvite(invite.id, { expiresAt: value }); - }} - /> - - - setDeleteInviteId(invite.id)} - > - - - - - ))} + + + ); + })}
@@ -2233,6 +2260,38 @@ function InvitesSection({ ); } +function buildInviteUrl(token: string) { + if (typeof window === "undefined") return `/invite/${token}`; + + return new URL(`/invite/${token}`, window.location.origin).toString(); +} + +async function copyToClipboard(value: string) { + if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + if (typeof document === "undefined") { + throw new Error("Clipboard API is not available"); + } + + const input = document.createElement("textarea"); + input.value = value; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.opacity = "0"; + document.body.append(input); + input.select(); + + const copied = document.execCommand("copy"); + input.remove(); + + if (!copied) { + throw new Error("Fallback copy failed"); + } +} + function SyncSection({ data, clientId,