11"use client" ;
22
33import { format , formatDistanceToNowStrict } from "date-fns" ;
4- import { ExternalLinkIcon , InfoIcon } from "lucide-react" ;
4+ import { ExternalLink , Info , ChevronDown , ChevronRight } from "lucide-react" ;
55import Link from "next/link" ;
6+ import { useState } from "react" ;
67import { hexToNumber , isHex , type ThirdwebClient , toEther } from "thirdweb" ;
78import type { Project } from "@/api/projects" ;
89import { WalletAddress } from "@/components/blocks/wallet-address" ;
@@ -16,15 +17,18 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
1617import { ChainIconClient } from "@/icons/ChainIcon" ;
1718import { statusDetails } from "../../analytics/tx-table/tx-table-ui" ;
1819import type { Transaction } from "../../analytics/tx-table/types" ;
20+ import { type ActivityLogEntry } from "../../lib/analytics" ;
1921
2022export function TransactionDetailsUI ( {
2123 transaction,
2224 client,
25+ activityLogs,
2326} : {
2427 transaction : Transaction ;
2528 teamSlug : string ;
2629 client : ThirdwebClient ;
2730 project : Project ;
31+ activityLogs : ActivityLogEntry [ ] ;
2832} ) {
2933 const { idToChain } = useAllChainsData ( ) ;
3034
@@ -45,8 +49,8 @@ export function TransactionDetailsUI({
4549 executionResult && "error" in executionResult
4650 ? executionResult . error . message
4751 : executionResult && "revertData" in executionResult
48- ? executionResult . revertData ?. revertReason
49- : null ;
52+ ? executionResult . revertData ?. revertReason
53+ : null ;
5054 const errorDetails =
5155 executionResult && "error" in executionResult
5256 ? executionResult . error
@@ -68,12 +72,18 @@ export function TransactionDetailsUI({
6872 // Gas information
6973 const gasUsed =
7074 executionResult && "actualGasUsed" in executionResult
71- ? `${ isHex ( executionResult . actualGasUsed ) ? hexToNumber ( executionResult . actualGasUsed ) : executionResult . actualGasUsed } `
75+ ? `${
76+ isHex ( executionResult . actualGasUsed )
77+ ? hexToNumber ( executionResult . actualGasUsed )
78+ : executionResult . actualGasUsed
79+ } `
7280 : "N/A" ;
7381
7482 const gasCost =
7583 executionResult && "actualGasCost" in executionResult
76- ? `${ toEther ( BigInt ( executionResult . actualGasCost || "0" ) ) } ${ chain ?. nativeCurrency . symbol || "" } `
84+ ? `${ toEther ( BigInt ( executionResult . actualGasCost || "0" ) ) } ${
85+ chain ?. nativeCurrency . symbol || ""
86+ } `
7787 : "N/A" ;
7888
7989 return (
@@ -156,7 +166,10 @@ export function TransactionDetailsUI({
156166 rel = "noopener noreferrer"
157167 target = "_blank"
158168 >
159- { `${ transactionHash . slice ( 0 , 8 ) } ...${ transactionHash . slice ( - 6 ) } ` } { " " }
169+ { `${ transactionHash . slice (
170+ 0 ,
171+ 8 ,
172+ ) } ...${ transactionHash . slice ( - 6 ) } `} { " " }
160173 < ExternalLinkIcon className = "size-4 text-muted-foreground" />
161174 </ Link >
162175 </ Button >
@@ -165,7 +178,10 @@ export function TransactionDetailsUI({
165178 className = "font-mono text-muted-foreground text-sm"
166179 copyIconPosition = "left"
167180 textToCopy = { transactionHash }
168- textToShow = { `${ transactionHash . slice ( 0 , 6 ) } ...${ transactionHash . slice ( - 4 ) } ` }
181+ textToShow = { `${ transactionHash . slice (
182+ 0 ,
183+ 6 ,
184+ ) } ...${ transactionHash . slice ( - 4 ) } `}
169185 tooltip = "Copy transaction hash"
170186 variant = "ghost"
171187 />
@@ -347,7 +363,122 @@ export function TransactionDetailsUI({
347363 ) }
348364 </ CardContent >
349365 </ Card >
366+
367+ { /* Activity Log Card */ }
368+ < ActivityLogCard activityLogs = { activityLogs } />
350369 </ div >
351370 </ >
352371 ) ;
353372}
373+
374+ // Activity Log Timeline Component
375+ function ActivityLogCard ( {
376+ activityLogs,
377+ } : { activityLogs : ActivityLogEntry [ ] } ) {
378+ return (
379+ < Card >
380+ < CardHeader >
381+ < CardTitle className = "text-lg" > Activity Log</ CardTitle >
382+ </ CardHeader >
383+ < CardContent >
384+ { activityLogs . length === 0 ? (
385+ < p className = "text-muted-foreground text-sm" >
386+ No activity logs available for this transaction
387+ </ p >
388+ ) : (
389+ < div className = "space-y-4" >
390+ { activityLogs . map ( ( log , index ) => (
391+ < ActivityLogEntry
392+ key = { log . id }
393+ log = { log }
394+ isLast = { index === activityLogs . length - 1 }
395+ />
396+ ) ) }
397+ </ div >
398+ ) }
399+ </ CardContent >
400+ </ Card >
401+ ) ;
402+ }
403+
404+ function ActivityLogEntry ( {
405+ log,
406+ isLast,
407+ } : { log : ActivityLogEntry ; isLast : boolean } ) {
408+ const [ isExpanded , setIsExpanded ] = useState ( false ) ;
409+
410+ return (
411+ < div className = "relative" >
412+ { /* Timeline line */ }
413+ { ! isLast && (
414+ < div className = "absolute left-4 top-8 h-full w-0.5 bg-border" />
415+ ) }
416+
417+ < div className = "flex items-start gap-4" >
418+ { /* Timeline dot */ }
419+ < div className = "relative flex h-8 w-8 items-center justify-center rounded-full bg-muted" >
420+ < div className = "h-3 w-3 rounded-full bg-primary" />
421+ </ div >
422+
423+ { /* Content */ }
424+ < div className = "flex-1 min-w-0" >
425+ < button
426+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
427+ className = "flex w-full items-center justify-between py-2 text-left hover:bg-muted/50 rounded-md px-2 -ml-2"
428+ >
429+ < div className = "flex items-center gap-2" >
430+ < span className = "font-medium text-sm" > { log . stageName } </ span >
431+ < span className = "text-muted-foreground text-xs" >
432+ { formatDistanceToNowStrict ( new Date ( log . timestamp ) , {
433+ addSuffix : true ,
434+ } ) }
435+ </ span >
436+ </ div >
437+ { isExpanded ? (
438+ < ChevronDown className = "h-4 w-4 text-muted-foreground" />
439+ ) : (
440+ < ChevronRight className = "h-4 w-4 text-muted-foreground" />
441+ ) }
442+ </ button >
443+
444+ { isExpanded && (
445+ < div className = "mt-2 space-y-3 px-2" >
446+ < div className = "grid grid-cols-2 gap-4 text-sm" >
447+ < div >
448+ < div className = "text-muted-foreground" > Event Type</ div >
449+ < div className = "font-mono" > { log . eventType } </ div >
450+ </ div >
451+ < div >
452+ < div className = "text-muted-foreground" > Executor</ div >
453+ < div className = "font-mono" > { log . executorName } </ div >
454+ </ div >
455+ < div >
456+ < div className = "text-muted-foreground" > Batch Index</ div >
457+ < div className = "font-mono" > { log . batchIndex } </ div >
458+ </ div >
459+ < div >
460+ < div className = "text-muted-foreground" > Timestamp</ div >
461+ < div className = "font-mono text-xs" >
462+ { format ( new Date ( log . timestamp ) , "PP pp z" ) }
463+ </ div >
464+ </ div >
465+ </ div >
466+
467+ { log . payload && (
468+ < div >
469+ < div className = "text-muted-foreground text-sm mb-2" >
470+ Payload
471+ </ div >
472+ < CodeClient
473+ code = { JSON . stringify ( log . payload , null , 2 ) }
474+ lang = "json"
475+ />
476+ </ div >
477+ ) }
478+ </ div >
479+ ) }
480+ </ div >
481+ </ div >
482+ </ div >
483+ ) ;
484+ }
0 commit comments