Ctrl K
ComunidadGuías…

src/lib/company.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Working out what a company's website is.
 *
 * The website on the company record is the answer. A posting URL is not: job
 * listings live on Greenhouse, Ashby, Workday and a dozen other boards, so a
 * link to one tells you where the employer advertises, not who they are —
 * and a logo taken from it is the applicant tracking system's logo, which is
 * both wrong and confusingly identical across half the pipeline.
 *
 * When there is no website we guess the name as a domain, because that is
 * right often enough to be worth it and the failure is invisible: a wrong
 * guess 404s and the monogram was already on screen. The company page is
 * where you correct it.
 */

function hostOf(url: string): string | null {
  const trimmed = url.trim();
  if (!trimmed) return null;
  try {
    const parsed = new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`);
    return parsed.hostname.replace(/^www\./, "").toLowerCase();
  } catch {
    return null;
  }
}

/**
 * "Vertex Payments, Inc." -> "vertexpayments". Suffixes are noise in a domain,
 * and they are the same noise that makes "Stripe" and "Stripe, Inc." two rows —
 * so this doubles as the key for spotting a duplicate employer.
 *
 * Deliberately aggressive: it also folds "Meta" and "Meta Labs" together. That
 * is why it may only ever SUGGEST a merge for a person to look at, never
 * perform one.
 */
export function companyKey(name: string): string {
  return slugify(name);
}

function slugify(name: string): string {
  return name
    .toLowerCase()
    .replace(/[’']/g, "")
    .replace(/\b(inc|llc|ltd|limited|corp|corporation|co|gmbh|plc|sa|ag|bv|pty|group|labs|technologies|technology)\b/g, "")
    .replace(/[^a-z0-9]+/g, "");
}

/**
 * The best guess at a company's domain, or null when there isn't one worth
 * making. `website` wins outright; the name is a fallback and is only tried
 * when it is short enough to plausibly be a domain.
 */
export function companyDomain(input: { name: string; website?: string | null }): string | null {
  const fromWebsite = hostOf(input.website ?? "");
  if (fromWebsite) return fromWebsite;

  const slug = slugify(input.name);
  return slug.length >= 2 && slug.length <= 24 ? `${slug}.com` : null;
}

/** One or two letters, for when there is no logo to show. */
export function monogram(name: string): string {
  const words = name.trim().split(/\s+/).filter(Boolean);
  if (words.length === 0) return "?";
  if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
  return (words[0][0] + words[1][0]).toUpperCase();
}

/**
 * A stable hue per company, so the same employer is the same colour on every
 * screen and two companies in a list are rarely the same. Only used behind a
 * monogram, where the colour is decoration rather than information — a stage
 * chip next to it is what actually carries meaning.
 */
export function monogramHue(name: string): number {
  let hash = 0;
  for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) % 360;
  return hash;
}

/**
 * twenty-icons.com is Twenty CRM's free favicon service. It serves a fixed set
 * of sizes and answers 400 Invalid size for anything else — so asking for the
 * 52 pixels a 26px avatar actually wants gets you no logo at all, silently,
 * which is indistinguishable from the company not having one.
 *
 * Snapping happens here rather than at the call sites so the UI can keep
 * choosing whatever size the layout needs. Round up, never down: a larger
 * source scaled into a smaller box stays sharp, the reverse does not.
 */
const ICON_SIZES = [16, 32, 64, 128, 180, 192];

export function logoUrl(domain: string, size = 64): string {
  const served = ICON_SIZES.find((candidate) => candidate >= size) ?? ICON_SIZES[ICON_SIZES.length - 1];
  return `https://twenty-icons.com/${domain}/${served}`;
}