Files
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

77 lines
2.8 KiB
TypeScript

import { format, formatDistanceToNow } from "date-fns";
export function formatDateTime(iso: string | null | undefined): string {
if (!iso) return "—";
return format(new Date(iso), "MMM d, yyyy h:mm a");
}
export function formatRelative(iso: string | null | undefined): string {
if (!iso) return "—";
return formatDistanceToNow(new Date(iso), { addSuffix: true });
}
/**
* NinjaPear's funding fields (`total_raised`, a round's `amount`) come back
* as raw numeric strings (e.g. "500000000"), with no currency formatting.
* Turns that into "$500,000,000"; leaves already-formatted or non-numeric
* values (e.g. "$1M", "Undisclosed") untouched.
*/
export function formatMoney(value: string): string {
const trimmed = value.trim();
if (!trimmed || !/^\d+(\.\d+)?$/.test(trimmed)) return trimmed;
const amount = Number(trimmed);
if (!Number.isFinite(amount)) return trimmed;
return `$${amount.toLocaleString("en-US")}`;
}
/**
* NinjaPear's competitor/customer `name` fields sometimes come back as a
* bare URL (e.g. "https://squareup.com") rather than a company name. Turns
* that into something readable; leaves already-clean names untouched.
*/
export function companyNameFromUrl(value: string): string {
const trimmed = value.trim();
const withoutProtocol = trimmed.replace(/^https?:\/\//i, "").replace(/^www\./i, "");
if (!withoutProtocol.includes(".")) return trimmed;
const label = withoutProtocol.split("/")[0]?.split(".")[0];
if (!label) return trimmed;
return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase();
}
const ENRICHMENT_REASON_LABELS: Record<string, string> = {
product_overlap: "Similar products",
organic_keyword_overlap: "Overlapping search keywords",
};
/** Turns a NinjaPear competitor-match slug (e.g. "product_overlap") into a
* human-readable label; unrecognized slugs still read as words. */
export function formatEnrichmentReason(reason: string): string {
const known = ENRICHMENT_REASON_LABELS[reason];
if (known) return known;
return reason
.split("_")
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
/** Light heuristics to turn a raw exception string from an enrichment
* section failure into a short, non-technical summary. */
export function summarizeEnrichmentError(raw: string): string {
const firstLine = raw.split("\n")[0]?.trim() ?? raw;
if (/^\d+ validation errors?/i.test(firstLine)) {
return "The data returned didn't match the expected format.";
}
if (/404 not found/i.test(raw)) {
return "The requested data wasn't found.";
}
if (/timed? ?out/i.test(raw)) {
return "The request timed out.";
}
if (/\b5\d\d\b/.test(raw) && /error/i.test(raw)) {
return "The data provider returned a server error.";
}
return firstLine.length > 100 ? `${firstLine.slice(0, 100)}…` : firstLine;
}