feat(insights,dashboard): empty states for first-run views

Closes the last two surfaces from the C5 audit.

- /insights — Top-level "no data" path replaces a bare "No data
  available" sentence with EmptyState (TrendingUp icon) + "Add a
  measurement" CTA into /measurements. The BMI section's
  height-not-set fallback gets a compact plain-variant EmptyState
  (Ruler icon) + "Open account settings" CTA instead of a single
  grey line that looked like a bug.
- / (dashboard) — When trendCards.length === 0 AND charts.length ===
  0 (a brand-new account with no data and the Getting-started
  checklist already dismissed) the dashboard rendered an empty
  0-px tile strip below the welcome banner. Now paints an
  EmptyState with the same quick-entry dialog wired to the CTA so
  the user can log their first measurement without leaving the page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 21:53:05 +02:00
1 parent 1d65f3b925
commit 65faf1d156
2 files changed
+64 -4

No files matched your search

+34 -4
View File
@@ -7,7 +7,10 @@ import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { Progress } from "@/components/ui/progress";
import Link from "next/link";
import dynamic from "next/dynamic";
const HealthChart = dynamic(
@@ -809,10 +812,20 @@ export default function InsightsPage() {
}
if (!data) {
// v1.4.15 phase-C5: replace bare "no data" text with EmptyState +
// CTA into /measurements so the user has a single-click path out
// of the empty insights view.
return (
<div className="text-muted-foreground py-20 text-center">
{t("common.noData")}
</div>
<EmptyState
icon={<TrendingUp className="size-6" />}
title={t("insights.emptyTitle")}
description={t("insights.emptyDescription")}
action={
<Button size="sm" asChild>
<Link href="/measurements">{t("insights.emptyAddMeasurement")}</Link>
</Button>
}
/>
);
}
@@ -1491,7 +1504,24 @@ export default function InsightsPage() {
valueBands={bmiBands}
/>
) : (
<p className="text-muted-foreground text-sm">{t("common.noData")}</p>
// v1.4.15 phase-C5: explicit empty state when the user
// hasn't set a height yet — without it the BMI card looked
// broken. Plain variant so the dashed border doesn't double
// up inside the section card.
<EmptyState
variant="plain"
size="compact"
icon={<Ruler className="size-5" />}
title={t("insights.bmiEmptyTitle")}
description={t("insights.bmiEmptyDescription")}
action={
<Button variant="outline" size="sm" asChild>
<Link href="/settings/account">
{t("insights.bmiEmptyAction")}
</Link>
</Button>
}
/>
)}
<InsightStatusCard
+30
View File
@@ -24,6 +24,7 @@ import {
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 {
Dialog,
DialogContent,
@@ -950,6 +951,35 @@ export default function DashboardPage() {
charts.sort((a, b) => a.order - b.order);
// v1.4.15 phase-C5: dashboard fully-empty state. When no tile
// and no chart has data the dashboard would otherwise paint a
// 0-px tile strip with the welcome banner above it — visually
// looked like a half-broken page. Render an EmptyState that
// re-uses the existing quick-entry dialog so the user has a
// single click into "Log measurement" without leaving the page.
// The GettingStartedChecklist above renders its own self-gated
// surface for very-new accounts; this empty state covers the
// case where the checklist has been dismissed but no data was
// logged afterwards.
if (trendCards.length === 0 && charts.length === 0) {
return (
<EmptyState
icon={<Activity className="size-6" />}
title={t("dashboard.emptyTitle")}
description={t("dashboard.emptyDescription")}
action={
<Button
size="sm"
onClick={() => setQuickEntryDialog("measurement")}
>
<Plus className="mr-1 h-4 w-4" />
{t("dashboard.emptyAddMeasurement")}
</Button>
}
/>
);
}
return (
<>
{/* v1.4: dashboard tiles are *always* a single row.