diff --git a/src/app/targets/page.tsx b/src/app/targets/page.tsx index a95989188..a7169f80d 100644 --- a/src/app/targets/page.tsx +++ b/src/app/targets/page.tsx @@ -27,6 +27,7 @@ import { Droplet, } from "lucide-react"; import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose"; +import { TargetsSummaryHeader } from "@/components/targets/targets-summary-header"; interface TargetData { type: string; @@ -699,6 +700,17 @@ export default function TargetsPage() { )} + {/* v1.4.25 W3e — page-level summary line. Renders nothing when + the API hasn't shipped pageSummary yet (older clients during + rollout / mocked test fixtures that pre-date this addition). */} + {data.pageSummary && ( + + )} + {/* Target cards grid */} {visibleTargets.length > 0 ? (
diff --git a/src/components/targets/__tests__/targets-summary-header.test.tsx b/src/components/targets/__tests__/targets-summary-header.test.tsx new file mode 100644 index 000000000..5449d80b8 --- /dev/null +++ b/src/components/targets/__tests__/targets-summary-header.test.tsx @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { TargetsSummaryHeader } from "../targets-summary-header"; + +/** + * v1.4.25 W3e — page-level summary line. Three load-bearing cases: + * 1. Partial: "4 of 6 targets met this week" + * 2. Full: "All 6 targets met this week" + check mark + * 3. With streak highlight chip + */ + +function render(props: Parameters[0]) { + return renderToStaticMarkup( + + + , + ); +} + +describe("", () => { + it("renders the partial-count headline when met < total", () => { + const html = render({ + targetsMetThisWeek: 4, + totalTargets: 6, + streakHighlight: null, + }); + expect(html).toContain('data-slot="targets-summary-title"'); + expect(html).toContain("4 of 6 targets met this week"); + // Check mark should NOT render in the partial state. + expect(html).not.toContain("lucide-check"); + }); + + it("renders the all-met headline + check mark when every target is met", () => { + const html = render({ + targetsMetThisWeek: 6, + totalTargets: 6, + streakHighlight: null, + }); + expect(html).toContain("All 6 targets met this week"); + expect(html).toContain("text-[var(--dracula-green)]"); + }); + + it("renders the streak highlight chip when present", () => { + const html = render({ + targetsMetThisWeek: 4, + totalTargets: 6, + streakHighlight: { metric: "BLOOD_PRESSURE", days: 5 }, + }); + expect(html).toContain('data-slot="targets-summary-streak"'); + expect(html).toContain("5-day streak: Blood pressure"); + }); + + it("renders nothing when there are no targets at all", () => { + const html = render({ + targetsMetThisWeek: 0, + totalTargets: 0, + streakHighlight: null, + }); + expect(html).toBe(""); + }); +}); diff --git a/src/components/targets/targets-summary-header.tsx b/src/components/targets/targets-summary-header.tsx new file mode 100644 index 000000000..290670d1b --- /dev/null +++ b/src/components/targets/targets-summary-header.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { CheckCircle2 } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { useTranslations } from "@/lib/i18n/context"; + +/** + * v1.4.25 W3e — page-level summary line. + * + * "4 of 6 targets met this week" (DE: "4 von 6 Zielwerten diese Woche + * erreicht"). When every target hits the bar we surface a calm check + * mark + "All N targets met this week" copy — no animation, no + * celebration. Marc directive: stay quieter than the v1.4.20 Insights + * hero. + * + * Optional streak highlight chip when one metric has a current ≥ 3 day + * streak; the metric label is resolved from the targets.label.* i18n + * map so it renders in the user's locale. + */ +export interface TargetsSummaryHeaderProps { + targetsMetThisWeek: number; + totalTargets: number; + streakHighlight: { metric: string; days: number } | null; + className?: string; +} + +export function TargetsSummaryHeader({ + targetsMetThisWeek, + totalTargets, + streakHighlight, + className, +}: TargetsSummaryHeaderProps) { + const { t } = useTranslations(); + + if (totalTargets === 0) return null; + + const allMet = targetsMetThisWeek === totalTargets; + const headline = allMet + ? t("targets.summary.weekTitleAll", { total: String(totalTargets) }) + : t("targets.summary.weekTitle", { + met: String(targetsMetThisWeek), + total: String(totalTargets), + }); + + let highlightCopy: string | null = null; + if (streakHighlight && streakHighlight.days >= 3) { + const metricLabel = t(`targets.label.${streakHighlight.metric}`); + const resolvedMetric = + metricLabel === `targets.label.${streakHighlight.metric}` + ? streakHighlight.metric + : metricLabel; + highlightCopy = t("targets.summary.streakHighlight", { + count: String(streakHighlight.days), + metric: resolvedMetric, + }); + } + + return ( +
+
+ {allMet && ( +
+ {highlightCopy && ( + + {highlightCopy} + + )} +
+ ); +}