Skip to content

Commit 0a96b06

Browse files
authored
Merge pull request #169 from connelevalsam/feature/reusable-kpi-issue-fix
2 parents 6a4c38a + f468d95 commit 0a96b06

6 files changed

Lines changed: 175 additions & 1 deletion

File tree

app/(admin)/overview/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
66
import { CurrencyDisplay } from '@/components/shared/CurrencyDisplay';
77
import { Users, AlertTriangle, ArrowUpRight, Activity, DollarSign } from 'lucide-react';
88
import { Skeleton } from '@/components/ui/skeleton';
9+
import { StatCard } from '@/components/shared/StatCard';
910

1011
const PlatformVolumeChart = dynamic(() => import('@/components/charts/PlatformVolumeChart'), {
1112
ssr: false,
@@ -39,6 +40,31 @@ export default function AdminOverviewPage() {
3940
</div>
4041

4142
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
43+
<StatCard
44+
title="Total Processed (30d)"
45+
icon={Activity}
46+
value={<CurrencyDisplay amount={1452310.89} />}
47+
trend={{ icon: ArrowUpRight, label: "+12.5% from last month", color: "text-green-500" }}
48+
/>
49+
<StatCard
50+
title="Platform Fees Generated"
51+
icon={DollarSign}
52+
value={<CurrencyDisplay amount={14523.10} />}
53+
trend={{ label: "1.0% flat fee across volume" }}
54+
/>
55+
<StatCard
56+
title="Active Merchants"
57+
icon={Users}
58+
value="142"
59+
trend={{ icon: ArrowUpRight, label: "+12 new this week", color: "text-green-500" }}
60+
/>
61+
<StatCard
62+
title="Pending KYB Reviews"
63+
icon={AlertTriangle}
64+
value="8"
65+
variant="destructive"
66+
trend={{ label: "Requires immediate action" }}
67+
/>
4268
<Card className="bg-card border shadow-sm relative overflow-hidden">
4369
<div className="absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mr-10 -mt-10 blur-xl"></div>
4470
<CardHeader className="flex flex-row items-center justify-between pb-2 z-10 relative">

app/(merchant)/dashboard/page.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
77
import { Skeleton } from '@/components/ui/skeleton';
88
import { CurrencyDisplay } from '@/components/shared/CurrencyDisplay';
99
import { StatusBadge } from '@/components/shared/StatusBadge';
10+
import { StatCard } from '@/components/shared/StatCard';
1011
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
1112
import {
1213
ArrowUpRight,
@@ -250,6 +251,34 @@ export default function DashboardPage() {
250251
</div>
251252
) : (
252253
<>
254+
<StatCard
255+
title="Total Volume (30d)"
256+
icon={Activity}
257+
color="amber"
258+
value={<CurrencyDisplay amount={45231.89} />}
259+
trend={{ icon: ArrowUpRight, label: "+20.1% from last month", color: "text-emerald-600" }}
260+
/>
261+
<StatCard
262+
title="Active Payment Links"
263+
icon={CreditCard}
264+
color="blue"
265+
value="12"
266+
trend={{ label: "+3 new links this week" }}
267+
/>
268+
<StatCard
269+
title="Available to Settle"
270+
icon={Wallet}
271+
color="emerald"
272+
value={<CurrencyDisplay amount={12450.00} />}
273+
trend={{ icon: ArrowDownRight, label: "Pending NGN conversion", color: "text-primary" }}
274+
/>
275+
<StatCard
276+
title="Current FX Rate"
277+
icon={RefreshCcw}
278+
color="purple"
279+
value="₦1,550"
280+
trend={{ label: "per USDC · Updated 5m ago" }}
281+
/>
253282
{/* Card 1 */}
254283
<Card className="relative overflow-hidden border border-border bg-card shadow-sm hover:shadow-md transition-shadow">
255284
<div className="absolute inset-0 bg-gradient-to-br from-amber-50/60 to-transparent pointer-events-none" />

app/(merchant)/settlement/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getStellarExplorerTxUrl } from '@/lib/utils/explorer';
2121
import { EmptyState } from '@/components/shared/EmptyState';
2222
import { useOfflineStore } from '@/lib/store/offlineStore';
2323
import { SettlementConfirmation } from '@/components/settlement/SettlementConfirmation';
24+
import { StatCard } from '@/components/shared/StatCard';
2425
import { memo } from 'react';
2526
import { mockSettlements, type Settlement } from '@/lib/mock/settlements';
2627

@@ -95,6 +96,22 @@ export default function SettlementPage() {
9596

9697
{/* Balance summary */}
9798
<div className="grid gap-3 grid-cols-2 sm:grid-cols-3">
99+
<StatCard
100+
title="Available to Settle"
101+
value={<CurrencyDisplay amount={12450.00} />}
102+
color="amber"
103+
trend={{ label: "≈ ₦19,297,500" }}
104+
/>
105+
<StatCard
106+
title="Pending Settlement"
107+
value={<CurrencyDisplay amount={8200.50} />}
108+
trend={{ icon: Clock, label: "Processing", color: "text-primary" }}
109+
/>
110+
<StatCard
111+
title="Total Settled (30d)"
112+
value={<CurrencyDisplay amount={38750.00} />}
113+
trend={{ icon: CheckCircle2, label: "All completed", color: "text-emerald-600" }}
114+
/>
98115
<Card className="border border-border border-t-2 border-t-amber-200 bg-card shadow-sm">
99116
<CardHeader className="pb-2 relative">
100117
<CardTitle className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Available to Settle</CardTitle>

components/shared/StatCard.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use client";
2+
3+
import type { LucideIcon } from 'lucide-react';
4+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5+
import { cn } from '@/lib/utils';
6+
7+
interface StatCardProps {
8+
title: string;
9+
value: React.ReactNode;
10+
icon?: LucideIcon;
11+
trend?: {
12+
label: string;
13+
icon?: LucideIcon;
14+
color?: string;
15+
};
16+
color?: 'amber' | 'blue' | 'emerald' | 'purple' | 'primary';
17+
variant?: 'default' | 'destructive';
18+
className?: string;
19+
}
20+
21+
const colorStyles = {
22+
amber: { gradient: 'from-amber-50/60', box: 'bg-primary/20', icon: 'text-primary' },
23+
blue: { gradient: 'from-blue-50/60', box: 'bg-blue-100', icon: 'text-blue-600' },
24+
emerald: { gradient: 'from-emerald-50/60', box: 'bg-emerald-100', icon: 'text-emerald-600' },
25+
purple: { gradient: 'from-purple-50/60', box: 'bg-purple-100', icon: 'text-purple-600' },
26+
primary: { gradient: 'from-primary/5', box: 'bg-primary/10', icon: 'text-primary' },
27+
} as const;
28+
29+
export function StatCard({ title, value, icon: Icon, trend, color, variant = 'default', className }: StatCardProps) {
30+
const isDestructive = variant === 'destructive';
31+
const cfg = color ? colorStyles[color] : null;
32+
33+
return (
34+
<Card className={cn(
35+
"relative overflow-hidden border shadow-sm hover:shadow-md transition-shadow",
36+
isDestructive ? "bg-destructive/10 border-destructive/20" : "bg-card border-border",
37+
className
38+
)}>
39+
{cfg && !isDestructive && (
40+
<div className={cn("absolute inset-0 bg-gradient-to-br pointer-events-none", cfg.gradient, "to-transparent")} />
41+
)}
42+
<CardHeader className="flex flex-row items-center justify-between pb-2 relative">
43+
<CardTitle className={cn(
44+
"font-semibold",
45+
isDestructive
46+
? "text-sm text-destructive"
47+
: "text-xs text-muted-foreground uppercase tracking-wider"
48+
)}>
49+
{title}
50+
</CardTitle>
51+
{Icon && (
52+
isDestructive ? (
53+
<Icon className="h-4 w-4 text-destructive" />
54+
) : (
55+
<div className={cn("w-8 h-8 rounded-lg flex items-center justify-center", cfg?.box)}>
56+
<Icon className={cn("h-4 w-4", cfg?.icon)} />
57+
</div>
58+
)
59+
)}
60+
</CardHeader>
61+
<CardContent className="p-3 sm:p-4 relative">
62+
<div className={cn(
63+
"text-xl sm:text-2xl font-bold",
64+
isDestructive ? "text-destructive" : "text-foreground"
65+
)}>
66+
{value}
67+
</div>
68+
{trend && (
69+
<p className={cn(
70+
"text-xs flex items-center mt-1.5 font-medium",
71+
trend.color || (isDestructive ? "text-destructive/80" : "text-muted-foreground")
72+
)}>
73+
{trend.icon && <trend.icon className="h-3 w-3 mr-1 shrink-0" />}
74+
{trend.label}
75+
</p>
76+
)}
77+
</CardContent>
78+
</Card>
79+
);
80+
}

fix.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
Description: The KPI stat card pattern (icon + title + value + trend) is repeated inline across 3 pages with ~11 instances total. A reusable StatCard component would reduce duplication and ensure consistent styling.
2+
3+
Requirements:
4+
5+
Create components/shared/StatCard.tsx
6+
Props: title, value (ReactNode), icon, trend?, trendLabel?, color? (amber/blue/emerald/purple), className?
7+
Handle the gradient overlay, icon background, and trend display internally
8+
Use the component in dashboard, settlement, and admin overview pages
9+
Suggested execution steps:
10+
11+
Create components/shared/StatCard.tsx
12+
Define the prop interface with all needed fields
13+
Implement the card with gradient overlay, icon, value, and optional trend
14+
Replace the 4 inline stat cards in app/(merchant)/dashboard/page.tsx
15+
Replace the 3 stat cards in app/(merchant)/settlement/page.tsx
16+
Replace the 4 stat cards in app/(admin)/overview/page.tsx
17+
Verify all look identical to the original
118
Description: Mock data arrays are defined inline at the top of page files: mockChartData in dashboard, mockTransactions in dashboard, mockPaymentLinks in dashboard, mockLinks in payments, mockTxHistory in wallet, mockSettlements in settlement, mockKeys in developers. This clutters page files and makes it hard to swap mock data for real API data.
219

320
Requirements:
@@ -16,4 +33,4 @@ Move mockSettlements from app/(merchant)/settlement/page.tsx
1633
Move mockKeys and codeExample from app/(merchant)/developers/page.tsx
1734
Move fxHistory and pairs from app/(merchant)/fx/page.tsx
1835
Move mockChartData from app/(admin)/overview/page.tsx
19-
Update all imports in page files
36+
Update all imports in page files

lib/mock/settlements.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export interface Settlement {
66
date: string;
77
bank: string;
88
accountNo: string;
9+
}
10+
11+
export const mockSettlements: Settlement[] = [
12+
{ id: 'stl_01', amount: 12450.00, amountNgn: 19297500, status: 'completed', date: '2024-01-10', bank: 'GTBank', accountNo: '012****567' },
13+
{ id: 'stl_02', amount: 8200.50, amountNgn: 12710775, status: 'pending', date: '2024-01-12', bank: 'First Bank', accountNo: '302****814' },
914
txHash?: string;
1015
}
1116

0 commit comments

Comments
 (0)