From ca1e2ba646d85bd74e07ee418f6452cb992aa9be Mon Sep 17 00:00:00 2001 From: Willie Chalmers III Date: Thu, 1 Oct 2026 10:35:11 -0700 Subject: [PATCH 1/2] fix(site): Explain member form fields where people answer Show short reasons for the information the join form requests, including how often email arrives and how interests guide suggestions. Put validation errors beside the affected fields as well as in the summary when someone continues with missing or malformed basic details. Co-authored-by: Codex --- apps/site/platform/messages/en.ts | 6 +- apps/site/public/scripts/join-steps.js | 62 +++++++++++++++++--- apps/site/src/components/join/JoinForm.astro | 10 +++- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/apps/site/platform/messages/en.ts b/apps/site/platform/messages/en.ts index 15b48136..645d8129 100644 --- a/apps/site/platform/messages/en.ts +++ b/apps/site/platform/messages/en.ts @@ -12,12 +12,16 @@ export const en = { intro: "Members get LVBT's updates, invitations to events and a say in what we work on. It's free.", emailLabel: 'Email address', + emailHint: "We'll send your welcome email and LVBT updates here, about 1–2 times a week.", givenNameLabel: 'First name', + givenNameHint: "We'll use this to greet you in your welcome email.", givenNameError: 'Enter your first name', familyNameLabel: 'Last name (optional)', + familyNameHint: 'Your account will show this with your first name.', phoneLabel: 'Phone number (optional)', phoneHint: 'Only for questions about your membership. LVBT updates come by email.', interestsHeading: 'What are you interested in? (optional)', + interestsHint: "We'll use your choices to suggest events and ways to help.", interestEvents: 'Coming to events', interestMeetings: 'Speaking up at public meetings', interestVolunteering: 'Volunteering on a team', @@ -25,7 +29,7 @@ export const en = { discordUsernameLabel: 'Discord username (optional)', discordUsernameHint: 'We can use this later to give you the right roles in our server.', transportHeading: 'What do you own for getting around? (optional)', - transportHint: 'Select all that apply, or leave this blank.', + transportHint: 'This helps us understand how members get around. You can skip it.', transportCar: 'Car', micromobilityBike: 'Bike (traditional)', micromobilityEbike: 'E-bike', diff --git a/apps/site/public/scripts/join-steps.js b/apps/site/public/scripts/join-steps.js index 9ee8cb23..44d133ae 100644 --- a/apps/site/public/scripts/join-steps.js +++ b/apps/site/public/scripts/join-steps.js @@ -16,6 +16,48 @@ }); } + function validateBasicFields({ form, summary, email, givenName, phone, consent }) { + function setFieldError(field, invalid) { + const error = form.querySelector(`[data-error-for="${field.id}"]`); + const item = summary.querySelector(`[data-summary-for="${field.id}"]`); + if (!error || !item) return; + error.hidden = !invalid; + item.hidden = !invalid; + field.toggleAttribute('aria-invalid', invalid); + const describedBy = (field.getAttribute('aria-describedby') || '') + .split(/\s+/) + .filter((id) => id && id !== error.id); + if (invalid) describedBy.unshift(error.id); + if (describedBy.length) field.setAttribute('aria-describedby', describedBy.join(' ')); + else field.removeAttribute('aria-describedby'); + summary.hidden = !summary.querySelector('[data-summary-for]:not([hidden])'); + } + + for (const field of [email, givenName, phone, consent]) { + field.addEventListener(field === consent ? 'change' : 'input', () => + setFieldError(field, false), + ); + } + + return () => { + const digits = phone.value.replace(/\D/g, ''); + const invalid = [ + [email, !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())], + [givenName, !givenName.value.trim()], + [ + phone, + Boolean(phone.value.trim()) && + !(digits.length === 10 || (digits.length === 11 && digits.startsWith('1'))), + ], + [consent, !consent.checked], + ]; + invalid.forEach(([field, failed]) => setFieldError(field, failed)); + if (!invalid.some(([, failed]) => failed)) return true; + summary.focus(); + return false; + }; + } + function run() { const form = document.querySelector('form[action="/join/member/"][data-enhance]'); const progress = document.querySelector('[data-join-progress]'); @@ -27,7 +69,9 @@ const backButton = form.querySelector('[data-join-back]'); const email = form.querySelector('[name="email"]'); const givenName = form.querySelector('[name="given_name"]'); + const phone = form.querySelector('[name="phone"]'); const consent = form.querySelector('[name="consent"]'); + const errorSummary = document.querySelector('[data-error-summary]'); const items = progress.querySelectorAll('li'); const basicHeading = document.querySelector('[data-join-heading-basic]'); const interestsHeading = document.querySelector('[data-join-heading-interests]'); @@ -40,7 +84,9 @@ backButton, email, givenName, + phone, consent, + errorSummary, basicHeading, interestsHeading, intro, @@ -52,6 +98,14 @@ progress.style.display = ''; continueButton.style.display = ''; backButton.style.display = ''; + const basicFieldsValid = validateBasicFields({ + form, + summary: errorSummary, + email, + givenName, + phone, + consent, + }); function show(step) { first.hidden = step !== 1; @@ -67,16 +121,10 @@ } function continueToInterests() { - if (!email.reportValidity()) return; - if (!givenName.value.trim()) givenName.setCustomValidity('Enter your first name'); - else givenName.setCustomValidity(''); - if (!givenName.reportValidity()) return; - if (!consent.reportValidity()) return; - show(2); + if (basicFieldsValid()) show(2); } second.hidden = true; - givenName.addEventListener('input', () => givenName.setCustomValidity('')); continueButton.addEventListener('click', continueToInterests); backButton.addEventListener('click', () => show(1)); form.addEventListener( diff --git a/apps/site/src/components/join/JoinForm.astro b/apps/site/src/components/join/JoinForm.astro index 97d94ea6..ef5d7b55 100644 --- a/apps/site/src/components/join/JoinForm.astro +++ b/apps/site/src/components/join/JoinForm.astro @@ -42,6 +42,7 @@ const { action } = Astro.props; - + Date: Thu, 1 Oct 2026 10:46:39 -0700 Subject: [PATCH 2/2] fix: Pin patched devalue for production dependencies Keep Astro's transitive serialization dependency on the patched 5.9 release so the production dependency audit remains clear. Co-authored-by: Codex --- pnpm-lock.yaml | 9 +++++---- pnpm-workspace.yaml | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7eefbbb5..943e0eb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,7 @@ catalogs: version: 4.5.4 overrides: + devalue: 5.9.4 sharp: 0.35.4 smol-toml: 1.8.0 svgo: 4.1.0 @@ -2674,8 +2675,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.9.4: + resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7798,7 +7799,7 @@ snapshots: clsx: 2.1.1 common-ancestor-path: 2.0.0 cookie: 2.0.1 - devalue: 5.8.1 + devalue: 5.9.4 diff: 9.0.0 dset: 3.1.4 es-module-lexer: 2.1.0 @@ -8430,7 +8431,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.8.1: {} + devalue@5.9.4: {} devlop@1.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 68a6eea2..5814738e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,7 @@ packageExtensions: # Temporary transitive pins for published security fixes not yet selected by # their direct dependents. overrides: + devalue: 5.9.4 sharp: 0.35.4 smol-toml: 1.8.0 svgo: 4.1.0