add copy to clipboard

This commit is contained in:
Eduard Gert
2026-05-26 11:23:08 +02:00
parent 4818599a93
commit b14feef1d7
3 changed files with 114 additions and 34 deletions

View File

@@ -0,0 +1,81 @@
import { useRef, useState, type ReactNode } from "react";
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/cn";
type CopyToClipboardProps = {
children: ReactNode;
message?: string;
size?: number;
iconAlignment?: "left" | "right";
className?: string;
alwaysShowIcon?: boolean;
};
export const CopyToClipboard = ({
children,
message,
size = 10,
iconAlignment = "right",
className,
alwaysShowIcon = false,
}: CopyToClipboardProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [copied, setCopied] = useState(false);
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const text = message ?? wrapperRef.current?.innerText ?? "";
if (!text) return;
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 500);
} catch {
//
}
};
return (
<div
ref={wrapperRef}
onClick={handleClick}
className={cn(
"inline-flex gap-2 items-center group/copy cursor-pointer wails-no-draggable",
className,
)}
>
<span className={cn("relative truncate min-w-0")}>
{children}
<span
className={
"absolute bottom-0 left-0 right-0 border-b border-dashed border-transparent group-hover/copy:border-nb-gray-500 pointer-events-none"
}
/>
</span>
<span
className={cn(
"shrink-0 inline-flex relative top-[2px] right-[1px]",
iconAlignment === "left" ? "order-first" : "order-last",
)}
>
<Check
size={size}
className={cn(
"text-nb-gray-100",
!copied && "hidden",
!alwaysShowIcon && !copied && "opacity-0",
)}
/>
<Copy
size={size}
className={cn(
"text-nb-gray-100 group-hover/copy:opacity-100",
copied && "hidden",
!alwaysShowIcon && "opacity-0",
)}
/>
</span>
</div>
);
};

View File

@@ -8,6 +8,7 @@ import { useStatus } from "@/modules/daemon-status/StatusContext.tsx";
import { useProfile } from "@/modules/profile/ProfileContext.tsx";
import { cn } from "@/lib/cn.ts";
import { formatErrorMessage } from "@/lib/errors.ts";
import { CopyToClipboard } from "@/components/CopyToClipboard";
import netbirdFullLogo from "@/assets/logos/netbird-full.svg";
enum ConnectionState {
@@ -323,22 +324,30 @@ export const ConnectionStatusSwitch = () => {
>
{t(STATUS_KEY[connState])}
</h1>
<p
<CopyToClipboard
message={fqdn}
className={cn(
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-2 transition-opacity duration-300 wails-no-draggable",
showLocal && fqdn ? "opacity-100" : "opacity-0",
"min-h-[1em] transition-opacity duration-300",
"relative left-2.5",
showLocal && fqdn ? "opacity-100" : "opacity-0 pointer-events-none",
)}
>
{fqdn || " "}
</p>
<p
<span className={"font-mono text-xs leading-tight text-nb-gray-300"}>
{fqdn || " "}
</span>
</CopyToClipboard>
<CopyToClipboard
message={ip}
className={cn(
"font-mono text-xs leading-tight min-h-[1em] text-nb-gray-300 mt-0.5 transition-opacity duration-300 wails-no-draggable",
showLocal && ip ? "opacity-100" : "opacity-0",
"min-h-[1em] transition-opacity duration-300 ",
"relative left-2.5",
showLocal && ip ? "opacity-100" : "opacity-0 pointer-events-none",
)}
>
{ip || " "}
</p>
<span className={"font-mono text-xs leading-tight text-nb-gray-300"}>
{ip || " "}
</span>
</CopyToClipboard>
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next";
import type { PeerStatus } from "@bindings/services/models.js";
import { cn } from "@/lib/cn";
import { CopyToClipboard } from "@/components/CopyToClipboard";
const dotClass = (connStatus: string): string => {
switch (connStatus) {
@@ -17,40 +18,29 @@ export const PeersList = ({ data }: { data: PeerStatus[] }) => {
const { t } = useTranslation();
if (data.length === 0) {
return (
<div className={"py-12 text-center text-sm text-nb-gray-400"}>
{t("peers.empty")}
</div>
<div className={"py-12 text-center text-sm text-nb-gray-400"}>{t("peers.empty")}</div>
);
}
return (
<ul className={"flex flex-col"}>
{data.map((peer) => (
<li
key={peer.pubKey}
className={"flex items-center gap-3 px-7 py-3 min-w-0"}
>
<li key={peer.pubKey} className={"flex items-center gap-3 px-7 py-3 min-w-0"}>
<span
className={cn(
"h-2 w-2 rounded-full shrink-0",
dotClass(peer.connStatus),
)}
className={cn("h-2 w-2 rounded-full shrink-0", dotClass(peer.connStatus))}
title={peer.connStatus}
/>
<span
className={
"text-[0.81rem] font-medium text-nb-gray-100 truncate"
}
<CopyToClipboard message={peer.fqdn} className={"min-w-0 flex-1"}>
<span className={"text-[0.81rem] font-medium text-nb-gray-100"}>
{peer.fqdn}
</span>
</CopyToClipboard>
<CopyToClipboard
message={peer.ip}
className={cn("ml-auto shrink-0", "relative left-2.5")}
>
{peer.fqdn}
</span>
<span
className={
"ml-auto text-xs font-mono text-nb-gray-400 shrink-0"
}
>
{peer.ip}
</span>
<span className={"text-xs font-mono text-nb-gray-400"}>{peer.ip}</span>
</CopyToClipboard>
</li>
))}
</ul>