1- import { StatCard } from "components/analytics/stat" ;
1+ import { THIRDWEB_ENGINE_CLOUD_URL } from "@/constants/env" ;
2+ import { getAuthToken } from "app/api/lib/getAuthToken" ;
3+ import { StatCard } from "components/analytics/stat" ; // Assuming correct path
24import { ActivityIcon , CoinsIcon } from "lucide-react" ;
35import { Suspense } from "react" ;
6+ // Import the specific utility function needed
7+ import { toEther } from "thirdweb/utils" ;
48
5- // TODO: implement this
9+ // Define the structure of the data we expect back from our fetch function
10+ type TransactionSummaryData = {
11+ totalCount : number ;
12+ totalGasCostWei : string ;
13+ totalGasUnitsUsed : string ; // Keep fetched data structure
14+ } ;
15+
16+ // Define the structure of the API response
17+ type AnalyticsSummaryApiResponse = {
18+ result : {
19+ summary : {
20+ totalCount : number ;
21+ totalGasCostWei : string ;
22+ totalGasUnitsUsed : string ;
23+ } ;
24+ metadata : {
25+ startDate ?: string ;
26+ endDate ?: string ;
27+ } ;
28+ } ;
29+ } ;
30+
31+ // Fetches data from the /analytics-summary endpoint
632async function getTransactionAnalyticsSummary ( props : {
733 teamId : string ;
8- projectId : string ;
9- } ) {
10- console . log ( "getTransactionAnalyticsSummary called with" , props ) ;
11- await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
12-
13- return {
14- foo : 100 ,
15- bar : 200 ,
34+ clientId : string ;
35+ } ) : Promise < TransactionSummaryData > {
36+ const authToken = await getAuthToken ( ) ;
37+ const body = { } ;
38+ const defaultData : TransactionSummaryData = {
39+ totalCount : 0 ,
40+ totalGasCostWei : "0" ,
41+ totalGasUnitsUsed : "0" ,
1642 } ;
43+
44+ try {
45+ const response = await fetch (
46+ `${ THIRDWEB_ENGINE_CLOUD_URL } /project/transactions/analytics-summary` ,
47+ {
48+ method : "POST" ,
49+ headers : {
50+ "Content-Type" : "application/json" ,
51+ "x-team-id" : props . teamId ,
52+ "x-client-id" : props . clientId ,
53+ Authorization : `Bearer ${ authToken } ` ,
54+ } ,
55+ body : JSON . stringify ( body ) ,
56+ } ,
57+ ) ;
58+
59+ if ( ! response . ok ) {
60+ if ( response . status === 401 ) {
61+ console . error ( "Unauthorized fetching transaction summary" ) ;
62+ return defaultData ;
63+ }
64+ const errorText = await response . text ( ) ;
65+ throw new Error (
66+ `Error fetching transaction summary: ${ response . status } ${ response . statusText } - ${ errorText } ` ,
67+ ) ;
68+ }
69+
70+ const data = ( await response . json ( ) ) as AnalyticsSummaryApiResponse ;
71+
72+ return {
73+ totalCount : data . result . summary . totalCount ?? 0 ,
74+ totalGasCostWei : data . result . summary . totalGasCostWei ?? "0" ,
75+ totalGasUnitsUsed : data . result . summary . totalGasUnitsUsed ?? "0" ,
76+ } ;
77+ } catch ( error ) {
78+ console . error ( "Failed to fetch transaction summary:" , error ) ;
79+ return defaultData ;
80+ }
1781}
1882
19- // TODO: rename props, change labels and icons
83+ // Renders the UI based on fetched data or pending state
2084function TransactionAnalyticsSummaryUI ( props : {
21- data :
22- | {
23- foo : number ;
24- bar : number ;
25- }
26- | undefined ;
85+ data : TransactionSummaryData | undefined ;
2786 isPending : boolean ;
2887} ) {
88+ // Formatter function specifically for the StatCard prop
89+ // Typed to accept number for StatCard's prop type, but receives the string via `as any`
90+ const parseTotalGasCost = ( valueFromCard : string ) : number => {
91+ // At runtime, valueFromCard is the string passed via `as any` if data exists,
92+ // or potentially the fallback number (like 0) if data doesn't exist.
93+ // We prioritize the actual string from props.data if available.
94+ const weiString = props . data ?. totalGasCostWei ?? "0" ;
95+
96+ // Check if the effective value is zero
97+ if ( weiString === "0" ) {
98+ return 0 ;
99+ }
100+
101+ try {
102+ // Convert the definitive wei string to BigInt
103+ const weiBigInt = BigInt ( weiString ) ;
104+ // Use the imported toEther function
105+ return Number . parseFloat ( toEther ( weiBigInt ) ) ;
106+ } catch ( e ) {
107+ // Catch potential errors during BigInt conversion or formatting
108+ console . error ( "Error formatting wei value:" , weiString , e ) ;
109+ // Check if the value passed from card was actually the fallback number
110+ if ( typeof valueFromCard === "number" && valueFromCard === 0 ) {
111+ return 0 ; // If fallback 0 was passed, display 0
112+ }
113+ }
114+ return 0 ;
115+ } ;
116+
117+ // NOTE: props.data?.totalGasUnitsUsed is fetched but not currently displayed.
118+
29119 return (
30- < div className = "grid grid-cols-2 gap-4" >
120+ < div className = "grid grid-cols-1 gap-4 md:grid-cols-2 " >
31121 < StatCard
32- label = "Foo"
33- value = { props . data ?. foo }
122+ label = "Total Transactions"
123+ // Value is a number, standard formatter works
124+ value = { props . data ?. totalCount }
34125 icon = { ActivityIcon }
35126 isPending = { props . isPending }
36127 />
37128 < StatCard
38- label = "Bar "
39- value = { props . data ?. bar }
40- icon = { CoinsIcon }
41- formatter = { ( value : number ) =>
42- new Intl . NumberFormat ( "en-US" , {
43- style : "currency" ,
44- currency : "USD" ,
45- } ) . format ( value )
129+ label = "Total Gas Cost (ETH) "
130+ // If pending, value doesn't matter much.
131+ // If not pending, pass the wei string `as any` if data exists, otherwise pass 0.
132+ // Passing 0 ensures StatCard receives a number if data is missing post-loading.
133+ value = {
134+ props . isPending
135+ ? undefined
136+ : parseTotalGasCost ( props . data ?. totalGasCostWei ?? "0" )
46137 }
138+ formatter = { ( v : number ) => v . toFixed ( 12 ) }
139+ icon = { CoinsIcon }
140+ // Pass the formatter that handles the type juggling
47141 isPending = { props . isPending }
48142 />
143+ { /*
144+ // Example of how totalGasUnitsUsed could be added later:
145+ <StatCard
146+ label="Total Gas Units Used"
147+ value={(props.isPending ? undefined : (props.data?.totalGasUnitsUsed ?? 0)) as any} // Pass string as any
148+ icon={SomeOtherIcon}
149+ isPending={props.isPending}
150+ formatter={(value: number) => { // Formatter receives string via `as any`
151+ const unitString = props.data?.totalGasUnitsUsed ?? '0';
152+ if (unitString === '0') return '0';
153+ try {
154+ return new Intl.NumberFormat("en-US").format(BigInt(unitString));
155+ } catch {
156+ return "Error";
157+ }
158+ }}
159+ />
160+ */ }
49161 </ div >
50162 ) ;
51163}
52164
53- // fetches data and renders the UI
165+ // Fetches data and renders the UI component
54166async function AsyncTransactionsAnalyticsSummary ( props : {
55167 teamId : string ;
56- projectId : string ;
168+ clientId : string ;
57169} ) {
58170 const data = await getTransactionAnalyticsSummary ( {
59171 teamId : props . teamId ,
60- projectId : props . projectId ,
172+ clientId : props . clientId ,
61173 } ) ;
62-
63174 return < TransactionAnalyticsSummaryUI data = { data } isPending = { false } /> ;
64175}
65176
66- // shows loading state while fetching data
177+ // Main component: Shows loading state (Suspense fallback) while fetching data
67178export function TransactionAnalyticsSummary ( props : {
68179 teamId : string ;
69- projectId : string ;
180+ clientId : string ;
70181} ) {
71182 return (
72183 < Suspense
@@ -76,7 +187,7 @@ export function TransactionAnalyticsSummary(props: {
76187 >
77188 < AsyncTransactionsAnalyticsSummary
78189 teamId = { props . teamId }
79- projectId = { props . projectId }
190+ clientId = { props . clientId }
80191 />
81192 </ Suspense >
82193 ) ;
0 commit comments