Skip to content

Commit 2d4f14d

Browse files
authored
Merge pull request #134 from techAlhaji/feat/loading-skeletons
2 parents 83063c7 + 669eada commit 2d4f14d

13 files changed

Lines changed: 441 additions & 46 deletions

File tree

app/(admin)/loading.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from "react";
2+
import { Skeleton } from "@/components/ui/skeleton";
3+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
4+
5+
export default function AdminLoading() {
6+
return (
7+
<div className="space-y-6">
8+
{/* Title + subtitle skeleton */}
9+
<div className="space-y-2">
10+
<Skeleton className="h-9 w-48 bg-slate-200" />
11+
<Skeleton className="h-4 w-96 bg-slate-200 max-w-full" />
12+
</div>
13+
14+
{/* KPI Cards Skeleton */}
15+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
16+
{Array.from({ length: 4 }).map((_, i) => (
17+
<Card key={i} className="bg-card border shadow-sm">
18+
<CardHeader className="flex flex-row items-center justify-between pb-2">
19+
<Skeleton className="h-3 w-28 bg-slate-200" />
20+
<Skeleton className="h-4 w-4 bg-slate-200" />
21+
</CardHeader>
22+
<CardContent className="space-y-2">
23+
<Skeleton className="h-8 w-32 bg-slate-200" />
24+
<Skeleton className="h-3.5 w-24 bg-slate-200" />
25+
</CardContent>
26+
</Card>
27+
))}
28+
</div>
29+
30+
{/* Chart + System Health Grid */}
31+
<div className="grid gap-4 md:grid-cols-7">
32+
{/* Platform Volume Chart Skeleton */}
33+
<Card className="col-span-4 bg-card border shadow-sm">
34+
<CardHeader>
35+
<Skeleton className="h-4.5 w-40 bg-slate-200" />
36+
</CardHeader>
37+
<CardContent className="space-y-4">
38+
<Skeleton className="h-[300px] w-full bg-slate-100/70 mt-4" />
39+
</CardContent>
40+
</Card>
41+
42+
{/* System Health Status List Skeleton */}
43+
<Card className="col-span-3 bg-card border shadow-sm">
44+
<CardHeader>
45+
<Skeleton className="h-4.5 w-28 bg-slate-200" />
46+
</CardHeader>
47+
<CardContent>
48+
<div className="space-y-6">
49+
{Array.from({ length: 4 }).map((_, i) => (
50+
<div key={i} className="flex items-center justify-between">
51+
<div className="flex items-center gap-3">
52+
{/* Status dot indicator */}
53+
<Skeleton className="w-2.5 h-2.5 rounded-full bg-slate-200" />
54+
<div className="space-y-1.5">
55+
<Skeleton className="h-3.5 w-28 bg-slate-200" />
56+
<Skeleton className="h-3 w-16 bg-slate-200" />
57+
</div>
58+
</div>
59+
<Skeleton className="h-3 w-12 bg-slate-200" />
60+
</div>
61+
))}
62+
</div>
63+
</CardContent>
64+
</Card>
65+
</div>
66+
</div>
67+
);
68+
}

app/(merchant)/loading.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React from "react";
2+
import { Skeleton } from "@/components/ui/skeleton";
3+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
4+
5+
export default function MerchantLoading() {
6+
return (
7+
<div className="space-y-8 pb-8">
8+
{/* Welcome Header Skeleton */}
9+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
10+
<div className="space-y-2">
11+
{/* Tagline */}
12+
<Skeleton className="h-3 w-28 bg-slate-200" />
13+
{/* Title */}
14+
<Skeleton className="h-9 w-64 bg-slate-200" />
15+
{/* Subtitle */}
16+
<Skeleton className="h-4 w-96 bg-slate-200 max-w-full" />
17+
</div>
18+
<div>
19+
{/* Header Action Button */}
20+
<Skeleton className="h-10 w-44 rounded-xl bg-slate-200" />
21+
</div>
22+
</div>
23+
24+
{/* KPI Stat Cards Skeleton */}
25+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
26+
{Array.from({ length: 4 }).map((_, i) => (
27+
<Card key={i} className="border border-slate-200 bg-white shadow-sm">
28+
<CardHeader className="flex flex-row items-center justify-between pb-2">
29+
<Skeleton className="h-3 w-24 bg-slate-200" />
30+
<Skeleton className="h-8 w-8 rounded-lg bg-slate-200" />
31+
</CardHeader>
32+
<CardContent className="space-y-2">
33+
<Skeleton className="h-8 w-32 bg-slate-200" />
34+
<Skeleton className="h-3.5 w-28 bg-slate-200" />
35+
</CardContent>
36+
</Card>
37+
))}
38+
</div>
39+
40+
{/* Charts + Recent Transactions Skeleton */}
41+
<div className="grid gap-6 lg:grid-cols-7">
42+
{/* Revenue Chart Card */}
43+
<Card className="lg:col-span-4 border border-slate-200 bg-white shadow-sm">
44+
<CardHeader className="space-y-1">
45+
<div className="flex items-center justify-between">
46+
<div className="space-y-2">
47+
<Skeleton className="h-4 w-36 bg-slate-200" />
48+
<Skeleton className="h-3 w-56 bg-slate-200" />
49+
</div>
50+
<Skeleton className="h-7 w-28 bg-slate-200" />
51+
</div>
52+
</CardHeader>
53+
<CardContent className="space-y-6">
54+
{/* Chart Area Placeholder */}
55+
<Skeleton className="h-[260px] w-full bg-slate-100/70" />
56+
<div className="flex items-center gap-6 pt-4 border-t border-slate-100">
57+
<div className="space-y-1.5">
58+
<Skeleton className="h-3 w-16 bg-slate-200" />
59+
<Skeleton className="h-4 w-28 bg-slate-200" />
60+
</div>
61+
<div className="space-y-1.5">
62+
<Skeleton className="h-3 w-16 bg-slate-200" />
63+
<Skeleton className="h-4 w-20 bg-slate-200" />
64+
</div>
65+
</div>
66+
</CardContent>
67+
</Card>
68+
69+
{/* Recent Transactions Card */}
70+
<Card className="lg:col-span-3 border border-slate-200 bg-white shadow-sm">
71+
<CardHeader>
72+
<div className="flex items-center justify-between">
73+
<Skeleton className="h-4 w-28 bg-slate-200" />
74+
<Skeleton className="h-6 w-16 bg-slate-200" />
75+
</div>
76+
</CardHeader>
77+
<CardContent>
78+
<div className="space-y-4">
79+
{Array.from({ length: 5 }).map((_, i) => (
80+
<div key={i} className="flex items-center gap-3">
81+
<Skeleton className="w-9 h-9 rounded-xl bg-slate-200 flex-shrink-0" />
82+
<div className="flex-1 space-y-2">
83+
<Skeleton className="h-3.5 w-full max-w-[140px] bg-slate-200" />
84+
<Skeleton className="h-3 w-full max-w-[100px] bg-slate-200" />
85+
</div>
86+
<div className="flex flex-col items-end gap-1.5 flex-shrink-0">
87+
<Skeleton className="h-3.5 w-12 bg-slate-200" />
88+
<Skeleton className="h-4.5 w-16 rounded-full bg-slate-200" />
89+
</div>
90+
</div>
91+
))}
92+
</div>
93+
</CardContent>
94+
</Card>
95+
</div>
96+
97+
{/* Quick Actions + Payment Link Performance Skeleton */}
98+
<div className="grid gap-6 lg:grid-cols-7">
99+
{/* Quick Actions Card */}
100+
<Card className="lg:col-span-3 border border-slate-200 bg-white shadow-sm">
101+
<CardHeader>
102+
<Skeleton className="h-4 w-24 bg-slate-200" />
103+
</CardHeader>
104+
<CardContent className="grid grid-cols-2 gap-3">
105+
{Array.from({ length: 4 }).map((_, i) => (
106+
<div key={i} className="flex flex-col gap-3 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
107+
<Skeleton className="w-5 h-5 rounded-md bg-slate-200" />
108+
<Skeleton className="h-3 w-20 bg-slate-200" />
109+
</div>
110+
))}
111+
</CardContent>
112+
</Card>
113+
114+
{/* Payment Link Performance Card */}
115+
<Card className="lg:col-span-4 border border-slate-200 bg-white shadow-sm">
116+
<CardHeader>
117+
<div className="flex items-center justify-between">
118+
<Skeleton className="h-4 w-44 bg-slate-200" />
119+
<Skeleton className="h-6 w-16 bg-slate-200" />
120+
</div>
121+
</CardHeader>
122+
<CardContent>
123+
<div className="space-y-4">
124+
{Array.from({ length: 3 }).map((_, i) => (
125+
<div key={i} className="flex items-center gap-4 p-3 rounded-xl border border-slate-100">
126+
<Skeleton className="w-10 h-10 rounded-xl bg-slate-200 flex-shrink-0" />
127+
<div className="flex-1 space-y-2">
128+
<Skeleton className="h-3.5 w-28 bg-slate-200" />
129+
<Skeleton className="h-3 w-36 bg-slate-200" />
130+
<div className="flex items-center gap-2 mt-1">
131+
<Skeleton className="flex-1 h-1.5 rounded-full bg-slate-200" />
132+
<Skeleton className="h-3 w-6 bg-slate-200" />
133+
</div>
134+
</div>
135+
<div className="flex flex-col items-end gap-1.5 flex-shrink-0">
136+
<Skeleton className="h-3.5 w-8 bg-slate-200" />
137+
<Skeleton className="h-3 w-14 bg-slate-200" />
138+
</div>
139+
</div>
140+
))}
141+
</div>
142+
</CardContent>
143+
</Card>
144+
</div>
145+
</div>
146+
);
147+
}

app/(merchant)/transactions/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import { EmptyState } from '@/components/shared/EmptyState';
1212
import { mockTransactions } from '@/lib/mock/transactions';
1313
import { formatDate } from '@/lib/utils/format';
1414
import { Search, Download, Filter, SearchX } from 'lucide-react';
15+
import { TransactionDetail } from '@/components/transactions/TransactionDetail';
16+
import { Transaction } from '@/lib/mock/transactions';
1517

1618
export default function TransactionsPage() {
1719
const [searchTerm, setSearchTerm] = useState('');
1820
const [filterCount] = useState(0);
21+
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
1922

2023
const filteredTransactions = mockTransactions.filter(tx =>
2124
tx.txHash.toLowerCase().includes(searchTerm.toLowerCase()) ||

app/auth/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ShieldCheck, CheckCircle2 } from 'lucide-react';
1+
import { CheckCircle2 } from 'lucide-react';
22

33
const highlights = [
44
'Non-custodial — you always control your funds',

app/auth/loading.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
import { Skeleton } from "@/components/ui/skeleton";
3+
4+
export default function AuthLoading() {
5+
return (
6+
<div className="space-y-6">
7+
{/* Form Header Skeleton */}
8+
<div className="space-y-2 text-center">
9+
<Skeleton className="h-7 w-48 bg-slate-200 mx-auto" />
10+
<Skeleton className="h-4 w-72 bg-slate-200 mx-auto max-w-full" />
11+
</div>
12+
13+
{/* Input Fields Skeleton */}
14+
<div className="space-y-4">
15+
{/* Field 1 */}
16+
<div className="space-y-2">
17+
<Skeleton className="h-3.5 w-12 bg-slate-200" />
18+
<Skeleton className="h-10 w-full bg-slate-200 rounded-xl" />
19+
</div>
20+
21+
{/* Field 2 */}
22+
<div className="space-y-2">
23+
<div className="flex justify-between">
24+
<Skeleton className="h-3.5 w-16 bg-slate-200" />
25+
<Skeleton className="h-3.5 w-24 bg-slate-200" />
26+
</div>
27+
<Skeleton className="h-10 w-full bg-slate-200 rounded-xl" />
28+
</div>
29+
30+
{/* Action Button */}
31+
<Skeleton className="h-10 w-full bg-slate-200 rounded-xl mt-2" />
32+
</div>
33+
34+
{/* Divider */}
35+
<div className="relative flex items-center justify-center py-2">
36+
<div className="absolute inset-0 flex items-center">
37+
<div className="w-full border-t border-slate-100" />
38+
</div>
39+
<span className="relative bg-white px-2">
40+
<Skeleton className="h-3 w-8 bg-slate-200" />
41+
</span>
42+
</div>
43+
44+
{/* Secondary Button (Wallet Connect) */}
45+
<Skeleton className="h-10 w-full bg-slate-200 rounded-xl" />
46+
47+
{/* Footer Text Link */}
48+
<Skeleton className="h-4.5 w-44 bg-slate-200 mx-auto" />
49+
</div>
50+
);
51+
}

app/auth/login/page.tsx

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

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, Suspense } from 'react';
44
import { useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { useForm } from 'react-hook-form';
@@ -17,6 +17,8 @@ import { Button } from '@/components/ui/button';
1717
import { Input } from '@/components/ui/input';
1818
import { Label } from '@/components/ui/label';
1919

20+
import { WalletModalFallback } from '@/components/wallet/WalletModalFallback';
21+
2022
const WalletModal = dynamic(() => import('@/components/wallet/WalletModal').then(m => m.WalletModal), { ssr: false });
2123

2224
export default function LoginPage() {
@@ -137,7 +139,9 @@ export default function LoginPage() {
137139

138140
return (
139141
<div className="w-full animate-in fade-in slide-in-from-bottom-4 duration-500">
140-
<WalletModal open={walletModalOpen} onOpenChange={setWalletModalOpen} onConnected={onWalletConnected} />
142+
<Suspense fallback={<WalletModalFallback open={walletModalOpen} onOpenChange={setWalletModalOpen} />}>
143+
<WalletModal open={walletModalOpen} onOpenChange={setWalletModalOpen} onConnected={onWalletConnected} />
144+
</Suspense>
141145

142146
{/* Heading */}
143147
<div className="mb-10">

app/auth/register/page.tsx

Lines changed: 5 additions & 2 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, Suspense } from 'react';
44
import { useRouter } from 'next/navigation';
55
import Link from 'next/link';
66
import { useForm } from 'react-hook-form';
@@ -19,6 +19,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
1919
import Header from '@/components/layout/Header';
2020
import Footer from '@/components/layout/Footer';
2121
import dynamic from 'next/dynamic';
22+
import { WalletModalFallback } from '@/components/wallet/WalletModalFallback';
2223
const WalletModal = dynamic(() => import('@/components/wallet/WalletModal').then(m => m.WalletModal), { ssr: false });
2324

2425
export default function RegisterPage() {
@@ -83,7 +84,9 @@ export default function RegisterPage() {
8384
return (
8485
<div className="w-full">
8586
<Header />
86-
<WalletModal open={walletOpen} onOpenChange={setWalletOpen} />
87+
<Suspense fallback={<WalletModalFallback open={walletOpen} onOpenChange={setWalletOpen} />}>
88+
<WalletModal open={walletOpen} onOpenChange={setWalletOpen} />
89+
</Suspense>
8790

8891
<motion.div
8992
initial={{ opacity: 0, y: 20 }}

app/loading.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
3+
export default function RootLoading() {
4+
return (
5+
<div className="fixed inset-0 flex flex-col items-center justify-center bg-background text-foreground z-50">
6+
<div className="flex flex-col items-center space-y-4">
7+
{/* Animated logo container */}
8+
<div className="relative w-16 h-16 bg-slate-900 rounded-2xl flex items-center justify-center shadow-md animate-pulse">
9+
<img
10+
src="/logo.png"
11+
alt="BettaPay Logo"
12+
className="w-10 h-10 object-contain"
13+
/>
14+
</div>
15+
16+
{/* Brand name */}
17+
<h1 className="text-xl font-bold tracking-tight text-slate-900">
18+
BettaPay
19+
</h1>
20+
21+
{/* Lightweight custom spinner */}
22+
<div className="w-6 h-6 border-2 border-amber-500/20 border-t-amber-500 rounded-full animate-spin" />
23+
</div>
24+
</div>
25+
);
26+
}

0 commit comments

Comments
 (0)