mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user