Skip to content

Commit 121dc2f

Browse files
committed
feat: add WalletAddress component with copy button and explorer link
- Created reusable WalletAddress component - Shows shortened address (0x1234...5678) - Copy button to copy full address - Link to block explorer (Gnosis, Ethereum, etc.) - Updated finance pages and transaction table to use it
1 parent 595a30d commit 121dc2f

5 files changed

Lines changed: 137 additions & 26 deletions

File tree

src/app/[year]/finance/[accountSlug]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { MoneriumOrder } from "@/lib/monerium-node";
88
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
99
import { Badge } from "@/components/ui/badge";
1010
import { FinanceTransactionTable } from "@/components/finance-transaction-table";
11+
import { WalletAddress } from "@/components/wallet-address";
1112

1213
interface PageProps {
1314
params: Promise<{
@@ -388,10 +389,10 @@ export default async function YearlyFinancePage({ params }: PageProps) {
388389
<p className="text-muted-foreground">
389390
{year} - {augmentedTransactions.length} transactions
390391
</p>
391-
<div className="flex gap-2 mt-2">
392+
<div className="flex flex-wrap items-center gap-2 mt-2">
392393
<Badge variant="outline">{account.chain}</Badge>
393394
<Badge variant="outline">{account.token.symbol}</Badge>
394-
<Badge variant="outline">{shortenAddress(account.address)}</Badge>
395+
<WalletAddress address={account.address} chain={account.chain} />
395396
</div>
396397
</div>
397398

src/app/finance/[slug]/page.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
RefreshCw,
2929
} from "lucide-react";
3030
import Link from "next/link";
31+
import { WalletAddress } from "@/components/wallet-address";
3132
import settings from "@/settings/settings.json";
3233

3334
interface MonthlyBreakdown {
@@ -348,28 +349,26 @@ export default function AccountPage() {
348349
<span className="text-muted-foreground">Chain</span>
349350
<span className="font-medium capitalize">{data.chain}</span>
350351
</div>
351-
<div className="flex justify-between">
352+
<div className="flex justify-between items-center">
352353
<span className="text-muted-foreground">Wallet Address</span>
353-
<code className="font-mono text-sm bg-muted px-2 py-1 rounded">
354-
{data.address}
355-
</code>
354+
<WalletAddress
355+
address={data.address}
356+
chain={data.chain || "ethereum"}
357+
/>
356358
</div>
357359
<div className="flex justify-between">
358360
<span className="text-muted-foreground">Token</span>
359361
<span className="font-medium">{data.tokenSymbol}</span>
360362
</div>
361363
{"token" in accountConfig && accountConfig.token && (
362-
<div className="flex justify-between">
364+
<div className="flex justify-between items-center">
363365
<span className="text-muted-foreground">
364366
Token Contract
365367
</span>
366-
<Link
367-
href={`${explorerBaseUrl}/token/${accountConfig.token.address}`}
368-
target="_blank"
369-
className="font-mono text-sm text-primary hover:underline"
370-
>
371-
{formatAddress(accountConfig.token.address)}
372-
</Link>
368+
<WalletAddress
369+
address={accountConfig.token.address}
370+
chain={data.chain || "ethereum"}
371+
/>
373372
</div>
374373
)}
375374
</CardContent>

src/app/finance/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "@/components/ui/table";
2020
import { ArrowDownLeft, ArrowUpRight, Wallet, RefreshCw } from "lucide-react";
2121
import Link from "next/link";
22+
import { WalletAddress } from "@/components/wallet-address";
2223

2324
interface MonthlyBreakdown {
2425
month: string;
@@ -344,10 +345,10 @@ export default function FinanceOverviewPage() {
344345
<CardTitle className="text-lg flex items-center justify-between">
345346
<span>{account.name}</span>
346347
{account.address && (
347-
<span className="text-xs font-mono text-muted-foreground">
348-
{account.address.slice(0, 6)}...
349-
{account.address.slice(-4)}
350-
</span>
348+
<WalletAddress
349+
address={account.address}
350+
chain={account.chain || "ethereum"}
351+
/>
351352
)}
352353
</CardTitle>
353354
<CardDescription>

src/components/finance-transaction-table.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "@/components/ui/select";
1414
import { Input } from "@/components/ui/input";
1515
import { InlineDescriptionEditor } from "@/components/inline-description-editor";
16+
import { WalletAddress } from "@/components/wallet-address";
1617
import type { TokenTransfer } from "@/lib/etherscan";
1718
import type { MoneriumOrder } from "@/lib/monerium-node";
1819
import settings from "@/settings/settings.json";
@@ -1082,14 +1083,12 @@ export function FinanceTransactionTable({
10821083
{tx.moneriumOrder.counterpart.details.name}
10831084
</div>
10841085
) : (
1085-
<a
1086-
href={`https://gnosisscan.io/address/${isIncoming ? tx.from : tx.to}`}
1087-
target="_blank"
1088-
rel="noopener noreferrer"
1089-
className="font-mono text-xs hover:underline text-muted-foreground"
1090-
>
1091-
{shortenAddress(isIncoming ? tx.from : tx.to)}
1092-
</a>
1086+
<WalletAddress
1087+
address={isIncoming ? tx.from : tx.to}
1088+
chain={chain}
1089+
showLink={true}
1090+
showCopy={true}
1091+
/>
10931092
)}
10941093
{isAdmin && tx.counterpartyId && (
10951094
<div onClick={(e) => e.stopPropagation()}>

src/components/wallet-address.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Copy, Check, ExternalLink } from "lucide-react";
5+
import { cn } from "@/lib/utils";
6+
7+
interface WalletAddressProps {
8+
address: string;
9+
chain?: string;
10+
className?: string;
11+
showCopy?: boolean;
12+
showLink?: boolean;
13+
}
14+
15+
/**
16+
* Get block explorer URL for a chain
17+
*/
18+
function getExplorerUrl(chain: string, address: string): string {
19+
const explorers: Record<string, string> = {
20+
gnosis: "https://gnosisscan.io/address/",
21+
ethereum: "https://etherscan.io/address/",
22+
polygon: "https://polygonscan.com/address/",
23+
arbitrum: "https://arbiscan.io/address/",
24+
optimism: "https://optimistic.etherscan.io/address/",
25+
base: "https://basescan.org/address/",
26+
celo: "https://celoscan.io/address/",
27+
};
28+
const baseUrl = explorers[chain.toLowerCase()] || "https://etherscan.io/address/";
29+
return `${baseUrl}${address}`;
30+
}
31+
32+
/**
33+
* Shorten an address for display
34+
*/
35+
function shortenAddress(address: string): string {
36+
if (!address || address.length < 10) return address;
37+
return `${address.slice(0, 6)}${address.slice(-4)}`;
38+
}
39+
40+
export function WalletAddress({
41+
address,
42+
chain = "ethereum",
43+
className,
44+
showCopy = true,
45+
showLink = true,
46+
}: WalletAddressProps) {
47+
const [copied, setCopied] = useState(false);
48+
49+
const handleCopy = async (e: React.MouseEvent) => {
50+
e.preventDefault();
51+
e.stopPropagation();
52+
try {
53+
await navigator.clipboard.writeText(address);
54+
setCopied(true);
55+
setTimeout(() => setCopied(false), 2000);
56+
} catch (err) {
57+
console.error("Failed to copy address:", err);
58+
}
59+
};
60+
61+
const explorerUrl = getExplorerUrl(chain, address);
62+
63+
return (
64+
<span
65+
className={cn(
66+
"inline-flex items-center gap-1 font-mono text-xs text-muted-foreground",
67+
className
68+
)}
69+
>
70+
{showLink ? (
71+
<a
72+
href={explorerUrl}
73+
target="_blank"
74+
rel="noopener noreferrer"
75+
className="hover:text-foreground transition-colors hover:underline"
76+
onClick={(e) => e.stopPropagation()}
77+
title={`View on ${chain} explorer`}
78+
>
79+
{shortenAddress(address)}
80+
</a>
81+
) : (
82+
<span title={address}>{shortenAddress(address)}</span>
83+
)}
84+
{showCopy && (
85+
<button
86+
onClick={handleCopy}
87+
className="p-0.5 hover:bg-muted rounded transition-colors"
88+
title={copied ? "Copied!" : "Copy address"}
89+
>
90+
{copied ? (
91+
<Check className="w-3 h-3 text-green-500" />
92+
) : (
93+
<Copy className="w-3 h-3" />
94+
)}
95+
</button>
96+
)}
97+
{showLink && (
98+
<a
99+
href={explorerUrl}
100+
target="_blank"
101+
rel="noopener noreferrer"
102+
className="p-0.5 hover:bg-muted rounded transition-colors"
103+
onClick={(e) => e.stopPropagation()}
104+
title={`View on ${chain} explorer`}
105+
>
106+
<ExternalLink className="w-3 h-3" />
107+
</a>
108+
)}
109+
</span>
110+
);
111+
}

0 commit comments

Comments
 (0)