Skip to content

Commit 4e3ac0d

Browse files
tusharshah21joelamouche
authored andcommitted
feat: Add copy to clipboard icons next to all ethereum addresses
1 parent 4cacd81 commit 4e3ac0d

4 files changed

Lines changed: 89 additions & 7 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useState } from "react";
2+
import { Copy, Check } from "lucide-react";
3+
import { cn } from "@/lib/utils";
4+
5+
interface CopyAddressToClipboardProps {
6+
address: string;
7+
displayAddress?: string;
8+
className?: string;
9+
iconSize?: "sm" | "md";
10+
showFeedback?: boolean;
11+
}
12+
13+
export function CopyAddressToClipboard({
14+
address,
15+
displayAddress,
16+
className,
17+
iconSize = "sm",
18+
showFeedback = true,
19+
}: CopyAddressToClipboardProps) {
20+
const [copied, setCopied] = useState(false);
21+
22+
const handleCopy = async (e: React.MouseEvent) => {
23+
e.preventDefault();
24+
e.stopPropagation();
25+
26+
try {
27+
await navigator.clipboard.writeText(address);
28+
if (showFeedback) {
29+
setCopied(true);
30+
setTimeout(() => setCopied(false), 2000);
31+
}
32+
} catch (err) {
33+
console.error("Failed to copy address:", err);
34+
}
35+
};
36+
37+
const iconClasses = cn(
38+
"transition-colors hover:text-gray-700 cursor-pointer",
39+
iconSize === "sm" ? "h-3.5 w-3.5" : "h-4 w-4",
40+
copied ? "text-green-600" : "text-gray-500"
41+
);
42+
43+
return (
44+
<div className={cn("inline-flex items-center gap-2", className)}>
45+
{displayAddress && (
46+
<span className="font-mono">{displayAddress}</span>
47+
)}
48+
<button
49+
onClick={handleCopy}
50+
className="inline-flex items-center justify-center p-1 rounded hover:bg-gray-100 transition-colors"
51+
title={copied ? "Copied!" : "Copy full address to clipboard"}
52+
aria-label={copied ? "Address copied" : "Copy address to clipboard"}
53+
>
54+
{copied ? (
55+
<Check className={iconClasses} />
56+
) : (
57+
<Copy className={iconClasses} />
58+
)}
59+
</button>
60+
</div>
61+
);
62+
}
63+
64+
export default CopyAddressToClipboard;

frontend/src/components/profiles/list/ProfileCard.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useAccount } from "wagmi";
1212
import DeleteProfileDialog from "../action-buttons/DeleteProfileDialog";
1313
import { AddAttestationDialog } from "../action-buttons/AddAttestationDialog";
1414
import AddressTokenBalance from "@/components/AddressTokenBalance";
15+
import CopyAddressToClipboard from "@/components/CopyAddressToClipboard";
1516

1617
interface ProfileCardProps {
1718
address: string;
@@ -75,10 +76,15 @@ export function ProfileCard({
7576
</span>
7677
)}
7778
</CardTitle>
78-
<CardDescription className="font-mono">
79-
<a href={`/profiles/${address}`} className="hover:underline">
79+
<CardDescription>
80+
<a href={`/profiles/${address}`} className="hover:underline font-mono">
8081
{displayAddress}
8182
</a>
83+
<CopyAddressToClipboard
84+
address={address}
85+
className="ml-2"
86+
iconSize="sm"
87+
/>
8288
</CardDescription>
8389
<AddressTokenBalance address={address as `0x${string}`} />
8490
</div>

frontend/src/components/profiles/profile-page/ProfileAttestations.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AccordionTrigger,
99
} from "@/components/ui/accordion";
1010
import { useGetProfiles } from "@/hooks/profiles/use-get-profiles";
11+
import CopyAddressToClipboard from "@/components/CopyAddressToClipboard";
1112

1213
export function ProfileAttestations({ address }: { address: string }) {
1314
const attestationsQuery = useGetAttestations();
@@ -87,17 +88,22 @@ export function ProfileAttestations({ address }: { address: string }) {
8788
<p className="text-sm text-gray-800">
8889
{it.justification}
8990
</p>
90-
<p className="text-xs text-gray-500 mt-1">
91-
Issued by{" "}
91+
<div className="text-xs text-gray-500 mt-1 flex items-center gap-1">
92+
<span>Issued by{" "}</span>
9293
<a
9394
className="text-indigo-600 hover:underline"
9495
href={`/profiles/${it.issuer}`}
9596
>
9697
{profileNameByAddress.get(
9798
it.issuer.toLowerCase()
98-
) || it.issuer}
99+
) || `${it.issuer.slice(0, 6)}...${it.issuer.slice(-4)}`}
99100
</a>
100-
</p>
101+
<CopyAddressToClipboard
102+
address={it.issuer}
103+
iconSize="sm"
104+
showFeedback={false}
105+
/>
106+
</div>
101107
</div>
102108
</div>
103109
</li>

frontend/src/components/profiles/profile-page/ProfileHeader.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useAccount } from "wagmi";
33
import { useMemo } from "react";
44
import { useGetProfiles } from "@/hooks/profiles/use-get-profiles";
55
import AddressTokenBalance from "@/components/AddressTokenBalance";
6+
import CopyAddressToClipboard from "@/components/CopyAddressToClipboard";
67

78
export function ProfileHeader({ address }: { address: string }) {
89
const profilesQuery = useGetProfiles();
@@ -43,7 +44,12 @@ export function ProfileHeader({ address }: { address: string }) {
4344
)}
4445
</h1>
4546
{displayAddress ? (
46-
<p className="font-mono text-sm text-gray-600">{displayAddress}</p>
47+
<CopyAddressToClipboard
48+
address={address}
49+
displayAddress={displayAddress}
50+
className="text-sm text-gray-600"
51+
iconSize="sm"
52+
/>
4753
) : null}
4854
<AddressTokenBalance address={address as `0x${string}`} />
4955
</div>

0 commit comments

Comments
 (0)