Simpl-Resultat/src/components/reports/HubReportNavCard.tsx
le king fu 553da0ce8c feat(gating): gate routes and Sidebar for budget, advanced reports, balance
Apply the tier gating to routes and navigation on top of the #298 UI
guard:

- App.tsx: pathless RequireFeature layout-routes grouping /balance,
  /balance/accounts, /balance/snapshot under "balance"; /reports/
  highlights|compare|category|cartes under "reports-advanced"; /budget
  under "budget"; /adjustments under "adjustments". The /reports hub and
  /reports/trends stay Free and ungated.
- NavItem gains an optional `feature?: FeatureKey`; set in NAV_ITEMS on
  budget, adjustments and balance only — NOT on reports (Free hub).
- Sidebar: local NavLock child component (hook at component top level)
  renders a lock badge only when the license is ready AND the feature is
  not allowed — no locked flash at boot; items stay clickable and lead
  to the upsell via the gated route. Tooltip/aria reuse nav.locked.
- ReportsPage hub: single useEntitlement("reports-advanced") call
  drives a `locked` badge on the 4 advanced tiles via a new additive
  HubReportNavCard `locked?` prop; the Trends tile is never locked.
- Pure contract test on NAV_ITEMS (gated trio present, reports/Free
  items ungated, exactly 3 of 9 gated).

No new i18n strings (nav.locked shipped with #298), no DB migration.
Changelog centralized in #302.

Resolves #299

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 22:12:51 -04:00

49 lines
1.4 KiB
TypeScript

import type { ReactNode } from "react";
import { Link } from "react-router-dom";
import { Lock } from "lucide-react";
import { useTranslation } from "react-i18next";
export interface HubReportNavCardProps {
to: string;
icon: ReactNode;
title: string;
description: string;
/**
* Show a lock badge on the card (gated feature, license ready and not
* entitled). The card stays clickable — the gated route renders the upsell.
*/
locked?: boolean;
}
export default function HubReportNavCard({
to,
icon,
title,
description,
locked,
}: HubReportNavCardProps) {
const { t } = useTranslation();
return (
<Link
to={to}
className="relative group bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 flex flex-col gap-2 hover:border-[var(--primary)] hover:shadow-sm transition-all"
>
{locked && (
<span
className="absolute top-4 right-4 text-[var(--muted-foreground)]"
title={t("nav.locked")}
aria-label={t("nav.locked")}
role="img"
>
<Lock size={16} aria-hidden="true" />
</span>
)}
<div className="text-[var(--primary)]">{icon}</div>
<h3 className="text-base font-semibold text-[var(--foreground)] group-hover:text-[var(--primary)]">
{title}
</h3>
<p className="text-sm text-[var(--muted-foreground)]">{description}</p>
</Link>
);
}