Midday / apps /dashboard /src /utils /format.ts
Jules
Final deployment with all fixes and verified content
c09f67c
import { TZDate } from "@date-fns/tz";
import {
differenceInDays,
differenceInMonths,
format,
startOfDay,
} from "date-fns";
import { normalizeCurrencyCode } from "./currency";
export function formatSize(bytes: number): string {
const units = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte"];
const unitIndex = Math.max(
0,
Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1),
);
return Intl.NumberFormat("en-US", {
style: "unit",
unit: units[unitIndex],
}).format(+Math.round(bytes / 1024 ** unitIndex));
}
type FormatAmountParams = {
currency: string;
amount: number;
locale?: string | null;
maximumFractionDigits?: number;
minimumFractionDigits?: number;
};
export function formatAmount({
currency,
amount,
locale = "en-US",
minimumFractionDigits,
maximumFractionDigits,
}: FormatAmountParams) {
if (!currency) {
return;
}
// Normalize currency code to ISO 4217 format
const normalizedCurrency = normalizeCurrencyCode(currency);
// Fix: locale can be null, but Intl.NumberFormat expects string | string[] | undefined
// So, if locale is null, pass undefined instead
const safeLocale = locale ?? undefined;
try {
return Intl.NumberFormat(safeLocale, {
style: "currency",
currency: normalizedCurrency,
minimumFractionDigits,
maximumFractionDigits,
}).format(amount);
} catch (error) {
// Fallback to USD if currency is invalid
console.warn(
`Invalid currency code: ${currency} (normalized to ${normalizedCurrency}), falling back to USD`,
error,
);
return Intl.NumberFormat(safeLocale, {
style: "currency",
currency: "USD",
minimumFractionDigits,
maximumFractionDigits,
}).format(amount);
}
}
export function secondsToHoursAndMinutes(seconds: number) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours && minutes) {
return `${hours}h ${minutes}m`;
}
if (hours) {
return `${hours}h`;
}
if (minutes) {
return `${minutes}m`;
}
return "0m";
}
type BurnRateData = {
value: number;
date: string;
};
export function calculateAvgBurnRate(data: BurnRateData[] | null) {
if (!data) {
return 0;
}
return data?.reduce((acc, curr) => acc + curr.value, 0) / data?.length;
}
export function formatAccountName({
name = "",
currency,
}: {
name?: string;
currency?: string | null;
}) {
if (currency) {
return `${name} (${currency})`;
}
return name;
}
export function formatDateRange(dates: TZDate[]): string {
if (!dates.length) return "";
const formatFullDate = (date: TZDate) => format(date, "MMM d");
const formatDay = (date: TZDate) => format(date, "d");
const startDate = dates[0];
const endDate = dates[1];
if (!startDate) return "";
if (
dates.length === 1 ||
!endDate ||
startDate.getTime() === endDate.getTime()
) {
return formatFullDate(startDate);
}
if (startDate.getMonth() === endDate.getMonth()) {
// Same month
return `${format(startDate, "MMM")} ${formatDay(startDate)} - ${formatDay(endDate)}`;
}
// Different months
return `${formatFullDate(startDate)} - ${formatFullDate(endDate)}`;
}
export function getDueDateStatus(dueDate: string): string {
// Parse due date as UTC (it's stored as UTC midnight)
const due = new TZDate(dueDate, "UTC");
// Get current date in UTC for consistent comparison
const now = new Date();
const nowUTC = new TZDate(now.toISOString(), "UTC");
// Compare at the day level in UTC
const nowDay = startOfDay(nowUTC);
const dueDay = startOfDay(due);
const diffDays = differenceInDays(dueDay, nowDay);
const diffMonths = differenceInMonths(dueDay, nowDay);
if (diffDays === 0) return "Today";
if (diffDays === 1) return "Tomorrow";
if (diffDays === -1) return "Yesterday";
if (diffDays > 0) {
if (diffMonths < 1) return `in ${diffDays} days`;
return `in ${diffMonths} month${diffMonths === 1 ? "" : "s"}`;
}
if (diffMonths < 1)
return `${Math.abs(diffDays)} day${Math.abs(diffDays) === 1 ? "" : "s"} ago`;
return `${diffMonths} month${diffMonths === 1 ? "" : "s"} ago`;
}
export function formatRelativeTime(date: Date): string {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return "just now";
}
const intervals = [
{ label: "y", seconds: 31536000 },
{ label: "mo", seconds: 2592000 },
{ label: "d", seconds: 86400 },
{ label: "h", seconds: 3600 },
{ label: "m", seconds: 60 },
] as const;
for (const interval of intervals) {
const count = Math.floor(diffInSeconds / interval.seconds);
if (count > 0) {
return `${count}${interval.label} ago`;
}
}
return "just now";
}
export function formatCompactAmount(
amount: number,
locale?: string | null,
): string {
const absAmount = Math.abs(amount);
const safeLocale = locale ?? "en-US";
if (absAmount >= 1000000) {
const formatted = (absAmount / 1000000).toLocaleString(safeLocale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
return `${formatted}m`;
}
// Always show in thousands notation
const formatted = (absAmount / 1000).toLocaleString(safeLocale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
return `${formatted}k`;
}