validate.ts
1 export type FormData = { 2 name: string 3 email: string 4 address1: string 5 address2: string 6 city: string 7 state: string 8 zip: string 9 phone: string 10 usLocation: boolean 11 } 12 13 export type ValidationError = { 14 message: string 15 } 16 17 export function validateField( 18 field: keyof FormData, 19 value: string, 20 ): ValidationError | null { 21 // Trim whitespace for validation 22 const trimmed = value.trim() 23 24 if (!trimmed && field === 'address2') { 25 return null // address2 is optional 26 } 27 28 // Basic required field check 29 if (!trimmed) { 30 return { message: 'This field is required' } 31 } 32 33 switch (field) { 34 case 'email': { 35 const emailRegex = 36 /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ 37 if (!emailRegex.test(trimmed)) { 38 return { message: 'Please enter a valid email address' } 39 } 40 break 41 } 42 43 case 'name': 44 if (trimmed.length < 2) { 45 return { message: 'Name must be at least 2 characters long' } 46 } 47 break 48 49 case 'address1': { 50 if (trimmed.length < 3) { 51 return { message: 'Please enter a valid address' } 52 } 53 // Accept PO Box format or regular street address 54 const isPOBox = /^P\.?O\.?\s*Box\s+\d+$/i.test(trimmed) 55 const hasNumber = /\d+/.test(trimmed) 56 if (!isPOBox && !hasNumber) { 57 return { message: 'Please include a number in the street address' } 58 } 59 break 60 } 61 case 'address2': 62 break 63 64 case 'city': 65 if (trimmed.length < 2) { 66 return { message: 'City name must be at least 2 characters long' } 67 } 68 if (!/^[a-zA-Z\s.-]+$/.test(trimmed)) { 69 return { 70 message: 71 'City can only contain letters, spaces, periods, and hyphens', 72 } 73 } 74 break 75 76 case 'state': { 77 const states = new Set([ 78 'AL', 79 'AK', 80 'AZ', 81 'AR', 82 'CA', 83 'CO', 84 'CT', 85 'DE', 86 'FL', 87 'GA', 88 'HI', 89 'ID', 90 'IL', 91 'IN', 92 'IA', 93 'KS', 94 'KY', 95 'LA', 96 'ME', 97 'MD', 98 'MA', 99 'MI', 100 'MN', 101 'MS', 102 'MO', 103 'MT', 104 'NE', 105 'NV', 106 'NH', 107 'NJ', 108 'NM', 109 'NY', 110 'NC', 111 'ND', 112 'OH', 113 'OK', 114 'OR', 115 'PA', 116 'RI', 117 'SC', 118 'SD', 119 'TN', 120 'TX', 121 'UT', 122 'VT', 123 'VA', 124 'WA', 125 'WV', 126 'WI', 127 'WY', 128 'DC', 129 ]) 130 const stateCode = trimmed.toUpperCase() 131 if (!states.has(stateCode)) { 132 return { message: 'Please enter a valid US state code (e.g. CA)' } 133 } 134 break 135 } 136 137 case 'usLocation': { 138 const normalized = trimmed.toLowerCase() 139 if (!['y', 'yes', 'n', 'no'].includes(normalized)) { 140 return { message: 'Please enter y/yes or n/no' } 141 } 142 break 143 } 144 145 case 'zip': 146 // ZIP code validation for US 147 if (!/^\d{5}(-\d{4})?$/.test(trimmed)) { 148 return { 149 message: 'Please enter a valid ZIP code (e.g. 12345 or 12345-6789)', 150 } 151 } 152 break 153 154 case 'phone': 155 // Phone validation for US (allow various formats) 156 if (!/^(\+1\s?)?(\d{3}[-.\s]??)?\d{3}[-.\s]??\d{4}$/.test(trimmed)) { 157 return { 158 message: 'Please enter a valid US phone number', 159 } 160 } 161 break 162 } 163 164 return null 165 }