Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 84 additions & 26 deletions frontend/src/app/(authenticated)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import RecentPayments from "@/components/RecentPayments";
import WithdrawalModal from "@/components/WithdrawalModal";
import DashboardSkeleton from "@/components/DashboardSkeleton";
import Link from "next/link";
import { useMerchantHydrated, useHydrateMerchantStore, useMerchantApiKey } from "@/lib/merchant-store";
import {
useMerchantHydrated,
useHydrateMerchantStore,
useMerchantApiKey,
} from "@/lib/merchant-store";
import { useTranslations } from "next-intl";
import FirstApiKeyModal from "@/components/FirstApiKeyModal";

Expand Down Expand Up @@ -35,7 +39,7 @@ export default function DashboardPage() {
if (hydrated && !loading && !apiKey) {
const timer = setTimeout(() => {
setIsFirstKeyModalOpen(true);
}, 1500);
}, 1500);
return () => clearTimeout(timer);
}
}, [hydrated, loading, apiKey]);
Expand All @@ -48,60 +52,102 @@ export default function DashboardPage() {
<div className="flex flex-col gap-10 animate-in fade-in duration-500">
<header className="flex flex-col gap-4">
<h1 className="text-4xl font-bold text-white">{t("title")}</h1>
<p className="max-w-2xl text-slate-400">
{t("description")}
</p>
<p className="max-w-2xl text-slate-400">{t("description")}</p>
</header>

<div className="grid gap-10 lg:grid-cols-3">
{/* Left Column: Metrics and Activity */}
<div className="flex flex-col gap-10 lg:col-span-2">
<section className="flex flex-col gap-4">
<h2 className="text-xl font-semibold text-white">{t("paymentMetrics")}</h2>
<h2 className="text-xl font-semibold text-white">
{t("paymentMetrics")}
</h2>
<PaymentMetrics showSkeleton={loading} />
</section>

<section className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">{t("recentActivity")}</h2>
<Link href="/payments" className="text-sm text-mint hover:text-glow">
<h2 className="text-xl font-semibold text-white">
{t("recentActivity")}
</h2>
<Link
href="/payments"
className="text-sm text-mint hover:text-glow"
>
{t("viewAllPayments")} →
</Link>
</div>
<RecentPayments showSkeleton={loading} />
{/* <RecentPayments showSkeleton={loading} /> */}
</section>
</div>

{/* Right Column: Quick Actions & Guides */}
<aside className="flex flex-col gap-8">
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 backdrop-blur">
<h3 className="mb-4 text-lg font-semibold text-white">{t("quickActions")}</h3>
<h3 className="mb-4 text-lg font-semibold text-white">
{t("quickActions")}
</h3>
<div className="flex flex-col gap-3">
<Link
href="/dashboard/create"
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"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
{t("createPaymentLink")}
</Link>
<button
onClick={() => setIsWithdrawOpen(true)}
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"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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"
/>
</svg>
{t("withdrawFunds")}
</button>
<Link
href="/settings"
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"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
Settings
</Link>
Expand All @@ -111,16 +157,28 @@ export default function DashboardPage() {
rel="noopener noreferrer"
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"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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"
/>
</svg>
View Docs
</a>
</div>
</section>

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

<WithdrawalModal
isOpen={isWithdrawOpen}
onClose={() => setIsWithdrawOpen(false)}
<WithdrawalModal
isOpen={isWithdrawOpen}
onClose={() => setIsWithdrawOpen(false)}
/>

<FirstApiKeyModal
isOpen={isFirstKeyModalOpen}
onClose={() => setIsFirstKeyModalOpen(false)}
<FirstApiKeyModal
isOpen={isFirstKeyModalOpen}
onClose={() => setIsFirstKeyModalOpen(false)}
/>
</div>
);
Expand Down
116 changes: 116 additions & 0 deletions frontend/src/components/MetricsSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* MetricsSkeleton
*
* A pixel-accurate skeleton of the full PaymentMetrics layout.
* Mirrors every section in render order:
* 1. 4-card summary grid (total volume, total payments, confirmed, success rate)
* 2. Chart panel
* a. Header row (title/subtitle + range pills + export button)
* b. Asset toggle pills
* c. Chart area
*
* All dimensions are taken directly from the live component so there is zero
* layout shift once real data replaces the skeleton.
*/

import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";

// ─── Individual section skeletons ─────────────────────────────────────────────

/**
* Matches the 4-column `summary` stat grid.
* On small screens the grid collapses to 2 columns (sm:grid-cols-2),
* on large screens it expands to 4 (lg:grid-cols-4).
*/
function SummaryGridSkeleton() {
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur"
>
{/* Label — e.g. "7-Day Volume" */}
<Skeleton width={110} height={12} borderRadius={4} />

{/* Value + unit on the same baseline */}
<div className="mt-2 flex items-baseline gap-2">
<Skeleton width={90} height={36} borderRadius={6} />
<Skeleton width={36} height={18} borderRadius={4} />
</div>
</div>
))}
</div>
);
}

/**
* Matches the chart panel: the outer card with its header, toggle strip, and
* chart area.
*
* CHART_HEIGHT is 300 px in the live component — keep in sync if changed.
*/
function ChartPanelSkeleton() {
return (
<div className="flex flex-col gap-4 rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur">
{/* ── Header row ── */}
<div className="flex flex-wrap items-start justify-between gap-3">
{/* Title + subtitle (left side) */}
<div className="flex flex-col gap-2">
<Skeleton width={220} height={22} borderRadius={6} />
<Skeleton width={160} height={14} borderRadius={4} />
</div>

{/* Range pill group + export button (right side) */}
<div className="flex flex-wrap items-center gap-2">
{/* Range selector — 3 pills inside a rounded container */}
<div className="flex gap-1 rounded-lg border border-white/10 bg-white/5 p-1">
<Skeleton width={34} height={28} borderRadius={6} />
<Skeleton width={34} height={28} borderRadius={6} />
<Skeleton width={34} height={28} borderRadius={6} />
</div>

{/* Export button */}
<Skeleton width={148} height={36} borderRadius={10} />
</div>
</div>

{/* ── Asset toggle pills ── */}
<div className="flex flex-wrap gap-2">
<Skeleton width={62} height={26} borderRadius={999} />
<Skeleton width={62} height={26} borderRadius={999} />
</div>

{/* ── Chart area ── */}
<div className="mt-2 h-[300px]">
<Skeleton height="100%" borderRadius={8} />
</div>
</div>
);
}

// ─── Composed export ──────────────────────────────────────────────────────────

/**
* Drop-in replacement for the full `PaymentMetrics` component during loading.
*
* Usage in `PaymentMetrics.tsx`:
*
* ```tsx
* import MetricsSkeleton from "@/components/MetricsSkeleton";
*
* // Replace the existing inline skeleton block with:
* if (showSkeleton || loading || !hydrated) return <MetricsSkeleton />;
* ```
*/
export default function MetricsSkeleton() {
return (
<SkeletonTheme baseColor="#1e293b" highlightColor="#334155">
<div className="flex flex-col gap-6">
<SummaryGridSkeleton />
<ChartPanelSkeleton />
</div>
</SkeletonTheme>
);
}
Loading
Loading