diff --git a/src/__tests__/format.test.ts b/src/__tests__/format.test.ts new file mode 100644 index 0000000..5f80b8c --- /dev/null +++ b/src/__tests__/format.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { formatLocaleNumber, formatCryptoAmount } from '../lib/format'; + +describe('formatLocaleNumber utility', () => { + it('formats whole numbers with thousands separators in en-US', () => { + expect(formatLocaleNumber(50000, { locale: 'en-US' })).toBe('50,000'); + expect(formatLocaleNumber('1000000', { locale: 'en-US' })).toBe('1,000,000'); + }); + + it('formats decimals within specified precision', () => { + expect(formatLocaleNumber(1234.567, { locale: 'en-US', maximumFractionDigits: 2 })).toBe('1,234.57'); + expect(formatLocaleNumber(1234.5, { locale: 'en-US', minimumFractionDigits: 2 })).toBe('1,234.50'); + }); + + it('formats numbers correctly across European locales (de-DE)', () => { + const formatted = formatLocaleNumber(50000, { locale: 'de-DE' }); + // de-DE uses period as thousands separator + expect(formatted).toBe('50.000'); + }); + + it('handles string inputs containing existing commas cleanly', () => { + expect(formatLocaleNumber('32,500', { locale: 'en-US' })).toBe('32,500'); + }); + + it('returns safe fallback for empty, null, or invalid inputs', () => { + expect(formatLocaleNumber(null)).toBe('0'); + expect(formatLocaleNumber(undefined)).toBe('0'); + expect(formatLocaleNumber('')).toBe('0'); + expect(formatLocaleNumber('invalid-number')).toBe('0'); + }); +}); + +describe('formatCryptoAmount utility', () => { + it('appends default XLM ticker symbol', () => { + expect(formatCryptoAmount(25000, 'XLM', { locale: 'en-US' })).toBe('25,000 XLM'); + }); + + it('supports custom token tickers', () => { + expect(formatCryptoAmount('500.5', 'USDC', { locale: 'en-US' })).toBe('500.5 USDC'); + }); +}); \ No newline at end of file diff --git a/src/components/financing/ProductionFinancingSection.tsx b/src/components/financing/ProductionFinancingSection.tsx index 5869a94..cc5a429 100644 --- a/src/components/financing/ProductionFinancingSection.tsx +++ b/src/components/financing/ProductionFinancingSection.tsx @@ -15,6 +15,7 @@ import { ArrowUpRight, } from 'lucide-react'; import contractsConfig from '@/config/contracts.json'; +import { formatLocaleNumber, formatCryptoAmount } from '../../lib/format'; interface Proposal { id: string; @@ -108,7 +109,7 @@ export const ProductionFinancingSection: React.FC= parseFloat(p.targetAmount.replace(/,/g, '')) ? 'Funded' : 'FundingActive', }; } diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..059a754 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,43 @@ +/** + * Utility functions for locale-aware numeric and currency formatting across fashion financing and escrow flows. + */ + +interface FormatOptions { + locale?: string; + minimumFractionDigits?: number; + maximumFractionDigits?: number; +} + +/** + * Formats a numeric value or string according to user locale with thousands separators. + */ +export function formatLocaleNumber( + amount: number | string | null | undefined, + options: FormatOptions = {} +): string { + if (amount === null || amount === undefined || amount === '') return '0'; + const num = typeof amount === 'number' ? amount : parseFloat(String(amount).replace(/,/g, '')); + if (isNaN(num)) return '0'; + + const { + locale = typeof navigator !== 'undefined' ? navigator.language : 'en-US', + minimumFractionDigits = 0, + maximumFractionDigits = 2, + } = options; + + return new Intl.NumberFormat(locale, { + minimumFractionDigits, + maximumFractionDigits, + }).format(num); +} + +/** + * Formats a numeric figure with an asset/currency ticker suffix (e.g. "50,000 XLM"). + */ +export function formatCryptoAmount( + amount: number | string | null | undefined, + symbol: string = 'XLM', + options?: FormatOptions +): string { + return `${formatLocaleNumber(amount, options)} ${symbol}`; +} \ No newline at end of file