"use client"; import { useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { Loader2, Plus, RefreshCw, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { CycleRing } from "./cycle-ring"; import { BbtChart } from "./bbt-chart"; import { CycleCalendar } from "./cycle-calendar"; import { CycleDisclaimer } from "./cycle-disclaimer"; import { CycleHistoryChart } from "./cycle-history-chart"; import { LogDaySheet } from "./log-day-sheet"; import { PhaseEducationCard } from "./phase-education-card"; import { PredictionsPanel } from "./predictions-panel"; import { CyclePhaseHeadline, CyclePhaseCrosstab } from "./cycle-phase-crosstab"; import { CycleSymptomPatterns } from "./cycle-symptom-patterns"; import { CycleSettings } from "./cycle-settings"; import { deriveWheelState } from "./wheel-state"; import { PHASE_HUE } from "./phase-tokens"; import { localYmd, useCycleCalendar, useCycleHistory, useCycleInsights, useCycleProfile, } from "./use-cycle"; /** * v1.15.0 — the cycle vertical's client orchestrator. * * Holds the four tabs (Calendar · Predictions · Insights · Settings), the * wheel above the tab strip, and the log-day sheet. The calendar read drives * the ring (day-of-cycle + phase), the grid, and the BBT chart; the * predictions read powers the panel; history powers the stats. The page-level * RSC has already gated on `cycleTrackingEnabled`. * * The wheel earns the once-per-session "signature reveal" (sweep + glow + * sheen + count-up): a `data-revealed` flag set on first visit drives the CSS * keyframes, gated by sessionStorage so a background calendar refetch never * re-triggers the moment. */ /** YYYY-MM-DD for `n` days from now in the local tz (shares `localYmd`). */ function shiftToday(days: number): string { const d = new Date(); d.setDate(d.getDate() + days); return localYmd(d); } /** The four cycle tabs, in render order. The deep-link value is validated * against this set so a junk `?tab=` falls back to the calendar tab. */ const CYCLE_TABS = ["calendar", "predictions", "insights", "settings"] as const; type CycleTab = (typeof CYCLE_TABS)[number]; export function CycleView() { const { t } = useTranslations(); // Deep-link support: `/cycle?tab=insights` opens the insights tab. The value // is read ONCE for the initial `defaultValue` (uncontrolled tabs — so normal // clicking still works), validated against the known set, and falls back to // the calendar tab on anything else. const searchParams = useSearchParams(); const initialTab: CycleTab = useMemo(() => { const raw = searchParams?.get("tab"); return raw && (CYCLE_TABS as readonly string[]).includes(raw) ? (raw as CycleTab) : "calendar"; }, [searchParams]); const today = useMemo(() => shiftToday(0), []); const from = useMemo(() => shiftToday(-90), []); const to = useMemo(() => shiftToday(180), []); const calendar = useCycleCalendar(from, to); const history = useCycleHistory(); const profileQuery = useCycleProfile(); const insights = useCycleInsights(); const [sheetOpen, setSheetOpen] = useState(false); const [selectedDate, setSelectedDate] = useState(today); // The signature reveal plays ONCE per browser session (mirrors the wellness // strip): a background calendar refetch after a quick-log would otherwise // replay the sweep/sheen and read as janky. const [play] = useState(() => { if (typeof window === "undefined") return false; try { if (sessionStorage.getItem("cycle-wheel-revealed")) return false; sessionStorage.setItem("cycle-wheel-revealed", "1"); return true; } catch { return true; } }); // Pass the profile's typical lengths so a low-data tracker (few logged // cycles) draws the canonical four-phase ring instead of one dominant arc. const wheel = useMemo( () => deriveWheelState(calendar.data?.days ?? [], today, { typicalCycleLength: profileQuery.data?.typicalCycleLength, typicalPeriodLength: profileQuery.data?.typicalPeriodLength, lutealPhaseLength: profileQuery.data?.lutealPhaseLength, }), [calendar.data, today, profileQuery.data], ); function openSheet(date: string) { setSelectedDate(date); setSheetOpen(true); } const loading = calendar.isLoading && !calendar.data; const calendarError = calendar.isError && !calendar.data; const tileHue = wheel.phase ? PHASE_HUE[wheel.phase] : undefined; // AVOID_PREGNANCY surfaces the fertile window, so it must show the stronger // "not a contraceptive method" caveat. Prefer the server-resolved // prediction.disclaimer (already goal-correct); fall back to the goal-derived // key for the no-prediction calendar tab (QA H-1). const goal = calendar.data?.profile.goal; // Honesty-gate inputs for the phase-education card (Clue precedent): the // predictive phase framing only shows when prediction is on, not in // raw-chart mode, and at least three cycles are observed. const calProfile = calendar.data?.profile; const disclaimerText = calendar.data?.prediction?.disclaimer ?? t( goal === "AVOID_PREGNANCY" ? "cycle.disclaimer" : "cycle.prediction.disclaimer", ); return (

{t("cycle.title")}

{t("cycle.subtitle")}

{/* Desktop: ring (left) + tabs (right). Single column below lg. */}
{/* Wheel + compact phase card — grouped so they stick together. */}
{/* Wheel — the signature ring on the premium wellness-tile surface. `data-revealed` must sit on an ANCESTOR of `.wellness-tile-rise` — the keyframe selector is `[data-revealed="true"] .wellness-tile-rise` (descendant combinator). Both on the same node never matched, leaving the tile stuck at the rise rule's `opacity: 0` (invisible but still boxed). The `contents` wrapper carries the flag without adding a layout box. */}
{loading ? (
) : calendarError ? (

{t("cycle.loadError")}

) : ( )} {!calendarError ? (

{t("cycle.ring.caption")}

) : null} {/* First-period CTA — only when no cycle is active yet. */} {!loading && !calendarError && wheel.dayOfCycle == null ? ( ) : null}
{/* Phase-education card beneath the wheel — the single instance, including the "you often notice this here" chip row sourced from the user's own symptom-by-phase history. */} {!loading && !calendarError ? ( ) : null}
{/* v1.15.10 — the four German labels (Kalender/Prognosen/ Erkenntnisse/Einstellungen) overflowed the `flex-1` equal-width cells at 375 px and forced a horizontal scroll. Below `sm` the triggers size to their content (`text-xs`, no `flex-1`) and the strip scrolls cleanly if needed; from `sm` up they reclaim the equal-width `flex-1` layout. */} {t("cycle.tabs.calendar")} {t("cycle.tabs.predictions")} {t("cycle.tabs.insights")} {t("cycle.tabs.settings")} {loading ? (

{t("cycle.calendar.loading")}

) : calendarError ? ( void calendar.refetch()} /> ) : ( <> {/* Cold start: the grid carries no fertile/ovulation/phase overlays yet (the server suppresses them while learning), so name the calm state rather than letting a bare grid imply a confident forecast. */} {calendar.data?.stillLearning ? (
) : null} )}
{!loading && !calendarError ? ( ) : null}
{calendarError ? ( void calendar.refetch()} /> ) : ( <> )} {insights.isError ? ( void insights.refetch()} /> ) : ( <> )} {profileQuery.isError ? ( void profileQuery.refetch()} /> ) : profileQuery.isLoading ? (
) : profileQuery.data ? ( ) : null}
); } /** A compact in-place error + Retry for a failed tab read. */ function TabError({ onRetry }: { onRetry: () => void }) { const { t } = useTranslations(); return (
{t("cycle.loadError")}
); }