diff --git a/client/lib/utils.js b/client/lib/utils.ts similarity index 73% rename from client/lib/utils.js rename to client/lib/utils.ts index e0ea3a2..73ec192 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.ts @@ -1,36 +1,37 @@ -import { clsx } from 'clsx'; +import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { formatUSD } from './money'; -export function cn(...inputs) { +export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)); } // Canonical dollar formatter for the app ("$1,234.50"). Kept here for the many // existing call sites; the implementation lives in ./money (formatUSD) so all -// currency formatting has a single source of truth. -export function fmt(amount) { +// currency formatting has a single source of truth. Inherits formatUSD's typed +// (branded-dollars) input. +export function fmt(amount: Parameters[0]): string { return formatUSD(amount); } -export function fmtDate(dateStr) { +export function fmtDate(dateStr: string | null | undefined): string { if (!dateStr) return '—'; - const [y, m, d] = dateStr.split('-'); + const [y = '', m = '', d = ''] = dateStr.split('-'); return `${parseInt(m)}/${parseInt(d)}/${y}`; } -export function localDateString(date = new Date()) { +export function localDateString(date: Date = new Date()): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } -export function todayStr() { +export function todayStr(): string { return localDateString(); } -export function fmtUptime(seconds) { +export function fmtUptime(seconds: number): string { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); @@ -41,14 +42,21 @@ export function fmtUptime(seconds) { return `${s}s`; } -export function fmtBytes(bytes) { +export function fmtBytes(bytes: number | null | undefined): string { if (!bytes) return '0 B'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1048576).toFixed(2)} MB`; } -const CATEGORY_COLOR_TONES = [ +interface CategoryTone { + border: string; + bg: string; + text: string; + bar: string; +} + +const CATEGORY_COLOR_TONES: CategoryTone[] = [ { border: 'border-emerald-300/50', bg: 'bg-emerald-400/15', text: 'text-emerald-700 dark:text-emerald-200', bar: 'bg-emerald-500' }, { border: 'border-sky-300/50', bg: 'bg-sky-400/15', text: 'text-sky-700 dark:text-sky-200', bar: 'bg-sky-500' }, { border: 'border-amber-300/50', bg: 'bg-amber-400/15', text: 'text-amber-700 dark:text-amber-200', bar: 'bg-amber-500' }, @@ -61,12 +69,12 @@ const CATEGORY_COLOR_TONES = [ // Deterministic color tone for a category (or merchant) name, used for // badges and avatars so the same name always renders the same color. -export function categoryColor(name) { +export function categoryColor(name: string | null | undefined): CategoryTone { const key = String(name || 'Uncategorized'); let hash = 0; for (let i = 0; i < key.length; i++) { hash = (hash * 31 + key.charCodeAt(i)) | 0; } const index = Math.abs(hash) % CATEGORY_COLOR_TONES.length; - return CATEGORY_COLOR_TONES[index]; + return CATEGORY_COLOR_TONES[index]!; // index is always in range }