feat(insights): link the overview Trends cards to their detail pages

The overview charted each trend but never offered a way into the metric's
detail page — the steps page in particular exists and is used, yet Insights
never linked to it. Give every Trends-row chart a routed detail target and a
discreet "view details" link below the caption, so a metric the row surfaces
(steps included, when the briefing flags it) drills into /insights/<slug>.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 21:24:02 +02:00
1 parent 71b29f5b6f
commit 34fbfe1c67
10 files changed
+83 -7

No files matched your search

+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Geh- und Laufstrecke der letzten 30 Tage.",
"vo2_max": "VO₂max der letzten 30 Tage.",
"body_temp": "Körpertemperatur der letzten 30 Tage."
}
},
"viewDetail": "{metric} im Detail"
},
"vo2Max": {
"statLatest": "Aktuell",
+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Walking and running distance over the last 30 days.",
"vo2_max": "VO₂ max over the last 30 days.",
"body_temp": "Body temperature over the last 30 days."
}
},
"viewDetail": "View {metric} details"
},
"vo2Max": {
"statLatest": "Latest",
+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Distancia caminando y corriendo de los últimos 30 días.",
"vo2_max": "VO₂ máx de los últimos 30 días.",
"body_temp": "Temperatura corporal de los últimos 30 días."
}
},
"viewDetail": "Ver detalles de {metric}"
},
"vo2Max": {
"statLatest": "Actual",
+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Distance de marche et de course des 30 derniers jours.",
"vo2_max": "VO₂ max des 30 derniers jours.",
"body_temp": "Température corporelle des 30 derniers jours."
}
},
"viewDetail": "Voir le détail de {metric}"
},
"vo2Max": {
"statLatest": "Actuel",
+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Distanza a piedi e di corsa degli ultimi 30 giorni.",
"vo2_max": "VO₂ max degli ultimi 30 giorni.",
"body_temp": "Temperatura corporea degli ultimi 30 giorni."
}
},
"viewDetail": "Vedi i dettagli di {metric}"
},
"vo2Max": {
"statLatest": "Attuale",
+2 -1
View File
@@ -2029,7 +2029,8 @@
"distance": "Dystans marszu i biegu z ostatnich 30 dni.",
"vo2_max": "VO₂ max z ostatnich 30 dni.",
"body_temp": "Temperatura ciała z ostatnich 30 dni."
}
},
"viewDetail": "Zobacz szczegóły: {metric}"
},
"vo2Max": {
"statLatest": "Aktualne",
@@ -284,4 +284,22 @@ describe("<TrendsRow>", () => {
expect(html).toContain("Moderate confidence");
expect(html).toContain("Low confidence");
});
// ── v1.18.6 — detail-page cross-links (the Insights→steps gap) ──────
it("links each metric card to its detail page", () => {
const html = render(<TrendsRow />);
// The default BP / weight / mood triple each link to their sub-page.
expect(html).toContain('data-slot="trends-row-detail-link"');
expect(html).toContain('href="/insights/blood-pressure"');
expect(html).toContain('href="/insights/weight"');
expect(html).toContain('href="/insights/mood"');
});
it("offers the steps detail link when the briefing flags steps", () => {
// Steps has a routed sub-page but the overview never linked to it; a
// briefing finding on steps now charts it AND drills into /insights/steps.
const html = render(<TrendsRow briefing={briefing(["steps"])} />);
expect(html).toMatch(/data-metric="steps"/);
expect(html).toContain('href="/insights/steps"');
});
});
+18 -1
View File
@@ -1,7 +1,8 @@
"use client";
import dynamic from "next/dynamic";
import { TrendingUp } from "lucide-react";
import Link from "next/link";
import { ArrowRight, TrendingUp } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { useAuth } from "@/hooks/use-auth";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
@@ -301,6 +302,22 @@ export function TrendsRow({
text={t(config.captionKey)}
/>
)}
{/* v1.18.6 — drill-in link to the metric's detail page. The
overview charted these metrics but never offered a way INTO
their detail pages — notably the steps page, which exists and
is used but was unreachable from Insights. The link sits below
the caption so the chart keeps its own interactions. */}
{config.detailHref ? (
<Link
href={config.detailHref}
data-slot="trends-row-detail-link"
data-metric={config.metric}
className="text-muted-foreground hover:text-foreground mt-auto inline-flex items-center gap-1 self-start text-xs font-medium transition-colors"
>
{t("insights.trendsRow.viewDetail", { metric: title })}
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
) : null}
</div>
);
})}
@@ -151,4 +151,17 @@ describe("selectTrendCharts", () => {
}
}
});
it("every chart-bearing sourceMetric carries a routed detailHref", () => {
// v1.18.6 — the overview Trends row drills into the metric's detail page.
// Each chartable slot must resolve to a `/insights/<slug>` target so the
// cross-link never lands on a non-route — steps in particular.
for (const [metric, config] of Object.entries(TREND_CHART_CONFIG)) {
if (!config) continue;
expect(config.detailHref, `${metric} detailHref`).toMatch(
/^\/insights\//,
);
}
expect(TREND_CHART_CONFIG.steps?.detailHref).toBe("/insights/steps");
});
});
+22
View File
@@ -74,6 +74,15 @@ export interface TrendChartConfig {
* for additive metrics, which fall back to {@link captionKey}.
*/
annotationKey?: TrendAnnotationKey;
/**
* Routed detail page for the metric (`/insights/<slug>`), when one exists.
* The Trends-row card links to it so the overview can drill into the metric
* — notably the one cross-link to `/insights/steps`, which the overview
* otherwise never offered despite the steps page existing and being used.
* Omitted for slots with no dedicated page (mood routes through its own
* surface; metabolic re-frames live on their composite pages).
*/
detailHref?: string;
}
/**
@@ -100,6 +109,7 @@ export const TREND_CHART_CONFIG: Record<
titleKey: "charts.bloodPressure",
captionKey: "insights.trendsRow.caption.bp",
annotationKey: "bp",
detailHref: "/insights/blood-pressure",
},
weight: {
metric: "weight",
@@ -110,6 +120,7 @@ export const TREND_CHART_CONFIG: Record<
titleKey: "charts.weight",
captionKey: "insights.trendsRow.caption.weight",
annotationKey: "weight",
detailHref: "/insights/weight",
},
pulse: {
metric: "pulse",
@@ -120,6 +131,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "bpm",
titleKey: "charts.pulse",
captionKey: "insights.trendsRow.caption.pulse",
detailHref: "/insights/pulse",
},
mood: {
metric: "mood",
@@ -129,6 +141,7 @@ export const TREND_CHART_CONFIG: Record<
titleKey: "charts.mood",
captionKey: "insights.trendsRow.caption.mood",
annotationKey: "mood",
detailHref: "/insights/mood",
},
sleep: {
metric: "sleep",
@@ -139,6 +152,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "h",
titleKey: "charts.sleep",
captionKey: "insights.trendsRow.caption.sleep",
detailHref: "/insights/sleep",
},
steps: {
metric: "steps",
@@ -149,6 +163,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "steps",
titleKey: "charts.steps",
captionKey: "insights.trendsRow.caption.steps",
detailHref: "/insights/steps",
},
hrv: {
metric: "hrv",
@@ -159,6 +174,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "ms",
titleKey: "charts.hrv",
captionKey: "insights.trendsRow.caption.hrv",
detailHref: "/insights/hrv",
},
resting_hr: {
metric: "resting_hr",
@@ -169,6 +185,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "bpm",
titleKey: "charts.restingHeartRate",
captionKey: "insights.trendsRow.caption.resting_hr",
detailHref: "/insights/resting-pulse",
},
active_energy: {
metric: "active_energy",
@@ -179,6 +196,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "kcal",
titleKey: "charts.activeEnergy",
captionKey: "insights.trendsRow.caption.active_energy",
detailHref: "/insights/active-energy",
},
flights: {
metric: "flights",
@@ -189,6 +207,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "flights",
titleKey: "charts.flights",
captionKey: "insights.trendsRow.caption.flights",
detailHref: "/insights/flights-climbed",
},
distance: {
metric: "distance",
@@ -199,6 +218,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "km",
titleKey: "charts.distance",
captionKey: "insights.trendsRow.caption.distance",
detailHref: "/insights/walking-distance",
},
vo2_max: {
metric: "vo2_max",
@@ -209,6 +229,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "mL/kg·min",
titleKey: "charts.vo2Max",
captionKey: "insights.trendsRow.caption.vo2_max",
detailHref: "/insights/cardio-fitness",
},
body_temp: {
metric: "body_temp",
@@ -219,6 +240,7 @@ export const TREND_CHART_CONFIG: Record<
yAxisUnit: "°C",
titleKey: "charts.bodyTemperature",
captionKey: "insights.trendsRow.caption.body_temp",
detailHref: "/insights/body-temperature",
},
// Adherence-context findings — no single plottable trend series in
// this row. A finding on one of these is skipped during selection so