1// Contact page: interactive project brief + mobile quick-contact dock. 2(function () { 3 var form = document.querySelector('form[data-brief]'); 4 if (form) { 5 var chips = form.querySelectorAll('.cf-chip'), segs = form.querySelectorAll('.cf-seg'); 6 var bar = form.querySelector('.cf-bar i'), pct = form.querySelector('.cf-pct'); 7 var note = form.querySelector('.cf-note'), btn = form.querySelector('.cf-send'); 8 var f = function (n) { return form.querySelector('[name=' + n + ']'); }; 9 var picked = function (list) { return [].filter.call(list, function (b) { return b.getAttribute('aria-pressed') === 'true'; }).map(function (b) { return b.getAttribute('data-v') || b.textContent; }); }; 10 var progress = function () { 11 var done = [f('name').value.trim(), /\S+@\S+\.\S+/.test(f('email').value), picked(chips).length, f('message').value.trim().length > 15].filter(Boolean).length; 12 var p = Math.round(done / 4 * 100); 13 bar.style.width = p + '%'; 14 pct.textContent = p === 100 ? 'Ready to send' : p + '% complete'; 15 pct.classList.toggle('cf-ready', p === 100); 16 }; 17 chips.forEach(function (b) { b.addEventListener('click', function () { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'); progress(); }); }); 18 segs.forEach(function (b) { b.addEventListener('click', function () { segs.forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); }); }); }); 19 form.addEventListener('input', progress); 20 form.addEventListener('submit', function (e) { 21 e.preventDefault(); 22 var bad = null; 23 ['name', 'email', 'message'].forEach(function (n) { 24 var el = f(n), ok = n === 'email' ? /\S+@\S+\.\S+/.test(el.value) : el.value.trim().length > 0; 25 el.closest('.cf-field').classList.toggle('cf-err', !ok); 26 if (!ok && !bad) bad = el; 27 }); 28 if (bad) { bad.focus(); note.className = 'cf-note cf-bad'; note.textContent = 'Please add your name, a valid email and a few words about the project.'; return; } 29 var needs = picked(chips), budget = picked(segs)[0] || ''; 30 var msg = f('message').value.trim(); 31 btn.disabled = true; btn.classList.add('cf-busy'); note.className = 'cf-note'; note.textContent = 'Sending your brief...'; 32 fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: f('name').value.trim(), email: f('email').value.trim(), phone: f('phone').value.trim(), subject: 'Website brief: ' + (needs.join(', ') || 'New project') + (budget ? ' (' + budget + ')' : ''), message: msg, needs: needs, budget: budget, page: location.href, website: f('website').value }) }) 33 .then(function (r) { return r.json().catch(function () { return {}; }).then(function (d) { if (!r.ok || !d.ok) throw new Error(); }); }) 34 .then(function () { form.classList.add('cf-sent'); form.querySelector('.cf-done').hidden = false; }) 35 .catch(function () { note.className = 'cf-note cf-bad'; note.textContent = 'We could not send your brief. Please try again, or email [email protected].'; }) 36 .finally(function () { btn.disabled = false; btn.classList.remove('cf-busy'); }); 37 }); 38 progress(); 39 } 40 // Mobile quick-contact dock: show once the hero is scrolled past. 41 var dock = document.querySelector('.ct-dock'), hero = document.querySelector('.ct-hero'); 42 if (dock && hero && 'IntersectionObserver' in window) { 43 new IntersectionObserver(function (e) { dock.classList.toggle('ct-dock-on', !e[0].isIntersecting); }).observe(hero); 44 } 45})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.