Skip to content

Commit fc0afaf

Browse files
authored
Merge pull request #142 from devdianax/main
2 parents d3c7ad4 + b7e9e0b commit fc0afaf

8 files changed

Lines changed: 213 additions & 21 deletions

File tree

app/(admin)/layout.tsx

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

33
import { useState } from 'react';
4-
import { AdminSidebar } from '@/components/layout/AdminSidebar';
4+
import { AdminSidebar, adminNavItems } from '@/components/layout/AdminSidebar';
5+
import { MobileNavDrawer } from '@/components/layout/MobileNavDrawer';
56
import { Topbar } from '@/components/layout/Topbar';
67
import Footer from '@/components/layout/Footer';
8+
import { ShieldCheck } from 'lucide-react';
79

810
export default function AdminLayout({
911
children,
@@ -15,7 +17,18 @@ export default function AdminLayout({
1517
return (
1618
<div className="flex h-screen overflow-hidden bg-background">
1719
<AdminSidebar />
18-
{/* Mobile nav drawer would go here */}
20+
<MobileNavDrawer
21+
isOpen={mobileMenuOpen}
22+
onClose={() => setMobileMenuOpen(false)}
23+
navItems={adminNavItems}
24+
brandLabel="BettaPay ADMIN"
25+
logo={
26+
<span className="font-bold text-xl tracking-tight text-sidebar-foreground flex items-center gap-2">
27+
<ShieldCheck className="w-5 h-5 text-primary" />
28+
BettaPay <span className="text-primary text-sm font-normal ml-0.5">ADMIN</span>
29+
</span>
30+
}
31+
/>
1932

2033
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
2134
<Topbar onMenuClick={() => setMobileMenuOpen(!mobileMenuOpen)} isMenuOpen={mobileMenuOpen} title="Platform Operations" />

app/(merchant)/dashboard/page.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { CurrencyDisplay } from '@/components/shared/CurrencyDisplay';
@@ -93,6 +93,15 @@ export default function DashboardPage() {
9393
const { user } = useAuthStore();
9494
const [activePeriod, setActivePeriod] = useState<Period>('7D');
9595
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
96+
const [isMobile, setIsMobile] = useState(false);
97+
98+
useEffect(() => {
99+
const mq = window.matchMedia('(max-width: 639px)');
100+
setIsMobile(mq.matches);
101+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
102+
mq.addEventListener('change', handler);
103+
return () => mq.removeEventListener('change', handler);
104+
}, []);
96105

97106
// Error simulation states
98107
const [simulationEnabled, setSimulationEnabled] = useState(false);
@@ -268,7 +277,7 @@ export default function DashboardPage() {
268277
key={p}
269278
onClick={() => setActivePeriod(p)}
270279
className={cn(
271-
'px-3 py-1 rounded-md text-xs font-semibold transition-all',
280+
'min-h-[44px] min-w-[44px] px-3 py-1 rounded-md text-xs font-semibold transition-all',
272281
activePeriod === p
273282
? 'bg-white text-slate-900 shadow-sm'
274283
: 'text-slate-400 hover:text-slate-600'
@@ -291,7 +300,7 @@ export default function DashboardPage() {
291300
) : (
292301
<div className="h-[260px] w-full">
293302
<ResponsiveContainer width="100%" height="100%">
294-
<AreaChart data={mockChartData} margin={{ top: 4, right: 4, bottom: 0, left: -16 }}>
303+
<AreaChart data={mockChartData} margin={{ top: 4, right: 4, bottom: 0, left: isMobile ? 0 : -16 }}>
295304
<defs>
296305
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
297306
<stop offset="5%" stopColor="#F0A500" stopOpacity={0.25} />
@@ -354,7 +363,7 @@ export default function DashboardPage() {
354363
<div className="flex items-center justify-between">
355364
<CardTitle className="text-base font-semibold text-slate-900">Recent Activity</CardTitle>
356365
<Link href="/transactions">
357-
<Button variant="ghost" className="text-xs text-amber-600 hover:text-amber-700 hover:bg-amber-50 h-7 px-2 rounded-lg font-semibold">
366+
<Button variant="ghost" className="text-xs text-amber-600 hover:text-amber-700 hover:bg-amber-50 min-h-[44px] px-2 rounded-lg font-semibold">
358367
View all <ChevronRight className="w-3 h-3 ml-0.5" />
359368
</Button>
360369
</Link>
@@ -443,7 +452,7 @@ export default function DashboardPage() {
443452
<div className="flex items-center justify-between">
444453
<CardTitle className="text-base font-semibold text-slate-900">Payment Link Performance</CardTitle>
445454
<Link href="/payments">
446-
<Button variant="ghost" className="text-xs text-amber-600 hover:text-amber-700 hover:bg-amber-50 h-7 px-2 rounded-lg font-semibold">
455+
<Button variant="ghost" className="text-xs text-amber-600 hover:text-amber-700 hover:bg-amber-50 min-h-[44px] px-2 rounded-lg font-semibold">
447456
Manage <ArrowRight className="w-3 h-3 ml-0.5" />
448457
</Button>
449458
</Link>
@@ -485,11 +494,11 @@ export default function DashboardPage() {
485494
<span className="text-xs text-slate-400">{link.clicks} clicks</span>
486495
</div>
487496
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
488-
<Button variant="ghost" size="icon" aria-label="Copy payment link" className="h-7 w-7 rounded-lg" onClick={() => handleCopy(`https://${link.url}`)}>
497+
<Button variant="ghost" size="icon" aria-label="Copy payment link" className="min-h-[44px] min-w-[44px] rounded-lg" onClick={() => handleCopy(`https://${link.url}`)}>
489498
<Copy className="w-3 h-3 text-slate-400" />
490499
</Button>
491500
<Link href={`https://${link.url}`} target="_blank">
492-
<Button variant="ghost" size="icon" aria-label="Open payment link" className="h-7 w-7 rounded-lg">
501+
<Button variant="ghost" size="icon" aria-label="Open payment link" className="min-h-[44px] min-w-[44px] rounded-lg">
493502
<ExternalLink className="w-3 h-3 text-slate-400" />
494503
</Button>
495504
</Link>

app/(merchant)/fx/page.tsx

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

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { RefreshCcw, TrendingUp, TrendingDown, Info, Bell, BellRing, Trash2, Plus, ArrowRight, X } from 'lucide-react';
@@ -15,7 +15,6 @@ import {
1515
SelectValue,
1616
} from "@/components/ui/select";
1717
import { Input } from '@/components/ui/input';
18-
import { useEffect } from 'react';
1918

2019
const fxHistory = [
2120
{ date: 'Jan 7', rate: 1480 },
@@ -57,7 +56,16 @@ interface RateAlert {
5756
export default function FxRatesPage() {
5857
const [lastRefresh] = useState('Just now');
5958
const [alerts, setAlerts] = useState<RateAlert[]>([]);
59+
const [isMobile, setIsMobile] = useState(false);
6060
const notify = useNotify();
61+
62+
useEffect(() => {
63+
const mq = window.matchMedia('(max-width: 639px)');
64+
setIsMobile(mq.matches);
65+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
66+
mq.addEventListener('change', handler);
67+
return () => mq.removeEventListener('change', handler);
68+
}, []);
6169
const [newPair, setNewPair] = useState('USDC/NGN');
6270
const [newCondition, setNewCondition] = useState<'above' | 'below'>('above');
6371
const [newTarget, setNewTarget] = useState('');
@@ -178,7 +186,7 @@ export default function FxRatesPage() {
178186
<CardContent>
179187
<div className="h-[240px]">
180188
<ResponsiveContainer width="100%" height="100%">
181-
<LineChart data={fxHistory} margin={{ top: 4, right: 4, bottom: 0, left: -10 }}>
189+
<LineChart data={fxHistory} margin={{ top: 4, right: 4, bottom: 0, left: isMobile ? 0 : -10 }}>
182190
<CartesianGrid strokeDasharray="3 3" stroke="#F1F5F9" vertical={false} />
183191
<XAxis dataKey="date" fontSize={11} tickLine={false} axisLine={false} tick={{ fill: '#94A3B8' }} />
184192
<YAxis fontSize={11} tickLine={false} axisLine={false} tickFormatter={(v) => `₦${v}`} tick={{ fill: '#94A3B8' }} domain={['auto', 'auto']} />
@@ -320,7 +328,7 @@ export default function FxRatesPage() {
320328
size="icon"
321329
aria-label={alert.enabled ? "Disable alert" : "Enable alert"}
322330
className={cn(
323-
"h-8 w-8 rounded-lg",
331+
"min-h-[44px] min-w-[44px] rounded-lg",
324332
alert.enabled ? "text-emerald-500 hover:text-emerald-600 hover:bg-emerald-50" : "text-slate-400 hover:text-slate-600 hover:bg-slate-200"
325333
)}
326334
onClick={() => toggleAlert(alert.id)}
@@ -331,7 +339,7 @@ export default function FxRatesPage() {
331339
variant="ghost"
332340
size="icon"
333341
aria-label="Delete alert"
334-
className="h-8 w-8 rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50"
342+
className="min-h-[44px] min-w-[44px] rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50"
335343
onClick={() => deleteAlert(alert.id)}
336344
>
337345
<Trash2 className="w-3.5 h-3.5" />

app/(merchant)/transactions/page.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default function TransactionsPage() {
7979

8080
<Card className="bg-brand-surface border-border/50 shadow-sm">
8181
<CardContent className="pt-4">
82-
<div className="rounded-md border border-border/50 overflow-hidden">
82+
<div className="rounded-md border border-border/50 overflow-x-auto hidden md:block">
8383
<Table>
8484
<TableHeader className="bg-muted/50">
8585
<TableRow className="border-border/50 hover:bg-transparent">
@@ -146,6 +146,60 @@ export default function TransactionsPage() {
146146
</TableBody>
147147
</Table>
148148
</div>
149+
150+
<div className="md:hidden space-y-3">
151+
{filteredTransactions.length === 0 ? (
152+
<EmptyState
153+
icon={SearchX}
154+
title={searchTerm ? 'No transactions match your search' : 'No transactions found'}
155+
description={
156+
searchTerm
157+
? 'Try adjusting your search terms or clearing filters.'
158+
: 'Transactions will appear here once payments are received.'
159+
}
160+
action={
161+
searchTerm
162+
? { label: 'Clear search', onClick: () => setSearchTerm('') }
163+
: undefined
164+
}
165+
/>
166+
) : (
167+
filteredTransactions.map((tx) => (
168+
<div
169+
key={tx.id}
170+
className="border border-border/50 rounded-lg p-4 space-y-3 cursor-pointer hover:bg-muted/30 transition-colors"
171+
onClick={() => setSelectedTx(tx)}
172+
>
173+
<div className="flex items-center justify-between">
174+
<span className="text-sm text-muted-foreground">{formatDate(tx.timestamp)}</span>
175+
<StatusBadge status={tx.status} />
176+
</div>
177+
<div className="space-y-2">
178+
<div className="flex items-center justify-between">
179+
<span className="text-xs text-muted-foreground">Payer</span>
180+
<CopyAddress address={tx.payerAddress} />
181+
</div>
182+
<div className="flex items-center justify-between">
183+
<span className="text-xs text-muted-foreground">Tx Hash</span>
184+
<CopyAddress address={tx.txHash} />
185+
</div>
186+
<div className="flex items-center justify-between">
187+
<span className="text-xs text-muted-foreground">Source</span>
188+
<span className="text-sm text-muted-foreground">{tx.source}</span>
189+
</div>
190+
<div className="flex items-center justify-between">
191+
<span className="text-xs text-muted-foreground">Amount (USDC)</span>
192+
<CurrencyDisplay amount={tx.amountUsdc} currency="USDC" />
193+
</div>
194+
<div className="flex items-center justify-between">
195+
<span className="text-xs text-muted-foreground">Amount (NGN)</span>
196+
<CurrencyDisplay amount={tx.amountNgn} currency="NGN" showDecimals={false} />
197+
</div>
198+
</div>
199+
</div>
200+
))
201+
)}
202+
</div>
149203
</CardContent>
150204
</Card>
151205

components/layout/AdminSidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
ShieldCheck
1515
} from 'lucide-react';
1616

17-
const navItems = [
17+
export const adminNavItems = [
1818
{ href: '/overview', label: 'Platform Overview', icon: BarChart3 },
1919
{ href: '/merchants', label: 'Merchants', icon: Users },
2020
{ href: '/admin/transactions', label: 'Transactions', icon: ListOrdered },
@@ -39,7 +39,7 @@ export const AdminSidebar = () => {
3939
</div>
4040

4141
<nav className="flex-1 px-4 py-4 space-y-1">
42-
{navItems.map((item) => {
42+
{adminNavItems.map((item) => {
4343
const isActive = pathname.startsWith(item.href);
4444
const Icon = item.icon;
4545

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client";
2+
3+
import { useEffect } from 'react';
4+
import Link from 'next/link';
5+
import { usePathname } from 'next/navigation';
6+
import { cn } from '@/lib/utils';
7+
import { X } from 'lucide-react';
8+
import { Button } from '@/components/ui/button';
9+
10+
interface NavItem {
11+
href: string;
12+
label: string;
13+
icon: React.ComponentType<{ className?: string }>;
14+
}
15+
16+
interface MobileNavDrawerProps {
17+
isOpen: boolean;
18+
onClose: () => void;
19+
navItems: NavItem[];
20+
brandLabel?: string;
21+
logo?: React.ReactNode;
22+
}
23+
24+
export const MobileNavDrawer = ({
25+
isOpen,
26+
onClose,
27+
navItems,
28+
brandLabel = 'BettaPay',
29+
logo,
30+
}: MobileNavDrawerProps) => {
31+
const pathname = usePathname();
32+
33+
useEffect(() => {
34+
if (isOpen) {
35+
document.body.style.overflow = 'hidden';
36+
} else {
37+
document.body.style.overflow = '';
38+
}
39+
return () => {
40+
document.body.style.overflow = '';
41+
};
42+
}, [isOpen]);
43+
44+
return (
45+
<>
46+
<div
47+
className={cn(
48+
'fixed inset-0 z-40 bg-black/50 transition-opacity duration-300 md:hidden',
49+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
50+
)}
51+
onClick={onClose}
52+
aria-hidden="true"
53+
/>
54+
<div
55+
className={cn(
56+
'fixed inset-y-0 left-0 z-50 w-64 bg-sidebar border-r border-sidebar-border shadow-xl transform transition-transform duration-300 ease-in-out md:hidden',
57+
isOpen ? 'translate-x-0' : '-translate-x-full'
58+
)}
59+
role="dialog"
60+
aria-modal="true"
61+
aria-label="Mobile navigation"
62+
>
63+
<div className="flex items-center justify-between p-6">
64+
{logo ? (
65+
logo
66+
) : (
67+
<span className="font-bold text-xl tracking-tight text-sidebar-foreground">
68+
{brandLabel}
69+
</span>
70+
)}
71+
<Button
72+
variant="ghost"
73+
size="icon"
74+
onClick={onClose}
75+
className="text-sidebar-foreground/60 hover:text-sidebar-foreground min-h-[44px] min-w-[44px]"
76+
aria-label="Close navigation menu"
77+
>
78+
<X className="h-5 w-5" />
79+
</Button>
80+
</div>
81+
82+
<nav className="flex-1 px-4 py-4 space-y-1">
83+
{navItems.map((item) => {
84+
const isActive = pathname.startsWith(item.href);
85+
const Icon = item.icon;
86+
87+
return (
88+
<Link
89+
key={item.href}
90+
href={item.href}
91+
onClick={onClose}
92+
className={cn(
93+
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors border-l-2 min-h-[44px]",
94+
isActive
95+
? "border-primary bg-sidebar-accent/30 text-sidebar-foreground"
96+
: "border-transparent text-muted-foreground hover:bg-sidebar-accent/20 hover:text-sidebar-foreground"
97+
)}
98+
>
99+
<Icon className="w-5 h-5" />
100+
{item.label}
101+
</Link>
102+
);
103+
})}
104+
</nav>
105+
</div>
106+
</>
107+
);
108+
};

components/layout/Topbar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const Topbar = ({ onMenuClick, isMenuOpen, title }: TopbarProps) => {
4949
<Button
5050
variant="ghost"
5151
size="icon"
52-
className="md:hidden text-slate-400 hover:text-slate-700"
52+
className="md:hidden text-slate-400 hover:text-slate-700 min-h-[44px] min-w-[44px]"
5353
onClick={onMenuClick}
5454
aria-expanded={isMenuOpen}
5555
aria-label="Toggle mobile menu"
@@ -72,7 +72,7 @@ export const Topbar = ({ onMenuClick, isMenuOpen, title }: TopbarProps) => {
7272
</div>
7373

7474
{/* Notifications */}
75-
<Button variant="ghost" size="icon" aria-label="View notifications" className="relative text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-xl h-9 w-9">
75+
<Button variant="ghost" size="icon" aria-label="View notifications" className="relative text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded-xl min-h-[44px] min-w-[44px]">
7676
<Bell className="h-4.5 w-4.5" />
7777
<span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-red-500 border-2 border-white"></span>
7878
</Button>
@@ -82,7 +82,7 @@ export const Topbar = ({ onMenuClick, isMenuOpen, title }: TopbarProps) => {
8282
<DropdownMenuTrigger render={
8383
<Button
8484
variant="ghost"
85-
className="relative h-9 w-9 rounded-xl p-0 hover:bg-slate-100"
85+
className="relative min-h-[44px] min-w-[44px] rounded-xl p-0 hover:bg-slate-100"
8686
aria-expanded={isDropdownOpen}
8787
aria-label="User menu"
8888
>

components/shared/CopyAddress.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const CopyAddress = ({
4444
<Button
4545
variant="ghost"
4646
size="icon"
47-
className={cn('h-8 w-8 text-muted-foreground hover:text-foreground', className)}
47+
className={cn('min-h-[44px] min-w-[44px] text-muted-foreground hover:text-foreground', className)}
4848
onClick={handleCopy}
4949
title="Copy address"
5050
aria-label={`Copy address ${displayAddress}`}

0 commit comments

Comments
 (0)