Admin dashboard: fix stats waterfall, redesign layout, add animations
Deploy to route.crispygoat.com / deploy (push) Failing after 3m13s

- Fetch dashboard stats server-side in admin/page.tsx to eliminate
  client-side useEffect waterfall and the '—' placeholder flash
- Pass pre-fetched stats as props to DashboardClient
- Lazy-load UpgradePlanModal via next/dynamic (only loads when needed)
- Redesign stats cards with compact layout, smaller icons, Fraunces serif values
- Improve Quick Actions + Usage row: side-by-side on desktop, stacked mobile
- Clean up Recent Orders as a proper list with icon/name/time/amount/badge
- Add staggered fade-up entrance animations (0/60/120/180/240ms delays)
- Consolidate loading.tsx skeleton to match actual dashboard structure
- Mobile-responsive: 2-col stats on mobile, stacked usage, collapsible header
This commit is contained in:
Tyler
2026-06-10 12:45:08 -06:00
parent 6c1c616c1c
commit 0cf2ee7948
11 changed files with 1463 additions and 493 deletions
+95 -30
View File
@@ -1,46 +1,111 @@
import LoadingSkeleton, { SkeletonCard, SkeletonTable } from "@/components/shared/LoadingSkeleton";
export default function AdminLoading() {
return (
<main className="min-h-screen px-4 sm:px-6 md:px-8 py-8" style={{ backgroundColor: "var(--admin-bg)" }}>
<div className="max-w-7xl mx-auto space-y-6">
{/* Header skeleton */}
<div className="flex items-center justify-between">
<main className="min-h-screen px-4 sm:px-6 md:px-8 py-6" style={{ backgroundColor: "var(--admin-bg)" }}>
{/* Header skeleton */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="animate-pulse w-9 h-9 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-2">
<LoadingSkeleton variant="text" width="w-48" height="h-8" />
<LoadingSkeleton variant="text" width="w-64" height="h-4" />
<div className="animate-pulse h-5 w-36 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<LoadingSkeleton variant="button" />
</div>
<div className="animate-pulse h-8 w-28 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
{/* Stats cards skeleton */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="rounded-xl border bg-white p-5"
style={{ borderColor: "var(--admin-border)" }}
>
<div className="flex items-center gap-3">
<div className="animate-pulse rounded-xl h-10 w-10" style={{ backgroundColor: "var(--admin-bg)" }} />
<div className="flex-1 space-y-2">
<div className="animate-pulse h-3 w-16 rounded" style={{ backgroundColor: "var(--admin-bg)" }} />
<div className="animate-pulse h-5 w-12 rounded" style={{ backgroundColor: "var(--admin-bg)" }} />
{/* Stats cards skeleton */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-center gap-3 p-4">
<div className="animate-pulse w-9 h-9 rounded-xl flex-shrink-0" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-1.5 flex-1">
<div className="animate-pulse h-2.5 w-16 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-6 w-12 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
</div>
))}
</div>
{/* Quick Actions + Usage skeleton */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 mb-5">
<div className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-3 w-20 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="grid grid-cols-2 gap-2 p-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="animate-pulse rounded-lg h-10" style={{ backgroundColor: "var(--admin-border)" }} />
))}
</div>
</div>
<div className="lg:col-span-2 rounded-xl bg-white border overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-5 w-16 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 p-4">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<div className="flex items-center justify-between">
<div className="animate-pulse h-2.5 w-10 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-12 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="animate-pulse h-1.5 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
))}
</div>
</div>
</div>
{/* Recent orders skeleton */}
<div className="rounded-xl bg-white border overflow-hidden mb-5">
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--admin-border-light)" }}>
<div className="animate-pulse h-3 w-20 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-3 w-14 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="divide-y" style={{ borderColor: "var(--admin-border-light)" }}>
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="animate-pulse w-8 h-8 rounded-lg flex-shrink-0" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="space-y-1">
<div className="animate-pulse h-3 w-28 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-16 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
<div className="flex items-center gap-3">
<div className="animate-pulse h-3 w-14 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-5 w-16 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
))}
</div>
</div>
{/* Content cards skeleton */}
<div className="grid gap-4 lg:grid-cols-3">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
{/* Filter tabs skeleton */}
<div className="flex gap-2 mb-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="animate-pulse h-9 w-20 rounded-lg" style={{ backgroundColor: "var(--admin-border)" }} />
))}
</div>
{/* Table skeleton */}
<SkeletonTable rows={8} cols={4} />
{/* Section cards skeleton */}
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="rounded-xl bg-white border overflow-hidden">
<div className="flex items-start justify-between p-4">
<div className="animate-pulse w-9 h-9 rounded-xl" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse w-12 h-4 rounded-full" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
<div className="px-4 pb-4 space-y-1.5">
<div className="animate-pulse h-3 w-24 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-full rounded" style={{ backgroundColor: "var(--admin-border)" }} />
<div className="animate-pulse h-2.5 w-3/4 rounded" style={{ backgroundColor: "var(--admin-border)" }} />
</div>
</div>
))}
</div>
</main>
);
+6
View File
@@ -3,6 +3,7 @@ import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { isFeatureEnabled } from "@/lib/feature-flags";
import { getBillingOverview } from "@/actions/billing/billing-overview";
import { getDashboardStats } from "@/actions/dashboard";
import DashboardClient from "@/components/admin/DashboardClient";
import { pool } from "@/lib/db";
@@ -122,6 +123,10 @@ export default async function AdminPage() {
);
}
// Fetch dashboard stats server-side to eliminate client-side waterfall.
// Stats are now available immediately — no "—" placeholder flash.
const stats = await getDashboardStats();
return (
<DashboardClient
brandId={adminUser?.brand_id ?? null}
@@ -131,6 +136,7 @@ export default async function AdminPage() {
enabledAddons={enabledAddons}
usage={usage}
limits={limits}
stats={stats}
/>
);
}