5020 Commits

Author SHA1 Message Date
mkfain 3a06dcd4cb Translate the new HRF/WLC category set and the refreshed campaigns tagline
Two pieces of stale i18n caught up:

1. The 16 new campaignsCreate.categories.* keys (human-rights,
   democracy, press-freedom, political-prisoners, humanitarian-aid,
   civil-resistance, digital-rights, anti-corruption, women-girls,
   refugees, legal-aid, emergency-relief, animal-rights, education,
   medical, community) translated into all 15 non-English locales.

2. campaigns.all.sectionTagline rewritten across all 16 locales to
   match the discovery-section fix that now lists featured campaigns
   first and the rest of the network underneath, instead of
   featured-only-with-fallback. Old copy ('Highlighted by moderators.
   Search or sort to browse the full network.') implied search was
   required to see non-featured campaigns, which is no longer true.
2026-05-30 13:12:54 +02:00
mkfain d7144200fb Replace generic campaign categories with HRF/WLC-aligned set
Swap the picker's preset list from the generic GoFundMe-style
catalog (adoption, animals, church, family, memorial, mission,
non-profit, event, first-responders, political) to a set that
reflects Agora's editorial focus on the kinds of activism HRF and
the World Liberty Congress champion: human rights, democracy,
press freedom, political prisoners, civil resistance, digital
rights, anti-corruption, women & girls, refugees & exiles, legal
aid. Plus humanitarian aid (per request), animal rights, emergency
relief, education, medical, and community to round out the
breadth.

16 entries total, ordered by editorial prominence (freedom /
democracy themes first, everyday humanitarian needs after). The
picker UI is unchanged \u2014 it iterates the array, so swapping
contents is enough.

Existing campaigns that selected one of the dropped slugs keep
their on-chain `t` tag intact \u2014 only the editor stops lighting up
a pill for them. No migration; we're pre-launch.

Strip the now-orphaned campaignsCreate.categories.* keys from the
15 non-English locales; the new English keys are in en.json only,
non-English locales will fall back to English at runtime until
proper translations land in a follow-up.
2026-05-30 13:07:49 +02:00
mkfain b5cb884004 Swap Campaigns and Activity order in the main nav
Campaigns is the primary surface of Agora; lead with it.
2026-05-30 12:49:42 +02:00
mkfain 0800b854ae Restore four-section home page and stop dropping approved campaigns
The home page community grid was missing approved campaigns whenever
the approval was older than the most recent 200 events on the
network. The grid was fed by a single `useCampaigns({ limit: 200 })`
call, so an approved campaign with a low `created_at` would silently
fall off the end of the chronological window and disappear from the
public surface even though its approval label was still active.

Two fixes here:

1. Add a second `useCampaigns` call keyed on every approved + hidden
   coord, alongside the existing recent-stream query. Merge both
   result sets, de-dupe by aTag, keep whichever revision is newer.
   Approved coverage no longer depends on recency.

2. Restore the four-section layout the home page was supposed to
   have: Featured, Community (approved only), Pending (mods-only),
   Hidden (mods-only, collapsed by default). The single
   chronological-all-with-toggle grid this commit replaces was the
   wrong target \u2014 censorship-resistant viewing belongs on
   /campaigns/all, the home page should be the moderator-curated
   front door.

Extend ModeratorCollapsibleSection with an explicit `defaultOpen`
prop so the Hidden section can be forced closed independent of the
existing 'auto-open when short' heuristic.
2026-05-30 12:47:10 +02:00
Alex Gleason 3a98e38f7b Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-05-30 12:36:31 +02:00
mkfain e198e8d572 Bump home-page Featured cap from 4 to 12 2026-05-30 12:35:51 +02:00
Alex Gleason cf6364a84b Fail over on 404 from always-present Esplora paths
mempool.space serves a 404 (instead of 429) to rate-limited clients,
which is common on carrier-NAT'd mobile connections where many users
share an egress IP. esploraFetch treated 404 as a legitimate "not
found", marked the endpoint healthy, and returned it WITHOUT failing
over — so getFeeRates threw 'Failed to fetch fee estimates', the query
swallowed it, and the on-chain Zap/donation dialogs showed no fee rates.
This is why fees loaded on WiFi but not LTE.

Add a per-call retryStatuses option to esploraFetch that extends the
retryable set (failover + cool-down) for that call, and apply [404] to
the paths that always exist on a healthy backend: /fee-estimates,
/address, /address/txs, /address/utxo, and the /tx broadcast. The
/tx/{txid} lookup keeps 404 meaningful (genuinely-unknown tx).
2026-05-30 12:35:48 +02:00
mkfain 34cae4c9ad Stop hiding approved-not-featured campaigns on /campaigns/all
The idle (no search / no sort / no country) view of the campaigns
discovery section was a featured-only shelf with a fallback to
chronological only when nothing was featured at all. As soon as a
moderator featured one campaign, every approved-but-not-featured
campaign vanished until the viewer typed a search, picked a sort,
or filtered by country.

Switch idle mode to a true featured-first list: pin featured at the
top of the grid, then append every other non-hidden campaign in
chronological order, deduped against the featured set. Approved-
not-featured now shows up where viewers expect it.

Active mode is unchanged \u2014 it already rendered the full result set.
The section tagline still reads 'Highlighted by moderators. Search
or sort to browse the full network.' which is now slightly stale;
leaving the translation update for after we confirm the new
behavior in the wild.
2026-05-30 12:32:40 +02:00
mkfain 0c686a2091 Let anyone unhide hidden campaigns on the Campaigns page
The Show-hidden toggle in CampaignsDiscoverySection was gated to
moderators. Drop that gate so every viewer of /campaigns/all sees
the toggle and can unhide what mods have suppressed.

Rationale: moderation labels live on public relays regardless, so
hiding the toggle was security-by-obscurity. The Campaigns page is
the censorship-resistant browseable index; the only honest UX is
transparent moderation. The home page (/) keeps its curated
behavior \u2014 only mods see hidden campaigns there \u2014 and the Hidden
collapsible *below* the discovery section on /campaigns/all stays
mod-only because it's a review workflow with one-click hide/unhide
affordances, not a discovery surface.

The toggle's default is unchanged: off. Viewers see only non-hidden
campaigns until they opt in.
2026-05-30 12:26:58 +02:00
Alex Gleason d07bc64032 Add custom fee rate to wallet Send; stop showing empty fee tiers
The HD-wallet Send dialog's fee popover relied on getUniqueBitcoinFeeSpeeds
falling back to all four preset tiers when rates hadn't loaded — rendering
clickable tiers with no sat/vB value (and no way to send at all when the
Blockbook estimate API was down).

- Show loading/error status (with a Retry) in the fee popover instead of
  bare tiers when rates haven't loaded.
- Add a "Custom" fee tier with an inline sat/vB input so users can always
  specify a rate, including when the estimate API is unavailable.
- Disable Send when the resolved rate is < 1 and surface an inline error.
- Add resolveBitcoinFeeRate + a PresetBitcoinFeeSpeed type so 'custom' is
  handled distinctly from the preset tiers.
2026-05-30 12:15:45 +02:00
mkfain e8acf45656 Hide Groups and Pledges from main nav for launch
Comment out the two NAV_ITEMS entries (desktop nav and mobile drawer
share this array, so one edit covers both). Routes and feature code
stay intact \u2014 visiting /groups or /pledges still works, in-page CTAs
still link, only the persistent nav chrome stops promoting them.

Re-enable by uncommenting the two lines and re-adding the Users and
Megaphone icon imports.
2026-05-30 11:34:42 +02:00
mkfain 3c28e2b789 Show every campaign on the home page, with a hidden toggle
The community grid stops gating on moderator approval and now lists
every kind-33863 campaign on the network, newest-first. A Switch in
the section header reveals the moderator-hidden bucket on demand (off
by default, count badge when something's there).

The moderator-only Pending and Hidden collapsibles disappear with
this change — Pending is now part of the main grid, and Hidden is one
toggle flip away. The non-mod 'Your campaigns' pending shelf goes
away for the same reason: a creator's not-yet-approved campaign
already shows up in the main grid.

Featured row, hero, and 'Browse all campaigns' link are untouched.
2026-05-30 10:46:54 +02:00
Alex Gleason dc43f723fb Trim the eager countries chunk from 244 KB to 47 KB
src/lib/countries.ts imported the full iso-3166 package solely to build
a Set of valid ISO 3166-2 subdivision codes for validation. That dataset
(~5000 objects with names, parents, and tree structure) landed in the
eagerly-preloaded countries chunk because NoteContent, ComposeBox, and
campaign.ts all import from countries.ts on the critical path.

Ship only the subdivision code strings instead, generated at build time
into src/lib/subdivisionCodes.ts via scripts/gen-subdivision-codes.mjs.
iso-3166 moves to devDependencies since only the generator script needs
it now. The strict-validation contract (rejecting US-ZZ etc.) is
preserved.
2026-05-30 02:20:01 +02:00
Alex Gleason c7ed31305d Lazy-load locale bundles to shrink the initial bundle
i18n.ts statically imported all 16 locale JSON files (~2.4 MB),
collapsing them into a single eager chunk that every user downloaded
on startup regardless of their language. Bundle the English fallback
only and fetch the other 15 locales on demand via dynamic import(),
so each language becomes its own lazily-loaded chunk.

This removes the 2.1 MB i18n chunk from the initial load; the eager
i18n chunk is now ~109 KB (runtime + English).
2026-05-30 02:10:35 +02:00
Alex Gleason 441eea160f Restore the full campaigns content area on the home page
The previous commit left the home page with a single
CampaignsDiscoverySection (search/sort/country toolbar over one grid).
The original layout was richer and read better: a dedicated Featured
row, the Community Campaigns grid with a "Browse all" link, the
moderator-only Pending / Hidden collapsibles, and a per-viewer "Your
campaigns" shelf.

Rebuild that content area from moderation labels (useCampaignModeration
+ useCampaignModerators + useCampaigns), keeping the current hero and
leaving campaigns as the home page's sole focus. The shared discovery
components and the dedicated /campaigns/all, /groups, and /pledges pages
that consume them are untouched.

Regression-of: 7ccff2fb
2026-05-29 16:41:12 -05:00
Alex Gleason 4f056dfac0 Show only campaigns on the home page, not groups and pledges
The home page is the primary browse surface for campaigns and reads best
when it stays focused on them. Groups and Pledges each have their own
dedicated browse pages (/groups, /pledges), so surfacing all three on /
duplicated those experiences and diluted the page.

Drop the GroupsDiscoverySection and PledgesDiscoverySection from the home
page, leaving only CampaignsDiscoverySection. The shared discovery
components and the dedicated pages that consume them are untouched.

Regression-of: 7ccff2fb
2026-05-29 16:36:56 -05:00
lemon f16d5ea334 Use wallet price source in dashboard balance card 2026-05-29 14:16:34 -07:00
lemon ef8e6f9564 Use wallet price source in header balance 2026-05-29 14:12:22 -07:00
Lemon 40f3179a63 Merge branch 'style/campaign-wizard' into 'main'
Campaign Wizard

See merge request soapbox-pub/agora!38
2026-05-29 13:52:20 -07:00
lemon 3b35b084fd Translate the wallet step's accept-mode pills, hints, and custom-wallet toggle into every shipping locale 2026-05-29 13:50:06 -07:00
lemon 0ade19c51e Translate the pledge and group wizard strings, plus the campaign categories, into every shipping locale 2026-05-29 13:50:06 -07:00
lemon 81f3c9e755 Show the Skip and Launch shortcut on the pledge wizard's Set Your Pledge step
The shortcut previously appeared only from step 3 onward. Once the
user fills the pledge amount on step 2 they're fully submittable —
title and description (the step 1 gate) plus a positive pledge
amount cover every required field. Forcing one extra Next click to
reach the shortcut on step 3 just to skip the rest was friction
for no benefit.

Moving the shortcut to step 2 reuses the same canAdvanceFromStep
gate the Next button does, so the button is visibly grayed out
until the pledge amount resolves to a positive sats value. Once
the amount is filled, both Next (continue to Cover) and Skip and
Launch (publish now) light up together and the user picks the
path. A minimal pledge is now two Next clicks plus a Skip and
Launch tap.
2026-05-29 13:50:06 -07:00
lemon 657c0e43e3 Align the pledge wizard tags step with campaigns and drop the dedicated deadline step
Two coordinated tweaks to the pledge create flow:

1. The free-form tag input is replaced with the same pill-style
   CategoryPicker that campaigns and groups already use, drawing
   from the curated 15-entry CAMPAIGN_CATEGORIES vocabulary. The
   tag list emitted on publish is now ordered canonically (the
   CAMPAIGN_CATEGORIES order) rather than insertion-order from
   the comma-separated input — same posture campaigns and groups
   adopted when they swapped pickers. Side effects:

     - parseContentTagInput is no longer imported by this file
       (still used by CreateEventPage and CreateCommunityEventDialog).
     - pledges.create.tagsPlaceholder is dropped from en.json and
       all fifteen non-en locales, since the picker has no
       free-text input to placeholder.
     - The step subtitle stays "Help the right people find your
       pledge"; the title is renamed "Country and categories" to
       match the picker (groups uses the same string).

2. The dedicated Deadline step is folded into the Pledge-amount
   step. The two questions answer the same beat — "how much, and
   by when?" — and a step that often gets skipped felt like
   padding next to the amount field it conceptually belongs
   with. The timezone subsection still reveals only once a date
   is chosen, the date is still required to be present-or-future,
   and the deadline tag still publishes only when a date is set.

Step count drops from 5 to 4: Title+Description, Pledge+Deadline,
Cover, Country+Categories. The Skip Next and Launch shortcut keeps
its from-step-3 placement (both required gates still clear by the
end of step 2), so a minimal pledge takes two Next clicks plus
one Skip and Launch tap.

i18n: deletes pledges.create.wizard.deadlineStepTitle and
deadlineStepSubtitle from en.json (they exist only in en so no
locale cleanup is needed). Updates pledgeStepSubtitle to mention
the optional deadline. Renames tagsStepTitle to "Country and
categories" to match the picker.
2026-05-29 13:50:06 -07:00
lemon 5a72cf1fd0 Convert the pledge create flow into the wizard layout
Pledges followed the original single-page stacked form for every
create. With campaigns and groups both running through the captive
wizard overlay, pledges were the odd one out — the FAB / hero CTA
landed on a long scrolling form while every other create flow
opened a focused step-by-step layout. This brings them inline.

Five steps:

  1. Title + Description (both required; step 1 gates on both)
  2. Pledge amount (required; gates on a positive sats preview so
     the BTC/USD price has resolved before publish)
  3. Cover image (optional)
  4. Deadline + timezone (optional; the timezone subsection still
     reveals only when a date is chosen)
  5. Country + Tags (optional, terminal)

Skip Next and Launch appears from step 3 onward. Steps 1 and 2 hide
the shortcut because publishing without their fields would trip a
server-side validation error; once both required gates are
cleared, the remaining three steps are explicitly optional and a
single-tap launch is the desired escape hatch. Matches the same
posture the campaign wizard uses for its required title + wallet
gates.

Side cleanups while in the file:

  - The local CountrySelect is replaced with the shared one. The
    pledges.create.{countryClearAria, flagOfAria, countryHint}
    locale keys were already absent from non-en locales (cleaned
    out during the earlier campaign/groups extraction), so this
    just removes the now-orphan en.json entries.
  - pledges.create.{publishing, uploadingCover} were dead since
    the page was already reading forms.publishing /
    forms.uploadingCover; deleted from all sixteen locales.
  - OrganizationContextChip now rides along inside step 1 as a
    step1Lead, same treatment the campaign wizard gives it. The
    captive overlay swallows the page header chrome, so the
    "publishing under <org>" affordance has to live inside the
    step body to stay visible.

No edit-mode path: pledges aren't editable today, and the file
mirrors that — there's a single create branch and that's it. If
edit support is ever added, the campaign / groups pattern (the
single-page form lives behind an isEditMode branch above the
wizard return) is the template.

i18n: adds pledges.create.wizard.{titleStepTitle, titleStepSubtitle,
pledgeStepTitle, pledgeStepSubtitle, coverStepTitle,
coverStepSubtitle, deadlineStepTitle, deadlineStepSubtitle,
tagsStepTitle, tagsStepSubtitle, launchNow} to en.json. Other
locales fall back to English until translated.
2026-05-29 13:50:06 -07:00
lemon b3163ea2c9 Show the Skip and Launch shortcut on step 1 of the group wizard
Groups require only a name to publish. The shortcut used to appear
from step 2 onward, which still forced one mandatory Next click
before the user could opt out of the remaining optional steps.
Moving it to step 1 lets a minimal group publish in a single
action: type a name, tap Skip and Launch.

The shortcut shares its disabled state with the Next button via
canAdvanceFromStep, so on step 1 it only becomes clickable once
the name field is non-empty.

Also tightens Wizard's canSubmit calculation: the mid-wizard
shortcut now respects the same canAdvance gate the Next button
does. Previously a launch button placed on a gated step would
remain clickable even when the gate was unmet, then trip a
server-side validation error. The terminal step's own submit
button keeps its old behavior because by definition every gated
step has already been cleared by then.
2026-05-29 13:50:06 -07:00
lemon 1f545e7361 Add a Skip Next and Launch shortcut to the group create wizard, restore the single-page form for edits
Two changes that go together:

1. The group create wizard now exposes a Skip Next and Launch ghost
   shortcut from step 2 onward. Name is the only required field
   (it is the gate on step 1 and the slug source); once a user
   clears that step, everything else is opt-in and they should
   not have to click Next, Next, Next through three optional
   screens just to publish a minimal group. Matches the same
   shortcut the campaign wizard offers from step 3 onward.

2. Edit mode now renders the original single-page stacked form
   instead of the wizard, mirroring the create-vs-edit split the
   campaign flow uses. Editing a populated entity benefits from
   seeing all fields at once: every wizard step would already be
   pre-filled, and walking through them adds friction without
   adding clarity. The edit form reuses the exact same section
   bundles the wizard does (nameDescriptionSection, coverSection,
   moderatorsSection, countryCategoriesSection) so create and
   edit stay byte-identical in their field rendering. Ordering
   matches the pre-wizard page: name, description, country,
   categories, cover, moderators.

i18n: adds groups.create.wizard.launchNow ("Skip Next and Launch")
to en.json. Other locales fall back to English until translated.
2026-05-29 13:50:06 -07:00
lemon 1b21edef19 Convert the group create / edit flow into the wizard layout
Groups used to render every field on a single long form. Now they
share the same captive overlay the campaign flow uses — sticky
progress bar across the top, one focused decision per step, top-left
back chrome and top-right escape, big rounded primary CTA. Four
steps:

  1. Name + Description (gated; name is required to advance)
  2. Cover image
  3. Moderators
  4. Country + Categories

The free-form 'mutual-aid, local-news, digital-rights' tag input is
replaced with the same pill-style CategoryPicker the campaign flow
uses, drawing from the same 15-entry CAMPAIGN_CATEGORIES vocabulary
so the two creation surfaces feel like the same product. Country
input uses the shared CountrySelect.

Edit mode behaviors:

  - The d-tag stays immutable (kept as editCommunity.community.dTag).
  - The pre-fill loop only pre-selects existing  tags that exist
    in the curated category set. Arbitrary  tags an older
    free-form entry may have published (e.g. 'mutual-aid') are
    intentionally dropped from the picker — the user has no way to
    re-select them, and silently re-publishing tags they can't see
    would be a stealth foot-gun. Same posture campaigns adopted when
    their tag input was swapped.
  - The preserved-tag list in the edit branch already strips every
    ; nothing else changes there.

No 'Skip Next & Launch' shortcut here. Groups are only four steps
and three of them are optional, so a mid-wizard submit shortcut
would clutter the footer without saving real effort.

i18n: adds groups.create.wizard.{name,cover,moderators,tags}Step{Title,Subtitle}
to en.json. The non-en locales fall back to English for these new
strings until they're translated.
2026-05-29 13:50:06 -07:00
lemon 2ba19fc135 Extract Wizard, CategoryPicker into reusable components
The wizard scaffolding (progress bar, captive overlay, step-aware
header chrome, Enter-to-advance keyboard handling) had been living
inline at the bottom of CreateCampaignPage.tsx as CampaignWizard and
WizardStep, and the category-pill picker was inline as well. Both
need to drop into the group-creation flow next, so they get lifted
into src/components/Wizard.tsx and src/components/CategoryPicker.tsx
with no behavioral change for the campaign page.

The campaign-specific bits — the org chip and the 'Skip Next &
Launch' shortcut — survive the move as optional props (step1Lead,
launchNowLabel) so the group flow can omit them without dragging
along irrelevant chrome. The wizard's Back / Next / close labels now
read from common.back / common.next / common.goBack, which both
flows can reuse.

CountrySelect had already been pulled out into its own component for
the calendar-event flow; it now gains the same localization the
inline campaign copy had (countryClearAria, flagOfAria, countryHint
move to the shared forms.* namespace, replacing the hardcoded
English strings the calendar-event flow shipped with) plus an
optional id prop so callers that own their own <label htmlFor> can
keep wiring it explicitly.

The three localized strings used to live duplicated under
campaignsCreate.* and groups.create.*; both copies are removed from
en.json and from every non-en locale so the locales test passes.
2026-05-29 13:50:06 -07:00
lemon 934495a7d3 Reflow the category picker into auto-wrapping pills
The grid layout forced every chip to the width of the widest label,
which left half the pills with awkward whitespace and the rest with
truncation pressure. Switching to a flex-wrap row lets each pill
size to its own text — short labels (Family, Legal) take less room,
long labels (First Responders) take more, and the row breaks
whenever the next pill wouldn't fit. Some rows naturally fit three
pills, others fit four, depending on which labels neighbor each
other on a given line.

Also drops Current Events from the curated set (it overlaps heavily
with the Event category and was usually mis-selected as a synonym)
and bumps the chip font from text-xs back to text-sm now that the
text is no longer constrained by a narrow grid cell.
2026-05-29 13:50:06 -07:00
lemon a2a4c8b2a7 Trim the campaign category picker down to a 16-tile, 3-col grid
Drops Competitive, Creative, Evangelism, and Business — those four
were the weakest fit for the kinds of fundraisers that actually run
on Agora (memorial drives, medical emergencies, mission trips,
mutual-aid efforts), and including them in the curated set diluted
the signal of the other 16. Also renames Animals / Pets → Animals,
which reads cleaner in the chip and avoids the awkward slash.

Locks the picker to a three-column grid (was 2/3/4 responsive) so
the full label is always visible — at the wizard's narrow max-w-md
column the previous two-column layout left half the chips with
truncated labels, and the four-column layout never had room for
multi-word categories like 'First Responders' or 'Current Events'.
Three columns gives every short label its own line and lets the two
long ones wrap to two; a min-h-[3rem] keeps the grid uniform.
2026-05-29 13:50:06 -07:00
lemon c560bd8acd Replace the wizard's tag input with a curated category picker
The free-form 'beach-cleanup, mutual-aid, …' input asked donors to
invent and spell their own taxonomy on the spot, which produced
sparse and inconsistent tag data (no two campaigns used the same
slug for 'medical', the picker on the discover page never had a
stable set to filter against, etc). Replaces it with a fixed
20-category multi-select grid — Adoption, Animals/Pets, Business,
Church, Community, Creative, Current Events, Education, Emergency,
Evangelism, Event, Family, First Responders, Legal, Medical,
Memorial, Mission, Non-Profit, Political, Competitive — each chip
rendered with its Lucide icon.

Selected categories are persisted as ordinary lowercase 't' tags,
identical at the protocol level to anything the old input would
have produced, so existing readers (relays, the discover feed,
cross-client viewers) need no changes. Edit mode intersects the
event's existing 't' tags with the curated slug set so a campaign
created under this picker round-trips cleanly.

Also restores the previously-merged 'goal & deadline' and 'country &
tags' wizard steps as separate screens — collapsing them into one
turned out to push the category picker too far down the page on
mobile to be the first thing the user sees on the final step.
2026-05-29 13:50:06 -07:00
lemon 21907014e0 Translate the campaign wizard step titles into every shipping locale
The wizard's step titles, subtitles, and footer button labels lived
only in en.json, so every non-English user saw the captive create
flow in English — even after the locale fell back gracefully for the
rest of the page. Adds the wizard subobject to all 15 other locales
with idiomatic translations matching each file's established voice.
2026-05-29 13:50:05 -07:00
lemon 0b77980fc7 Merge goal, deadline, country, and tags into one final wizard step
The wizard's last two screens were each only ~one field of work:
goal+deadline (a USD input and a date) and country+tags (a country
combobox and a comma-list). Asking users to advance twice through
near-empty steps was busywork — both screens fit comfortably on the
same step without breaking the captive flow's vertical rhythm.

Collapses them into a single 'Goal, deadline, and tags' step, which
becomes the new terminal step where the Launch button lives. The
shortcut still appears from the banner step onward, so the wizard
remains a five-step flow with the same opt-in tail.
2026-05-29 13:50:05 -07:00
lemon 3bab0ef3e0 Don't let Enter on a non-terminal step silently publish the campaign
A <form> with a single text input treats Enter as submit. The wizard
sets the form's onSubmit to the publish handler, so hitting Enter on
step 1 (title) would call submitMutation.mutate() — and for a logged-in
nsec user the wallet picker already defaults to a valid HD-wallet
'mine' / 'all' configuration, so the publish actually went through and
the campaign launched after a single Enter on the title field. There
was no opportunity to fill in anything else.

Intercept Enter on the form's onKeyDown:
* If we're on the terminal step, do nothing — Enter should submit.
* If the focused element is a <textarea>, do nothing — Enter is a
  legitimate newline inside the field.
* If we're mid-IME composition, do nothing — let the IME finish.
* Otherwise preventDefault and call the same "advance" logic the Next
  button uses, gated by submitting + canAdvance so the gate behaves
  identically.

Also wrap each child of the custom-wallet header in a block <div> so
the "← Use my Agora wallet" link stacks beneath the "Custom wallet"
title instead of sitting on the same line. Both children were
inline-flex; the parent's space-y-1 only adds margin between block
children, so on wide enough viewports the two pieces ended up
side-by-side.
2026-05-29 13:39:18 -07:00
lemon cb52920259 Quiet down the wallet step's identity row and accept-mode pills
Four small refinements after first review:

* Drop the card chrome (border + bg) around the identity row and
  remove the pencil. The row is now a plain avatar + name + balance
  display sitting on the wizard's transparent background — visual
  confirmation of the destination, not a button. The "Use a custom
  wallet instead" sub-link beneath becomes the only affordance for
  the swap.
* Stack the "← Use my Agora wallet" link beneath the "Custom wallet"
  heading instead of placing it on the same row. Two pieces of
  hierarchy fighting for the same line was too much; the swap link
  reads more clearly on its own line.
* Drop the icons (sparkles / bitcoin / radar) from the accept-mode
  toggle pills. Each pill now carries just its label. The icons
  were trying to compress meaning into one glyph each and the
  captions already say the same thing.
* Expand the toggle labels to "Accept All" / "Public Only" /
  "Private Only" — full enough to read as commands rather than tags.

Cleans up the lucide imports (Pencil, Sparkles, Bitcoin, Radar) and
locale key (walletEditAria) the previous version introduced and the
new version no longer needs.
2026-05-29 13:39:18 -07:00
lemon 6e4eff602a Redesign the wizard's wallet step around the user's wallet card
The wallet step previously stacked two generic dropdowns (source +
accept) on top of two custom-address inputs that the user had to expand
explicitly. Every donation flow starts the same way: pick "my wallet"
and accept everything. The redesign treats that path as the default
view, not one of two dropdown options.

What changed:

* The Source dropdown becomes an inline identity card — avatar +
  display name on the left, live USD/BTC balance on the right
  (modeled on the wallet-page treatment), pencil affordance on the
  far right. Tapping anywhere on the card swaps the view into the
  custom-wallet inputs; a quieter "Use a custom wallet instead"
  sub-link beneath it offers the same swap. From custom mode a small
  "← Use my Agora wallet" mirror-link snaps back.

* The Accept dropdown becomes a three-pill segmented ToggleGroup —
  All / Public / Private — with icons (sparkles / bitcoin / radar)
  and a one-line caption beneath that explains the current
  selection. The All and Private buttons disable when silent
  payments aren't supported by the current login. Default stays
  'all' (HD wallet with SP); empty toggle deselects are coerced
  back to the previous value since the field is required.

* Balance comes from the parent's existing useHdWallet hook (passed
  in via new `totalBalance` + `balanceLoading` props) plus an
  in-component useBtcPrice call. Loading state shows a small
  Skeleton in place of the price line; missing price falls back to
  BTC-only.

* When no HD wallet is available (extension / bunker logins) the
  picker collapses to just the two custom inputs with the existing
  intro copy — no card, no toggle.

Existing locale keys are reused where the strings still fit; new
ones cover the toggle short labels, the captions, and the swap
affordances. The wider "Custom" label widens to "Custom wallet" so
the segmented header reads cleanly. Other locales fall back to
English on the new keys until the copy settles.
2026-05-29 13:39:18 -07:00
lemon 337d18951a Split the campaign wizard into six single-purpose steps
The previous four-step layout bundled title with wallet and banner with
story. Each pairing forced the user to mentally context-switch inside a
single screen. Splitting them out makes every step ask exactly one
question:

    1. title
    2. wallet
    3. banner
    4. story
    5. goal + deadline
    6. country + tags (terminal)

The 'Skip Next & Launch' shortcut now appears from step 3 onward — once
both required steps (title @ 1, wallet @ 2) are cleared. Earlier steps
hide the shortcut entirely so the user can't try to publish before the
wallet picker has been shown.

The wizard signature changes from positional step1..step4 props to a
single `steps` array plus a `canAdvanceFromStep` predicate and a
`launchAvailableFromStep` cursor, so future step inserts / removals
don't ripple through the type. Step state moves from a `1|2|3|4`
literal union to `number`, validated against `steps.length` at runtime.

Step copy is rewritten to be concise — one question, one line of
context. Other locales already fell back to English; the wizard keys
they don't yet have stay untranslated until the copy settles.
2026-05-29 13:39:18 -07:00
lemon 31154f382d Polish the campaign wizard's header and shortcut affordance
Four small refinements on the captive overlay:

* Drop the 'Step N of 4' eyebrow above each title — the sticky
  progress fill at the top of the overlay already carries that signal,
  and removing the duplicate keeps the focus on the step heading.
* Rename the launch shortcut on steps 1-3 from 'Launch campaign' to
  'Skip Next & Launch' so its relationship to the primary Next button
  is unambiguous. Step 4's terminal button keeps the 'Launch campaign'
  label (it isn't a shortcut, it's the only forward action).
* Move the per-step Back affordance from a text link under the launch
  button up to a round icon button mirroring the close X in the
  top-left corner. The two header buttons now bracket the dialog
  symmetrically and the footer stays focused on forward motion.
* Reverse the order of the banner and story fields inside step 2 so
  the banner upload sits on top — it's the first thing a donor sees on
  the campaign card and feels like the natural first decision when
  telling the story.

Campaign launches still navigate to the campaign details page on
success via encodeCampaignNaddr in submitMutation.onSuccess; no change
needed there.
2026-05-29 13:39:18 -07:00
lemon 236e6aa211 Render the campaign wizard as a fullscreen captive overlay
Mounts the create-mode wizard as a 'fixed inset-0 z-50' dialog so it
sits above the persistent TopNav, matching the captive OnboardingGate
signup flow. Creating a campaign is now a focused, distraction-free
task without the app's regular chrome competing for attention.

The page-level back arrow + heading are replaced by an unobtrusive
top-right X (same affordance as the onboarding overlay). The
OrganizationContextChip — previously sat under the page heading —
moves inline into step 1 so the 'publishing under <org>' context
isn't lost.

Edit mode is unaffected — it still renders inside the normal
FundraiserLayout with the page header intact.
2026-05-29 13:39:18 -07:00
lemon 8c684aeef2 Restyle the campaign wizard after the captive onboarding flow
Swaps the segmented-pill progress indicator and boxed step body for the
visual language Chad established in OnboardingGate: a sticky single-bar
progress fill across the top, a centered narrow column per step, a
centered eyebrow / heading / subtitle block, a big rounded-full primary
CTA, and a subtle text 'Back' link. Steps now fade-and-slide in on
transition so the swap reads as navigation rather than a re-render.

Step boundaries are unchanged. Step 1 still holds the required fields
(title + wallet) and gates 'Next' on a non-empty title; every step from
1 onward surfaces a ghost 'Launch campaign' shortcut so the rest of the
wizard stays opt-in. Step 4 is terminal — its only forward action is
the primary 'Launch campaign' button.

Edit mode is unaffected — it keeps the single-page form.
2026-05-29 13:39:18 -07:00
lemon ab59960233 Break the new-campaign form into a four-step wizard
New campaigns now use a multi-step flow modeled on AuthDialog's signup:
required fields (title + wallet) on step 1, story + banner on step 2,
goal + deadline on step 3, country + tags on step 4. A 'Launch campaign'
button sits next to 'Next' on every step from step 1 onward, so once the
required fields are filled the user can publish immediately and skip the
rest. Step 1's 'Next' is disabled until the title is non-empty.

Edit mode (?edit=naddr) keeps the original single-page layout — all
pre-populated fields stay visible and editable in one place, which the
linear wizard isn't optimized for.
2026-05-29 13:39:18 -07:00
lemon 3565ebf098 Hoist three duplicated discovery helpers to shared modules
Three small extractions that consolidate hand-rolled copies in the
discovery surfaces. No behavior change.

- getPledgeCoord → src/lib/pledges.ts. Was defined three times
  (PledgesDiscoverySection, ActionsPage, ActionShareMenu), each with
  the same '36639:<pubkey>:<d>' template. Lifted into the existing
  pledges lib and typed structurally on { pubkey, id } so the lib
  layer doesn't take a hook dep on Action.

- parseSort + toQuerySort → exported from useDiscoveryFilters and
  useAllCampaigns respectively. AllCampaignsPage was carrying its own
  copy of both with an apologetic comment ('mirroring the one in
  useDiscoveryFilters'); CampaignsDiscoverySection had its own
  toQuerySort. One source of truth each now, with the pages and the
  section importing from the same module as the hook that consumes
  the result.

- PledgeCardSkeleton → exported from PledgeCard. Replaces two
  byte-identical ActionSkeleton components in PledgesDiscoverySection
  and ActionsPage. Naming matches the existing CampaignCardSkeleton /
  CommunityMiniCardSkeleton convention of placing the skeleton next
  to its card.
2026-05-29 13:39:18 -07:00
lemon 0dcc2f2b93 Skip wasted discovery-section queries
Two related gates on the unified discovery sections:

- PledgesDiscoverySection: the chronological useActions({ limit: 300 })
  query only feeds the idle render branch (via idlePledges), but it
  was firing in active-search mode too. Active mode renders searchHits
  from useNip50Search, which never reads rawActions. On every keystroke
  that activates search we were burning a 300-event relay round-trip
  whose results went nowhere. Gate the query on !isSearching so the
  fetch happens only when the idle branch can actually consume it.

- CampaignsDiscoverySection: align the featured-coords useCampaigns
  query's enabled flag with the pledges section's pattern. useCampaigns
  already short-circuits on an empty coordinates array, so this is
  purely about not creating an empty cache entry when moderators have
  curated nothing — but it removes a small asymmetry that would have
  made the next reviewer second-guess which pattern is intentional.
2026-05-29 13:39:18 -07:00
lemon 4cbc9f64c1 Extract three reusable discovery sections shared by home and dedicated pages
The home page now shows the same Campaigns / Groups / Pledges sections
as their dedicated pages (/campaigns/all, /groups, /pledges), with the
same titles, taglines, and search/sort/country toolbars instead of
'Browse all' shortcut links. Each surface's discovery logic lived
in its own page and the home page was about to grow a fourth copy of
it, so the section bodies move into reusable components:

  CampaignsDiscoverySection  src/components/discovery/
  GroupsDiscoverySection
  PledgesDiscoverySection

Each owns the section header (title / tagline switch on active
search), the DiscoverySearchToolbar, the idle featured grid, the
active search/sort/country grid, and the per-section empty / no-match
cards. Filter state (search input, sort, country, debouncing) lives
in a new useDiscoveryFilters hook which has two modes:

  filterPersistence='url'   - flat ?q=&sort=&country= params. Used by
                              the dedicated pages so search results
                              are shareable and survive refresh.
  filterPersistence='local' - local-only state. Used by / where three
                              sections coexist and can't all own ?q=.
                              Refreshing the home lands on the curated
                              idle view, which matches what we want.

The dedicated pages keep their hero, optional Your-X shelf, and the
moderator-only Hidden collapsible — those stay page-level because
each page wants its own copy. They drive the section's Show-hidden
toolbar switch via a hoisted prop so the page-level Hidden
collapsible can read the same flag.

Side effects:

  - ActionShareMenu moves from inside ActionsPage to its own file
    so PledgesDiscoverySection can render it on every card without
    re-importing the page module.

  - useDiscoverCommunities is unchanged but only the dedicated
    /groups page calls it now (for the Hidden collapsible /
    hidden-count badge). The home page never triggers it.

  - browseAllGroups and browseAllPledges locale keys drop from all
    16 locales since the launchpad layout that needed them no
    longer exists.
2026-05-29 13:39:18 -07:00
lemon 7ccff2fbad Turn the home page into a featured-only launchpad for all three surfaces
The home page used to be the canonical browse view for campaigns: hero
plus featured row, then the full community grid, then moderator-only
Pending/Hidden sections, then a per-viewer 'Your campaigns' shelf.
With /campaigns/all, /groups, and /pledges all now hosting their own
dedicated browse views (featured + search + sort + country in one
unified section), the home page no longer needs to duplicate the
campaigns browse experience.

Rebuild / as a three-section launchpad:

  Hero  -> unchanged (HeroLightningMap, Bebas Neue tagline, brand CTAs).
  Featured campaigns  -> capped at 4, links to /campaigns/all.
  Featured groups     -> capped at 8, links to /groups.
  Featured pledges    -> capped at 8, links to /pledges.

Each section pulls its featured set from the same moderation labels
that drive the dedicated page, so what surfaces here matches what
surfaces there — just truncated. Sections with no featured items
collapse silently (no empty card) so the page degrades gracefully if
moderators only curate one or two surfaces.

Each section's skeleton respects the dependency chain that gates its
underlying query: campaigns wait on useCampaignModeration, groups on
useOrganizationModeration (because useFeaturedOrganizations is
internally gated on it), pledges on usePledgeModeration. While those
are still resolving the section renders skeleton cards rather than
flashing an empty state.

Drop the unmoderated community grid, the Pending/Hidden moderator
sections, the 'Your campaigns' shelf, and the campaign-search
toolbar from the home page. All of that now lives on /campaigns/all
where viewers actually expect to browse and filter.

Add browseAllGroups and browseAllPledges to campaigns.home in all
16 locales so each section can link out with locale-appropriate copy.
2026-05-29 13:39:18 -07:00
lemon 83554c726d Keep skeleton up while moderation labels are still resolving
useFeaturedOrganizations is internally gated on moderationReady — while
the organization moderation labels are loading, the underlying query
is disabled and reports isLoading: false / data: undefined. The Groups
page was using only that isLoading flag to decide whether to show the
skeleton, so during the moderation-loading window it rendered the
empty state for a moment before the curated grid popped in.

Track moderation readiness alongside the featured query and treat any
of the three states — moderation not ready, featured query in flight,
featured data not yet defined — as loading.
2026-05-29 13:39:18 -07:00
lemon 3adaf9709f Render featured groups directly without intermediate event flash
The Groups page was firing a global kind-34550 query through
useDiscoverCommunities, rendering the full results, then filtering
them client-side for the 'agora' client tag. This produced a brief
flash of unrelated communities before the curated set settled.

Drop the client-side Agora-tag filter entirely and stop using the
all-communities fetch for the idle render path. The unified Groups
section now renders moderator-featured groups directly, gated on
useFeaturedOrganizations's own loading state, so the page goes
skeleton → curated grid with no intermediate render.

useDiscoverCommunities is still called for moderators only — it
feeds the Hidden collapsible section and the hidden-count badge on
the toolbar. Non-moderators no longer trigger the global fetch at
all.
2026-05-29 13:39:18 -07:00
lemon eebb6bf424 Merge Featured and All sections on discovery pages
Campaigns, Groups, and Pledges each previously stacked a Featured
shelf above an All-X section. Collapse them into a single section
titled simply 'Campaigns' / 'Groups' / 'Pledges' that:

- Idle (no query, no sort, no country) shows the moderator-featured
  grid. If nothing is featured yet, falls back to the chronological
  all-X grid so the page is never blank.
- Active (the user typed, picked Top/New, or chose a country) shows
  the full result set, ranked or chronological per the toolbar.

The shared toolbar drops the 'default' sort option from its dropdown
(now only Top and New). Clicking an already-active sort returns the
page to the curated idle view, giving users a clear exit affordance
now that 'default' is no longer an explicit menu choice.

Personal shelves (My pledges / My groups / Your campaigns) stay
above the unified section as separate, user-scoped lists.
2026-05-29 13:39:18 -07:00
lemon 51d3acd076 Shorten All groups tagline 2026-05-29 13:39:18 -07:00
lemon 58bcd56787 Hide group shelves until content is known
Regression-of: 5607f5fa
2026-05-29 13:39:18 -07:00