Skip to content

Commit 374c003

Browse files
committed
fix: add tx hash copy action in timeline rows
Add compact transaction timeline rows with truncated tx hashes and inline copy action feedback for success/failure states. Closes #160 Made-with: Cursor
1 parent 754b924 commit 374c003

2 files changed

Lines changed: 166 additions & 19 deletions

File tree

PR_DESCRIPTION_160.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Summary
2+
- #160: Add truncated tx hash copy action in timeline rows
3+
4+
## Changes
5+
- Replaced the placeholder timeline empty-state panel with compact transaction timeline rows that include truncated transaction hash entries.
6+
- Added a copy-to-clipboard action per row for tx hashes, with brief inline feedback for both success (`check`) and failure (`x`) outcomes.
7+
- Kept the row layout compact and scannable by using a dense 4-column row structure (action, amount, hash+copy action, status/time).
8+
9+
## Testing
10+
- [ ] Open the landing page and confirm timeline rows render under "Transaction timeline pattern" with truncated hash values.
11+
- [ ] Click the copy icon for each row and verify success feedback appears briefly, then resets.
12+
- [ ] Simulate clipboard denial/unavailable state and verify failure feedback appears briefly, then resets.
13+
- [ ] Confirm row layout remains compact on small and medium breakpoints.
14+
15+
## Closing
16+
Closes accesslayerorg/accesslayer-client#160
Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,156 @@
1+
import { useState } from 'react';
12
import { Button } from '@/components/ui/button';
2-
import { Clock3 } from 'lucide-react';
3+
import { Check, Clock3, Copy, XCircle } from 'lucide-react';
4+
5+
type CopyState = 'idle' | 'success' | 'error';
6+
7+
interface TimelineEntry {
8+
id: string;
9+
action: string;
10+
amount: string;
11+
txHash: string;
12+
timeLabel: string;
13+
status: 'confirmed' | 'pending' | 'failed';
14+
}
15+
16+
const TIMELINE_ENTRIES: TimelineEntry[] = [
17+
{
18+
id: 'entry-1',
19+
action: 'Buy',
20+
amount: '+2 keys',
21+
txHash: '0x2a43bcfdef77ca4c50ef7d38148dd5d7f0149a6e2e20f70f04ce1f4b66fe55dd',
22+
timeLabel: '2m ago',
23+
status: 'confirmed',
24+
},
25+
{
26+
id: 'entry-2',
27+
action: 'Sell',
28+
amount: '-1 key',
29+
txHash: '0x90c82ac01478b42fcbf9db73a26ed32bd8e50a8917e2408c31c95e9f6a59fc19',
30+
timeLabel: '18m ago',
31+
status: 'pending',
32+
},
33+
{
34+
id: 'entry-3',
35+
action: 'Buy',
36+
amount: '+3 keys',
37+
txHash: '0x16d2ffbc4297a8c2c3086e07c16e66f47287df0d5a1ce1aef9e448e2f0f3ab51',
38+
timeLabel: '51m ago',
39+
status: 'failed',
40+
},
41+
];
42+
43+
const shortenTxHash = (hash: string) => `${hash.slice(0, 8)}...${hash.slice(-6)}`;
344

445
const EmptyTransactionTimelineState: React.FC = () => {
5-
return (
6-
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 md:p-8">
7-
<div className="mx-auto max-w-2xl text-center">
8-
<div className="mx-auto mb-4 inline-flex size-12 items-center justify-center rounded-full border border-white/15 bg-white/10 text-amber-300">
9-
<Clock3 className="size-5" />
10-
</div>
11-
<h3 className="font-grotesque text-2xl font-bold text-white">
12-
No transactions yet
13-
</h3>
14-
<p className="mt-2 text-sm text-white/65">
15-
New users will see transaction milestones here after submitting their first buy or sell action.
16-
</p>
17-
<div className="mt-5 flex justify-center">
18-
<Button className="rounded-xl">Buy your first key</Button>
19-
</div>
20-
</div>
21-
</section>
22-
);
46+
const [copyStateById, setCopyStateById] = useState<Record<string, CopyState>>({});
47+
48+
const copyTxHash = async (entryId: string, txHash: string) => {
49+
try {
50+
await navigator.clipboard.writeText(txHash);
51+
setCopyStateById(current => ({ ...current, [entryId]: 'success' }));
52+
} catch {
53+
setCopyStateById(current => ({ ...current, [entryId]: 'error' }));
54+
}
55+
56+
window.setTimeout(() => {
57+
setCopyStateById(current => ({ ...current, [entryId]: 'idle' }));
58+
}, 1300);
59+
};
60+
61+
return (
62+
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 md:p-8">
63+
<div className="mx-auto max-w-4xl">
64+
<div className="mb-5 flex items-start justify-between gap-3">
65+
<div>
66+
<h3 className="font-grotesque text-2xl font-bold text-white">
67+
Transaction timeline
68+
</h3>
69+
<p className="mt-2 text-sm text-white/65">
70+
Recent trade events with quick copy access for tx hashes.
71+
</p>
72+
</div>
73+
<div className="inline-flex size-11 shrink-0 items-center justify-center rounded-full border border-white/15 bg-white/10 text-amber-300">
74+
<Clock3 className="size-5" />
75+
</div>
76+
</div>
77+
78+
<div className="space-y-2">
79+
{TIMELINE_ENTRIES.map(entry => {
80+
const copyState = copyStateById[entry.id] ?? 'idle';
81+
const isSuccess = copyState === 'success';
82+
const isError = copyState === 'error';
83+
const statusClass =
84+
entry.status === 'confirmed'
85+
? 'text-emerald-300'
86+
: entry.status === 'pending'
87+
? 'text-amber-300'
88+
: 'text-rose-300';
89+
90+
return (
91+
<div
92+
key={entry.id}
93+
className="grid grid-cols-[auto_auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg border border-white/5 bg-white/[0.04] px-3 py-2 text-xs md:text-sm"
94+
>
95+
<span className="text-white/80">{entry.action}</span>
96+
<span className="font-semibold text-white">{entry.amount}</span>
97+
<div className="min-w-0">
98+
<div className="flex items-center gap-2">
99+
<span
100+
className="truncate font-mono text-white/70"
101+
title={entry.txHash}
102+
>
103+
{shortenTxHash(entry.txHash)}
104+
</span>
105+
<button
106+
type="button"
107+
onClick={() => copyTxHash(entry.id, entry.txHash)}
108+
className="inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-white/5 text-white/55 transition-colors hover:bg-white/10 hover:text-white"
109+
aria-label={
110+
isSuccess
111+
? 'Transaction hash copied'
112+
: isError
113+
? 'Transaction hash copy failed'
114+
: 'Copy transaction hash'
115+
}
116+
>
117+
{isSuccess ? (
118+
<Check className="size-3 text-emerald-400" />
119+
) : isError ? (
120+
<XCircle className="size-3 text-rose-400" />
121+
) : (
122+
<Copy className="size-3" />
123+
)}
124+
</button>
125+
</div>
126+
<span
127+
role="status"
128+
aria-live="polite"
129+
aria-atomic="true"
130+
className="sr-only"
131+
>
132+
{isSuccess
133+
? 'Transaction hash copied to clipboard'
134+
: isError
135+
? 'Failed to copy transaction hash'
136+
: ''}
137+
</span>
138+
</div>
139+
<div className="text-right">
140+
<p className={statusClass}>{entry.status}</p>
141+
<p className="text-[10px] text-white/40">{entry.timeLabel}</p>
142+
</div>
143+
</div>
144+
);
145+
})}
146+
</div>
147+
148+
<div className="mt-5 flex justify-center">
149+
<Button className="rounded-xl">Open full history</Button>
150+
</div>
151+
</div>
152+
</section>
153+
);
23154
};
24155

25156
export default EmptyTransactionTimelineState;

0 commit comments

Comments
 (0)