125 lines · 4.8 KB
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { getHovercard } from "~/fns/hovercards";
import { isReservedName } from "~/lib/reserved-names";

const SHOW_DELAY_MS = 350;
const HIDE_DELAY_MS = 200;
const KINDS = ["issue", "user", "team", "commit"] as const;
/** Cards describe immutable refs (a commit) or slow-moving ones (a user), so a page visit shares one fetch. */
const CARD_STALE_MS = 5 * 60 * 1000;

type Kind = (typeof KINDS)[number];
type Card = NonNullable<Awaited<ReturnType<typeof getHovercard>>>;
export type HovercardProfile = NonNullable<Card["profile"]>;
export type HovercardState = { card: Card; top: number; left: number } | null;

export const hovercardQueryKey = (kind: Kind, ref: string) => ["hovercard", kind, ref] as const;

function isKind(value: string | undefined): value is Kind {
  return KINDS.some((kind) => kind === value);
}

/** A link straight to a profile page, e.g. author names, which gets a user card without opting in. */
const PROFILE_PATH = /^\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))$/;

type Target = { link: HTMLAnchorElement; kind: Kind; ref: string };

function hoverTarget(event: Event): Target | null {
  if (!(event.target instanceof Element)) return null;
  const tagged = event.target.closest<HTMLAnchorElement>("a[data-hovercard]");
  if (tagged) {
    const kind = tagged.dataset.hovercard;
    const ref = tagged.dataset.ref;
    return isKind(kind) && ref ? { link: tagged, kind, ref } : null;
  }
  const link = event.target.closest<HTMLAnchorElement>("a[href]");
  if (!link || link.closest("header, nav, [data-no-hovercard]") || link.origin !== window.location.origin) return null;
  const name = PROFILE_PATH.exec(link.pathname)?.[1];
  return name && !isReservedName(name) ? { link, kind: "user", ref: name } : null;
}