// ============================================================ // Happy Fence Quote Quiz — React 18 + Babel Standalone // Loaded as an external script so WordPress doesn't mangle // the && operators in this file. // // Update log: // 2026-05-08 - Promoted A2P 10DLC SMS consent to its own dedicated // final screen, but kept the user-facing step counter at 5 (not 6) // so the consent reads as a final-confirmation continuation of // Step 5 rather than a new piece of information being collected. // Implementation: TOTAL = 6 internally for state navigation, but // TOTAL_DISPLAY = 5 drives the progress bar fill, "Step X of Y" // label, and time estimate. The consent screen's StepHeading // kicker shows "Final step" (no number). Step 6 renders a yellow // "One last thing" card with custom 26px brand checkbox, a // state-aware helper line ("↑ Check the box to unlock submit" // -> "✓ All set - click submit below"), and a full-width submit // button whose disabled vs active treatment is unmistakable. // Step 5 (contact form) needs a manual "Next ->" button since it // no longer holds submit. Auto-advance still applies to steps // 1-4. Modal disclosure + Zapier consent payload (sms_consent, // sms_consent_text, sms_consent_at) carry forward unchanged. // ============================================================ // ZAPIER WEBHOOK // 1. In Zapier: Create Zap -> "Webhooks by Zapier" -> "Catch Hook" // 2. Copy the webhook URL // 3. Paste it below, replacing PASTE_YOUR_ZAPIER_WEBHOOK_URL_HERE const ZAPIER_WEBHOOK_URL = "PASTE_YOUR_ZAPIER_WEBHOOK_URL_HERE"; // Internal step count includes the consent screen as step 6. // TOTAL_DISPLAY is what the user sees in the progress bar / counter // — we keep it at 5 so the consent screen reads as a continuation // of step 5, not a new step. const TOTAL = 6; const TOTAL_DISPLAY = 5; // A2P 10DLC required disclosure shown in the consent popup. // Must match what the visible UI tells the user they're agreeing to. const SMS_CONSENT_TEXT = "By submitting, you authorize Happy Fence Company LLC to text/call the number above for informational/transactional messages, possibly using automated means. Msg/data rates apply, msg frequency varies. Consent is not a condition of purchase. See terms (https://www.leadconnectorhq.com/terms2) and privacy policy (https://www.leadconnectorhq.com/privacy-policy). Text HELP for help and STOP to unsubscribe."; const MATERIALS = [ { key:'wood', name:'Wood', price:'$', life:'Lasts 10\u201315 years', blurb:'Pressure-treated pine or cedar. Classic look, warm character.', color:'#a0785a' }, { key:'durafence', name:'Durafence', price:'$$', life:'Lasts 40+ years', blurb:'Galvanized steel. Hurricane-tough. Bulletproof.', color:'var(--yellow-dk)' }, { key:'vinyl', name:'Vinyl', price:'$$$', life:'Lasts 20\u201330 years', blurb:'Zero maintenance, fade-resistant. HOA favorite.', color:'var(--blue-dk)' }, { key:'aluminum', name:'Custom Aluminum', price:'$$$$', life:'Lasts 30+ years', blurb:'Decorative, pool-code compliant. Built to last.', color:'#8a8f99' }, { key:'unsure', name:'Not sure \u2014 help me decide', price:'\u2014', life:'Expert recommends on the call', blurb:"Tell us the goal \u2014 we'll match the right material.", color:'#ccd5de' }, ]; const FEET = [ { key:'u80', name:'Under 80 ft', sub:'Small yard / partial section' }, { key:'80_150', name:'80 \u2013 150 ft', sub:'Typical backyard' }, { key:'150_250',name:'150 \u2013 250 ft', sub:'Full property perimeter' }, { key:'250p', name:'250+ ft', sub:'Large lot / double lot' }, { key:'unsure', name:'Not sure', sub:"We'll measure on the walkthrough" }, ]; const TIMELINE = [ { key:'asap', name:'ASAP', sub:'Within 2 weeks', tag:'Priority' }, { key:'2_6', name:'2 \u2013 6 weeks', sub:'Standard booking' }, { key:'1_3m', name:'1 \u2013 3 months', sub:'Planning ahead' }, { key:'browse', name:'Just exploring', sub:'Gathering info' }, ]; const PERMITS = [ { key:'yes', name:'Yes, please', sub:'Pull permits for me' }, { key:'no', name:'No thanks', sub:"I've got it handled" }, { key:'unsure', name:'Not sure', sub:"We'll walk you through it" }, ]; function ProgressBar({ step, compact }) { // Clamp the displayed step to TOTAL_DISPLAY so the consent screen // (internal step 6) reads as "Step 5 of 5" with the bar at 100%. const displayStep = Math.min(step, TOTAL_DISPLAY); const pct = (displayStep / TOTAL_DISPLAY) * 100; return (
Step {displayStep} of {TOTAL_DISPLAY}
~{Math.max(0, (TOTAL_DISPLAY - displayStep + 1) * 15)} sec left
); } function StepHeading({ kicker, title, subtitle, compact }) { return (
{kicker}

{title}

{subtitle ?

{subtitle}

: null}
); } function Tile({ selected, onClick, children, minH }) { return ( ); } function Step1_Material({ value, onPick, compact }) { return (
{MATERIALS.map(m => ( onPick(m.key)} minH={compact ? 120 : 150}>
{m.name} {m.price}
{m.life}

{m.blurb}

))}
); } function Step2_Feet({ value, onPick, compact }) { return (
{FEET.map(f => ( onPick(f.key)} minH={compact ? 72 : 92}>
{f.name}
{f.sub}
))}
); } function Step3_Timeline({ value, onPick, compact }) { return (
{TIMELINE.map(t => ( onPick(t.key)} minH={compact ? 80 : 100}>
{t.name} {t.tag ? {t.tag} : null}
{t.sub}
))}
); } function Step4_Permits({ value, onPick, compact }) { return (
{PERMITS.map(p => ( onPick(p.key)} minH={compact ? 80 : 110}>
{p.name}
{p.sub}
))}
); } function TextField({ label, name, placeholder, value, onChange, type, required, compact }) { const t = type || 'text'; return (
onChange(e.target.value)} style={{ width:'100%', padding: compact ? '12px 14px' : '14px 16px', fontFamily:'"Quicksand",sans-serif', fontSize: compact ? 15 : 16, background:'var(--cream)', border:'2px solid var(--ink)', borderRadius:8, boxShadow:'3px 3px 0 var(--ink)', boxSizing:'border-box', color:'var(--ink)', outline:'none', }} onFocus={e=>e.target.style.boxShadow='5px 5px 0 var(--ink)'} onBlur={e=>e.target.style.boxShadow='3px 3px 0 var(--ink)'} />
); } function ConsentModal({ open, onClose, compact }) { React.useEffect(() => { if (!open) return; const onKey = e => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = prevOverflow; }; }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()} style={{ background:'var(--cream)', border:'2px solid var(--ink)', borderRadius:12, boxShadow:'6px 6px 0 var(--ink)', maxWidth:540, width:'100%', maxHeight:'85vh', overflowY:'auto', padding: compact ? '24px 22px 22px' : '28px 30px 26px', position:'relative', boxSizing:'border-box', }}>
Messaging consent

SMS Terms

{"By submitting, you authorize Happy Fence Company LLC to text/call the number above for informational/transactional messages, possibly using automated means. Msg/data rates apply, msg frequency varies. Consent is not a condition of purchase. "} See terms {" and "} privacy policy {". Text HELP for help and STOP to unsubscribe."}

); } function Step5_Contact({ data, onChange, compact }) { return (
onChange('name', v)} /> onChange('phone', v)} /> onChange('email', v)} /> onChange('address', v)} />
{['Morning','Afternoon','Evening','Anytime'].map(t => ( ))}
What happens next
    {[ 'A rep calls you to grab the basics', 'You send us a short backyard video', 'We book a Zoom walkthrough (often within 48 hrs)', 'You get your full quote on the Zoom', ].map((t,i) => (
  1. {i+1} {t}
  2. ))}
); } function Step6_Consent({ consent, onConsentToggle, onShowConsentTerms, onSubmit, sending, compact }) { return (
!
One last thing
onConsentToggle(!consent)} role="button" aria-pressed={consent} tabIndex={0} onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onConsentToggle(!consent); } }} style={{ display:'flex', alignItems:'flex-start', gap:12, background:'var(--cream)', border:'2px solid var(--ink)', borderRadius:8, padding:'12px 14px', marginBottom: compact ? 12 : 14, cursor:'pointer', }} >
{consent ? ( ) : null}
{"I agree to receive "}SMS & calls{" from Happy Fence Company at the number above. "}
{consent ? ( {'All set — click submit below'} ) : '↑ Check the box to unlock submit'}
); } function SuccessScreen({ compact }) { return (

You're in good hands.

{"A rep will reach out shortly to grab your basics and request a short video of your backyard. Once we have it, we'll book a Zoom walkthrough \u2014 usually within 48 hours \u2014 and you'll get your full quote live on that call."}

Don't want to wait?
Call (786) 882-6056
{['Quote on the Zoom call','$500 off','8/10 founder spots taken'].map(c => (
{'\u2713 '}{c}
))}
); } function Quiz({ compact }) { const isCompact = compact === true; const [step, setStep] = React.useState(1); const [answers, setAnswers] = React.useState({ material:null, feet:null, timeline:null, permits:null, contact:{} }); const [submitted, setSubmitted] = React.useState(false); const [sending, setSending] = React.useState(false); const [consent, setConsent] = React.useState(false); const [consentModalOpen, setConsentModalOpen] = React.useState(false); const setContact = (k, v) => setAnswers(a => ({ ...a, contact: { ...a.contact, [k]: v } })); const contactReady = Boolean( answers.contact.name ) && Boolean( answers.contact.phone ) && Boolean( answers.contact.email ) && Boolean( answers.contact.address ); const canAdvance = ({ 1: answers.material, 2: answers.feet, 3: answers.timeline, 4: answers.permits, 5: contactReady, 6: consent, })[step]; const buildPayload = () => { const materialMap = Object.fromEntries(MATERIALS.map(m => [m.key, m.name])); const feetMap = Object.fromEntries(FEET.map(f => [f.key, f.name])); const timelineMap = Object.fromEntries(TIMELINE.map(t => [t.key, t.name])); const permitsMap = Object.fromEntries(PERMITS.map(p => [p.key, p.name])); return { name: answers.contact.name || '', phone: answers.contact.phone || '', email: answers.contact.email || '', address: answers.contact.address || '', best_time_to_call: answers.contact.time || '', material: materialMap[answers.material] || '', linear_feet: feetMap[answers.feet] || '', timeline: timelineMap[answers.timeline] || '', permits: permitsMap[answers.permits] || '', material_key: answers.material, feet_key: answers.feet, timeline_key: answers.timeline, permits_key: answers.permits, source: 'happyfencecompany.com /get-a-quote/', submitted_at: new Date().toISOString(), page_url: typeof window !== 'undefined' ? window.location.href : '', user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '', // A2P 10DLC / TCPA consent record — keep these so the Zap // logs the exact disclosure shown to the user and the time // they actively checked the consent box. sms_consent: consent ? 'yes' : 'no', sms_consent_text: SMS_CONSENT_TEXT, sms_consent_at: consent ? new Date().toISOString() : '', }; }; const submitToZapier = async () => { if (!ZAPIER_WEBHOOK_URL) return; if (ZAPIER_WEBHOOK_URL.indexOf('PASTE_YOUR_ZAPIER_WEBHOOK_URL_HERE') !== -1) { console.warn('Zapier webhook URL not set. Skipping webhook POST.'); return; } try { setSending(true); await fetch(ZAPIER_WEBHOOK_URL, { method: 'POST', body: JSON.stringify(buildPayload()), }); } catch (e) { console.warn('Zapier webhook failed:', e); } finally { setSending(false); } }; const next = async () => { if (step < TOTAL) { setStep(s => s + 1); return; } if (!canAdvance) return; if (!consent) return; // safety — UI also disables the button await submitToZapier(); setSubmitted(true); }; const back = () => setStep(s => Math.max(1, s - 1)); const autoAdvance = (val, key) => { setAnswers(a => ({ ...a, [key]: val })); setTimeout(() => setStep(s => Math.min(TOTAL, s + 1)), 260); }; if (submitted) return ; const backDisabled = step === 1 || sending; return (
{step === 1 ? autoAdvance(v,'material')} compact={isCompact}/> : null} {step === 2 ? autoAdvance(v,'feet')} compact={isCompact}/> : null} {step === 3 ? autoAdvance(v,'timeline')} compact={isCompact}/> : null} {step === 4 ? autoAdvance(v,'permits')} compact={isCompact}/> : null} {step === 5 ? ( ) : null} {step === 6 ? ( setConsentModalOpen(true)} onSubmit={next} sending={sending} compact={isCompact} /> ) : null}
{step === TOTAL ? null : ( step === 5 ? ( ) : (
{'Pick to continue \u2192'}
) )}
setConsentModalOpen(false)} compact={isCompact} />
); } function QuizRoot(){ const q = '(max-width: 700px)'; const [compact, setCompact] = React.useState( () => typeof window !== 'undefined' && window.matchMedia(q).matches ); React.useEffect(() => { const mq = window.matchMedia(q); const handler = e => setCompact(e.matches); if (mq.addEventListener) { mq.addEventListener('change', handler); } else { mq.addListener(handler); } return () => { if (mq.removeEventListener) { mq.removeEventListener('change', handler); } else { mq.removeListener(handler); } }; }, []); return ; } ReactDOM.createRoot(document.getElementById('quiz-root')).render();