perf(charts): wire chart-skeleton loading state across dynamic imports

This commit is contained in:
Marc-André Bombeck committed 2026-05-16 01:43:37 +02:00
1 parent 4c6d877921
commit d286220bed
8 files changed
+76 -9

No files matched your search

+2 -1
View File
@@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -42,7 +43,7 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export default function InsightsBlutdruckPage() {
+2 -1
View File
@@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -35,7 +36,7 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
const BMI_BANDS = [
+2 -1
View File
@@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -36,7 +37,7 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export default function InsightsGewichtPage() {
+2 -1
View File
@@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -37,7 +38,7 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
interface AnalyticsData {
+2 -1
View File
@@ -11,6 +11,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -30,7 +31,7 @@ const MoodChart = dynamic(
import("@/components/charts/mood-chart").then((mod) => ({
default: mod.MoodChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
interface ComprehensiveMoodData {
+4 -3
View File
@@ -27,6 +27,7 @@ import type { DataSummary as DataSummaryType } from "@/lib/analytics/trends";
import dynamic from "next/dynamic";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
@@ -48,21 +49,21 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
const MoodChart = dynamic(
() =>
import("@/components/charts/mood-chart").then((mod) => ({
default: mod.MoodChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
const MedicationComplianceChart = dynamic(
() =>
import("@/components/charts/medication-compliance-chart").then((mod) => ({
default: mod.MedicationComplianceChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
/**
* v1.4.28 R3d (R1.2 H4) — layout-stable loading shell for every
* `next/dynamic` chart import on the dashboard and the insights
* sub-pages.
*
* The chart components (`<HealthChart>`, `<MoodChart>`, …) live behind
* `dynamic(() => import("…"), { ssr: false })`. Pre-fix, the loading
* slot was empty: the page hydrated, the Recharts chunk fetched, and
* the layout collapsed/expanded as the chart arrived. On a cold visit
* over a slow network the gap was visible for several seconds (R1.2
* H4).
*
* This skeleton mirrors the same container that `health-chart.tsx`
* paints (rounded card, border, `p-4 md:p-6`) plus a faux header row
* (title + range tabs) and a chart band sized to the CSS variable the
* chart itself reads (`--chart-height` → 240 px mobile / 280 px md+).
* The cumulative box matches the loaded chart's height to within a
* pixel so the page does not jump on hydration.
*
* `prefers-reduced-motion` is honoured automatically — the `<Skeleton>`
* primitive's `animate-pulse` is suppressed via Tailwind's
* `motion-reduce:animate-none` modifier.
*/
export function ChartSkeleton({ className }: { className?: string }) {
return (
<div
data-slot="chart-skeleton"
role="status"
aria-busy="true"
aria-live="polite"
className={cn(
"bg-card border-border rounded-xl border p-4 md:p-6",
className,
)}
>
<span className="sr-only">Loading chart…</span>
{/* Header row — title + range tabs match the real chart's
`mb-4 flex flex-col gap-2 sm:flex-row` chrome. */}
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Skeleton className="h-4 w-32" />
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-12" />
<Skeleton className="h-6 w-12" />
<Skeleton className="h-6 w-12" />
<Skeleton className="h-6 w-12" />
</div>
</div>
{/* Chart band — mirrors the same `h-[var(--chart-height,…)]`
contract `health-chart.tsx` paints at line 1087-1089. */}
<Skeleton className="h-[var(--chart-height,240px)] w-full md:h-[var(--chart-height-md,280px)]" />
</div>
);
}
@@ -2,6 +2,7 @@
import dynamic from "next/dynamic";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { useTranslations } from "@/lib/i18n/context";
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
@@ -26,7 +27,7 @@ const HealthChart = dynamic(
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false },
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export interface SleepDurationChartProps {