The 'switching is laggy with many accounts' report — root cause is that both /page.tsx and /users/page.tsx are Server Components that block on the API fetch before sending any HTML. During the wait, the previous route stays frozen (no spinner, no feedback) — the user perceives a 'lag' that grows with row count. App Router's loading.tsx convention solves this: Next.js renders it INSTANTLY on navigation, then streams in the real RSC tree once the data fetch resolves. The skeleton matches the shape of the real shell + a few placeholder rows so the swap is layout-stable. Files: - web/components/table-skeleton.tsx — shared skeleton (PageHead + N rows) - web/app/loading.tsx — used for / - web/app/users/loading.tsx — used for /users If row counts keep growing past a few hundred and the table itself becomes the bottleneck (vs the network fetch this addresses), the next step is pagination: accept ?limit=&offset= on /acc/ and /user/ in cm_api.py and add a 'Load more' button (or a virtual list) at the table-component layer.
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
type Props = { eyebrow: string; title: string; rows?: number };
|
|
|
|
/**
|
|
* Lightweight skeleton that mimics the AccountsTable / UsersTable shell.
|
|
* Rendered by app/loading.tsx and app/users/loading.tsx — Next.js shows
|
|
* this immediately on tab navigation, then streams in the real Server
|
|
* Component once its data fetch resolves. Without it, the previous
|
|
* route's UI freezes until the fetch finishes (the "tab switch is
|
|
* laggy" symptom).
|
|
*
|
|
* The pulse animation comes from Tailwind's animate-pulse on each
|
|
* placeholder bar; no JS, no layout shift when the real content swaps in.
|
|
*/
|
|
export default function TableSkeleton({ eyebrow, title, rows = 8 }: Props) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
<div>
|
|
<p className="text-[11px] font-medium uppercase tracking-wider text-zinc-500">
|
|
{eyebrow}
|
|
</p>
|
|
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
|
|
{title}
|
|
<span className="ml-2 inline-block h-4 w-8 animate-pulse rounded bg-zinc-200 align-middle" />
|
|
</h1>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="h-8 w-20 animate-pulse rounded-full bg-zinc-200" />
|
|
<span className="h-8 w-16 animate-pulse rounded-full bg-zinc-200" />
|
|
</div>
|
|
</div>
|
|
<ul className="space-y-2">
|
|
{Array.from({ length: rows }).map((_, i) => (
|
|
<li
|
|
key={i}
|
|
className="rounded-xl bg-white px-4 py-3 ring-1 ring-zinc-200/60"
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
<span className="h-4 w-24 animate-pulse rounded bg-zinc-200" />
|
|
<span className="h-4 w-16 animate-pulse rounded-full bg-zinc-100" />
|
|
</div>
|
|
<span className="h-6 w-6 animate-pulse rounded-md bg-zinc-100" />
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|