From a0ca42af26079132d0b56d9c49cab818a2fec9ab Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 14:14:32 -0300 Subject: [PATCH 01/16] Polish dashboard page to match app design system Normalize the dashboard layout and card styling for visual consistency with the rest of the app. No data fetching or behavioral changes. Page container: - Widen content from max-w-4xl to max-w-5xl (justifies noMaxWidth opt-out) - Add responsive padding (px-4 sm:px-6) - Replace non-standard pb-24 with pb-16 sidebar:pb-0 - Add min-h-screen to prevent short-page footer ride-up - Error state now uses the same max-w-5xl container (no layout jump) Header actions: - Replace Trash2 icon with PanelLeftClose for sidebar toggle (less alarming) - Convert sidebar toggle from outline button with text to ghost icon button - Add aria-label attributes for accessibility - Tighten gap from gap-2 to gap-1.5 for compact icon-button row - Move statusBadge/headerActions above the error early-return so both code paths share the same header Chart cards (ActivityChart, TopRegionsChart, DistributionDonut): - Replace raw rounded-2xl border divs with shadcn Card/CardHeader/CardContent - Picks up consistent rounded-lg, bg-card, shadow-sm, and standard padding List cards (ParticipantsList, RecentActivityList): - Normalize from rounded-2xl to rounded-lg with bg-card and shadow-sm - Preserve overflow-hidden and custom internal grid layouts Skeleton: - Add tabs placeholder skeleton - Use Card/CardHeader/CardContent for chart skeletons - Normalize table skeleton wrapper to match new card styles Tabs: - Add bg-muted/50 to TabsList for subtle visual grounding --- .../event-dashboard/ActivityChart.tsx | 85 ++++++++++--------- .../event-dashboard/DashboardSkeleton.tsx | 30 +++++-- .../event-dashboard/DistributionDonut.tsx | 85 ++++++++++--------- .../event-dashboard/ParticipantsList.tsx | 2 +- .../event-dashboard/RecentActivityList.tsx | 2 +- .../event-dashboard/TopRegionsChart.tsx | 71 +++++++++------- src/pages/EventDashboardPage.tsx | 76 +++++++++-------- 7 files changed, 190 insertions(+), 161 deletions(-) diff --git a/src/components/event-dashboard/ActivityChart.tsx b/src/components/event-dashboard/ActivityChart.tsx index 00c67f47..557a1d4a 100644 --- a/src/components/event-dashboard/ActivityChart.tsx +++ b/src/components/event-dashboard/ActivityChart.tsx @@ -14,6 +14,7 @@ import { ChartLegendContent, type ChartConfig, } from '@/components/ui/chart'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import type { TimeSeriesBucket } from './types'; interface ActivityChartProps { @@ -27,45 +28,49 @@ const chartConfig: ChartConfig = { export function ActivityChart({ data }: ActivityChartProps) { return ( -
-

Publishing Activity (5-min intervals)

- - - - - - } /> - } /> - - - - -
+ + + Publishing Activity (5-min intervals) + + + + + + + + } /> + } /> + + + + + + ); } diff --git a/src/components/event-dashboard/DashboardSkeleton.tsx b/src/components/event-dashboard/DashboardSkeleton.tsx index 78932933..6232906b 100644 --- a/src/components/event-dashboard/DashboardSkeleton.tsx +++ b/src/components/event-dashboard/DashboardSkeleton.tsx @@ -1,8 +1,12 @@ import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; export function DashboardSkeleton() { return (
+ {/* Tabs skeleton */} + + {/* KPI grid skeleton */}
{Array.from({ length: 6 }).map((_, i) => ( @@ -14,19 +18,27 @@ export function DashboardSkeleton() {
{/* Chart skeleton */} -
- - -
+ + + + + + + + {/* Bar chart skeleton */} -
- - -
+ + + + + + + + {/* Table skeleton */} -
+
diff --git a/src/components/event-dashboard/DistributionDonut.tsx b/src/components/event-dashboard/DistributionDonut.tsx index 7269fa1c..8ce28a62 100644 --- a/src/components/event-dashboard/DistributionDonut.tsx +++ b/src/components/event-dashboard/DistributionDonut.tsx @@ -5,6 +5,7 @@ import { ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import type { DistributionSlice } from './types'; interface DistributionDonutProps { @@ -19,46 +20,50 @@ export function DistributionDonut({ data }: DistributionDonutProps) { ); return ( -
-

Post Distribution

-
-
- - - } /> - - {data.map((slice, i) => ( - - ))} - - - + + + Post Distribution + + +
+
+ + + } /> + + {data.map((slice, i) => ( + + ))} + + + +
+
+ {data.map((slice) => { + const pct = total > 0 ? Math.round((slice.value / total) * 100) : 0; + return ( +
+ + {slice.name} + {pct}% + {slice.value} +
+ ); + })} +
-
- {data.map((slice) => { - const pct = total > 0 ? Math.round((slice.value / total) * 100) : 0; - return ( -
- - {slice.name} - {pct}% - {slice.value} -
- ); - })} -
-
-
+ + ); } diff --git a/src/components/event-dashboard/ParticipantsList.tsx b/src/components/event-dashboard/ParticipantsList.tsx index f22388ca..62b56514 100644 --- a/src/components/event-dashboard/ParticipantsList.tsx +++ b/src/components/event-dashboard/ParticipantsList.tsx @@ -26,7 +26,7 @@ export function ParticipantsList({ data, territorialLevel }: ParticipantsListPro const columnLabel = territorialLevel === 'states' ? 'State' : 'Municipality'; return ( -
+

{title}

diff --git a/src/components/event-dashboard/RecentActivityList.tsx b/src/components/event-dashboard/RecentActivityList.tsx index a095a448..550aedb6 100644 --- a/src/components/event-dashboard/RecentActivityList.tsx +++ b/src/components/event-dashboard/RecentActivityList.tsx @@ -68,7 +68,7 @@ export function RecentActivityList({ data }: RecentActivityListProps) { const paged = data.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE); return ( -
+

Recent Activity

diff --git a/src/components/event-dashboard/TopRegionsChart.tsx b/src/components/event-dashboard/TopRegionsChart.tsx index 80f4f3c8..f691a797 100644 --- a/src/components/event-dashboard/TopRegionsChart.tsx +++ b/src/components/event-dashboard/TopRegionsChart.tsx @@ -12,6 +12,7 @@ import { ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import type { LeaderboardEntry, TerritorialLevel } from './types'; interface TopRegionsChartProps { @@ -33,38 +34,42 @@ export function TopRegionsChart({ data, territorialLevel }: TopRegionsChartProps const title = territorialLevel === 'states' ? 'Top 5 States' : 'Top 5 Municipalities'; return ( -
-

{title}

- - - - - - } /> - - {data.map((_, i) => ( - - ))} - - - -
+ + + {title} + + + + + + + + } /> + + {data.map((_, i) => ( + + ))} + + + + + ); } diff --git a/src/pages/EventDashboardPage.tsx b/src/pages/EventDashboardPage.tsx index cfda8a1c..d95f0da3 100644 --- a/src/pages/EventDashboardPage.tsx +++ b/src/pages/EventDashboardPage.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Activity, PanelLeft, Radio, Settings, Trash2 } from 'lucide-react'; +import { Activity, PanelLeft, PanelLeftClose, Radio, Settings } from 'lucide-react'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { PageHeader } from '@/components/PageHeader'; import { Card, CardContent } from '@/components/ui/card'; @@ -49,17 +49,44 @@ export function EventDashboardPage() { status, isLoading, error, } = useEventDashboard({ enabled: true, territorialLevel }); + // Status badge + const statusBadge = ( + + + + + + {status === 'syncing' ? 'Syncing' : status === 'live' ? 'Live' : status === 'disconnected' ? 'Disconnected' : 'Connecting'} + + ); + + const headerActions = ( +
+ {statusBadge} + + +
+ ); + // Error state if (error && kpis.totalPosts === 0) { return ( -
+
}> - - - Disconnected - + {headerActions} -
+
@@ -72,43 +99,18 @@ export function EventDashboardPage() {
+
); } - // Status badge - const statusBadge = ( - - - - - - {status === 'syncing' ? 'Syncing' : status === 'live' ? 'Live' : status === 'disconnected' ? 'Disconnected' : 'Connecting'} - - ); - return ( -
+
}> -
- {statusBadge} - - -
+ {headerActions}
-
+
{isLoading ? ( ) : ( @@ -118,7 +120,7 @@ export function EventDashboardPage() { value={territorialLevel} onValueChange={(v) => setTerritorialLevel(v as TerritorialLevel)} > - + States Municipalities From 7bb960b6b30355b4c79ab3cfa28ffbd3de38c325 Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 14:24:20 -0300 Subject: [PATCH 02/16] Align dashboard header with content column and add bottom spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass max-w-5xl mx-auto sm:px-6 to PageHeader via its className prop so the title and action buttons sit inside the same centered column as the dashboard body. This is a local override — the shared PageHeader component is not modified. Add pb-8 to the content container for comfortable breathing room between the last dashboard section and the footer. --- src/pages/EventDashboardPage.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/EventDashboardPage.tsx b/src/pages/EventDashboardPage.tsx index d95f0da3..b11083c0 100644 --- a/src/pages/EventDashboardPage.tsx +++ b/src/pages/EventDashboardPage.tsx @@ -79,11 +79,13 @@ export function EventDashboardPage() {
); + const headerClassName = 'max-w-5xl mx-auto sm:px-6'; + // Error state if (error && kpis.totalPosts === 0) { return (
- }> + } className={headerClassName}> {headerActions}
@@ -106,11 +108,11 @@ export function EventDashboardPage() { return (
- }> + } className={headerClassName}> {headerActions} -
+
{isLoading ? ( ) : ( From 45242292d6cdf8590026d0c91d044ea8da3d0ee2 Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 14:32:50 -0300 Subject: [PATCH 03/16] Use stable COUNT floor for participants list counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply getStableCount to the participants/full-state-list section so per-state numbers are consistent with the leaderboard and distribution chart. Previously the participants list used raw event-based counts while the other sections used NIP-45 COUNT floors, causing visible mismatches (e.g. Miranda showing 847 in the list but 859 in the donut). In municipalities view this is a no-op — getStableCount returns the raw feed.count unchanged since there are no per-municipality COUNT queries. Live/activity indicators still derive from loaded events. --- src/hooks/useEventDashboard.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hooks/useEventDashboard.ts b/src/hooks/useEventDashboard.ts index 22ecaaff..87078cea 100644 --- a/src/hooks/useEventDashboard.ts +++ b/src/hooks/useEventDashboard.ts @@ -326,21 +326,23 @@ export function useEventDashboard({ enabled, territorialLevel }: UseEventDashboa }, [displayFeeds, getStableCount]); // Participants (full sorted list). - // Intentionally uses raw event-based counts (no COUNT floor) because each - // row derives live/activity state from the loaded events themselves. + // Counts use the same stable COUNT floor as leaderboard/distribution so + // that per-state numbers are consistent across all dashboard sections. + // Live/activity state still derives from the loaded events themselves. const participants = useMemo(() => { const thirtySecondsAgo = now - 30; return [...displayFeeds] - .sort((a, b) => b.count - a.count) + .map((feed) => ({ ...feed, stableCount: getStableCount(feed) })) + .sort((a, b) => b.stableCount - a.stableCount) .map((feed, i) => ({ rank: i + 1, regionId: feed.regionId, label: feed.label, hashtag: feed.code, - count: feed.count, + count: feed.stableCount, isActive: feed.posts.length > 0 && feed.posts[0].created_at > thirtySecondsAgo, })); - }, [displayFeeds, now]); + }, [displayFeeds, getStableCount, now]); // Activity items (from globalPosts with label resolution) const activity = useMemo(() => { From 6eccacc06a3e55313cc3f2b6d5ba3b3b02ef25e2 Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 14:59:48 -0300 Subject: [PATCH 04/16] Update getStableCount comment to reflect current usage The comment said it was not used in participants, but it now is. --- src/hooks/useEventDashboard.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useEventDashboard.ts b/src/hooks/useEventDashboard.ts index 87078cea..dc2e4173 100644 --- a/src/hooks/useEventDashboard.ts +++ b/src/hooks/useEventDashboard.ts @@ -243,8 +243,8 @@ export function useEventDashboard({ enabled, territorialLevel }: UseEventDashboa return [...muniFeeds, ...customFeeds]; }, [regionFeeds, regionById, territorialLevel]); - // Helper: apply relay COUNT as a stable floor for state-level counts. - // Used only in leaderboard/distribution, NOT in participants. + // Applies the relay COUNT floor to displayed state-level counts. + // No-op for municipalities. const getStableCount = useCallback((feed: AggregatedFeed): number => territorialLevel === 'states' ? Math.max(feed.count, stateCounts?.get(feed.code) ?? 0) From ba08d749accdc454ba49b4f0ab0856b01e5d2e58 Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 15:55:44 -0300 Subject: [PATCH 05/16] Remove sidebar add/remove button from dashboard header --- src/pages/EventDashboardPage.tsx | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/pages/EventDashboardPage.tsx b/src/pages/EventDashboardPage.tsx index b11083c0..7e62e471 100644 --- a/src/pages/EventDashboardPage.tsx +++ b/src/pages/EventDashboardPage.tsx @@ -1,13 +1,11 @@ import { useState } from 'react'; -import { Activity, PanelLeft, PanelLeftClose, Radio, Settings } from 'lucide-react'; +import { Activity, Radio, Settings } from 'lucide-react'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { PageHeader } from '@/components/PageHeader'; import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { useFeedSettings } from '@/hooks/useFeedSettings'; -import { useToast } from '@/hooks/useToast'; import { useEventDashboard } from '@/hooks/useEventDashboard'; import { KpiGrid } from '@/components/event-dashboard/KpiGrid'; @@ -26,20 +24,6 @@ import type { TerritorialLevel } from '@/components/event-dashboard/types'; export function EventDashboardPage() { const [territorialLevel, setTerritorialLevel] = useState('municipalities'); const [configOpen, setConfigOpen] = useState(false); - const { addToSidebar, removeFromSidebar, orderedItems } = useFeedSettings(); - const { toast } = useToast(); - const isInSidebar = orderedItems.includes('dashboard'); - - const handleToggleSidebar = () => { - if (isInSidebar) { - removeFromSidebar('dashboard'); - toast({ title: 'Removed from sidebar' }); - return; - } - - addToSidebar('dashboard'); - toast({ title: 'Added to sidebar' }); - }; // Use wider layout — removes 600px cap but keeps sidebar shell useLayoutOptions({ noMaxWidth: true, rightSidebar: null }); @@ -63,16 +47,6 @@ export function EventDashboardPage() { const headerActions = (
{statusBadge} - From 2f8569c30284b99035df354f0a4cbf556657d69d Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 20:09:36 -0300 Subject: [PATCH 06/16] Fix five small UI polish issues - Push Leaflet zoom controls below the sticky header on /world so they remain clickable instead of sitting behind the 64px top bar. - Replace plain tags with React Router in the site footer so navigation to /help, /privacy, /safety, and /changelog is client-side instead of triggering a full page reload. - Use plain-text preview with line-clamp-3 in the post more-menu instead of NoteContent + a conflicting max-h hard clip, so long content truncates with an ellipsis rather than cutting off abruptly. - Switch the campaign detail Donate/Share row from a rigid 4-column grid to flex so the Share button gets its natural width instead of being cramped into 25% of the row on mobile. - Make the campaign URL preview in /campaigns/new truncate with an ellipsis for long slugs instead of overflowing or clipping silently. --- src/components/FundraiserLayout.tsx | 10 +++++----- src/components/NoteMoreMenu.tsx | 7 ++++--- src/index.css | 9 +++++++++ src/pages/CampaignDetailPage.tsx | 12 ++++++------ src/pages/CreateCampaignPage.tsx | 8 ++++---- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/FundraiserLayout.tsx b/src/components/FundraiserLayout.tsx index 3f194b4e..ff5916e1 100644 --- a/src/components/FundraiserLayout.tsx +++ b/src/components/FundraiserLayout.tsx @@ -1,5 +1,5 @@ import { Suspense, useCallback, useMemo, useRef, useState } from 'react'; -import { Outlet } from 'react-router-dom'; +import { Link, Outlet } from 'react-router-dom'; import { TopNav } from '@/components/TopNav'; import { Skeleton } from '@/components/ui/skeleton'; @@ -97,10 +97,10 @@ function SiteFooter() { diff --git a/src/components/NoteMoreMenu.tsx b/src/components/NoteMoreMenu.tsx index e5da6950..5ac6a454 100644 --- a/src/components/NoteMoreMenu.tsx +++ b/src/components/NoteMoreMenu.tsx @@ -39,7 +39,6 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { BanConfirmDialog } from '@/components/BanConfirmDialog'; -import { NoteContent } from '@/components/NoteContent'; import { EmojifiedText } from '@/components/CustomEmoji'; import { ReportDialog } from '@/components/ReportDialog'; import { CommunityReportDialog } from '@/components/CommunityReportDialog'; @@ -520,11 +519,13 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe · {timeAgo(event.created_at)}
-
+
{/^[A-Za-z0-9+/=_-]{20,}$/.test(event.content.trim()) ? ( Encrypted content ) : ( - +

+ {event.content} +

)}
diff --git a/src/index.css b/src/index.css index 07021d7c..d80ad862 100644 --- a/src/index.css +++ b/src/index.css @@ -167,6 +167,15 @@ } } +/* Push Leaflet's top-positioned controls (zoom +/−) below the sticky app + header so they remain clickable on /world. The header is 4rem (64px) + tall; the extra 0.5rem gives a comfortable gap. The default + `leaflet.css` sets `.leaflet-top .leaflet-control { margin-top: 10px }` + which lands behind the header. */ +.leaflet-top .leaflet-control { + margin-top: 4.5rem !important; +} + /* Leaflet control theming — override the default white/gray controls to match the app's themed palette. The default `leaflet/dist/leaflet.css` ships with hardcoded `background: white` / dark border colors that diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index ab390c03..cd929081 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -446,10 +446,10 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { )} -
+
- @@ -716,9 +716,9 @@ function CampaignDetailSkeleton() {
-
- - +
+ +
diff --git a/src/pages/CreateCampaignPage.tsx b/src/pages/CreateCampaignPage.tsx index b7bf87d9..12611627 100644 --- a/src/pages/CreateCampaignPage.tsx +++ b/src/pages/CreateCampaignPage.tsx @@ -615,12 +615,12 @@ export function CreateCampaignPage() { maxLength={200} required /> -

- URL preview:{' '} - +

+ URL preview: + /{activeIdentifier || 'your-campaign-title'} - {isEditMode && ' (kept from original)'} + {isEditMode && (kept from original)}

From fc950865c4c78d80e5b24a3d88388bc6d0c4fdff Mon Sep 17 00:00:00 2001 From: filemon Date: Mon, 18 May 2026 20:35:10 -0300 Subject: [PATCH 07/16] Fix post menu URL overflow, campaign slug ellipsis, and zoom control jump - NoteMoreMenu: use overflow-wrap-anywhere so long URLs break safely within the 3-line post preview instead of overflowing the dialog - CreateCampaignPage: add min-w-0 to the slug truncate span and show trailing '...' when the slug was clipped to the 64-char limit - index.css: move Leaflet top offset from margin-top on individual controls to top on the .leaflet-top container, preventing the visual jump that occurred when Leaflet re-rendered controls on zoom --- src/components/NoteMoreMenu.tsx | 2 +- src/index.css | 9 --------- src/pages/CreateCampaignPage.tsx | 4 ++-- src/pages/WorldPage.tsx | 13 ++++++------- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/components/NoteMoreMenu.tsx b/src/components/NoteMoreMenu.tsx index 5ac6a454..09c6362f 100644 --- a/src/components/NoteMoreMenu.tsx +++ b/src/components/NoteMoreMenu.tsx @@ -523,7 +523,7 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe {/^[A-Za-z0-9+/=_-]{20,}$/.test(event.content.trim()) ? ( Encrypted content ) : ( -

+

{event.content}

)} diff --git a/src/index.css b/src/index.css index d80ad862..07021d7c 100644 --- a/src/index.css +++ b/src/index.css @@ -167,15 +167,6 @@ } } -/* Push Leaflet's top-positioned controls (zoom +/−) below the sticky app - header so they remain clickable on /world. The header is 4rem (64px) - tall; the extra 0.5rem gives a comfortable gap. The default - `leaflet.css` sets `.leaflet-top .leaflet-control { margin-top: 10px }` - which lands behind the header. */ -.leaflet-top .leaflet-control { - margin-top: 4.5rem !important; -} - /* Leaflet control theming — override the default white/gray controls to match the app's themed palette. The default `leaflet/dist/leaflet.css` ships with hardcoded `background: white` / dark border colors that diff --git a/src/pages/CreateCampaignPage.tsx b/src/pages/CreateCampaignPage.tsx index 12611627..33ff338b 100644 --- a/src/pages/CreateCampaignPage.tsx +++ b/src/pages/CreateCampaignPage.tsx @@ -617,8 +617,8 @@ export function CreateCampaignPage() { />

URL preview: - - /{activeIdentifier || 'your-campaign-title'} + + /{activeIdentifier || 'your-campaign-title'}{!isEditMode && derivedIdentifier.length >= 64 && '...'} {isEditMode && (kept from original)}

diff --git a/src/pages/WorldPage.tsx b/src/pages/WorldPage.tsx index c79cea58..947e67b1 100644 --- a/src/pages/WorldPage.tsx +++ b/src/pages/WorldPage.tsx @@ -68,13 +68,12 @@ export function WorldPage() { }); return ( - // h-dvh inside the column fills the full viewport on both mobile (where - // the column's negative margin pulls content under the translucent top - // bar) and desktop (where there's no top/bottom chrome). The floating - // discovery button is absolutely positioned inside this wrapper so it - // stays scoped to the column and doesn't overlap the docked desktop - // discovery panel. -
+ // The height must account for the sticky TopNav (h-16 = 4rem) so the + // map fills exactly the remaining viewport. Using `h-dvh` (100dvh) + // would make the page scrollable and let the sticky header overlap the + // top of the map — hiding the Leaflet zoom controls. The calc keeps + // the page at exactly one viewport with no scroll. +
From f6b209949a0d2b51e312df31e164a346b879d4b0 Mon Sep 17 00:00:00 2001 From: filemon Date: Tue, 19 May 2026 12:00:53 -0300 Subject: [PATCH 08/16] Fix post menu preview: keep NoteContent with working line-clamp The previous attempt replaced NoteContent with plain text, losing rich rendering of hashtags, mentions, custom emoji, and nostr identifiers. Reverting to NoteContent while keeping the overflow fix needed two changes to make line-clamp-3 work: - Render NoteContent as a (as="span") so it participates in the parent's -webkit-box line counting. The default
wrapper with overflow-hidden created a separate block formatting context that defeated line-clamp entirely. - Add disableNoteEmbeds to prevent block-level EmbeddedNote/ EmbeddedNaddr cards from appearing inside the compact preview. The outer container keeps overflow-wrap-anywhere so long URLs break safely within the clamped area. Regression-of: fc950865 --- src/components/NoteMoreMenu.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/NoteMoreMenu.tsx b/src/components/NoteMoreMenu.tsx index 09c6362f..8963ab1a 100644 --- a/src/components/NoteMoreMenu.tsx +++ b/src/components/NoteMoreMenu.tsx @@ -39,6 +39,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { BanConfirmDialog } from '@/components/BanConfirmDialog'; +import { NoteContent } from '@/components/NoteContent'; import { EmojifiedText } from '@/components/CustomEmoji'; import { ReportDialog } from '@/components/ReportDialog'; import { CommunityReportDialog } from '@/components/CommunityReportDialog'; @@ -519,13 +520,11 @@ function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onRe · {timeAgo(event.created_at)}
-
+
{/^[A-Za-z0-9+/=_-]{20,}$/.test(event.content.trim()) ? ( Encrypted content ) : ( -

- {event.content} -

+ )}
From cb32405e558b4f64970c38f1ba5767bb16d7e1d0 Mon Sep 17 00:00:00 2001 From: filemon Date: Tue, 19 May 2026 16:48:53 -0300 Subject: [PATCH 09/16] Clarify that campaign goal is saved as sats, prevent edit drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goal input accepts USD but the published event stores sats. The UI gave no indication of this, so the displayed USD goal silently drifted as BTC price changed — confusing creators. Create mode: the preview now reads "Saved as X sats · about $Y today. The USD estimate may change with BTC price." Edit mode: a goalTouched flag tracks whether the user actually changed the goal field. If untouched, the submit handler preserves the original goalSats exactly instead of round-tripping through USD at the current price. A helper note shows the current saved sats so the creator knows the field is pre-filled from a reverse conversion. --- src/pages/CreateCampaignPage.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/pages/CreateCampaignPage.tsx b/src/pages/CreateCampaignPage.tsx index 549dbdcd..3a47ef7b 100644 --- a/src/pages/CreateCampaignPage.tsx +++ b/src/pages/CreateCampaignPage.tsx @@ -205,6 +205,7 @@ export function CreateCampaignPage() { const [coverUploading, setCoverUploading] = useState(false); const [tagInput, setTagInput] = useState(''); const [goalUsd, setGoalUsd] = useState(''); + const [goalTouched, setGoalTouched] = useState(false); const [deadline, setDeadline] = useState(''); const [countryQuery, setCountryQuery] = useState(''); const [countryCode, setCountryCode] = useState(''); @@ -377,8 +378,11 @@ export function CreateCampaignPage() { } // Goal / deadline. + // In edit mode, preserve the exact stored sats unless the user changed the field. let goalNum: number | undefined; - if (goalUsd.trim()) { + if (isEditMode && !goalTouched) { + goalNum = editCampaign?.goalSats; + } else if (goalUsd.trim()) { const n = Number(goalUsd.replace(/[, $]/g, '')); if (!Number.isFinite(n) || n <= 0) { throw new Error('Goal must be a positive USD amount.'); @@ -736,16 +740,30 @@ export function CreateCampaignPage() { inputMode="decimal" placeholder="100,000" value={goalUsd} - onChange={(e) => setGoalUsd(e.target.value)} + onChange={(e) => { + setGoalUsd(e.target.value); + setGoalTouched(true); + }} className="pl-7 pr-14" /> USD
- {goalSatsPreview > 0 && btcPrice && ( + {isEditMode && editCampaign?.goalSats && !goalTouched && (

- {formatSats(goalSatsPreview)} sats ({satsToUSDWhole(goalSatsPreview, btcPrice)}). + Current saved goal: {formatSats(editCampaign.goalSats)} sats + {btcPrice + ? <> — about {satsToUSDWhole(editCampaign.goalSats, btcPrice)} today + : null + }. Only edit this field if you want to change the goal. +

+ )} + {(!isEditMode || goalTouched) && goalSatsPreview > 0 && btcPrice && ( +

+ Your goal will be saved as {formatSats(goalSatsPreview)} sats — about{' '} + {satsToUSDWhole(goalSatsPreview, btcPrice)} today. + The dollar estimate may change with Bitcoin's price.

)} From e7f7d9419dae18bb4b973774fb2bc01ff077dba3 Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 14:41:12 -0500 Subject: [PATCH 10/16] Add Donor and Activist guide pages with privacy-focused content Help page now opens with an amber disclaimer that Agora is recommended only for above-ground activism, followed by two large buttons routing to new /help/donors and /help/activists guide pages. The guides cover how on-chain donations work on Agora, why they're publicly visible on the Bitcoin blockchain and Nostr, and the main paths for protecting donor privacy or cashing out privately (non-KYC purchase, coinjoin, Lightning swaps via Boltz, peer-to-peer exchanges like Bisq and RoboSats). Each tradeoff section is rendered as Pros/Cons bullets. Also adds an 'About Agora' FAQ category to the existing accordion covering the design rationale for not using Lightning, silent payments, or server-rotated addresses. The inline-markup renderer used by FAQ answers is extracted to src/lib/helpMarkup.tsx so it can be reused by the guide pages. --- src/AppRouter.tsx | 4 + src/components/GuideSectionCard.tsx | 52 ++++++ src/components/HelpFAQSection.tsx | 49 +----- src/components/HelpTip.tsx | 37 +---- src/lib/helpContent.ts | 245 ++++++++++++++++++++++++++++ src/lib/helpMarkup.tsx | 60 +++++++ src/pages/ActivistGuidePage.tsx | 71 ++++++++ src/pages/DonorGuidePage.tsx | 69 ++++++++ src/pages/HelpPage.tsx | 67 +++++++- 9 files changed, 566 insertions(+), 88 deletions(-) create mode 100644 src/components/GuideSectionCard.tsx create mode 100644 src/lib/helpMarkup.tsx create mode 100644 src/pages/ActivistGuidePage.tsx create mode 100644 src/pages/DonorGuidePage.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 51ff8ea8..9ac81bce 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -56,6 +56,8 @@ const ExternalContentPage = lazy(() => import("./pages/ExternalContentPage").the const GeotagPage = lazy(() => import("./pages/GeotagPage").then(m => ({ default: m.GeotagPage }))); const HashtagPage = lazy(() => import("./pages/HashtagPage").then(m => ({ default: m.HashtagPage }))); const HelpPage = lazy(() => import("./pages/HelpPage").then(m => ({ default: m.HelpPage }))); +const DonorGuidePage = lazy(() => import("./pages/DonorGuidePage").then(m => ({ default: m.DonorGuidePage }))); +const ActivistGuidePage = lazy(() => import("./pages/ActivistGuidePage").then(m => ({ default: m.ActivistGuidePage }))); const KindFeedPage = lazy(() => import("./pages/KindFeedPage").then(m => ({ default: m.KindFeedPage }))); const LetterComposePage = lazy(() => import("./pages/LetterComposePage").then(m => ({ default: m.LetterComposePage }))); const LetterPreferencesPage = lazy(() => import("./pages/LetterPreferencesPage").then(m => ({ default: m.LetterPreferencesPage }))); @@ -299,6 +301,8 @@ export function AppRouter() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/GuideSectionCard.tsx b/src/components/GuideSectionCard.tsx new file mode 100644 index 00000000..c7ea3960 --- /dev/null +++ b/src/components/GuideSectionCard.tsx @@ -0,0 +1,52 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { type GuideSection } from '@/lib/helpContent'; +import { renderInlineMarkup } from '@/lib/helpMarkup'; + +/** + * Renders a single {@link GuideSection} as a Card. Used by the Donor Guide + * and Activist Guide pages. + * + * Paragraphs accept the same inline markup as FAQ answers (**bold** and + * [link](url)). Optional `pros` / `cons` arrays render as colored bullet + * lists beneath the paragraphs. + */ +export function GuideSectionCard({ section }: { section: GuideSection }) { + return ( + + + {section.heading} + + + {section.paragraphs.map((p, i) => ( +

{renderInlineMarkup(p)}

+ ))} + + {section.pros && section.pros.length > 0 && ( +
+

+ Pros +

+
    + {section.pros.map((p, i) => ( +
  • {renderInlineMarkup(p)}
  • + ))} +
+
+ )} + + {section.cons && section.cons.length > 0 && ( +
+

+ Cons +

+
    + {section.cons.map((c, i) => ( +
  • {renderInlineMarkup(c)}
  • + ))} +
+
+ )} +
+
+ ); +} diff --git a/src/components/HelpFAQSection.tsx b/src/components/HelpFAQSection.tsx index c67b9677..ac6c6416 100644 --- a/src/components/HelpFAQSection.tsx +++ b/src/components/HelpFAQSection.tsx @@ -8,54 +8,7 @@ import { } from '@/components/ui/accordion'; import { useAppContext } from '@/hooks/useAppContext'; import { getFAQCategories, type FAQCategory, type FAQItem } from '@/lib/helpContent'; - -// ── Inline markup renderer ──────────────────────────────────────────────────── - -/** - * Very lightweight inline markup: **bold** and [text](url). - * Returns an array of React nodes. - */ -function renderInlineMarkup(text: string): React.ReactNode[] { - const nodes: React.ReactNode[] = []; - // Match **bold** or [text](url) - const regex = /\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\)/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - - while ((match = regex.exec(text)) !== null) { - // Push text before this match - if (match.index > lastIndex) { - nodes.push(text.slice(lastIndex, match.index)); - } - - if (match[1] !== undefined) { - // **bold** - nodes.push({match[1]}); - } else if (match[2] !== undefined && match[3] !== undefined) { - // [text](url) - nodes.push( - - {match[2]} - , - ); - } - - lastIndex = match.index + match[0].length; - } - - // Trailing text - if (lastIndex < text.length) { - nodes.push(text.slice(lastIndex)); - } - - return nodes; -} +import { renderInlineMarkup } from '@/lib/helpMarkup'; // ── Component ───────────────────────────────────────────────────────────────── diff --git a/src/components/HelpTip.tsx b/src/components/HelpTip.tsx index b9c3ceb4..8e42ae64 100644 --- a/src/components/HelpTip.tsx +++ b/src/components/HelpTip.tsx @@ -4,42 +4,7 @@ import { Link } from 'react-router-dom'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { useAppContext } from '@/hooks/useAppContext'; import { getFAQItem } from '@/lib/helpContent'; - -/** - * Renders **bold** and [text](url) markup in FAQ answer strings. - */ -function renderInlineMarkup(text: string): React.ReactNode[] { - const nodes: React.ReactNode[] = []; - const regex = /\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\)/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - nodes.push(text.slice(lastIndex, match.index)); - } - if (match[1] !== undefined) { - nodes.push({match[1]}); - } else if (match[2] !== undefined && match[3] !== undefined) { - nodes.push( - - {match[2]} - , - ); - } - lastIndex = match.index + match[0].length; - } - if (lastIndex < text.length) { - nodes.push(text.slice(lastIndex)); - } - return nodes; -} +import { renderInlineMarkup } from '@/lib/helpMarkup'; // ── Component ───────────────────────────────────────────────────────────────── diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index a132ad35..71b7fd4d 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -345,6 +345,61 @@ const FAQ_TEMPLATE: FAQCategory[] = [ }, ], }, + + // ── About Agora (design rationale) ────────────────────────────────────── + { + id: 'agora-design', + label: 'About Agora', + items: [ + { + id: 'what-is-agora', + question: 'What is {appName} for?', + answer: [ + '{appName} is a Nostr platform for sending on-chain Bitcoin donations directly to activists. No middleman, no payment processor, no account to freeze.', + ], + }, + { + id: 'donations-are-public-general', + question: 'Are donations on {appName} public?', + answer: [ + 'Yes. Every donation \u2014 given or received \u2014 is recorded on the public Bitcoin blockchain and on Nostr. Anyone can see the amounts, the timing, and the addresses involved.', + 'Read the **Donor Guide** and **Activist Guide** for what this means in practice and how to protect your privacy if you need to.', + ], + }, + { + id: 'why-not-lightning', + question: 'Why doesn\'t {appName} use Lightning?', + answer: [ + 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down, pressured, or pulled offline by a bad actor. Non-custodial Lightning is technically demanding and unreliable for newcomers.', + 'We want {appName} to work for someone whose only Bitcoin experience is Cash App. On-chain Bitcoin works with every wallet on the planet.', + ], + }, + { + id: 'why-not-silent-payments', + question: 'Why doesn\'t {appName} use silent payments?', + answer: [ + 'Silent payments only work when the **sender\'s** wallet supports them. Most popular wallets \u2014 Cash App, Strike, and nearly every custodial wallet \u2014 do not.', + 'Asking donors to install new software is a barrier we won\'t put in front of activists who need support.', + ], + }, + { + id: 'why-not-rotating-addresses', + question: 'Why doesn\'t {appName} generate a new address for every donation?', + answer: [ + 'Generating a fresh address per donation would require {appName} to run a server that signs and serves addresses. That server becomes a single point of failure \u2014 someone could shut it down to silence activists.', + '{appName} derives each user\'s donation address from their Nostr public key. No server is required, and the platform itself can\'t be turned off to censor anyone.', + ], + }, + { + id: 'why-onchain', + question: 'Why on-chain Bitcoin?', + answer: [ + 'On-chain Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.', + 'The tradeoff is that on-chain transactions are public and pay a miner fee. The Donor and Activist guides explain how to handle both.', + ], + }, + ], + }, ]; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -404,3 +459,193 @@ export function getFAQItem(appName: string, itemId: string): FAQItem | undefined * canonical constant directly. */ export { TEAM_SOAPBOX as TEAM_SOAPBOX_PACK } from '@/lib/agoraDefaults'; + +// ── Donor / Activist guide content ──────────────────────────────────────────── + +/** + * A single section inside a long-form guide page (Donor Guide / Activist + * Guide). Each section renders as a Card on the guide page. + * + * `paragraphs` accept the same inline markup as FAQ answers (**bold** and + * [link](url)), rendered by `renderInlineMarkup` from `@/lib/helpMarkup`. + * + * `pros` / `cons` are optional and render as a bullet pair underneath the + * paragraphs. They are used for tradeoff-heavy topics like cash-out methods. + */ +export interface GuideSection { + /** Stable key, used for React keys and potential deep-linking. */ + id: string; + /** Section heading. */ + heading: string; + /** Body paragraphs, in order. */ + paragraphs: string[]; + /** Optional positives, rendered as a green-flavored bullet list. */ + pros?: string[]; + /** Optional negatives / caveats, rendered as an amber-flavored bullet list. */ + cons?: string[]; +} + +const DONOR_GUIDE_TEMPLATE: GuideSection[] = [ + { + id: 'how-donating-works', + heading: 'How donating works', + paragraphs: [ + 'You send real Bitcoin on-chain directly to the activist. {appName} doesn\'t hold or route the money \u2014 the address you\'re paying is derived from the activist\'s Nostr key, so there\'s no middleman in between.', + 'You pay a small network fee to Bitcoin miners. Once the transaction is broadcast, it\'s public and irreversible.', + ], + }, + { + id: 'why-public', + heading: 'Why your donation is public', + paragraphs: [ + 'Bitcoin is a public ledger. Anyone can look up an activist\'s address and see every donation \u2014 the amount, the time, and the address it came from.', + 'Your sending address can usually be traced back to wherever you bought the Bitcoin (Cash App, Coinbase, Strike, etc.). That link is what ties a donation to your real identity.', + ], + }, + { + id: 'privacy-non-kyc', + heading: 'For privacy: use non-KYC Bitcoin', + paragraphs: [ + 'Buy Bitcoin peer-to-peer so it isn\'t linked to your government ID. Marketplaces like [Bisq](https://bisq.network), [RoboSats](https://learn.robosats.com), and [HodlHodl](https://hodlhodl.com) let you trade directly with another person.', + ], + pros: ['No exchange knows who you are.', 'Strongest privacy starting point.'], + cons: ['Slower and harder than Cash App.', 'Requires finding a counterparty.'], + }, + { + id: 'privacy-coinjoin', + heading: 'For privacy: coinjoin before donating', + paragraphs: [ + 'A coinjoin mixes your Bitcoin with other people\'s coins so the output can\'t be linked back to the input. Wallets like [Wasabi](https://wasabiwallet.io), [Sparrow](https://sparrowwallet.com), and [JoinMarket](https://github.com/JoinMarket-Org/joinmarket-clientserver) support this.', + ], + pros: ['Breaks the on-chain trail from your KYC purchase.', 'Non-custodial \u2014 you keep your keys.'], + cons: ['Costs fees and takes time.', 'Fewer maintained tools after the Samourai shutdown.'], + }, + { + id: 'fresh-wallet', + heading: 'Use a fresh wallet', + paragraphs: [ + 'Donate from a wallet that has never touched a KYC exchange or your main identity. Even one shared transaction input can link the wallet back to you.', + 'Free options include [Sparrow](https://sparrowwallet.com) on desktop and [BlueWallet](https://bluewallet.io) on mobile.', + ], + }, + { + id: 'vary-amounts', + heading: 'Vary amounts and timing', + paragraphs: [ + 'Round numbers ($50, $100) and recurring donations create a pattern that\'s easy to fingerprint. Send unusual amounts at irregular times if you want to be harder to track.', + ], + }, + { + id: 'what-cash-app-cant-do', + heading: 'What Cash App and similar apps can\'t do', + paragraphs: [ + 'Cash App, Strike, and most custodial wallets are convenient but tied to your real identity. They can\'t make a donation truly anonymous, no matter how you send it.', + 'If anonymity matters to you, use a non-custodial wallet you control.', + ], + }, +]; + +const ACTIVIST_GUIDE_TEMPLATE: GuideSection[] = [ + { + id: 'how-receiving-works', + heading: 'How receiving works', + paragraphs: [ + 'Your {appName} donation address is derived from your Nostr public key. Donors send on-chain Bitcoin directly to it. No one stands between you and the funds, and no server can be shut down to stop the donations.', + ], + }, + { + id: 'why-public', + heading: 'Why incoming donations are public', + paragraphs: [ + 'Bitcoin is a public ledger. Anyone can look up your address and see every donation \u2014 the amount, the time, and the sending address. Your supporters\' addresses are visible too.', + ], + }, + { + id: 'dont-keep-funds', + heading: 'Don\'t keep funds at your {appName} address', + paragraphs: [ + 'Move funds to a wallet you control as soon as practical. Treat your {appName} address like a mailbox, not a savings account.', + 'Good self-custody wallets to move funds into: [Sparrow](https://sparrowwallet.com), [BlueWallet](https://bluewallet.io), or [Phoenix](https://phoenix.acinq.co) (Lightning).', + ], + }, + { + id: 'cashout-overview', + heading: 'Cashing out privately \u2014 overview', + paragraphs: [ + 'To spend donations without revealing who you are, you have to break the on-chain trail before converting to cash. The next sections cover the main paths. Each has tradeoffs in custody, privacy, difficulty, and fees.', + ], + }, + { + id: 'cashout-lightning-swap', + heading: 'Lightning swap (Boltz, Bolt.exchange)', + paragraphs: [ + 'Services like [Boltz](https://boltz.exchange) atomic-swap your on-chain Bitcoin into Lightning. Lightning payments are private by default \u2014 they don\'t appear on the public blockchain.', + ], + pros: ['Instant and non-custodial.', 'Lightning payments aren\'t publicly traceable.'], + cons: ['Per-swap limits and swap fees.', 'Depends on the swap service being online.'], + }, + { + id: 'cashout-coinjoin', + heading: 'Coinjoin', + paragraphs: [ + 'A coinjoin mixes your Bitcoin with other users\' coins so the output can\'t be linked to the input. [Wasabi](https://wasabiwallet.io) and [JoinMarket](https://github.com/JoinMarket-Org/joinmarket-clientserver) are the main maintained options after the Samourai shutdown.', + ], + pros: ['Strong on-chain unlinkability.', 'Non-custodial.'], + cons: ['Fees and wait time.', 'Steeper learning curve than a swap.'], + }, + { + id: 'cashout-p2p', + heading: 'Peer-to-peer exchange', + paragraphs: [ + 'Trade Bitcoin for fiat directly with another person on [Bisq](https://bisq.network), [RoboSats](https://learn.robosats.com), or [HodlHodl](https://hodlhodl.com). No exchange records your identity.', + ], + pros: ['Cash in hand without KYC.', 'No central exchange knows you.'], + cons: ['Slower than an exchange.', 'Requires a willing counterparty.', 'Some learning curve.'], + }, + { + id: 'cashout-tumblers', + heading: 'Tumblers and centralized mixers', + paragraphs: [ + '**Generally not recommended.** Centralized tumblers are custodial \u2014 you have to trust the operator not to steal your coins or log who sent what. Many are scams or law-enforcement honeypots.', + 'Coinjoin is the non-custodial alternative and is almost always the better choice.', + ], + }, + { + id: 'cashout-comparison', + heading: 'Quick comparison', + paragraphs: [ + '**Lightning swap (Boltz):** non-custodial \u00b7 medium privacy \u00b7 easy \u00b7 low fees.', + '**Coinjoin (Wasabi, JoinMarket):** non-custodial \u00b7 high privacy \u00b7 medium difficulty \u00b7 medium fees.', + '**Peer-to-peer (Bisq, RoboSats):** non-custodial \u00b7 high privacy \u00b7 harder \u00b7 variable fees.', + '**Tumblers:** custodial \u00b7 unpredictable privacy \u00b7 easy \u00b7 high risk. **Avoid.**', + ], + }, + { + id: 'donors-can-be-seen', + heading: 'Your donation history is visible to future supporters', + paragraphs: [ + 'Anyone considering supporting you can look up your address and see the full donation history. Keep in mind how that history reads to a new donor.', + ], + }, +]; + +/** Substitute placeholders in a single guide section. */ +function substituteGuideSection(section: GuideSection, appName: string): GuideSection { + return { + ...section, + heading: substitute(section.heading, appName), + paragraphs: section.paragraphs.map((p) => substitute(p, appName)), + pros: section.pros?.map((p) => substitute(p, appName)), + cons: section.cons?.map((c) => substitute(c, appName)), + }; +} + +/** Donor guide sections with `{appName}` resolved. */ +export function getDonorGuideSections(appName: string): GuideSection[] { + return DONOR_GUIDE_TEMPLATE.map((s) => substituteGuideSection(s, appName)); +} + +/** Activist guide sections with `{appName}` resolved. */ +export function getActivistGuideSections(appName: string): GuideSection[] { + return ACTIVIST_GUIDE_TEMPLATE.map((s) => substituteGuideSection(s, appName)); +} diff --git a/src/lib/helpMarkup.tsx b/src/lib/helpMarkup.tsx new file mode 100644 index 00000000..00279836 --- /dev/null +++ b/src/lib/helpMarkup.tsx @@ -0,0 +1,60 @@ +/** + * Shared inline-markup renderer used by Help/FAQ surfaces and the + * Donor / Activist guide pages. + * + * Supports a deliberately tiny syntax so authors can write content in plain + * strings without pulling in a full markdown parser: + * + * **bold** → bold + * [link text](url) → link text + * + * The renderer returns an array of React nodes suitable for splatting into a + * paragraph or span. It is intentionally non-recursive: bold inside a link or + * vice versa is not supported (and not needed by current content). + */ + +export function renderInlineMarkup(text: string): React.ReactNode[] { + const nodes: React.ReactNode[] = []; + // Match **bold** or [text](url) + const regex = /\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\)/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) !== null) { + // Push text before this match + if (match.index > lastIndex) { + nodes.push(text.slice(lastIndex, match.index)); + } + + if (match[1] !== undefined) { + // **bold** + nodes.push( + + {match[1]} + , + ); + } else if (match[2] !== undefined && match[3] !== undefined) { + // [text](url) + nodes.push( + + {match[2]} + , + ); + } + + lastIndex = match.index + match[0].length; + } + + // Trailing text + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + + return nodes; +} diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx new file mode 100644 index 00000000..7f67c38b --- /dev/null +++ b/src/pages/ActivistGuidePage.tsx @@ -0,0 +1,71 @@ +import { useSeoMeta } from '@unhead/react'; +import { AlertTriangle, Megaphone } from 'lucide-react'; + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { GuideSectionCard } from '@/components/GuideSectionCard'; +import { PageHeader } from '@/components/PageHeader'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useLayoutOptions } from '@/contexts/LayoutContext'; +import { getActivistGuideSections } from '@/lib/helpContent'; + +/** + * Activist Guide — long-form companion to the Help page. + * + * Explains how receiving donations works on Agora, why incoming donations + * are public, and the main paths for cashing out privately. Linked from + * `/help` as one of the two large guide buttons. + */ +export function ActivistGuidePage() { + const { config } = useAppContext(); + useLayoutOptions({}); + + useSeoMeta({ + title: `Activist Guide | ${config.appName}`, + description: `How to receive donations on ${config.appName} and cash out privately.`, + }); + + const sections = getActivistGuideSections(config.appName); + + return ( +
+ } + backTo="/help" + /> + +
+ {/* Above-ground recommendation alert */} + + + + Recommended for above-ground activism + + +

+ {config.appName} is recommended only for above-ground activism. Every donation you + receive is recorded publicly on the Bitcoin blockchain and on Nostr. If you or your + donors require extreme privacy — including protection from state actors + — additional steps are needed to protect yourself and the people supporting you. + Read the sections below before accepting donations. +

+
+
+ + {/* Short intro */} +

+ Receiving support on {config.appName} means donors send Bitcoin directly to an address + derived from your Nostr key. Here's how it works, and how to move funds privately if + you need to. +

+ + {/* Sections */} + {sections.map((section) => ( + + ))} +
+
+ ); +} + +export default ActivistGuidePage; diff --git a/src/pages/DonorGuidePage.tsx b/src/pages/DonorGuidePage.tsx new file mode 100644 index 00000000..1edb9e20 --- /dev/null +++ b/src/pages/DonorGuidePage.tsx @@ -0,0 +1,69 @@ +import { useSeoMeta } from '@unhead/react'; +import { AlertTriangle, HandHeart } from 'lucide-react'; + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { GuideSectionCard } from '@/components/GuideSectionCard'; +import { PageHeader } from '@/components/PageHeader'; +import { useAppContext } from '@/hooks/useAppContext'; +import { useLayoutOptions } from '@/contexts/LayoutContext'; +import { getDonorGuideSections } from '@/lib/helpContent'; + +/** + * Donor Guide — long-form companion to the Help page. + * + * Explains how on-chain donations on Agora work, why they are publicly + * visible, and what a donor can do if they need privacy. Linked from + * `/help` as one of the two large guide buttons. + */ +export function DonorGuidePage() { + const { config } = useAppContext(); + useLayoutOptions({}); + + useSeoMeta({ + title: `Donor Guide | ${config.appName}`, + description: `How donating works on ${config.appName} and how to protect your privacy.`, + }); + + const sections = getDonorGuideSections(config.appName); + + return ( +
+ } + backTo="/help" + /> + +
+ {/* Above-ground recommendation alert */} + + + + Recommended for above-ground activism + + +

+ {config.appName} is recommended only for supporting above-ground activism. Your + donation is public on the Bitcoin blockchain and on Nostr. If you need extreme + privacy — including protection from state actors — additional steps are + required before donating. Read the sections below first. +

+
+
+ + {/* Short intro */} +

+ Supporting an activist on {config.appName} means sending real Bitcoin on-chain. Here's + how it works, and how to do it privately if you need to. +

+ + {/* Sections */} + {sections.map((section) => ( + + ))} +
+
+ ); +} + +export default DonorGuidePage; diff --git a/src/pages/HelpPage.tsx b/src/pages/HelpPage.tsx index fc5241aa..28dd2dfe 100644 --- a/src/pages/HelpPage.tsx +++ b/src/pages/HelpPage.tsx @@ -1,7 +1,8 @@ import { useSeoMeta } from '@unhead/react'; -import { HelpCircle, Shield } from 'lucide-react'; +import { AlertTriangle, ChevronRight, HandHeart, HelpCircle, Megaphone, Shield } from 'lucide-react'; import { Link } from 'react-router-dom'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { useAppContext } from '@/hooks/useAppContext'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { PageHeader } from '@/components/PageHeader'; @@ -14,21 +15,54 @@ export function HelpPage() { useSeoMeta({ title: `Help | ${config.appName}`, - description: `Get help with ${config.appName} — Nostr 101, FAQs, and support`, + description: `Get help with ${config.appName} — donor and activist guides, FAQs, and support`, }); return (
} /> + {/* Top-of-page disclaimer: first thing visitors see */} +
+ + + Read this first + +

+ {config.appName} is recommended only for above-ground activism. Every donation + — given or received — is public on the Bitcoin blockchain and on Nostr. If + you or your donors require extreme privacy, including from state actors, additional + steps are required to protect yourself. Read the Donor Guide and{' '} + Activist Guide below before participating. +

+
+
+
+ + {/* Two large guide buttons */} +
+ } + title="Donor Guide" + description="How to support activists privately and safely." + /> + } + title="Activist Guide" + description="Receiving donations and cashing out privately." + /> +
+ {/* Team Soapbox follow pack */} - + {/* FAQ heading */}

Frequently Asked Questions

- Everything you need to know about Nostr, {config.appName}, and how it all works. + Everything else you need to know about Nostr, {config.appName}, and how it all works.

@@ -48,3 +82,28 @@ export function HelpPage() {
); } + +interface GuideButtonProps { + to: string; + icon: React.ReactNode; + title: string; + description: string; +} + +function GuideButton({ to, icon, title, description }: GuideButtonProps) { + return ( + +
+ {icon} +
+
+

{title}

+

{description}

+
+ + + ); +} From 99e4fd0406b75e20fbc8cecd34e47556318744b8 Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 14:54:06 -0500 Subject: [PATCH 11/16] Trim Help FAQs to core Agora features and rename categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FAQ accordion was carrying a lot of generic Nostr-client content (profile field formats, app-store availability, Mastodon/Bluesky comparisons, marketing copy) that wasn't relevant to Agora's core: on- chain Bitcoin donations to activists. Restructure into four focused categories: - 'About Agora' (formerly 'Getting Started') — what Agora is, what Nostr is, key management, cost. - 'Bitcoin Donations' — how sending works, the wallet, zaps, censorship resistance. - 'Network & Safety' — relays, Blossom, reporting, profile fields. - 'About Bitcoin Payments on Agora' (formerly 'About Agora') — the design-rationale Q&A added in the previous commit (why not Lightning / silent payments / rotating addresses). All FAQ item IDs referenced by HelpTip on other pages are preserved (connect-wallet, fyp, profile-fields, what-are-zaps, vs-mastodon- bluesky, send-bitcoin-onchain, send-bitcoin-lightning, what-is-nostr, what-are-relays, what-are-blossom, report-content, censorship- resistance) — their content has been rewritten to be relevant to Agora's donation flow without breaking the callers. Also moves the 'Need help? Meet Team Soapbox' follow-pack card from the top of the page to the bottom, after the FAQ. --- src/lib/helpContent.ts | 253 +++++++++-------------------------------- src/pages/HelpPage.tsx | 10 +- 2 files changed, 60 insertions(+), 203 deletions(-) diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index 71b7fd4d..ddad2d55 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -40,316 +40,173 @@ export interface FAQCategory { * and friends. */ const FAQ_TEMPLATE: FAQCategory[] = [ - // ── Getting Started ───────────────────────────────────────────────────── + // ── About Agora ───────────────────────────────────────────────────────── { id: 'getting-started', - label: 'Getting Started', + label: 'About Agora', items: [ { id: 'what-is-ditto', question: 'What is {appName}?', answer: [ - '{appName} is a social media platform built on Nostr \u2014 a new kind of open, decentralized network. Think of {appName} as the app you\'re using right now to connect with people, post, and discover content.', - 'Because {appName} is built on Nostr, your account isn\'t locked to this site. You own your identity and can take it to any other Nostr app.', + '{appName} is a platform for sending on-chain Bitcoin donations directly to activists. There\'s no middleman, no payment processor, and no account that can be frozen.', + '{appName} is built on Nostr, so your identity isn\'t locked to this site \u2014 you own it.', ], }, { id: 'what-is-nostr', question: 'What is Nostr?', answer: [ - 'Nostr is a new kind of social network where **you** own your account, not a company. Think of it like email \u2014 you can use different apps, but your identity stays the same. Nobody can ban you from the entire network.', - 'Everything you post, every person you follow, and your entire identity is portable. You can take it with you anywhere. To learn more, check out [Nostr 101](https://soapbox.pub/blog/nostr101).', - ], - }, - { - id: 'login-other-apps', - question: 'Can I log into other Nostr apps with my {appName} account?', - answer: [ - 'Yes! Your {appName} account **is** a Nostr account. You can use the same keys to log into any Nostr app \u2014 Primal, Damus, Amethyst, Coracle, and many more. Your posts, followers, and profile carry over everywhere.', - 'Explore the full range of Nostr apps at [nostrapps.com](https://nostrapps.com/).', + 'Nostr is an open network where **you** own your account, not a company. Your identity is a cryptographic key you control, not a username on someone else\'s server.', + 'On {appName}, that same key is also what your donation address is derived from \u2014 which is why you can receive Bitcoin without signing up with anyone.', ], }, { id: 'why-login-different', question: 'Why is my sign-in so different and long?', answer: [ - 'Instead of a username and password controlled by a company, Nostr uses a pair of cryptographic keys \u2014 like a really secure digital ID.', - 'Your "public key" (starts with **npub**) is your username that everyone can see. Your "secret key" (starts with **nsec**) is your password. The long string of characters is what makes it virtually impossible to hack.', + 'Instead of a username and password controlled by a company, Nostr uses a pair of cryptographic keys.', + 'Your "public key" (starts with **npub**) is your username. Your "secret key" (starts with **nsec**) is your password. The long string is what makes it virtually impossible to guess.', ], }, { id: 'lose-secret-key', question: 'What happens if I lose my secret key?', answer: [ - '**There is no "forgot password" button.** No company stores your key or can reset it for you. If you lose it, your account is gone forever.', - 'This is the tradeoff for true ownership \u2014 nobody can take your account away, but nobody can recover it either. **Save your secret key somewhere safe right now.** For tips on keeping your key safe, read [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', + '**There is no "forgot password" button.** Nobody can reset it for you. If you lose it, your account \u2014 and any Bitcoin sitting at your donation address \u2014 is gone forever.', + '**Save your secret key somewhere safe right now.** For tips, read [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', ], }, { id: 'manage-secret-key', question: 'Can I save my secret key in my phone\'s password manager?', answer: [ - 'Yes! You can save it in your device\'s password manager (like iCloud Keychain, 1Password, or Bitwarden). On iPhone, if you save it correctly in Passwords, you can even use Face ID or Touch ID to log in.', - 'For a full guide on the best ways to store and manage your keys, check out [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', + 'Yes. You can save it in your device\'s password manager (iCloud Keychain, 1Password, Bitwarden, etc.). On iPhone, saving it in Passwords lets you use Face ID or Touch ID to log in.', + 'For a full guide, see [Managing Your Nostr Keys](https://soapbox.pub/blog/managing-nostr-keys).', ], }, { id: 'cost-to-use', question: 'Does {appName} cost anything?', answer: [ - '**Nope!** {appName} is completely free to use. Zaps (tips) are optional and just for fun. There are no premium tiers, no paywalls, no hidden fees.', + '**No.** {appName} takes no platform fee. When you donate, you pay only the Bitcoin network fee that goes to miners \u2014 not to us.', ], }, { - id: 'beginner-guide', - question: 'Is there a step-by-step guide for getting started?', + id: 'who-made-this', + question: 'Who made {appName}?', answer: [ - 'You\'re looking at it! This Help section covers everything you need. Start by saving your secret key, then explore your feed, follow some people, and try posting.', - 'Don\'t worry about getting everything perfect \u2014 you can always come back here.', + '{appName} is built by [Soapbox](https://soapbox.pub), an open-source team building tools for the Nostr ecosystem.', ], }, ], }, - // ── Apps & Access ─────────────────────────────────────────────────────── - { - id: 'apps-access', - label: 'Apps & Access', - items: [ - { - id: 'download-app', - question: 'Can I download this on the App Store or Google Play?', - answer: [ - 'This site works as a web app right from your browser \u2014 no download needed! You can also "Add to Home Screen" on your phone to get an app-like experience.', - 'On Android, you can download {appName} from [Zap Store](https://zapstore.dev/apps/spot.agora.app), a community-driven app store for the Nostr ecosystem. iOS support is planned for the future \u2014 stay tuned!', - ], - }, - { - id: 'one-account-many-apps', - question: 'Can I use my account on other apps?', - answer: [ - 'Yes! That\'s one of the best things about Nostr. Your account isn\'t locked to any single app.', - 'You can take your keys to Primal, Damus, Amethyst, Coracle, or any other Nostr app and everything carries over \u2014 your posts, your followers, all of it.', - ], - }, - { - id: 'nostr-app-store', - question: 'Is there a Nostr-specific app store?', - answer: [ - 'Yes! [Zap Store](https://zapstore.dev/) is a community-driven app store built specifically for the Nostr ecosystem. You can discover and download Nostr apps, and the apps are verified by the community rather than a corporation. {appName} is listed there \u2014 [get it on Zap Store](https://zapstore.dev/apps/spot.agora.app).', - 'You can also browse a directory of Nostr apps at [nostrapps.com](https://nostrapps.com/).', - ], - }, - ], - }, - - // ── Payments & Zaps ───────────────────────────────────────────────────── + // ── Bitcoin Donations ─────────────────────────────────────────────────── { id: 'payments', - label: 'Payments & Zaps', + label: 'Bitcoin Donations', items: [ - { - id: 'what-are-zaps', - question: 'What are zaps?', - answer: [ - 'Zaps are tips! They let you send tiny amounts of Bitcoin to someone as a way of saying "great post" or "thanks."', - 'Think of it like a super-powered Like button that actually sends real money. They use the Lightning Network, which makes them instant and nearly free. To learn more, check out [Understanding Zaps](https://nostr.how/en/zaps).', - ], - }, { id: 'send-bitcoin-onchain', question: 'How does sending Bitcoin work?', answer: [ - 'This sends real Bitcoin on-chain, using your Nostr key as your wallet \u2014 no separate account, no top-up.', - 'Your send pays a small network fee to miners so the transaction gets confirmed. Faster confirmation costs a bit more; {appName} picks a sensible default.', - 'Once broadcast, it\'s public and irreversible. The creator\'s post gets tagged so they know the Bitcoin came from you.', + 'You send real Bitcoin on-chain directly to the activist. Your Nostr key is your wallet \u2014 no separate account, no top-up.', + 'You pay a small network fee to miners so the transaction gets confirmed. Once broadcast, it\'s public and irreversible.', ], }, { id: 'send-bitcoin-lightning', question: 'How does sending Bitcoin over Lightning work?', answer: [ - 'Lightning is a faster, cheaper layer built on top of Bitcoin. Payments settle in seconds and fees are usually fractions of a cent.', - 'You\'ll pay from your connected Lightning wallet. The creator receives the Bitcoin right away, and the payment is attached to their post as a zap so everyone can see the support.', - 'To learn more, check out [Understanding Zaps](https://nostr.how/en/zaps).', + 'If a recipient has a Lightning address on their profile, you can send to that instead. Lightning settles in seconds and fees are tiny.', + 'Lightning donations don\'t use {appName}\'s donation address \u2014 they go straight to the Lightning wallet the recipient set up themselves.', ], }, { id: 'connect-wallet', - question: 'How do I connect a wallet?', + question: 'What is the wallet on {appName}?', answer: [ - 'To send or receive zaps, you need a Lightning wallet. Great options for beginners include [Alby](https://getalby.com/), [Zeus](https://zeusln.com/), and [Wallet of Satoshi](https://www.walletofsatoshi.com/).', - 'Once you have one, add your Lightning address to your profile settings, and you\'re ready to go.', + 'Your {appName} wallet is an on-chain Bitcoin address derived from your Nostr key. There\'s nothing to sign up for \u2014 it exists the moment you have an account.', + 'Donations sent to you arrive at that address. To spend them, see the **Activist Guide**.', ], }, { - id: 'only-bitcoin', - question: 'Can I only use Bitcoin, or can I use regular money?', + id: 'what-are-zaps', + question: 'What are zaps?', answer: [ - 'Zaps use Bitcoin\'s Lightning Network. If you don\'t have Bitcoin, you can skip zaps entirely \u2014 they\'re completely optional.', - 'If you\'re curious, most Lightning wallets let you buy small amounts of Bitcoin right inside the app.', + 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow. They only work if the recipient has a Lightning address on their profile.', + 'Zaps are optional. The core donation experience on {appName} is on-chain.', + ], + }, + { + id: 'censorship-resistance', + question: 'What does "censorship-resistant" mean here?', + answer: [ + 'No company sits between a donor and an activist. {appName} doesn\'t hold the funds and can\'t freeze the address.', + 'As long as the Bitcoin network is running, donations can be sent and received. {appName} itself going offline wouldn\'t stop them.', ], }, ], }, - // ── Content & Safety ──────────────────────────────────────────────────── + // ── Network & Safety ──────────────────────────────────────────────────── { id: 'content-safety', - label: 'Content & Safety', + label: 'Network & Safety', items: [ { id: 'fyp', - question: 'Will I have a "For You" page? How do I make my feed relevant?', + question: 'How does the feed work?', answer: [ - 'Your feed shows posts from people you follow \u2014 there\'s no algorithm deciding what you see. The more people you follow, the better your feed gets.', - 'Use the "Trends" page to discover popular content, and check out Follow Packs (curated groups of people) to quickly fill your feed with interesting voices.', + 'Your feed shows campaigns and posts from people you follow. There\'s no algorithm deciding what you see.', + 'Use the Trends page or Follow Packs to discover more activists and campaigns.', ], }, { id: 'what-are-relays', question: 'What are relays?', answer: [ - 'Relays are the servers that store and deliver your posts. Think of them like different mail carriers \u2014 your messages get sent through them to reach other people.', - 'You don\'t need to think about relays to use Nostr; the defaults work great. But if you\'re curious, you can add or remove relays in Settings > Network.', - 'Using multiple relays means your content is backed up in more places, making it harder for anyone to silence you. To dive deeper, read [Understanding Nostr Relays](https://nostr.how/en/relays).', + 'Relays are the servers that store and deliver Nostr events \u2014 posts, donation receipts, profile info. Think of them like different mail carriers.', + 'The defaults work out of the box. Using multiple relays means your content is backed up in more places, making it harder for anyone to silence you. To dive deeper, read [Understanding Nostr Relays](https://nostr.how/en/relays).', ], }, { id: 'what-are-blossom', question: 'What are Blossom servers?', answer: [ - 'Blossom servers are where your media files (photos, videos, audio) get stored when you upload them. Think of them like cloud storage for your files.', - 'Different Blossom servers are run by different people in different places. You can manage which servers you use in Settings > Network. To learn more about how Blossom works, read [The Blossom Protocol](https://onnostr.substack.com/p/the-blossom-protocol-supercharging).', - ], - }, - { - id: 'media-content', - question: 'What happens to media I upload? Can it be removed?', - answer: [ - 'When you upload media to Nostr, it gets stored on a Blossom server. That server has the right to remove any content for any reason, including based on the laws of their region.', - 'This is why it\'s important to use multiple Blossom servers, manage your server connections, and make informed choices about where you store your data.', + 'Blossom servers store media files (campaign images, profile pictures) when you upload them. You can manage which servers you use in Settings > Network.', ], }, { id: 'report-content', question: 'How do I report harmful content?', answer: [ - 'To report a post, tap the three-dot menu (**...**) on any post and select "Report." You can also mute or block individual users from the same menu.', - 'Because Nostr is decentralized, there\'s no single company reviewing reports \u2014 but relay operators can choose to remove content from their servers, and your mute list keeps your feed clean for you.', - ], - }, - { - id: 'terms-of-service', - question: 'Are there terms of service I need to agree to?', - answer: [ - 'Nostr itself is a protocol (like email or the web) \u2014 it doesn\'t have terms of service. Individual relays and apps may have their own rules.', - 'Since no single entity controls the network, the community largely self-moderates. Think of it less like a walled garden and more like the open internet.', - ], - }, - ], - }, - - // ── Profile & Identity ─────────────────────────────────────────────────── - { - id: 'profile-identity', - label: 'Profile & Identity', - items: [ - { - id: 'profile-fields', - question: 'What are profile fields?', - answer: [ - 'Profile fields let you add extra info to your profile sidebar — like links, wallet addresses, music, photos, videos, and more. They\'re a way to express yourself and share what matters to you.', - 'You can add fields from the profile settings page. Each field has a **label** (what it\'s called) and a **value** (the content). For media fields, you can upload files directly and they\'ll render as players or embeds on your profile.', - ], - }, - { - id: 'profile-fields-music', - question: 'What audio formats can I upload for music fields?', - answer: [ - 'You can upload audio files in these formats: **MP3**, **OGG**, **WAV**, **FLAC**, **AAC**, **M4A**, and **Opus**. They\'ll appear as a mini audio player on your profile sidebar.', - ], - }, - { - id: 'profile-fields-media', - question: 'What image and video formats are supported?', - answer: [ - 'For images: **JPG**, **PNG**, **GIF**, **WebP**, **SVG**, and **AVIF**. For video: **MP4**, **WebM**, and **MOV**.', - 'Images will display as linked thumbnails, and videos will be embedded inline on your profile.', - ], - }, - ], - }, - - // ── Why is this different from Big Tech? ──────────────────────────────── - { - id: 'big-tech', - label: 'Why Is This Different from Big Tech?', - items: [ - { - id: 'why-different', - question: 'How is this different from Instagram, X, or Facebook?', - answer: [ - 'On traditional social media, a company owns your account, controls what you see, and can delete your profile at any time.', - 'On Nostr, **you** own your identity. No company can lock you out, shadowban you, or shut down your account. Your followers, your posts, and your identity belong to you \u2014 not a corporation. We take this seriously \u2014 read our [ethics pledge](https://soapbox.pub/ethics) to see what we stand for.', + 'Tap the three-dot menu (**...**) on any post and select "Report." You can mute or block users from the same menu.', ], }, { id: 'vs-mastodon-bluesky', - question: 'How is this different from Mastodon or Bluesky?', + question: 'How is Nostr different from Mastodon or Bluesky?', answer: [ - 'Mastodon and Bluesky are also alternatives to Big Tech, but they work very differently from Nostr. On Mastodon, your account is tied to a specific server \u2014 if that server shuts down or bans you, you lose your account and have to start over. On Bluesky, the network is technically decentralized but in practice almost everyone depends on a single company (bsky.social), which can block entire servers.', - 'Nostr is different because your identity is a cryptographic key that **you** control. It\'s not tied to any server, company, or app. No one can delete your account, and you can switch between apps freely while keeping your followers and posts.', - 'The good news is you don\'t have to choose just one \u2014 bridges like Mostr let you follow people across all three networks. For a deeper comparison, check out [Nostr vs. Fediverse vs. Bluesky](https://soapbox.pub/blog/comparing-protocols).', + 'On Mastodon, your account lives on a specific server \u2014 if it shuts down or bans you, you start over. On Bluesky, most accounts depend on one company.', + 'On Nostr, your identity is a key you control. No server can lock you out, and your donation address goes with you to any Nostr app.', ], }, { - id: 'what-is-decentralization', - question: 'What does "decentralized" actually mean?', + id: 'profile-fields', + question: 'What are profile fields?', answer: [ - 'It means there\'s no single company or server running everything. Nostr is a network of independent relays and apps, all speaking the same language.', - 'If one relay goes down or kicks you off, your account still works everywhere else. It\'s like the difference between one company owning all the roads vs. having thousands of independent roads anyone can build and use. For more on why this matters, read [The Future Is Decentralized](https://soapbox.pub/blog/future-is-decentralized).', - ], - }, - { - id: 'censorship-resistance', - question: 'What does "censorship-resistant" mean?', - answer: [ - 'It means no single person, company, or government can stop you from posting.', - 'On traditional platforms, one decision by a content moderation team can erase your entire online presence. On Nostr, as long as there\'s at least one relay willing to host your content, you can keep posting. You may lose reach on some relays, but you can never be fully silenced.', - ], - }, - { - id: 'open-source', - question: 'What does "open source" mean, and why does it matter?', - answer: [ - 'Open source means the code that powers this app is publicly available for anyone to read, verify, and improve. There are no hidden algorithms, no secret data collection, and no backdoors.', - 'Anyone can check exactly what the software does. It\'s the digital equivalent of a restaurant with a glass kitchen \u2014 nothing to hide. You can browse the [{appName} source code](https://gitlab.com/soapbox-pub/agora-3) yourself.', - ], - }, - { - id: 'self-host', - question: 'Can I self-host {appName}?', - answer: [ - 'Yes! Because {appName} is open source, anyone can run their own instance. You get full control over your server, your data, and your community.', - 'If you\'re interested, check out the project README for self-hosting and deployment steps.', - ], - }, - { - id: 'who-made-this', - question: 'Who made this?', - answer: [ - 'This platform is built by [Soapbox](https://soapbox.pub), a team of developers who believe social media should be owned by its users, not corporations.', - 'Soapbox builds open-source tools for the Nostr ecosystem. You can learn more about the team and their mission at [soapbox.pub](https://soapbox.pub).', + 'Profile fields let you add extra info to your profile \u2014 links, wallet addresses, music, photos, videos. Useful for activists who want to share context about their work.', ], }, ], }, - // ── About Agora (design rationale) ────────────────────────────────────── + // ── About Bitcoin Payments on Agora ───────────────────────────────────── { id: 'agora-design', - label: 'About Agora', + label: 'About Bitcoin Payments on Agora', items: [ { id: 'what-is-agora', @@ -370,7 +227,7 @@ const FAQ_TEMPLATE: FAQCategory[] = [ id: 'why-not-lightning', question: 'Why doesn\'t {appName} use Lightning?', answer: [ - 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down, pressured, or pulled offline by a bad actor. Non-custodial Lightning is technically demanding and unreliable for newcomers.', + 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down or pressured. Non-custodial Lightning is technically demanding and unreliable for newcomers.', 'We want {appName} to work for someone whose only Bitcoin experience is Cash App. On-chain Bitcoin works with every wallet on the planet.', ], }, diff --git a/src/pages/HelpPage.tsx b/src/pages/HelpPage.tsx index 28dd2dfe..3399a38c 100644 --- a/src/pages/HelpPage.tsx +++ b/src/pages/HelpPage.tsx @@ -55,11 +55,8 @@ export function HelpPage() { />
- {/* Team Soapbox follow pack */} - - {/* FAQ heading */} -
+

Frequently Asked Questions

Everything else you need to know about Nostr, {config.appName}, and how it all works. @@ -67,7 +64,10 @@ export function HelpPage() {

{/* FAQ accordion sections */} - + + + {/* Team Soapbox follow pack — at the end, after the FAQ */} + {/* Privacy policy link */}
From 8f065379a0408b161cfad05800c4a85d81aecef8 Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 15:02:59 -0500 Subject: [PATCH 12/16] Merge Bitcoin Donations FAQ section and drop Lightning/zap content The 'Bitcoin Donations' and 'About Bitcoin Payments on Agora' FAQ sections were saying overlapping things in two places. Combine them into a single 'Bitcoin Donations on Agora' section, placed in the higher slot (right after 'About Agora'). Agora's donation flow is on-chain only, so Lightning and zap items are removed from the visible FAQ. The two IDs that other pages reference via HelpTip (send-bitcoin-lightning in ZapDialog, what-are-zaps in ProfileSettings) are kept in a new hidden 'Legacy' category so those call sites don't break. HelpFAQSection skips hidden categories on the default render but the per-ID lookup used by HelpTip still finds them. Also adds a 'Back to Help' button at the bottom of both the Donor Guide and Activist Guide pages so users don't have to scroll back up to the header to navigate out. --- src/components/HelpFAQSection.tsx | 7 ++ src/lib/helpContent.ts | 125 +++++++++++++++++------------- src/pages/ActivistGuidePage.tsx | 14 +++- src/pages/DonorGuidePage.tsx | 14 +++- 4 files changed, 102 insertions(+), 58 deletions(-) diff --git a/src/components/HelpFAQSection.tsx b/src/components/HelpFAQSection.tsx index ac6c6416..2ca3e2c8 100644 --- a/src/components/HelpFAQSection.tsx +++ b/src/components/HelpFAQSection.tsx @@ -46,6 +46,13 @@ export function HelpFAQSection({ categories, items, hideHeadings, className }: H const filteredCategories = useMemo(() => { let cats: FAQCategory[] = getFAQCategories(config.appName); + // Drop hidden categories from the default render. They still exist in + // the underlying template so `HelpTip` can look up individual items by + // ID, but they don't show up in the FAQ accordion. + if (!categories && !items) { + cats = cats.filter((c) => !c.hidden); + } + // Filter to specific categories if (categories) { cats = cats.filter((c) => categories.includes(c.id)); diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index ddad2d55..797cb719 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -30,6 +30,13 @@ export interface FAQCategory { label: string; description?: string; items: FAQItem[]; + /** + * If true, this category is excluded from the default `HelpFAQSection` + * render. Used for legacy items kept around so existing `HelpTip` call + * sites on other pages don't break, without exposing them in the public + * FAQ accordion. + */ + hidden?: boolean; } // ── Data ────────────────────────────────────────────────────────────────────── @@ -102,11 +109,21 @@ const FAQ_TEMPLATE: FAQCategory[] = [ ], }, - // ── Bitcoin Donations ─────────────────────────────────────────────────── + // ── Bitcoin Donations on Agora ────────────────────────────────────────── + // Merged section combining the practical "how it works" Q&A with the + // "why we designed it this way" rationale. On-chain only — Lightning and + // zap content is intentionally absent. { id: 'payments', - label: 'Bitcoin Donations', + label: 'Bitcoin Donations on Agora', items: [ + { + id: 'what-is-agora', + question: 'What is {appName} for?', + answer: [ + '{appName} is a Nostr platform for sending on-chain Bitcoin donations directly to activists. No middleman, no payment processor, no account to freeze.', + ], + }, { id: 'send-bitcoin-onchain', question: 'How does sending Bitcoin work?', @@ -115,14 +132,6 @@ const FAQ_TEMPLATE: FAQCategory[] = [ 'You pay a small network fee to miners so the transaction gets confirmed. Once broadcast, it\'s public and irreversible.', ], }, - { - id: 'send-bitcoin-lightning', - question: 'How does sending Bitcoin over Lightning work?', - answer: [ - 'If a recipient has a Lightning address on their profile, you can send to that instead. Lightning settles in seconds and fees are tiny.', - 'Lightning donations don\'t use {appName}\'s donation address \u2014 they go straight to the Lightning wallet the recipient set up themselves.', - ], - }, { id: 'connect-wallet', question: 'What is the wallet on {appName}?', @@ -132,11 +141,11 @@ const FAQ_TEMPLATE: FAQCategory[] = [ ], }, { - id: 'what-are-zaps', - question: 'What are zaps?', + id: 'donations-are-public-general', + question: 'Are donations on {appName} public?', answer: [ - 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow. They only work if the recipient has a Lightning address on their profile.', - 'Zaps are optional. The core donation experience on {appName} is on-chain.', + 'Yes. Every donation \u2014 given or received \u2014 is recorded on the public Bitcoin blockchain and on Nostr. Anyone can see the amounts, the timing, and the addresses involved.', + 'Read the **Donor Guide** and **Activist Guide** for what this means in practice and how to protect your privacy if you need to.', ], }, { @@ -147,6 +156,38 @@ const FAQ_TEMPLATE: FAQCategory[] = [ 'As long as the Bitcoin network is running, donations can be sent and received. {appName} itself going offline wouldn\'t stop them.', ], }, + { + id: 'why-onchain', + question: 'Why on-chain Bitcoin?', + answer: [ + 'On-chain Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.', + 'The tradeoff is that on-chain transactions are public and pay a miner fee. The Donor and Activist guides explain how to handle both.', + ], + }, + { + id: 'why-not-lightning', + question: 'Why doesn\'t {appName} use Lightning?', + answer: [ + 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down or pressured. Non-custodial Lightning is technically demanding and unreliable for newcomers.', + 'We want {appName} to work for someone whose only Bitcoin experience is Cash App. On-chain Bitcoin works with every wallet on the planet.', + ], + }, + { + id: 'why-not-silent-payments', + question: 'Why doesn\'t {appName} use silent payments?', + answer: [ + 'Silent payments only work when the **sender\'s** wallet supports them. Most popular wallets \u2014 Cash App, Strike, and nearly every custodial wallet \u2014 do not.', + 'Asking donors to install new software is a barrier we won\'t put in front of activists who need support.', + ], + }, + { + id: 'why-not-rotating-addresses', + question: 'Why doesn\'t {appName} generate a new address for every donation?', + answer: [ + 'Generating a fresh address per donation would require {appName} to run a server that signs and serves addresses. That server becomes a single point of failure \u2014 someone could shut it down to silence activists.', + '{appName} derives each user\'s donation address from their Nostr public key. No server is required, and the platform itself can\'t be turned off to censor anyone.', + ], + }, ], }, @@ -203,56 +244,28 @@ const FAQ_TEMPLATE: FAQCategory[] = [ ], }, - // ── About Bitcoin Payments on Agora ───────────────────────────────────── + // ── Hidden legacy items ───────────────────────────────────────────────── + // Kept so existing HelpTip call sites on other pages don't break, but + // excluded from the visible FAQ. {appName} is on-chain only; Lightning + // and zaps are not part of the public help content. { - id: 'agora-design', - label: 'About Bitcoin Payments on Agora', + id: 'legacy-lightning', + label: 'Legacy', + hidden: true, items: [ { - id: 'what-is-agora', - question: 'What is {appName} for?', + id: 'send-bitcoin-lightning', + question: 'How does sending Bitcoin over Lightning work?', answer: [ - '{appName} is a Nostr platform for sending on-chain Bitcoin donations directly to activists. No middleman, no payment processor, no account to freeze.', + 'If a recipient has a Lightning address on their profile, you can send to that. Lightning settles in seconds and fees are tiny.', + 'Lightning sends don\'t use {appName}\'s donation address \u2014 they go straight to whatever Lightning wallet the recipient set up themselves. {appName}\'s own donation flow is on-chain only.', ], }, { - id: 'donations-are-public-general', - question: 'Are donations on {appName} public?', + id: 'what-are-zaps', + question: 'What are zaps?', answer: [ - 'Yes. Every donation \u2014 given or received \u2014 is recorded on the public Bitcoin blockchain and on Nostr. Anyone can see the amounts, the timing, and the addresses involved.', - 'Read the **Donor Guide** and **Activist Guide** for what this means in practice and how to protect your privacy if you need to.', - ], - }, - { - id: 'why-not-lightning', - question: 'Why doesn\'t {appName} use Lightning?', - answer: [ - 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down or pressured. Non-custodial Lightning is technically demanding and unreliable for newcomers.', - 'We want {appName} to work for someone whose only Bitcoin experience is Cash App. On-chain Bitcoin works with every wallet on the planet.', - ], - }, - { - id: 'why-not-silent-payments', - question: 'Why doesn\'t {appName} use silent payments?', - answer: [ - 'Silent payments only work when the **sender\'s** wallet supports them. Most popular wallets \u2014 Cash App, Strike, and nearly every custodial wallet \u2014 do not.', - 'Asking donors to install new software is a barrier we won\'t put in front of activists who need support.', - ], - }, - { - id: 'why-not-rotating-addresses', - question: 'Why doesn\'t {appName} generate a new address for every donation?', - answer: [ - 'Generating a fresh address per donation would require {appName} to run a server that signs and serves addresses. That server becomes a single point of failure \u2014 someone could shut it down to silence activists.', - '{appName} derives each user\'s donation address from their Nostr public key. No server is required, and the platform itself can\'t be turned off to censor anyone.', - ], - }, - { - id: 'why-onchain', - question: 'Why on-chain Bitcoin?', - answer: [ - 'On-chain Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.', - 'The tradeoff is that on-chain transactions are public and pay a miner fee. The Donor and Activist guides explain how to handle both.', + 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow.', ], }, ], diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx index 7f67c38b..9edd03e7 100644 --- a/src/pages/ActivistGuidePage.tsx +++ b/src/pages/ActivistGuidePage.tsx @@ -1,7 +1,9 @@ import { useSeoMeta } from '@unhead/react'; -import { AlertTriangle, Megaphone } from 'lucide-react'; +import { ArrowLeft, AlertTriangle, Megaphone } from 'lucide-react'; +import { Link } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; import { GuideSectionCard } from '@/components/GuideSectionCard'; import { PageHeader } from '@/components/PageHeader'; import { useAppContext } from '@/hooks/useAppContext'; @@ -63,6 +65,16 @@ export function ActivistGuidePage() { {sections.map((section) => ( ))} + + {/* Bottom navigation back to the Help page */} +
+ +
); diff --git a/src/pages/DonorGuidePage.tsx b/src/pages/DonorGuidePage.tsx index 1edb9e20..add50340 100644 --- a/src/pages/DonorGuidePage.tsx +++ b/src/pages/DonorGuidePage.tsx @@ -1,7 +1,9 @@ import { useSeoMeta } from '@unhead/react'; -import { AlertTriangle, HandHeart } from 'lucide-react'; +import { ArrowLeft, AlertTriangle, HandHeart } from 'lucide-react'; +import { Link } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; import { GuideSectionCard } from '@/components/GuideSectionCard'; import { PageHeader } from '@/components/PageHeader'; import { useAppContext } from '@/hooks/useAppContext'; @@ -61,6 +63,16 @@ export function DonorGuidePage() { {sections.map((section) => ( ))} + + {/* Bottom navigation back to the Help page */} +
+ +
); From be262fe0d6dec738b9e45b7b2914a8d452c90fef Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 15:07:58 -0500 Subject: [PATCH 13/16] Cut 'What is Agora for?' and the entire Network & Safety FAQ section The 'What is Agora for?' item duplicated 'What is Agora?' from the About Agora section. Drop it. The Network & Safety section was carrying Nostr-protocol explainers (feed, relays, Blossom, Mastodon/Bluesky comparison, profile fields, reporting) that aren't core to Agora's donation flow. Drop the whole section from the visible FAQ. The six IDs in that section that other pages reference via HelpTip (fyp, what-are-relays, what-are-blossom, report-content, vs-mastodon- bluesky, profile-fields) move into the existing hidden 'Legacy' category alongside the Lightning/zap stubs, so the call sites on NetworkSettingsPage, ContentSettingsPage, ContentPage, SearchPage, RelayListManager, and ProfileSettings continue to resolve. --- src/lib/helpContent.ts | 69 ++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index 797cb719..c4643c93 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -117,13 +117,6 @@ const FAQ_TEMPLATE: FAQCategory[] = [ id: 'payments', label: 'Bitcoin Donations on Agora', items: [ - { - id: 'what-is-agora', - question: 'What is {appName} for?', - answer: [ - '{appName} is a Nostr platform for sending on-chain Bitcoin donations directly to activists. No middleman, no payment processor, no account to freeze.', - ], - }, { id: 'send-bitcoin-onchain', question: 'How does sending Bitcoin work?', @@ -191,25 +184,43 @@ const FAQ_TEMPLATE: FAQCategory[] = [ ], }, - // ── Network & Safety ──────────────────────────────────────────────────── + // ── Hidden legacy items ───────────────────────────────────────────────── + // Kept so existing HelpTip call sites on other pages don't break, but + // excluded from the visible FAQ. {appName} is on-chain only; Lightning, + // zaps, and the network/safety topics aren't part of the public help + // content right now. { - id: 'content-safety', - label: 'Network & Safety', + id: 'legacy', + label: 'Legacy', + hidden: true, items: [ + { + id: 'send-bitcoin-lightning', + question: 'How does sending Bitcoin over Lightning work?', + answer: [ + 'If a recipient has a Lightning address on their profile, you can send to that. Lightning settles in seconds and fees are tiny.', + 'Lightning sends don\'t use {appName}\'s donation address \u2014 they go straight to whatever Lightning wallet the recipient set up themselves. {appName}\'s own donation flow is on-chain only.', + ], + }, + { + id: 'what-are-zaps', + question: 'What are zaps?', + answer: [ + 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow.', + ], + }, { id: 'fyp', question: 'How does the feed work?', answer: [ 'Your feed shows campaigns and posts from people you follow. There\'s no algorithm deciding what you see.', - 'Use the Trends page or Follow Packs to discover more activists and campaigns.', ], }, { id: 'what-are-relays', question: 'What are relays?', answer: [ - 'Relays are the servers that store and deliver Nostr events \u2014 posts, donation receipts, profile info. Think of them like different mail carriers.', - 'The defaults work out of the box. Using multiple relays means your content is backed up in more places, making it harder for anyone to silence you. To dive deeper, read [Understanding Nostr Relays](https://nostr.how/en/relays).', + 'Relays are the servers that store and deliver Nostr events \u2014 posts, donation receipts, profile info. The defaults work out of the box; you can add or remove relays in Settings > Network.', ], }, { @@ -230,42 +241,14 @@ const FAQ_TEMPLATE: FAQCategory[] = [ id: 'vs-mastodon-bluesky', question: 'How is Nostr different from Mastodon or Bluesky?', answer: [ - 'On Mastodon, your account lives on a specific server \u2014 if it shuts down or bans you, you start over. On Bluesky, most accounts depend on one company.', - 'On Nostr, your identity is a key you control. No server can lock you out, and your donation address goes with you to any Nostr app.', + 'On Mastodon, your account lives on a specific server. On Bluesky, most accounts depend on one company. On Nostr, your identity is a key you control, and your donation address goes with you to any Nostr app.', ], }, { id: 'profile-fields', question: 'What are profile fields?', answer: [ - 'Profile fields let you add extra info to your profile \u2014 links, wallet addresses, music, photos, videos. Useful for activists who want to share context about their work.', - ], - }, - ], - }, - - // ── Hidden legacy items ───────────────────────────────────────────────── - // Kept so existing HelpTip call sites on other pages don't break, but - // excluded from the visible FAQ. {appName} is on-chain only; Lightning - // and zaps are not part of the public help content. - { - id: 'legacy-lightning', - label: 'Legacy', - hidden: true, - items: [ - { - id: 'send-bitcoin-lightning', - question: 'How does sending Bitcoin over Lightning work?', - answer: [ - 'If a recipient has a Lightning address on their profile, you can send to that. Lightning settles in seconds and fees are tiny.', - 'Lightning sends don\'t use {appName}\'s donation address \u2014 they go straight to whatever Lightning wallet the recipient set up themselves. {appName}\'s own donation flow is on-chain only.', - ], - }, - { - id: 'what-are-zaps', - question: 'What are zaps?', - answer: [ - 'Zaps are small Lightning tips on Nostr, separate from {appName}\'s on-chain donation flow.', + 'Profile fields let you add extra info to your profile \u2014 links, wallet addresses, music, photos, videos.', ], }, ], From a20a91de0df26c5978f3e154bf3b356957e8dcb4 Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 15:11:09 -0500 Subject: [PATCH 14/16] Broaden consumer-app examples in Donor Guide and add sticky guide nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Donor Guide and a few related FAQ items singled out Cash App as the example of a custodial consumer Bitcoin app. Replace those with a broader list — Cash App, Coinbase, Strike, Venmo, PayPal, Kraken, Binance — so it doesn't read as picking on one product. The Bisq Pros/Cons and the 'What consumer apps can't do' section heading follow the same edit. Also move the 'Back to Help' navigation on the Donor and Activist guide pages from a bottom button into a sticky top bar, PWA-style. It sits right under the TopNav (top-16, z-30) and stays visible while scrolling so users can return to /help from anywhere on the page without scrolling back up. Replaces the previous PageHeader, which hid its back arrow on desktop and made navigating out of the guides awkward. --- src/lib/helpContent.ts | 14 ++++++------- src/pages/ActivistGuidePage.tsx | 35 ++++++++++++++++----------------- src/pages/DonorGuidePage.tsx | 35 ++++++++++++++++----------------- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index c4643c93..36e12123 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -162,14 +162,14 @@ const FAQ_TEMPLATE: FAQCategory[] = [ question: 'Why doesn\'t {appName} use Lightning?', answer: [ 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down or pressured. Non-custodial Lightning is technically demanding and unreliable for newcomers.', - 'We want {appName} to work for someone whose only Bitcoin experience is Cash App. On-chain Bitcoin works with every wallet on the planet.', + 'We want {appName} to work for someone whose only Bitcoin experience is a regular consumer app like Cash App, Coinbase, Strike, Venmo, or PayPal. On-chain Bitcoin works with every wallet on the planet.', ], }, { id: 'why-not-silent-payments', question: 'Why doesn\'t {appName} use silent payments?', answer: [ - 'Silent payments only work when the **sender\'s** wallet supports them. Most popular wallets \u2014 Cash App, Strike, and nearly every custodial wallet \u2014 do not.', + 'Silent payments only work when the **sender\'s** wallet supports them. Most popular consumer apps \u2014 Cash App, Coinbase, Strike, Venmo, PayPal, and nearly every custodial wallet \u2014 do not.', 'Asking donors to install new software is a barrier we won\'t put in front of activists who need support.', ], }, @@ -352,7 +352,7 @@ const DONOR_GUIDE_TEMPLATE: GuideSection[] = [ heading: 'Why your donation is public', paragraphs: [ 'Bitcoin is a public ledger. Anyone can look up an activist\'s address and see every donation \u2014 the amount, the time, and the address it came from.', - 'Your sending address can usually be traced back to wherever you bought the Bitcoin (Cash App, Coinbase, Strike, etc.). That link is what ties a donation to your real identity.', + 'Your sending address can usually be traced back to wherever you bought the Bitcoin \u2014 a consumer app like Cash App, Coinbase, Strike, Venmo, PayPal, Kraken, or Binance. That link is what ties a donation to your real identity.', ], }, { @@ -362,7 +362,7 @@ const DONOR_GUIDE_TEMPLATE: GuideSection[] = [ 'Buy Bitcoin peer-to-peer so it isn\'t linked to your government ID. Marketplaces like [Bisq](https://bisq.network), [RoboSats](https://learn.robosats.com), and [HodlHodl](https://hodlhodl.com) let you trade directly with another person.', ], pros: ['No exchange knows who you are.', 'Strongest privacy starting point.'], - cons: ['Slower and harder than Cash App.', 'Requires finding a counterparty.'], + cons: ['Slower and harder than a consumer app.', 'Requires finding a counterparty.'], }, { id: 'privacy-coinjoin', @@ -389,10 +389,10 @@ const DONOR_GUIDE_TEMPLATE: GuideSection[] = [ ], }, { - id: 'what-cash-app-cant-do', - heading: 'What Cash App and similar apps can\'t do', + id: 'what-consumer-apps-cant-do', + heading: 'What consumer apps can\'t do', paragraphs: [ - 'Cash App, Strike, and most custodial wallets are convenient but tied to your real identity. They can\'t make a donation truly anonymous, no matter how you send it.', + 'Consumer apps like Cash App, Coinbase, Strike, Venmo, and PayPal are convenient, but they require ID verification and tie every transaction to your real identity. They can\'t make a donation truly anonymous, no matter how you send it.', 'If anonymity matters to you, use a non-custodial wallet you control.', ], }, diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx index 9edd03e7..56e71160 100644 --- a/src/pages/ActivistGuidePage.tsx +++ b/src/pages/ActivistGuidePage.tsx @@ -3,9 +3,7 @@ import { ArrowLeft, AlertTriangle, Megaphone } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; import { GuideSectionCard } from '@/components/GuideSectionCard'; -import { PageHeader } from '@/components/PageHeader'; import { useAppContext } from '@/hooks/useAppContext'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { getActivistGuideSections } from '@/lib/helpContent'; @@ -30,13 +28,24 @@ export function ActivistGuidePage() { return (
- } - backTo="/help" - /> + {/* PWA-style sticky back-to-help nav */} +
+
+ + + Back to Help + +
+ + Activist Guide +
+
+
-
+
{/* Above-ground recommendation alert */} @@ -65,16 +74,6 @@ export function ActivistGuidePage() { {sections.map((section) => ( ))} - - {/* Bottom navigation back to the Help page */} -
- -
); diff --git a/src/pages/DonorGuidePage.tsx b/src/pages/DonorGuidePage.tsx index add50340..dd15872f 100644 --- a/src/pages/DonorGuidePage.tsx +++ b/src/pages/DonorGuidePage.tsx @@ -3,9 +3,7 @@ import { ArrowLeft, AlertTriangle, HandHeart } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Button } from '@/components/ui/button'; import { GuideSectionCard } from '@/components/GuideSectionCard'; -import { PageHeader } from '@/components/PageHeader'; import { useAppContext } from '@/hooks/useAppContext'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { getDonorGuideSections } from '@/lib/helpContent'; @@ -30,13 +28,24 @@ export function DonorGuidePage() { return (
- } - backTo="/help" - /> + {/* PWA-style sticky back-to-help nav */} +
+
+ + + Back to Help + +
+ + Donor Guide +
+
+
-
+
{/* Above-ground recommendation alert */} @@ -63,16 +72,6 @@ export function DonorGuidePage() { {sections.map((section) => ( ))} - - {/* Bottom navigation back to the Help page */} -
- -
); From b1d4237beeb98d312a9c4a8f77681c26f472cb7d Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 15:17:29 -0500 Subject: [PATCH 15/16] Give Donor and Activist guides photo heros and expand payment Q&A MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the plain sticky 'Back to Help' bar on the two guide pages with a hero section in the same recipe as the Organize and Actions homepage heros: rotating photo banner (HeroBanner) + atmospheric tint (HeroAtmosphere) + scrims + overlay copy. Sub-page sized — ~280px instead of the 460px homepage heros — and embeds a glassy 'Back to Help' chip in the eyebrow row so navigation out stays prominent without a separate sticky strip. - Donor Guide reuses the World Liberty Congress photos in /public/hero with the cool palette: reads as community / supporters. - Activist Guide reuses the protest cover gallery from DEFAULT_ACTION_COVERS with the warm hope palette: reads as people in motion. Both pages drop into the new shared GuideHero component to keep the two pages DRY. FAQ updates in the Bitcoin Donations section: - 'Why on-chain Bitcoin?' now spells out that on-chain donations require zero extra setup for activists who already have a Nostr account and zero extra setup for donors who already hold Bitcoin — the accessibility argument that makes Agora viable for normal people. - 'Why doesn't Agora use Lightning?' now names Strike and Breez alongside Wallet of Satoshi as examples of the popular custodial Lightning wallets that can be shut down or pressured. - New 'Why not Monero or another cryptocurrency?' item: Bitcoin's adoption is what makes it easiest for donors to send and activists to receive and spend; niche coins create a barrier neither side should have to clear. --- src/components/GuideHero.tsx | 101 ++++++++++++++++++++++++++++++++ src/lib/helpContent.ts | 11 +++- src/pages/ActivistGuidePage.tsx | 48 +++++++-------- src/pages/DonorGuidePage.tsx | 49 ++++++++-------- 4 files changed, 157 insertions(+), 52 deletions(-) create mode 100644 src/components/GuideHero.tsx diff --git a/src/components/GuideHero.tsx b/src/components/GuideHero.tsx new file mode 100644 index 00000000..d3e5061d --- /dev/null +++ b/src/components/GuideHero.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +import { HeroAtmosphere } from '@/components/HeroAtmosphere'; +import { HeroBanner } from '@/components/HeroBanner'; +import { HOPE_PALETTE, type HopeHue } from '@/lib/hopePalette'; + +interface GuideHeroProps { + /** Eyebrow label rendered above the headline (e.g. "Donor Guide"). */ + eyebrow: string; + /** Large hero headline. */ + title: string; + /** Short subtitle under the headline. */ + subtitle: string; + /** Rotating banner images. Pass at least one. */ + images: readonly string[]; + /** + * Color palette to cycle through for the atmospheric tint. Defaults + * to {@link HOPE_PALETTE} (warm). Pass {@link COOL_PALETTE} for the + * blue/green Organize-style vibe. + */ + palette?: readonly HopeHue[]; +} + +/** + * Compact photo hero shared by the Donor Guide and Activist Guide pages. + * + * Same structural recipe as the Organize / Actions homepage heroes + * ({@link HeroBanner} + {@link HeroAtmosphere} + scrims + overlay copy), + * but tuned smaller because these are sub-pages, not primary destinations. + * Also embeds a "Back to Help" link in the top-left as the page's + * primary navigation out — so a separate sticky bar isn't needed. + */ +export function GuideHero({ + eyebrow, + title, + subtitle, + images, + palette = HOPE_PALETTE, +}: GuideHeroProps) { + // Cycle through the palette on a slow cadence so the photo never + // feels static even when a single banner image is on screen. + const [hueIndex, setHueIndex] = useState(0); + useEffect(() => { + if (palette.length <= 1) return; + const id = window.setInterval(() => { + setHueIndex((i) => (i + 1) % palette.length); + }, 9_000); + return () => window.clearInterval(id); + }, [palette]); + + const activeHue = palette[hueIndex]; + + return ( +
+ + + + {/* Top + bottom scrims so the overlay text stays legible across + every photo in the rotation. */} +
+ ); +} diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts index 36e12123..f4b652fb 100644 --- a/src/lib/helpContent.ts +++ b/src/lib/helpContent.ts @@ -154,6 +154,7 @@ const FAQ_TEMPLATE: FAQCategory[] = [ question: 'Why on-chain Bitcoin?', answer: [ 'On-chain Bitcoin is the most widely supported and censorship-resistant payment rail in the world. Every Bitcoin wallet can send it.', + 'It requires **zero extra setup** for activists once they have a Nostr account, and **zero extra setup** for donors who already hold Bitcoin. That accessibility is what makes {appName} actually viable for normal people to use every day.', 'The tradeoff is that on-chain transactions are public and pay a miner fee. The Donor and Activist guides explain how to handle both.', ], }, @@ -161,10 +162,18 @@ const FAQ_TEMPLATE: FAQCategory[] = [ id: 'why-not-lightning', question: 'Why doesn\'t {appName} use Lightning?', answer: [ - 'Lightning requires a Lightning wallet. The easiest ones (like Wallet of Satoshi) are **custodial** \u2014 a company holds the funds and can be shut down or pressured. Non-custodial Lightning is technically demanding and unreliable for newcomers.', + 'Lightning requires a Lightning wallet. The easiest ones (Wallet of Satoshi, Strike, Breez) are **custodial** \u2014 a company holds the funds and can be shut down, geo-blocked, or pressured into freezing accounts. Non-custodial Lightning is technically demanding and unreliable for newcomers.', 'We want {appName} to work for someone whose only Bitcoin experience is a regular consumer app like Cash App, Coinbase, Strike, Venmo, or PayPal. On-chain Bitcoin works with every wallet on the planet.', ], }, + { + id: 'why-not-other-crypto', + question: 'Why not Monero or another cryptocurrency?', + answer: [ + 'Bitcoin is by far the most widely adopted cryptocurrency. That means it\'s the easiest for donors to buy and send, and the easiest for activists to receive, hold, and spend.', + 'Privacy-focused coins like Monero solve some problems on-chain Bitcoin doesn\'t, but they\'re unsupported by most consumer apps and harder to convert back to local currency. Asking either side of a donation to first acquire a niche cryptocurrency is a barrier {appName} won\'t put in the way.', + ], + }, { id: 'why-not-silent-payments', question: 'Why doesn\'t {appName} use silent payments?', diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx index 56e71160..56234ac4 100644 --- a/src/pages/ActivistGuidePage.tsx +++ b/src/pages/ActivistGuidePage.tsx @@ -1,12 +1,14 @@ import { useSeoMeta } from '@unhead/react'; -import { ArrowLeft, AlertTriangle, Megaphone } from 'lucide-react'; -import { Link } from 'react-router-dom'; +import { AlertTriangle } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { GuideHero } from '@/components/GuideHero'; import { GuideSectionCard } from '@/components/GuideSectionCard'; import { useAppContext } from '@/hooks/useAppContext'; import { useLayoutOptions } from '@/contexts/LayoutContext'; +import { DEFAULT_ACTION_COVERS } from '@/lib/defaultActionCovers'; import { getActivistGuideSections } from '@/lib/helpContent'; +import { HOPE_PALETTE } from '@/lib/hopePalette'; /** * Activist Guide — long-form companion to the Help page. @@ -28,24 +30,15 @@ export function ActivistGuidePage() { return (
- {/* PWA-style sticky back-to-help nav */} -
-
- - - Back to Help - -
- - Activist Guide -
-
-
+ -
+
{/* Above-ground recommendation alert */} @@ -63,13 +56,6 @@ export function ActivistGuidePage() { - {/* Short intro */} -

- Receiving support on {config.appName} means donors send Bitcoin directly to an address - derived from your Nostr key. Here's how it works, and how to move funds privately if - you need to. -

- {/* Sections */} {sections.map((section) => ( @@ -79,4 +65,14 @@ export function ActivistGuidePage() { ); } +/** + * Hero images for the Activist Guide. Reuses the protest / action cover + * gallery already used by the Actions page hero — raised fists, people + * power, freedom imagery — so the page reads as belonging to activists, + * not just generic "users." + */ +const ACTIVIST_HERO_IMAGES: readonly string[] = DEFAULT_ACTION_COVERS.map( + (c) => c.url, +); + export default ActivistGuidePage; diff --git a/src/pages/DonorGuidePage.tsx b/src/pages/DonorGuidePage.tsx index dd15872f..c9dfc9d8 100644 --- a/src/pages/DonorGuidePage.tsx +++ b/src/pages/DonorGuidePage.tsx @@ -1,12 +1,13 @@ import { useSeoMeta } from '@unhead/react'; -import { ArrowLeft, AlertTriangle, HandHeart } from 'lucide-react'; -import { Link } from 'react-router-dom'; +import { AlertTriangle } from 'lucide-react'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { GuideHero } from '@/components/GuideHero'; import { GuideSectionCard } from '@/components/GuideSectionCard'; import { useAppContext } from '@/hooks/useAppContext'; import { useLayoutOptions } from '@/contexts/LayoutContext'; import { getDonorGuideSections } from '@/lib/helpContent'; +import { COOL_PALETTE } from '@/lib/hopePalette'; /** * Donor Guide — long-form companion to the Help page. @@ -28,24 +29,15 @@ export function DonorGuidePage() { return (
- {/* PWA-style sticky back-to-help nav */} -
-
- - - Back to Help - -
- - Donor Guide -
-
-
+ -
+
{/* Above-ground recommendation alert */} @@ -62,12 +54,6 @@ export function DonorGuidePage() { - {/* Short intro */} -

- Supporting an activist on {config.appName} means sending real Bitcoin on-chain. Here's - how it works, and how to do it privately if you need to. -

- {/* Sections */} {sections.map((section) => ( @@ -77,4 +63,17 @@ export function DonorGuidePage() { ); } +/** + * Hero images for the Donor Guide. Reuses the World Liberty Congress + * event photos already in `/public/hero/` — they read as "community of + * supporters," which fits a donor-facing page. Same assets used by the + * Organize and Communities homepage heroes, so we get free preload + * caching across the app. + */ +const DONOR_HERO_IMAGES: readonly string[] = [ + '/hero/wlc-1.webp', + '/hero/wlc-2.webp', + '/hero/wlc-3.webp', +]; + export default DonorGuidePage; From 598358338818eeee1811c22c2f3dcb9e11f28488 Mon Sep 17 00:00:00 2001 From: mkfain Date: Wed, 20 May 2026 15:19:59 -0500 Subject: [PATCH 16/16] Drop guide hero eyebrow; promote 'Donor Guide' / 'Activist Guide' to headline The eyebrow label in the top-right of the guide hero was redundant with the headline beside it. Cut the eyebrow prop and use the page name ('Donor Guide' / 'Activist Guide') as the actual headline. The 'Back to Help' chip stays on its own row at the top of the hero. --- src/components/GuideHero.tsx | 12 +++--------- src/pages/ActivistGuidePage.tsx | 3 +-- src/pages/DonorGuidePage.tsx | 3 +-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/GuideHero.tsx b/src/components/GuideHero.tsx index d3e5061d..4d186dcf 100644 --- a/src/components/GuideHero.tsx +++ b/src/components/GuideHero.tsx @@ -7,8 +7,6 @@ import { HeroBanner } from '@/components/HeroBanner'; import { HOPE_PALETTE, type HopeHue } from '@/lib/hopePalette'; interface GuideHeroProps { - /** Eyebrow label rendered above the headline (e.g. "Donor Guide"). */ - eyebrow: string; /** Large hero headline. */ title: string; /** Short subtitle under the headline. */ @@ -33,7 +31,6 @@ interface GuideHeroProps { * primary navigation out — so a separate sticky bar isn't needed. */ export function GuideHero({ - eyebrow, title, subtitle, images, @@ -69,9 +66,9 @@ export function GuideHero({ />
- {/* Back-to-Help action sits in the eyebrow row, on the left, so - it doubles as both the navigation out and the breadcrumb. */} -
+ {/* Back-to-Help action sits on its own row at the top so it + doubles as both the navigation out and the breadcrumb. */} +
Back to Help - - {eyebrow} -
{/* Headline + subtitle anchored to the bottom of the hero so the diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx index 56234ac4..6446a3ac 100644 --- a/src/pages/ActivistGuidePage.tsx +++ b/src/pages/ActivistGuidePage.tsx @@ -31,8 +31,7 @@ export function ActivistGuidePage() { return (