!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=``;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:`
Selected files
π·
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()}
Remove `;
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 =>
` `
).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:`Tomorrow In 2 days This weekend Next week (Mon)
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: '' +
'' +
'π Use Current Location ' +
'π Enter Address ' +
'
' +
'' +
'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 = `
`;
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 = `
`;
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=`Your Budget (optional) Slide to select your approximate budget.
R 300 R 5 000+
Save Budget Skip
`;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=`Additional or Important Details (optional) Anything the pro should know before arriving. Save Skip
`;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%
${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);