11"use client" ;
22import { formatDistanceToNow } from "date-fns" ;
33import {
4+ ArrowRightIcon ,
45 ChevronLeftIcon ,
56 ChevronRightIcon ,
67 ExternalLinkIcon ,
78} from "lucide-react" ;
8- import { useState } from "react" ;
9+ import { useMemo , useState } from "react" ;
910import { type ThirdwebClient , type ThirdwebContract , toTokens } from "thirdweb" ;
1011import type { ChainMetadata } from "thirdweb/chains" ;
1112import { WalletAddress } from "@/components/blocks/wallet-address" ;
@@ -20,7 +21,6 @@ import {
2021 TableHeader ,
2122 TableRow ,
2223} from "@/components/ui/table" ;
23- import { cn } from "@/lib/utils" ;
2424import {
2525 type TokenTransfersData ,
2626 useTokenTransfers ,
@@ -45,6 +45,32 @@ function RecentTransfersUI(props: {
4545 explorerUrl : string ;
4646 client : ThirdwebClient ;
4747} ) {
48+ const groupedData = useMemo ( ( ) => {
49+ const data : Array < {
50+ group : TokenTransfersData [ ] ;
51+ transactionHash : string ;
52+ blockTimestamp : string ;
53+ } > = [ ] ;
54+
55+ for ( const transfer of props . data ) {
56+ const existingGroup = data . find (
57+ ( group ) => group . transactionHash === transfer . transaction_hash ,
58+ ) ;
59+
60+ if ( existingGroup ) {
61+ existingGroup . group . push ( transfer ) ;
62+ } else {
63+ data . push ( {
64+ group : [ transfer ] ,
65+ transactionHash : transfer . transaction_hash ,
66+ blockTimestamp : transfer . block_timestamp ,
67+ } ) ;
68+ }
69+ }
70+
71+ return data ;
72+ } , [ props . data ] ) ;
73+
4874 return (
4975 < div >
5076 < div className = "p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-lg" >
@@ -74,76 +100,89 @@ function RecentTransfersUI(props: {
74100 // biome-ignore lint/suspicious/noArrayIndexKey: EXPECTED
75101 < SkeletonRow key = { index } />
76102 ) )
77- : props . data . map ( ( transfer ) => (
103+ : groupedData . map ( ( group ) => (
78104 < TableRow
79105 className = "fade-in-0 animate-in duration-300"
80- key = {
81- transfer . transaction_hash +
82- transfer . amount +
83- transfer . block_number +
84- transfer . from_address
85- }
106+ key = { group . transactionHash }
86107 >
87- < TableCell className = "text-sm" >
88- < WalletAddress
89- address = { transfer . from_address }
90- client = { props . client }
91- />
92- </ TableCell >
93- < TableCell className = "text-sm" >
94- < WalletAddress
95- address = { transfer . to_address }
96- client = { props . client }
97- />
108+ { /* From */ }
109+ < TableCell className = "relative space-y-1" >
110+ { group . group . map ( ( transfer ) => (
111+ < div
112+ className = "h-10 flex items-center gap-6 w-[150px]"
113+ key = { transfer . log_index }
114+ >
115+ < WalletAddress
116+ address = { transfer . from_address }
117+ client = { props . client }
118+ iconClassName = "size-4.5"
119+ />
120+ < ArrowRightIcon className = "size-4 text-muted-foreground/50 absolute -right-1 lg:right-3" />
121+ </ div >
122+ ) ) }
98123 </ TableCell >
99- < TableCell className = "text-sm " >
100- < div className = "flex items-center gap-1.5" >
101- < span >
102- { tokenAmountFormatter . format (
103- Number (
104- toTokens (
105- BigInt ( transfer . amount ) ,
106- props . tokenMetadata . decimals ,
107- ) ,
108- ) ,
109- ) }
110- </ span >
111- < span className = "text-muted-foreground text-xs" >
112- { props . tokenMetadata . symbol }
113- </ span >
114- </ div >
124+
125+ { /* To */ }
126+ < TableCell className = "relative space-y-1" >
127+ { group . group . map ( ( transfer ) => (
128+ < div
129+ className = "h-10 flex items-center gap-6 w-[150px]"
130+ key = { transfer . log_index }
131+ >
132+ < WalletAddress
133+ address = { transfer . to_address }
134+ client = { props . client }
135+ key = { transfer . log_index }
136+ iconClassName = "size-4.5"
137+ />
138+ </ div >
139+ ) ) }
115140 </ TableCell >
116- < TableCell className = "text-sm" >
117- { formatDistanceToNow (
118- new Date (
119- transfer . block_timestamp . endsWith ( "Z" )
120- ? transfer . block_timestamp
121- : `${ transfer . block_timestamp } Z` ,
122- ) ,
123- {
124- addSuffix : true ,
125- } ,
126- ) }
141+
142+ { /* Amount */ }
143+ < TableCell className = "space-y-1" >
144+ { group . group . map ( ( transfer ) => (
145+ < div
146+ className = "h-10 flex items-center"
147+ key = { transfer . log_index }
148+ >
149+ < TokenAmount
150+ amount = { transfer . amount }
151+ decimals = { props . tokenMetadata . decimals }
152+ symbol = { props . tokenMetadata . symbol }
153+ />
154+ </ div >
155+ ) ) }
127156 </ TableCell >
157+
158+ { /* timestamp */ }
128159 < TableCell >
129- < Button
130- asChild
131- className = "h-8 w-8 p-0"
132- size = "sm"
133- variant = "ghost"
160+ < div
161+ key = { group . blockTimestamp }
162+ className = "capitalize text-muted-foreground text-sm"
134163 >
135- < a
136- className = { cn (
137- "flex items-center justify-center" ,
138- "hover:bg-accent hover:text-accent-foreground" ,
139- ) }
140- href = { `${ props . explorerUrl } /tx/${ transfer . transaction_hash } ` }
141- rel = "noopener noreferrer"
142- target = "_blank"
164+ { timestamp ( group . blockTimestamp ) }
165+ </ div >
166+ </ TableCell >
167+
168+ { /* transaction */ }
169+ < TableCell >
170+ < div className = "flex items-center justify-center" >
171+ < Button
172+ asChild
173+ size = "sm"
174+ variant = "outline"
175+ className = "text-muted-foreground hover:text-foreground rounded-full size-9 p-0 flex items-center justify-center"
143176 >
144- < ExternalLinkIcon className = "h-4 w-4" />
145- </ a >
146- </ Button >
177+ < a
178+ href = { `${ props . explorerUrl } /tx/${ group . transactionHash } ` }
179+ rel = "noopener noreferrer"
180+ target = "_blank"
181+ >
182+ < ExternalLinkIcon className = "size-3.5" />
183+ </ a >
184+ </ Button >
185+ </ div >
147186 </ TableCell >
148187 </ TableRow >
149188 ) ) }
@@ -183,6 +222,34 @@ function RecentTransfersUI(props: {
183222 ) ;
184223}
185224
225+ function timestamp ( block_timestamp : string ) {
226+ return formatDistanceToNow (
227+ new Date (
228+ block_timestamp . endsWith ( "Z" ) ? block_timestamp : `${ block_timestamp } Z` ,
229+ ) ,
230+ {
231+ addSuffix : true ,
232+ } ,
233+ ) ;
234+ }
235+
236+ function TokenAmount ( props : {
237+ amount : string ;
238+ decimals : number ;
239+ symbol : string ;
240+ } ) {
241+ return (
242+ < div className = "flex items-center gap-1.5" >
243+ < span >
244+ { tokenAmountFormatter . format (
245+ Number ( toTokens ( BigInt ( props . amount ) , props . decimals ) ) ,
246+ ) }
247+ </ span >
248+ < span className = "text-muted-foreground text-xs" > { props . symbol } </ span >
249+ </ div >
250+ ) ;
251+ }
252+
186253function SkeletonRow ( ) {
187254 return (
188255 < TableRow className = "fade-in-0 h-[73px] animate-in duration-300" >
@@ -199,7 +266,7 @@ function SkeletonRow() {
199266 < Skeleton className = "h-6 w-32" />
200267 </ TableCell >
201268 < TableCell >
202- < Skeleton className = "h-6 w-6 " />
269+ < Skeleton className = "size-9 rounded-full " />
203270 </ TableCell >
204271 </ TableRow >
205272 ) ;
0 commit comments