mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
refactor(settings): unify the integration card skeleton
Every Settings → Integrations block now paints one consistent shape: heading (icon + name + status pill) → a white primary description → a grey secondary description that ends with the setup-guide link rendered inline, instead of a standalone bottom-of-card element. OAuth-only (WHOOP/Fitbit), BYO-credential (Withings/Oura/Polar) and manual-key (Nightscout) cards share the same skeleton via the new IntegrationCardDescription helper; the credential form stays as an extra block where it applies. The "where to find these credentials" hint moves above the Save button instead of sitting below it. Rewrite the per-provider description strings across all six locales into a parallel two-part shape with even length, and drop the now-orphaned overlap/experimental sub-notes and the standalone setup-guide link.
This commit is contained in:
1 parent
d35c7660ea
commit
aa092df9f0
12 files changed
+154
-104
No files matched your search
@@ -51,7 +51,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
import { IntegrationCardDescription } from "./setup-guide-link";
|
||||
|
||||
export function FitbitCard({
|
||||
viewModel,
|
||||
@@ -210,18 +210,10 @@ export function FitbitCard({
|
||||
</>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
<p>{t("settings.fitbitDescription")}</p>
|
||||
<p
|
||||
data-testid="fitbit-experimental-note"
|
||||
className="text-muted-foreground/80"
|
||||
>
|
||||
{t("settings.fitbitExperimentalNote")}
|
||||
</p>
|
||||
<p className="text-muted-foreground/80">
|
||||
{t("settings.fitbitOverlapNote")}
|
||||
</p>
|
||||
</>
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix="settings.fitbit"
|
||||
provider="fitbit"
|
||||
/>
|
||||
}
|
||||
status={
|
||||
<IntegrationStatusPill
|
||||
@@ -293,9 +285,6 @@ export function FitbitCard({
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t("settings.fitbitCredentials")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.fitbitCredentialsHelp")}
|
||||
</p>
|
||||
<form onSubmit={handleSaveCredentials} className="space-y-3">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
@@ -341,6 +330,9 @@ export function FitbitCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 text-xs">
|
||||
{t("settings.integrationCredentialsHint")}
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -485,8 +477,6 @@ export function FitbitCard({
|
||||
{t("settings.fitbitNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="fitbit" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { IntegrationErrorMessage } from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
import { IntegrationCardDescription } from "./setup-guide-link";
|
||||
|
||||
interface NightscoutStatus {
|
||||
connected: boolean;
|
||||
@@ -170,7 +170,12 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
|
||||
{t("settings.nightscoutTag")}
|
||||
</span>
|
||||
}
|
||||
description={<p>{t("settings.nightscoutDescription")}</p>}
|
||||
description={
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix="settings.nightscout"
|
||||
provider="nightscout"
|
||||
/>
|
||||
}
|
||||
status={
|
||||
<IntegrationStatusPill
|
||||
state={pillState}
|
||||
@@ -219,10 +224,6 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.nightscoutHelp")}
|
||||
</p>
|
||||
|
||||
<form ref={formRef} onSubmit={handleConnect} className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="nightscout-url">{t("settings.nightscoutUrl")}</Label>
|
||||
@@ -367,8 +368,6 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="nightscout" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,7 +41,10 @@ import {
|
||||
measurementDependentKeys,
|
||||
queryKeys,
|
||||
} from "@/lib/query-keys";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
import {
|
||||
IntegrationCardDescription,
|
||||
type IntegrationDocsProvider,
|
||||
} from "./setup-guide-link";
|
||||
|
||||
export interface OAuthProviderStatus {
|
||||
connected: boolean;
|
||||
@@ -205,7 +208,12 @@ export function OAuthProviderCard({
|
||||
<SettingsCardHeader
|
||||
icon={icon}
|
||||
title={t(`${i18nPrefix}`)}
|
||||
description={<p>{t(`${i18nPrefix}Description`)}</p>}
|
||||
description={
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix={i18nPrefix}
|
||||
provider={provider as IntegrationDocsProvider}
|
||||
/>
|
||||
}
|
||||
status={
|
||||
<IntegrationStatusPill
|
||||
state={pillState}
|
||||
@@ -257,18 +265,11 @@ export function OAuthProviderCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(`${i18nPrefix}Help`)}
|
||||
</p>
|
||||
|
||||
{credentials && (
|
||||
<div className="space-y-3" data-testid={`${provider}-credentials`}>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t(`${i18nPrefix}Credentials`)}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(`${i18nPrefix}CredentialsHelp`)}
|
||||
</p>
|
||||
<form onSubmit={handleSaveCredentials} className="space-y-3">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
@@ -314,6 +315,9 @@ export function OAuthProviderCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 text-xs">
|
||||
{t("settings.integrationCredentialsHint")}
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -424,8 +428,6 @@ export function OAuthProviderCard({
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider={provider} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,23 +35,38 @@ export function integrationDocsHref(provider: IntegrationDocsProvider): string {
|
||||
return `${INTEGRATION_DOCS_BASE}/${provider}`;
|
||||
}
|
||||
|
||||
export function IntegrationSetupGuideLink({
|
||||
/**
|
||||
* v1.18.7 (Wave F) — the one description skeleton every integration card paints:
|
||||
* a WHITE primary sentence (`${i18nPrefix}Description`), then a GREY secondary
|
||||
* sentence (`${i18nPrefix}DescriptionSecondary`) that ends with the setup-guide
|
||||
* link rendered INLINE at the very end of the grey copy — not a standalone
|
||||
* bottom-of-card element. Single-sourced so all six cards read as one family.
|
||||
*/
|
||||
export function IntegrationCardDescription({
|
||||
i18nPrefix,
|
||||
provider,
|
||||
}: {
|
||||
i18nPrefix: string;
|
||||
provider: IntegrationDocsProvider;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<a
|
||||
href={integrationDocsHref(provider)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid={`${provider}-setup-guide`}
|
||||
data-slot="integration-setup-guide"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("settings.integrationSetupGuide")}
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
<>
|
||||
<p className="text-foreground">{t(`${i18nPrefix}Description`)}</p>
|
||||
<p className="text-muted-foreground/80">
|
||||
{t(`${i18nPrefix}DescriptionSecondary`)}{" "}
|
||||
<a
|
||||
href={integrationDocsHref(provider)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid={`${provider}-setup-guide`}
|
||||
data-slot="integration-setup-guide"
|
||||
className="text-muted-foreground 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>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
import { IntegrationCardDescription } from "./setup-guide-link";
|
||||
|
||||
export function WhoopCard({
|
||||
viewModel,
|
||||
@@ -187,12 +187,10 @@ export function WhoopCard({
|
||||
icon={Activity}
|
||||
title={t("settings.whoop")}
|
||||
description={
|
||||
<>
|
||||
<p>{t("settings.whoopDescription")}</p>
|
||||
<p className="text-muted-foreground/80">
|
||||
{t("settings.whoopOverlapNote")}
|
||||
</p>
|
||||
</>
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix="settings.whoop"
|
||||
provider="whoop"
|
||||
/>
|
||||
}
|
||||
status={
|
||||
<IntegrationStatusPill
|
||||
@@ -265,9 +263,6 @@ export function WhoopCard({
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t("settings.whoopCredentials")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.whoopCredentialsHelp")}
|
||||
</p>
|
||||
<form onSubmit={handleSaveCredentials} className="space-y-3">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
@@ -313,6 +308,9 @@ export function WhoopCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 text-xs">
|
||||
{t("settings.integrationCredentialsHint")}
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -457,8 +455,6 @@ export function WhoopCard({
|
||||
{t("settings.whoopNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="whoop" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
import { IntegrationCardDescription } from "./setup-guide-link";
|
||||
|
||||
export function WithingsCard({
|
||||
viewModel,
|
||||
@@ -194,7 +194,12 @@ export function WithingsCard({
|
||||
<SettingsCardHeader
|
||||
icon={Link2}
|
||||
title={t("settings.withings")}
|
||||
description={t("settings.withingsDescription")}
|
||||
description={
|
||||
<IntegrationCardDescription
|
||||
i18nPrefix="settings.withings"
|
||||
provider="withings"
|
||||
/>
|
||||
}
|
||||
status={
|
||||
<IntegrationStatusPill
|
||||
state={pillState}
|
||||
@@ -347,6 +352,9 @@ export function WithingsCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 text-xs">
|
||||
{t("settings.integrationCredentialsHint")}
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -492,8 +500,6 @@ export function WithingsCard({
|
||||
{t("settings.withingsNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="withings" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user