From b2e2f78ac2bf57d0d9cc1f919fc5a4d208ea778d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 22:13:46 +0200 Subject: [PATCH] feat(charts): show 'still computing' caption on long chart loads --- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/pl.json | 1 + .../charts/__tests__/chart-skeleton.test.tsx | 17 +++++++++ src/components/charts/chart-skeleton.tsx | 37 +++++++++++++++++++ 8 files changed, 60 insertions(+) diff --git a/messages/de.json b/messages/de.json index 08e333443..c9b7e2fa6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Diagramm wird geladen…", + "loadingSlowHint": "Auswertungen werden berechnet — das kann einen Moment dauern.", "weight": "Gewicht", "bloodPressure": "Blutdruck", "pulse": "Puls", diff --git a/messages/en.json b/messages/en.json index 1cc8031cb..30d61380d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Loading chart…", + "loadingSlowHint": "Computing analytics — this can take a moment.", "weight": "Weight", "bloodPressure": "Blood Pressure", "pulse": "Pulse", diff --git a/messages/es.json b/messages/es.json index 3643bdc7b..56080b14d 100644 --- a/messages/es.json +++ b/messages/es.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Cargando la gráfica…", + "loadingSlowHint": "Calculando los análisis — puede tardar un momento.", "weight": "Peso", "bloodPressure": "Tensión arterial", "pulse": "Pulso", diff --git a/messages/fr.json b/messages/fr.json index 63e6b7201..bca4d77b2 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Chargement du graphique…", + "loadingSlowHint": "Calcul des analyses — cela peut prendre un instant.", "weight": "Poids", "bloodPressure": "Tension artérielle", "pulse": "Pouls", diff --git a/messages/it.json b/messages/it.json index 2f75cba22..a2f6d006a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Caricamento del grafico…", + "loadingSlowHint": "Calcolo delle analisi — può richiedere un momento.", "weight": "Peso", "bloodPressure": "Pressione arteriosa", "pulse": "Pulsazioni", diff --git a/messages/pl.json b/messages/pl.json index 24e2e0ba1..87f33a0f9 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -793,6 +793,7 @@ }, "charts": { "loadingLabel": "Ładowanie wykresu…", + "loadingSlowHint": "Obliczanie analiz — to może chwilę potrwać.", "weight": "Waga", "bloodPressure": "Ciśnienie tętnicze", "pulse": "Tętno", diff --git a/src/components/charts/__tests__/chart-skeleton.test.tsx b/src/components/charts/__tests__/chart-skeleton.test.tsx index 7597d147c..d9d6447ec 100644 --- a/src/components/charts/__tests__/chart-skeleton.test.tsx +++ b/src/components/charts/__tests__/chart-skeleton.test.tsx @@ -50,4 +50,21 @@ describe("", () => { const html = render(); expect(html).toMatch(/h-\[140px\]/); }); + + // v1.4.43 QoL (L4) — slow-hint caption appears after a 3 s grace. + // SSR markup never paints the caption because `useEffect` does not + // run server-side; this also keeps hydration mismatch-free. + it("does NOT paint the slow-hint caption in the initial SSR markup", () => { + const html = render(); + expect(html).not.toContain('data-slot="chart-skeleton-slow-hint"'); + expect(html).not.toContain("Computing analytics"); + }); + + it("mini variant never sets up the slow-hint timer", () => { + // Read the component source: the `useEffect` early-returns when + // `mini` is true, so a `` row in trends + // never paints the multi-line caption that would overflow it. + const html = render(); + expect(html).not.toContain('data-slot="chart-skeleton-slow-hint"'); + }); }); diff --git a/src/components/charts/chart-skeleton.tsx b/src/components/charts/chart-skeleton.tsx index 25e218351..a4401fbdc 100644 --- a/src/components/charts/chart-skeleton.tsx +++ b/src/components/charts/chart-skeleton.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useState } from "react"; + import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; @@ -31,10 +33,21 @@ import { useTranslations } from "@/lib/i18n/context"; * `rounded-md p-2` and pins the inner band at 140 px so the loading * shell occupies the same visible box as the painted chart. * + * v1.4.43 QoL (L4) — when the skeleton is still painted three seconds + * in, surface a small caption ("Computing analytics — this can take + * a moment") so the user understands the chart is being computed + * rather than broken. Today the `/api/analytics` correlations branch + * can sit on the skeleton for ~9 s; the caption sets expectation. + * Only fires on the full variant — the mini-skeleton sits in a + * dashboard row strip where a multi-line caption would overflow. + * * `prefers-reduced-motion` is honoured automatically — the `` * primitive's `animate-pulse` is suppressed via Tailwind's * `motion-reduce:animate-none` modifier. */ + +const SLOW_HINT_DELAY_MS = 3_000; + export function ChartSkeleton({ className, mini = false, @@ -48,6 +61,21 @@ export function ChartSkeleton({ mini?: boolean; }) { const { t } = useTranslations(); + // v1.4.43 QoL (L4) — `showSlowHint` flips true 3 s into the load so + // the caption only appears for genuinely slow renders. Using a + // `useEffect` keeps the SSR markup identical (caption absent) so + // hydration never mismatches; the timer kicks in client-side once + // the skeleton actually paints. + const [showSlowHint, setShowSlowHint] = useState(false); + useEffect(() => { + if (mini) return undefined; + const handle = setTimeout( + () => setShowSlowHint(true), + SLOW_HINT_DELAY_MS, + ); + return () => clearTimeout(handle); + }, [mini]); + if (mini) { return (
+ + {showSlowHint && ( +

+ {t("charts.loadingSlowHint")} +

+ )}
); }