Commit Graph

4795 Commits

Author SHA1 Message Date
Chad Curtis 13a0bb3e3a release: v2.8.2 v2.8.2 2026-06-02 03:09:16 -05:00
Chad Curtis 646ed9777f ci: pass alias key password to keytool keystore migration
The build-apk JKS->PKCS12 migration only supplied the store password,
so keytool prompted for the upload key's distinct password on the
non-interactive runner and failed with 'Too many failures - try later'.
Pass -srckeypass/-destkeypass ($KEY_PASSWORD) to match key.properties.
2026-06-02 03:07:04 -05:00
Chad Curtis 437613641a release: v2.8.1 v2.8.1 2026-06-02 03:02:12 -05:00
Chad Curtis d0836328a4 qrcode: stop fixed pixel styles overflowing the container
The qrcode library hard-codes inline width/height pixel styles on the
canvas, overriding the Tailwind sizing classes (h-auto w-full) callers
pass in. On viewports narrower than the QR's intrinsic size this made
the code spill outside its rounded box — visible on the campaign
details donate panel. Remove the inline styles after rendering so the
caller's className controls the responsive size.
2026-06-02 02:02:58 -05:00
Chad Curtis 123f53e7a6 campaigns: carry public/private framing into custom wallet mode
Switching to a custom (manual-entry) wallet used to drop the friendly
accept picker entirely, leaving two unlabeled-purpose address inputs.
Restore the hand-holding: add an intro line restating the field-driven
model (public address, private code, or both) and label each input
with its meaning. The public/on-chain input is marked with a Bitcoin
icon and a 'Public. Anyone can see these donations.' caption; the
silent-payment input with an EyeOff icon and a privacy caption. Both
inputs keep the Wallet leading icon. Updates all 16 locales.
2026-06-02 01:59:44 -05:00
Chad Curtis 977fd000ea campaigns: make the donation-accept picker friendlier
Replace the three terse jargon pills (Accept All / Public Only /
Private Only, captioned with 'on-chain' and 'silent payment') with a
vertical stack of selectable option cards. Each card has a friendly
icon, a plain-language title, and a one-sentence reassurance written
for an anxious first-time creator, with the SP-dependent options
clearly disabled when the login can't support them.

Also softens the wallet hero card: drop the linked-icon trio for a
simple campaign-to-wallet arrow, and rewrite the copy without the
key/posts technical aside or em dashes. Updates all 16 locales.
2026-06-02 01:59:44 -05:00
Chad Curtis 5132141aa2 campaigns: give the wallet step a hero coupling card
Redesign the 'My wallet' branch of the campaign wizard's donation
destination step. Replace the plain identity+balance row with a
primary-tinted hero card modelled on the onboarding 'Save your key'
surface: a linked-icon trio (campaign -> key -> wallet) explains that
donations land in the creator's own Agora wallet unlocked by the same
key that signs their posts, with the avatar+live-balance chip
confirming the exact destination and a ShieldCheck reassurance line
below.
2026-06-02 01:59:44 -05:00
Chad Curtis b6dc57eb85 Merge branch 'feat/send-MAX' into 'main'
Feat/send max

See merge request soapbox-pub/agora!40
2026-06-02 06:36:55 +00:00
Chad Curtis 016a7b4a7d Constrain event detail pages to max-w-3xl
Both PostDetailShell (Nostr event details) and ExternalContentPage
(NIP-73 external content like bitcoin:tx) rendered their <main> with no
max-width under the wide layout, stretching edge-to-edge on large
screens. Add w-full max-w-3xl mx-auto to match the narrow-layout column
width used elsewhere.
2026-06-02 01:30:55 -05:00
mkfain 7ae63883e9 campaigns: surface 'Add to list' in the detail-page three-dots menu 2026-06-02 00:28:08 +02:00
mkfain d4cf4ba0d8 campaigns: collapse curated lists strip after first 5 pills 2026-06-02 00:28:08 +02:00
Chad Curtis 399dc53395 Merge branch 'remove-pin' into 'main'
Remove redundant campaign location pin icons

See merge request soapbox-pub/agora!39
2026-06-01 22:26:29 +00:00
filemon 699e505fb5 Merge remote-tracking branch 'origin/main' into remove-pin
# Conflicts:
#	src/components/CampaignCard.tsx
2026-06-02 00:23:07 +02:00
lemon 20839f4de3 wallet: match silent inputs without prevout index 2026-06-01 15:18:13 -07:00
lemon 4e9da2d168 wallet: prune rediscovered spent silent outputs 2026-06-01 15:18:13 -07:00
lemon 32b477bd01 wallet: refresh history after pruning silent inputs 2026-06-01 15:18:13 -07:00
lemon 564459e12d wallet: disable send button when balance is unspendable 2026-06-01 15:18:13 -07:00
lemon c97d0723a6 wallet: add max send option 2026-06-01 15:18:12 -07:00
filemon 53da626461 Remove redundant campaign location pin icons
Regression-of: ba2c541c
2026-06-02 00:13:35 +02:00
Chad Curtis c79699ca71 campaigns: remove deadline from events and form
Drop the optional `deadline` tag from kind 33863 campaigns. Removes the
date input and validation from the create/edit form, the deadline chips
on the card, detail, and inline-preview surfaces, and the derived
"ended" state that disabled donations after the deadline. Cleans up the
associated locale keys and NIP.md documentation.
2026-06-01 17:03:30 -05:00
Alex Gleason e58c031a85 Merge branch 'main' of gitlab.com:soapbox-pub/agora 2026-06-01 23:17:58 +02:00
Alex Gleason bc80dba826 home: fan out single-relay queries to fix load waterfall
The home page serialized its first paint behind relay.ditto.pub:
useCampaignLists queried that one relay via nostr.relay(DITTO_RELAY)
(awaited, up to an 8s timeout) and every hero campaign was gated on
its result, so a slow ditto.pub stalled the whole page. Connection
sharing made it worse — pooled queries multiplexed onto the same
stalled socket.

Switch the home-critical moderation/list/discovery hooks from
single-relay nostr.relay(DITTO_RELAY) calls to the parallel pooled
nostr.query() fan-out:

- useCampaignLists: authors:[curator] filter enforces trust; relay
  pin was unnecessary and headed the waterfall.
- useCampaignModeration: authors:[moderators] filter enforces trust.
- useFeaturedOrganizations: per-author filters enforce curation.
- useDiscoverCommunities: global discovery — fan-out broadens coverage.

useDashboardCounts stays pinned: NIP-45 COUNT is a single-relay
primitive and isn't mergeable across relays, and it's off the home
critical path.

Regression-of: 3d825aef
2026-06-01 23:16:45 +02:00
Chad Curtis 611f97488e home: drop label-based hidden filter from the WLC hero row
The home page hero row is already a moderator-curated kind-30003 list,
so re-filtering its members through the agora.moderation hide axis was
redundant: a campaign that shouldn't appear simply shouldn't be on the
list. The hidden-filter only mattered in the narrow window where a
listed campaign also carried a moderator hidden label, and it cost an
extra limit:2000 kind-1985 query to DITTO_RELAY on every landing-page
load for that edge case.

Render the curated list verbatim, in list order. Label-based hidden
moderation still lives on /campaigns and every other surface; only the
home hero row stops consulting it.
2026-06-01 16:04:22 -05:00
Alex Gleason a948725245 home: stop fetching kind 1985 moderation labels
The home page (CampaignsPage) called useCampaignModeration() solely to
drop hidden campaigns from the WLC hero row, which fired a kind 1985
label query (limit 2000) on every initial load just to check ≤6
curated coords. Remove the dependency: the hero row now only reorders
to the moderator-curated list order. Hidden-campaign moderation already
lives entirely on /campaigns, so the home page no longer needs it.
2026-06-01 22:51:19 +02:00
mkfain dde9865284 campaigns: drop duplicate arrow from browseAll button label
The 'Browse all campaigns' Link on the home page renders an <ArrowRight>
lucide icon next to t('campaigns.home.browseAll'), but the translated
string itself ended in '→' (or '←' for RTL locales), so the button
displayed two arrows. Strip the literal arrow from all 16 locale files
and let the icon do the visual work — it already handles RTL via
rtl:rotate-180 in CampaignsPage.tsx.
2026-06-01 22:41:06 +02:00
Chad Curtis 3d825aef04 campaigns: hardcode moderators, gate lists on a single curator
The home page used to serialize two single-relay round-trips before any
campaign card could render: useCampaignModerators fetched the Team Soapbox
follow pack (kind 39089), and useCampaignLists waited on it to apply an
authors: gate. Each could stall up to an 8s EOSE timeout against the app
relay.

Both lookups are now eliminated from the critical path:

- CAMPAIGN_MODERATORS in agoraDefaults.ts is a hardcoded snapshot of the
  pack's p tags. useCampaignModerators serves it synchronously (no
  queryFn network call), keeping its useQuery return shape so all ~15
  consumers work unchanged. The roster changes rarely; update the array
  and re-cut a release when it does.

- Lists are an editorial surface curated by one identity (MK Fain / Team
  Soapbox), not the whole moderator pack. useCampaignLists now pins
  authors: to LIST_CURATOR_PUBKEY and no longer depends on the moderator
  query at all. The multi-author allowlist remains for labels only
  (approve/hide), where any pack member is trusted.

Regression-of: be1fadfc
2026-06-01 15:36:08 -05:00
Chad Curtis 575603554b home: decouple funding-bar skeleton from card, parallelize list queries
CampaignCard now paints immediately and shows a dedicated skeleton for
the funding/progress bar while useCampaignDonations resolves, instead of
flashing a misleading "0 raised" before the on-chain balance lands.

useCampaignLists no longer serializes behind useCampaignModerators: the
list relay query fires immediately on the hashtag filter and the
moderator allowlist is applied client-side in foldCampaignLists. The two
single-relay round-trips (each up to an 8s EOSE timeout) now run in
parallel on cold sessions. The trust gate is unchanged — a list authored
by a non-moderator is dropped before it reaches the UI.
2026-06-01 15:36:08 -05:00
Alex Gleason dfb0a52603 Upgrade Nostrify 2026-06-01 22:20:43 +02:00
mkfain 545e6cf4be home: rewrite whyDifferent lede as a concrete manifesto strapline
Replace the generic "Three things that make us not like the
others." with copy that names the actual mechanism and three
specific threat models the section addresses:

  "Direct Bitcoin from donor to activist. No platform in the
   way, no custodian holding the bag, no permission required."

Three short clauses, one per block:

  • "No platform in the way" sets up block 1 (vs GoFundMe / Stripe /
    Visa platform censorship).
  • "No custodian holding the bag" sets up block 2 (vs other
    "Bitcoin" platforms with Lightning custodians / LSPs).
  • "No permission required" sets up block 3 (the public/private
    receiving choice — your threat model, your call).

Updated across all 11 locales. The headline ("Built different.")
stays in place; the lede now carries the weight that the giant
Bebas Neue display headline needs as support.
2026-06-01 22:09:54 +02:00
mkfain eb978d651c home: trim block1 heading to just "Unlike GoFundMe"
Drop the trailing "and similar sites" qualifier across all 11
locales. Matches the shorter form already used in the
\`about.twoWays.noCustody.gofundme.heading\` key, so the two
surfaces ("home / why different" and "/about / no custody")
now read consistently.
2026-06-01 22:03:08 +02:00
mkfain 7a52631eb2 home: redesign whyDifferent as a manifesto-style editorial section
Drop the brand-orange band entirely. The section now sits on the
canonical \`bg-background\` so it reads as a continuation of the
home page, not a separate marketing slab. No more navy/slate
surfaces.

New visual structure:

  • Decorative spine — a soft vertical brand-orange gradient line
    runs down the left margin (md+), evoking an editorial /
    manifesto feel without changing the page surface.

  • Eyebrow framed with brand-orange leader lines + tracking-wider
    "WHY ÁGORA" wordmark — reads like a chapter marker.

  • Giant Bebas Neue italic display headline at scale (text-5xl
    → text-7xl), uppercase, stroke-painted. The headline now
    earns the visual weight that a colored background was doing
    before. Matches the page hero typography exactly.

  • Three numbered chapters (01 / 02 / 03), each anchored by a
    massive italic Bebas Neue numeral in brand orange paired
    with a thin orange seam line. No card chrome — chapters sit
    directly on the page background so the section reads as
    continuous editorial copy, not three boxed tiles. Each
    chapter has heading + mission paragraph + brand-orange
    ✓ checklist for blocks 1-2.

  • Block 3 uses a split-card public/private cell pair with
    brand-orange (public) and muted (private) tints, framed in
    a single rounded border — a tiny diagram of the "your
    choice" framing rather than a generic bullet list.

  • Soft brand-orange halo behind the headline (CSS only, blur-3xl)
    for depth.

  • Closing CTA is now a small text-link with an underlined wordmark
    and a chevron that nudges on hover — quieter than a button,
    consistent with the editorial idiom.

Drops the indigo accent that the v1 design used and standardizes
on brand-orange + neutral foreground/muted-foreground tokens,
which means dark mode inherits the canonical dark surface and
typography automatically.

No new translation keys; reuses the existing
\`campaigns.home.whyDifferent.*\` strings as-is. Dropped unused
\`Bitcoin\` and \`ShieldOff\` icon imports.
2026-06-01 21:49:46 +02:00
mkfain d48094ff68 home: recolor whyDifferent section to brand orange band
Background was cream-on-light / dark-navy-on-dark. Swap to a
brand-orange band (`bg-primary`) with dark slate type on top:

- Section heading: `text-slate-900` for ~9:1 contrast on orange
- Eyebrow: `text-white/90` (label-on-orange feel, AA on hsl(24 100% 50%))
- Lede: `text-slate-800/90`
- Cards: solid white in light mode (was `bg-white`-on-cream, now
  reads as crisp surfaces lifted off the orange) and dark slate
  in dark mode; copy is slate-700/-600
- Card shadows bumped to `shadow-md` so cards sit proud of the
  saturated orange instead of disappearing into it
- Block 3 accent changed from indigo to neutral slate so the
  third card doesn't compete chromatically with the orange band
- Read-the-full-breakdown CTA is now a solid dark-slate pill
  with white text (instead of an outline button that disappeared
  on the new background)
2026-06-01 21:49:46 +02:00
mkfain 247fbefa9b home: add "Why Ágora is different" info section at the bottom
Three-block info band beneath the WLC hero row and topic-list
strip explaining what makes Ágora different:

  1. Unlike GoFundMe and similar sites — no platform freeze, no
     payment-processor middleman, zero platform fees.
  2. Unlike other "Bitcoin" platforms — no central Lightning
     node, custodian, or LSP; settles on-chain to a wallet you
     control.
  3. Public or private — receiving-option contrast (Bitcoin
     on-chain vs BIP-352 silent payments) with a CTA to the
     long-form breakdown at /about#how-it-works.

Visual idiom matches the AboutPage sections (cream / dark-navy
band, brand-orange eyebrow, Inter Bold heading, RailCard-style
cards with icon chip + checklist) so the home page reads as a
shorter front-door version of /about. Always visible — the
value prop is part of the home page identity, not gated on
auth state.

Strings live under `campaigns.home.whyDifferent.*` with full
translations in all ten canonical locales (ar, es, fa, fr, km,
ps, pt, ru, sn, zh). Technical tokens (GoFundMe, Stripe, Visa,
Bitcoin, Lightning, LSP, BIP-352, QR) and the {{appName}}
placeholder are preserved verbatim across locales.
2026-06-01 21:49:46 +02:00
Chad Curtis 4a3c5df519 Don't let an empty persisted translateWorkerUrl hide the Translate button
A blank `translateWorkerUrl` saved to localStorage was shadowing the
build-time default in the config merge, so the Translate button's
"no worker configured" guard hid it even when VITE_TRANSLATE_WORKER_URL
was set. Coalesce an empty persisted value back to the default, and stop
the Advanced Settings field from persisting an empty string on blur.
2026-06-01 14:32:09 -05:00
mkfain 74478ee8ac Remove the 'View the full list' link under the WLC hero row
The link encouraged users to navigate away from the home page to
see members beyond the visible cap. The home page is the
editorial surface; if a campaign isn't in the visible cap, that's
the curator's call. Cleanup drops the link, the campaigns.home.viewFullList
key across all 16 locales, and nothing else.
2026-06-01 21:18:56 +02:00
mkfain da94609855 Replace the Featured campaign concept with the World Liberty Congress list
The home page's hero row was driven by kind-1985 'featured' /
'unfeatured' moderation labels (the campaign-specific Featured
axis). Now that curated lists exist, the WLC-published list with
d='world-liberty-congress' is a strictly better mechanism: same
trust model (moderator-published), explicit ordering (positional
'a' tags instead of a separate rank stream), and the membership is
edited through the same Add-to-list flow that powers every other
list.

Changes:

- CampaignsPage: replace the Featured row with a hero row backed by
  useCampaignList('world-liberty-congress'). Capped at 6 entries
  with a 'View the full list' link to the list's detail page when
  there's overflow. The WLC avatar/name/check still anchor the
  heading. The empty state covers both 'no list yet' and 'list
  exists but empty'.
- CampaignCard: drop the verifiedBy prop and the WLC verified-by
  chip. Nothing else passed verifiedBy.
- CampaignCard: stop opting into the 'featured' axis on the
  moderator kebab. Only 'hide' remains for campaigns.
- ModerationMenu / ModerationOverlay: strip the reorder prop chain
  (only the deleted Featured row consumed it). Pledge / group
  surfaces keep their 'featured' axis since their featured shelves
  are unchanged.
- Delete useReorderCampaign, ReorderableCampaignGrid,
  ReorderProvider, reorderContext — the campaign-rank reordering
  infrastructure they served is gone.
- Update i18n: drop campaigns.home.featured, featuredDesc,
  verifiedByAria across all 16 locales. Add wlcDesc and
  viewFullList. Translations dispatched in parallel.

The featuredCoords / featuredOrder fields in the shared moderation
fold (agoraModeration.ts) stay — they're still consumed by
useFeaturedOrganizations (groups) and usePledgeModeration (pledges).
Existing kind-1985 'featured' labels referencing campaign coords
become inert: nothing reads them, but the label namespace is
shared so we don't garbage-collect them.
2026-06-01 21:18:56 +02:00
mkfain 8b90ef90f7 Wrap campaign list pills instead of horizontal scroll
The strip used overflow-x-auto with a thin scrollbar, which cut off
pills past the viewport edge on smaller screens. Switch to
flex-wrap so the pills flow onto multiple rows and stay fully
visible without any scroll affordance.
2026-06-01 21:18:07 +02:00
mkfain 49f0ec2765 Remove the moderator-only Hidden section from the home page
The home page is meant to be tightly curated — Featured row + topic
strip + browse-all CTA. Even keeping the Hidden collapsible closed-
by-default for moderators meant the home page was carrying a
review surface that belongs on /campaigns, where the Show-hidden
toggle is already available to everyone and the structured Hidden
collapsible already exists.

Drops the Hidden section's rendering and all of its supporting
state: the recent-stream useCampaigns call, the targeted hidden-
coord useCampaigns call, hiddenCoordList, hiddenCampaigns, isMod,
plus the imports they kept alive (EyeOff, ModeratorCollapsibleSection,
CampaignGridSkeleton).
2026-06-01 21:18:07 +02:00
mkfain 72c2170139 Replace the home page 'All campaigns' grid with the topic-lists strip
The chronological 'All campaigns' grid on the home page duplicated
what /campaigns already does better (search, sort, country filters,
unbounded scroll). Swap it for the curated topic-list strip
(CampaignListsStrip) followed by a single 'Browse all campaigns'
CTA that links to /campaigns. The Verified hero row above and the
moderator-only Hidden section below are unchanged.

Removed the no-longer-needed allCampaignsChronological derived
state, featuredCoordSet O(1) lookup, useReorderCampaign /
useToast / onFeaturedMoveToTop/Up/Down callbacks, and the
ConditionalReorderProvider helper that wrapped the chronological
grid for moderators.
2026-06-01 21:17:44 +02:00
mkfain a0082cbbcd Refetch list membership when the Add-to-List dialog opens
useCampaignLists caches its query for 30 seconds, so a moderator who
added a campaign to a list from one surface (e.g. the list detail
page) and then opened the per-campaign membership dialog for the
same campaign from another card would see stale 'Add' buttons for
those lists until the cache expired.

Invalidate the campaign-lists query whenever the dialog opens so the
membership state always reflects the latest published revisions
without requiring a page refresh.
2026-06-01 21:17:01 +02:00
mkfain a8561f46f9 Fix list-membership dialog navigation + hide hidden campaigns from add-to-list search
Two fixes for the curated lists feature:

1. Clicking an Add/Added toggle in the per-campaign membership dialog
   was navigating to the campaign's detail page. Although Radix Dialog
   portals content to document.body, React's synthetic events still
   bubble through the React tree — past the Link that wraps the
   CampaignCard the moderator opened the kebab from. Stop propagation
   on the toggle's click handler and at the DialogContent root.
   Applied the same stopPropagation to ListFormDialog and IconPicker
   since both can mount inside the membership flow.

2. The campaign-search dialog opened from a list detail page was
   surfacing campaigns hidden by moderators. Filter the search
   results through useCampaignModeration.hiddenCoords so suppressed
   campaigns don't get encouraged into curated lists. Existing list
   members that later get hidden remain visible in the dialog so a
   moderator can still remove them.
2026-06-01 21:17:01 +02:00
mkfain 2c248f8269 Add 'Add to list…' row to the campaign moderator kebab
Adds a new row at the top of the moderator dropdown on campaign cards
(both / and /campaigns) that opens a per-campaign list-membership
modal. Each known curated list renders as a row with the campaign's
current membership state — toggling immediately publishes a new
revision of the list event through useCampaignListActions, so a
moderator can multi-tag a campaign without leaving the dialog. The
modal also exposes a '+ New list' shortcut that runs the standard
create flow and auto-adds the campaign to the just-created list.

The membership dialog's state is owned by ModerationMenu (the kebab
trigger), not by the dropdown content. Radix unmounts content on
close, so a sibling dialog rendered inside DropdownMenuContent would
be torn down on the same tick the user clicks the item. Lifting the
state to the trigger lets the dialog survive the menu closing.
2026-06-01 21:17:01 +02:00
mkfain b8749f7064 Add moderator-curated campaign lists to /campaigns
Lists are NIP-51 kind 30003 Bookmark Sets authored by Team Soapbox
moderators (the same allowlist gating Featured / Hidden), carrying
the 'agora.campaign-list' hashtag plus a custom 'icon' tag holding a
Lucide icon name. Membership order is encoded in the order of the
'a' tags on the event; the order of the topic strip itself is held
in a sentinel kind 30003 with d='agora.campaign-lists.index'.

Replaces the 'Your campaigns' shelf on /campaigns with a horizontal
strip of pill buttons (one per list). Each pill links to a new
/campaigns/lists/:slug detail page rendering the list members in
moderator-defined order. Moderators see a trailing '+' pill to
create a list, a per-pill kebab for edit/delete/move, and drag-and-
drop to reorder the strip on desktop. Inside each list, moderators
can search and add campaigns, remove members, and reorder via the
same native-HTML5 DnD pattern.

The icon picker is searchable over every named Lucide icon. The
registry is dynamically imported through a single shared module so
the full library lives in its own Vite chunk and the main bundle
isn't penalized; LucideIcon renders a 'List' fallback while the
chunk resolves.
2026-06-01 21:17:01 +02:00
Chad Curtis f800d55451 Stop clobbering VITE_* CI vars with literal placeholders
The deploy-web job re-declared project-level CI/CD variables as `KEY: $KEY`.
When a source variable is out of scope for the job (e.g. a Protected variable
on an unprotected ref), GitLab leaves the reference unexpanded, so the literal
string "$VITE_TRANSLATE_WORKER_URL" got inlined into the build and surfaced in
the UI. Project-level variables are already in the job environment, so the
re-declaration is removed entirely.
2026-06-01 14:15:38 -05:00
Chad Curtis ee8414f694 Make translation worker URL user-configurable via AppConfig
The DeepL translate worker endpoint is now a configurable AppConfig field
(translateWorkerUrl), defaulting to the build-time VITE_TRANSLATE_WORKER_URL
env value with no hardcoded fallback. Users can override or clear it in
Advanced Settings (System section), and the setting syncs across devices via
encrypted NIP-78 settings. The Translate button hides itself when no worker
is configured.
2026-06-01 13:53:52 -05:00
lemon 23ac55af6b Merge campaign profile setup into wizard 2026-06-01 11:28:54 -07:00
lemon 2ef0642f6d Keep wizard chrome above body content 2026-06-01 11:15:18 -07:00
lemon 18aacad290 Require campaign creator profiles 2026-06-01 11:09:47 -07:00
mkfain e82f0146d2 Allow reordering over-cap featured campaigns from All Campaigns
The 6-card cap on the WLC Verified row meant moderators couldn't
reach featured campaigns at positions 7..N to reorder them — the
drag handles and kebab move rows only existed on cards in the
visible hero row.

Wrap the All Campaigns section in a ReorderProvider seeded with the
*full* featuredCoords list (not just what's visible) so every
WLC-chipped card in the chronological grid gets the same Move up /
Move down / Move to top rows in its kebab. The provider only mounts
for moderators; non-mods see no behavior change.

Non-featured cards aren't in the provider's byCoord lookup, so
their kebab simply doesn't show reorder rows — the moderation menu
already gates the section behind canMoveUp || canMoveDown.

No optimistic local reorder here: the chronological grid is sorted
by createdAt, not by featured rank, so a successful 'Move to top'
on a position-12 card lifts it into the Verified hero row above
(and out of the chronological feed, via the existing heroSet
dedupe) once the moderation pack invalidates and refetches.
Failures surface as a toast, matching ReorderableCampaignGrid.

A small ConditionalReorderProvider helper keeps the JSX clean and
spares non-mods the provider work.
2026-06-01 19:51:51 +02:00
mkfain 973defcd28 All campaigns: deduplicate vs hero row, sort oldest-first
Two corrections to the new 'All campaigns' section on the home page:

1. Deduplicate against the Verified hero row. Campaigns rendered in
   the row above are now excluded from the chronological feed below
   (matched by aTag against orderedFeatured). Over-cap featured
   campaigns — the ones a moderator featured beyond the 6-card cap —
   still appear here, and still pick up the WLC chip via
   featuredCoordSet. The user sees each campaign at most once on the
   home page.

2. Sort by createdAt ascending (oldest first), not descending. The
   spec was 'chronological order from when they were created,' not
   reverse-chronological. The allCampaignsDesc copy is also updated
   in all 16 locales to drop the 'newest first' language.
2026-06-01 19:21:05 +02:00