1(function() { 2 'use strict'; 3 4 var API_ENDPOINT = 'https://ndlvlwz3z8.execute-api.us-east-1.amazonaws.com/prod/submit'; 5 var BLOCKED_DOMAINS_ENDPOINT = 'https://ndlvlwz3z8.execute-api.us-east-1.amazonaws.com/prod/blocked-domains'; 6 7 var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 8 var blockedDomains = []; 9 10 /* 11 * The homepage "What's your challenge?" recommender collects three dropdown 12 * answers and no contact details, then redirects to a matching case study. 13 * There is no email address, so it cannot create a GHL contact on its own. 14 * Rather than gate it behind a contact form (which would cost conversions on 15 * the homepage), complogix-quiz.js stashes the answers under STORAGE_KEY and 16 * we attach them to whichever clx-form the visitor submits next. The 17 * qualification data then lands on the contact when they actually convert. 18 * 19 * The key and object shape are a contract shared with complogix-quiz.js. 20 */ 21 var QUIZ_STORAGE_KEY = 'clxChallengeQuiz'; 22 var QUIZ_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days 23 var QUIZ_MAX_VALUE_LEN = 120; 24 var QUIZ_FIELDS = [ 25 { key: 'position', label: 'Position' }, 26 { key: 'employees', label: 'Employees' }, 27 { key: 'challenge', label: 'Biggest challenge' } 28 ]; 29 30 function fetchBlockedDomains() { 31 fetch(BLOCKED_DOMAINS_ENDPOINT) 32 .then(function(response) { return response.json(); }) 33 .then(function(data) { 34 if (data.domains && Array.isArray(data.domains)) { 35 blockedDomains = data.domains.map(function(d) { return d.toLowerCase(); }); 36 } 37 }) 38 .catch(function() { 39 blockedDomains = []; 40 }); 41 } 42 43 function isBlockedEmailDomain(email) { 44 if (!blockedDomains.length) return false; 45 var atIndex = email.lastIndexOf('@'); 46 if (atIndex < 0) return false; 47 var domain = email.substring(atIndex + 1).toLowerCase(); 48 if (blockedDomains.indexOf(domain) !== -1) return true; 49 var parts = domain.split('.'); 50 for (var i = 1; i < parts.length; i++) { 51 if (blockedDomains.indexOf(parts.slice(i).join('.')) !== -1) return true; 52 } 53 return false; 54 } 55 56 function sanitize(str) { 57 if (!str) return ''; 58 var div = document.createElement('div'); 59 div.appendChild(document.createTextNode(str)); 60 return div.innerHTML; 61 } 62 63 // localStorage throws in private-browsing modes on some browsers, so every 64 // access is guarded. Losing the quiz answers must never break a submission. 65 function safeStorage() { 66 try { 67 var s = window.localStorage; 68 var probe = '__clx_probe__'; 69 s.setItem(probe, '1'); 70 s.removeItem(probe); 71 return s; 72 } catch (e) { 73 return null; 74 } 75 } 76 77 function readQuizAnswers() { 78 var store = safeStorage(); 79 if (!store) return null; 80 var raw; 81 try { 82 raw = store.getItem(QUIZ_STORAGE_KEY); 83 } catch (e) { 84 return null; 85 } 86 if (!raw) return null; 87 88 var data; 89 try { 90 data = JSON.parse(raw); 91 } catch (e) { 92 clearQuizAnswers(); 93 return null; 94 } 95 if (!data || typeof data !== 'object') return null; 96 97 if (!data.at || (new Date().getTime() - data.at) > QUIZ_TTL_MS) { 98 clearQuizAnswers(); 99 return null; 100 } 101 return data; 102 } 103 104 function clearQuizAnswers() { 105 var store = safeStorage(); 106 if (!store) return; 107 try { 108 store.removeItem(QUIZ_STORAGE_KEY); 109 } catch (e) { /* nothing useful to do */ } 110 } 111 112 /* 113 * Build the block appended to the submitted message. Values come back out of 114 * localStorage, which the visitor can edit, so they are trimmed, length 115 * capped and escaped exactly like any other user-supplied field. 116 */ 117 function formatQuizAnswers(data) { 118 var lines = []; 119 for (var i = 0; i < QUIZ_FIELDS.length; i++) { 120 var value = data[QUIZ_FIELDS[i].key]; 121 if (typeof value !== 'string') continue; 122 value = value.trim(); 123 if (!value) continue; 124 if (value.length > QUIZ_MAX_VALUE_LEN) value = value.substring(0, QUIZ_MAX_VALUE_LEN); 125 lines.push(QUIZ_FIELDS[i].label + ': ' + sanitize(value)); 126 } 127 if (!lines.length) return ''; 128 return 'Homepage challenge quiz\n' + lines.join('\n'); 129 } 130 131 function showError(field, message) { 132 field.classList.add('clx-error'); 133 var errorEl = field.parentElement.querySelector('.clx-field-error'); 134 if (errorEl) { 135 errorEl.textContent = message; 136 errorEl.classList.add('clx-visible'); 137 } 138 field.setAttribute('aria-invalid', 'true'); 139 } 140 141 function clearError(field) { 142 field.classList.remove('clx-error'); 143 var errorEl = field.parentElement.querySelector('.clx-field-error'); 144 if (errorEl) { 145 errorEl.textContent = ''; 146 errorEl.classList.remove('clx-visible'); 147 } 148 field.setAttribute('aria-invalid', 'false'); 149 } 150 151 function clearAllErrors(form) { 152 var fields = form.querySelectorAll('input, textarea'); 153 for (var i = 0; i < fields.length; i++) { 154 clearError(fields[i]); 155 } 156 } 157 158 function validateForm(form) {
159 var valid = true; 160 clearAllErrors(form); 161 162 var required = form.querySelectorAll('[required]'); 163 for (var i = 0; i < required.length; i++) { 164 if (!required[i].value.trim()) { 165 showError(required[i], required[i].getAttribute('data-label') + ' is required'); 166 valid = false; 167 } 168 } 169 170 var emailField = form.querySelector('input[type="email"]'); 171 if (emailField && emailField.value.trim()) { 172 if (!EMAIL_REGEX.test(emailField.value.trim())) { 173 showError(emailField, 'Please enter a valid email address'); 174 valid = false; 175 } else if (isBlockedEmailDomain(emailField.value.trim())) { 176 showError(emailField, 'Please use your business email address'); 177 valid = false; 178 } 179 } 180 181 return valid; 182 } 183 184 function showMessage(form, type, text) { 185 var msgEl = form.querySelector('.clx-form-message'); 186 if (!msgEl) return; 187 msgEl.className = 'clx-form-message clx-visible ' + (type === 'success' ? 'clx-success' : 'clx-error-msg'); 188 msgEl.textContent = text; 189 msgEl.setAttribute('role', 'alert'); 190 } 191 192 function hideMessage(form) { 193 var msgEl = form.querySelector('.clx-form-message'); 194 if (msgEl) { 195 msgEl.className = 'clx-form-message'; 196 msgEl.textContent = ''; 197 } 198 } 199 200 function submitForm(form) { 201 if (!validateForm(form)) return; 202 203 var formId = form.getAttribute('data-form-id'); 204 var btn = form.querySelector('.clx-submit-btn'); 205 var btnText = btn.textContent; 206 207 var payload = { 208 formId: formId, 209 firstName: sanitize(form.querySelector('[name="firstName"]').value.trim()), 210 lastName: sanitize(form.querySelector('[name="lastName"]').value.trim()), 211 email: form.querySelector('[name="email"]').value.trim().toLowerCase(), 212 sourcePage: window.location.pathname 213 }; 214 215 var phoneField = form.querySelector('[name="phone"]'); 216 if (phoneField && phoneField.value.trim()) { 217 payload.phone = sanitize(phoneField.value.trim()); 218 } 219 220 var companyField = form.querySelector('[name="company"]'); 221 if (companyField && companyField.value.trim()) { 222 payload.company = sanitize(companyField.value.trim()); 223 } 224 225 var messageField = form.querySelector('[name="message"]'); 226 if (messageField && messageField.value.trim()) { 227 payload.message = sanitize(messageField.value.trim()); 228 } 229 230 // Carry any earlier homepage quiz answers through on this submission. The 231 // visitor's own message stays first so it is what a human reads first. 232 var quizAnswers = readQuizAnswers(); 233 var quizBlock = quizAnswers ? formatQuizAnswers(quizAnswers) : ''; 234 var quizAttached = false; 235 if (quizBlock) { 236 payload.message = payload.message ? payload.message + '\n\n' + quizBlock : quizBlock; 237 quizAttached = true; 238 } 239 240 btn.disabled = true; 241 btn.classList.add('clx-loading'); 242 hideMessage(form); 243 244 fetch(API_ENDPOINT, { 245 method: 'POST', 246 headers: { 'Content-Type': 'application/json' }, 247 body: JSON.stringify(payload) 248 }) 249 .then(function(response) { return response.json(); }) 250 .then(function(data) { 251 btn.disabled = false; 252 btn.classList.remove('clx-loading'); 253 254 if (data.success) { 255 showMessage(form, 'success', 'Thank you! We\'ll be in touch shortly.'); 256 form.reset(); 257 clearAllErrors(form); 258 259 // Only drop the answers once they have actually reached the CRM, so a 260 // failed or retried submission does not lose them. 261 if (quizAttached) clearQuizAnswers(); 262 263 if (typeof window.dataLayer !== 'undefined') { 264 window.dataLayer.push({ 265 event: 'form_submission', 266 form_id: formId, 267 form_type: formId.replace(/-/g, '_'), 268 quiz_answers_attached: quizAttached 269 }); 270 } 271 } else { 272 var errorText = data.errors ? data.errors.join('. ') : data.message; 273 showMessage(form, 'error', errorText || 'Something went wrong. Please try again.'); 274 } 275 }) 276 .catch(function() { 277 btn.disabled = false; 278 btn.classList.remove('clx-loading'); 279 showMessage(form, 'error', 'Network error. Please check your connection and try again.'); 280 }); 281 } 282 283 function initForms() { 284 var forms = document.querySelectorAll('.clx-form'); 285 if (!forms.length) return; 286 287 fetchBlockedDomains(); 288 for (var i = 0; i < forms.length; i++) { 289 (function(form) { 290 form.addEventListener('submit', function(e) { 291 e.preventDefault(); 292 submitForm(form); 293 }); 294 295 var inputs = form.querySelectorAll('input, textarea'); 296 for (var j = 0; j < inputs.length; j++) { 297 inputs[j].addEventListener('input', function() { 298 clearError(this); 299 hideMessage(form); 300 }); 301 } 302 })(forms[i]); 303 } 304 } 305 306 if (document.readyState === 'loading') { 307 document.addEventListener('DOMContentLoaded', initForms); 308 } else { 309 initForms(); 310 } 311})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.