chore(insights): remove @deprecated InsightsPageHero (zero callers since v1.4.20)

`<HeroStrip>` (v1.4.20 phase B1) replaced this component on every
surface. Last production import was retired with the dashboard rewrite
and only the deprecated source + its self-test remained. Comment
markers in `hero-strip.tsx` already note the migration.

Pre-existing typecheck failures in the working tree (locale enum drift
from concurrent W9e i18n work in medication-card / medication-form /
target-card) are unrelated to this delete and stay as-is.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 15:29:57 +02:00
1 parent 42d88fa5a8
commit 1149ae5021
2 files changed
-277

No files matched your search

@@ -1,107 +0,0 @@
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { InsightsPageHero } from "../insights-page-hero";
/**
* v1.4.16 phase B1b — Apple-Health-style page hero.
*
* The /insights page entry-point gets a gradient hero shell:
* - h1 + subtitle (kept from the old layout, just polished)
* - "Generated <relative-time>" caption when an updatedAt is supplied
* - regenerate button when an onRegenerate handler is supplied
* - "Based on your last 90 days" personal-baseline indicator
* - smooth fade-in (CSS animation, gated by prefers-reduced-motion)
*
* The hero is a pure presentational shell; the page passes the values
* in. This test pins the slot positions so a future polish pass can't
* silently drop one (e.g. dropping the personal-baseline caption).
*/
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
);
}
describe("<InsightsPageHero>", () => {
it("renders the heading + overview subtitle", () => {
const html = render(<InsightsPageHero />);
expect(html).toContain("Insights");
expect(html).toContain("Trends, risks, progress at a glance");
});
it("paints a gradient background using Dracula tokens", () => {
const html = render(<InsightsPageHero />);
// The hero wrapper should carry a gradient + Dracula tokens so the
// page header reads as the Apple-Health-style band.
expect(html).toMatch(/data-slot="insights-page-hero"/);
expect(html).toMatch(/bg-gradient-to/);
});
it("renders the personal-baseline caption", () => {
const html = render(<InsightsPageHero />);
expect(html).toContain("Based on your last 90 days");
});
it("renders the German personal-baseline caption", () => {
const html = render(<InsightsPageHero />, "de");
expect(html).toContain("Basierend auf deinen letzten 90 Tagen");
});
it("renders the generated-time caption when updatedAt is supplied", () => {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000).toISOString();
const html = render(<InsightsPageHero updatedAt={fiveMinutesAgo} />);
// Either "just now" / "5 minutes ago" / "vor 5 Minuten" — we just
// assert the "Generated" label preceeds the relative-time string so
// a future Intl.RelativeTimeFormat tweak can't dodge the slot.
expect(html).toMatch(/data-slot="insights-page-hero-generated"/);
expect(html).toContain("Generated");
});
it("does NOT render the generated caption when updatedAt is missing", () => {
const html = render(<InsightsPageHero />);
expect(html).not.toContain('data-slot="insights-page-hero-generated"');
});
it("renders a regenerate button when onRegenerate is supplied", () => {
// The button is a real <button>, so SSR includes the markup verbatim.
const html = render(<InsightsPageHero onRegenerate={() => {}} />);
expect(html).toMatch(/data-slot="insights-page-hero-regenerate"/);
});
it("does NOT render the regenerate button when onRegenerate is missing", () => {
const html = render(<InsightsPageHero />);
expect(html).not.toContain('data-slot="insights-page-hero-regenerate"');
});
it("disables the regenerate button while regenerating", () => {
const html = render(
<InsightsPageHero onRegenerate={() => {}} regenerating />,
);
expect(html).toMatch(
/data-slot="insights-page-hero-regenerate"[^>]*disabled/,
);
});
it("German locale — heading + subtitle translate", () => {
const html = render(<InsightsPageHero />, "de");
// Reuses existing insights.title + insights.overviewSubtitle keys.
// The "Insights" string is identical in DE so we assert the subtitle.
expect(html).toContain("Trends, Risiken, Fortschritt auf einen Blick");
});
// ── B1b acceptance #6 — dark-mode contrast hygiene ────────────────
it("uses high-enough gradient + border opacity to remain visible on dark bg", () => {
const html = render(<InsightsPageHero />);
// Border opacity must be >= 20% so it reads against the dark
// background — `border-dracula-purple/20` or higher passes the
// 3:1 contrast bar for a non-text UI element.
expect(html).toMatch(/border-dracula-purple\/(2[0-9]|[3-9][0-9])/);
// Gradient opacity must be at least /10 (we ship /10 + /5 on the
// band; the cumulative effect on a dark bg renders as ~12% which
// is the lower bound for visible band-vs-page differentiation).
expect(html).toMatch(/from-dracula-purple\/(1[0-9]|[2-9][0-9])/);
});
});
@@ -1,170 +0,0 @@
"use client";
import { Loader2, RefreshCw, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.4.16 phase B1b — Apple-Health-style page hero for `/insights`.
*
* @deprecated v1.4.20 phase B1 — superseded by `<HeroStrip>` in
* `./hero-strip.tsx`. Kept here for the dashboard preview's continued
* use; will be removed once that surface migrates.
*
* Polished header band that frames the page below. Pure presentational —
* the page passes timestamps + the regenerate handler in. Stays a thin
* shell around the existing translation keys so a future redesign only
* touches this file.
*
* Visual language:
* - subtle Dracula gradient background (purple → cyan, low opacity so
* dark-mode contrast is preserved against `var(--background)`)
* - sparkles glyph + heading + overview subtitle on the left
* - meta row underneath with personal-baseline indicator + the
* "Generated <relative-time>" caption when an updatedAt is supplied
* - regenerate button on the right (top-aligned on desktop, stacked
* under the meta row on mobile)
* - smooth fade-in via the existing `animate-insight-in` keyframes;
* `prefers-reduced-motion: reduce` already disables that animation
* globally (see globals.css §Insight card animations).
*
* Layout slots:
* - `data-slot="insights-page-hero"` — outer wrapper
* - `data-slot="insights-page-hero-generated"` — "Generated <time>"
* - `data-slot="insights-page-hero-baseline"` — "Based on …"
* - `data-slot="insights-page-hero-regenerate"` — regenerate button
* - `data-slot="insights-page-hero-meta-slot"` — v1.4.19 A3 page-
* level meta slot
* (comparison toggle)
*/
interface InsightsPageHeroProps {
/** ISO-8601 timestamp of the last AI generation; controls the "Generated …" caption. */
updatedAt?: string | null;
/** Click handler for the regenerate button; omit to hide the control. */
onRegenerate?: () => void;
/** Disables the regenerate button + flips the icon to a spinner. */
regenerating?: boolean;
/**
* v1.4.19 A3 — page-level metadata slot. The comparison-baseline
* toggle is mounted into the hero meta row (next to the personal-
* baseline indicator + the "Generated …" caption) instead of
* standing alone above the advisor card. Keeps every page-level
* control in one band so the surface stops feeling like three
* stacked cards with separate refresh / toggle / header
* affordances.
*/
metaSlot?: React.ReactNode;
}
/**
* Bucketed relative-time using i18n translation keys so every surface
* shares vocabulary. Buckets: <1m, <1h, <24h, else days.
*/
function formatRelativeTime(
iso: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const target = new Date(iso).getTime();
if (Number.isNaN(target)) return "";
const diffMs = Date.now() - target;
if (diffMs < 60_000) return t("insights.relativeJustNow");
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
const days = Math.floor(hours / 24);
return t("insights.relativeDaysAgo", { count: days });
}
export function InsightsPageHero({
updatedAt,
onRegenerate,
regenerating = false,
metaSlot,
}: InsightsPageHeroProps) {
const { t } = useTranslations();
const generatedLine = updatedAt
? t("insights.heroGenerated", { time: formatRelativeTime(updatedAt, t) })
: null;
return (
<div
data-slot="insights-page-hero"
className="border-dracula-purple/25 from-dracula-purple/15 via-dracula-cyan/8 animate-insight-in relative overflow-hidden rounded-xl border bg-gradient-to-br to-transparent px-4 py-5 sm:px-6 sm:py-6"
>
{/* Decorative sparkle glow — purely visual, kept inert for screen readers. */}
<div
aria-hidden="true"
className="bg-dracula-purple/10 pointer-events-none absolute -top-12 -right-12 h-32 w-32 rounded-full blur-3xl"
/>
<div
aria-hidden="true"
className="bg-dracula-cyan/10 pointer-events-none absolute -bottom-16 -left-10 h-36 w-36 rounded-full blur-3xl"
/>
<div className="relative flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-center gap-2">
<Sparkles
className="text-dracula-purple h-5 w-5 shrink-0"
aria-hidden="true"
/>
<h1 className="text-2xl font-bold tracking-tight">
{t("insights.title")}
</h1>
</div>
<p className="text-muted-foreground text-sm leading-snug">
{t("insights.overviewSubtitle")}
</p>
<div className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 pt-1 text-xs">
<span data-slot="insights-page-hero-baseline">
{t("insights.heroPersonalBaseline")}
</span>
{generatedLine && (
<>
<span aria-hidden="true" className="opacity-50">
·
</span>
<span data-slot="insights-page-hero-generated">
{generatedLine}
</span>
</>
)}
</div>
{metaSlot && (
<div
data-slot="insights-page-hero-meta-slot"
className="flex flex-wrap items-center gap-2 pt-2"
>
{metaSlot}
</div>
)}
</div>
{onRegenerate && (
<div className="flex shrink-0 items-start">
<Button
type="button"
variant="outline"
size="sm"
onClick={onRegenerate}
disabled={regenerating}
data-slot="insights-page-hero-regenerate"
className="gap-1.5"
>
{regenerating ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
<span>
{regenerating
? t("insights.heroRegenerating")
: t("insights.heroRegenerate")}
</span>
</Button>
</div>
)}
</div>
</div>
);
}