УЛУЧШЕНИЕ - NODEDC LAUNCHER: полные ссылки инвайтов

This commit is contained in:
DCCONSTRUCTIONS 2026-05-05 16:05:54 +03:00
parent bd1575d18a
commit 821a4009f0
2 changed files with 126 additions and 59 deletions

View File

@ -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;

View File

@ -2090,6 +2090,7 @@ function InvitesSection({
const [email, setEmail] = useState("");
const [role, setRole] = useState<ClientMembershipRole>("member");
const [deleteInviteId, setDeleteInviteId] = useState<string | null>(null);
const [copiedInviteId, setCopiedInviteId] = useState<string | null>(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 (
<div className="invites-layout invites-layout--catalog">
<GlassSurface className="invite-form invite-form--compact">
@ -2149,7 +2170,11 @@ function InvitesSection({
</tr>
</thead>
<tbody>
{invites.map((invite) => (
{invites.map((invite) => {
const inviteUrl = buildInviteUrl(invite.token);
const isCopied = copiedInviteId === invite.id;
return (
<tr key={invite.id}>
<td>
<input
@ -2180,12 +2205,13 @@ function InvitesSection({
</td>
<td>
<div className="invite-link-cell">
<code>{`/invite/${invite.token}`}</code>
<code title={inviteUrl}>{inviteUrl}</code>
{isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
<IconButton
label={`Скопировать инвайт ${invite.email}`}
label={isCopied ? `Инвайт ${invite.email} скопирован` : `Скопировать инвайт ${invite.email}`}
className="admin-circle-action services-admin-table__edit"
type="button"
onClick={() => void navigator.clipboard?.writeText(`/invite/${invite.token}`)}
onClick={() => void handleCopyInvite(invite)}
>
<Copy size={14} />
</IconButton>
@ -2211,7 +2237,8 @@ function InvitesSection({
</IconButton>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</GlassSurface>
@ -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,