mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(dashboard): paint tile-strip skeleton during slow slim-analytics fetch
The tile strip only painted once `trendCards.length > 0`. When both slim + thick `/api/analytics` slices lagged (cache eviction, cold start) the user saw the page header + zero tiles + then a burst of tiles seconds later. Reserve the strip's footprint by rendering N silhouette cards (`auto-fit minmax(min(100%,11rem),1fr)` track, matching the real strip) keyed off the user's configured tile count while `analyticsSlimQuery.isLoading`. The placeholders swap to the real tiles the moment slim resolves; the EmptyState branch is suppressed for the loading window so the user no longer sees a flash of "no data" when the network is just slow.
This commit is contained in:
1 parent
9c9a99aea4
commit
d02deb1769
2 files changed
+126
-1
No files matched your search
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.43 W11-M5 — dashboard tile-strip skeleton during slow slim
|
||||
* fetches.
|
||||
*
|
||||
* Pre-fix the strip only painted once `trendCards.length > 0`. When
|
||||
* both slim + thick `/api/analytics` slices lagged (cache eviction,
|
||||
* cold start) the user saw the page header + zero tiles + then a
|
||||
* burst of tiles 9 s later. The fix paints a layout-stable silhouette
|
||||
* keyed off the user's configured tile count
|
||||
* (`layout.widgets.filter(w => w.tileVisible ?? w.visible).length`)
|
||||
* while `analyticsSlimQuery.isLoading` is still in flight, so the
|
||||
* page reserves the strip's footprint immediately and swaps to the
|
||||
* real tiles the moment slim resolves.
|
||||
*
|
||||
* Textual pins keep the contract honest — render-mounting the
|
||||
* dashboard page would haul in TanStack-Query, auth, i18n, layout
|
||||
* and analytics scaffolding for a guard a substring scan already
|
||||
* proves.
|
||||
*/
|
||||
const PAGE_PATH = join(process.cwd(), "src/app/page.tsx");
|
||||
|
||||
describe("v1.4.43 W11 — dashboard tile-strip skeleton", () => {
|
||||
it("derives `configuredTileCount` from the resolved layout", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
expect(src).toMatch(
|
||||
/const\s+configuredTileCount\s*=\s*layout\.widgets\.filter\([\s\S]*?w\.tileVisible\s*\?\?\s*w\.visible[\s\S]*?\)\.length;/,
|
||||
);
|
||||
});
|
||||
|
||||
it("gates the skeleton on slim-analytics loading + empty trend cards + configured tiles", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
expect(src).toMatch(
|
||||
/const\s+showTileStripSkeleton\s*=\s*[\s\S]*?trendCards\.length\s*===\s*0[\s\S]*?analyticsSlimQuery\.isLoading[\s\S]*?configuredTileCount\s*>\s*0/,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the skeleton with the same auto-fit grid track the real strip uses", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
expect(src).toMatch(
|
||||
/showTileStripSkeleton\s*&&\s*\(\s*<div[\s\S]*?\[grid-template-columns:repeat\(auto-fit,minmax\(min\(100%,11rem\),1fr\)\)\]/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keys the skeleton card count off configuredTileCount", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
expect(src).toMatch(
|
||||
/Array\.from\(\{\s*length:\s*configuredTileCount\s*\}\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("marks the skeleton aria-hidden + carries motion-reduce", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
const skeletonBlock = src.match(
|
||||
/showTileStripSkeleton\s*&&[\s\S]*?<\/div>\s*\)\}/,
|
||||
);
|
||||
expect(skeletonBlock).not.toBeNull();
|
||||
const block = skeletonBlock?.[0] ?? "";
|
||||
expect(block).toContain('aria-hidden="true"');
|
||||
expect(block).toContain("motion-reduce:animate-none");
|
||||
});
|
||||
|
||||
it("suppresses the EmptyState while the skeleton is showing", () => {
|
||||
const src = readFileSync(PAGE_PATH, "utf8");
|
||||
expect(src).toMatch(
|
||||
/trendCards\.length\s*===\s*0\s*&&\s*charts\.length\s*===\s*0\s*&&\s*!showTileStripSkeleton/,
|
||||
);
|
||||
});
|
||||
});
|
||||
+54
-1
@@ -1303,6 +1303,27 @@ export default function DashboardPage() {
|
||||
|
||||
charts.sort((a, b) => a.order - b.order);
|
||||
|
||||
// v1.4.43 W11-M5 — tile-strip skeleton during slow slim-analytics
|
||||
// fetches.
|
||||
//
|
||||
// `trendCards` only fills once the slim `/api/analytics` slice
|
||||
// resolves and reports per-type `count > 0` flags. The v1.4.39.2
|
||||
// slim/thick split keeps the strip painting fast when slim wins,
|
||||
// but when *both* slices lag (cache eviction, cold start) the
|
||||
// user used to see the page header + 0 tiles + then 7 tiles
|
||||
// appear at once 9 s later. The audit recommendation is to
|
||||
// render a layout-stable tile silhouette keyed off the user's
|
||||
// configured tile count so the strip's footprint is reserved
|
||||
// during the slow window. The skeleton swaps in for the real
|
||||
// strip the moment `analyticsSlimQuery.isLoading` flips false.
|
||||
const configuredTileCount = layout.widgets.filter(
|
||||
(w) => w.tileVisible ?? w.visible,
|
||||
).length;
|
||||
const showTileStripSkeleton =
|
||||
trendCards.length === 0 &&
|
||||
analyticsSlimQuery.isLoading &&
|
||||
configuredTileCount > 0;
|
||||
|
||||
// v1.4.15 phase-C5: dashboard fully-empty state. When no tile
|
||||
// and no chart has data the dashboard would otherwise paint a
|
||||
// 0-px tile strip with the welcome banner above it — visually
|
||||
@@ -1313,7 +1334,15 @@ export default function DashboardPage() {
|
||||
// surface for very-new accounts; this empty state covers the
|
||||
// case where the checklist has been dismissed but no data was
|
||||
// logged afterwards.
|
||||
if (trendCards.length === 0 && charts.length === 0) {
|
||||
//
|
||||
// v1.4.43 W11 — the empty-state only fires once the slim slice
|
||||
// resolves with no tiles to show. While slim is in flight the
|
||||
// skeleton strip below carries the layout footprint.
|
||||
if (
|
||||
trendCards.length === 0 &&
|
||||
charts.length === 0 &&
|
||||
!showTileStripSkeleton
|
||||
) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<Activity className="size-6" />}
|
||||
@@ -1357,6 +1386,30 @@ export default function DashboardPage() {
|
||||
CSS-grid `auto-fit + minmax + auto-rows-fr` track that
|
||||
continues to give every visible tile equal width / equal
|
||||
height for any non-zero count. */}
|
||||
{showTileStripSkeleton && (
|
||||
<div
|
||||
// v1.4.43 W11-M5 — tile-strip skeleton mirrors the real
|
||||
// grid track (`auto-fit minmax(min(100%,11rem),1fr)`) so
|
||||
// the layout footprint is reserved while slim-analytics
|
||||
// is in flight. Cards are keyed off the user's
|
||||
// configured tile count (`configuredTileCount`) — if the
|
||||
// user trimmed the strip to 3 tiles in Settings, the
|
||||
// skeleton shows 3 silhouettes, not 7.
|
||||
aria-hidden="true"
|
||||
data-slot="dashboard-tile-strip-skeleton"
|
||||
className={cn(
|
||||
"grid auto-rows-fr gap-3",
|
||||
"[grid-template-columns:repeat(auto-fit,minmax(min(100%,11rem),1fr))]",
|
||||
)}
|
||||
>
|
||||
{Array.from({ length: configuredTileCount }).map((_, idx) => (
|
||||
<div
|
||||
key={`tile-skeleton-${idx}`}
|
||||
className="bg-card border-border min-h-[6rem] animate-pulse rounded-xl border p-4 motion-reduce:animate-none md:p-6"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{trendCards.length > 0 && (
|
||||
<div
|
||||
// v1.4.33 A3 Win 2 + F3 — collapse the bifurcated
|
||||
|
||||
Reference in new issue
Block a user