"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { ClipboardCheck, FileUp, Plug, Watch, Wifi, type LucideIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { apiPost } from "@/lib/api/api-fetch"; /** * Onboarding step 2 (source). * * Presents the data sources HealthLog ships today. The headline cards * cover the two paths most people start on: * 1. Manual entry (enabled, recommended — works on any device) * 2. Withings (enabled — opens OAuth `/api/withings/connect` in a new * tab; the callback flips the connection server-side) * * Below them a calm "more sources" row lists the remaining shipped * integrations (Apple Health, WHOOP, Oura, Polar, Nightscout, Fitbit), * each linking to Settings → Integrations where it is actually * connected. We don't try to run six OAuth flows inside the wizard; * the row sets the expectation and hands off. A discreet import link * points at the Apple Health export.zip path — the cold-start escape * hatch for users with existing history. * * Garmin is omitted: not shipped, so a card would over-claim. * * Selection is non-binding — Manual is the implicit default and the * Next CTA always advances. Nothing here is auto-configured; the step * only signposts where each source is connected. */ interface SourceCard { slug: "manual" | "withings"; Icon: LucideIcon; titleKey: string; bodyKey: string; /** * One of: * - "enabled-select": tap selects the card (Manual) * - "enabled-oauth": tap opens OAuth in a new tab (Withings) */ state: "enabled-select" | "enabled-oauth"; /** External href for `enabled-oauth`. */ href?: string; /** Optional recommended badge. */ recommendedKey?: string; } const SOURCE_CARDS: ReadonlyArray = [ { slug: "manual", Icon: ClipboardCheck, titleKey: "onboarding.source.manual.title", bodyKey: "onboarding.source.manual.body", state: "enabled-select", recommendedKey: "onboarding.source.recommended", }, { slug: "withings", Icon: Watch, titleKey: "onboarding.source.withings.title", bodyKey: "onboarding.source.withings.body", state: "enabled-oauth", href: "/api/withings/connect", }, ]; /** * The remaining shipped integrations. Listed compactly so the step * stays calm — the full connect flow for each lives in Settings. */ const MORE_SOURCES: ReadonlyArray<{ slug: string; labelKey: string }> = [ { slug: "apple-health", labelKey: "onboarding.source.more.appleHealth" }, { slug: "whoop", labelKey: "onboarding.source.more.whoop" }, { slug: "oura", labelKey: "onboarding.source.more.oura" }, { slug: "polar", labelKey: "onboarding.source.more.polar" }, { slug: "nightscout", labelKey: "onboarding.source.more.nightscout" }, { slug: "fitbit", labelKey: "onboarding.source.more.fitbit" }, ]; export function SourceCardGrid() { const { t } = useTranslations(); const router = useRouter(); const queryClient = useQueryClient(); const [selected, setSelected] = useState("manual"); const [advancing, setAdvancing] = useState(false); async function advance() { if (advancing) return; setAdvancing(true); try { await apiPost("/api/onboarding/step", { step: 3 }); await queryClient.invalidateQueries({ queryKey: ["auth"] }); router.push("/onboarding/3"); } catch (err) { const message = err instanceof Error ? err.message : t("onboarding.errorGeneric"); toast.error(message); setAdvancing(false); } } return (

{t("onboarding.source.title")}

{t("onboarding.source.body")}

{SOURCE_CARDS.map((card) => ( setSelected(card.slug)} titleLabel={t(card.titleKey)} bodyLabel={t(card.bodyKey)} recommendedLabel={ card.recommendedKey ? t(card.recommendedKey) : null } withingsCtaLabel={t("onboarding.source.withings.cta")} /> ))}

{t("onboarding.source.more.title")}

{t("onboarding.source.more.body")}

    {MORE_SOURCES.map((item) => (
  • ))}
{t("onboarding.source.import")}
); } interface SourceCardItemProps { card: SourceCard; selected: boolean; onSelect: () => void; titleLabel: string; bodyLabel: string; recommendedLabel: string | null; withingsCtaLabel: string; } function SourceCardItem({ card, selected, onSelect, titleLabel, bodyLabel, recommendedLabel, withingsCtaLabel, }: SourceCardItemProps) { const Icon = card.Icon; const baseClasses = cn( "relative flex flex-col gap-3 rounded-lg border p-4 text-left transition-colors duration-150 ease-out motion-reduce:transition-none", "bg-card border-border", ); const headerRow = (

{titleLabel}

{recommendedLabel ? ( {recommendedLabel} ) : null}
); const body = (

{bodyLabel}

); if (card.state === "enabled-oauth") { return (
{headerRow} {body}
); } // enabled-select (Manual) return ( ); }