1
- ' use client'
1
+ " use client" ;
2
2
3
- import { useState } from 'react'
4
- import { LineChart , Line , XAxis , YAxis , CartesianGrid , Tooltip , ResponsiveContainer , TooltipProps } from 'recharts'
5
- import { useSubscribeToAccountBalanceSubscription } from '@/lib/gql/urql'
3
+ import { useState } from "react" ;
4
+ import {
5
+ LineChart ,
6
+ Line ,
7
+ XAxis ,
8
+ YAxis ,
9
+ CartesianGrid ,
10
+ Tooltip ,
11
+ ResponsiveContainer ,
12
+ TooltipProps ,
13
+ } from "recharts" ;
14
+ import { useSubscribeToAccountBalanceSubscription } from "@/lib/gql/urql" ;
6
15
7
16
interface BalanceDataPoint {
8
- timestamp : string
9
- balance : number
10
- time : string // formatted time for display
17
+ timestamp : string ;
18
+ timestampMs : number ;
19
+ balance : number ;
20
+ time : string ; // formatted time for display
11
21
}
12
22
13
23
interface BalanceChartProps {
14
- accountId : string
15
- initialBalance : number
16
- className ?: string
24
+ accountId : string ;
25
+ initialBalance : number ;
26
+ className ?: string ;
17
27
}
18
28
29
+ const DAY_MS = 24 * 60 * 60 * 1000 ; // 24 hours
30
+
19
31
function formatCurrency ( cents : number ) : string {
20
- return new Intl . NumberFormat ( ' en-US' , {
21
- style : ' currency' ,
22
- currency : ' USD' ,
23
- } ) . format ( cents / 100 )
32
+ return new Intl . NumberFormat ( " en-US" , {
33
+ style : " currency" ,
34
+ currency : " USD" ,
35
+ } ) . format ( cents / 100 ) ;
24
36
}
25
37
26
38
function formatTime ( date : Date ) : string {
27
- return new Intl . DateTimeFormat ( ' en-US' , {
28
- hour : ' 2-digit' ,
29
- minute : ' 2-digit' ,
30
- second : ' 2-digit' ,
31
- } ) . format ( date )
39
+ return new Intl . DateTimeFormat ( " en-US" , {
40
+ hour : " 2-digit" ,
41
+ minute : " 2-digit" ,
42
+ second : " 2-digit" ,
43
+ } ) . format ( date ) ;
32
44
}
33
45
34
- export function BalanceChart ( { accountId, initialBalance, className } : BalanceChartProps ) {
35
- const [ balanceHistory , setBalanceHistory ] = useState < BalanceDataPoint [ ] > ( ( ) => {
36
- const now = new Date ( )
37
- return [ {
38
- timestamp : now . toISOString ( ) ,
39
- balance : initialBalance ,
40
- time : formatTime ( now )
41
- } ]
42
- } )
43
- const [ isLive , setIsLive ] = useState ( false )
46
+ export function BalanceChart ( {
47
+ accountId,
48
+ initialBalance,
49
+ className,
50
+ } : BalanceChartProps ) {
51
+ const [ balanceHistory , setBalanceHistory ] = useState < BalanceDataPoint [ ] > (
52
+ ( ) => {
53
+ const now = new Date ( ) ;
54
+ const dayAgo = new Date ( now . getTime ( ) - DAY_MS ) ;
55
+ // Seed with a baseline point 24h ago so the time axis spans the full window
56
+ return [
57
+ {
58
+ timestamp : dayAgo . toISOString ( ) ,
59
+ timestampMs : dayAgo . getTime ( ) ,
60
+ balance : initialBalance ,
61
+ time : formatTime ( dayAgo ) ,
62
+ } ,
63
+ {
64
+ timestamp : now . toISOString ( ) ,
65
+ timestampMs : now . getTime ( ) ,
66
+ balance : initialBalance ,
67
+ time : formatTime ( now ) ,
68
+ } ,
69
+ ] ;
70
+ }
71
+ ) ;
72
+ const [ isLive , setIsLive ] = useState ( false ) ;
44
73
45
74
// Subscribe to balance updates using GraphQL SSE subscription
46
75
const [ subscriptionResult ] = useSubscribeToAccountBalanceSubscription (
47
76
{ variables : { accountId } } ,
48
77
( prev , data ) => {
49
78
if ( data ?. accountBalanceUpdated ?. balance !== undefined ) {
50
- const now = new Date ( )
79
+ const now = new Date ( ) ;
51
80
const newDataPoint : BalanceDataPoint = {
52
81
timestamp : now . toISOString ( ) ,
82
+ timestampMs : now . getTime ( ) ,
53
83
balance : data . accountBalanceUpdated . balance ,
54
- time : formatTime ( now )
55
- }
56
-
57
- setBalanceHistory ( prev => {
58
- // Keep only the last 20 data points to prevent the chart from becoming too cluttered
59
- const newHistory = [ ...prev , newDataPoint ]
60
- return newHistory . slice ( - 20 )
61
- } )
62
- setIsLive ( true )
84
+ time : formatTime ( now ) ,
85
+ } ;
86
+
87
+ setBalanceHistory ( ( prev ) => {
88
+ // Keep only points from the last 24 hours (and cap to 500 to avoid unbounded growth)
89
+ const cutoff = now . getTime ( ) - DAY_MS ;
90
+ const newHistory = [ ...prev , newDataPoint ] . filter (
91
+ ( p ) => p . timestampMs >= cutoff
92
+ ) ;
93
+ return newHistory . slice ( - 500 ) ;
94
+ } ) ;
95
+ setIsLive ( true ) ;
63
96
}
64
- return data
97
+ return data ;
65
98
}
66
- )
99
+ ) ;
67
100
68
101
// Custom tooltip component for the chart
69
- const CustomTooltip = ( { active, payload, label } : TooltipProps < number , string > ) => {
102
+ const CustomTooltip = ( {
103
+ active,
104
+ payload,
105
+ label,
106
+ } : TooltipProps < number , string > ) => {
70
107
if ( active && payload && payload . length ) {
108
+ const ts = typeof label === "number" ? label : Number ( label ) ;
109
+ const labelTime = Number . isFinite ( ts )
110
+ ? new Intl . DateTimeFormat ( "en-US" , {
111
+ hour : "2-digit" ,
112
+ minute : "2-digit" ,
113
+ second : "2-digit" ,
114
+ } ) . format ( new Date ( ts ) )
115
+ : String ( label ) ;
71
116
return (
72
117
< div className = "bg-white p-3 border border-gray-200 rounded shadow-lg" >
73
- < p className = "text-sm text-gray-600" > { `Time: ${ label } ` } </ p >
118
+ < p className = "text-sm text-gray-600" > { `Time: ${ labelTime } ` } </ p >
74
119
< p className = "text-sm font-semibold" >
75
120
{ `Balance: ${ formatCurrency ( payload [ 0 ] . value || 0 ) } ` }
76
121
</ p >
77
122
</ div >
78
- )
123
+ ) ;
79
124
}
80
- return null
81
- }
125
+ return null ;
126
+ } ;
82
127
83
128
return (
84
129
< div className = { className } >
@@ -87,11 +132,13 @@ export function BalanceChart({ accountId, initialBalance, className }: BalanceCh
87
132
{ isLive && (
88
133
< div className = "flex items-center gap-1" >
89
134
< div className = "w-2 h-2 bg-green-500 rounded-full animate-pulse" > </ div >
90
- < span className = "text-xs text-green-600 font-medium" > LIVE UPDATES</ span >
135
+ < span className = "text-xs text-green-600 font-medium" >
136
+ LIVE UPDATES
137
+ </ span >
91
138
</ div >
92
139
) }
93
140
</ div >
94
-
141
+
95
142
< div className = "h-64 w-full" >
96
143
< ResponsiveContainer width = "100%" height = "100%" >
97
144
< LineChart
@@ -104,42 +151,63 @@ export function BalanceChart({ accountId, initialBalance, className }: BalanceCh
104
151
} }
105
152
>
106
153
< CartesianGrid strokeDasharray = "3 3" className = "opacity-30" />
107
- < XAxis
108
- dataKey = "time"
154
+ < XAxis
155
+ dataKey = "timestampMs"
156
+ type = "number"
157
+ scale = "time"
158
+ domain = { [ "dataMin" , "dataMax" ] }
159
+ tickFormatter = { ( value ) => {
160
+ const n = typeof value === "number" ? value : Number ( value ) ;
161
+ if ( ! Number . isFinite ( n ) ) return "" ;
162
+ const d = new Date ( n ) ;
163
+ if ( Number . isNaN ( d . getTime ( ) ) ) return "" ;
164
+ return new Intl . DateTimeFormat ( "en-US" , {
165
+ hour : "2-digit" ,
166
+ minute : "2-digit" ,
167
+ second : "2-digit" ,
168
+ } ) . format ( d ) ;
169
+ } }
109
170
tick = { { fontSize : 12 } }
110
171
interval = "preserveStartEnd"
111
172
/>
112
- < YAxis
173
+ < YAxis
113
174
tickFormatter = { ( value ) => formatCurrency ( value ) }
114
175
tick = { { fontSize : 12 } }
115
- domain = { [ ' dataMin - 100' , ' dataMax + 100' ] }
176
+ domain = { [ " dataMin - 100" , " dataMax + 100" ] }
116
177
/>
117
178
< Tooltip content = { < CustomTooltip /> } />
118
- < Line
119
- type = "monotone"
120
- dataKey = "balance"
121
- stroke = "#10b981"
179
+ < Line
180
+ type = "monotone"
181
+ dataKey = "balance"
182
+ stroke = "#10b981"
122
183
strokeWidth = { 2 }
123
- dot = { { fill : '#10b981' , strokeWidth : 2 , r : 4 } }
124
- activeDot = { { r : 6 , stroke : '#10b981' , strokeWidth : 2 , fill : '#fff' } }
184
+ dot = { { fill : "#10b981" , strokeWidth : 2 , r : 4 } }
185
+ activeDot = { {
186
+ r : 6 ,
187
+ stroke : "#10b981" ,
188
+ strokeWidth : 2 ,
189
+ fill : "#fff" ,
190
+ } }
125
191
/>
126
192
</ LineChart >
127
193
</ ResponsiveContainer >
128
194
</ div >
129
-
195
+
130
196
< div className = "mt-2 text-xs text-muted-foreground" >
131
197
{ isLive ? (
132
- < p > Real-time balance updates via SSE subscription • Showing last 20 data points</ p >
198
+ < p >
199
+ Real-time balance updates via SSE subscription • Showing last 24 hours
200
+ </ p >
133
201
) : (
134
202
< p > Waiting for live balance updates...</ p >
135
203
) }
136
204
</ div >
137
-
205
+
138
206
{ subscriptionResult . error && (
139
207
< p className = "text-xs text-red-500 mt-1" >
140
208
Chart subscription error: { subscriptionResult . error . message }
141
209
</ p >
142
210
) }
143
211
</ div >
144
- )
145
- }
212
+ ) ;
213
+ }
0 commit comments