UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

627 lines (527 loc) 16.4 kB
import type {XID} from '../types'; // Date formatting utilities export const formatDate = ( date: Date | string | number, options: Intl.DateTimeFormatOptions = {}, locale = 'en' ): string => { try { const dateObj = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date; if (isNaN(dateObj.getTime())) { return 'Invalid Date'; } const defaultOptions: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric', ...options, }; return new Intl.DateTimeFormat(locale, defaultOptions).format(dateObj); } catch { return 'Invalid Date'; } }; export const formatDateTime = ( date: Date | string | number, options: Intl.DateTimeFormatOptions = {}, locale = 'en' ): string => { const defaultOptions: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', ...options, }; return formatDate(date, defaultOptions, locale); }; export const formatTime = ( date: Date | string | number, options: Intl.DateTimeFormatOptions = {}, locale = 'en' ): string => { const defaultOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true, ...options, }; return formatDate(date, defaultOptions, locale); }; export const formatRelativeTime = ( date: Date | string | number, locale = 'en' ): string => { try { const dateObj = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date; if (isNaN(dateObj.getTime())) { return 'Invalid Date'; } const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - dateObj.getTime()) / 1000); if (diffInSeconds < 60) { return 'just now'; } const diffInMinutes = Math.floor(diffInSeconds / 60); if (diffInMinutes < 60) { return `${diffInMinutes} minute${diffInMinutes === 1 ? '' : 's'} ago`; } const diffInHours = Math.floor(diffInMinutes / 60); if (diffInHours < 24) { return `${diffInHours} hour${diffInHours === 1 ? '' : 's'} ago`; } const diffInDays = Math.floor(diffInHours / 24); if (diffInDays < 7) { return `${diffInDays} day${diffInDays === 1 ? '' : 's'} ago`; } const diffInWeeks = Math.floor(diffInDays / 7); if (diffInWeeks < 4) { return `${diffInWeeks} week${diffInWeeks === 1 ? '' : 's'} ago`; } const diffInMonths = Math.floor(diffInDays / 30); if (diffInMonths < 12) { return `${diffInMonths} month${diffInMonths === 1 ? '' : 's'} ago`; } const diffInYears = Math.floor(diffInDays / 365); return `${diffInYears} year${diffInYears === 1 ? '' : 's'} ago`; } catch { return 'Invalid Date'; } }; export const formatDuration = (milliseconds: number): string => { if (milliseconds < 0) return '0s'; const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } if (hours > 0) { return `${hours}h ${minutes % 60}m`; } if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; }; export const formatTimeAgo = (date: Date | string | number): string => { return formatRelativeTime(date); }; // Number formatting utilities export const formatNumber = ( value: number, options: Intl.NumberFormatOptions = {}, locale = 'en' ): string => { try { return new Intl.NumberFormat(locale, options).format(value); } catch { return value.toString(); } }; export const formatCurrency = ( amount: number, currency = 'USD', locale = 'en' ): string => { return formatNumber(amount, { style: 'currency', currency, }, locale); }; export const formatPercentage = ( value: number, decimals = 1, locale = 'en' ): string => { return formatNumber(value / 100, { style: 'percent', minimumFractionDigits: decimals, maximumFractionDigits: decimals, }, locale); }; export const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; }; export const formatCompactNumber = ( value: number, locale = 'en' ): string => { if (Math.abs(value) < 1000) { return value.toString(); } try { return new Intl.NumberFormat(locale, { notation: 'compact', compactDisplay: 'short', }).format(value); } catch { // Fallback for browsers that don't support compact notation const k = 1000; const sizes = ['', 'K', 'M', 'B', 'T']; const i = Math.floor(Math.log(Math.abs(value)) / Math.log(k)); return `${Number.parseFloat((value / Math.pow(k, i)).toFixed(1))}${sizes[i]}`; } }; // String formatting utilities export const formatName = (firstName?: string, lastName?: string): string => { const parts = [firstName, lastName].filter(Boolean); return parts.join(' '); }; export const formatInitials = (name: string): string => { return name .split(' ') .map(word => word.charAt(0).toUpperCase()) .slice(0, 2) .join(''); }; export const formatDisplayName = (user: { firstName?: string; lastName?: string; username?: string; emailAddress?: string; }): string => { if (user.firstName || user.lastName) { return formatName(user.firstName, user.lastName); } if (user.username) { return user.username; } if (user.emailAddress) { return user.emailAddress.split('@')[0]; } return 'Unknown User'; }; export const formatEmail = (email: string): string => { return email.trim().toLowerCase(); }; export const formatPhoneNumber = ( phone: string, format: 'international' | 'national' | 'e164' = 'national' ): string => { // Remove all non-digit characters except + const cleaned = phone.replace(/[^\d+]/g, ''); // Basic formatting for US numbers (extend as needed) if (cleaned.length === 10) { // US domestic number const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/); if (match) { switch (format) { case 'international': return `+1 ${match[1]} ${match[2]} ${match[3]}`; case 'e164': return `+1${cleaned}`; case 'national': default: return `(${match[1]}) ${match[2]}-${match[3]}`; } } } if (cleaned.length === 11 && cleaned.startsWith('1')) { // US number with country code const number = cleaned.substring(1); const match = number.match(/^(\d{3})(\d{3})(\d{4})$/); if (match) { switch (format) { case 'international': return `+1 ${match[1]} ${match[2]} ${match[3]}`; case 'e164': return `+${cleaned}`; case 'national': default: return `(${match[1]}) ${match[2]}-${match[3]}`; } } } // For international numbers or unrecognized formats if (cleaned.startsWith('+')) { return cleaned; } return phone; // Return original if we can't format it }; export const maskEmail = (email: string): string => { const [localPart, domain] = email.split('@'); if (!domain) return email; if (localPart.length <= 3) { return `${localPart[0]}***@${domain}`; } const firstChar = localPart[0]; const lastChar = localPart[localPart.length - 1]; const middleLength = Math.max(3, localPart.length - 2); return `${firstChar}${'*'.repeat(middleLength)}${lastChar}@${domain}`; }; export const maskPhoneNumber = (phone: string): string => { const cleaned = phone.replace(/[^\d]/g, ''); if (cleaned.length >= 10) { const lastFour = cleaned.slice(-4); const masked = '*'.repeat(cleaned.length - 4); return `${masked}${lastFour}`; } return phone; }; export const truncateText = ( text: string, maxLength: number, suffix = '...' ): string => { if (text.length <= maxLength) return text; const truncated = text.substring(0, maxLength - suffix.length); return truncated + suffix; }; export const truncateMiddle = ( text: string, maxLength: number, separator = '...' ): string => { if (text.length <= maxLength) return text; const sepLen = separator.length; const charsToShow = maxLength - sepLen; const frontChars = Math.ceil(charsToShow / 2); const backChars = Math.floor(charsToShow / 2); return text.substring(0, frontChars) + separator + text.substring(text.length - backChars); }; export const formatTextCase = ( text: string, format: 'camel' | 'pascal' | 'snake' | 'kebab' | 'sentence' | 'title' | 'upper' | 'lower' ): string => { switch (format) { case 'camel': return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { return index === 0 ? word.toLowerCase() : word.toUpperCase(); }).replace(/\s+/g, ''); case 'pascal': return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => { return word.toUpperCase(); }).replace(/\s+/g, ''); case 'snake': return text.replace(/\W+/g, ' ') .split(/ |\B(?=[A-Z])/) .map(word => word.toLowerCase()) .join('_'); case 'kebab': return text.replace(/\W+/g, ' ') .split(/ |\B(?=[A-Z])/) .map(word => word.toLowerCase()) .join('-'); case 'sentence': return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); case 'title': return text.replace(/\w\S*/g, (txt) => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); case 'upper': return text.toUpperCase(); case 'lower': return text.toLowerCase(); default: return text; } }; // ID and token formatting utilities export const formatId = (id: XID): string => { // Truncate long IDs for display if (id.length > 12) { return truncateMiddle(id, 12); } return id; }; export const formatTokenPreview = (token: string): string => { if (token.length <= 8) return token; const start = token.substring(0, 4); const end = token.substring(token.length - 4); return `${start}...${end}`; }; // Address formatting utilities export const formatAddress = (address: { street?: string; city?: string; state?: string; postalCode?: string; country?: string; }): string => { const parts = [ address.street, address.city, address.state && address.postalCode ? `${address.state} ${address.postalCode}` : address.state || address.postalCode, address.country, ].filter(Boolean); return parts.join(', '); }; // List formatting utilities export const formatList = ( items: string[], options: { style?: 'long' | 'short' | 'narrow'; type?: 'conjunction' | 'disjunction'; locale?: string; } = {} ): string => { const { style = 'long', type = 'conjunction', locale = 'en' } = options; if (items.length === 0) return ''; if (items.length === 1) return items[0]; try { return new Intl.DateTimeFormat(locale, { style, type }).format(items); } catch { // Fallback for browsers that don't support Intl.ListFormat if (items.length === 2) { return `${items[0]} ${type === 'conjunction' ? 'and' : 'or'} ${items[1]}`; } const lastItem = items[items.length - 1]; const otherItems = items.slice(0, -1); const connector = type === 'conjunction' ? 'and' : 'or'; return `${otherItems.join(', ')}, ${connector} ${lastItem}`; } }; // JSON formatting utilities export const formatJSON = ( obj: any, indent = 2, maxDepth = 10 ): string => { try { return JSON.stringify(obj, null, indent); } catch { return '[Circular Reference]'; } }; export const formatJSONCompact = (obj: any): string => { try { return JSON.stringify(obj); } catch { return '[Circular Reference]'; } }; // URL formatting utilities export const formatDomain = (url: string): string => { try { const urlObj = new URL(url); return urlObj.hostname; } catch { return url; } }; export const formatURL = (url: string): string => { if (!url.startsWith('http://') && !url.startsWith('https://')) { return `https://${url}`; } return url; }; // Color formatting utilities export const formatHexColor = (color: string): string => { const hex = color.replace('#', ''); // Expand short hex to full hex if (hex.length === 3) { return `#${hex.split('').map(char => char + char).join('')}`; } if (hex.length === 6) { return `#${hex}`; } return color; }; export const formatRGBColor = (r: number, g: number, b: number): string => { return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; }; export const formatRGBAColor = (r: number, g: number, b: number, a: number): string => { return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})`; }; // Validation message formatting export const formatValidationError = ( field: string, rule: string, value?: any ): string => { const fieldName = formatTextCase(field, 'sentence'); switch (rule) { case 'required': return `${fieldName} is required`; case 'email': return `${fieldName} must be a valid email address`; case 'minLength': return `${fieldName} must be at least ${value} characters`; case 'maxLength': return `${fieldName} must be no more than ${value} characters`; case 'pattern': return `${fieldName} format is invalid`; case 'number': return `${fieldName} must be a number`; case 'min': return `${fieldName} must be at least ${value}`; case 'max': return `${fieldName} must be no more than ${value}`; default: return `${fieldName} is invalid`; } }; export function getTitleAlignment(align: 'left' | 'center' | 'right'): string { switch (align) { case 'left': return 'text-left'; case 'center': return 'text-center'; case 'right': return 'text-right'; default: return 'text-left'; } } // Export utilities object export const FormatUtils = { // Date and time formatDate, formatDateTime, formatTime, formatRelativeTime, formatDuration, formatTimeAgo, // Numbers formatNumber, formatCurrency, formatPercentage, formatFileSize, formatCompactNumber, // Strings formatName, formatInitials, formatDisplayName, formatEmail, formatPhoneNumber, maskEmail, maskPhoneNumber, truncateText, truncateMiddle, formatTextCase, // IDs and tokens formatId, formatTokenPreview, // Addresses formatAddress, // Lists formatList, // JSON formatJSON, formatJSONCompact, // URLs formatDomain, formatURL, // Colors formatHexColor, formatRGBColor, formatRGBAColor, // Validation formatValidationError, };