1(function () { 2 'use strict'; 3 4 5 // ===== A/B Variant: PhoneâName (control) vs NameâPhone (test) ===== 6 // 50/50 split assigned on first /bodify/book load, persisted in localStorage. 7 // Control â window.__bodifyVersion = 'v1b' (unchanged â preserves dashboard continuity) 8 // Test â window.__bodifyVersion = 'v1b-name' 9 // QA override: append ?variant=v1b or ?variant=v1b-name to the URL. 10 // If a /bodify/book-vN route already set window.__bodifyVersion server-side, leave it alone. 11 var IS_NAME_FIRST = (function () { 12 if (window.__bodifyVersion) { 13 return window.__bodifyVersion === 'v1b-name'; 14 } 15 var urlVariant = null; 16 try { 17 var v = new URLSearchParams(window.location.search).get('variant'); 18 if (v === 'v1b' || v === 'v1b-name') urlVariant = v; 19 } catch (e) {} 20 21 var stored = null; 22 try { stored = localStorage.getItem('bodify_book_variant'); } catch (e) {} 23 if (stored !== 'v1b' && stored !== 'v1b-name') stored = null; 24 25 var assigned = urlVariant || stored; 26 if (!assigned) { 27 assigned = Math.random() < 0.5 ? 'v1b' : 'v1b-name'; 28 try { localStorage.setItem('bodify_book_variant', assigned); } catch (e) {} 29 } 30 window.__bodifyVersion = assigned; 31 return assigned === 'v1b-name'; 32 })(); 33 34 // ===== Step indices ===== 35 var STEP_WELCOME = 0; 36 var STEP_PHONE = 1; 37 var STEP_NAME = 2; 38 var STEP_LOCATION = 3; 39 var STEP_VISITTYPE = 4; 40 var STEP_GOALS = 5; 41 var STEP_TREATMENT = 6; // only reached via "skip consultation" 42 var STEP_DATETIME = 7; 43 var STEP_EMAIL = 8; 44 var STEP_CONFIRM = 9; 45 46 var STEP_NAMES = { 47 0: 'Welcome', 48 1: 'Phone', 49 2: 'Name', 50 3: 'Location', 51 4: 'Visit Type', 52 5: 'Goals', 53 6: 'Treatment', 54 7: 'Date/Time', 55 8: 'Email', 56 9: 'Confirm' 57 }; 58 59 var GILBERT_OPEN_DATES = [ 60 '2026-08-20', 61 '2026-08-26', 62 '2026-09-01', 63 '2026-09-03', 64 '2026-09-10', 65 '2026-09-15', 66 '2026-09-18', 67 '2026-09-23', 68 '2026-10-05', 69 '2026-10-08', 70 '2026-10-13', 71 '2026-10-15', 72 '2026-10-20', 73 '2026-10-22', 74 '2026-10-28', 75 '2026-10-30' 76 ]; 77 78 function isGilbertOpenDate(dateISO) { 79 return GILBERT_OPEN_DATES.indexOf(dateISO) !== -1; 80 } 81 82 // ===== Google Sheets Tracking ===== 83 var TRACKING_URL = 'https://script.google.com/macros/s/AKfycbyU2QyCNLWYo19JoM3Vlo54Dhizolv0iqRTkiQFgadSVqEdCAEniHe8ieC4nCeLhuHq/exec'; 84 var sessionId = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8); 85 86 function trackStep(stepIndex) { 87 var channel = window.__bodifyChannel || 'direct'; 88 var formVersion = window.__bodifyVersion || 'v1b'; 89 var params = new URLSearchParams({ 90 timestamp: new Date().toISOString(), 91 sessionId: sessionId, 92 firstName: state.data.firstName, 93 lastName: state.data.lastName, 94 phone: state.data.phone, 95 email: state.data.email, 96 location: state.data.location, 97 visitType: state.data.visitType, 98 goal: state.data.goal, 99 service: state.data.service, 100 preferredDate: state.data.preferredDate, 101 preferredTime: state.data.preferredTime, 102 lastStep: STEP_NAMES[stepIndex] || 'Unknown', 103 completed: stepIndex === STEP_CONFIRM ? 'Yes' : 'No', 104 channel: channel, 105 formVersion: formVersion, 106 cherryClicked: state.data.cherryClicked || '', 107 cherryStatus: state.data.cherryStatus || '', 108 cherryAmount: state.data.cherryAmount || '' 109 }); 110 try { 111 var img = new Image(); 112 img.src = TRACKING_URL + '?' + params.toString(); 113 } catch (e) { /* tracking should never break the form */ } 114 115 // Push CompleteRegistration event to GTM dataLayer on form completion 116 if (stepIndex === STEP_CONFIRM) { 117 window.dataLayer = window.dataLayer || []; 118 window.dataLayer.push({ 119 'event': 'CompleteRegistration', 120 'formLocation': state.data.location, 121 'formService': state.data.service, 122 'formChannel': channel 123 }); 124 } 125 } 126 127 // Goal â Booker consultation mapping (in-person) 128 var GOAL_MAP = { 129 'fat-reduction': 'CoolSculpting Elite Consultation', 130 'skin-tightening':'Sofwave Consultation', 131 'build-muscle': 'CoolTone Consultation', 132 'all': 'The Bodify Method Consultation', 133 'not-sure': 'CoolSculpting Elite Consultation', 134 }; 135 136 // Goal â Booker consultation mapping (virtual) 137 var VIRTUAL_GOAL_MAP = { 138 'fat-reduction': 'Virtual CoolSculpting Elite Consultation - Zoom', 139 'skin-tightening':'Virtual Sofwave Consultation - Zoom', 140 'build-muscle': 'Virtual CoolSculpting Elite Consultation - Zoom', 141 'all': 'Virtual CoolSculpting Elite Consultation - Zoom', 142 'not-sure': 'Virtual CoolSculpting Elite Consultation - Zoom', 143 }; 144 145 // ===== State ===== 146 var state = { 147 currentStep: 0, 148 totalSteps: 10, 149 skippedConsultation: false, 150 leadId: null, // set when lead is first created (phone step) 151 data: { 152 firstName: '', 153 lastName: '', 154 phone: '', 155 email: '', 156 location: '', 157 visitType: '', 158 talkFirst: false, 159 goal: '', 160 service: '', 161 preferredDate: '', 162 preferredTime: '', 163 sameDayService: '', 164 }, 165 calendarDate: new Date(), 166 }; 167 168 // ===== DOM refs ===== 169 var $ = function (sel) { return document.querySelector(sel); }; 170 var $$ = function (sel) { return document.querySelectorAll(sel); }; 171 172 var progressFill = $('#progressFill'); 173 var backBtn = $('#backBtn'); 174 var steps = $$('.step'); 175 176 // ===== Navigation ===== 177 function goTo(nextIndex, direction) { 178 if (nextIndex < 0 || nextIndex >= state.totalSteps) return; 179 180 var currentEl = steps[state.currentStep]; 181 var nextEl = steps[nextIndex]; 182 var forward = direction === undefined ? nextIndex > state.currentStep : direction === 'forward'; 183 184 currentEl.classList.remove('active'); 185 currentEl.classList.add(forward ? 'exit-left' : 'exit-right'); 186 187 nextEl.classList.remove('exit-left', 'exit-right', 'enter-left', 'enter-right'); 188 nextEl.classList.add(forward ? 'enter-right' : 'enter-left'); 189 190 void nextEl.offsetWidth; 191 192 nextEl.classList.remove('enter-right', 'enter-left'); 193 nextEl.classList.add('active'); 194 195 setTimeout(function () { 196 currentEl.classList.remove('exit-left', 'exit-right'); 197 }, 350); 198 199 state.currentStep = nextIndex; 200 updateProgress(); 201 updateBackBtn(); 202 focusFirstInput(nextEl); 203 trackStep(nextIndex); 204 205 // Filter time slots when arriving at the date/time step 206 if (nextIndex === STEP_DATETIME && typeof filterTimeSlots === 'function') { 207 filterTimeSlots(); 208 } 209 } 210 211 function updateProgress() { 212 // Calculate progress skipping the treatment step when not used, 213 // and also skipping Goals for the talk-first path. 214 var effectiveSteps = state.skippedConsultation ? 10 : (state.data.talkFirst ? 8 : 9); 215 var effectiveCurrent = state.currentStep; 216 if (state.data.talkFirst && state.currentStep > STEP_VISITTYPE) { 217 // Talk-first skips GOALS (5) and TREATMENT (6) 218 effectiveCurrent = state.currentStep - 2; 219 } else if (!state.skippedConsultation && state.currentStep > STEP_GOALS) { 220 effectiveCurrent = state.currentStep - 1; 221 } 222 var pct = effectiveCurrent === 0 ? 0 : (effectiveCurrent / (effectiveSteps - 1)) * 100; 223 progressFill.style.width = pct + '%'; 224 } 225 226 function updateBackBtn() { 227 // Show back button for all active steps except Welcome and Confirm 228 if (state.currentStep >= 1 && state.currentStep <= STEP_EMAIL) { 229 backBtn.classList.add('visible'); 230 } else { 231 backBtn.classList.remove('visible'); 232 } 233 } 234 235 function handleBack() { 236 var cur = state.currentStep; 237 if (cur <= 0 || cur > STEP_EMAIL) return; 238 239 // Control flow: Welcome â Location â VisitType â Goals â DateTime â Phone â Name â Email â Confirm 240 // Name-first: Welcome â Location â VisitType â Goals â DateTime â Name â Phone â Email â Confirm 241 // Talk-first: Welcome â Location â VisitType â DateTime â Name â Phone â Email â Confirm 242 var nameFirstNav = state.data.talkFirst || IS_NAME_FIRST; 243 if (cur === STEP_LOCATION) { 244 goTo(STEP_WELCOME, 'back'); 245 } else if (cur === STEP_VISITTYPE) { 246 goTo(STEP_LOCATION, 'back'); 247 } else if (cur === STEP_GOALS) { 248 goTo(STEP_VISITTYPE, 'back'); 249 } else if (cur === STEP_DATETIME) { 250 goTo(state.data.talkFirst ? STEP_VISITTYPE : STEP_GOALS, 'back'); 251 } else if (cur === STEP_PHONE) { 252 goTo(nameFirstNav ? STEP_NAME : STEP_DATETIME, 'back'); 253 } else if (cur === STEP_NAME) { 254 goTo(nameFirstNav ? STEP_DATETIME : STEP_PHONE, 'back'); 255 } else if (cur === STEP_TREATMENT) { 256 goTo(STEP_GOALS, 'back'); 257 } else if (cur === STEP_EMAIL) { 258 goTo(nameFirstNav ? STEP_PHONE : STEP_NAME, 'back'); 259 } else { 260 goTo(cur - 1, 'back'); 261 } 262 } 263 264 function focusFirstInput(stepEl) { 265 var input = stepEl.querySelector('input'); 266 if (input) { 267 setTimeout(function () { input.focus(); }, 360); 268 } 269 } 270 271 // ===== Validation ===== 272 function clearError(inputEl, errorEl) { 273 inputEl.classList.remove('invalid'); 274 errorEl.textContent = ''; 275 } 276 277 function showError(inputEl, errorEl, msg) { 278 inputEl.classList.add('invalid'); 279 errorEl.textContent = msg; 280 } 281 282 function validateName() { 283 var first = $('#firstName'); 284 var last = $('#lastName'); 285 var firstErr = $('#firstNameError'); 286 var lastErr = $('#lastNameError');
287 var valid = true; 288 289 clearError(first, firstErr); 290 clearError(last, lastErr); 291 292 if (!first.value.trim()) { 293 showError(first, firstErr, 'First name is required'); 294 valid = false; 295 } 296 if (!last.value.trim()) { 297 showError(last, lastErr, 'Last name is required'); 298 valid = false; 299 } 300 301 if (valid) { 302 state.data.firstName = first.value.trim(); 303 state.data.lastName = last.value.trim(); 304 } 305 return valid; 306 } 307 308 function formatPhone(value) { 309 var allDigits = value.replace(/\D/g, ''); 310 var countryCode = ''; 311 var digits = allDigits; 312 313 if (value.match(/^\+1/) && allDigits.length === 11 && allDigits[0] === '1') { 314 countryCode = '+1 '; 315 digits = allDigits.slice(1); 316 } else if (value.match(/^\+/)) { 317 var match = value.match(/^(\+\d{1,3})\s*(.*)/); 318 if (match) { 319 countryCode = match[1] + ' '; 320 digits = match[2].replace(/\D/g, ''); 321 } 322 } else if (allDigits.length === 11 && allDigits[0] === '1') { 323 countryCode = '+1 '; 324 digits = allDigits.slice(1); 325 } 326 327 if (digits.length === 0) return countryCode; 328 if (digits.length <= 3) return countryCode + '(' + digits; 329 if (digits.length <= 6) return countryCode + '(' + digits.slice(0, 3) + ') ' + digits.slice(3); 330 return countryCode + '(' + digits.slice(0, 3) + ') ' + digits.slice(3, 6) + '-' + digits.slice(6, 10); 331 } 332 333 function validatePhone() { 334 var phone = $('#phone'); 335 var phoneErr = $('#phoneError'); 336 clearError(phone, phoneErr); 337 338 var val = phone.value.trim(); 339 if (!val) { 340 showError(phone, phoneErr, 'Phone number is required'); 341 return false; 342 } 343 344 var countryCode = '+1'; 345 var numberPart = val; 346 var ccMatch = val.match(/^(\+\d{1,3})\s*(.*)/); 347 if (ccMatch) { 348 countryCode = ccMatch[1]; 349 numberPart = ccMatch[2]; 350 } 351 352 var digits = numberPart.replace(/\D/g, ''); 353 if (!ccMatch && digits.length === 11 && digits[0] === '1') { 354 digits = digits.slice(1); 355 } 356 357 if (digits.length < 7) { 358 showError(phone, phoneErr, 'Please enter a valid phone number'); 359 return false; 360 } 361 362 state.data.phone = countryCode + digits; 363 return true; 364 } 365 366 function validateEmail() { 367 var email = $('#email'); 368 var emailErr = $('#emailError'); 369 clearError(email, emailErr); 370 371 var val = email.value.trim(); 372 if (!val) { 373 showError(email, emailErr, 'Email is required'); 374 return false; 375 } 376 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) { 377 showError(email, emailErr, 'Please enter a valid email address'); 378 return false; 379 } 380 381 state.data.email = val; 382 return true; 383 } 384 385 function validateDateTime() { 386 var err = $('#dateTimeError'); 387 err.textContent = ''; 388 389 if (!state.data.preferredDate) { 390 err.textContent = 'Please select a date'; 391 return false; 392 } 393 if (!state.data.preferredTime) { 394 err.textContent = 'Please select a time'; 395 return false; 396 } 397 return true; 398 } 399 400 // ===== Calendar ===== 401 function renderCalendar() { 402 var year = state.calendarDate.getFullYear(); 403 var month = state.calendarDate.getMonth(); 404 var today = new Date(); 405 today.setHours(0, 0, 0, 0); 406 407 var monthNames = [ 408 'January', 'February', 'March', 'April', 'May', 'June', 409 'July', 'August', 'September', 'October', 'November', 'December', 410 ]; 411 412 $('#calMonth').textContent = monthNames[month] + ' ' + year; 413 414 var firstDay = new Date(year, month, 1).getDay(); 415 var daysInMonth = new Date(year, month + 1, 0).getDate(); 416 var container = $('#calDays'); 417 container.innerHTML = ''; 418 419 for (var i = 0; i < firstDay; i++) { 420 var empty = document.createElement('span'); 421 empty.className = 'cal-day empty'; 422 container.appendChild(empty); 423 } 424 425 for (var d = 1; d <= daysInMonth; d++) { 426 var btn = document.createElement('button'); 427 btn.className = 'cal-day'; 428 btn.textContent = d; 429 var date = new Date(year, month, d); 430 431 var day = date.getDay(); 432 var loc = (state.data.location || '').toLowerCase(); 433 var dateISO = date.getFullYear() + '-' + String(date.getMonth()+1).padStart(2,'0') + '-' + String(date.getDate()).padStart(2,'0'); 434 var isGilbertClosed = loc === 'gilbert' && !isGilbertOpenDate(dateISO); 435 var scottsdaleClosed = ['2026-04-04','2026-04-18','2026-05-02','2026-05-16','2026-05-30']; 436 var isScottsdaleClosed = loc === 'scottsdale' && (day === 1 || scottsdaleClosed.indexOf(dateISO) !== -1); 437 var maxDate = new Date(); maxDate.setDate(maxDate.getDate() + 60); maxDate.setHours(23,59,59,999); 438 if (date < today || date > maxDate || day === 0 || isGilbertClosed || isScottsdaleClosed) { 439 btn.classList.add('disabled'); 440 } else { 441 (function (dt, b) { 442 b.addEventListener('click', function () { selectDate(dt, b); }); 443 })(date, btn); 444 } 445 446 if (date.getTime() === today.getTime()) { 447 btn.classList.add('today'); 448 } 449 450 if (state.data.preferredDate) { 451 var sel = new Date(state.data.preferredDate + 'T00:00:00'); 452 if (date.getFullYear() === sel.getFullYear() && 453 date.getMonth() === sel.getMonth() && 454 date.getDate() === sel.getDate()) { 455 btn.classList.add('selected'); 456 } 457 } 458 459 container.appendChild(btn); 460 } 461 } 462 463 function selectDate(date, btnEl) { 464 var yyyy = date.getFullYear(); 465 var mm = String(date.getMonth() + 1).padStart(2, '0'); 466 var dd = String(date.getDate()).padStart(2, '0'); 467 state.data.preferredDate = yyyy + '-' + mm + '-' + dd; 468 469 $$('.cal-day').forEach(function (el) { el.classList.remove('selected'); }); 470 btnEl.classList.add('selected'); 471 472 var tg = document.getElementById('timeGrid'); 473 var ld = document.getElementById('availabilityLoading'); 474 if (tg) tg.style.display = 'none'; 475 if (ld) ld.style.display = 'flex';
476 $('#timeSlots').classList.add('visible'); 477 $('#dateTimeError').textContent = ''; 478 479 // Request availability for this specific date from the extension (if running locally) 480 try { 481 window.postMessage({ 482 type: 'bodify-request-date-availability', 483 date: state.data.preferredDate, 484 location: state.data.location 485 }, '*'); 486 } catch (e) { /* extension not installed */ } 487 488 // Fetch availability from Google Sheet (for external users without extension) 489 fetchSheetAvailability(state.data.preferredDate, state.data.location); 490 491 // Filter with whatever data we have (may update when fetch responds) 492 if (typeof filterTimeSlots === 'function') { 493 filterTimeSlots(); 494 } 495 } 496 497 // ===== Extension Messaging ===== 498 function buildLeadObject() { 499 return { 500 id: state.leadId || ('lead_' + Date.now()), 501 firstName: state.data.firstName, 502 lastName: state.data.lastName, 503 phone: state.data.phone, 504 email: state.data.email, 505 location: state.data.location, 506 visitType: state.data.visitType, 507 goal: state.data.goal, 508 service: state.data.service, 509 preferredDate: state.data.preferredDate, 510 preferredTime: state.data.preferredTime, 511 sameDayService: state.data.sameDayService, 512 createdAt: state.leadCreatedAt || new Date().toISOString(), 513 status: 'new', 514 }; 515 } 516 517 function notifyExtension(type, lead) { 518 try { 519 window.postMessage({ type: 'bodify-' + type, lead: lead }, '*'); 520 } catch (e) { /* extension not installed */ } 521 } 522 523 function createLeadOnPhone() { 524 state.leadId = 'lead_' + Date.now(); 525 state.leadCreatedAt = new Date().toISOString(); 526 var lead = buildLeadObject(); 527 lead.status = 'new'; 528 notifyExtension('new-lead', lead); 529 530 // Also save to localStorage 531 var leads = JSON.parse(localStorage.getItem('bodify_leads') || '[]'); 532 leads.push(lead); 533 localStorage.setItem('bodify_leads', JSON.stringify(leads)); 534 } 535 536 function updateLeadProgress() { 537 if (!state.leadId) return; 538 var lead = buildLeadObject(); 539 lead.status = 'in-progress'; 540 notifyExtension('update-lead', lead); 541 542 // Update in localStorage 543 var leads = JSON.parse(localStorage.getItem('bodify_leads') || '[]'); 544 for (var i = 0; i < leads.length; i++) { 545 if (leads[i].id === state.leadId) { 546 leads[i] = lead; 547 break; 548 } 549 } 550 localStorage.setItem('bodify_leads', JSON.stringify(leads)); 551 } 552 553 function completeLeadOnEmail() { 554 if (!state.leadId) return; 555 var lead = buildLeadObject(); 556 lead.status = 'booked'; 557 notifyExtension('complete-lead', lead); 558 559 // Update in localStorage 560 var leads = JSON.parse(localStorage.getItem('bodify_leads') || '[]'); 561 for (var i = 0; i < leads.length; i++) { 562 if (leads[i].id === state.leadId) { 563 leads[i] = lead; 564 break; 565 } 566 } 567 localStorage.setItem('bodify_leads', JSON.stringify(leads)); 568 569 return lead; 570 } 571 572 // ===== Save Lead ===== 573 function saveLead() { 574 var lead = { 575 id: 'lead_' + Date.now(), 576 firstName: state.data.firstName, 577 lastName: state.data.lastName, 578 phone: state.data.phone, 579 email: state.data.email, 580 location: state.data.location, 581 visitType: state.data.visitType, 582 goal: state.data.goal, 583 service: state.data.service, 584 preferredDate: state.data.preferredDate, 585 preferredTime: state.data.preferredTime, 586 sameDayService: state.data.sameDayService, 587 createdAt: new Date().toISOString(), 588 status: 'pending', 589 }; 590 591 var leads = JSON.parse(localStorage.getItem('bodify_leads') || '[]'); 592 leads.push(lead); 593 localStorage.setItem('bodify_leads', JSON.stringify(leads)); 594 595 // Notify Chrome extension if available 596 try { 597 window.postMessage({ type: 'bodify-new-lead', lead: lead }, '*'); 598 } catch (e) { /* extension not installed */ } 599 600 return lead; 601 } 602 603 // ===== Render Confirmation =====
604 function renderSummary(lead) { 605 var summary = $('#summary'); 606 var items = [ 607 { label: 'Name', value: lead.firstName + ' ' + lead.lastName }, 608 { label: 'Phone', value: formatPhone(lead.phone.replace('+1', '')) }, 609 { label: 'Email', value: lead.email }, 610 { label: 'Visit', value: lead.visitType === 'virtual' ? 'Virtual' : (lead.visitType === 'talk-first' ? 'Phone Consultation' : 'In-Person') }, 611 { label: 'Service', value: lead.service }, 612 { label: 'Location', value: lead.location }, 613 { label: 'Date', value: formatDisplayDate(lead.preferredDate) }, 614 { label: 'Time', value: lead.preferredTime }, 615 ]; 616 if (lead.sameDayService) { 617 var sdMap = { yes: 'Yes', no: 'No', maybe: 'Maybe' }; 618 items.push({ label: 'Same-Day', value: sdMap[lead.sameDayService] || lead.sameDayService }); 619 } 620 var editableFields = ['Name', 'Phone', 'Email']; 621 summary.innerHTML = items 622 .map(function (item) { 623 var isEditable = editableFields.indexOf(item.label) !== -1; 624 return '<div class="summary-item' + (isEditable ? ' summary-editable' : '') + '"' + 625 (isEditable ? ' data-field="' + item.label.toLowerCase() + '"' : '') + '>' + 626 '<span class="summary-label">' + item.label + '</span>' + 627 '<span class="summary-value">' + item.value + 628 (isEditable ? '<svg class="edit-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' : '') + 629 '</span></div>'; 630 }) 631 .join(''); 632 633 // Attach click handlers for editable fields 634 summary.querySelectorAll('.summary-editable').forEach(function (row) { 635 row.addEventListener('click', function () { 636 var field = row.dataset.field; 637 var valueEl = row.querySelector('.summary-value'); 638 var currentText = valueEl.textContent.trim(); 639 640 var input = document.createElement('input'); 641 input.type = field === 'email' ? 'email' : 'text'; 642 input.className = 'summary-edit-input'; 643 input.value = currentText; 644 645 valueEl.innerHTML = ''; 646 valueEl.appendChild(input); 647 input.focus(); 648 input.select(); 649 650 function save() { 651 var val = input.value.trim(); 652 if (!val) return; 653 654 if (field === 'name') { 655 var parts = val.split(/\s+/); 656 state.data.firstName = parts[0] || ''; 657 state.data.lastName = parts.slice(1).join(' ') || ''; 658 lead.firstName = state.data.firstName; 659 lead.lastName = state.data.lastName; 660 } else if (field === 'phone') { 661 var digits = val.replace(/\D/g, ''); 662 if (digits.length === 11 && digits[0] === '1') digits = digits.slice(1); 663 if (digits.length !== 10) return; 664 state.data.phone = '+1' + digits; 665 lead.phone = state.data.phone; 666 } else if (field === 'email') { 667 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) return; 668 state.data.email = val; 669 lead.email = val; 670 } 671 672 // Update localStorage 673 var leads = JSON.parse(localStorage.getItem('bodify_leads') || '[]'); 674 for (var i = leads.length - 1; i >= 0; i--) { 675 if (leads[i].id === lead.id) { 676 leads[i] = lead; 677 break; 678 } 679 } 680 localStorage.setItem('bodify_leads', JSON.stringify(leads)); 681 682 renderSummary(lead); 683 } 684 685 input.addEventListener('blur', save); 686 input.addEventListener('keydown', function (e) { 687 if (e.key === 'Enter') { e.preventDefault(); input.blur(); } 688 if (e.key === 'Escape') { renderSummary(lead); } 689 }); 690 }); 691 }); 692 } 693 694 function formatDisplayDate(dateStr) { 695 var parts = dateStr.split('-').map(Number); 696 var date = new Date(parts[0], parts[1] - 1, parts[2]); 697 return date.toLocaleDateString('en-US', { 698 weekday: 'short', 699 month: 'short', 700 day: 'numeric', 701 year: 'numeric', 702 }); 703 } 704 705 // ===== Event Listeners =====
706 function init() { 707 // Welcome â go to Location first 708 $('#startBtn').addEventListener('click', function () { goTo(STEP_LOCATION); }); 709 710 // Back 711 backBtn.addEventListener('click', handleBack); 712 713 // Phone 714 $('#phone').addEventListener('input', function (e) { 715 var pos = e.target.selectionStart; 716 var prevLen = e.target.value.length; 717 e.target.value = formatPhone(e.target.value); 718 var newLen = e.target.value.length; 719 e.target.setSelectionRange(pos + (newLen - prevLen), pos + (newLen - prevLen)); 720 }); 721 722 $('#phoneNext').addEventListener('click', function () { 723 if (validatePhone()) { 724 if (state.data.talkFirst || IS_NAME_FIRST) { 725 // Name was captured on the previous step; phone is the last identity 726 // field, so create the lead now and continue to email. 727 createLeadOnPhone(); 728 goTo(STEP_EMAIL, 'forward'); 729 } else { 730 createLeadOnPhone(); 731 goTo(STEP_NAME); 732 } 733 } 734 }); 735 736 // Name â control: â Email; name-first / talk-first: â Phone 737 $('#nameNext').addEventListener('click', function () { 738 if (validateName()) { 739 if (state.data.talkFirst || IS_NAME_FIRST) { 740 // Phone hasn't been captured yet â no lead exists; just advance. 741 goTo(STEP_PHONE, 'forward'); 742 } else { 743 updateLeadProgress(); 744 goTo(STEP_EMAIL); 745 } 746 } 747 }); 748 749 // Location â auto-advance to Visit Type 750 $$('.location-card').forEach(function (card) { 751 card.addEventListener('click', function () { 752 $$('.location-card').forEach(function (c) { c.classList.remove('selected'); }); 753 card.classList.add('selected'); 754 state.data.location = card.dataset.location; 755 renderCalendar(); // Re-render with location-specific closed days 756 updateLeadProgress(); 757 setTimeout(function () { goTo(STEP_VISITTYPE); }, 350); 758 }); 759 }); 760 761 // Visit Type â auto-advance 762 $$('.visit-type-card').forEach(function (card) { 763 card.addEventListener('click', function () { 764 $$('.visit-type-card').forEach(function (c) { c.classList.remove('selected'); }); 765 card.classList.add('selected'); 766 state.data.visitType = card.dataset.visit; 767 768 // Talk-to-team: force Phoenix + Bodify Intro Call, skip Goals/Treatment 769 if (card.dataset.visit === 'talk-first') { 770 state.data.talkFirst = true; 771 state.data.location = 'Phoenix'; 772 state.data.service = 'Bodify Intro Call'; 773 state.data.goal = ''; 774 state.data.sameDayService = ''; 775 var skipBtnTf = document.getElementById('skipToTreatment'); 776 if (skipBtnTf) skipBtnTf.style.display = 'none'; 777 var sameDayElTf = document.querySelector('.same-day-question'); 778 if (sameDayElTf) sameDayElTf.style.display = 'none'; 779 renderCalendar(); // Phoenix-specific closed days 780 updateLeadProgress(); 781 setTimeout(function () { goTo(STEP_DATETIME); }, 350); 782 return; 783 } 784 785 state.data.talkFirst = false; 786 updateLeadProgress(); 787 788 // Hide "I Already Know What I Want!" for virtual (treatments are in-person only) 789 var skipBtn = document.getElementById('skipToTreatment'); 790 if (skipBtn) { 791 skipBtn.style.display = card.dataset.visit === 'virtual' ? 'none' : ''; 792 } 793 794 // Hide same-day service question for virtual 795 var sameDayEl = document.querySelector('.same-day-question'); 796 if (sameDayEl) { 797 sameDayEl.style.display = card.dataset.visit === 'virtual' ? 'none' : ''; 798 } 799 if (card.dataset.visit === 'virtual') { 800 state.data.sameDayService = ''; 801 } 802 803 setTimeout(function () { goTo(STEP_GOALS); }, 350); 804 }); 805 }); 806 807 // Goals â auto-advance, map to service 808 $$('.goal-card').forEach(function (card) { 809 card.addEventListener('click', function () { 810 $$('.goal-card').forEach(function (c) { c.classList.remove('selected'); }); 811 card.classList.add('selected'); 812 var goal = card.dataset.goal; 813 state.data.goal = goal; 814 if (state.data.visitType === 'virtual') { 815 state.data.service = VIRTUAL_GOAL_MAP[goal] || 'Virtual CoolSculpting Elite Consultation - Zoom'; 816 } else { 817 state.data.service = GOAL_MAP[goal] || 'CoolSculpting Elite Consultation'; 818 } 819 state.skippedConsultation = false;
820 updateLeadProgress(); 821 setTimeout(function () { goTo(STEP_DATETIME); }, 350); 822 }); 823 }); 824 825 // Skip consultation â book treatment 826 $('#skipToTreatment').addEventListener('click', function () { 827 state.skippedConsultation = true; 828 state.data.goal = 'treatment'; 829 goTo(STEP_TREATMENT); 830 }); 831 832 // Treatment cards â auto-advance 833 $$('.treatment-card').forEach(function (card) { 834 card.addEventListener('click', function () { 835 $$('.treatment-card').forEach(function (c) { c.classList.remove('selected'); }); 836 card.classList.add('selected'); 837 state.data.service = card.dataset.treatment; 838 updateLeadProgress(); 839 setTimeout(function () { goTo(STEP_DATETIME); }, 350); 840 }); 841 }); 842 843 // Calendar nav 844 $('#calPrev').addEventListener('click', function () { 845 state.calendarDate.setDate(1); 846 state.calendarDate.setMonth(state.calendarDate.getMonth() - 1); 847 renderCalendar(); 848 }); 849 850 $('#calNext').addEventListener('click', function () { 851 state.calendarDate.setDate(1); 852 state.calendarDate.setMonth(state.calendarDate.getMonth() + 1); 853 renderCalendar(); 854 }); 855 856 // Time slots 857 $$('.time-slot').forEach(function (slot) { 858 slot.addEventListener('click', function () { 859 $$('.time-slot').forEach(function (s) { s.classList.remove('selected'); }); 860 slot.classList.add('selected'); 861 state.data.preferredTime = slot.dataset.time; 862 $('#dateTimeError').textContent = ''; 863 }); 864 }); 865 866 // Date/Time continue â control: â Phone; name-first / talk-first: â Name 867 $('#dateTimeNext').addEventListener('click', function () { 868 if (validateDateTime()) { 869 updateLeadProgress(); 870 goTo((state.data.talkFirst || IS_NAME_FIRST) ? STEP_NAME : STEP_PHONE, 'forward'); 871 } 872 }); 873 874 // Same-day service buttons 875 $$('.same-day-btn').forEach(function (btn) { 876 btn.addEventListener('click', function () { 877 $$('.same-day-btn').forEach(function (b) { b.classList.remove('selected'); }); 878 btn.classList.add('selected'); 879 state.data.sameDayService = btn.dataset.sameday; 880 }); 881 }); 882 883 // Email â complete lead â confirmation 884 $('#emailNext').addEventListener('click', function () { 885 if (validateEmail()) { 886 var lead = completeLeadOnEmail(); 887 renderSummary(lead); 888 goTo(STEP_CONFIRM); 889 } 890 }); 891 892 // ===== Cherry Application â iframe variant ===== 893 // Clicking Apply on ip-done opens Cherry's hosted application in an iframe. 894 // 3s fallback timer reveals "Open in new tab" if Cherry's X-Frame-Options blocks embedding. 895 var CHERRY_IFRAME_BASE = 'https://pay.withcherry.com/bodify/phone'; 896 $('#cherryYesBtn').addEventListener('click', function () { 897 state.data.cherryClicked = 'Yes'; 898 trackStep(STEP_CONFIRM); 899 900 var overlay = $('#cherryIframeOverlay'); 901 var iframe = $('#cherryIframe'); 902 var fallback = $('#iframeFallback'); 903 var openLink = $('#iframeOpenNew'); 904 905 iframe.src = CHERRY_IFRAME_BASE; 906 openLink.href = CHERRY_IFRAME_BASE; 907 fallback.hidden = true; 908 909 var loaded = false; 910 iframe.addEventListener('load', function () { loaded = true; }, { once: true }); 911 setTimeout(function () { if (!loaded) fallback.hidden = false; }, 3000); 912 913 overlay.style.display = ''; 914 window.scrollTo(0, 0); 915 }); 916 917 // Reset 918 $('#resetBtn').addEventListener('click', function () { 919 state.data = { 920 firstName: '', 921 lastName: '', 922 phone: '', 923 email: '', 924 location: '', 925 visitType: '', 926 talkFirst: false, 927 goal: '', 928 service: '', 929 preferredDate: '', 930 preferredTime: '', 931 sameDayService: '', 932 }; 933 state.calendarDate = new Date(); 934 state.skippedConsultation = false; 935 state.leadId = null; 936 state.leadCreatedAt = null; 937 938 $$('input').forEach(function (inp) { 939 inp.value = ''; 940 inp.classList.remove('invalid'); 941 }); 942 $$('.field-error').forEach(function (el) { el.textContent = ''; }); 943 $$('.location-card').forEach(function (c) { c.classList.remove('selected'); }); 944 $$('.visit-type-card').forEach(function (c) { c.classList.remove('selected'); }); 945 $$('.goal-card').forEach(function (c) { c.classList.remove('selected'); }); 946 $$('.treatment-card').forEach(function (c) { c.classList.remove('selected'); }); 947 $$('.time-slot').forEach(function (s) { s.classList.remove('selected'); }); 948 $$('.same-day-btn').forEach(function (b) { b.classList.remove('selected'); }); 949 $$('.cal-day').forEach(function (d) { d.classList.remove('selected'); });
950 $('#timeSlots').classList.remove('visible'); 951 952 renderCalendar(); 953 goTo(STEP_WELCOME, 'back'); 954 }); 955 956 // Keyboard 957 document.addEventListener('keydown', function (e) { 958 if (e.key === 'Enter') { 959 var step = state.currentStep; 960 if (step === STEP_WELCOME) $('#startBtn').click(); 961 else if (step === STEP_PHONE) $('#phoneNext').click(); 962 else if (step === STEP_NAME) $('#nameNext').click(); 963 else if (step === STEP_DATETIME) $('#dateTimeNext').click(); 964 else if (step === STEP_EMAIL) $('#emailNext').click(); 965 } 966 }); 967 968 // ===== Availability from Booker extension ===== 969 // Listen for real-time availability data from the Chrome extension 970 window.addEventListener('message', function (event) { 971 if (!event.data || event.data.type !== 'bodify-availability') return; 972 973 var availability = event.data.availability; 974 if (!availability || Object.keys(availability).length === 0) return; 975 976 console.log('[Bodify Intake] Received availability:', availability); 977 978 // Store it for use when filtering time slots 979 state.availability = availability; 980 981 // If we're on the date/time step, apply the filter now 982 if (state.currentStep === STEP_DATETIME) { 983 filterTimeSlots(); 984 } 985 }); 986 987 // Init 988 updateProgress(); 989 updateBackBtn(); 990 renderCalendar(); 991 } 992 993 // ===== Fetch availability from Google Sheet ===== 994 var AVAILABILITY_URL = 'https://script.google.com/macros/s/AKfycbyU2QyCNLWYo19JoM3Vlo54Dhizolv0iqRTkiQFgadSVqEdCAEniHe8ieC4nCeLhuHq/exec'; 995 996 function fetchSheetAvailability(date, location) { 997 var params = new URLSearchParams({ 998 action: 'readAvailability', 999 location: location || '', 1000 _: String(Date.now()) 1001 }); 1002 1003 fetch(AVAILABILITY_URL + '?' + params.toString(), { cache: 'no-store' }) 1004 .then(function (res) { return res.json(); }) 1005 .then(function (data) { 1006 var availability = normalizeSheetAvailability((data && data.availability) || {}); 1007 state._sheetFetched = true; 1008 if (Object.keys(availability).length > 0) { 1009 console.log('[Bodify Intake] Sheet availability loaded:', Object.keys(availability)); 1010 if (!state.availability) state.availability = {}; 1011 for (var l in availability) { 1012 if (!state.availability[l]) state.availability[l] = {}; 1013 for (var d in availability[l]) { 1014 state.availability[l][d] = availability[l][d]; 1015 } 1016 } 1017 if (state.currentStep === STEP_DATETIME) { 1018 filterTimeSlots(); 1019 } 1020 } else { 1021 console.log('[Bodify Intake] No availability data found for', location); 1022 } 1023 }) 1024 .catch(function (err) { 1025 console.log('[Bodify Intake] Sheet availability fetch error:', err); 1026 state._sheetFetched = true; 1027 }); 1028 } 1029 1030 // Parse a single CSV row handling quoted fields with commas 1031 function parseCSVRow(line) { 1032 var result = []; 1033 var current = ''; 1034 var inQuotes = false; 1035 for (var i = 0; i < line.length; i++) { 1036 var ch = line[i]; 1037 if (ch === '"') { 1038 inQuotes = !inQuotes; 1039 } else if (ch === ',' && !inQuotes) { 1040 result.push(current); 1041 current = ''; 1042 } else { 1043 current += ch; 1044 } 1045 } 1046 result.push(current); 1047 return result; 1048 } 1049 1050 function normalizeSheetAvailability(availability) { 1051 var normalized = {}; 1052 if (!availability || typeof availability !== 'object') return normalized; 1053 for (var loc in availability) { 1054 var locationDates = availability[loc]; 1055 if (!locationDates || typeof locationDates !== 'object') continue; 1056 if (!normalized[loc]) normalized[loc] = {}; 1057 for (var dateKey in locationDates) { 1058 var entry = locationDates[dateKey] || {}; 1059 var slots = entry.slots || entry; 1060 normalized[loc][dateKey] = { slots: {} }; 1061 for (var col in slots) { 1062 if (!slots.hasOwnProperty(col)) continue; 1063 var values = Array.isArray(slots[col]) ? slots[col] : String(slots[col] || '').split(','); 1064 normalized[loc][dateKey].slots[col] = values.map(normalizeSheetTime).filter(
1064Boolean); 1065 } 1066 } 1067 } 1068 return normalized; 1069 } 1070 1071 function normalizeSheetTime(value) { 1072 var text = String(value || '').trim(); 1073 if (!text) return ''; 1074 var direct = text.match(/^(\d{1,2}):(\d{2})\s*([AP]M)$/i); 1075 if (direct) { 1076 return formatTimeLabel(parseInt(direct[1], 10), parseInt(direct[2], 10), direct[3]); 1077 } 1078 var parsed = new Date(text); 1079 if (!isNaN(parsed.getTime()) && parsed.getFullYear() < 1901) { 1080 return formatTimeLabel(parsed.getHours(), parsed.getMinutes()); 1081 } 1082 return text; 1083 } 1084 1085 function formatTimeLabel(hours, minutes, meridiem) { 1086 if (meridiem) { 1087 return String(hours) + ':' + String(minutes).padStart(2, '0') + ' ' + meridiem.toUpperCase(); 1088 } 1089 var suffix = hours >= 12 ? 'PM' : 'AM'; 1090 var displayHour = hours % 12; 1091 if (displayHour === 0) displayHour = 12; 1092 return displayHour + ':' + String(minutes).padStart(2, '0') + ' ' + suffix; 1093 } 1094 1095 // ===== Filter time slots based on Booker availability ===== 1096 // Availability structure: { "Phoenix": { "Thu, Mar 12, 2026": { slots: {...} }, ... }, ... } 1097 function filterTimeSlots() { 1098 var loadingEl = document.getElementById('availabilityLoading'); 1099 var timeGrid = document.getElementById('timeGrid'); 1100 var noAvailEl = document.getElementById('noAvailability'); 1101 1102 // Start with spinner visible, time grid hidden 1103 if (noAvailEl) noAvailEl.style.display = 'none'; 1104 if (timeGrid) timeGrid.style.display = 'none'; 1105 if (loadingEl) loadingEl.style.display = 'flex'; 1106 1107 var isGilbert = (state.data.location || '').toLowerCase() === 'gilbert'; 1108 // Gilbert doesn't need service selection â always uses Jessica column 1109 if (!state.availability || (!state.data.service && !isGilbert) || !state.data.preferredDate) { 1110 if (loadingEl) loadingEl.style.display = 'none'; 1111 return; 1112 } 1113 1114 var serviceColumns = state.data.service ? SERVICE_COLUMN_MAP[state.data.service] : null; 1115 if (!serviceColumns && !isGilbert) return; 1116 1117 var selectedDate = state.data.preferredDate; // "2026-03-12" 1118 1119 // Find matching location data 1120 var matchingDateData = null; 1121 for (var locationKey in state.availability) { 1122 var loc = (state.data.location || '').toLowerCase(); 1123 var key = locationKey.toLowerCase(); 1124 var matchesLocation = !loc || key.indexOf(loc) !== -1 || loc.indexOf(key) !== -1; 1125 if (!matchesLocation) continue; 1126 1127 var locationDates = state.availability[locationKey]; 1128 if (!locationDates || typeof locationDates !== 'object') continue; 1129 1130 // Search through all scraped dates for this location 1131 for (var scrapedDate in locationDates) { 1132 if (datesMatch(scrapedDate, selectedDate)) { 1133 matchingDateData = locationDates[scrapedDate]; 1134 break; 1135 } 1136 } 1137 if (matchingDateData) break; 1138 } 1139 1140 if (!matchingDateData) { 1141 // Show loading spinner â data not yet scraped for this date 1142 if (loadingEl) loadingEl.style.display = 'flex'; 1143 if (timeGrid) timeGrid.style.display = 'none'; 1144 console.log('[Bodify Intake] No data for', selectedDate, 'â showing loader (requesting from Booker...)'); 1145 1146 // Fallback: after 10 seconds, show no availability 1147 if (!state._loadingTimer) { 1148 state._loadingTimer = setTimeout(function () { 1149 state._loadingTimer = null; 1150 if (loadingEl && loadingEl.style.display !== 'none') { 1151 console.log('[Bodify Intake] Loading timeout â showing no availability'); 1152 if (loadingEl) loadingEl.style.display = 'none'; 1153 if (timeGrid) timeGrid.style.display = 'none'; 1154 if (noAvailEl) noAvailEl.style.display = 'block'; 1155 } 1156 }, 10000); 1157 } 1158 return; 1159 } 1160 1161 // Data found â cancel any loading timer 1162 if (state._loadingTimer) { 1163 clearTimeout(state._loadingTimer); 1164 state._loadingTimer = null; 1165 } 1166 1167 console.log('[Bodify Intake] Filtering for', state.data.service, 'at', state.data.location, 'on', selectedDate); 1168 console.log('[Bodify Intake] matchingDateData:', JSON.stringify(matchingDateData)); 1169 1170 // Collect available times across relevant columns 1171 var availableTimes = {}; 1172 var slots = matchingDateData.slots || matchingDateData; 1173 1174 if (isGilbert) { 1175 // Gilbert: ONLY use Jessica column 1176 var jessicaSlots = slots['Jessica']; 1177 if (jessicaSlots && Array.isArray(jessicaSlots)) {
1178 jessicaSlots.forEach(function (time) { 1179 availableTimes[time] = true; 1180 }); 1181 } 1182 } else if (!serviceColumns) { 1183 for (var colName in slots) { 1184 var colSlots = slots[colName]; 1185 if (colSlots && Array.isArray(colSlots)) { 1186 colSlots.forEach(function (time) { 1187 availableTimes[time] = true; 1188 }); 1189 } 1190 } 1191 } else { 1192 serviceColumns.forEach(function (colName) { 1193 var colSlots = slots[colName]; 1194 if (colSlots && Array.isArray(colSlots)) { 1195 colSlots.forEach(function (time) { 1196 availableTimes[time] = true; 1197 }); 1198 } 1199 }); 1200 } 1201 1202 // Gilbert opens at 10 AM â never offer 9 AM 1203 if (isGilbert) delete availableTimes['9:00 AM']; 1204 1205 // Weekend hour restrictions â hard rules 1206 var loc = (state.data.location || '').toLowerCase(); 1207 var selParts = selectedDate.split('-'); 1208 var selDateObj = new Date(parseInt(selParts[0]), parseInt(selParts[1]) - 1, parseInt(selParts[2])); 1209 var selDay = selDateObj.getDay(); 1210 if (loc === 'scottsdale' && selDay === 6) { 1211 delete availableTimes['4:00 PM']; 1212 delete availableTimes['5:00 PM']; 1213 } 1214 if (loc === 'phoenix' && selDay === 6) { 1215 delete availableTimes['3:00 PM']; 1216 delete availableTimes['4:00 PM']; 1217 delete availableTimes['5:00 PM']; 1218 } 1219 1220 var availableList = Object.keys(availableTimes); 1221 console.log('[Bodify Intake] Available times:', availableList); 1222 1223 if (loadingEl) loadingEl.style.display = 'none'; 1224 1225 if (availableList.length === 0) { 1226 console.log('[Bodify Intake] No available slots for this date/service â showing no availability'); 1227 if (timeGrid) timeGrid.style.display = 'none'; 1228 if (noAvailEl) noAvailEl.style.display = 'block'; 1229 return; 1230 } 1231 1232 // Show only available slots 1233 if (timeGrid) timeGrid.style.display = ''; 1234 $$('.time-slot').forEach(function (slot) { 1235 var time = slot.dataset.time; 1236 slot.style.display = availableTimes[time] ? '' : 'none'; 1237 }); 1238 } 1239 1240 // Compare scraped date string ("Thu, Mar 12, 2026") to selected date ("2026-03-12") 1241 function datesMatch(scrapedDate, selectedDate) { 1242 if (!scrapedDate || !selectedDate) return false; 1243 try { 1244 var scraped = new Date(scrapedDate); 1245 var parts = selectedDate.split('-'); 1246 var selected = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); 1247 return scraped.getFullYear() === selected.getFullYear() && 1248 scraped.getMonth() === selected.getMonth() && 1249 scraped.getDate() === selected.getDate(); 1250 } catch (e) { 1251 return false; 1252 } 1253 } 1254 1255 // Service â Booker column mapping (mirrors booker-content.js) 1256 var SERVICE_COLUMN_MAP = { 1257 // In-person consultations 1258 'CoolSculpting Elite Consultation': ['CoolSculpting 1', 'CoolSculpting 2'], 1259 'CoolTone Consultation': ['CoolSculpting 1', 'CoolSculpting 2'], 1260 'Sofwave Consultation': ['Sofwave/Morpheus8'], 1261 'The Bodify Method Consultation': ['CoolSculpting 1', 'CoolSculpting 2'], 1262 // In-person treatments ("I Already Know What I Want!") 1263 'CoolSculpting Treatment': ['CoolSculpting 1', 'CoolSculpting 2'], 1264 'Morpheus8 Boost': ['Sofwave/Morpheus8'], 1265 'Sofwave Treatment': ['Sofwave/Morpheus8'], 1266 // Virtual consultations 1267 'Virtual CoolSculpting Elite Consultation - Zoom': ['Virtual Consult'], 1268 'Virtual Morpheus8 Consultation - Zoom': ['Virtual Consult'], 1269 'Virtual Sofwave Consultation - Zoom': ['Virtual Consult'], 1270 // Talk-to-team phone consult â Phoenix only, Virtual Consult column 1271 'Bodify Intro Call': ['Virtual Consult'], 1272 }; 1273 1274 if (document.readyState === 'loading') { 1275 document.addEventListener('DOMContentLoaded', init); 1276 } else { 1277 init(); 1278 } 1279})();
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.