Files
HealthLog/src/components/settings/integrations/setup-guide-link.tsx
T
Marc-André Bombeck b2e1aa0cde refactor(settings): one header, one action row, one integration card
The sweep that follows from the primitives. Nothing here is new behaviour;
every change removes a second way of saying something the surface already
said one way somewhere else.

Headers. The two collapsible security cards stop rebuilding
`SettingsCardHeader` by hand to carry a chevron — the chevron rides inside
the title button, and the description moves out of the collapsed region, so
the first paint is no longer a bare title. The import card shell, the export
and import section headings, and the four free-standing group headers stop
inventing their own type scale: a heading above a GROUP of cards is
`SectionHeading` (`text-base`), a card title is `SettingsCardHeader`
(`text-lg`), and the step between them is what says which is which. The
targets card gets a title instead of a naked icon.

Actions. Every card-level row is `SettingsCardActions`. The primary action
comes out of the header's status slot in six cards (password, web push,
pillar order, coach memory, and the two danger-zone triggers) and out of the
middle of the card in three more; the reset that hung in the dashboard-layout
header joins save as a secondary in the same row. Result and status lines
move ABOVE the row — sources' cross-link, the invite form's two status lines
— because nothing follows a card's actions. Revoking a share link is
`variant="destructive"` rather than an `outline` hand-tinted red in two
different ways. The import row had four variants for four buttons; it has
two. Export downloads are the card's primary, not an outline link.

Integrations. One description sentence with the setup-guide link inline: the
second paragraph was byte-identical across six of eight cards, which is the
tell that it said nothing about the provider it sat under. The `<hr>` under
the header — which existed only in this folder — is gone. So is the hint that
pointed at a guide 40 px above it, and the second format for "not
configured". Connect is one button everywhere: primary, `sm`, in the action
row, instead of primary-and-small in three cards and outline-and-large in
four.

Plus the standards leftovers the audit listed: two `pl-5` list gutters, one
`scroll-mt-20` anchor, a purple header icon, `text-[10px]` badges, four
hand-rolled callout shells that `SettingsInfoTile` already paints, and the
two inline buttons that never had the mobile tap floor at all.
2026-08-07 19:39:11 +02:00

139 lines
4.8 KiB
TypeScript

"use client";
/**
* v1.17.1 — shared "Setup guide" doc-link for every Settings → Integrations
* card. Each card paints the same affordance: one discreet, external link to
* the provider's setup runbook ("was eingeben, wo klicken"). Single-sourced
* here so all six cards (WHOOP / Withings / Fitbit / Polar / Oura / Nightscout)
* read as one family and the docs host lives in exactly one place.
*
* The runbooks themselves are authored separately; this link wires the
* destination now so a user who is mid-setup always knows where to go.
*/
import { ExternalLink } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
/**
* Base for every provider setup runbook. The provider key is appended as a
* path segment, e.g. `https://docs.healthlog.dev/integrations/whoop`. Kept as
* a single constant so the host never drifts across cards.
*/
export const INTEGRATION_DOCS_BASE = "https://docs.healthlog.dev/integrations";
export type IntegrationDocsProvider =
| "whoop"
| "withings"
| "fitbit"
| "polar"
| "oura"
| "nightscout"
// v1.27.0 — Google Health runbook (docs.healthlog.dev/integrations/google-health).
| "google-health"
// v1.28.x — Strava runbook (docs.healthlog.dev/integrations/strava).
| "strava";
export function integrationDocsHref(provider: IntegrationDocsProvider): string {
return `${INTEGRATION_DOCS_BASE}/${provider}`;
}
/**
* The OAuth callback path every BYO-key provider registers against. Every
* provider's `getRedirectUri()` (`src/lib/{provider}/client.ts`) derives the
* same shape from `NEXT_PUBLIC_APP_URL` unless an operator overrides it with
* an explicit `*_REDIRECT_URI` env var — the common case this card targets.
*/
export function integrationCallbackUrl(
provider: IntegrationDocsProvider,
): string {
const base = process.env.NEXT_PUBLIC_APP_URL ?? "";
return `${base}/api/${provider}/callback`;
}
/**
* v1.29.x (UX audit H2) — compact 3-step BYO-developer-app guide for the
* six providers that require a self-hoster to register their own vendor
* app before an OAuth connect can ever succeed (Withings / WHOOP / Fitbit /
* Polar / Oura / Strava). The external "Setup guide" link explains the
* *why*; this inline callout gives the one value every vendor form asks
* for and the app already knows — the callback URL — so it doesn't have to
* be reconstructed by hand from the docs. Shown only while the user hasn't
* saved their own credentials yet; it steps out of the way once configured.
*/
export function IntegrationRedirectGuide({
provider,
providerLabel,
}: {
provider: IntegrationDocsProvider;
providerLabel: string;
}) {
const { t } = useTranslations();
const callbackUrl = integrationCallbackUrl(provider);
return (
<div
className="bg-muted/40 border-border/60 space-y-1.5 rounded-md border p-3 text-xs"
data-testid={`${provider}-redirect-guide`}
>
<p className="text-muted-foreground">
{t("settings.integrationRedirectGuide.intro", {
provider: providerLabel,
})}
</p>
<ol className="text-muted-foreground list-decimal space-y-1 pl-4">
<li>
{t("settings.integrationRedirectGuide.step1", {
provider: providerLabel,
})}
</li>
<li>
{t("settings.integrationRedirectGuide.step2")}{" "}
<code
className="bg-background text-foreground rounded px-1 py-0.5 font-mono break-all"
data-testid={`${provider}-redirect-uri`}
>
{callbackUrl}
</code>
</li>
<li>{t("settings.integrationRedirectGuide.step3")}</li>
</ol>
</div>
);
}
/**
* The one description every integration card paints: ONE sentence, then the
* setup-guide link inline at the end of it.
*
* It used to be two paragraphs stacked in the description slot — a white
* primary sentence and a grey secondary one whose only job was to carry the
* link. The secondary sentence was byte-identical across six of the eight
* cards, which is the tell: it said nothing about the provider it sat under.
* The link is the part that carried information, so the link is what stayed.
*/
export function IntegrationCardDescription({
i18nPrefix,
provider,
}: {
i18nPrefix: string;
provider: IntegrationDocsProvider;
}) {
const { t } = useTranslations();
return (
<>
{t(`${i18nPrefix}Description`)}{" "}
<a
href={integrationDocsHref(provider)}
target="_blank"
rel="noopener noreferrer"
data-testid={`${provider}-setup-guide`}
data-slot="integration-setup-guide"
className="hover:text-foreground inline-flex items-center gap-1 underline underline-offset-2"
>
{t("settings.integrationSetupGuide")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
</>
);
}