Skip to content

Commit 655b900

Browse files
authored
Merge pull request #396 from RaymondAbiola/feat/improv
Feat/improv
2 parents 2d2d2af + b7570ee commit 655b900

8 files changed

Lines changed: 315 additions & 108 deletions

File tree

app/(merchant)/dashboard/page.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ const mockPaymentLinks = [
4848
{ id: 'link_02', label: 'E-commerce Checkout', url: 'betta.pay/pay/link_02', clicks: 112, converted: 47 },
4949
{ id: 'link_03', label: 'Donation Campaign', url: 'betta.pay/pay/link_03', clicks: 58, converted: 19 },
5050
];
51-
import RevenueChart from '@/components/charts/RevenueChart';
5251

5352
const PERIOD_OPTIONS = ['7D', '30D', '90D'] as const;
5453
type Period = typeof PERIOD_OPTIONS[number];
@@ -202,7 +201,6 @@ export default function DashboardPage() {
202201
</div>
203202
</CardHeader>
204203
<CardContent className="pt-0">
205-
<RevenueChart height={260} />
206204
{chartError ? (
207205
<div className="h-[260px] flex items-center justify-center">
208206
<ErrorDisplay
@@ -211,7 +209,7 @@ export default function DashboardPage() {
211209
/>
212210
</div>
213211
) : (
214-
<RevenueChart height={260} />
212+
<RevenueChart height={260} data={payments} />
215213
)}
216214
{/* Summary row */}
217215
<div className="flex items-center gap-6 pt-4 border-t border-border mt-2">

app/(merchant)/fx/page.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
11
"use client";
22

3-
import { useState } from 'react';
4-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5-
import { Button } from '@/components/ui/button';
6-
import { RefreshCcw, TrendingUp, TrendingDown, Info } from 'lucide-react';
7-
import dynamic from 'next/dynamic';
8-
9-
const FxRateChart = dynamic(() => import('@/components/charts/FxRateChart'), {
10-
ssr: false,
11-
loading: () => <div className="h-[240px] bg-slate-50 animate-pulse rounded-xl w-full" />
12-
});
133
import { useState, useEffect } from 'react';
144
import dynamic from 'next/dynamic';
155
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui';
@@ -220,7 +210,11 @@ export default function FxRatesPage() {
220210
<CardTitle className="text-base font-semibold text-foreground">USDC/NGN — 7 Day Chart</CardTitle>
221211
</CardHeader>
222212
<CardContent>
223-
<FxRateChart height={240} />
213+
<FxRateChart
214+
height={240}
215+
error={fxError ? 'Failed to load FX rate history' : ratesError}
216+
onRetry={refetch}
217+
/>
224218
</CardContent>
225219
</Card>
226220

@@ -265,8 +259,6 @@ export default function FxRatesPage() {
265259
<Bell className="w-4 h-4 text-primary" /> Create Rate Alert
266260
</CardTitle>
267261
</CardHeader>
268-
<CardContent>
269-
<FxRateChart height={240} />
270262
<CardContent className="space-y-4">
271263
<div className="grid grid-cols-2 gap-4">
272264
<div className="space-y-2">

components/charts/FxRateChart.tsx

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
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>
233
import { useState, useEffect } from "react";
244
import { useTheme } from "next-themes";
255
import {
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 }} />
8463
interface 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

Comments
 (0)