TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { formatCompact, formatSC } from "@spinza/shared";2import { ApiClientError } from "@/lib/api";34/** Coerce Postgres bigint strings / nulls to a plain number. */5export function num(v: unknown): number {6 if (typeof v === "number") return Number.isFinite(v) ? v : 0;7 if (typeof v === "string" && v.trim() !== "") {8 const n = Number(v);9 return Number.isFinite(n) ? n : 0;10 }11 if (typeof v === "bigint") return Number(v);12 return 0;13}1415export function pct(v: number | null | undefined, digits = 2): string {16 if (v === null || v === undefined || !Number.isFinite(v)) return "—";17 return `${(v * 100).toFixed(digits)}%`;18}1920export function signedPct(v: number | null | undefined, digits = 2): string {21 if (v === null || v === undefined || !Number.isFinite(v)) return "—";22 return `${v > 0 ? "+" : ""}${(v * 100).toFixed(digits)}%`;23}2425export function int(v: unknown): string {26 return Math.trunc(num(v)).toLocaleString("en-US");27}2829export function compact(v: unknown): string {30 return formatCompact(num(v));31}3233/** Signed credits: +1,250 SC / −400 SC. Never a currency sign. */34export function signedSC(v: unknown): string {35 const n = num(v);36 const s = formatSC(Math.abs(n));37 return n < 0 ? `−${s}` : n > 0 ? `+${s}` : s;38}3940export function sc(v: unknown): string {41 return formatSC(num(v));42}4344export function ms(v: number | null | undefined, digits = 0): string {45 if (v === null || v === undefined || !Number.isFinite(v)) return "—";46 return `${v.toFixed(digits)} ms`;47}4849/** 93784 -> "1d 2h 3m" */50export function duration(sec: number | null | undefined): string {51 if (sec === null || sec === undefined || !Number.isFinite(sec)) return "—";52 const s = Math.max(0, Math.round(sec));53 const d = Math.floor(s / 86400);54 const h = Math.floor((s % 86400) / 3600);55 const m = Math.floor((s % 3600) / 60);56 if (d > 0) return `${d}d ${h}h ${m}m`;57 if (h > 0) return `${h}h ${m}m`;58 if (m > 0) return `${m}m ${s % 60}s`;59 return `${s}s`;60}6162export function durationMs(v: number | null | undefined): string {63 if (v === null || v === undefined || !Number.isFinite(v)) return "—";64 if (v < 1000) return `${Math.round(v)} ms`;65 return duration(v / 1000);66}6768export function bytes(v: unknown): string {69 const n = num(v);70 if (n < 1024) return `${n} B`;71 if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;72 if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB`;73 return `${(n / 1024 ** 3).toFixed(2)} GB`;74}7576export function dateTime(iso: string | null | undefined): string {77 if (!iso) return "—";78 const d = new Date(iso);79 if (Number.isNaN(d.getTime())) return "—";80 return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });81}8283export function shortDate(iso: string | null | undefined): string {84 if (!iso) return "—";85 const d = new Date(iso);86 if (Number.isNaN(d.getTime())) return "—";87 return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });88}8990export function isoDay(iso: string): string {91 const d = new Date(iso);92 return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);93}9495export function multiplier(v: unknown): string {96 const n = num(v);97 if (n >= 100) return `${Math.round(n).toLocaleString("en-US")}×`;98 if (n >= 10) return `${n.toFixed(1).replace(/\.0$/, "")}×`;99 return `${n.toFixed(2).replace(/\.?0+$/, "")}×`;100}101102export function describeError(e: unknown): string {103 if (e instanceof ApiClientError) return e.code && e.code !== `HTTP_${e.status}` ? `${e.message} (${e.code})` : e.message;104 if (e instanceof Error) return e.message;105 return "Unexpected error.";106}107108export function titleCase(s: string): string {109 return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());110}111