PageSourceSearch

https://joinmosaic.ai/waitlist.js

js joinmosaic.ai collected 2026-10-02 01:11:32 UTC 5,112 bytes, 127 lines download raw bytes

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.