From e91db330949ee136e9c7328d4134d4a62b2a04a3 Mon Sep 17 00:00:00 2001 From: Willie Chalmers III Date: Thu, 1 Oct 2026 08:40:45 -0700 Subject: [PATCH] fix: Clarify member signup and give site fields a visible surface Require a first name when someone joins through the member form while keeping the compact newsletter signup available. Explain the optional phone number, allow transportation questions to be left blank, and give editable fields a consistent raised background across the site. Co-authored-by: Codex --- apps/site/functions/join/_form.ts | 6 +---- apps/site/platform/core/join-form.ts | 3 ++- apps/site/platform/messages/en.ts | 7 +++--- apps/site/public/scripts/join-steps.js | 19 +++++---------- .../site/src/components/NewsletterEmbed.astro | 2 +- apps/site/src/components/PagefindSearch.astro | 2 +- apps/site/src/components/app/styles.ts | 2 +- apps/site/src/components/join/JoinForm.astro | 11 +++++++-- apps/site/tests/platform-core.test.ts | 12 ++++++++++ apps/site/tests/platform-join.test.ts | 24 +++++++++++++++---- 10 files changed, 57 insertions(+), 31 deletions(-) diff --git a/apps/site/functions/join/_form.ts b/apps/site/functions/join/_form.ts index cfee767d..46ba9a95 100644 --- a/apps/site/functions/join/_form.ts +++ b/apps/site/functions/join/_form.ts @@ -26,11 +26,7 @@ const TEXT_FIELDS: [string, keyof JoinInput][] = [ ]; function restoreTravelAnswers(rewriter: HTMLRewriter, input: JoinInput): HTMLRewriter { - const checked = [ - ...(input.ownsCar === 'yes' ? ['car'] : []), - ...input.micromobility, - ...(input.ownsCar === 'no' && input.micromobility.length === 0 ? ['none'] : []), - ]; + const checked = [...(input.ownsCar === 'yes' ? ['car'] : []), ...input.micromobility]; for (const answer of checked) { rewriter = rewriter.on(`input[name="transport_options"][value="${answer}"]`, { element(element) { diff --git a/apps/site/platform/core/join-form.ts b/apps/site/platform/core/join-form.ts index 33ad202a..6fdba31d 100644 --- a/apps/site/platform/core/join-form.ts +++ b/apps/site/platform/core/join-form.ts @@ -40,7 +40,7 @@ export interface JoinInput { honeypot: string; } -export type JoinField = 'email' | 'zip' | 'phone' | 'consent'; +export type JoinField = 'email' | 'given_name' | 'zip' | 'phone' | 'consent'; export type JoinErrors = Partial>; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -118,6 +118,7 @@ export function formatPhone(e164: string): string { export function validateJoin(input: JoinInput): JoinErrors { const errors: JoinErrors = {}; if (!EMAIL_PATTERN.test(input.email)) errors.email = input.email ? 'invalid' : 'required'; + if (input.origin === 'join_form' && !input.givenName.trim()) errors.given_name = 'required'; if (input.zip && !/^\d{5}$/.test(input.zip)) errors.zip = 'invalid'; if (input.phone && normalizePhone(input.phone) === null) errors.phone = 'invalid'; if (!input.consent) errors.consent = 'required'; diff --git a/apps/site/platform/messages/en.ts b/apps/site/platform/messages/en.ts index 0f8c9e06..15b48136 100644 --- a/apps/site/platform/messages/en.ts +++ b/apps/site/platform/messages/en.ts @@ -12,9 +12,11 @@ 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', - givenNameLabel: 'First name (optional)', + givenNameLabel: 'First name', + givenNameError: 'Enter your first name', familyNameLabel: 'Last name (optional)', phoneLabel: 'Phone number (optional)', + phoneHint: 'Only for questions about your membership. LVBT updates come by email.', interestsHeading: 'What are you interested in? (optional)', interestEvents: 'Coming to events', interestMeetings: 'Speaking up at public meetings', @@ -23,9 +25,8 @@ 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.', + transportHint: 'Select all that apply, or leave this blank.', transportCar: 'Car', - transportNone: 'None of these', micromobilityBike: 'Bike (traditional)', micromobilityEbike: 'E-bike', micromobilityScooter: 'Scooter', diff --git a/apps/site/public/scripts/join-steps.js b/apps/site/public/scripts/join-steps.js index 7529b49e..9ee8cb23 100644 --- a/apps/site/public/scripts/join-steps.js +++ b/apps/site/public/scripts/join-steps.js @@ -26,6 +26,7 @@ const continueButton = form.querySelector('[data-join-continue]'); const backButton = form.querySelector('[data-join-back]'); const email = form.querySelector('[name="email"]'); + const givenName = form.querySelector('[name="given_name"]'); const consent = form.querySelector('[name="consent"]'); const items = progress.querySelectorAll('li'); const basicHeading = document.querySelector('[data-join-heading-basic]'); @@ -38,6 +39,7 @@ continueButton, backButton, email, + givenName, consent, basicHeading, interestsHeading, @@ -66,26 +68,17 @@ 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); } second.hidden = true; + givenName.addEventListener('input', () => givenName.setCustomValidity('')); continueButton.addEventListener('click', continueToInterests); backButton.addEventListener('click', () => show(1)); - form.querySelector('#transport_options')?.addEventListener('change', (event) => { - const changed = event.target; - if (!changed?.matches?.('input[name="transport_options"]') || !changed.checked) return; - const options = form.querySelectorAll('input[name="transport_options"]'); - if (changed.value === 'none') { - options.forEach((option) => { - if (option.value !== 'none') option.checked = false; - }); - } else { - const none = form.querySelector('input[name="transport_options"][value="none"]'); - if (none) none.checked = false; - } - }); form.addEventListener( 'submit', (event) => { diff --git a/apps/site/src/components/NewsletterEmbed.astro b/apps/site/src/components/NewsletterEmbed.astro index 003f9b14..65cc094f 100644 --- a/apps/site/src/components/NewsletterEmbed.astro +++ b/apps/site/src/components/NewsletterEmbed.astro @@ -60,7 +60,7 @@ const { variant = 'light', compact = false, stack = false } = Astro.props; aria-label="Email address" placeholder="you@example.com" aria-describedby="newsletter-status" - class="flex-1 min-w-0 px-4 py-3 border-2 border-current bg-transparent placeholder:opacity-80 focus:outline-hidden" + class="flex-1 min-w-0 px-4 py-3 border-2 border-on-surface bg-surface-container text-on-surface placeholder:opacity-80 focus:outline-hidden" />