refactor(charts): collapse health-chart dynamic imports onto re-export

This commit is contained in:
Marc-André Bombeck committed 2026-05-16 01:52:18 +02:00
1 parent 235e52cb95
commit 8f3bfc37a0
7 files changed
+54 -73

No files matched your search

+2 -11
View File
@@ -1,7 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { HeartPulse } from "lucide-react";
@@ -11,7 +10,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 { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -38,14 +37,6 @@ import { hasMetricData } from "@/lib/insights/metric-availability";
interface AnalyticsData {
summaries: Record<string, DataSummary>;
}
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export default function InsightsBlutdruckPage() {
const { isAuthenticated, user } = useAuth();
const { t } = useTranslations();
@@ -127,7 +118,7 @@ export default function InsightsBlutdruckPage() {
title={t("insights.bloodPressureSectionTitle")}
description={t("insights.subPage.blutdruckDescription")}
>
<HealthChart
<HealthChartDynamic
chartKey="bp"
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
title={t("charts.bloodPressure")}
+3 -12
View File
@@ -1,7 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Ruler } from "lucide-react";
@@ -11,7 +10,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 { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -26,19 +25,11 @@ interface AnalyticsData {
* v1.4.25 W4 — `/insights/bmi`.
*
* Routed BMI sub-page. BMI is derived from `WEIGHT / (height/100)^2`,
* so the chart sets `valueMode="bmi"` on `<HealthChart>` and the
* so the chart sets `valueMode="bmi"` on `<HealthChartDynamic>` and the
* underlying WEIGHT series is reused. When the user has no height set
* the chart can't compute; we surface the same plain empty-state the
* mother page used.
*/
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
const BMI_BANDS = [
{ min: 0, max: 17, color: "#ff5555", opacity: 0.16 },
{ min: 17, max: 18.5, color: "#ffb86c", opacity: 0.18 },
@@ -131,7 +122,7 @@ export default function InsightsBmiPage() {
title={t("insights.bmiSectionTitle")}
description={t("insights.subPage.bmiDescription")}
>
<HealthChart
<HealthChartDynamic
chartKey="bmi"
types={["WEIGHT"]}
title={t("targets.bmi")}
+2 -11
View File
@@ -1,7 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Scale } from "lucide-react";
@@ -11,7 +10,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 { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -32,14 +31,6 @@ interface AnalyticsData {
* trend lines + comparison overlay independently from the dashboard
* weight card.
*/
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export default function InsightsGewichtPage() {
const { isAuthenticated, user } = useAuth();
const { t } = useTranslations();
@@ -103,7 +94,7 @@ export default function InsightsGewichtPage() {
title={t("insights.weightSectionTitle")}
description={t("insights.subPage.gewichtDescription")}
>
<HealthChart
<HealthChartDynamic
chartKey="weight"
types={["WEIGHT"]}
title={t("charts.weight")}
+2 -11
View File
@@ -1,7 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Heart } from "lucide-react";
@@ -11,7 +10,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 { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import { SubPageShell } from "@/components/insights/sub-page-shell";
@@ -33,14 +32,6 @@ import { hasMetricData } from "@/lib/insights/metric-availability";
* MeasurementType filter is `PULSE` (the same field used elsewhere
* in the codebase).
*/
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
interface AnalyticsData {
summaries: Record<string, DataSummary>;
}
@@ -140,7 +131,7 @@ export default function InsightsPulsPage() {
title={t("insights.pulseSectionTitle")}
description={t("insights.subPage.pulsDescription")}
>
<HealthChart
<HealthChartDynamic
chartKey="pulse"
types={["PULSE"]}
title={t("charts.pulse")}
+8 -14
View File
@@ -28,6 +28,7 @@ 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 { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
@@ -44,13 +45,6 @@ import { GettingStartedChecklist } from "@/components/onboarding/getting-started
import { TourLauncher } from "@/components/onboarding/tour-launcher";
import { RecentAchievementsCard } from "@/components/gamification/recent-achievements-card";
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
const MoodChart = dynamic(
() =>
import("@/components/charts/mood-chart").then((mod) => ({
@@ -959,7 +953,7 @@ export default function DashboardPage() {
order: widgetOrder("weight"),
count: w?.count ?? 0,
node: (
<HealthChart
<HealthChartDynamic
key="weight-chart"
chartKey="weight"
types={["WEIGHT"]}
@@ -977,7 +971,7 @@ export default function DashboardPage() {
id: "bmi-chart",
order: widgetOrder("weight") + 0.5,
node: (
<HealthChart
<HealthChartDynamic
key="bmi-chart"
chartKey="bmi"
types={["WEIGHT"]}
@@ -1004,7 +998,7 @@ export default function DashboardPage() {
order: widgetOrder("bp"),
count: Math.max(sys?.count ?? 0, dia?.count ?? 0),
node: (
<HealthChart
<HealthChartDynamic
key="bp-chart"
chartKey="bp"
types={["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"]}
@@ -1025,7 +1019,7 @@ export default function DashboardPage() {
order: widgetOrder("pulse"),
count: p?.count ?? 0,
node: (
<HealthChart
<HealthChartDynamic
key="pulse-chart"
chartKey="pulse"
types={["PULSE"]}
@@ -1045,7 +1039,7 @@ export default function DashboardPage() {
order: widgetOrder("bodyFat"),
count: bf?.count ?? 0,
node: (
<HealthChart
<HealthChartDynamic
key="bodyFat-chart"
chartKey="bodyFat"
types={["BODY_FAT"]}
@@ -1080,7 +1074,7 @@ export default function DashboardPage() {
order: widgetOrder("sleep"),
count: sleepSummary?.count ?? 0,
node: (
<HealthChart
<HealthChartDynamic
key="sleep-chart"
chartKey="sleep"
types={["SLEEP_DURATION"]}
@@ -1099,7 +1093,7 @@ export default function DashboardPage() {
order: widgetOrder("steps"),
count: stepsSummary?.count ?? 0,
node: (
<HealthChart
<HealthChartDynamic
key="steps-chart"
chartKey="steps"
types={["ACTIVITY_STEPS"]}
@@ -0,0 +1,29 @@
"use client";
import dynamic from "next/dynamic";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
/**
* v1.4.28 R3d (BK-F-M2) — single `next/dynamic` boundary for the
* canonical `<HealthChart>` component.
*
* Pre-fix, six call sites (`src/app/page.tsx`, the five insights
* sub-pages, and `sleep-duration-chart.tsx`) duplicated the same
* `dynamic(() => import("@/components/charts/health-chart"), { ... })`
* incantation. The duplication meant the loading skeleton + `ssr: false`
* flag had to be re-wired on every caller (R1.2 H4 paid for this once;
* this commit prevents future regressions).
*
* Re-exporting through this module gives every consumer the same
* pre-configured `<ChartSkeleton>`-backed lazy boundary. Consumers
* still forward the same props they did before — the type contract
* mirrors `<HealthChart>` exactly via `dynamic()`'s own inference.
*/
export const HealthChartDynamic = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
@@ -1,16 +1,14 @@
"use client";
import dynamic from "next/dynamic";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
import { useTranslations } from "@/lib/i18n/context";
import type { ComparisonBaseline } from "@/lib/dashboard-layout";
/**
* v1.4.25 W4c — total nightly sleep duration over time.
*
* Thin wrapper around `<HealthChart>` so the sleep sub-page gets the
* same chart-cog parity (`chartKey="sleep"`) as the dashboard chart
* Thin wrapper around `<HealthChartDynamic>` so the sleep sub-page gets
* the same chart-cog parity (`chartKey="sleep"`) as the dashboard chart
* surfaces. SLEEP_DURATION rows from Apple Health are per-stage (one
* row per stage per night) — the analytics API aggregates per Berlin
* day before summarising, and `HealthChart` itself relies on the
@@ -21,15 +19,11 @@ import type { ComparisonBaseline } from "@/lib/dashboard-layout";
* (v1.4.23 schema note). The chart shows the raw minutes value with a
* "min" unit suffix — the parent sub-page renders a separate
* "X h Y min" headline above the chart for the human-friendly read.
*
* v1.4.28 R3d (BK-F-M2) — the inline `dynamic()` call site was retired
* in favour of the shared `<HealthChartDynamic>` re-export so the
* `<ChartSkeleton>` + `ssr: false` configuration stays in one place.
*/
const HealthChart = dynamic(
() =>
import("@/components/charts/health-chart").then((mod) => ({
default: mod.HealthChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
export interface SleepDurationChartProps {
compareBaseline?: ComparisonBaseline;
userTimezone?: string;
@@ -41,7 +35,7 @@ export function SleepDurationChart({
}: SleepDurationChartProps) {
const { t } = useTranslations();
return (
<HealthChart
<HealthChartDynamic
chartKey="sleep"
types={["SLEEP_DURATION"]}
title={t("charts.sleep")}