Ctrl K
ComunidadGuías…

src/lib/utils.ts

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

Archivo original ↗Preguntar a KiBot
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { daysBetween, SERVER_ZONE } from "@/lib/time";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export function initials(name: string) {
  return (
    name
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 2)
      .map((p) => p[0]?.toUpperCase() ?? "")
      .join("") || "?"
  );
}

/** "2021-03" | "2021-03-01" | "Mar 2021" -> "Mar 2021" */
export function formatMonth(value?: string | null) {
  if (!value) return "";
  const trimmed = value.trim();
  if (!trimmed) return "";
  const iso = /^(\d{4})-(\d{2})(-\d{2})?$/.exec(trimmed);
  if (iso) {
    const d = new Date(Number(iso[1]), Number(iso[2]) - 1, 1);
    return d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
  }
  if (/^\d{4}$/.test(trimmed)) return trimmed;
  return trimmed;
}

export function dateRange(start?: string | null, end?: string | null, current?: boolean) {
  const s = formatMonth(start);
  const e = current ? "Present" : formatMonth(end);
  if (s && e) return `${s} – ${e}`;
  return s || e || "";
}

/**
 * "Today", "Tomorrow", "3d overdue" — a date read as a calendar, not a clock.
 *
 * `timeZone` is whose calendar. In the browser it can be left off, because the
 * host clock IS the reader's; on the server it must be passed or "today" means
 * today in whatever zone the instance runs in, which on a hosted one is UTC.
 * See src/lib/time.ts.
 */
export function relativeDay(
  date: Date | string | null | undefined,
  timeZone: string = SERVER_ZONE,
) {
  if (!date) return "";
  const d = typeof date === "string" ? new Date(date) : date;
  const days = daysBetween(new Date(), d, timeZone);
  if (days === 0) return "Today";
  if (days === 1) return "Tomorrow";
  if (days === -1) return "Yesterday";
  if (days < 0) return `${Math.abs(days)}d overdue`;
  return `in ${days}d`;
}

/** Past-tense counterpart to relativeDay: how long since something happened. */
export function agoDay(date: Date | string | null | undefined, timeZone: string = SERVER_ZONE) {
  if (!date) return "";
  const d = typeof date === "string" ? new Date(date) : date;
  const days = daysBetween(d, new Date(), timeZone);
  if (days <= 0) return "Today";
  if (days === 1) return "Yesterday";
  if (days < 14) return `${days}d ago`;
  if (days < 61) return `${Math.round(days / 7)}w ago`;
  return `${Math.round(days / 30.4)}mo ago`;
}

export function slugify(input: string) {
  return input
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/(^-|-$)/g, "")
    .slice(0, 60);
}

export function truncate(text: string, max = 160) {
  if (text.length <= max) return text;
  return `${text.slice(0, max - 1).trimEnd()}…`;
}

/**
 * What a stage checklist just put on somebody's list, for the toast.
 *
 * Named rather than counted. Tasks appearing from nowhere read as a bug, and
 * "Added: Send a thank-you" is the line that makes the feature legible the
 * first time it fires. Undefined when nothing fired, so the toast stays a
 * single line in the ordinary case.
 */
export function describeAdded(titles: string[]): string | undefined {
  if (titles.length === 0) return undefined;
  if (titles.length <= 2) return `Added: ${titles.join(", ")}`;
  return `Added ${titles.length} tasks: ${titles.slice(0, 2).join(", ")}, and ${titles.length - 2} more`;
}