import { useEffect, useState } from "react"; import { Link } from "@tanstack/react-router"; import { buttonClass } from "~/ui/Button"; import { TONE_SURFACE } from "~/ui/tones"; import type { Tone } from "~/ui/tones"; import { cn } from "~/utils/cn"; const DAY_MS = 24 * 60 * 60 * 1000; const PREFIX = "gitfor:nag:"; type Action = { label: string; to: string } | { label: string; onClick: () => void }; type Props = { /** Stable id; snoozing is remembered per id in this browser. */ id: string; tone?: Tone; title: React.ReactNode; children?: React.ReactNode; action?: Action; secondary?: Action; /** Days "Remind me later" hides it for; 0 hides the button and keeps the banner until resolved. */ snoozeDays?: number; }; function snoozedUntil(id: string): number { try { return Number(localStorage.getItem(PREFIX + id) ?? 0); } catch { return 0; } } function ActionButton({ action, primary }: { action: Action; primary: boolean }) { const className = buttonClass(primary ? "primary" : "secondary", "sm"); if ("to" in action) { return ( {action.label} ); } return ( {action.label} ); } /** * A persistent prompt across the top of the app (plan maxed out, set up two-factor…). Renders only after * mount, so a snoozed banner never flashes and server and client markup always agree. */ export function NagBanner({ id, tone = "warning", title, children, action, secondary, snoozeDays = 3 }: Props) { const [visible, setVisible] = useState(false); useEffect(() => setVisible(snoozedUntil(id) < Date.now()), [id]); if (!visible) return null; const snooze = () => { try { localStorage.setItem(PREFIX + id, String(Date.now() + snoozeDays * DAY_MS)); } catch { // Storage blocked: hide for this page only. } setVisible(false); }; return ( ); }