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.