"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.subtitle")}
{t("cycle.loadError")}
{t("cycle.ring.caption")}
) : null} {/* First-period CTA — only when no cycle is active yet. */} {!loading && !calendarError && wheel.dayOfCycle == null ? ( ) : null}{t("cycle.calendar.loading")}
) : calendarError ? (