Skip to content

Commit d0f6d62

Browse files
authored
Merge branch 'main' into feat/csrf-token-generation
2 parents 7dd979d + 2d2d2af commit d0f6d62

28 files changed

Lines changed: 1018 additions & 396 deletions

app/(admin)/layout.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,56 @@
11
"use client";
22

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
4+
import { useRouter } from 'next/navigation';
45
import { AdminSidebar } from '@/components/layout';
56
import { adminNavItems } from '@/lib/navigation/adminNav';
67
import { PageTransition } from '@/components/shared';
78
import { MobileNavDrawer } from '@/components/layout';
89
import { Topbar } from '@/components/layout';
910
import Footer from '@/components/layout';
1011
import Image from 'next/image';
12+
import { useAuthStore } from '@/lib/store/authStore';
1113

1214
export default function AdminLayout({
1315
children,
1416
}: {
1517
children: React.ReactNode;
1618
}) {
1719
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
20+
const router = useRouter();
21+
const role = useAuthStore((s) => s.role);
22+
23+
// The persisted role only exists on the client, so the guard is deferred to
24+
// an effect. `mounted` keeps SSR and the first client render identical while
25+
// zustand rehydrates from storage — without it the layout would flash for
26+
// merchants who type an /admin URL directly, before middleware redirects.
27+
const [mounted, setMounted] = useState(false);
28+
useEffect(() => {
29+
setMounted(true);
30+
}, []);
31+
32+
const isMerchant = mounted && role === 'merchant';
33+
34+
useEffect(() => {
35+
if (isMerchant) {
36+
router.replace('/dashboard');
37+
}
38+
}, [isMerchant, router]);
39+
40+
if (!mounted || isMerchant) {
41+
return (
42+
<div
43+
role="status"
44+
aria-live="polite"
45+
className="flex h-screen items-center justify-center bg-background"
46+
>
47+
<div className="flex flex-col items-center gap-3">
48+
<div className="w-8 h-8 rounded-full border-2 border-primary/30 border-t-primary animate-spin" />
49+
<p className="text-sm text-muted-foreground">Redirecting&hellip;</p>
50+
</div>
51+
</div>
52+
);
53+
}
1854

1955
return (
2056
<div className="flex h-screen overflow-hidden bg-background">

app/(admin)/overview/page.tsx

Lines changed: 68 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,12 @@
22

33
import { memo } from 'react';
44
import dynamic from 'next/dynamic';
5-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
6-
import { CurrencyDisplay } from '@/components/shared';
5+
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from '@/components/ui';
6+
import { CurrencyDisplay, ErrorDisplay, StatCard } from '@/components/shared';
77
import { Users, AlertTriangle, ArrowUpRight, Activity, DollarSign } from 'lucide-react';
8-
import dynamic from 'next/dynamic';
9-
10-
const PlatformVolumeChart = dynamic(() => import('@/components/charts/PlatformVolumeChart'), {
11-
ssr: false,
12-
loading: () => <div className="h-[300px] bg-slate-50 animate-pulse rounded-xl w-full" />
13-
});
8+
import { useAdminStats } from '@/lib/api/hooks';
149

1510

16-
import { Skeleton } from '@/components/ui';
17-
import { StatCard } from '@/components/shared';
18-
19-
const PlatformVolumeChart = dynamic(() => import('@/components/charts/PlatformVolumeChart'), {
20-
ssr: false,
21-
loading: () => <Skeleton className="h-[300px] w-full rounded-xl" />,
22-
});
2311

2412
// Memoised so future additions of state to the parent won't re-render the chart.
2513
const AdminChartSection = memo(function AdminChartSection() {
@@ -37,7 +25,24 @@ const AdminChartSection = memo(function AdminChartSection() {
3725
);
3826
});
3927

28+
function StatCardSkeleton() {
29+
return (
30+
<Card className="bg-card border shadow-sm">
31+
<CardHeader className="flex flex-row items-center justify-between pb-2">
32+
<Skeleton className="h-3 w-24" />
33+
<Skeleton className="h-8 w-8 rounded-lg" />
34+
</CardHeader>
35+
<CardContent className="p-3 sm:p-4 space-y-2">
36+
<Skeleton className="h-7 w-32" />
37+
<Skeleton className="h-3 w-20" />
38+
</CardContent>
39+
</Card>
40+
);
41+
}
42+
4043
export default function AdminOverviewPage() {
44+
const { data: stats, isLoading, error, refetch } = useAdminStats();
45+
4146
return (
4247
<div className="space-y-6">
4348
<div>
@@ -47,104 +52,59 @@ export default function AdminOverviewPage() {
4752
</p>
4853
</div>
4954

50-
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
51-
<StatCard
52-
title="Total Processed (30d)"
53-
icon={Activity}
54-
value={<CurrencyDisplay amount={1452310.89} />}
55-
trend={{ icon: ArrowUpRight, label: "+12.5% from last month", color: "text-green-500" }}
56-
/>
57-
<StatCard
58-
title="Platform Fees Generated"
59-
icon={DollarSign}
60-
value={<CurrencyDisplay amount={14523.10} />}
61-
trend={{ label: "1.0% flat fee across volume" }}
55+
{error && (
56+
<ErrorDisplay
57+
message={`${error} Showing the last known figures.`}
58+
onRetry={refetch}
6259
/>
63-
<StatCard
64-
title="Active Merchants"
65-
icon={Users}
66-
value="142"
67-
trend={{ icon: ArrowUpRight, label: "+12 new this week", color: "text-green-500" }}
68-
/>
69-
<StatCard
70-
title="Pending KYB Reviews"
71-
icon={AlertTriangle}
72-
value="8"
73-
variant="destructive"
74-
trend={{ label: "Requires immediate action" }}
75-
/>
76-
<Card className="bg-card border shadow-sm relative overflow-hidden">
77-
<div className="absolute top-0 right-0 w-24 h-24 bg-primary/5 rounded-full -mr-10 -mt-10 blur-xl"></div>
78-
<CardHeader className="flex flex-row items-center justify-between pb-2 z-10 relative">
79-
<CardTitle className="text-sm font-medium text-muted-foreground">Total Processed (30d)</CardTitle>
80-
<Activity className="h-4 w-4 text-primary" />
81-
</CardHeader>
82-
<CardContent className="p-3 sm:p-4 z-10 relative">
83-
<div className="text-xl sm:text-2xl font-bold text-foreground">
84-
<CurrencyDisplay amount={1452310.89} />
85-
</div>
86-
<p className="text-xs text-success flex items-center mt-1">
87-
<ArrowUpRight className="h-3 w-3 mr-1" />
88-
+12.5% from last month
89-
</p>
90-
</CardContent>
91-
</Card>
92-
93-
<Card className="bg-card border shadow-sm">
94-
<CardHeader className="flex flex-row items-center justify-between pb-2">
95-
<CardTitle className="text-sm font-medium text-muted-foreground">Platform Fees Generated</CardTitle>
96-
<DollarSign className="h-4 w-4 text-success" />
97-
</CardHeader>
98-
<CardContent className="p-3 sm:p-4">
99-
<div className="text-xl sm:text-2xl font-bold text-foreground">
100-
<CurrencyDisplay amount={14523.10} />
101-
</div>
102-
<p className="text-xs text-muted-foreground mt-1">
103-
1.0% flat fee across volume
104-
</p>
105-
</CardContent>
106-
</Card>
107-
108-
<Card className="bg-card border shadow-sm">
109-
<CardHeader className="flex flex-row items-center justify-between pb-2">
110-
<CardTitle className="text-sm font-medium text-muted-foreground">Active Merchants</CardTitle>
111-
<Users className="h-4 w-4 text-primary" />
112-
</CardHeader>
113-
<CardContent className="p-3 sm:p-4">
114-
<div className="text-xl sm:text-2xl font-bold text-foreground">142</div>
115-
<p className="text-xs text-success flex items-center mt-1">
116-
<ArrowUpRight className="h-3 w-3 mr-1" />
117-
+12 new this week
118-
</p>
119-
</CardContent>
120-
</Card>
60+
)}
12161

122-
<Card className="col-span-1 bg-destructive/10 border-destructive/20 shadow-sm">
123-
<CardHeader className="flex flex-row items-center justify-between pb-2">
124-
<CardTitle className="text-sm font-medium text-destructive">Pending KYB Reviews</CardTitle>
125-
<AlertTriangle className="h-4 w-4 text-destructive" />
126-
</CardHeader>
127-
<CardContent className="p-3 sm:p-4">
128-
<div className="text-xl sm:text-2xl font-bold text-destructive">8</div>
129-
<p className="text-xs text-destructive/80 mt-1">
130-
Requires immediate action
131-
</p>
132-
</CardContent>
133-
</Card>
62+
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
63+
{isLoading ? (
64+
Array.from({ length: 4 }, (_, i) => <StatCardSkeleton key={i} />)
65+
) : (
66+
<>
67+
<StatCard
68+
title="Total Processed (30d)"
69+
icon={Activity}
70+
color="primary"
71+
value={<CurrencyDisplay amount={stats.totalProcessed} />}
72+
trend={{
73+
icon: ArrowUpRight,
74+
label: `+${stats.totalProcessedChangePct}% from last month`,
75+
color: 'text-success',
76+
}}
77+
/>
78+
<StatCard
79+
title="Platform Fees Generated"
80+
icon={DollarSign}
81+
color="emerald"
82+
value={<CurrencyDisplay amount={stats.platformFees} />}
83+
trend={{ label: `${stats.feeRatePct.toFixed(1)}% flat fee across volume` }}
84+
/>
85+
<StatCard
86+
title="Active Merchants"
87+
icon={Users}
88+
color="blue"
89+
value={stats.activeMerchants.toLocaleString()}
90+
trend={{
91+
icon: ArrowUpRight,
92+
label: `+${stats.newMerchantsThisWeek} new this week`,
93+
color: 'text-success',
94+
}}
95+
/>
96+
<StatCard
97+
title="Pending KYB Reviews"
98+
icon={AlertTriangle}
99+
value={stats.pendingKyb.toLocaleString()}
100+
variant="destructive"
101+
trend={{ label: 'Requires immediate action' }}
102+
/>
103+
</>
104+
)}
134105
</div>
135106

136107
<div className="grid gap-4 md:grid-cols-7">
137-
<Card className="col-span-4 bg-card border shadow-sm">
138-
<CardHeader>
139-
<CardTitle>Platform Volume vs Fees</CardTitle>
140-
</CardHeader>
141-
<CardContent className="pl-2">
142-
<div className="mt-4">
143-
<PlatformVolumeChart height={300} />
144-
</div>
145-
</CardContent>
146-
</Card>
147-
{/* Chart section is memoised */}
148108
<AdminChartSection />
149109

150110
<Card className="col-span-3 bg-card border shadow-sm">

app/(merchant)/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
55
import { AlertTriangle } from "lucide-react";
66
import { MerchantSidebar } from "@/components/layout";
77
import { merchantNavItems } from "@/lib/navigation/merchantNav";
8-
import { PageTransition } from "@/components/shared";
8+
import { PageTransition, ErrorBoundary } from "@/components/shared";
99
import { MobileNavDrawer } from "@/components/layout";
1010
import { Topbar } from "@/components/layout";
1111
import Footer from "@/components/layout";
@@ -84,7 +84,9 @@ export default function MerchantLayout({
8484
<main id="main-content" tabIndex={-1} className="flex-1 overflow-y-auto bg-background/50 pb-20 md:pb-0">
8585
<div className="mx-auto max-w-7xl px-3 sm:px-6 py-4 sm:py-8 space-y-6">
8686
<OnboardingWizard />
87-
<PageTransition>{children}</PageTransition>
87+
<PageTransition>
88+
<ErrorBoundary>{children}</ErrorBoundary>
89+
</PageTransition>
8890
</div>
8991
</main>
9092

0 commit comments

Comments
 (0)