import { XMarkIcon } from "@heroicons/react/20/solid"; import { useLayoutEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import { cn } from "~/utils/cn"; export function NotificationCard({ title, description, image, actionUrl, onDismiss, onCardClick, onLinkClick, }: { title: string; description: string; image?: string; actionUrl?: string; onDismiss?: () => void; onCardClick?: () => void; onLinkClick?: () => void; }) { const [isExpanded, setIsExpanded] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false); const descriptionRef = useRef(null); useLayoutEffect(() => { const el = descriptionRef.current; if (!el) return; const check = () => setIsOverflowing(el.scrollHeight - el.clientHeight > 1); check(); const observer = new ResizeObserver(check); observer.observe(el); return () => observer.disconnect(); }, [description]); const handleDismiss = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onDismiss?.(); }; const handleToggleExpand = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setIsExpanded((v) => !v); }; const safeActionUrl = sanitizeUrl(actionUrl); const safeImage = sanitizeUrl(image); return (
{safeActionUrl && ( )}

{title}

{description}
{(isOverflowing || isExpanded) && ( )} {safeImage && }
); } function getMarkdownComponents(onLinkClick?: () => void) { return { p: ({ children }: { children?: React.ReactNode }) => (

{children}

), a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
{ e.stopPropagation(); onLinkClick?.(); }} > {children} ), strong: ({ children }: { children?: React.ReactNode }) => ( {children} ), em: ({ children }: { children?: React.ReactNode }) => {children}, code: ({ children }: { children?: React.ReactNode }) => ( {children} ), }; } const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:"]); /** Sanitize a URL to prevent XSS via javascript: or data: URIs. Returns "" if invalid. */ function sanitizeUrl(url: string | undefined): string { if (!url) return ""; try { const parsed = new URL(url); return SAFE_URL_PROTOCOLS.has(parsed.protocol) ? parsed.href : ""; } catch { return ""; } }