Skip to content

Commit f3439e2

Browse files
authored
Merge pull request #448 from markdavid000/ui/metrics-skeletons
ui: add skeleton loading states to dashboard metrics
2 parents 872c601 + d76bfd9 commit f3439e2

3 files changed

Lines changed: 276 additions & 98 deletions

File tree

‎frontend/src/app/(authenticated)/dashboard/page.tsx‎

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import RecentPayments from "@/components/RecentPayments";
66
import WithdrawalModal from "@/components/WithdrawalModal";
77
import DashboardSkeleton from "@/components/DashboardSkeleton";
88
import Link from "next/link";
9-
import { useMerchantHydrated, useHydrateMerchantStore, useMerchantApiKey } from "@/lib/merchant-store";
9+
import {
10+
useMerchantHydrated,
11+
useHydrateMerchantStore,
12+
useMerchantApiKey,
13+
} from "@/lib/merchant-store";
1014
import { useTranslations } from "next-intl";
1115
import FirstApiKeyModal from "@/components/FirstApiKeyModal";
1216

@@ -35,7 +39,7 @@ export default function DashboardPage() {
3539
if (hydrated && !loading && !apiKey) {
3640
const timer = setTimeout(() => {
3741
setIsFirstKeyModalOpen(true);
38-
}, 1500);
42+
}, 1500);
3943
return () => clearTimeout(timer);
4044
}
4145
}, [hydrated, loading, apiKey]);
@@ -48,60 +52,102 @@ export default function DashboardPage() {
4852
<div className="flex flex-col gap-10 animate-in fade-in duration-500">
4953
<header className="flex flex-col gap-4">
5054
<h1 className="text-4xl font-bold text-white">{t("title")}</h1>
51-
<p className="max-w-2xl text-slate-400">
52-
{t("description")}
53-
</p>
55+
<p className="max-w-2xl text-slate-400">{t("description")}</p>
5456
</header>
5557

5658
<div className="grid gap-10 lg:grid-cols-3">
5759
{/* Left Column: Metrics and Activity */}
5860
<div className="flex flex-col gap-10 lg:col-span-2">
5961
<section className="flex flex-col gap-4">
60-
<h2 className="text-xl font-semibold text-white">{t("paymentMetrics")}</h2>
62+
<h2 className="text-xl font-semibold text-white">
63+
{t("paymentMetrics")}
64+
</h2>
6165
<PaymentMetrics showSkeleton={loading} />
6266
</section>
6367

6468
<section className="flex flex-col gap-4">
6569
<div className="flex items-center justify-between">
66-
<h2 className="text-xl font-semibold text-white">{t("recentActivity")}</h2>
67-
<Link href="/payments" className="text-sm text-mint hover:text-glow">
70+
<h2 className="text-xl font-semibold text-white">
71+
{t("recentActivity")}
72+
</h2>
73+
<Link
74+
href="/payments"
75+
className="text-sm text-mint hover:text-glow"
76+
>
6877
{t("viewAllPayments")} →
6978
</Link>
7079
</div>
71-
<RecentPayments showSkeleton={loading} />
80+
{/* <RecentPayments showSkeleton={loading} /> */}
7281
</section>
7382
</div>
7483

7584
{/* Right Column: Quick Actions & Guides */}
7685
<aside className="flex flex-col gap-8">
7786
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur">
78-
<h3 className="mb-4 text-lg font-semibold text-white">{t("quickActions")}</h3>
87+
<h3 className="mb-4 text-lg font-semibold text-white">
88+
{t("quickActions")}
89+
</h3>
7990
<div className="flex flex-col gap-3">
8091
<Link
8192
href="/dashboard/create"
8293
className="flex items-center gap-3 rounded-xl border border-mint/20 bg-mint/5 px-4 py-3 text-sm font-medium text-mint transition-all hover:bg-mint/10"
8394
>
84-
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
95+
<svg
96+
className="h-5 w-5"
97+
fill="none"
98+
stroke="currentColor"
99+
viewBox="0 0 24 24"
100+
>
101+
<path
102+
strokeLinecap="round"
103+
strokeLinejoin="round"
104+
strokeWidth={2}
105+
d="M12 4v16m8-8H4"
106+
/>
86107
</svg>
87108
{t("createPaymentLink")}
88109
</Link>
89110
<button
90111
onClick={() => setIsWithdrawOpen(true)}
91112
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
92113
>
93-
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
114+
<svg
115+
className="h-5 w-5"
116+
fill="none"
117+
stroke="currentColor"
118+
viewBox="0 0 24 24"
119+
>
120+
<path
121+
strokeLinecap="round"
122+
strokeLinejoin="round"
123+
strokeWidth={2}
124+
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
125+
/>
95126
</svg>
96127
{t("withdrawFunds")}
97128
</button>
98129
<Link
99130
href="/settings"
100131
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
101132
>
102-
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
103-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
104-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
133+
<svg
134+
className="h-5 w-5"
135+
fill="none"
136+
stroke="currentColor"
137+
viewBox="0 0 24 24"
138+
>
139+
<path
140+
strokeLinecap="round"
141+
strokeLinejoin="round"
142+
strokeWidth={2}
143+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
144+
/>
145+
<path
146+
strokeLinecap="round"
147+
strokeLinejoin="round"
148+
strokeWidth={2}
149+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
150+
/>
105151
</svg>
106152
Settings
107153
</Link>
@@ -111,16 +157,28 @@ export default function DashboardPage() {
111157
rel="noopener noreferrer"
112158
className="flex items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm font-medium text-slate-300 transition-all hover:bg-white/10 hover:text-white"
113159
>
114-
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
115-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
160+
<svg
161+
className="h-5 w-5"
162+
fill="none"
163+
stroke="currentColor"
164+
viewBox="0 0 24 24"
165+
>
166+
<path
167+
strokeLinecap="round"
168+
strokeLinejoin="round"
169+
strokeWidth={2}
170+
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
171+
/>
116172
</svg>
117173
View Docs
118174
</a>
119175
</div>
120176
</section>
121177

122178
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur">
123-
<h3 className="mb-4 text-lg font-semibold text-white">{t("development")}</h3>
179+
<h3 className="mb-4 text-lg font-semibold text-white">
180+
{t("development")}
181+
</h3>
124182
<div className="space-y-4 text-sm text-slate-400">
125183
<div className="flex items-start gap-3">
126184
<div className="mt-1 h-1.5 w-1.5 rounded-full bg-mint" />
@@ -135,14 +193,14 @@ export default function DashboardPage() {
135193
</aside>
136194
</div>
137195

138-
<WithdrawalModal
139-
isOpen={isWithdrawOpen}
140-
onClose={() => setIsWithdrawOpen(false)}
196+
<WithdrawalModal
197+
isOpen={isWithdrawOpen}
198+
onClose={() => setIsWithdrawOpen(false)}
141199
/>
142200

143-
<FirstApiKeyModal
144-
isOpen={isFirstKeyModalOpen}
145-
onClose={() => setIsFirstKeyModalOpen(false)}
201+
<FirstApiKeyModal
202+
isOpen={isFirstKeyModalOpen}
203+
onClose={() => setIsFirstKeyModalOpen(false)}
146204
/>
147205
</div>
148206
);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* MetricsSkeleton
3+
*
4+
* A pixel-accurate skeleton of the full PaymentMetrics layout.
5+
* Mirrors every section in render order:
6+
* 1. 4-card summary grid (total volume, total payments, confirmed, success rate)
7+
* 2. Chart panel
8+
* a. Header row (title/subtitle + range pills + export button)
9+
* b. Asset toggle pills
10+
* c. Chart area
11+
*
12+
* All dimensions are taken directly from the live component so there is zero
13+
* layout shift once real data replaces the skeleton.
14+
*/
15+
16+
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
17+
import "react-loading-skeleton/dist/skeleton.css";
18+
19+
// ─── Individual section skeletons ─────────────────────────────────────────────
20+
21+
/**
22+
* Matches the 4-column `summary` stat grid.
23+
* On small screens the grid collapses to 2 columns (sm:grid-cols-2),
24+
* on large screens it expands to 4 (lg:grid-cols-4).
25+
*/
26+
function SummaryGridSkeleton() {
27+
return (
28+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
29+
{Array.from({ length: 4 }).map((_, i) => (
30+
<div
31+
key={i}
32+
className="rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur"
33+
>
34+
{/* Label — e.g. "7-Day Volume" */}
35+
<Skeleton width={110} height={12} borderRadius={4} />
36+
37+
{/* Value + unit on the same baseline */}
38+
<div className="mt-2 flex items-baseline gap-2">
39+
<Skeleton width={90} height={36} borderRadius={6} />
40+
<Skeleton width={36} height={18} borderRadius={4} />
41+
</div>
42+
</div>
43+
))}
44+
</div>
45+
);
46+
}
47+
48+
/**
49+
* Matches the chart panel: the outer card with its header, toggle strip, and
50+
* chart area.
51+
*
52+
* CHART_HEIGHT is 300 px in the live component — keep in sync if changed.
53+
*/
54+
function ChartPanelSkeleton() {
55+
return (
56+
<div className="flex flex-col gap-4 rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur">
57+
{/* ── Header row ── */}
58+
<div className="flex flex-wrap items-start justify-between gap-3">
59+
{/* Title + subtitle (left side) */}
60+
<div className="flex flex-col gap-2">
61+
<Skeleton width={220} height={22} borderRadius={6} />
62+
<Skeleton width={160} height={14} borderRadius={4} />
63+
</div>
64+
65+
{/* Range pill group + export button (right side) */}
66+
<div className="flex flex-wrap items-center gap-2">
67+
{/* Range selector — 3 pills inside a rounded container */}
68+
<div className="flex gap-1 rounded-lg border border-white/10 bg-white/5 p-1">
69+
<Skeleton width={34} height={28} borderRadius={6} />
70+
<Skeleton width={34} height={28} borderRadius={6} />
71+
<Skeleton width={34} height={28} borderRadius={6} />
72+
</div>
73+
74+
{/* Export button */}
75+
<Skeleton width={148} height={36} borderRadius={10} />
76+
</div>
77+
</div>
78+
79+
{/* ── Asset toggle pills ── */}
80+
<div className="flex flex-wrap gap-2">
81+
<Skeleton width={62} height={26} borderRadius={999} />
82+
<Skeleton width={62} height={26} borderRadius={999} />
83+
</div>
84+
85+
{/* ── Chart area ── */}
86+
<div className="mt-2 h-[300px]">
87+
<Skeleton height="100%" borderRadius={8} />
88+
</div>
89+
</div>
90+
);
91+
}
92+
93+
// ─── Composed export ──────────────────────────────────────────────────────────
94+
95+
/**
96+
* Drop-in replacement for the full `PaymentMetrics` component during loading.
97+
*
98+
* Usage in `PaymentMetrics.tsx`:
99+
*
100+
* ```tsx
101+
* import MetricsSkeleton from "@/components/MetricsSkeleton";
102+
*
103+
* // Replace the existing inline skeleton block with:
104+
* if (showSkeleton || loading || !hydrated) return <MetricsSkeleton />;
105+
* ```
106+
*/
107+
export default function MetricsSkeleton() {
108+
return (
109+
<SkeletonTheme baseColor="#1e293b" highlightColor="#334155">
110+
<div className="flex flex-col gap-6">
111+
<SummaryGridSkeleton />
112+
<ChartPanelSkeleton />
113+
</div>
114+
</SkeletonTheme>
115+
);
116+
}

0 commit comments

Comments
 (0)