refactor(ts): convert client/lib/utils to TypeScript (TS4)
The highest-traffic leaf module (cn, fmt, date/byte/uptime formatters, categoryColor) is now strict .ts. fmt inherits formatUSD's branded-dollars input via Parameters<typeof formatUSD>; noUncheckedIndexedAccess handled (destructuring defaults, in-range assertion). .jsx callers unaffected (Vite resolves the .ts). typecheck + build + 48 client tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7d9bf12bdc
commit
b221e02d85
|
|
@ -1,36 +1,37 @@
|
||||||
import { clsx } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { formatUSD } from './money';
|
import { formatUSD } from './money';
|
||||||
|
|
||||||
export function cn(...inputs) {
|
export function cn(...inputs: ClassValue[]): string {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonical dollar formatter for the app ("$1,234.50"). Kept here for the many
|
// 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
|
// existing call sites; the implementation lives in ./money (formatUSD) so all
|
||||||
// currency formatting has a single source of truth.
|
// currency formatting has a single source of truth. Inherits formatUSD's typed
|
||||||
export function fmt(amount) {
|
// (branded-dollars) input.
|
||||||
|
export function fmt(amount: Parameters<typeof formatUSD>[0]): string {
|
||||||
return formatUSD(amount);
|
return formatUSD(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fmtDate(dateStr) {
|
export function fmtDate(dateStr: string | null | undefined): string {
|
||||||
if (!dateStr) return '—';
|
if (!dateStr) return '—';
|
||||||
const [y, m, d] = dateStr.split('-');
|
const [y = '', m = '', d = ''] = dateStr.split('-');
|
||||||
return `${parseInt(m)}/${parseInt(d)}/${y}`;
|
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 year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function todayStr() {
|
export function todayStr(): string {
|
||||||
return localDateString();
|
return localDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fmtUptime(seconds) {
|
export function fmtUptime(seconds: number): string {
|
||||||
const d = Math.floor(seconds / 86400);
|
const d = Math.floor(seconds / 86400);
|
||||||
const h = Math.floor((seconds % 86400) / 3600);
|
const h = Math.floor((seconds % 86400) / 3600);
|
||||||
const m = Math.floor((seconds % 3600) / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
@ -41,14 +42,21 @@ export function fmtUptime(seconds) {
|
||||||
return `${s}s`;
|
return `${s}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fmtBytes(bytes) {
|
export function fmtBytes(bytes: number | null | undefined): string {
|
||||||
if (!bytes) return '0 B';
|
if (!bytes) return '0 B';
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
return `${(bytes / 1048576).toFixed(2)} MB`;
|
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-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-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' },
|
{ 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
|
// Deterministic color tone for a category (or merchant) name, used for
|
||||||
// badges and avatars so the same name always renders the same color.
|
// 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');
|
const key = String(name || 'Uncategorized');
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < key.length; i++) {
|
for (let i = 0; i < key.length; i++) {
|
||||||
hash = (hash * 31 + key.charCodeAt(i)) | 0;
|
hash = (hash * 31 + key.charCodeAt(i)) | 0;
|
||||||
}
|
}
|
||||||
const index = Math.abs(hash) % CATEGORY_COLOR_TONES.length;
|
const index = Math.abs(hash) % CATEGORY_COLOR_TONES.length;
|
||||||
return CATEGORY_COLOR_TONES[index];
|
return CATEGORY_COLOR_TONES[index]!; // index is always in range
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue