← back to Jill Website

src/utils/phoneFormatter.ts

150 lines

/**
 * Phone number formatting utilities
 * Handles US and international phone number formatting
 */

export interface PhoneValidationResult {
  isValid: boolean;
  formatted?: string;
  error?: string;
}

/**
 * Format a phone number to (555) 123-4567 format
 * Supports various input formats:
 * - 5551234567
 * - 555-123-4567
 * - (555) 123-4567
 * - +1 555 123 4567
 * - +15551234567
 */
export function formatPhoneNumber(phone: string): string {
  if (!phone) {
    return '';
  }

  // Remove all non-digit characters
  const digits = phone.replace(/\D/g, '');

  // Handle different digit lengths
  if (digits.length === 10) {
    // US number without country code: 5551234567 -> (555) 123-4567
    return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
  } else if (digits.length === 11 && digits.startsWith('1')) {
    // US number with country code: 15551234567 -> (555) 123-4567
    return `(${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
  } else if (digits.length > 11) {
    // International number: keep country code separate
    // Example: +50626824242 -> +506 2682-4242
    const countryCode = digits.slice(0, digits.length - 10);
    const localNumber = digits.slice(-10);
    return `+${countryCode} ${localNumber.slice(0, 4)}-${localNumber.slice(4)}`;
  } else if (digits.length >= 7) {
    // Partial number with at least 7 digits: format as much as possible
    const areaCode = digits.slice(0, 3);
    const prefix = digits.slice(3, 6);
    const line = digits.slice(6);

    if (digits.length === 7) {
      return `${prefix}-${line}`;
    } else {
      return `(${areaCode}) ${prefix}-${line}`;
    }
  }

  // Return original if less than 7 digits (can't format meaningfully)
  return phone;
}

/**
 * Convert formatted phone number to tel: link format
 * Removes all formatting and keeps only digits and leading +
 */
export function formatPhoneForTel(phone: string): string {
  if (!phone) {
    return '';
  }

  // Keep only digits and leading +
  const cleaned = phone.replace(/[^\d+]/g, '');

  // If starts with +, keep it; otherwise add +1 for US numbers if 10 digits
  if (cleaned.startsWith('+')) {
    return cleaned;
  }

  const digits = cleaned.replace(/\D/g, '');
  if (digits.length === 10) {
    return `+1${digits}`;
  } else if (digits.length === 11 && digits.startsWith('1')) {
    return `+${digits}`;
  }

  return `+${digits}`;
}

/**
 * Validate phone number format
 * Accepts US and international numbers with various formats
 */
export function validatePhoneNumber(phone: string): PhoneValidationResult {
  if (!phone || typeof phone !== 'string') {
    return {
      isValid: false,
      error: 'Phone number is required'
    };
  }

  const trimmed = phone.trim();

  if (trimmed.length === 0) {
    return {
      isValid: false,
      error: 'Phone number cannot be empty'
    };
  }

  // Remove all non-digit characters to count digits
  const digits = trimmed.replace(/\D/g, '');

  // Must have at least 7 digits (minimum for a valid phone number)
  if (digits.length < 7) {
    return {
      isValid: false,
      error: 'Phone number must have at least 7 digits'
    };
  }

  // Maximum 15 digits (international standard E.164)
  if (digits.length > 15) {
    return {
      isValid: false,
      error: 'Phone number cannot exceed 15 digits'
    };
  }

  // Check for valid characters (digits, spaces, dashes, parentheses, plus)
  const validCharsRegex = /^[\d\s\-\(\)\+\.]+$/;
  if (!validCharsRegex.test(trimmed)) {
    return {
      isValid: false,
      error: 'Phone number contains invalid characters'
    };
  }

  // Format the phone number
  const formatted = formatPhoneNumber(trimmed);

  return {
    isValid: true,
    formatted
  };
}

/**
 * Check if a phone number is valid (boolean only)
 */
export function isValidPhoneNumber(phone: string): boolean {
  return validatePhoneNumber(phone).isValid;
}