1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 Shared waitlist submit handler. 3 Auto-binds to every form.waitlist-form on the page: 4 - validates email 5 - persists to localStorage so a returning visitor sees "On the list" 6 - mirrors the submitted state across all forms on the page 7 - smooth-scrolls the nav button (href="#waitlist") into the page 8 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 9(function () { 10 const STORE_KEY = 'mosaic_waitlist_email'; 11 const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 12 const DONE_TEXT = 'You\u2019re on the list \u2713'; 13 const DONE_MSG = 'You\u2019re on the list'; 14 15 let stored = null; 16 try { stored = localStorage.getItem(STORE_KEY); } catch (e) {} 17 18 function status(form) { 19 // Status element: explicitly linked via aria-describedby, else nearest sibling .waitlist-status 20 const id = form.getAttribute('aria-describedby'); 21 if (id) { 22 const el = document.getElementById(id); 23 if (el) return el; 24 } 25 let s = form.nextElementSibling; 26 while (s) { 27 if (s.classList && s.classList.contains('waitlist-status')) return s; 28 s = s.nextElementSibling; 29 } 30 return null; 31 } 32 33 function applyDone(form, email) { 34 form.classList.remove('is-error'); 35 form.classList.add('is-submitted'); 36 let conf = form.querySelector('.waitlist-confirmation'); 37 if (!conf) { 38 conf = document.createElement('span'); 39 conf.className = 'waitlist-confirmation'; 40 conf.textContent = DONE_TEXT; 41 form.appendChild(conf); 42 } 43 const st = status(form); 44 if (st) st.textContent = ''; 45 } 46 47 function bind(form) { 48 const input = form.querySelector('input[type="email"]'); 49 if (!input) return; 50 if (stored) applyDone(form, stored); 51 52 form.addEventListener('submit', function (e) { 53 e.preventDefault(); 54 const v = (input.value || '').trim(); 55 if (!EMAIL_RE.test(v)) { 56 form.classList.remove('is-submitted'); 57 form.classList.add('is-error'); 58 const st = status(form); 59 if (st) st.textContent = 'Please enter a valid email address.'; 60 input.focus(); 61 return; 62 } 63 const btn = form.querySelector('button[type="submit"]'); 64 if (btn) btn.disabled = true; 65 const fd = new FormData(); 66 fd.set('email', v); 67 // 12 s timeout via AbortController so a slow/dead API doesn't leave the 68 // button disabled forever. 69 const ctl = new AbortController(); 70 const timeoutId = setTimeout(function () { ctl.abort(); }, 12000); 71 fetch('/api/waitlist', { method: 'POST', body: fd, signal: ctl.signal }) 72 .then(function (r) { 73 if (!r.ok) throw new Error('http ' + r.status); 74 try { localStorage.setItem(STORE_KEY, v); } catch (err) {} 75 stored = v; 76 document.querySelectorAll('form.waitlist-form').forEach(function (f) { 77 applyDone(f, v); 78 }); 79 }) 80 .catch(function () { 81 form.classList.remove('is-submitted'); 82 form.classList.add('is-error'); 83 const st = status(form); 84 if (st) st.textContent = 'Could not join the waitlist. Please try again.'; 85 // Auto-clear the error after 6 s so the form stops looking "broken" 86 // even if the user doesn't type. They can press submit again. 87 setTimeout(function () { form.classList.remove('is-error'); }, 6000); 88 }) 89 .finally(function () { 90 clearTimeout(timeoutId); 91 if (btn) btn.disabled = false; 92 }); 93 }); 94 95 // Any keypress or change clears the error state immediately. 96 input.addEventListener('input', function () { 97 form.classList.remove('is-error'); 98 }); 99 input.addEventListener('change', function () { 100 form.classList.remove('is-error'); 101 }); 102 } 103
104 function init() { 105 document.querySelectorAll('form.waitlist-form').forEach(bind); 106 107 // Nav button â focus the first waitlist form and scroll it into view. 108 document.querySelectorAll('a[href="#waitlist"]').forEach(function (a) { 109 a.addEventListener('click', function (e) { 110 e.preventDefault(); 111 const target = document.getElementById('waitlist') 112 || document.querySelector('form.waitlist-form'); 113 if (!target) return; 114 target.scrollIntoView({ behavior: 'smooth', block: 'center' }); 115 const inp = (target.tagName === 'FORM' ? target : target.querySelector('form.waitlist-form')) 116 && (target.tagName === 'FORM' ? target : target.querySelector('form.waitlist-form')).querySelector('input[type="email"]'); 117 if (inp) setTimeout(function () { inp.focus({ preventScroll: true }); }, 320); 118 }); 119 }); 120 } 121 122 if (document.readyState === 'loading') { 123 document.addEventListener('DOMContentLoaded', init); 124 } else { 125 init(); 126 } 127})();
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.