mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(insights): one CrossLinkCard for the card-sized page pointers
The VO2 max pointer on the pulse page and the ECG pointer on the heart pages were two hand-rolled link cards; the ECG one had no md:p-6 step and sat narrower than every card beside it. CrossLinkCard composes Card (TileHeader, one meta line, the call to action) inside the link that carries the focus ring.
This commit is contained in:
1 parent
e7ee416e19
commit
f2af0c9650
3 files changed
+94
-45
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Gauge, Heart } from "lucide-react";
|
||||
import { Gauge, Heart } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
|
||||
@@ -19,9 +19,9 @@ import { MeasurementDiversityNudge } from "@/components/insights/measurement-div
|
||||
import { MetricTargetSummary } from "@/components/insights/metric-target-summary";
|
||||
import { EcgCrossLink } from "@/components/insights/ecg-cross-link";
|
||||
import { SubPageShell } from "@/components/insights/sub-page-shell";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { CrossLinkCard } from "@/components/insights/cross-link-card";
|
||||
|
||||
// Load the intraday area chart through the shared chart-runtime boundary (the
|
||||
// one recharts chunk), not as a direct import — a direct `from "recharts"`
|
||||
@@ -199,27 +199,17 @@ export default function InsightsPulsPage() {
|
||||
the full chart plus the plain-language assessment — rather than
|
||||
duplicating a second, divergent VO₂ max view here.
|
||||
|
||||
v1.12.8 — the card now leads with the canonical `<TileHeader>` so the
|
||||
`Gauge` glyph reads in the foreground colour at the same size and
|
||||
position as every other tile header, with the body + CTA beneath. */}
|
||||
The card is the shared `CrossLinkCard`: the canonical `TileHeader`
|
||||
(foreground `Gauge` glyph), the body and the CTA beneath. */}
|
||||
{hasVo2 ? (
|
||||
<Link
|
||||
<CrossLinkCard
|
||||
href="/insights/cardio-fitness"
|
||||
data-slot="vo2-cardio-link"
|
||||
className="bg-card hover:bg-accent/40 focus-visible:ring-ring/50 block space-y-1.5 rounded-xl border p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none md:p-6"
|
||||
>
|
||||
<TileHeader
|
||||
icon={Gauge}
|
||||
title={t("insights.vo2Max.cardioLinkTitle")}
|
||||
/>
|
||||
<span className="text-muted-foreground block text-xs leading-snug">
|
||||
{t("insights.vo2Max.cardioLinkBody")}
|
||||
</span>
|
||||
<span className="text-primary inline-flex shrink-0 items-center gap-1 text-xs font-medium">
|
||||
{t("insights.vo2Max.cardioLinkCta")}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</Link>
|
||||
slot="vo2-cardio-link"
|
||||
icon={Gauge}
|
||||
title={t("insights.vo2Max.cardioLinkTitle")}
|
||||
body={t("insights.vo2Max.cardioLinkBody")}
|
||||
cta={t("insights.vo2Max.cardioLinkCta")}
|
||||
/>
|
||||
) : null}
|
||||
</SubPageShell>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, type LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* A whole card that points to another page: the tile header, one meta line,
|
||||
* and the call to action. The pulse page's way to VO₂ max and the ECG pointer
|
||||
* on the heart pages were two hand-rolled copies of it, one of them without
|
||||
* the `md:p-6` step every other card on the page has.
|
||||
*
|
||||
* The link is the outer element and carries the focus ring; the surface is a
|
||||
* `Card`, so padding, radius and shadow are the card contract. The compact
|
||||
* card rhythm (`gap-1.5`) keeps the three lines together.
|
||||
*/
|
||||
export function CrossLinkCard({
|
||||
href,
|
||||
slot,
|
||||
icon,
|
||||
title,
|
||||
body,
|
||||
cta,
|
||||
className,
|
||||
cardClassName,
|
||||
style,
|
||||
}: {
|
||||
href: string;
|
||||
slot: string;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
body: string;
|
||||
cta: string;
|
||||
className?: string;
|
||||
/** Classes on the card surface (an identity edge such as `metric-accent`). */
|
||||
cardClassName?: string;
|
||||
style?: CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
data-slot={slot}
|
||||
className={cn(
|
||||
"group focus-visible:ring-ring/50 block rounded-xl focus-visible:ring-2 focus-visible:outline-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
"group-hover:bg-accent/40 gap-1.5 px-4 transition-colors md:gap-1.5 md:px-6",
|
||||
cardClassName,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<TileHeader icon={icon} title={title} />
|
||||
<span className="text-muted-foreground block text-xs leading-snug">
|
||||
{body}
|
||||
</span>
|
||||
<span className="text-primary inline-flex shrink-0 items-center gap-1 text-xs font-medium">
|
||||
{cta}
|
||||
<ArrowRight className="size-4" aria-hidden="true" />
|
||||
</span>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, HeartPulse } from "lucide-react";
|
||||
import { HeartPulse } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { CrossLinkCard } from "@/components/insights/cross-link-card";
|
||||
import { useEcgList } from "./use-ecg-list";
|
||||
|
||||
/**
|
||||
@@ -61,27 +59,20 @@ export function EcgCrossLink({ enabled = true, className }: EcgCrossLinkProps) {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
<CrossLinkCard
|
||||
href="/insights/ecg"
|
||||
data-slot="ecg-cross-link"
|
||||
className={cn(
|
||||
// `.metric-accent` — the heart-family identity edge (`--tile-strain`,
|
||||
// the wellness vocabulary's cardiovascular hue), the same mark the
|
||||
// `ecg_new_recording` rail card carries, so the two ECG pointers
|
||||
// read as one family.
|
||||
"bg-card hover:bg-accent/40 focus-visible:ring-ring/50 metric-accent block space-y-1.5 rounded-xl border p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none",
|
||||
className,
|
||||
)}
|
||||
slot="ecg-cross-link"
|
||||
icon={HeartPulse}
|
||||
title={t("insights.ecg.crossLink.title")}
|
||||
body={resultText ? `${countText} ${resultText}` : countText}
|
||||
cta={t("insights.ecg.crossLink.cta")}
|
||||
className={className}
|
||||
// `.metric-accent` — the heart-family identity edge (`--tile-strain`,
|
||||
// the wellness vocabulary's cardiovascular hue), the same mark the
|
||||
// `ecg_new_recording` rail card carries, so the two ECG pointers read
|
||||
// as one family.
|
||||
cardClassName="metric-accent"
|
||||
style={{ "--tile-hue": "var(--tile-strain)" } as React.CSSProperties}
|
||||
>
|
||||
<TileHeader icon={HeartPulse} title={t("insights.ecg.crossLink.title")} />
|
||||
<span className="text-muted-foreground block text-xs leading-snug">
|
||||
{resultText ? `${countText} ${resultText}` : countText}
|
||||
</span>
|
||||
<span className="text-primary inline-flex shrink-0 items-center gap-1 text-xs font-medium">
|
||||
{t("insights.ecg.crossLink.cta")}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</Link>
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user