PageSourceSearch

https://whitefang.ai/js/wallet-signup.js

js whitefang.ai collected 2026-10-05 13:17:58 UTC 6,411 bytes, 160 lines download raw bytes

1/* WhiteFang — the wallet signup form
2 *
3 * The hero's "Access Member Wallet" button is a mailto link. This file takes
4 * that click over and opens the form in index.html instead, writes the row to
5 * web.wallet_signups, and lets the database send the one email that says so.
6 *
7 * It gates nothing. Where there is no script, no Supabase keys or no dialog in
8 * the page, the click is left alone and the browser opens the mail app exactly
9 * as it did before this file existed. That is why the button is still an
10 * anchor with a real href rather than a <button>.
11 *
12 * The dialog is the site's own: the same .wf-dialog markup and the same
13 * open/close behaviour as the search sheet and the filters modal. That
14 * behaviour is repeated here in short rather than shared, because
15 * js/home-search.js owns it inside a closure that returns early when the map
16 * mount is missing, and a signup form must not depend on a map being present.
17 */
18
19(function () {
20  'use strict';
21
22  var modal = document.querySelector('[data-wf-signup-modal]');
23  var opener = document.querySelector('[data-wf-signup-open]');
24  if (!modal || !opener) return;
25
26  var form = modal.querySelector('[data-wf-signup-form]');
27  var errorEl = modal.querySelector('[data-wf-signup-error]');
28  var doneEl = modal.querySelector('[data-wf-signup-done]');
29  var submit = modal.querySelector('[data-wf-signup-submit]');
30
31  var FOCUSABLE =
32    'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), ' +
33    'textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
34
35  var lastFocus = null;
36
37  // ── Open and close ─────────────────────────────────────────────────────────
38  function onKey(e) {
39    if (e.key === 'Escape') { e.preventDefault(); close(); return; }
40    if (e.key !== 'Tab') return;
41    var stops = Array.prototype.filter.call(
42      modal.querySelectorAll(FOCUSABLE),
43      function (n) { return n.offsetParent !== null || n === document.activeElement; }
44    );
45    if (!stops.length) return;
46    var first = stops[0];
47    var last = stops[stops.length - 1];
48    if (!modal.contains(document.activeElement)) {
49      e.preventDefault();
50      first.focus();
51    } else if (e.shiftKey && document.activeElement === first) {
52      e.preventDefault();
53      last.focus();
54    } else if (!e.shiftKey && document.activeElement === last) {
55      e.preventDefault();
56      first.focus();
57    }
58  }
59
60  function open() {
61    lastFocus = document.activeElement;
62    modal.hidden = false;
63    modal.classList.add('is-open');
64    document.documentElement.classList.add('wf-dialog-open');
65    opener.setAttribute('aria-expanded', 'true');
66    var first = modal.querySelector('[data-wf-dialog-focus]') ||
67                modal.querySelector(FOCUSABLE);
68    if (first) first.focus();
69    document.addEventListener('keydown', onKey, true);
70  }
71
72  function close() {
73    modal.classList.remove('is-open');
74    modal.hidden = true;
75    document.documentElement.classList.remove('wf-dialog-open');
76    opener.setAttribute('aria-expanded', 'false');
77    document.removeEventListener('keydown', onKey, true);
78    if (lastFocus && document.contains(lastFocus)) lastFocus.focus();
79  }
80
81  var closers = modal.querySelectorAll('[data-wf-dialog-close]');
82  for (var i = 0; i < closers.length; i++) {
83    closers[i].addEventListener('click', close);
84  }
85  /* The backdrop is the dialog element itself; the panel sits inside it, so a
86     click that never reached the panel was a click on the backdrop. */
87  modal.addEventListener('click', function (e) {
88    if (e.target === modal) close();
89  });
90
91  opener.addEventListener('click', function (e) {
92    /* No database to write to means no form worth opening: let the mailto go.
93       Same for a browser that got here with the script but without the
94       Supabase client, which is what WF.configured already answers. */
95    if (!window.WF || !window.WF.configured || !window.WF.db) return;
96    e.preventDefault();
97    open();
98  });
99
100  // ── Submit ─────────────────────────────────────────────────────────────────
101  function val(id) {
102    var el = document.getElementById(id);
103    return el ? el.value.trim() : '';
104  }
105
106  function fail(message) {
107    errorEl.textContent = message;
108    errorEl.hidden = false;
109  }
110
111  if (form) form.addEventListener('submit', async function (e) {
112    e.preventDefault();
113    errorEl.hidden = true;
114
115    var name = val('signup-name');
116    var email = val('signup-email');
117    var zip = val('signup-zip');
118
119    /* The same three rules the insert policy in supabase/wallet-signups.sql
120       enforces, said here so the answer is immediate. The policy is the one
121       that decides. */
122    if (!name) { fail('Please enter your name.'); return; }
123    if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) {
124      fail('Please enter a valid email address.');
125      return;
126    }
127    if (zip.length < 3) { fail('Please enter your ZIP code.'); return; }
128
129    submit.disabled = true;
130
131    var res = await window.WF.db.from('wallet_signups').insert({
132      name: name,
133      email: email,
134      zip: zip,
135      source: 'home_hero',
136      user_agent: navigator.userAgent
137    });
138
139    if (res.error) {
140      /* Deliberately not the database's own words. A shopper filling in three
141         boxes cannot act on "relation web.wallet_signups does not exist", and
142         that is the message they would get on a checkout where
143         supabase/wallet-signups.sql has not been run. The real one goes to the
144         console, where the person who can fix it will look. */
145      if (window.console) console.warn('[wf] wallet signup', res.error);
146      fail('Could not save that. Please try again, or email [email protected].');
147      submit.disabled = false;
148      return;
149    }
150
151    /* The form and the two lines around it, not the form alone: the note
152       above it asks for the details and the line below promises what they are
153       for, and both read as still pending beside a thank-you. */
154    form.hidden = true;
155    var intro = modal.querySelectorAll('[data-wf-signup-intro]');
156    for (var n = 0; n < intro.length; n++) intro[n].hidden = true;
157    doneEl.hidden = false;
158    doneEl.focus();
159  });
160}());

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.