"use client"; import { useEffect, useRef, useState } from "react"; import Link from "next/link"; type Preview = { slug: string; name: string; bio: string; logoUrl: string | null; website: string | null; hq: string; featured: boolean; exhibitsNab: boolean; exhibitsIbc: boolean; categories: string[]; isActiveAdvertiser: boolean; stories: { slug: string; title: string; date: string | null; image: string | null }[]; }; const cache = new Map>(); async function fetchPreview(slug: string): Promise { if (cache.has(slug)) return cache.get(slug)!; const p = (async () => { try { const r = await fetch(`/api/public/companies/${encodeURIComponent(slug)}/preview`); if (!r.ok) return null; return (await r.json()) as Preview; } catch { return null; } })(); cache.set(slug, p); return p; } /** * Mount once per page. Listens for hover/focus on any [data-company-slug] * element, fetches preview data, and pops a floating card. */ export default function CompanyMentionsHover() { const [anchor, setAnchor] = useState(null); const [preview, setPreview] = useState(null); const [loading, setLoading] = useState(false); const [pos, setPos] = useState<{ top: number; left: number; align: "left" | "right" } | null>(null); const hideTimer = useRef(null); const showTimer = useRef(null); useEffect(() => { function onEnter(e: MouseEvent | FocusEvent) { const t = (e.target as HTMLElement | null)?.closest?.("[data-company-slug]") as HTMLElement | null; if (!t) return; if (hideTimer.current) { window.clearTimeout(hideTimer.current); hideTimer.current = null; } if (showTimer.current) window.clearTimeout(showTimer.current); showTimer.current = window.setTimeout(async () => { const slug = t.getAttribute("data-company-slug"); if (!slug) return; setAnchor(t); setLoading(true); const data = await fetchPreview(slug); setLoading(false); if (!data) { setAnchor(null); return; } setPreview(data); const rect = t.getBoundingClientRect(); const cardWidth = 360; const fitsRight = rect.left + cardWidth < window.innerWidth - 12; setPos({ top: rect.bottom + window.scrollY + 6, left: fitsRight ? rect.left + window.scrollX : Math.max(12, rect.right + window.scrollX - cardWidth), align: fitsRight ? "left" : "right", }); }, 220); } function onLeave(e: MouseEvent | FocusEvent) { const related = (e as MouseEvent).relatedTarget as HTMLElement | null; if (related && (related.closest?.("[data-company-card]") || related.closest?.("[data-company-slug]"))) return; if (showTimer.current) { window.clearTimeout(showTimer.current); showTimer.current = null; } if (hideTimer.current) window.clearTimeout(hideTimer.current); hideTimer.current = window.setTimeout(() => { setAnchor(null); setPreview(null); setPos(null); }, 180); } document.addEventListener("mouseover", onEnter, true); document.addEventListener("mouseout", onLeave, true); document.addEventListener("focusin", onEnter, true); document.addEventListener("focusout", onLeave, true); return () => { document.removeEventListener("mouseover", onEnter, true); document.removeEventListener("mouseout", onLeave, true); document.removeEventListener("focusin", onEnter, true); document.removeEventListener("focusout", onLeave, true); }; }, []); if (!anchor || !pos) return null; return (
{ if (hideTimer.current) { window.clearTimeout(hideTimer.current); hideTimer.current = null; } }} onMouseLeave={() => { hideTimer.current = window.setTimeout(() => { setAnchor(null); setPreview(null); setPos(null); }, 180); }} > {loading || !preview ? (
Loading…
) : ( <>
{preview.logoUrl ? ( /* eslint-disable-next-line @next/next/no-img-element */ ) : (
{(preview.name || "?").trim().charAt(0).toUpperCase()}
)}
{preview.name}
{preview.isActiveAdvertiser && ( Sponsor )} {preview.exhibitsNab && ( 2026 NAB Show )} {preview.exhibitsIbc && ( IBC 2026 )} {preview.hq && {preview.hq}}
{preview.bio && (

{preview.bio}

)} {preview.stories.length > 0 && (
Recent coverage
    {preview.stories.slice(0, 3).map((s) => (
  • {s.title} {s.date && · {new Date(s.date).toISOString().slice(0, 10)}}
  • ))}
)} )}
); }