|
| 1 | +import { useState } from 'react'; |
1 | 2 | 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)}`; |
3 | 44 |
|
4 | 45 | 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 | + ); |
23 | 154 | }; |
24 | 155 |
|
25 | 156 | export default EmptyTransactionTimelineState; |
0 commit comments