/* ============================================================ form.js — Estimate form validation + submission Real-time validation, phone formatting, reCAPTCHA v3 hook ============================================================ */ (function () { 'use strict'; const PHONE = /^\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}$/; const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const RECAPTCHA_SITE_KEY = '6LdqrB8rAAAAAOrBCYmtk43IzemkiK_Fb2EYU5q2'; /* --- Helpers -------------------------------------------- */ function field(el) { return el.closest('.form-field'); } function setValid(el) { const f = field(el); if (!f) return; f.classList.remove('has-error'); el.classList.add('valid'); el.classList.remove('invalid'); } function setInvalid(el, msg) { const f = field(el); if (!f) return; f.classList.add('has-error'); el.classList.add('invalid'); el.classList.remove('valid'); const errEl = f.querySelector('.err-msg'); if (errEl && msg) errEl.textContent = msg; } function clearState(el) { const f = field(el); if (!f) return; f.classList.remove('has-error'); el.classList.remove('valid', 'invalid'); } /* --- Phone formatter ------------------------------------ */ function formatPhone(raw) { const digits = raw.replace(/\D/g, '').slice(0, 10); if (digits.length < 4) return digits; if (digits.length < 7) return '(' + digits.slice(0,3) + ') ' + digits.slice(3); return '(' + digits.slice(0,3) + ') ' + digits.slice(3,6) + '-' + digits.slice(6); } /* --- Validators ----------------------------------------- */ function validateRequired(el) { if (!el.value.trim()) { setInvalid(el, 'This field is required.'); return false; } setValid(el); return true; } function validateEmail(el) { if (!el.value.trim()) { setInvalid(el, 'Email address is required.'); return false; } if (!EMAIL.test(el.value.trim())) { setInvalid(el, 'Please enter a valid email address.'); return false; } setValid(el); return true; } function validatePhone(el) { const val = el.value.replace(/\D/g, ''); if (!val) { setInvalid(el, 'Phone number is required.'); return false; } if (val.length !== 10) { setInvalid(el, 'Please enter a 10-digit phone number.'); return false; } setValid(el); return true; } /* --- reCAPTCHA v3 token --------------------------------- */ function getRecaptchaToken(action) { return new Promise((resolve) => { if (typeof grecaptcha === 'undefined') { resolve(''); return; } grecaptcha.ready(() => { grecaptcha.execute(RECAPTCHA_SITE_KEY, { action }).then(resolve); }); }); } /* --- Form handler --------------------------------------- */ function initForm(form) { const nameEl = form.querySelector('#name'); const emailEl = form.querySelector('#email'); const phoneEl = form.querySelector('#phone'); const addrEl = form.querySelector('#address'); const serviceEl = form.querySelector('#service'); const msgEl = form.querySelector('#message'); const submit = form.querySelector('[type="submit"]'); const status = form.querySelector('.form-status'); if (!submit) return; /* Phone real-time format */ if (phoneEl) { phoneEl.addEventListener('input', () => { const pos = phoneEl.selectionStart; const prev = phoneEl.value; phoneEl.value = formatPhone(prev); /* restore cursor roughly */ const diff = phoneEl.value.length - prev.length; try { phoneEl.setSelectionRange(pos + diff, pos + diff); } catch (_) {} }); phoneEl.addEventListener('blur', () => validatePhone(phoneEl)); } /* Blur-time validation for other fields */ if (nameEl) nameEl.addEventListener('blur', () => validateRequired(nameEl)); if (emailEl) emailEl.addEventListener('blur', () => validateEmail(emailEl)); if (addrEl) addrEl.addEventListener('blur', () => validateRequired(addrEl)); if (serviceEl) serviceEl.addEventListener('change', () => validateRequired(serviceEl)); /* Submit */ form.addEventListener('submit', async (e) => { e.preventDefault(); const checks = [ nameEl ? validateRequired(nameEl) : true, emailEl ? validateEmail(emailEl) : true, phoneEl ? validatePhone(phoneEl) : true, addrEl ? validateRequired(addrEl) : true, serviceEl ? validateRequired(serviceEl) : true, ]; if (checks.includes(false)) { const firstErr = form.querySelector('.invalid'); if (firstErr) firstErr.focus(); return; } const origText = submit.textContent; submit.disabled = true; submit.textContent = 'Sending...'; if (status) { status.className = 'form-status'; status.textContent = ''; } const token = await getRecaptchaToken('estimate_form'); const payload = { name: nameEl ? nameEl.value.trim() : '', email: emailEl ? emailEl.value.trim() : '', phone: phoneEl ? phoneEl.value.trim() : '', address: addrEl ? addrEl.value.trim() : '', service: serviceEl ? serviceEl.value : '', message: msgEl ? msgEl.value.trim() : '', token, }; try { const res = await fetch('/api/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!status) { submit.disabled = false; submit.textContent = origText; return; } if (res.ok) { status.className = 'form-status form-status--success'; status.textContent = 'Thank you! We will get back to you within 1 business hour.'; form.reset(); form.querySelectorAll('input, textarea, select').forEach(clearState); } else { throw new Error(res.status); } } catch (_) { if (status) { status.className = 'form-status form-status--error'; status.textContent = 'Something went wrong. Please call us directly at (716) 602-1429.'; } } finally { submit.disabled = false; submit.textContent = origText; } }); } function boot() { document.querySelectorAll('.estimate-form').forEach(initForm); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();