!function(d){ const FP_CSS='https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css',FP_JS='https://cdn.jsdelivr.net/npm/flatpickr',CONFETTI_JS='https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js'; const inj={css:h=>{const l=d.createElement('link');l.rel='stylesheet';l.href=h;d.head.appendChild(l)},js:(s,c)=>{const e=d.createElement('script');e.src=s;if(c)e.onload=c;d.head.appendChild(e)}}; inj.css(FP_CSS);inj.js(FP_JS,initVF); function initVF(){ inj.js(CONFETTI_JS); const vf=d.createElement('script');vf.src='https://cdn.voiceflow.com/widget-next/bundle.mjs';vf.type='text/javascript'; vf.onload=()=>{ let V; const MZ=(()=>{const BP=1024;let m,o,c=0;const en=()=>{if(innerWidth>BP)return;if(!m){m=d.querySelector('meta[name="viewport"]');if(!m){m=d.createElement('meta');m.name='viewport';d.head.appendChild(m)}o=m.getAttribute('content')||'width=device-width, initial-scale=1'}c++;m.setAttribute('content','width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')},de=()=>{if(innerWidth>BP)return;setTimeout(()=>{c=Math.max(0,c-1);if(m&&c===0&&o!=null)m.setAttribute('content',o)},50)},bl=()=>{try{d.activeElement?.blur?.()}catch(e){}};return{enable:en,disable:de,blurActive:bl}})(); const CH=(()=>{const A='data-vf-hide-composer',CSS='.vfrc-input-container._1gdvh9t1{display:none!important}.vfrc-footer,.vfrc-composer,.vfrc-message-composer,.vfrc-chat-input-container{display:none!important}';let hid=false,roots=new Set,obs;const all=()=>{const res=new Set([d]),q=[d],seen=new Set(q);while(q.length){const r=q.shift(),nodes=(r instanceof Document||r instanceof ShadowRoot)?r.querySelectorAll('*'):[];nodes.forEach(el=>{if(el&&el.shadowRoot&&!seen.has(el.shadowRoot)){seen.add(el.shadowRoot);res.add(el.shadowRoot);q.push(el.shadowRoot)}})}return[...res]},add=r=>{if(!r||!(r instanceof Document||r instanceof ShadowRoot))return;if(r.querySelector(`style[${A}]`))return;const s=d.createElement('style');s.setAttribute(A,'1');s.textContent=CSS;try{r.appendChild(s);roots.add(r)}catch(e){}},rem=r=>{const s=r.querySelector?.(`style[${A}]`);if(s)try{s.remove()}catch(e){}roots.delete(r)};return{hide(){hid=true;all().forEach(add);if(!obs){obs=new MutationObserver(()=>{if(hid)all().forEach(add)});obs.observe(d.documentElement,{childList:true,subtree:true})}},show(){hid=false;roots.forEach(rem);roots.clear();obs?.disconnect?.();obs=null}}})(); const AH=(()=>{const SEL='.vfrc-avatar,[class*="vfrc-avatar"]',obsMap=new WeakMap(),tidy=c=>{c.querySelectorAll(SEL).forEach(a=>{a.style.display='none';a.style.visibility='hidden';a.style.width='0';a.style.margin='0';a.style.padding='0'})},find=n=>{let e=n;for(let i=0;i<12&&e;i++){if(e.querySelector?.(SEL))return e;e=e.parentElement||(e.getRootNode&&e.getRootNode().host)||null}return null},hideFor=node=>{const c=find(node);if(!c)return;tidy(c);if(obsMap.has(c))return;const mo=new MutationObserver(muts=>{let changed=false;for(const m of muts){if(m.addedNodes&&m.addedNodes.length)changed=true}if(changed)tidy(c)});mo.observe(c,{childList:true,subtree:true});obsMap.set(c,mo);setTimeout(()=>{mo.disconnect();obsMap.delete(c)},1200)};return{hideFor}})(); const SI=(()=>({inject:(css,attr)=>{const roots=[],q=[d],seen=new Set(q);while(q.length){const r=q.shift();roots.push(r);const nodes=(r instanceof Document||r instanceof ShadowRoot)?r.querySelectorAll('*'):[];nodes.forEach(el=>{if(el&&el.shadowRoot&&!seen.has(el.shadowRoot)){seen.add(el.shadowRoot);q.push(el.shadowRoot)}})}roots.forEach(r=>{if(!r||!(r instanceof Document||r instanceof ShadowRoot))return;if(r.querySelector(`style[${attr}]`))return;const s=d.createElement('style');s.setAttribute(attr,'1');s.textContent=css;try{r.appendChild(s)}catch(e){}})}}))(); SI.inject('.vf-wide-card{width:100%!important;max-width:100%!important;display:block!important}.vf-wide-card>.vf-form-card{width:100%!important;max-width:100%!important}.vf-savefx-success{background:#00bf63!important;transition:background .22s ease,transform .22s ease,opacity .3s ease}.vf-savefx-bounce{transform:translateY(-1px) scale(1.02)}.vf-savefx-hide{opacity:0;transform:translateY(-6px) scale(.98);pointer-events:none;transition:opacity .28s ease,transform .28s ease}.vf-progress-card{padding:16px;border:1px solid #e5e7eb;border-radius:8px;margin:12px 0;background:#fafafa;width:100%}.vf-progress-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.vf-progress-title{font-weight:700;color:#111827;font-size:14px}.vf-progress-sub{color:#6b7280;font-size:12px}.vf-progress-track{position:relative;height:10px;background:#e5e7eb;border-radius:9999px;overflow:hidden}.vf-progress-fill{position:absolute;inset:0 auto 0 0;width:0;height:100%;border-radius:9999px;background:linear-gradient(90deg,#4f46e5,#3b82f6,#06b6d4,#22c55e);transition:width .7s cubic-bezier(.22,1,.36,1)}.vf-step-dots{display:flex;justify-content:space-between;margin-top:10px}.vf-step-dot{width:22px;height:22px;border-radius:50%;background:#e5e7eb;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;box-shadow:inset 0 0 0 2px #fff}.vf-step-dot.complete{background:#22c55e}.vf-step-dot.current{background:#3b82f6;animation:vfPulse 1.2s ease-in-out infinite}@keyframes vfPulse{0%{transform:scale(1);box-shadow:0 0 0 0 rgba(59,130,246,.55)}70%{box-shadow:0 0 0 10px rgba(59,130,246,0)}100%{transform:scale(1)}}.vf-pros-card{padding:16px;border:1px solid #e5e7eb;border-radius:12px;margin:12px 0;background:#f8fafc;width:100%}.vf-pros-hero{display:flex;gap:14px;align-items:center}.vf-hero{position:relative;width:72px;height:72px;min-width:72px;min-height:72px;flex:0 0 auto;border-radius:50%;background:linear-gradient(135deg,#10b981,#22c55e);box-shadow:0 10px 26px rgba(16,185,129,.35)}.vf-hero-ring{position:absolute;inset:-6px;border-radius:50%;border:2px solid rgba(16,185,129,.35);animation:vfRing 1.4s ease-out forwards}@keyframes vfRing{0%{transform:scale(.7);opacity:.9}100%{transform:scale(1.35);opacity:0}}.vf-check{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.vf-check path{stroke:#fff;stroke-width:6;fill:none;stroke-linecap:round;stroke-linejoin:round}.vf-sparks{position:absolute;inset:0;pointer-events:none}.vf-spark{position:absolute;color:#fcd34d;opacity:0;font-size:14px;transform:translate(0,0) scale(.7);animation:vfSpark 1.1s ease-out forwards}@keyframes vfSpark{0%{opacity:0;transform:translate(0,0) scale(.7) rotate(0)}30%{opacity:1}100%{opacity:0;transform:translate(var(--tx,0),var(--ty,0)) scale(1) rotate(20deg)}}.vf-pros-title{font-size:16px;font-weight:800;color:#0f172a}.vf-pros-sub{font-size:14px;color:#334155;margin-top:2px}.vf-pros-meta{font-size:12px;color:#64748b;margin-top:8px}.vf-pros-stars{display:flex;gap:2px;margin-top:6px;color:#f59e0b}@media(max-width:640px){.vf-pros-hero{flex-direction:column;align-items:stretch}.vf-hero{align-self:center;margin-bottom:8px}}','data-vf-styles'); SI.inject('.vfrc-assistant-info .vfrc-avatar.g931q10.g931q13{display:block!important;visibility:visible!important;width:300px!important;height:300px!important;margin:initial!important;padding:initial!important}', 'data-vf-show-specific-avatar'); SI.inject('.vf-loader-card{padding:16px;border:1px solid #e5e7eb;border-radius:12px;margin:12px 0;background:#f8fafc;width:100%}.vf-loader-wrap{display:flex;align-items:center;gap:14px}.vf-spinner{width:46px;height:46px;border-radius:9999px;border:4px solid #dbeafe;border-top-color:#0551fb;animation:vfSpin 1s linear infinite;flex:0 0 auto}@keyframes vfSpin{to{transform:rotate(360deg)}}.vf-loader-text{display:flex;flex-direction:column}.vf-loader-title{font-weight:800;color:#0f172a;font-size:16px}.vf-loader-sub{color:#334155;font-size:14px;margin-top:2px}.vf-dots{display:inline-flex;gap:4px;margin-top:6px}.vf-dot{width:6px;height:6px;border-radius:9999px;background:#0551fb;opacity:.35;animation:vfBounce 1s ease-in-out infinite}.vf-dot:nth-child(2){animation-delay:.15s}.vf-dot:nth-child(3){animation-delay:.3s}@keyframes vfBounce{0%,80%,100%{transform:translateY(0);opacity:.35}40%{transform:translateY(-6px);opacity:1}}','data-vf-loader-styles'); function vfClosestWrapper(n){let e=n,h=0;while(e&&h<20){const c=(e.className||'')+'';if(e.getAttribute?.('role')==='listitem'||/vfrc-.*(message|bubble|response|chat|row)/i.test(c))return e;e=e.parentNode||(e.getRootNode&&e.getRootNode().host)||null;h++}return null} function removeIfEmptyWrapper(el){try{if(!el)return;if(el.childElementCount===0&&(el.textContent||'').trim()===''){const w=vfClosestWrapper(el)||el.parentNode;if(w)try{w.remove()}catch(e){}}}catch(e){}} function forceRemoveWrapper(el){ try{ if(!el)return; let current=el; for(let i=0;i<15;i++){ if(!current)break; const classList=current.className||''; if(classList.includes('vfrc-system-response')){ current.style.display='none'; current.style.height='0'; current.style.margin='0'; current.style.padding='0'; current.style.overflow='hidden'; break; } current=current.parentElement||(current.getRootNode&¤t.getRootNode().host)||null; } }catch(e){} } const __VF_LOADERS=new Set(),__vfParsePayload=t=>{try{return typeof t?.payload==='string'?JSON.parse(t.payload):(t?.payload||{})}catch(_){return{}}}; function __vfRemoveAllLoaders(){const now=Date.now();__VF_LOADERS.forEach(x=>{const{node,contentEl,started,minMs}=x,delay=Math.max(0,(started+minMs)-now);setTimeout(()=>{try{node?.remove()}catch(e){}removeIfEmptyWrapper(contentEl);try{__VF_LOADERS.delete(x)}catch(e){}if(__VF_LOADERS.size===0){CH.show();MZ.disable()}},delay)})} /* ==================== TRACKING UTILITY ==================== */ const TRACK = (() => { const sessions = new Map(); function trackEvent(name, params) { if (typeof gtag !== 'function') return; try { gtag('event', name, params); } catch (e) {} } function trackStart(key) { sessions.set(key, Date.now()); } function trackComplete(key, extra = {}) { const start = sessions.get(key); if (!start) return; const dur = Math.round((Date.now() - start) / 1000); trackEvent(`${key}_complete`, { ...extra, duration_seconds: dur }); } return { trackEvent, trackStart, trackComplete, sessions }; })(); /* ==================== TRACKING EXTENSIONS ==================== */ const ConversationStartTracker = { name: 'conversationStart', type: 'response', match: ({trace}) => trace?.type === 'conversationStart' || trace?.payload?.name === 'conversationStart', render: ({element}) => { TRACK.trackStart('conversation'); TRACK.trackEvent('conversation_start', { event_category: 'engagement', event_label: 'User opened Voiceflow chatbot' }); forceRemoveWrapper(element); return () => {}; } }; const JobDiagnosisStartTracker = { name: 'jobDiagnoseStart', type: 'response', match: ({trace}) => trace?.type === 'jobDiagnoseStart' || trace?.payload?.name === 'jobDiagnoseStart', render: ({element}) => { TRACK.trackStart('job_diagnosis'); TRACK.trackEvent('job_diagnosis_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const JobDiagnosisCompleteTracker = { name: 'jobDiagnoseComplete', type: 'response', match: ({trace}) => trace?.type === 'jobDiagnoseComplete' || trace?.payload?.name === 'jobDiagnoseComplete', render: ({trace, element}) => { const payload = __vfParsePayload(trace); TRACK.trackComplete('job_diagnosis', { job_type: payload.job_diagnosed || 'unknown' }); forceRemoveWrapper(element); return () => {}; } }; const LocationCaptureStartTracker = { name: 'locationCaptureStart', type: 'response', match: ({trace}) => trace?.type === 'locationCaptureStart' || trace?.payload?.name === 'locationCaptureStart', render: ({element}) => { TRACK.trackStart('location_capture'); TRACK.trackEvent('location_capture_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const LocationCaptureCompleteTracker = { name: 'locationCaptureCompleted', type: 'response', match: ({trace}) => trace?.type === 'locationCaptureCompleted' || trace?.payload?.name === 'locationCaptureCompleted', render: ({trace, element}) => { const payload = __vfParsePayload(trace); TRACK.trackComplete('location_capture', { suburb: payload.suburb || 'unknown' }); forceRemoveWrapper(element); return () => {}; } }; const CheckForProsStartTracker = { name: 'checkForProsStart', type: 'response', match: ({trace}) => trace?.type === 'checkForProsStart' || trace?.payload?.name === 'checkForProsStart', render: ({element}) => { TRACK.trackStart('check_for_pros'); TRACK.trackEvent('check_for_pros_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const CheckForProsCompleteTracker = { name: 'checkForProsCompleted', type: 'response', match: ({trace}) => trace?.type === 'checkForProsCompleted' || trace?.payload?.name === 'checkForProsCompleted', render: ({trace, element}) => { const payload = __vfParsePayload(trace); TRACK.trackComplete('check_for_pros', { pros_found: payload.prosFoundCount || 0 }); forceRemoveWrapper(element); return () => {}; } }; const DateTimeCaptureStartTracker = { name: 'dateTimeCaptureStart', type: 'response', match: ({trace}) => trace?.type === 'dateTimeCaptureStart' || trace?.payload?.name === 'dateTimeCaptureStart', render: ({element}) => { TRACK.trackStart('datetime_capture'); TRACK.trackEvent('datetime_capture_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const DateTimeCaptureCompleteTracker = { name: 'dateTimeCaptureCompleted', type: 'response', match: ({trace}) => trace?.type === 'dateTimeCaptureCompleted' || trace?.payload?.name === 'dateTimeCaptureCompleted', render: ({trace, element}) => { const payload = __vfParsePayload(trace); TRACK.trackComplete('datetime_capture', { selected_date: payload.date || 'unknown' }); forceRemoveWrapper(element); return () => {}; } }; const FinalDetailsCaptureStartTracker = { name: 'finalDetailsCaptureStart', type: 'response', match: ({trace}) => trace?.type === 'finalDetailsCaptureStart' || trace?.payload?.name === 'finalDetailsCaptureStart', render: ({element}) => { TRACK.trackStart('final_details_capture'); TRACK.trackEvent('final_details_capture_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const FinalDetailsCaptureCompleteTracker = { name: 'finalDetailsCaptureCompleted', type: 'response', match: ({trace}) => trace?.type === 'finalDetailsCaptureCompleted' || trace?.payload?.name === 'finalDetailsCaptureCompleted', render: ({element}) => { TRACK.trackComplete('final_details_capture'); forceRemoveWrapper(element); return () => {}; } }; const FinalAPIStartTracker = { name: 'finalAPIstart', type: 'response', match: ({trace}) => trace?.type === 'finalAPIstart' || trace?.payload?.name === 'finalAPIstart', render: ({element}) => { TRACK.trackStart('final_api'); TRACK.trackEvent('final_api_start', { event_category: 'funnel' }); forceRemoveWrapper(element); return () => {}; } }; const FinalAPICompleteTracker = { name: 'finalAPIcompleted', type: 'response', match: ({trace}) => trace?.type === 'finalAPIcompleted' || trace?.payload?.name === 'finalAPIcompleted', render: ({element}) => { TRACK.trackComplete('final_api'); forceRemoveWrapper(element); return () => {}; } }; const JobPostSuccessTracker = { name: 'jobPostSuccess', type: 'response', match: ({trace}) => trace?.type === 'jobPostSuccess' || trace?.payload?.name === 'jobPostSuccess', render: ({element}) => { const conversationStart = TRACK.sessions.get('conversation') || Date.now(); const totalDuration = Math.round((Date.now() - conversationStart) / 1000); TRACK.trackEvent('job_post_success', { event_category: 'conversion', event_label: 'Booking completed successfully', total_duration_seconds: totalDuration, value: 1 }); if (typeof gtag === 'function') { gtag('event', 'conversion', { 'send_to': 'G-BKZVM55MCM', 'value': 1.0, 'currency': 'ZAR' }); } forceRemoveWrapper(element); return () => {}; } }; const LO={name:'ext_loading_on',type:'response',match:({trace})=>trace?.type==='ext_loading_on'||trace?.payload?.name==='ext_loading_on',render:({trace,element})=>{const p=__vfParsePayload(trace),label=p.label||'Confirming your booking details',minMs=Math.max(1000,Number(p.min_ms||0));CH.hide();MZ.enable();const w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card vf-loader-card';w.setAttribute('role','status');w.setAttribute('aria-live','polite');w.setAttribute('aria-busy','true');w.innerHTML=`
Please wait a moment…
${label}
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);__VF_LOADERS.add({node:w,contentEl:element,wrapper:vfClosestWrapper(element),started:Date.now(),minMs});return()=>{}}}; const LF={name:'ext_loading_off',type:'response',match:({trace})=>trace?.type==='ext_loading_off'||trace?.payload?.name==='ext_loading_off',render:()=>{__vfRemoveAllLoaders();return()=>{}}}; const LO1={name:'ext_loading_on_first',type:'response',match:({trace})=>trace?.type==='ext_loading_on_first'||trace?.payload?.name==='ext_loading_on_first',render:({trace,element})=>{const p=__vfParsePayload(trace),label=p.label||'Please wait a moment...',minMs=Math.max(1000,Number(p.min_ms||0));CH.hide();MZ.enable();const w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card vf-loader-card';w.setAttribute('role','status');w.setAttribute('aria-live','polite');w.setAttribute('aria-busy','true');w.innerHTML=`
Looking for nearby Pros
${label}
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);__VF_LOADERS.add({node:w,contentEl:element,wrapper:vfClosestWrapper(element),started:Date.now(),minMs});return()=>{}}}; const LF1={name:'ext_loading_off_first',type:'response',match:({trace})=>trace?.type==='ext_loading_off_first'||trace?.payload?.name==='ext_loading_off_first',render:()=>{__vfRemoveAllLoaders();return()=>{}}}; const SF=(()=>{function hideBoth(a,b){if(a)a.classList.add('vf-savefx-hide');if(b)b.classList.add('vf-savefx-hide');setTimeout(()=>{if(a)a.style.display='none';if(b)b.style.display='none'},320)}return{celebrate(btn,sib){if(!btn)return;btn.classList.add('vf-savefx-success','vf-savefx-bounce');try{const r=btn.getBoundingClientRect();if(typeof confetti=='function')confetti({origin:{x:(r.left+r.width/2)/innerWidth,y:(r.top+r.height/2)/innerHeight},particleCount:24,spread:54,startVelocity:22,ticks:120,scalar:.8,zIndex:999999999})}catch(e){}setTimeout(()=>btn.classList.remove('vf-savefx-bounce'),240);setTimeout(()=>hideBoth(btn,sib),1e3)},hideBoth}})(); const stretch=(el,wrap)=>{try{el.style.width=el.style.maxWidth='100%';el.style.alignSelf='stretch'}catch(e){}try{wrap.style.width=wrap.style.maxWidth='100%'}catch(e){}let p=el.parentElement;for(let i=0;i<4&&p;i++){try{if(p.style){p.style.width=p.style.maxWidth='100%';p.style.flex='1 1 100%';p.style.alignSelf='stretch'}}catch(e){}p=p.parentElement||(p.getRootNode&&p.getRootNode().host)||null}}; const currencyR=n=>typeof n=='number'?'R '+n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,' '):n,setVar=(k,v)=>{try{V?.setVariable?.(k,v)}catch(e){}}; const makeShell=({label,description,htmlField,submitText='Save',errorText='Please fix the error above.'})=>{const w=d.createElement('div');w.className='vf-wide-card';w.innerHTML=`
${description?`

${description}

`:''}${htmlField}
`;if(!d.getElementById('vf-anti-zoom-style')){const s=d.createElement('style');s.id='vf-anti-zoom-style';s.textContent='@media(max-width:1024px){.vf-anti-zoom input,.vf-anti-zoom textarea,.vf-anti-zoom select{font-size:16px!important;-webkit-text-size-adjust:100%}}';d.head.appendChild(s)}return w}; /* ---------- Job Photos (composer hidden + Skip + auto-scroll) ---------- */ const JPE={name:'jobPhotosExtension',type:'response',match:({trace})=>trace?.type==='jobPhotosExtension'||trace?.payload?.name==='jobPhotosExtension',render:({element})=>{ CH.hide(); MZ.enable(); const shell=makeShell({ label:'Image upload (optional)', description:'This helps your Pro assess and prepare. You can choose multiple files.', submitText:'Upload Photos', errorText:'Upload failed β€” please try again.', htmlField:`
πŸ“·
Image upload skipped
You can continue without photos
` }); element.appendChild(shell);stretch(element,shell);AH.hideFor(shell); const input=shell.querySelector('.vf-job-photos'), head =shell.querySelector('.vf-files-head'), list =shell.querySelector('.vf-file-list'), prog =shell.querySelector('.vf-progress'), bar =shell.querySelector('.vf-bar'), err =shell.querySelector('.vf-error'), save =shell.querySelector('.vf-submit'), skippedMsg =shell.querySelector('.vf-skipped-message'), label = shell.querySelector('label'), description = shell.querySelector('.vf-desc'); /* create Skip button next to Upload */ const row=d.createElement('div');row.style.cssText='display:flex;gap:8px;margin-top:10px'; const skip=d.createElement('button'); skip.type='button';skip.className='vf-skip'; // Initially skip is blue (primary action) skip.style.cssText='flex:1;padding:12px 16px;background:#0551fb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;font-size:16px'; skip.textContent='Skip'; save.style.flex='1';save.style.marginTop='0'; save.parentNode.insertBefore(row,save);row.appendChild(save);row.appendChild(skip); save.disabled=true;save.style.background='#9ca3af';save.style.cursor='not-allowed'; let files=[], imagePreviews=[]; const fmt=b=>{if(!b)return'0 Bytes';const k=1024,u=['Bytes','KB','MB','GB'];const i=Math.floor(Math.log(b)/Math.log(k));return(b/Math.pow(k,i)).toFixed(2)+' '+u[i]}; const enable=()=>{ save.disabled=false; save.style.background='#0551fb'; save.style.cursor='pointer'; // When files selected, make skip secondary (grey but still clickable) skip.style.background='#9ca3af'; }; const disable=()=>{ save.disabled=true; save.style.background='#9ca3af'; save.style.cursor='not-allowed'; // When no files, make skip primary (blue) skip.style.background='#0551fb'; }; // Generate image previews const generatePreviews = async (fileList) => { imagePreviews = []; for (const file of fileList) { if (file.type.startsWith('image/')) { try { const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => resolve(e.target.result); reader.onerror = reject; reader.readAsDataURL(file); }); imagePreviews.push(dataUrl); } catch (e) { console.warn('Failed to generate preview for', file.name); } } } }; function renderList(){ list.innerHTML=''; if(!files.length){head.style.display='none';disable();return} head.style.display='block'; files.forEach((f,i)=>{ const row=d.createElement('div'); row.className='vf-file-row'; row.innerHTML=`
${f.name}
${fmt(f.size)} β€’ ${(f.type.split('/')[1]||'IMAGE').toUpperCase()}
`; list.appendChild(row); }); list.querySelectorAll('.vf-remove').forEach(b=>b.onclick=()=>{ const idx = +b.dataset.i; files.splice(idx,1); imagePreviews.splice(idx,1); renderList(); }); enable(); requestAnimationFrame(()=>save.scrollIntoView({behavior:'smooth',block:'center'})); } input.onchange=async e=>{ files=[...Array.from(e.target.files||[])]; await generatePreviews(files); renderList(); }; const showProg=()=>{prog.style.display='block';bar.style.width='0%'}; const setProg=p=>{bar.style.width=p+'%'}; const hideProg=()=>{prog.style.display='none'}; /* Upload */ save.onclick=async()=>{ if(!files.length)return; err.style.display='none';disable();input.disabled=true;showProg();save.textContent='Uploading…'; try{ const fd=new FormData();files.forEach(f=>fd.append('photos',f)); let p=0;const t=setInterval(()=>{p=Math.min(96,p+Math.random()*18);setProg(Math.round(p))},160); const res=await fetch('https://europe-west1-kandua-manager.cloudfunctions.net/jobPhotoCapture',{method:'POST',body:fd}); clearInterval(t);setProg(100); if(!res.ok)throw new Error('status '+res.status); const data=await res.json();hideProg(); if(data.success){ V.interact({type:'captured',payload:{photosUploaded:true,photoCount:files.length,uploadedFiles:data.uploadedImages||[],message:`${files.length} photos uploaded successfully`}}); // Hide form elements and show success message if(label) label.style.display='none'; if(description) description.style.display='none'; input.style.display='none'; head.style.display='none'; list.style.display='none'; prog.style.display='none'; err.style.display='none'; row.style.display='none'; // Show success message with uploaded image previews const successMsg=d.createElement('div'); successMsg.className='vf-skipped-message'; successMsg.style.display='block'; let imagePreviewsHtml = ''; if(imagePreviews.length > 0) { const imageElements = imagePreviews.map(preview => `Uploaded photo` ).join(''); imagePreviewsHtml = `
${imageElements}
`; } successMsg.innerHTML=`
βœ…
Upload successful
${files.length} photo${files.length !== 1 ? 's' : ''} uploaded
${imagePreviewsHtml} `; shell.querySelector('.vf-form-card').appendChild(successMsg); successMsg.scrollIntoView({behavior:'smooth',block:'center'}); CH.show(); MZ.disable(); }else throw new Error(data.message||'Upload failed'); }catch(e){ hideProg();save.textContent='Upload Photos';enable();input.disabled=false; err.textContent='Upload failed β€” please try again.';err.style.display='block'; } }; /* Skip */ skip.onclick=()=>{ // Hide form elements if(label) label.style.display='none'; if(description) description.style.display='none'; input.style.display='none'; head.style.display='none'; list.style.display='none'; prog.style.display='none'; err.style.display='none'; row.style.display='none'; // Show skipped message skippedMsg.style.display='block'; skippedMsg.scrollIntoView({behavior:'smooth',block:'center'}); // Trigger Voiceflow interaction V.interact({type:'captured',payload:{photosUploaded:false,photoCount:0,uploadedFiles:[],skipped:true}}); // Re-enable composer and disable mobile zoom CH.show(); MZ.disable(); }; return()=>{CH.show(); MZ.disable()}; }}; /* -------------------- Calendar -------------------- */ const CE={name:'calendar-picker',type:'response',match:({trace})=>trace?.type==='calendar'||trace?.payload?.name==='calendar-picker',render:({element})=>{CH.hide();MZ.enable();const shell=makeShell({label:'Preferred Date',description:'',submitText:'Confirm Date',errorText:'Please pick a date before confirming.',htmlField:`
Choose a date and then click "Confirm Date" to continue.
`});element.appendChild(shell);stretch(element,shell);AH.hideFor(shell);const calHost=shell.querySelector('.vf-calendar'),err=shell.querySelector('.vf-error'),save=shell.querySelector('.vf-submit');save.disabled=true;const picked=d.createElement('div');picked.className='vf-picked';picked.innerHTML='Date selected: ';save.insertAdjacentElement('afterend',picked);const pickedVal=picked.querySelector('.vf-picked-val'),fmtNice=iso=>{try{const[y,m,dd]=iso.split('-').map(Number),dt=new Date(y,m-1,dd);return dt.toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}catch{return iso}},toISODateLocal=dt=>{const y=dt.getFullYear(),m=String(dt.getMonth()+1).padStart(2,'0'),d2=String(dt.getDate()).padStart(2,'0');return`${y}-${m}-${d2}`},now=new Date,today=new Date(now.getFullYear(),now.getMonth(),now.getDate()),max=new Date(today.getFullYear()+1,11,31);let selectedISO=null;const fp=flatpickr(calHost,{inline:true,appendTo:calHost,monthSelectorType:'dropdown',minDate:today,maxDate:max,showMonths:1,disableMobile:true,dateFormat:'Y-m-d',defaultDate:null,onReady:(_d,_s,inst)=>{const h=calHost.querySelector('.flatpickr-current-month');if(h&&!h.querySelector('.vf-year-select')){const ys=d.createElement('select');ys.className='vf-year-select';for(let y=today.getFullYear();y<=max.getFullYear();y++){const o=d.createElement('option');o.value=o.textContent=String(y);ys.appendChild(o)}ys.value=String(inst.currentYear||today.getFullYear());ys.addEventListener('change',()=>inst.changeYear(+ys.value));h.appendChild(ys)}},onYearChange:(_d,_s,inst)=>{const ys=calHost.querySelector('.vf-year-select');if(ys)ys.value=String(inst.currentYear)},onMonthChange:(_d,_s,inst)=>{const ys=calHost.querySelector('.vf-year-select');if(ys)ys.value=String(inst.currentYear)},onChange:(_d,s)=>{selectedISO=s;if(s){save.disabled=false;err.style.display='none';picked.style.display='block';pickedVal.textContent=fmtNice(s)}}});shell.querySelectorAll('.vf-chip').forEach(b=>b.addEventListener('click',()=>{const q=b.getAttribute('data-q');let dt=new Date(today);if(q==='tomorrow')dt.setDate(dt.getDate()+1);else if(q==='in2')dt.setDate(dt.getDate()+2);else if(q==='weekend'){const day=dt.getDay(),delta=(6-day+7)%7;dt.setDate(dt.getDate()+delta)}else if(q==='nextweek'){const day=dt.getDay(),delta=(1-day+7)%7||7;dt.setDate(dt.getDate()+delta)}selectedISO=toISODateLocal(dt);try{fp?.setDate(dt,true)}catch(e){}save.disabled=false;err.style.display='none';picked.style.display='block';pickedVal.textContent=fmtNice(selectedISO)}));save.onclick=()=>{if(!selectedISO){err.style.display='block';return}err.style.display='none';SF.celebrate(save);['label','.vf-desc','.vf-cal-wrap'].forEach(s=>{const n=shell.querySelector(s);if(n)n.style.display='none'});picked.style.display='block';setVar('date',selectedISO);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{date:selectedISO,urgent:false}})};return()=>{CH.show();MZ.disable()}}}; /* -------------------- Location Picker -------------------- */ const LP = { name: 'ext_location_picker', type: 'response', match: ({trace}) => trace?.type === 'ext_location_picker' || trace?.payload?.name === 'ext_location_picker', render: ({element}) => { CH.hide(); MZ.enable(); const shell = makeShell({ label: 'Where do you need the work done?', description: 'Please enter your location below.', submitText: 'Confirm Location', errorText: 'Please select a location before continuing.', htmlField: '' + '
' + '' + '' + '
' + '
' + '' + '
' + '
' + '
Type at least 3 characters to see suggestions
' + '
' + '
Selected Location:
' + '
' + '
' + '
' + '
' + 'Getting your location...' + '
' }); element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const currentBtn = shell.querySelector('[data-method="current"]'); const searchBtn = shell.querySelector('[data-method="search"]'); const input = shell.querySelector('.vf-location-input'); const dropdownContainer = shell.querySelector('.vf-dropdown-container'); const inputHelper = shell.querySelector('.vf-input-helper'); const selectedDiv = shell.querySelector('.vf-selected-location'); const selectedAddress = shell.querySelector('.vf-selected-address'); const loading = shell.querySelector('.vf-location-loading'); const error = shell.querySelector('.vf-error'); const save = shell.querySelector('.vf-submit'); const locationButtons = shell.querySelector('.vf-location-buttons'); const autocompleteWrapper = shell.querySelector('.vf-autocomplete-wrapper'); const label = shell.querySelector('label'); const description = shell.querySelector('.vf-desc'); const fallbackLink = d.createElement('div'); fallbackLink.className = 'vf-location-fallback'; fallbackLink.innerHTML = 'Issues providing your location? Click here...'; save.insertAdjacentElement('afterend', fallbackLink); const fallbackAnchor = fallbackLink.querySelector('a'); let selectedLocation = null; let geocoder = null; let autocompleteService = null; let placesService = null; let sessionToken = null; let currentPredictions = []; save.disabled = true; save.style.background = '#9ca3af'; save.style.cursor = 'not-allowed'; function enableSubmit() { save.disabled = false; save.style.background = '#0551fb'; save.style.cursor = 'pointer'; } function showLoading(text) { loading.querySelector('span').textContent = text || 'Getting your location...'; loading.style.display = 'flex'; } function hideLoading() { loading.style.display = 'none'; } function showError(message) { error.textContent = message; error.style.display = 'block'; hideLoading(); } function hideError() { error.style.display = 'none'; } function showSelected(location) { selectedLocation = location; selectedAddress.textContent = location.formattedAddress; selectedDiv.classList.add('show'); inputHelper.classList.remove('show'); dropdownContainer.classList.remove('show'); enableSubmit(); hideError(); hideLoading(); // Hide input after selection on mobile if (window.innerWidth <= 768) { input.style.display = 'none'; autocompleteWrapper.style.marginBottom = '0'; } requestAnimationFrame(() => { save.scrollIntoView({behavior: 'smooth', block: 'center'}); }); } function extractSuburb(addressComponents) { const suburbTypes = ['sublocality_level_1', 'locality', 'administrative_area_level_3']; for (const type of suburbTypes) { const component = addressComponents.find(comp => comp.types.includes(type)); if (component) return component.long_name; } return ''; } // Render predictions in our custom dropdown function renderPredictions(predictions) { dropdownContainer.innerHTML = ''; if (!predictions || predictions.length === 0) { dropdownContainer.classList.remove('show'); return; } // Limit to 2 results const limitedPredictions = predictions.slice(0, 2); currentPredictions = limitedPredictions; limitedPredictions.forEach((prediction, index) => { const item = d.createElement('div'); item.className = 'vf-prediction-item'; item.style.cssText = 'padding:16px;cursor:pointer;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;-webkit-tap-highlight-color:transparent;transition:background 0.15s ease'; // Add icon const icon = d.createElement('div'); icon.style.cssText = 'width:20px;height:20px;flex-shrink:0;color:#6b7280'; icon.innerHTML = 'πŸ“'; item.appendChild(icon); // Add text content const textContainer = d.createElement('div'); textContainer.style.cssText = 'flex:1;min-width:0'; const mainText = d.createElement('div'); mainText.style.cssText = 'font-weight:600;color:#111827;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; mainText.textContent = prediction.structured_formatting.main_text; const secondaryText = d.createElement('div'); secondaryText.style.cssText = 'color:#6b7280;font-size:13px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; secondaryText.textContent = prediction.structured_formatting.secondary_text; textContainer.appendChild(mainText); textContainer.appendChild(secondaryText); item.appendChild(textContainer); // Remove last border if (index === limitedPredictions.length - 1) { item.style.borderBottom = 'none'; } // Hover effect item.addEventListener('mouseenter', () => { item.style.background = '#eff6ff'; }); item.addEventListener('mouseleave', () => { item.style.background = 'white'; }); // Click handler item.addEventListener('click', () => { selectPrediction(prediction); }); // Touch handlers for mobile item.addEventListener('touchstart', () => { item.style.background = '#dbeafe'; }, { passive: true }); item.addEventListener('touchend', () => { item.style.background = '#eff6ff'; }, { passive: true }); dropdownContainer.appendChild(item); }); dropdownContainer.classList.add('show'); } // Get place details and select location function selectPrediction(prediction) { if (!placesService) return; showLoading('Getting location details...'); dropdownContainer.classList.remove('show'); placesService.getDetails( { placeId: prediction.place_id, fields: ['address_components', 'formatted_address', 'geometry', 'name'], sessionToken: sessionToken }, (place, status) => { hideLoading(); if (status === google.maps.places.PlacesServiceStatus.OK && place) { const location = { formattedAddress: place.formatted_address, suburb: extractSuburb(place.address_components || []), lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }; // Set input value to selected address input.value = place.formatted_address; showSelected(location); // Create new session token for next search sessionToken = new google.maps.places.AutocompleteSessionToken(); } else { showError('Could not get location details. Please try another address.'); } } ); } function loadGoogleMaps() { return new Promise((resolve, reject) => { if (window.google && window.google.maps) { resolve(); return; } const script = d.createElement('script'); script.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyC51PJx9faiS3ptAxYuMfkYKKtTU00Tz-Q&libraries=places'; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load Google Maps')); d.head.appendChild(script); }); } async function initializeGoogleMaps() { try { await loadGoogleMaps(); geocoder = new google.maps.Geocoder(); autocompleteService = new google.maps.places.AutocompleteService(); // Create a hidden div for PlacesService (it requires a map or div) const placesDiv = d.createElement('div'); placesDiv.style.display = 'none'; d.body.appendChild(placesDiv); placesService = new google.maps.places.PlacesService(placesDiv); // Create session token sessionToken = new google.maps.places.AutocompleteSessionToken(); setupAutocomplete(); } catch (err) { showError('Failed to load location services. Please refresh and try again.'); } } function setupAutocomplete() { let searchTimeout; // Handle input changes input.addEventListener('input', () => { const value = input.value.trim(); // Clear previous timeout clearTimeout(searchTimeout); // Update helper text if (value.length > 0 && value.length < 3) { inputHelper.textContent = `Type ${3 - value.length} more character${3 - value.length === 1 ? '' : 's'}`; inputHelper.classList.add('show'); dropdownContainer.classList.remove('show'); return; } else if (value.length === 0) { inputHelper.classList.remove('show'); dropdownContainer.classList.remove('show'); return; } inputHelper.textContent = 'Select an address from the suggestions below'; inputHelper.classList.add('show'); // Debounce search searchTimeout = setTimeout(() => { if (value.length >= 3) { performSearch(value); } }, 300); }); // Prevent space from causing issues input.addEventListener('keydown', (e) => { if (e.key === ' ' || e.keyCode === 32) { e.stopPropagation(); } }); // Hide dropdown when clicking outside d.addEventListener('click', (e) => { if (!autocompleteWrapper.contains(e.target)) { dropdownContainer.classList.remove('show'); } }); } function performSearch(query) { if (!autocompleteService) return; autocompleteService.getPlacePredictions( { input: query, componentRestrictions: { country: 'za' }, types: ['address'], sessionToken: sessionToken }, (predictions, status) => { if (status === google.maps.places.PlacesServiceStatus.OK && predictions) { renderPredictions(predictions); } else if (status === google.maps.places.PlacesServiceStatus.ZERO_RESULTS) { dropdownContainer.innerHTML = '
No addresses found. Try a different search.
'; dropdownContainer.classList.add('show'); } else { dropdownContainer.classList.remove('show'); } } ); } async function getCurrentLocation() { if (!navigator.geolocation) { showError('Geolocation is not supported by your browser.'); return; } if (location.protocol !== 'https:' && location.hostname !== 'localhost') { showError('Location access requires a secure (HTTPS) connection.'); return; } if (!geocoder) { showError('Location services are still loading. Please try again in a moment.'); return; } showLoading('Getting your current location...'); hideError(); try { const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, timeout: 15000, maximumAge: 300000 }); }); const lat = position.coords.latitude; const lng = position.coords.longitude; showLoading('Finding your address...'); const response = await new Promise((resolve, reject) => { geocoder.geocode({ location: { lat, lng } }, (results, status) => { if (status === 'OK' && results && results.length > 0) { resolve(results[0]); } else { reject(new Error(`Geocoding failed: ${status}`)); } }); }); const location = { formattedAddress: response.formatted_address, suburb: extractSuburb(response.address_components || []), lat: lat, lng: lng }; showSelected(location); } catch (error) { hideLoading(); let errorMessage = 'Could not get your location: '; if (error.code) { switch (error.code) { case error.PERMISSION_DENIED: errorMessage += 'Location access was denied. Please enable location access in your browser settings.'; break; case error.POSITION_UNAVAILABLE: errorMessage += 'Your location information is unavailable. Please try entering your address manually.'; break; case error.TIMEOUT: errorMessage += 'Location request timed out. Please try again or enter your address manually.'; break; default: errorMessage += 'An unknown error occurred. Please try entering your address manually.'; } } else { errorMessage = 'Could not determine your address. Please enter it manually.'; } showError(errorMessage); } } currentBtn.addEventListener('click', () => { currentBtn.classList.add('active'); searchBtn.classList.remove('active'); input.style.display = 'none'; dropdownContainer.classList.remove('show'); inputHelper.classList.remove('show'); selectedDiv.classList.remove('show'); getCurrentLocation(); }); searchBtn.addEventListener('click', () => { searchBtn.classList.add('active'); currentBtn.classList.remove('active'); input.style.display = 'block'; input.value = ''; dropdownContainer.classList.remove('show'); selectedDiv.classList.remove('show'); selectedLocation = null; save.disabled = true; save.style.background = '#9ca3af'; hideLoading(); hideError(); setTimeout(() => { input.focus(); }, 100); }); fallbackAnchor.addEventListener('click', (e) => { e.preventDefault(); V.interact({ type: 'captured', payload: { locationFallback: true } }); MZ.blurActive(); CH.show(); MZ.disable(); }); save.onclick = () => { if (!selectedLocation) { showError('Please select a location before continuing.'); return; } hideError(); SF.celebrate(save); setTimeout(() => { if (label) label.style.display = 'none'; if (description) description.style.display = 'none'; if (locationButtons) locationButtons.style.display = 'none'; if (autocompleteWrapper) autocompleteWrapper.style.display = 'none'; if (loading) loading.style.display = 'none'; if (error) error.style.display = 'none'; if (save) save.style.display = 'none'; if (fallbackLink) fallbackLink.style.display = 'none'; if (inputHelper) inputHelper.style.display = 'none'; }, 1000); setVar('formattedAddress', selectedLocation.formattedAddress); setVar('suburb', selectedLocation.suburb); setVar('googleLat', selectedLocation.lat.toString()); setVar('googleLong', selectedLocation.lng.toString()); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { formattedAddress: selectedLocation.formattedAddress, suburb: selectedLocation.suburb, latitude: selectedLocation.lat, longitude: selectedLocation.lng, locationConfirmed: true } }); }; initializeGoogleMaps(); return () => { CH.show(); MZ.disable(); }; } }; /* -------------------- Booking Summary -------------------- */ const BS = { name: 'ext_booking_summary', type: 'response', match: ({trace}) => { const t = trace?.type, n = trace?.payload?.name; return t === 'ext_booking_summary' || n === 'ext_booking_summary' || t === 'booking_summary' || n === 'booking_summary'; }, render: ({trace, element}) => { CH.hide(); MZ.enable(); // Get values from payload first, fall back to Voiceflow variable syntax const p = (() => { try { return typeof trace?.payload === 'string' ? JSON.parse(trace.payload) : (trace?.payload || {}); } catch (_) { return {}; } })(); const jobDiagnosed = p.job_diagnosed ?? '{{job_diagnosed}}'; const formattedAddress = p.formattedAddress ?? '{{formattedAddress}}'; const dateRequest = p.date_request ?? '{{date_request}}'; const timeDisplayed = p.time_displayed ?? '{{time_displayed}}'; const userPhone = p.user_phone ?? '{{user_phone}}'; const shell = d.createElement('div'); shell.className = 'vf-wide-card'; shell.innerHTML = `
Booking Summary
πŸ”§
Need help with:
${jobDiagnosed || 'Not specified'}
πŸ“
Address:
${formattedAddress || 'Not specified'}
πŸ“…
Date and Time:
${(dateRequest || 'Not specified')} / ${(timeDisplayed || 'Not specified')}
πŸ“ž
Phone Number:
${userPhone || 'Not specified'}
By confirming your booking, you agree to Kandua's Terms of Service and consent to us sharing your job details with a trusted and vetted service provider. Booking subject to Pro availability
`; element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const confirmBtn = shell.querySelector('.vf-summary-confirm'); const changeBtn = shell.querySelector('.vf-summary-change'); const buttonsContainer = shell.querySelector('.vf-summary-buttons'); confirmBtn.onclick = () => { setVar('confirmation', 'confirm'); SF.celebrate(confirmBtn, changeBtn); setTimeout(() => { buttonsContainer.style.display = 'none'; }, 1000); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { confirmation: 'confirm', action: 'booking_confirmed' } }); }; changeBtn.onclick = () => { setVar('confirmation', 'update'); setTimeout(() => { buttonsContainer.style.display = 'none'; }, 1000); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { confirmation: 'update', action: 'change_details' } }); }; return () => { CH.show(); MZ.disable(); }; } }; /* -------------------- Intent Picker -------------------- */ const IP = { name: 'ext_Intent', type: 'response', match: ({trace}) => trace?.type === 'ext_Intent' || trace?.payload?.name === 'ext_Intent', render: ({element}) => { CH.hide(); MZ.enable(); const shell = d.createElement('div'); shell.className = 'vf-wide-card'; shell.innerHTML = `
You selected:
βœ“
How can I help you today?
πŸ”§
Get a plumber
⚑
Get an Electrician
❓
Get guidance
πŸ’¬
Get support
We currently only support plumbing and electrical services in Johannesburg and the Cape Town area. We'll be expanding soon πŸš€
`; element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const cards = shell.querySelectorAll('.vf-intent-card'); const header = shell.querySelector('.vf-intent-header'); const grid = shell.querySelector('.vf-intent-grid'); const disclaimer = shell.querySelector('.vf-intent-disclaimer'); const selectedView = shell.querySelector('.vf-intent-selected-view'); const selectedIcon = shell.querySelector('.vf-intent-selected-icon'); const selectedValue = shell.querySelector('.vf-intent-selected-value'); const formCard = shell.querySelector('.vf-form-card'); cards.forEach(card => { card.onclick = () => { const intent = card.getAttribute('data-intent'); const label = card.getAttribute('data-label'); const icon = card.querySelector('.vf-intent-icon').textContent; // Disable all cards cards.forEach(c => { c.style.pointerEvents = 'none'; c.style.opacity = '0.4'; }); // Highlight selected card card.classList.add('selected'); card.style.opacity = '1'; // Set Voiceflow variable setVar('mainIntent', intent); // After brief delay, show compact selected view setTimeout(() => { // Hide header, grid, and disclaimer header.style.opacity = '0'; grid.style.opacity = '0'; disclaimer.style.opacity = '0'; setTimeout(() => { header.style.display = 'none'; grid.style.display = 'none'; disclaimer.style.display = 'none'; // Show selected view selectedIcon.textContent = icon; selectedValue.textContent = label; selectedView.style.display = 'flex'; // Shrink the card formCard.style.padding = '12px'; // Animate in requestAnimationFrame(() => { selectedView.style.opacity = '0'; selectedView.style.transform = 'translateY(-10px)'; requestAnimationFrame(() => { selectedView.style.transition = 'all 0.3s ease'; selectedView.style.opacity = '1'; selectedView.style.transform = 'translateY(0)'; }); }); }, 300); }, 400); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { mainIntent: intent } }); }; }); return () => { CH.show(); MZ.disable(); }; } }; const XE={name:'confetti',type:'response',match:({trace})=>trace?.type==='booking_complete',render:({element})=>{try{confetti({particleCount:100,spread:60,gravity:.6,ticks:300,zIndex:999999999})}catch(e){}setTimeout(()=>{try{confetti({particleCount:80,spread:100,gravity:.6,ticks:300,zIndex:999999999})}catch(e){}},200);setTimeout(()=>{removeIfEmptyWrapper(element)},10);return()=>{}}}; /* -------------------- Combined Full Name Extension -------------------- */ const fNm={name:'ext_fullName',type:'response',match:({trace})=>trace?.type==='ext_fullName'||trace?.payload?.name==='ext_fullName',render:({trace,element})=>{CH.hide();MZ.enable();const p=typeof trace.payload==='string'?JSON.parse(trace.payload):(trace.payload||{}),ph=p.placeholder||'John Smith',sh=makeShell({label:'Full Name',description:'Please enter your first and last name.',htmlField:``,errorText:"Please enter both your first and last name (letters, spaces, hyphens and apostrophes only)."});element.appendChild(sh);stretch(element,sh);AH.hideFor(sh);const inp=sh.querySelector('.vf-input'),err=sh.querySelector('.vf-error'),save=sh.querySelector('.vf-submit'),validate=v=>{const trimmed=v.trim();if(!trimmed)return{valid:false,error:'Please enter your full name.'};if(!/^[A-Za-zΓ€-Γ–Γ˜-ΓΆΓΈ-ΓΏ' -]+$/.test(trimmed))return{valid:false,error:'Name can only contain letters, spaces, hyphens and apostrophes.'};const words=trimmed.split(/\s+/).filter(w=>w.length>0);if(words.length<2)return{valid:false,error:'Please enter both your first and last name.'};if(trimmed.length>100)return{valid:false,error:'Name is too long. Please enter a shorter name.'};return{valid:true,words}},splitName=words=>{const firstName=words[0],lastName=words[words.length-1],fullName=words.join(' ');return{firstName,lastName,fullName}};save.onclick=()=>{const v=inp.value.trim(),validation=validate(v);if(!validation.valid){err.textContent=validation.error;err.style.display='block';return}err.style.display='none';const{firstName,lastName,fullName}=splitName(validation.words);SF.celebrate(save);setVar('firstName',firstName);setVar('lastName',lastName);setVar('fullName',fullName);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{firstName,lastName,fullName}})};return()=>{CH.show();MZ.disable()}}}; const pN={name:'ext_phone',type:'response',match:({trace})=>trace?.type==='ext_phone'||trace?.payload?.name==='ext_phone',render:({element})=>{CH.hide();MZ.enable();const sh=makeShell({label:'Phone Number',description:"Please enter a valid, 10 digit South African phone number like 082 123 4567 - We'll add +27",htmlField:``,errorText:'Please enter a 10 digit number starting with 0.'});element.appendChild(sh);stretch(element,sh);AH.hideFor(sh);const tel=sh.querySelector('.vf-phone'),err=sh.querySelector('.vf-error'),save=sh.querySelector('.vf-submit');tel.addEventListener('keydown',e=>{if(e.key===' '||e.key==='Spacebar')e.preventDefault()});tel.addEventListener('input',()=>{tel.value=tel.value.replace(/\s+/g,'')});tel.addEventListener('paste',e=>{e.preventDefault();const t=(e.clipboardData||window.clipboardData).getData('text')||'';tel.value=t.replace(/\D/g,'')});function validate(raw){const d=(raw||'').replace(/\D/g,'');if(!/^0\d{9}$/.test(d)){const n=d.length;err.textContent=`Please enter a 10 digit number starting with 0 (you entered ${n} digit${n===1?'':'s'}).`;return null}return d}save.onclick=()=>{const dgt=validate(tel.value);if(!dgt){err.style.display='block';return}err.style.display='none';const e164='+27'+dgt.slice(1);SF.celebrate(save);setVar('userPhone',e164);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{userPhone:e164}})};return()=>{CH.show();MZ.disable()}}}; const eM={name:'ext_email',type:'response',match:({trace})=>trace?.type==='ext_email'||trace?.payload?.name==='ext_email',render:({trace,element})=>{CH.hide();MZ.enable();const p=typeof trace.payload==='string'?JSON.parse(trace.payload):(trace.payload||{}),ph=p.placeholder||'email@me.co.za',w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card';w.innerHTML=`
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);const inp=w.querySelector('.vf-input'),err=w.querySelector('.vf-error'),save=w.querySelector('.vf-save'),skip=w.querySelector('.vf-skip'),isEmail=v=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);save.onclick=()=>{const v=(inp.value||'').trim();if(!v||!isEmail(v)){err.style.display='block';return}err.style.display='none';SF.celebrate(save,skip);setVar('userEmail',v);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{userEmail:v,skipped:false}})};skip.onclick=()=>{err.style.display='none';SF.hideBoth(save,skip);setVar('userEmail','');MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{userEmail:'',skipped:true}})};return()=>{CH.show();MZ.disable()}}}; const bG={name:'ext_budget',type:'response',match:({trace})=>trace?.type==='ext_budget'||trace?.payload?.name==='ext_budget',render:({element})=>{CH.hide();MZ.enable();const w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card';w.innerHTML=`

Slide to select your approximate budget.

R 300R 5 000+
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);const sl=w.querySelector('.vf-range'),ro=w.querySelector('.vf-readout'),sv=w.querySelector('.vf-save'),sk=w.querySelector('.vf-skip');let used=false;const fmt=v=>Number(v)>=5000?'R 5 000+':currencyR(Number(v)),en=()=>{sv.disabled=false;sv.style.background='#0551fb';sv.style.cursor='pointer';sk.style.background='#9ca3af';sk.style.cursor='pointer'},show=()=>{ro.style.display='block';ro.textContent=fmt(sl.value)},first=()=>{if(used)return;used=true;en();show()};sl.addEventListener('input',()=>{first();ro.textContent=fmt(sl.value)});sv.onclick=()=>{if(sv.disabled)return;const v=Number(sl.value),out=v>=5000?'R 5 000+':currencyR(v);SF.celebrate(sv,sk);setVar('budget',out);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{budget:out,skipped:false}})};sk.onclick=()=>{SF.hideBoth(sv,sk);setVar('budget','');MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{budget:'',skipped:true}})};return()=>{CH.show();MZ.disable()}}}; const aD={name:'ext_additionalDetails',type:'response',match:({trace})=>trace?.type==='ext_additionalDetails'||trace?.payload?.name==='ext_additionalDetails',render:({trace,element})=>{CH.hide();MZ.enable();const p=typeof trace.payload==='string'?JSON.parse(trace.payload):(trace.payload||{}),ph=p.placeholder||'Ring unit 19 at the gate...',w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card';w.innerHTML=`
Anything the pro should know before arriving.
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);const ta=w.querySelector('.vf-textarea'),sv=w.querySelector('.vf-save'),sk=w.querySelector('.vf-skip');sv.onclick=()=>{const v=(ta.value||'').trim();SF.celebrate(sv,sk);setVar('additionalDetails',v);MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{additionalDetails:v,skipped:false}})};sk.onclick=()=>{SF.hideBoth(sv,sk);setVar('additionalDetails','');MZ.blurActive();CH.show();MZ.disable();V.interact({type:'captured',payload:{additionalDetails:'',skipped:true}})};return()=>{CH.show();MZ.disable()}}}; function makeProgressExt({name,step,title,subtitle}){return{name,type:'response',match:({trace})=>trace?.type===name||trace?.payload?.name===name,render:({element})=>{const pct=Math.min(100,Math.max(0,step*25)),w=d.createElement('div');w.className='vf-wide-card';w.innerHTML=`
Step ${step} of 4 β€” ${title}
0%
1
2
3
4
${subtitle?`
${subtitle}
`:''}
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);const pctEl=w.querySelector('.vf-progress-pct'),fill=w.querySelector('.vf-progress-fill'),dots=[...w.querySelectorAll('.vf-step-dot')];dots.forEach((dot,i)=>{const idx=i+1;dot.classList.remove('complete','current');if(idx1-Math.pow(1-t,3);(function f(n){const t=Math.min(1,(n-st)/dur),v=Math.round(sv+dl*ez(t));fill.style.width=v+'%';pctEl.textContent=v+'%';if(t<1)requestAnimationFrame(f)})(st)}}} const P1=makeProgressExt({name:'ext_progress_step1',step:1,title:'Problem diagnosis',subtitle:'We are understanding your issue.'}), P2=makeProgressExt({name:'ext_progress_step2',step:2,title:'Location',subtitle:'Where do you need help?'}), P3=makeProgressExt({name:'ext_progress_step3',step:3,title:'Personal details',subtitle:'We are almost there.'}), P4=makeProgressExt({name:'ext_progress_step4',step:4,title:'Done',subtitle:'All set!'}); const PF={name:'ext_pros_found',type:'response',match:({trace})=>trace?.type==='ext_pros_found'||trace?.payload?.name==='ext_pros_found',render:({trace,element})=>{const p=typeof trace?.payload==='string'?(()=>{try{return JSON.parse(trace.payload)}catch(e){return{}}})():(trace?.payload||{}),c=Number(p?.count)||3,a=p?.area||'';setVar('prosFound',true);setVar('prosFoundCount',c);const w=d.createElement('div');w.className='vf-wide-card';w.innerHTML=`
β˜…β˜…β˜…β˜…β˜…β˜…
Great news!
We have found ${c}+ top-rated, vetted Pros ${a?`near ${a}`:'near you'}.
All Kandua Pros have been pre-vetted and are rated 4.5β˜… or higher. No need to worry about choosing, we will make sure the right Pro gets your job βœ….
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);try{const hero=w.querySelector('.vf-hero');if(hero&&hero.style){hero.style.flexShrink='0';hero.style.minWidth=hero.style.minWidth||'72px';hero.style.minHeight=hero.style.minHeight||'72px'}}catch(e){}try{const pth=w.querySelector('.vf-check path'),L=pth.getTotalLength();pth.style.strokeDasharray=String(L);pth.style.strokeDashoffset=String(L);requestAnimationFrame(()=>{pth.style.transition='stroke-dashoffset 700ms cubic-bezier(.22,1,.36,1)';pth.style.strokeDashoffset='0'})}catch(e){}}}; V=window.voiceflow.chat; V.load({ verify:{projectID:'693bf3b7b6d7421621be9d3e'}, url:'https://general-runtime.sanlamstudios.voiceflow.com', versionID:'production', assistant:{persistence:'memory',extensions:[ LO1,LF1,LO,LF,P1,P2,P3,P4,PF,CE,JPE,XE,fNm,pN,eM,bG,aD,LP,BS,IP, ConversationStartTracker, JobDiagnosisStartTracker, JobDiagnosisCompleteTracker, LocationCaptureStartTracker, LocationCaptureCompleteTracker, CheckForProsStartTracker, CheckForProsCompleteTracker, DateTimeCaptureStartTracker, DateTimeCaptureCompleteTracker, FinalDetailsCaptureStartTracker, FinalDetailsCaptureCompleteTracker, FinalAPIStartTracker, FinalAPICompleteTracker, JobPostSuccessTracker ]} }).then(()=>{d.querySelectorAll('.open_v,.open_v_b').forEach(el=>el.addEventListener('click',()=>V.open()))}); }; d.body.appendChild(vf); } }(document);