import { useEffect, useRef, useState } from "react"; import { getHovercard } from "~/fns/hovercards"; const SHOW_DELAY_MS = 350; const HIDE_DELAY_MS = 200; const KINDS = ["issue", "user", "team", "commit"] as const; type Kind = (typeof KINDS)[number]; type Card = NonNullable>>; export type HovercardState = { card: Card; top: number; left: number } | null; function isKind(value: string | undefined): value is Kind { return KINDS.some((kind) => kind === value); } /** Watches the document for hovers over `a[data-hovercard]` links and loads preview cards (memoized per ref). */ export function useHovercards() { const [state, setState] = useState(null); const cache = useRef(new Map>()); const timer = useRef | undefined>(undefined); useEffect(() => { const target = (event: Event) => event.target instanceof Element ? event.target.closest("a[data-hovercard]") : null; const inPopup = (event: Event) => event.target instanceof Element && event.target.closest("[data-hovercard-popup]") !== null; const over = (event: MouseEvent) => { if (inPopup(event)) { clearTimeout(timer.current); return; } const link = target(event); if (!link) return; const kind = link.dataset.hovercard; const ref = link.dataset.ref; if (!isKind(kind) || !ref) return; clearTimeout(timer.current); timer.current = setTimeout(async () => { const key = `${kind}:${ref}`; let pending = cache.current.get(key); if (!pending) { pending = getHovercard({ data: { kind, ref } }).catch(() => null); cache.current.set(key, pending); } const card = await pending; if (!card || !link.isConnected) return; const rect = link.getBoundingClientRect(); setState({ card, top: rect.bottom + window.scrollY + 6, left: rect.left + window.scrollX }); }, SHOW_DELAY_MS); }; const out = (event: MouseEvent) => { if (!target(event) && !inPopup(event)) return; clearTimeout(timer.current); timer.current = setTimeout(() => setState(null), HIDE_DELAY_MS); }; document.addEventListener("mouseover", over); document.addEventListener("mouseout", out); return () => { clearTimeout(timer.current); document.removeEventListener("mouseover", over); document.removeEventListener("mouseout", out); }; }, []); return state; }