diff --git a/messages/en.json b/messages/en.json index c2248b9bd..7d353ae3f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -14,6 +14,10 @@ "header_about": "About", "header_community": "Community", "header_get_involved": "Get involved", + "header_subscribe": "Subscribe", + "home_newsletter_heading": "Subscribe", + "home_newsletter_button": "Sign up", + "home_newsletter_description": "Sign up to our newsletter and receive updates about how to support our movement.", "header_our_strategy": "Our strategy", "header_why": "Why this matters", "header_find_group": "Find your local group", diff --git a/src/lib/components/Home.svelte b/src/lib/components/Home.svelte index 5dd5581b8..c04b59e70 100644 --- a/src/lib/components/Home.svelte +++ b/src/lib/components/Home.svelte @@ -52,7 +52,12 @@ - + diff --git a/src/lib/components/navbar/wide/WideNavbar.svelte b/src/lib/components/navbar/wide/WideNavbar.svelte index 94f063236..5d75c02d5 100644 --- a/src/lib/components/navbar/wide/WideNavbar.svelte +++ b/src/lib/components/navbar/wide/WideNavbar.svelte @@ -2,6 +2,10 @@ import UniversalNavbar from '../universal/UniversalNavbar.svelte' import Navlink from '../Navlink.svelte' import NavDropdown from '../NavDropdown.svelte' + import MailIcon from '@lucide/svelte/icons/mail' + import LinkWithoutIcon from '$lib/components/LinkWithoutIcon.svelte' + import { page } from '$app/state' + import { localizeHref } from '$lib/paraglide/runtime' import type { NavItem } from '../navItems' interface Props { @@ -17,6 +21,15 @@ {#each items as item, i (item.label)} {#if item.children} + {:else if item.mail} + {@const active = !!item.href && localizeHref(page.url.pathname) === localizeHref(item.href)} + + + {item.label} + {:else} {item.label} diff --git a/src/lib/components/onboarding/OnboardingFlow.svelte b/src/lib/components/onboarding/OnboardingFlow.svelte index 5950d583e..d1b9a04b0 100644 --- a/src/lib/components/onboarding/OnboardingFlow.svelte +++ b/src/lib/components/onboarding/OnboardingFlow.svelte @@ -46,13 +46,31 @@ const intentOptions = $derived(getIntentOptions(msgs)) let { + initialEmail = '', initialCountry = '', initialCity = '', - initialLanguages = [] as string[] + initialFullName = '', + initialLanguages = [] as string[], + initialRecordId = '', + startStep = 1, + initialKeepInformed = false, + initialChapterShare = false }: { + initialEmail?: string initialCountry?: string initialCity?: string + initialFullName?: string initialLanguages?: string[] + // The /subscribe flow creates the record up front and then hands off here + // to let people go further. It seeds the created record id and starts at + // the intent step, so this submission updates that record instead of + // creating a duplicate, and pre-sets the newsletter opt-in they already made. + initialRecordId?: string + startStep?: 1 | 2 + initialKeepInformed?: boolean + // The chapter-updates choice made on the subscribe form, reposted so backing + // out of Volunteer/Lead restores it instead of leaving the escalation. + initialChapterShare?: boolean } = $props() // Starts true so an unanswered request keeps the anti-spam check required. @@ -84,7 +102,7 @@ } }) - let step: 1 | 2 | 3 | 4 = $state(1) + let step: 1 | 2 | 3 | 4 = $state(startStep) let flowEl: HTMLDivElement | undefined = $state() // Scroll back to the top of the flow on step changes. The volunteer form @@ -102,9 +120,10 @@ let mode: 'contact' | 'browse' = $state('contact') let intent: IntentKey | null = $state(null) // Airtable record id from the step-2 submission; later submissions send it - // back so the server updates the record instead of creating another. - let recordId = $state('') - let keepInformed = $state(false) + // back so the server updates the record instead of creating another. When the + // /subscribe flow hands off, it's seeded with the record already created there. + let recordId = $state(initialRecordId) + let keepInformed = $state(initialKeepInformed) let submitting = $state(false) let browseSignedUp = $state(false) let honeypot = $state('') @@ -124,13 +143,24 @@ // Step 1: basic info (shared with the browse-mode inline signup and the // volunteer form, which pre-fills from the same state) let basics = $state({ - fullName: '', - email: '', + fullName: initialFullName, + email: initialEmail, country: initialCountry, city: initialCity, newsletter: false }) + // A newsletter signup elsewhere (e.g. the homepage box) can hand off here + // via ?subscribe-email=...; that email arrives after mount. Apply it once into + // an empty field, then never again — so it neither clobbers typed input nor + // snaps back when the visitor clears it to fix a typo. + let prefillApplied = $state(false) + $effect(() => { + if (prefillApplied || !initialEmail) return + if (!basics.email) basics.email = initialEmail + prefillApplied = true + }) + const validLanguageStored = new Set(LANGUAGES.map((l) => l.stored)) const filteredInitialLanguages = initialLanguages.filter((l) => validLanguageStored.has(l)) const defaultLanguages = @@ -165,10 +195,19 @@ // user stays in the flow. let becomePayingMember = $state(false) + // The /subscribe "do more" hand-off: we start at the intent step with a record + // already created and its email opt-ins already chosen. In that mode the step + // only asks how involved they want to be — the opt-in cards and a second + // consent are hidden so it can't re-ask (or silently undo) what they already + // picked on the subscribe form. + const isContinuation = $derived(startStep === 2 && !!initialRecordId) + // GDPR data-processing consent gating every record-creating submission // (step 2 + browse). Chapter-sharing consent is not bundled here: it lives // in the optional "Keep me informed" opt-in, since we only share details // with a local chapter when the person asks to be connected to one. + // Stays false so it never pre-checks a visible consent box; the continuation + // already consented on the subscribe form and is exempted at the submit gate. let gdprConsent = $state(false) // Phone: dial code prefilled from country of residence, editable in case @@ -199,18 +238,30 @@ agreements.conduct ) - const stepperLabels = $derived( - intent === 'volunteer' - ? [ - msgs.onboarding_step_about, - msgs.onboarding_step_intent, - msgs.onboarding_step_volunteer_form, - msgs.onboarding_step_confirmed - ] - : intent === 'lead' - ? [msgs.onboarding_step_about, msgs.onboarding_step_intent, msgs.onboarding_step_next_steps] - : [msgs.onboarding_step_about, msgs.onboarding_step_intent, msgs.onboarding_step_confirmed] - ) + const stepperLabels = $derived.by(() => { + const labels = + intent === 'volunteer' + ? [ + msgs.onboarding_step_about, + msgs.onboarding_step_intent, + msgs.onboarding_step_volunteer_form, + msgs.onboarding_step_confirmed + ] + : intent === 'lead' + ? [ + msgs.onboarding_step_about, + msgs.onboarding_step_intent, + msgs.onboarding_step_next_steps + ] + : [ + msgs.onboarding_step_about, + msgs.onboarding_step_intent, + msgs.onboarding_step_confirmed + ] + // The /subscribe continuation begins after the subscribe form, so it never + // showed an "About you" step — drop it and start the stepper at Intent. + return isContinuation ? labels.slice(1) : labels + }) // Lead path: if the country already has a chapter, offer regional/city // leadership instead of founding a national group. Fetched lazily when the @@ -367,7 +418,10 @@ {/snippet} {#snippet checkboxConfirmations()} - {#if keepInformed || basics.newsletter} + + {#if !isContinuation && (keepInformed || basics.newsletter)}