diff --git a/src/app/insights/scores/[metric]/page.tsx b/src/app/insights/scores/[metric]/page.tsx index 3e3c82932..fb417c319 100644 --- a/src/app/insights/scores/[metric]/page.tsx +++ b/src/app/insights/scores/[metric]/page.tsx @@ -1,11 +1,9 @@ "use client"; -import Link from "next/link"; import { useParams } from "next/navigation"; -import { ArrowLeft } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; -import { Button } from "@/components/ui/button"; +import { BackLink } from "@/components/ui/back-link"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { CompositeScoreAnatomy, @@ -88,18 +86,11 @@ export default function CompositeScorePage() { description={t(header.description)} explainerMetric={EXPLAINER_METRIC[metric]} backLink={ - + } coachLaunch > diff --git a/src/app/insights/values/[type]/page.tsx b/src/app/insights/values/[type]/page.tsx index ca7c75b6d..e54f9868c 100644 --- a/src/app/insights/values/[type]/page.tsx +++ b/src/app/insights/values/[type]/page.tsx @@ -1,9 +1,7 @@ "use client"; import { use } from "react"; -import Link from "next/link"; import { notFound, useSearchParams } from "next/navigation"; -import { ArrowLeft } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { @@ -12,7 +10,7 @@ import { } from "@/lib/insights/values-back-link"; import { measurementTypeEnum } from "@/lib/validations/measurement"; import { MEASUREMENT_TYPE_LABEL_KEYS } from "@/components/measurements/measurement-list-meta"; -import { Button } from "@/components/ui/button"; +import { BackLink } from "@/components/ui/back-link"; import { MeasurementList } from "@/components/measurements/measurement-list"; import { SubPageShell } from "@/components/insights/sub-page-shell"; @@ -63,22 +61,17 @@ export default function InsightsMetricValuesPage({ title={t("insights.subPage.valuesPageTitle", { metric: metricLabel })} description={t("insights.subPage.valuesPageDescription")} backLink={ - + : t("insights.subPage.valuesBack") + } + dataSlot="metric-values-back" + /> } > diff --git a/src/app/insights/workouts/[id]/page.tsx b/src/app/insights/workouts/[id]/page.tsx index 74a81adf6..627a1e2f5 100644 --- a/src/app/insights/workouts/[id]/page.tsx +++ b/src/app/insights/workouts/[id]/page.tsx @@ -1,12 +1,10 @@ "use client"; import { use } from "react"; -import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; import { useTranslations } from "@/lib/i18n/context"; import { useWorkoutDetail } from "@/hooks/use-workouts"; -import { Button } from "@/components/ui/button"; +import { BackLink } from "@/components/ui/back-link"; import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { Skeleton } from "@/components/ui/skeleton"; @@ -41,18 +39,11 @@ export default function InsightsWorkoutDetailPage({ title={t("insights.workouts.title")} description={t("insights.workouts.description")} backLink={ - + } > {isLoading ? ( diff --git a/src/app/medications/[id]/page.tsx b/src/app/medications/[id]/page.tsx index b8f898195..ffe82a7be 100644 --- a/src/app/medications/[id]/page.tsx +++ b/src/app/medications/[id]/page.tsx @@ -13,16 +13,15 @@ */ import { use, useEffect } from "react"; -import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; -import { ArrowLeft, Loader2 } from "lucide-react"; +import { Loader2 } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; -import { Button } from "@/components/ui/button"; +import { BackLink } from "@/components/ui/back-link"; import { Card } from "@/components/ui/card"; import { MedicationDetailTabs, @@ -87,12 +86,7 @@ export default function MedicationDetailPage({ if (isError || !medication) { return (
- + ", () => { + it("renders an anchor to the href carrying the label", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain('href="/insights/workouts"'); + expect(html).toContain("Back to workouts"); + // The decorative chevron is hidden from screen readers; the label + // carries the meaning. + expect(html).toContain('aria-hidden="true"'); + }); + + it("keeps the shared left-bleed alignment on the control", () => { + const html = renderToStaticMarkup( + , + ); + // The `-ml-2 w-fit` left-bleed is the standard back-nav placement; it + // must survive on every adopting page. + expect(html).toContain("-ml-2"); + expect(html).toContain("w-fit"); + }); + + it("threads the optional data-slot for e2e/visual targeting", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain('data-slot="composite-score-back"'); + }); +}); diff --git a/src/components/ui/back-link.tsx b/src/components/ui/back-link.tsx new file mode 100644 index 000000000..41c71bd25 --- /dev/null +++ b/src/components/ui/back-link.tsx @@ -0,0 +1,47 @@ +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +/** + * v1.16.16 — shared back-navigation control. + * + * The detail / sub-page surfaces (`/insights/workouts/[id]`, + * `/insights/values/[type]`, `/insights/scores/[metric]`, + * `/medications/[id]`) each hand-rolled the identical + * `Button(ghost, sm, -ml-2 w-fit) > Link > ArrowLeft + label` block. This + * folds that into one component so the back-nav reads the same — same hit + * area, same icon, same left-bleed alignment — on every page, and a future + * change to the affordance lives in one place. + * + * The label travels in from the caller (each page already owns its own + * translation key, e.g. `insights.workouts.detail.backToList`), so this + * component adds no new i18n surface. + */ +export interface BackLinkProps { + /** Internal href the back-nav points at. */ + href: string; + /** Already-translated label text. */ + label: string; + /** Optional per-surface `data-slot` for e2e/visual targeting. */ + dataSlot?: string; + className?: string; +} + +export function BackLink({ href, label, dataSlot, className }: BackLinkProps) { + return ( + + ); +}