fix(insights): point the empty-state CTAs at the existing measurements route

The B4 insights empty-state CTAs (blutdruck, gewicht, puls, bmi)
linked to `/measurements/new`, a route that does not exist; a tap
landed on a Next.js 404. Re-route via `/measurements?add=<TYPE>`,
the parameter the measurements page now consumes. Each CTA picks a
sensible default: BLOOD_PRESSURE for the BP page, WEIGHT for the
weight and BMI pages, PULSE for the pulse page. The mood, sleep,
and medication empty-states already pointed at their own dedicated
routes and need no change.

Add a `?add=<TYPE>` consumer to the measurements page. The consumer
opens the add-measurement dialog with the right `defaultType` on
first render, then `router.replace` strips the param so the back
button returns to a clean `/measurements`. The allow-list mirrors
MEASUREMENT_TYPES inside `measurement-form.tsx` so a stale link
cannot trap the user on an unknown form. The render-driven set-state
pattern follows account-section.tsx and satisfies the
`react-hooks/set-state-in-effect` rule.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 19:09:34 +02:00
1 parent 7ad894b535
commit e52b64bf34
5 files changed
+75 -17

No files matched your search

+7 -4
View File
@@ -27,9 +27,12 @@ import { hasMetricData } from "@/lib/insights/metric-availability";
* assessment, and the chart's target zones tied to the user's age.
*
* v1.4.27 F17 — when the user has zero BP observations, the page
* short-circuits to an empty-state CTA pointing at `/measurements/new`.
* Apple Health convention: empty surfaces lead to onboarding hints
* rather than chart skeletons.
* short-circuits to an empty-state CTA pointing at
* `/measurements?add=BLOOD_PRESSURE`. Apple Health convention: empty
* surfaces lead to onboarding hints rather than chart skeletons.
* v1.4.27 MB6 — the previous `/measurements/new` href hit a 404; the
* measurements page now consumes `?add=<TYPE>` and auto-opens the
* dialog with the matching default type.
*/
interface AnalyticsData {
summaries: Record<string, DataSummary>;
@@ -79,7 +82,7 @@ export default function InsightsBlutdruckPage() {
description={t("insights.emptyState.bloodPressure.description")}
action={
<Button size="sm" asChild>
<Link href="/measurements/new">
<Link href="/measurements?add=BLOOD_PRESSURE">
{t("insights.emptyState.bloodPressure.cta")}
</Link>
</Button>
+4 -2
View File
@@ -68,7 +68,9 @@ export default function InsightsBmiPage() {
// v1.4.27 F17 — BMI is derived from WEIGHT. When no weight readings
// exist yet, the existing "set your height" branch can never compute
// anything useful either; surface the empty-state CTA pointing at
// `/measurements/new` instead so the user logs the weight first.
// `/measurements?add=WEIGHT` instead so the user logs the weight
// first. v1.4.27 MB6 — query-param replaces the dead
// `/measurements/new` route.
if (
isAuthenticated &&
analytics &&
@@ -86,7 +88,7 @@ export default function InsightsBmiPage() {
description={t("insights.emptyState.bmi.description")}
action={
<Button size="sm" asChild>
<Link href="/measurements/new">
<Link href="/measurements?add=WEIGHT">
{t("insights.emptyState.bmi.cta")}
</Link>
</Button>
+1 -1
View File
@@ -76,7 +76,7 @@ export default function InsightsGewichtPage() {
description={t("insights.emptyState.weight.description")}
action={
<Button size="sm" asChild>
<Link href="/measurements/new">
<Link href="/measurements?add=WEIGHT">
{t("insights.emptyState.weight.cta")}
</Link>
</Button>
+5 -3
View File
@@ -71,8 +71,10 @@ export default function InsightsPulsPage() {
// v1.4.27 F17 — gate the sub-page on at least one pulse observation.
// Brand-new accounts (no manual logs, no Apple-Health upload yet)
// see a one-line empty-state with a CTA into `/measurements/new`
// rather than a chart skeleton over a blank target band.
// see a one-line empty-state with a CTA into
// `/measurements?add=PULSE` rather than a chart skeleton over a
// blank target band. v1.4.27 MB6 — query-param replaces the dead
// `/measurements/new` route.
if (
isAuthenticated &&
analytics &&
@@ -90,7 +92,7 @@ export default function InsightsPulsPage() {
description={t("insights.emptyState.pulse.description")}
action={
<Button size="sm" asChild>
<Link href="/measurements/new">
<Link href="/measurements?add=PULSE">
{t("insights.emptyState.pulse.cta")}
</Link>
</Button>
+58 -7
View File
@@ -1,22 +1,63 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAuth } from "@/hooks/use-auth";
import { MeasurementForm } from "@/components/measurements/measurement-form";
import { MeasurementList } from "@/components/measurements/measurement-list";
import { Button } from "@/components/ui/button";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Plus, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "@/lib/i18n/context";
// v1.4.27 MB6 — the insights empty-state CTAs link here with a
// `?add=<TYPE>` query param to auto-open the add-measurement dialog
// with the right default type. Keep the allow-list aligned with
// MEASUREMENT_TYPES inside `measurement-form.tsx`; an unknown value
// is dropped silently so a stale link can never trap the user on a
// broken form.
const ALLOWED_ADD_TYPES = new Set([
"BLOOD_PRESSURE",
"WEIGHT",
"PULSE",
"BMI",
"GLUCOSE",
"TEMPERATURE",
"BODY_FAT",
"HEART_RATE",
]);
export default function MeasurementsPage() {
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
const [dialogOpen, setDialogOpen] = useState(false);
const searchParams = useSearchParams();
const { t } = useTranslations();
// v1.4.27 MB6 — read the `?add=<TYPE>` query param during render so
// the dialog opens on the first paint (no flash of the page behind
// it). The state-from-prop pattern follows `account-section.tsx`:
// store the "addToken we acted on" sentinel and let render kick off
// the open + replace transition once per param value. The lint rule
// `react-hooks/set-state-in-effect` rejects setState from inside an
// effect, so the open-on-param work is render-driven instead.
const addParam = searchParams.get("add");
const [dialogOpen, setDialogOpen] = useState(false);
const [defaultType, setDefaultType] = useState<string | undefined>(
undefined,
);
const [consumedAddParam, setConsumedAddParam] = useState<string | null>(null);
if (addParam && addParam !== consumedAddParam) {
setConsumedAddParam(addParam);
if (ALLOWED_ADD_TYPES.has(addParam)) {
setDefaultType(addParam);
setDialogOpen(true);
}
// Drop the query string so the back-button leaves the user on
// `/measurements` rather than re-opening the dialog.
router.replace("/measurements");
}
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push("/auth/login");
@@ -50,12 +91,22 @@ export default function MeasurementsPage() {
<ResponsiveSheet
open={dialogOpen}
onOpenChange={setDialogOpen}
onOpenChange={(open) => {
setDialogOpen(open);
if (!open) setDefaultType(undefined);
}}
title={t("measurements.addMeasurement")}
>
<MeasurementForm
onSuccess={() => setDialogOpen(false)}
onCancel={() => setDialogOpen(false)}
defaultType={defaultType}
onSuccess={() => {
setDialogOpen(false);
setDefaultType(undefined);
}}
onCancel={() => {
setDialogOpen(false);
setDefaultType(undefined);
}}
/>
</ResponsiveSheet>