feat(cycle): surface a gated cycle summary card on the insights page

Add a compact cycle teaser to the main Insights overview for accounts with
cycle tracking enabled. The card shows the current phase, cycle day, and the
single headline phase finding, and deep-links into the full cycle insights tab
so the cycle tab stays the single source of truth.

The card mounts only when cycleTrackingEnabled resolves true from
/api/auth/me — the same gate the sidebar cycle entry uses — and renders nothing
for every other account. Phase and cycle day derive from the same calendar read
plus deriveWheelState the cycle wheel uses, and the headline reuses the shared
CyclePhaseHeadline, including its honest empty line when nothing has cleared the
significance check.

The cycle view now opens the insights tab when the URL carries ?tab=insights,
validated against the known tab values; CycleView is wrapped in a Suspense
boundary for the search-params read.
This commit is contained in:
Marc-André Bombeck committed 2026-06-06 19:44:01 +02:00
1 parent 9d68b07b9e
commit c1d8ff9db9
11 files changed
+410 -3

No files matched your search

+15 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect } from "react";
import { Suspense, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
@@ -40,5 +40,18 @@ export default function CyclePage() {
);
}
return <CycleView />;
// `<CycleView>` reads `useSearchParams` for the `?tab=` deep-link; wrap it in
// a Suspense boundary so the client-search-params bailout never de-opts the
// build (Next's `missing-suspense-with-csr-bailout`).
return (
<Suspense
fallback={
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
</div>
}
>
<CycleView />
</Suspense>
);
}
+19
View File
@@ -153,6 +153,20 @@ const PeriodNarrativeCard = dynamic(
},
);
// v1.15.2 — the gated cycle-insights summary teaser. Mounted ONLY when
// `user.cycleTrackingEnabled` is true (the same /api/auth/me signal the
// sidebar nav entry gates on), so the cycle reads never fire for an account
// without the feature. Deferred behind `next/dynamic` like the other
// below-the-hero blocks; it owns its own calendar + insights reads and renders
// nothing while resolving / on error, so it carries no loading placeholder.
const CycleInsightSummaryCard = dynamic(
() =>
import("@/components/cycle/cycle-insight-summary-card").then((mod) => ({
default: mod.CycleInsightSummaryCard,
})),
{ ssr: false },
);
/**
* v1.4.25 W4d — Insights mother page.
*
@@ -384,6 +398,11 @@ export default function InsightsPage() {
{flags.briefing && <PeriodNarrativeCard enabled={isAuthenticated} />}
{/* v1.15.2 — gated cycle teaser. Render only for a cycle-tracking
account; for everyone else this is nothing (no card, no layout gap).
The card itself stays silent until its reads resolve. */}
{user?.cycleTrackingEnabled ? <CycleInsightSummaryCard /> : null}
<CoincidentDeviationCard enabled={isAuthenticated} />
<RhythmEventsCard enabled={isAuthenticated} />