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; - + =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