11"use client" ;
22
3- import React from 'react' ;
4- import { ResponsiveContainer , LineChart , Line , XAxis , YAxis , Tooltip , CartesianGrid } from 'recharts' ;
5-
6- const fxHistory = [
7- { date : 'Jan 7' , rate : 1480 } ,
8- { date : 'Jan 8' , rate : 1495 } ,
9- { date : 'Jan 9' , rate : 1510 } ,
10- { date : 'Jan 10' , rate : 1505 } ,
11- { date : 'Jan 11' , rate : 1520 } ,
12- { date : 'Jan 12' , rate : 1545 } ,
13- { date : 'Jan 13' , rate : 1550 } ,
14- ] ;
15-
16- export interface FxTooltipProps { active ?: boolean ; payload ?: { value : number } [ ] ; label ?: string ; }
17- export const FxTooltip = ( { active, payload, label } : FxTooltipProps ) => {
18- if ( active && payload && payload . length ) {
19- return (
20- < div className = "bg-white border border-slate-200 rounded-xl p-3 shadow-lg text-sm" >
21- < p className = "font-semibold text-slate-700 mb-1" > { label } </ p >
22- < p className = "text-amber-600 font-bold" > ₦{ payload [ 0 ] ?. value ?. toLocaleString ( ) } </ p >
233import { useState , useEffect } from "react" ;
244import { useTheme } from "next-themes" ;
255import {
266 ResponsiveContainer ,
27- LineChart ,
28- Line ,
7+ AreaChart ,
8+ Area ,
299 XAxis ,
3010 YAxis ,
3111 Tooltip ,
3212 CartesianGrid ,
3313} from "recharts" ;
14+ // Imported directly (not via the barrel) to keep this lazy-loaded chunk lean.
15+ import { ErrorDisplay } from "@/components/shared/ErrorDisplay" ;
3416
35- const fxHistory = [
17+ export interface FxRatePoint {
18+ date : string ;
19+ rate : number ;
20+ }
21+
22+ /** 7 days of USDC/NGN rates — used when the caller does not supply live data. */
23+ const fxHistory : FxRatePoint [ ] = [
3624 { date : "Jan 7" , rate : 1480 } ,
3725 { date : "Jan 8" , rate : 1495 } ,
3826 { date : "Jan 9" , rate : 1510 } ,
@@ -42,51 +30,54 @@ const fxHistory = [
4230 { date : "Jan 13" , rate : 1550 } ,
4331] ;
4432
45- interface FxTooltipProps {
33+ const formatNgn = ( value : number ) => `₦${ value . toLocaleString ( ) } ` ;
34+
35+ export interface FxTooltipProps {
4636 active ?: boolean ;
4737 payload ?: { value : number } [ ] ;
4838 label ?: string ;
4939}
5040
51- const FxTooltip = ( { active , payload , label } : FxTooltipProps) => {
52- const { resolvedTheme } = useTheme ( ) ;
53- const isDark = resolvedTheme === "dark" ;
54-
41+ export const FxTooltip = ( { active, payload, label } : FxTooltipProps ) => {
5542 if ( active && payload && payload . length ) {
5643 return (
57- < div
44+ < div
5845 className = "border rounded-xl p-3 shadow-lg text-sm"
59- style = { {
60- backgroundColor : isDark ? "var(--card)" : "var(--card)" ,
61- borderColor : isDark ? "var(--border)" : "var(--border)" ,
46+ style = { {
47+ backgroundColor : "var(--card)" ,
48+ borderColor : "var(--border)" ,
6249 } }
6350 >
64- < p className = "font-semibold mb-1" style = { { color : isDark ? "var(--foreground)" : "var(--foreground)" } } > { label } </ p >
65- < p className = "font-bold" style = { { color : isDark ? "var(--primary)" : "var(--primary)" } } >
66- ₦{ payload [ 0 ] ?. value ?. toLocaleString ( ) }
51+ < p className = "font-semibold mb-1" style = { { color : "var(--foreground)" } } >
52+ { label }
53+ </ p >
54+ < p className = "font-bold" style = { { color : "var(--primary)" } } >
55+ { formatNgn ( payload [ 0 ] ?. value ?? 0 ) }
6756 </ p >
6857 </ div >
6958 ) ;
7059 }
7160 return null ;
7261} ;
7362
74- export default function FxRateChart ( { height = 240 } : { height ?: number } ) {
75- return (
76- < div style = { { height, width : '100%' } } >
77- < ResponsiveContainer width = "100%" height = "100%" >
78- < LineChart data = { fxHistory } margin = { { top : 4 , right : 4 , bottom : 0 , left : - 10 } } >
79- < CartesianGrid strokeDasharray = "3 3" stroke = "#F1F5F9" vertical = { false } />
80- < XAxis dataKey = "date" fontSize = { 11 } tickLine = { false } axisLine = { false } tick = { { fill : '#94A3B8' } } />
81- < YAxis fontSize = { 11 } tickLine = { false } axisLine = { false } tickFormatter = { ( v ) => `₦${ v } ` } tick = { { fill : '#94A3B8' } } domain = { [ 'auto' , 'auto' ] } />
82- < Tooltip content = { < FxTooltip /> } />
83- < Line type = "monotone" dataKey = "rate" stroke = "#F0A500" strokeWidth = { 2.5 } dot = { false } activeDot = { { r : 5 , fill : '#F0A500' , stroke : '#fff' , strokeWidth : 2 } } />
8463interface FxRateChartProps {
8564 height ?: number ;
65+ /** Live rate history. Falls back to 7 days of mock data when omitted. */
66+ data ?: FxRatePoint [ ] ;
67+ /** Set when the caller's rate fetch failed — renders an in-chart error state. */
68+ error ?: string | Error | null ;
69+ onRetry ?: ( ) => void ;
8670}
8771
88- export default function FxRateChart({ height = 240 } : FxRateChartProps) {
72+ export default function FxRateChart ( {
73+ height = 240 ,
74+ data,
75+ error = null ,
76+ onRetry,
77+ } : FxRateChartProps ) {
8978 const [ isMobile , setIsMobile ] = useState ( false ) ;
79+ const { resolvedTheme } = useTheme ( ) ;
80+ const isDark = resolvedTheme === "dark" ;
9081
9182 useEffect ( ( ) => {
9283 const mq = window . matchMedia ( "(max-width: 639px)" ) ;
@@ -96,13 +87,37 @@ export default function FxRateChart({ height = 240 }: FxRateChartProps) {
9687 return ( ) => mq . removeEventListener ( "change" , handler ) ;
9788 } , [ ] ) ;
9889
90+ const chartData = data && data . length > 0 ? data : fxHistory ;
91+ const hasError = Boolean ( error ) || ( data !== undefined && data . length === 0 ) ;
92+
93+ if ( hasError ) {
94+ return (
95+ < div className = "w-full flex items-center justify-center" style = { { height } } >
96+ < ErrorDisplay
97+ message = {
98+ typeof error === "string"
99+ ? error
100+ : "Unable to load FX rate history."
101+ }
102+ onRetry = { onRetry }
103+ />
104+ </ div >
105+ ) ;
106+ }
107+
99108 return (
100109 < div className = "w-full" style = { { height } } >
101110 < ResponsiveContainer width = "100%" height = "100%" >
102- < LineChart
103- data = { fxHistory }
111+ < AreaChart
112+ data = { chartData }
104113 margin = { { top : 4 , right : 4 , bottom : 0 , left : isMobile ? 0 : - 10 } }
105114 >
115+ < defs >
116+ < linearGradient id = "colorFxRate" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
117+ < stop offset = "5%" stopColor = "var(--primary)" stopOpacity = { isDark ? 0.4 : 0.25 } />
118+ < stop offset = "95%" stopColor = "var(--primary)" stopOpacity = { isDark ? 0.05 : 0 } />
119+ </ linearGradient >
120+ </ defs >
106121 < CartesianGrid
107122 strokeDasharray = "3 3"
108123 stroke = "var(--border)"
@@ -119,16 +134,19 @@ export default function FxRateChart({ height = 240 }: FxRateChartProps) {
119134 fontSize = { 11 }
120135 tickLine = { false }
121136 axisLine = { false }
122- tickFormatter = { ( v ) => `₦ ${ v } ` }
137+ tickFormatter = { formatNgn }
123138 tick = { { fill : "var(--muted-foreground)" } }
124139 domain = { [ "auto" , "auto" ] }
140+ width = { isMobile ? 52 : 64 }
125141 />
126142 < Tooltip content = { < FxTooltip /> } />
127- < Line
143+ < Area
128144 type = "monotone"
129145 dataKey = "rate"
130146 stroke = "var(--primary)"
131147 strokeWidth = { 2.5 }
148+ fillOpacity = { 1 }
149+ fill = "url(#colorFxRate)"
132150 dot = { false }
133151 activeDot = { {
134152 r : 5 ,
@@ -137,7 +155,7 @@ export default function FxRateChart({ height = 240 }: FxRateChartProps) {
137155 strokeWidth : 2 ,
138156 } }
139157 />
140- </ LineChart >
158+ </ AreaChart >
141159 </ ResponsiveContainer >
142160 </ div >
143161 ) ;
0 commit comments