- {/* Back-to-Help action sits on its own row at the top so it
+
+ {/* Back-to-About action sits on its own row at the top so it
doubles as both the navigation out and the breadcrumb. */}
- Back to Help
+ Back to About
diff --git a/src/components/GuideSectionCard.tsx b/src/components/GuideSectionCard.tsx
deleted file mode 100644
index c7ea3960..00000000
--- a/src/components/GuideSectionCard.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-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 2ca3e2c8..018c7995 100644
--- a/src/components/HelpFAQSection.tsx
+++ b/src/components/HelpFAQSection.tsx
@@ -1,4 +1,4 @@
-import { useMemo, Fragment } from 'react';
+import { useMemo, useState, Fragment } from 'react';
import {
Accordion,
@@ -9,6 +9,7 @@ import {
import { useAppContext } from '@/hooks/useAppContext';
import { getFAQCategories, type FAQCategory, type FAQItem } from '@/lib/helpContent';
import { renderInlineMarkup } from '@/lib/helpMarkup';
+import { cn } from '@/lib/utils';
// ── Component ─────────────────────────────────────────────────────────────────
@@ -21,6 +22,29 @@ interface HelpFAQSectionProps {
hideHeadings?: boolean;
/** Additional class names for the wrapper. */
className?: string;
+ /**
+ * Rendering variant.
+ *
+ * - `'list'` (default): flat accordion list grouped by category. Used
+ * inside contextual settings pages where the FAQ is a small inline
+ * reference.
+ * - `'cards'`: each FAQ item renders as its own rounded card with a
+ * single-item accordion inside. Used by the About page so the FAQ
+ * reads as a designed section rather than an undifferentiated dump.
+ */
+ variant?: 'list' | 'cards';
+ /**
+ * When `variant='cards'`, render a row of category-tab pills above the
+ * cards. Only one category is shown at a time. No effect on `'list'`.
+ */
+ tabs?: boolean;
+ /**
+ * When `variant='list'`, choose between the default pill-tinted category
+ * heading (used by inline contextual FAQs in settings pages) and a
+ * quieter 'reference' tone used by the About page's `Need help?`
+ * section where the FAQ is the main content rather than a sidebar.
+ */
+ listTone?: 'default' | 'reference';
}
/**
@@ -31,16 +55,27 @@ interface HelpFAQSectionProps {
* relevant subset of questions.
*
* @example
- * // Full FAQ (Help page)
+ * // Full FAQ (legacy list layout)
*
*
+ * // About-page integrated FAQ (cards + category tabs)
+ *
+ *
* // Only payments questions (wallet settings page)
*
*
* // Specific questions (onboarding)
*
*/
-export function HelpFAQSection({ categories, items, hideHeadings, className }: HelpFAQSectionProps) {
+export function HelpFAQSection({
+ categories,
+ items,
+ hideHeadings,
+ className,
+ variant = 'list',
+ tabs = false,
+ listTone = 'default',
+}: HelpFAQSectionProps) {
const { config } = useAppContext();
const filteredCategories = useMemo(() => {
@@ -71,24 +106,97 @@ export function HelpFAQSection({ categories, items, hideHeadings, className }: H
return cats;
}, [categories, items, config.appName]);
+ // Tab state: first category is selected by default.
+ const [activeTab, setActiveTab] = useState
(
+ filteredCategories[0]?.id ?? null,
+ );
+
if (filteredCategories.length === 0) return null;
+ // ── Card variant ─────────────────────────────────────────────────────────
+ if (variant === 'cards') {
+ const showTabs = tabs && filteredCategories.length > 1;
+ const visibleCategories = showTabs
+ ? filteredCategories.filter((c) => c.id === activeTab)
+ : filteredCategories;
+
+ return (
+
+ {/* Category tab pills */}
+ {showTabs && (
+
+ {filteredCategories.map((category) => {
+ const active = category.id === activeTab;
+ return (
+
+ );
+ })}
+
+ )}
+
+ {visibleCategories.map((category) => (
+
+ {!hideHeadings && !showTabs && (
+
+ {category.label}
+
+ )}
+
+ {/* Masonry-style two-column grid on md+ */}
+
+ {category.items.map((item) => (
+
+ ))}
+
+
+ ))}
+
+ );
+ }
+
+ // ── List variant (default, unchanged behavior) ───────────────────────────
+ const reference = listTone === 'reference';
return (
{filteredCategories.map((category, catIndex) => (
{/* Category heading */}
{!hideHeadings && (
-
-
- {category.label}
-
-
+ reference ? (
+
+
+ {category.label}
+
+
+ ) : (
+
+
+ {category.label}
+
+
+ )
)}
-
+
{category.items.map((item) => (
-
+
))}
@@ -97,7 +205,40 @@ export function HelpFAQSection({ categories, items, hideHeadings, className }: H
);
}
-function FAQAccordionItem({ item }: { item: FAQItem }) {
+function FAQAccordionItem({
+ item,
+ reference,
+}: {
+ item: FAQItem;
+ reference?: boolean;
+}) {
+ if (reference) {
+ // 'reference' mode: each Q&A reads as a substantial card-row with
+ // a left orange-accent rule that lights up on hover and open. Used
+ // by the About page where the FAQ is a first-class chapter rather
+ // than a sidebar.
+ return (
+
+ {/* Left accent rule: orange when open, transparent otherwise */}
+
+
+ {item.question}
+
+
+ {item.answer.map((paragraph, i) => (
+ {renderInlineMarkup(paragraph)}
+ ))}
+
+
+ );
+ }
+
return (
@@ -111,3 +252,27 @@ function FAQAccordionItem({ item }: { item: FAQItem }) {
);
}
+
+/**
+ * A single FAQ entry rendered as its own rounded card. Wraps a one-item
+ * Radix Accordion so the click-to-expand UX is preserved. Used by the
+ * About page's card-variant FAQ section.
+ */
+function FAQCard({ item }: { item: FAQItem }) {
+ return (
+
+
+
+
+ {item.question}
+
+
+ {item.answer.map((paragraph, i) => (
+ {renderInlineMarkup(paragraph)}
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/components/HelpTip.tsx b/src/components/HelpTip.tsx
index 8e42ae64..8fe52d72 100644
--- a/src/components/HelpTip.tsx
+++ b/src/components/HelpTip.tsx
@@ -57,7 +57,7 @@ export function HelpTip({ faqId, iconSize = 'size-4', className }: HelpTipProps)
{/* Link to full FAQ */}
View all FAQs →
diff --git a/src/components/LandingHero.tsx b/src/components/LandingHero.tsx
index 40820999..f93802e6 100644
--- a/src/components/LandingHero.tsx
+++ b/src/components/LandingHero.tsx
@@ -30,7 +30,7 @@ export function LandingHero({ onJoinClick }: LandingHeroProps) {
Join
diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx
index 383cb87a..94db2cbd 100644
--- a/src/components/TopNav.tsx
+++ b/src/components/TopNav.tsx
@@ -3,8 +3,8 @@ import { Link, NavLink, useNavigate } from 'react-router-dom';
import {
Activity,
Bell,
- CircleHelp,
HandHeart,
+ Info,
Megaphone,
Menu,
Search,
@@ -208,7 +208,7 @@ function getProfileMenuItems({
{ label: 'Profile', to: `/${nip19.npubEncode(userPubkey)}`, icon: User },
{ label: 'Search', to: '/search', icon: Search },
{ label: 'Settings', to: '/settings', icon: Settings },
- { label: 'Help', to: '/help', icon: CircleHelp },
+ { label: 'About', to: '/about', icon: Info },
];
}
diff --git a/src/components/auth/AccountSwitcher.tsx b/src/components/auth/AccountSwitcher.tsx
index 99066426..7561b7f6 100644
--- a/src/components/auth/AccountSwitcher.tsx
+++ b/src/components/auth/AccountSwitcher.tsx
@@ -113,9 +113,9 @@ export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) {
-
+
- Help
+ About
diff --git a/src/components/guide/CalloutCard.tsx b/src/components/guide/CalloutCard.tsx
new file mode 100644
index 00000000..8da2ac41
--- /dev/null
+++ b/src/components/guide/CalloutCard.tsx
@@ -0,0 +1,73 @@
+import { AlertTriangle, CheckCircle2, Info, ShieldAlert } from 'lucide-react';
+import type { LucideIcon } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import type { GuideCalloutBlock } from '@/lib/helpContent';
+
+const VARIANT_STYLES: Record<
+ GuideCalloutBlock['variant'],
+ { container: string; icon: string; title: string; Icon: LucideIcon }
+> = {
+ info: {
+ container: 'border-sky-500/40 bg-sky-500/5',
+ icon: 'text-sky-600 dark:text-sky-400',
+ title: 'text-sky-700 dark:text-sky-300',
+ Icon: Info,
+ },
+ warning: {
+ container: 'border-amber-500/40 bg-amber-500/5',
+ icon: 'text-amber-600 dark:text-amber-400',
+ title: 'text-amber-700 dark:text-amber-300',
+ Icon: AlertTriangle,
+ },
+ danger: {
+ container: 'border-red-500/40 bg-red-500/5',
+ icon: 'text-red-600 dark:text-red-400',
+ title: 'text-red-700 dark:text-red-300',
+ Icon: ShieldAlert,
+ },
+ success: {
+ container: 'border-emerald-500/40 bg-emerald-500/5',
+ icon: 'text-emerald-600 dark:text-emerald-400',
+ title: 'text-emerald-700 dark:text-emerald-300',
+ Icon: CheckCircle2,
+ },
+};
+
+/**
+ * Tinted callout card with an icon, short title, and one-paragraph body.
+ * The four variants map to common semantic intents (info, warning,
+ * danger, success) and share the same layout so the page reads as a
+ * consistent rhythm of blocks rather than a parade of different shapes.
+ */
+export function CalloutCard({ block }: { block: GuideCalloutBlock }) {
+ const styles = VARIANT_STYLES[block.variant];
+ const { Icon } = styles;
+
+ return (
+
+
+
+
+
+
+ {renderInlineMarkup(block.title)}
+
+
+ {renderInlineMarkup(block.body)}
+
+
+
+ );
+}
diff --git a/src/components/guide/GuideProse.tsx b/src/components/guide/GuideProse.tsx
new file mode 100644
index 00000000..e96cd9d8
--- /dev/null
+++ b/src/components/guide/GuideProse.tsx
@@ -0,0 +1,21 @@
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import type { GuideProseBlock } from '@/lib/helpContent';
+
+/**
+ * Plain prose escape hatch. Used sparingly when nothing in the visual
+ * kit fits. Renders an optional heading and short paragraphs.
+ */
+export function GuideProse({ block }: { block: GuideProseBlock }) {
+ return (
+
+ {block.heading && (
+ {block.heading}
+ )}
+
+ {block.paragraphs.map((p, i) => (
+
{renderInlineMarkup(p)}
+ ))}
+
+
+ );
+}
diff --git a/src/components/guide/GuideSteps.tsx b/src/components/guide/GuideSteps.tsx
new file mode 100644
index 00000000..94d9a040
--- /dev/null
+++ b/src/components/guide/GuideSteps.tsx
@@ -0,0 +1,37 @@
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import type { GuideStepsBlock } from '@/lib/helpContent';
+
+/**
+ * Numbered vertical flow of short steps. Each step gets a primary-tinted
+ * circle on the left with its index, then title + body to the right.
+ *
+ * Visual goal: replace 3 or 4 paragraphs of "first X, then Y" prose with
+ * a scannable list that can be read in seconds.
+ */
+export function GuideSteps({ block }: { block: GuideStepsBlock }) {
+ return (
+
+ {block.heading}
+
+ {block.steps.map((step, i) => (
+ -
+
+ {i + 1}
+
+
+
+ {renderInlineMarkup(step.title)}
+
+
+ {renderInlineMarkup(step.body)}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/guide/GuideTLDR.tsx b/src/components/guide/GuideTLDR.tsx
new file mode 100644
index 00000000..37f51d22
--- /dev/null
+++ b/src/components/guide/GuideTLDR.tsx
@@ -0,0 +1,40 @@
+import { Check } from 'lucide-react';
+
+import { Card } from '@/components/ui/card';
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import type { GuideTldrBlock } from '@/lib/helpContent';
+
+/**
+ * Top-of-page summary card. Renders the lede on the left and a checklist
+ * of 2 to 3 next actions on the right (stacked on mobile). Sets the
+ * page's promise in a single screen.
+ */
+export function GuideTLDR({ block }: { block: GuideTldrBlock }) {
+ return (
+
+
+
+
+ The short version
+
+
+ {renderInlineMarkup(block.lede)}
+
+
+
+ {block.nextActions.map((action, i) => (
+ -
+
+
+
+ {renderInlineMarkup(action)}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/guide/InlinePaymentBadge.tsx b/src/components/guide/InlinePaymentBadge.tsx
new file mode 100644
index 00000000..c831ed8b
--- /dev/null
+++ b/src/components/guide/InlinePaymentBadge.tsx
@@ -0,0 +1,33 @@
+import { cn } from '@/lib/utils';
+import type { PaymentMode } from '@/lib/helpContent';
+
+interface InlinePaymentBadgeProps {
+ mode: PaymentMode;
+ className?: string;
+}
+
+/**
+ * Small inline pill that visually distinguishes the two payment options
+ * a campaign can accept (public Bitcoin or silent payments) wherever
+ * they're mentioned in guide copy or table headers.
+ *
+ * Public uses the project's primary accent (orange). Silent uses an
+ * indigo tint so the two read as visually different at a glance without
+ * either looking like a warning state.
+ */
+export function InlinePaymentBadge({ mode, className }: InlinePaymentBadgeProps) {
+ const label = mode === 'public' ? 'Public' : 'Silent';
+ return (
+
+ {label} Payments
+
+ );
+}
diff --git a/src/components/guide/OptionGrid.tsx b/src/components/guide/OptionGrid.tsx
new file mode 100644
index 00000000..8ad2e4ad
--- /dev/null
+++ b/src/components/guide/OptionGrid.tsx
@@ -0,0 +1,82 @@
+import { ExternalLink } from 'lucide-react';
+
+import { Card } from '@/components/ui/card';
+import { cn } from '@/lib/utils';
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import type { GuideOptionGridBlock, GuideOptionItem } from '@/lib/helpContent';
+
+/**
+ * Two-column grid of compact OptionCards (single column on mobile). Used
+ * for the "donate privately" and "cash out" sections. Condenses what
+ * used to be 4 to 6 long-form section cards into a scannable tile grid.
+ */
+export function OptionGrid({ block }: { block: GuideOptionGridBlock }) {
+ return (
+
+ {block.heading}
+ {block.intro && (
+
+ {renderInlineMarkup(block.intro)}
+
+ )}
+
+ {block.options.map((option) => (
+
+ ))}
+
+
+ );
+}
+
+function OptionCard({ option }: { option: GuideOptionItem }) {
+ const isLink = Boolean(option.href);
+
+ const inner = (
+
+
+
{option.name}
+ {isLink && (
+
+ )}
+
+
+ {renderInlineMarkup(option.purpose)}
+
+ {option.chips.length > 0 && (
+
+ {option.chips.map((chip) => (
+ -
+ {chip}
+
+ ))}
+
+ )}
+
+ );
+
+ if (isLink) {
+ return (
+
+ {inner}
+
+ );
+ }
+
+ return inner;
+}
diff --git a/src/components/guide/PaymentComparisonTable.tsx b/src/components/guide/PaymentComparisonTable.tsx
new file mode 100644
index 00000000..92e85cac
--- /dev/null
+++ b/src/components/guide/PaymentComparisonTable.tsx
@@ -0,0 +1,211 @@
+import { Bell, Bug, Eye, EyeOff, Gauge, ShieldCheck, Sparkles, Wallet } from 'lucide-react';
+import type { LucideIcon } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { renderInlineMarkup } from '@/lib/helpMarkup';
+import { InlinePaymentBadge } from './InlinePaymentBadge';
+import type { GuidePaymentComparisonBlock } from '@/lib/helpContent';
+
+interface Row {
+ label: string;
+ Icon: LucideIcon;
+ public: string;
+ silent: string;
+}
+
+const DONOR_ROWS: Row[] = [
+ {
+ label: 'What you see',
+ Icon: Eye,
+ public: 'A regular Bitcoin address you can pay from anywhere.',
+ silent:
+ 'The same QR. Your wallet picks the silent-payment endpoint if it supports BIP-352.',
+ },
+ {
+ label: 'Wallet support',
+ Icon: Wallet,
+ public:
+ 'Every Bitcoin wallet. Cash App, Coinbase, Strike, hardware, anything.',
+ silent:
+ 'Few wallets today. Most fall back to a regular Bitcoin transaction.',
+ },
+ {
+ label: 'Privacy of the donation',
+ Icon: ShieldCheck,
+ public:
+ 'Public on-chain. Your sending address is permanently linked to the campaign.',
+ silent:
+ "Receiving side is unlinkable on-chain. Your sending wallet's trail is still public.",
+ },
+ {
+ label: 'Settlement',
+ Icon: Gauge,
+ public: 'Normal Bitcoin confirmations.',
+ silent:
+ 'Same on-chain confirmations, but the activist has to scan their wallet to find it.',
+ },
+];
+
+const ACTIVIST_ROWS: Row[] = [
+ {
+ label: 'What donors see',
+ Icon: Sparkles,
+ public:
+ 'A regular Bitcoin address. Works with every wallet on earth.',
+ silent:
+ "A BIP-352 endpoint. Donors' wallets need silent-payments support; otherwise the donation falls back to a regular Bitcoin transaction.",
+ },
+ {
+ label: 'Receiving speed',
+ Icon: Gauge,
+ public: 'Push-style. Donations show up immediately on the campaign page.',
+ silent:
+ 'Manual scanning. Your wallet has to walk the blockchain looking for them. Minutes to hours, depending on the wallet.',
+ },
+ {
+ label: 'Push notifications',
+ Icon: Bell,
+ public: 'Yes. You see new donations the moment they arrive.',
+ silent: 'No. Open the wallet and trigger a scan to discover them.',
+ },
+ {
+ label: 'Donor list / campaign totals',
+ Icon: Eye,
+ public:
+ 'Public forever. Amounts and sending addresses are visible to anyone.',
+ silent:
+ "Private. The campaign page can't show silent-payments donation counts or totals.",
+ },
+ {
+ label: 'Ecosystem maturity',
+ Icon: Bug,
+ public: 'Mature. Settled tooling.',
+ silent:
+ 'Bleeding-edge. Wallets are still buggy; expect missed payments that show up on a later scan.',
+ },
+ {
+ label: 'Best for',
+ Icon: ShieldCheck,
+ public:
+ 'Above-ground fundraisers where social proof and visibility help.',
+ silent:
+ 'Campaigns where donor or activist privacy matters more than the visible total.',
+ },
+ {
+ label: 'Watch out for',
+ Icon: EyeOff,
+ public: 'Permanent public record of every donor.',
+ silent:
+ "Bumpy UX today. Some donations won't show until the activist scans.",
+ },
+];
+
+/**
+ * Side-by-side comparison of Public Payments vs. Silent Payments.
+ *
+ * - Desktop (`sm:` and up): three-column grid with row labels on the
+ * left, Public tinted in primary, Silent tinted in indigo.
+ * - Mobile: collapses to two stacked tinted cards (one per option) with
+ * the same row labels inside each card. No sideways scrolling.
+ *
+ * Row content is driven by the `audience` flag so donors and activists
+ * get row copy tuned to what they care about.
+ */
+export function PaymentComparisonTable({
+ block,
+}: {
+ block: GuidePaymentComparisonBlock;
+}) {
+ const rows = block.audience === 'donor' ? DONOR_ROWS : ACTIVIST_ROWS;
+
+ return (
+
+ {/* ── Desktop: aligned 3-column grid ──────────────────────────── */}
+
+
+ {/* Header row */}
+
+
+ {block.audience === 'donor' ? 'When you donate' : 'When you create'}
+
+
+
+
+
+
+
+
+ {/* Body rows */}
+ {rows.map((row, i) => (
+
+
+
+ {row.label}
+
+
+ {renderInlineMarkup(row.public)}
+
+
+ {renderInlineMarkup(row.silent)}
+
+
+ ))}
+
+
+
+ {/* ── Mobile: two stacked tinted cards ───────────────────────── */}
+
+
+ {block.footnote && (
+
+ {renderInlineMarkup(block.footnote)}
+
+ )}
+
+ );
+}
+
+function PaymentStack({
+ mode,
+ rows,
+}: {
+ mode: 'public' | 'silent';
+ rows: Row[];
+}) {
+ return (
+
+
+
+
+
+ {rows.map((row) => (
+
+
-
+
+ {row.label}
+
+ -
+ {renderInlineMarkup(mode === 'public' ? row.public : row.silent)}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/guide/index.ts b/src/components/guide/index.ts
new file mode 100644
index 00000000..0f55a168
--- /dev/null
+++ b/src/components/guide/index.ts
@@ -0,0 +1,13 @@
+/**
+ * Block primitives for the Donor Guide and Activist Guide pages. Each
+ * component takes the matching {@link GuideBlock} variant and renders it.
+ * The page just dispatches on `block.kind`.
+ */
+
+export { CalloutCard } from './CalloutCard';
+export { GuideProse } from './GuideProse';
+export { GuideSteps } from './GuideSteps';
+export { GuideTLDR } from './GuideTLDR';
+export { InlinePaymentBadge } from './InlinePaymentBadge';
+export { OptionGrid } from './OptionGrid';
+export { PaymentComparisonTable } from './PaymentComparisonTable';
diff --git a/src/lib/helpContent.ts b/src/lib/helpContent.ts
index 6dc6ad35..fd0ac843 100644
--- a/src/lib/helpContent.ts
+++ b/src/lib/helpContent.ts
@@ -344,212 +344,358 @@ 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.
+ * The Donor Guide and Activist Guide pages are composed from a typed
+ * sequence of {@link GuideBlock}s. Each block kind is rendered by a
+ * dedicated component from `@/components/guide/`. The page just
+ * dispatches on `block.kind`.
*
- * `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.
+ * String fields may contain the same inline markup as FAQ answers
+ * (`**bold**` and `[link](url)`), and the `{appName}` placeholder. Both
+ * are resolved at read-time by the `getDonorGuideBlocks` /
+ * `getActivistGuideBlocks` helpers below.
*/
-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[];
+
+/**
+ * The two payment options a campaign can offer. Used by table headers
+ * and inline badges.
+ *
+ * - `'public'`: a regular Bitcoin address. Visible on-chain, every
+ * wallet can pay it.
+ * - `'silent'`: a BIP-352 silent-payments endpoint. The receiving side
+ * is unlinkable on-chain, but most wallets can't send to it yet.
+ */
+export type PaymentMode = 'public' | 'silent';
+
+/**
+ * Top-of-page summary card. One-sentence lede, plus 2 to 3 chip-style
+ * next-actions that orient the reader without making them scroll.
+ */
+export interface GuideTldrBlock {
+ kind: 'tldr';
+ lede: string;
+ nextActions: string[];
}
-const DONOR_GUIDE_TEMPLATE: GuideSection[] = [
+/** Numbered vertical flow of 2 to 4 short steps. */
+export interface GuideStepsBlock {
+ kind: 'steps';
+ heading: string;
+ steps: { title: string; body: string }[];
+}
+
+/**
+ * Side-by-side comparison of Public Payments vs. Silent Payments.
+ * Rendered as a real two-column table on desktop and as two stacked
+ * tinted cards on mobile (no sideways scroll). Audience controls row
+ * copy: donors see "what to expect when paying," activists see "what
+ * to choose."
+ */
+export interface GuidePaymentComparisonBlock {
+ kind: 'paymentComparison';
+ audience: 'donor' | 'activist';
+ /** Optional one-line footnote rendered under the table. */
+ footnote?: string;
+}
+
+/** Single-line callout block with a tinted background and an icon. */
+export interface GuideCalloutBlock {
+ kind: 'callout';
+ variant: 'info' | 'warning' | 'danger' | 'success';
+ title: string;
+ body: string;
+}
+
+/** A short prose paragraph block (escape hatch for the rare "needs words"). */
+export interface GuideProseBlock {
+ kind: 'prose';
+ heading?: string;
+ paragraphs: string[];
+}
+
+/** A single tile inside a {@link GuideOptionGridBlock}. */
+export interface GuideOptionItem {
+ /** Tile heading. */
+ name: string;
+ /** One-sentence purpose / payoff. */
+ purpose: string;
+ /** Short tag chips (e.g. `non-custodial`, `low fees`). */
+ chips: string[];
+ /** Optional external URL the tile links to. */
+ href?: string;
+}
+
+/** Grid of compact OptionCard tiles. Used for cash-out and privacy options. */
+export interface GuideOptionGridBlock {
+ kind: 'optionGrid';
+ heading: string;
+ intro?: string;
+ options: GuideOptionItem[];
+}
+
+export type GuideBlock =
+ | GuideTldrBlock
+ | GuideStepsBlock
+ | GuidePaymentComparisonBlock
+ | GuideCalloutBlock
+ | GuideProseBlock
+ | GuideOptionGridBlock;
+
+const DONOR_GUIDE_TEMPLATE: GuideBlock[] = [
{
- id: 'how-donating-works',
- heading: 'How donating works',
- paragraphs: [
- 'When an activist creates a campaign, they choose what kinds of payments to accept: **public** (a regular Bitcoin address), **private** (silent payments), or **both**. The campaign\'s donate page shows a QR code that matches that choice.',
- 'When a campaign accepts **both**, the QR code encodes both endpoints. Silent-payment-capable wallets read it as a private payment; ordinary Bitcoin wallets read it as a normal payment to the public address. You don\'t have to choose \u2014 your wallet picks the right path automatically.',
- 'Either way, the money goes directly to the activist. {appName} doesn\'t hold or route it, and the address is derived from the activist\'s Nostr key so there\'s no middleman in between.',
+ kind: 'tldr',
+ lede: 'Pay the Bitcoin address on the campaign page from any wallet you already have. If the campaign accepts silent payments and your wallet supports them, your donation is private automatically.',
+ nextActions: [
+ 'Pay from any Bitcoin wallet',
+ 'No middleman, no holding period',
+ 'Want privacy? Read below',
],
},
{
- id: 'why-public',
- heading: 'When your donation is public',
- paragraphs: [
- 'Public Bitcoin donations are recorded on the public ledger. Anyone can look up an activist\'s address and see every public 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 \u2014 a KYC consumer app like Cash App, Coinbase, Strike, Venmo, PayPal, Kraken, or Binance ties every transaction to your real identity. That link is what connects a public donation to who you are.',
- '**Silent-payment donations are different.** They settle on-chain like any Bitcoin transaction, but the output can\'t be linked back to the activist\'s donation code. They don\'t appear in donor lists and don\'t count toward public totals. If the campaign accepts silent payments and your wallet supports them, your donation isn\'t visible to outside observers.',
+ kind: 'steps',
+ heading: 'How a donation flows',
+ steps: [
+ {
+ title: 'Open the campaign',
+ body: 'You see a single QR code. If the campaign accepts both options, it encodes both endpoints; your wallet picks the right one.',
+ },
+ {
+ title: 'Pay it from any wallet',
+ body: 'Cash App, Coinbase, Strike, a hardware wallet, anything. Pay the amount plus the network fee.',
+ },
+ {
+ title: 'It arrives directly',
+ body: "Funds settle straight to the activist. {appName} doesn't hold or route them, and the address is derived from the activist's Nostr key.",
+ },
],
},
{
- id: 'privacy-silent-payments',
- heading: 'For privacy: use a silent-payments wallet',
- paragraphs: [
- 'The easiest way to donate privately is to use a Bitcoin wallet that supports **silent payments** (BIP-352). When you scan a campaign\'s QR code, your wallet will use the silent-payment rail automatically if the campaign accepts it.',
- 'Two options:',
- '\u2022 [Ditto Wallet](https://ditto.pub) \u2014 a Nostr-native Bitcoin wallet that supports silent payments.',
- '\u2022 [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk) \u2014 a dedicated silent-payments wallet for Android.',
- 'One caveat: silent payments hide the donation itself, but not where the sats in your wallet came from. If you funded the wallet from a KYC exchange, the funding transaction is still traceable to you. For stronger privacy, fund the wallet with non-KYC Bitcoin (see below).',
- ],
- pros: ['Simplest privacy path \u2014 no coinjoin, no peer-to-peer trade.', 'Non-custodial \u2014 you keep your keys.', 'Donation never appears in public donor lists or totals.'],
- cons: ['Only works if the campaign accepts silent payments (most do).', 'Funding the wallet from a KYC exchange still leaks the funding step.'],
+ kind: 'paymentComparison',
+ audience: 'donor',
+ footnote:
+ 'Campaigns can accept Public only, Silent only, or both. If both, the QR code carries both endpoints. Your wallet picks the one it can use.',
},
{
- id: 'privacy-non-kyc',
- heading: 'For stronger privacy: source non-KYC Bitcoin',
- paragraphs: [
- 'To remove the funding-step link entirely, buy Bitcoin peer-to-peer so it isn\'t tied to your government ID. [Bisq](https://bisq.network) and [HodlHodl](https://hodlhodl.com) let you trade on-chain Bitcoin directly with another person. [RoboSats](https://learn.robosats.com) is Lightning-only, so you\'d swap to Lightning with [Boltz](https://boltz.exchange) and then receive on RoboSats.',
- 'Move those sats into a silent-payments wallet (Ditto Wallet or Dana) and you have both halves of the privacy story: no KYC link on the way in, and no public trace on the way out.',
- ],
- pros: ['No exchange knows who you are.', 'Combined with silent payments, this is the strongest privacy setup.'],
- cons: ['Slower and harder than a consumer app.', 'Requires finding a counterparty.'],
+ kind: 'callout',
+ variant: 'warning',
+ title: 'Public donations are visible on-chain forever',
+ body: 'A **Public** donation lands at a regular Bitcoin address tied to the campaign. Anyone can look up the address and see the amount, the time, and your sending address. **Silent** donations settle on-chain too, but the receiving side is unlinkable to the campaign so they stay out of public donor lists and totals.',
},
{
- id: 'privacy-coinjoin',
- heading: 'If you can\'t switch wallets: coinjoin first',
- paragraphs: [
- 'If you\'re stuck using an ordinary Bitcoin wallet (no silent-payments support) and the campaign only accepts public payments, 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.',
+ kind: 'optionGrid',
+ heading: 'Donating privately',
+ intro:
+ "These steps matter most for **Public** donations, where every transaction is permanently tied to a single address. **Silent** donations already hide the receiving side, so the risk is lower. Targeted analysis of your sending wallet is still possible either way, so if your risk is high these steps are worth taking. Pick one, or stack them.",
+ options: [
+ {
+ name: 'Use a silent-payments wallet',
+ purpose:
+ 'Pay with a Bitcoin wallet that supports BIP-352. If the campaign accepts silent payments, your wallet uses that endpoint automatically.',
+ chips: ['non-custodial', 'easiest', 'BIP-352'],
+ href: 'https://ditto.pub',
+ },
+ {
+ name: 'Buy non-KYC Bitcoin',
+ purpose:
+ "Buy Bitcoin peer-to-peer so it isn't linked to your government ID in the first place. Strongest privacy starting point.",
+ chips: ['peer-to-peer', 'no ID'],
+ href: 'https://bisq.network',
+ },
+ {
+ name: 'Coinjoin first',
+ purpose:
+ "Mix your Bitcoin with other people's coins so the output can't be traced to your KYC purchase. Useful when the campaign only accepts public.",
+ chips: ['non-custodial', 'breaks history'],
+ href: 'https://wasabiwallet.io',
+ },
+ {
+ name: 'Use a fresh wallet',
+ purpose:
+ 'Donate from a wallet that has never touched your main identity or a KYC exchange.',
+ chips: ['free', 'non-custodial', 'easiest'],
+ href: 'https://sparrowwallet.com',
+ },
],
- pros: ['Breaks the on-chain trail from your KYC purchase.', 'Non-custodial.'],
- cons: ['Costs fees and takes time.', 'Fewer maintained tools after the Samourai shutdown.', 'Silent payments are usually easier if the campaign accepts them.'],
},
{
- id: 'donor-comparison',
- heading: 'Quick comparison',
+ kind: 'callout',
+ variant: 'danger',
+ title: "Consumer apps can't make you anonymous",
+ body: 'Cash App, Coinbase, Strike, Venmo, Kraken, Binance, and PayPal all verify your ID. No matter how you send the donation, every transaction stays tied to your real identity. Use a non-custodial wallet you control.',
+ },
+ {
+ kind: 'prose',
+ heading: 'A note on silent payments today',
paragraphs: [
- '**Silent-payments wallet (Ditto Wallet, Dana):** non-custodial \u00b7 high privacy \u00b7 easy \u00b7 low fees. Best default if the campaign accepts private payments.',
- '**Non-KYC source + silent payments:** non-custodial \u00b7 strongest privacy \u00b7 harder \u00b7 variable fees.',
- '**Coinjoin + public donation:** non-custodial \u00b7 high privacy \u00b7 medium difficulty \u00b7 medium fees. Useful when only public payments are accepted.',
- '**Consumer app (Cash App, Coinbase, Strike, Venmo, PayPal):** custodial \u00b7 no privacy \u00b7 easiest \u00b7 ties the donation to your real identity. Convenient, but never anonymous.',
+ "Silent payments are the most private way to receive Bitcoin on-chain, but the ecosystem is young. Most popular wallets can't send to a silent-payment endpoint yet, so when a wallet can't, the donation falls back to a regular Bitcoin transaction to the campaign's public address (if the campaign accepts both).",
+ 'For activists, silent-payment donations also arrive without push notifications and only appear after the activist scans their wallet, which can take minutes to hours. None of this affects the safety of your funds; it just shapes the experience.',
],
},
];
-const ACTIVIST_GUIDE_TEMPLATE: GuideSection[] = [
+const ACTIVIST_GUIDE_TEMPLATE: GuideBlock[] = [
{
- id: 'how-receiving-works',
+ kind: 'tldr',
+ lede: "Pick what to accept when you create your campaign: Public, Silent, or both. Either option is non-custodial. {appName} never holds your funds.",
+ nextActions: [
+ 'Compare the two options',
+ 'Plan how you will cash out',
+ 'Sweep funds promptly',
+ ],
+ },
+ {
+ kind: 'prose',
heading: 'How receiving works',
paragraphs: [
- 'Your {appName} donation addresses are derived from your Nostr public key. When you create a campaign, you pick what kinds of payments to accept:',
- '\u2022 **Public payments only** \u2014 a regular Bitcoin address. Visible to everyone, works with every wallet.',
- '\u2022 **Private payments only** \u2014 silent payments (BIP-352). Settles on-chain but unlinkable to your donation code, so it stays out of public donor lists and totals. Donors need a silent-payment-capable wallet to send.',
- '\u2022 **Both** \u2014 {appName} generates a single QR code that encodes both endpoints. Silent-payment wallets read it as private; ordinary wallets pay the public address. Donors don\'t have to choose.',
- 'Accepting both is usually the right call: you get private donations from supporters who use a silent-payments wallet, and you stay open to donors whose only Bitcoin is in a consumer app. No one stands between you and the funds either way, and no server can be shut down to stop them.',
+ "Your {appName} donation addresses are derived from your Nostr public key. When you create a campaign, you pick what to accept:",
+ "**Public payments only.** A regular Bitcoin address. Visible to everyone, works with every wallet.",
+ "**Silent payments only.** BIP-352 silent payments. The receiving side is unlinkable on-chain, so donations stay out of public donor lists and totals. Donors need a silent-payments-capable wallet to send. If they don't have one, their donation can't go through.",
+ "**Both.** {appName} generates a single QR code that encodes both endpoints. Silent-payments wallets read it as private; ordinary wallets pay the public address. Donors don't have to choose.",
+ 'Accepting both is usually the right call: you get private donations from supporters who use a silent-payments wallet, and you stay open to donors whose only Bitcoin is in a consumer app.',
],
},
{
- id: 'why-public',
- heading: 'What\'s visible and what isn\'t',
+ kind: 'prose',
+ heading: 'What everyone can see',
paragraphs: [
- '**Public donations** are recorded on the Bitcoin blockchain. Anyone can look up your address and see every public donation \u2014 the amount, the time, and the sending address. Your supporters\' addresses are visible too. These donations show up in your campaign\'s donor list and progress totals.',
- '**Private donations** use silent payments. They settle on-chain like any Bitcoin transaction, but the output can\'t be linked back to your donation code. By design they don\'t appear in donor lists and don\'t count toward public totals \u2014 only you can see them, by scanning with the wallet that holds the key.',
+ 'If your campaign accepts public payments, anyone considering supporting you can look up the address and see the public donation history.',
+ "Silent-payment donations aren't part of that record. They're invisible to outside observers and don't show in the campaign's public totals; new donors only see whatever you publish about the campaign's progress.",
],
},
{
- id: 'dont-keep-funds',
- heading: 'Don\'t keep funds at your {appName} address',
+ kind: 'paymentComparison',
+ audience: 'activist',
+ footnote:
+ "You can't switch a campaign's accepted payment options after it's created. If you change your mind, make a new campaign.",
+ },
+ {
+ kind: 'prose',
+ heading: 'A note on silent payments today',
paragraphs: [
- 'Move funds to a wallet you control as soon as practical. Treat your {appName} address like a mailbox, not a savings account.',
- 'For public donations: [Sparrow](https://sparrowwallet.com), [BlueWallet](https://bluewallet.io), or [Phoenix](https://phoenix.acinq.co) (Lightning) are good self-custody options.',
- 'For private (silent-payment) donations: a silent-payments wallet like [Ditto Wallet](https://ditto.pub) or [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk) can scan for and hold them. {appName} itself supports scanning, but moving funds onward to a dedicated wallet is still recommended.',
+ "Silent payments are the most private way to receive Bitcoin on-chain, but the ecosystem is young. Most popular wallets can't send to a silent-payment endpoint yet, so when a donor's wallet can't, the donation falls back to a regular Bitcoin transaction to your public address (if you accept both).",
+ 'Silent-payment donations also arrive without push notifications and only appear after you scan your wallet, which can take minutes to hours. None of this affects the safety of your funds; it just shapes the day-to-day experience.',
],
},
{
- id: 'cashout-overview',
- heading: 'Cashing out privately \u2014 overview',
- paragraphs: [
- 'The simplest privacy exit is to **move your donations into a silent-payments wallet first**, and then spend onward from there. The hop into the silent-payments wallet breaks the link between your public campaign address and what comes next \u2014 once the funds are sitting in that wallet, you can send them to any Bitcoin address with a fresh chain-analysis trail.',
- 'The other approaches below (Lightning swap, coinjoin, peer-to-peer exchange) still work and have their own tradeoffs. Tumblers are usually a bad idea.',
+ kind: 'steps',
+ heading: 'Move donations promptly',
+ steps: [
+ {
+ title: 'Sweep to a wallet you control',
+ body: 'Good self-custody options: [Sparrow](https://sparrowwallet.com), [BlueWallet](https://bluewallet.io), or [Phoenix](https://phoenix.acinq.co) (Lightning).',
+ },
+ {
+ title: "Don't sit on funds at the campaign address",
+ body: 'Treat it like a mailbox, not a savings account. This applies to both Public and Silent donations.',
+ },
],
},
{
- id: 'cashout-silent-payments',
- heading: 'Cash out to a silent-payments wallet',
- paragraphs: [
- 'Move your public donations into a wallet that can receive via silent payments \u2014 [Ditto Wallet](https://ditto.pub) or [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk). Both generate a silent-payment receiving code; send your funds to that code from your {appName} address.',
- 'Once the money lands in the silent-payments wallet, the link between your public campaign address and your downstream spending is broken \u2014 the receiving transaction settles on-chain but isn\'t connected to the silent-payments wallet\'s reusable code. From there you can spend to any Bitcoin address and the chain-analysis trail starts fresh.',
- 'If your campaign was set to accept silent payments to begin with, the private donations are already going straight to whatever silent-payments wallet you scan them in \u2014 there\'s nothing extra to do.',
- ],
- pros: ['Simplest privacy exit \u2014 no swap services, no peer-to-peer trade.', 'Non-custodial \u2014 you keep your keys throughout.', 'Low fees: just standard on-chain miner fees for the move.'],
- cons: ['Silent-payments ecosystem is young; fewer wallet options.', 'You need to fund the silent-payments wallet from your {appName} address, which is itself an on-chain transaction.'],
- },
- {
- 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 the silent-payments hop.'],
- },
- {
- id: 'cashout-p2p',
- heading: 'Peer-to-peer exchange',
- paragraphs: [
- 'Trade Bitcoin for fiat directly with another person. [Bisq](https://bisq.network) and [HodlHodl](https://hodlhodl.com) trade on-chain Bitcoin. [RoboSats](https://learn.robosats.com) is Lightning-only \u2014 swap your on-chain Bitcoin to Lightning first with [Boltz](https://boltz.exchange), then sell on RoboSats. No exchange records your identity either way.',
- ],
- 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.',
- 'Silent payments and coinjoin are the non-custodial alternatives, and either is almost always the better choice.',
+ kind: 'optionGrid',
+ heading: 'Cashing out privately',
+ intro:
+ "Spending on-chain creates a trail unless you break it first. The simplest privacy exit is to **move funds into a silent-payments wallet first**, then spend onward; the hop breaks the link between your campaign address and what comes next. The other options below also work and have their own trade-offs.",
+ options: [
+ {
+ name: 'Silent-payments wallet hop',
+ purpose:
+ "Move your donations to a silent-payments wallet ([Ditto Wallet](https://ditto.pub), [Dana](https://github.com/cygnet3/dana/releases/download/v0.7.4/app-live-release.apk)). From there your downstream spending isn't tied to the campaign.",
+ chips: ['non-custodial', 'easiest', 'low fees'],
+ href: 'https://ditto.pub',
+ },
+ {
+ name: 'Lightning swap',
+ purpose:
+ "Atomic-swap on-chain Bitcoin to Lightning. Lightning payments don't hit the public blockchain.",
+ chips: ['non-custodial', 'easy', 'low fees'],
+ href: 'https://boltz.exchange',
+ },
+ {
+ name: 'Coinjoin',
+ purpose:
+ "Mix your Bitcoin with other users' coins so the output can't be linked back to the input.",
+ chips: ['non-custodial', 'high privacy'],
+ href: 'https://wasabiwallet.io',
+ },
+ {
+ name: 'Peer-to-peer',
+ purpose:
+ 'Trade Bitcoin for fiat directly with another person or through a broker on Bisq, HodlHodl, or RoboSats.',
+ chips: ['cash', 'no KYC'],
+ href: 'https://bisq.network',
+ },
+ {
+ name: 'Spend it directly',
+ purpose:
+ 'Buy gift cards (Amazon, Uber, groceries, travel) straight from Bitcoin without converting to cash first.',
+ chips: ['skip cash-out', 'instant'],
+ href: 'https://www.bitrefill.com/us/en/',
+ },
],
},
{
- id: 'cashout-comparison',
- heading: 'Quick comparison',
- paragraphs: [
- '**Silent-payments wallet (Ditto Wallet, Dana):** non-custodial \u00b7 high privacy \u00b7 easy \u00b7 low fees. Recommended default.',
- '**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, HodlHodl, RoboSats via Boltz):** 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 public donation history is visible to future supporters',
- paragraphs: [
- 'If your campaign accepts public payments, anyone considering supporting you can look up your address and see the full public donation history. Keep in mind how that history reads to a new donor.',
- 'Silent-payment donations aren\'t part of this \u2014 they\'re invisible to outside observers and don\'t show in your campaign\'s public totals.',
- ],
+ kind: 'callout',
+ variant: 'danger',
+ title: 'Avoid centralized tumblers',
+ body: 'Custodial mixers can steal your coins, log who sent what, or turn out to be law-enforcement honeypots. Use a silent-payments hop or a non-custodial coinjoin instead.',
},
];
-/** 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)),
- };
+/** Substitute placeholders in a single guide block. */
+function substituteGuideBlock(block: GuideBlock, appName: string): GuideBlock {
+ switch (block.kind) {
+ case 'tldr':
+ return {
+ ...block,
+ lede: substitute(block.lede, appName),
+ nextActions: block.nextActions.map((a) => substitute(a, appName)),
+ };
+ case 'steps':
+ return {
+ ...block,
+ heading: substitute(block.heading, appName),
+ steps: block.steps.map((s) => ({
+ title: substitute(s.title, appName),
+ body: substitute(s.body, appName),
+ })),
+ };
+ case 'paymentComparison':
+ return {
+ ...block,
+ footnote: block.footnote ? substitute(block.footnote, appName) : undefined,
+ };
+ case 'callout':
+ return {
+ ...block,
+ title: substitute(block.title, appName),
+ body: substitute(block.body, appName),
+ };
+ case 'prose':
+ return {
+ ...block,
+ heading: block.heading ? substitute(block.heading, appName) : undefined,
+ paragraphs: block.paragraphs.map((p) => substitute(p, appName)),
+ };
+ case 'optionGrid':
+ return {
+ ...block,
+ heading: substitute(block.heading, appName),
+ intro: block.intro ? substitute(block.intro, appName) : undefined,
+ options: block.options.map((o) => ({
+ ...o,
+ name: substitute(o.name, appName),
+ purpose: substitute(o.purpose, appName),
+ chips: o.chips.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));
+/** Donor guide blocks with `{appName}` resolved. */
+export function getDonorGuideBlocks(appName: string): GuideBlock[] {
+ return DONOR_GUIDE_TEMPLATE.map((b) => substituteGuideBlock(b, appName));
}
-/** Activist guide sections with `{appName}` resolved. */
-export function getActivistGuideSections(appName: string): GuideSection[] {
- return ACTIVIST_GUIDE_TEMPLATE.map((s) => substituteGuideSection(s, appName));
+/** Activist guide blocks with `{appName}` resolved. */
+export function getActivistGuideBlocks(appName: string): GuideBlock[] {
+ return ACTIVIST_GUIDE_TEMPLATE.map((b) => substituteGuideBlock(b, appName));
}
diff --git a/src/lib/sidebarItems.tsx b/src/lib/sidebarItems.tsx
index 37177ea9..fa421068 100644
--- a/src/lib/sidebarItems.tsx
+++ b/src/lib/sidebarItems.tsx
@@ -18,7 +18,7 @@ import {
HandHeart,
HelpCircle,
Highlighter,
- LifeBuoy,
+ Info,
List,
MessageSquare,
MessageSquareMore,
@@ -160,7 +160,7 @@ export const SIDEBAR_ITEMS: SidebarItemDef[] = [
},
{ id: "settings", label: "Settings", path: "/settings", icon: Settings },
{ id: "changelog", label: "Changelog", path: "/changelog", icon: ScrollText },
- { id: "help", label: "Help", path: "/help", icon: LifeBuoy },
+ { id: "help", label: "About", path: "/about", icon: Info },
{ id: "agent", label: "Agent", path: "/agent", icon: Bot },
// Content types
{ id: "actions", label: "Pledges", path: "/pledges", icon: Megaphone },
diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx
new file mode 100644
index 00000000..b764aa41
--- /dev/null
+++ b/src/pages/AboutPage.tsx
@@ -0,0 +1,872 @@
+import { useSeoMeta } from '@unhead/react';
+import { useMemo } from 'react';
+import {
+ AlertTriangle,
+ ArrowRight,
+ BadgeCheck,
+ Bitcoin,
+ CircleCheck,
+ Globe,
+ HandHeart,
+ Megaphone,
+ ShieldCheck,
+ ShieldOff,
+} from 'lucide-react';
+import { nip19 } from 'nostr-tools';
+import { Link } from 'react-router-dom';
+
+import { useAppContext } from '@/hooks/useAppContext';
+import { HelpFAQSection } from '@/components/HelpFAQSection';
+import { TEAM_SOAPBOX_PACK } from '@/lib/helpContent';
+import { cn } from '@/lib/utils';
+
+/**
+ * The /about page. A landing-style document modeled on the public
+ * https://soapbox.pub/agora landing, brought in-app to explain how
+ * the platform works. Five sections:
+ *
+ * 1. Hero (dark)
+ * 2. How it works, in three steps (cream)
+ * 3. Two ways to get paid: compare cards (dark)
+ * 4. Need help? FAQ in three chapters (cream)
+ * 5. Pick the side you're on: Donor / Activist guides (white)
+ *
+ * Typography follows the CampaignsPage hero recipe exactly: Bebas Neue
+ * (`font-display`) is reserved for the hero H1 (italic, normal weight,
+ * stroke-painted) and the step numerals. Every other heading uses
+ * Inter Bold (`font-sans font-bold tracking-tight`), the project's
+ * canonical section-heading idiom. Bebas Neue is never bolded; doing
+ * so produces unreadable synthetic-bold smear.
+ */
+export function AboutPage() {
+ const { config } = useAppContext();
+ // Routed under the wide FundraiserLayout in AppRouter so sections can
+ // span the viewport with their own backgrounds.
+
+ useSeoMeta({
+ title: `About | ${config.appName}`,
+ description: `How ${config.appName} works: connecting activists to unstoppable funding through Bitcoin and Nostr.`,
+ });
+
+ const appName = config.appName;
+
+ // In-app link to the Team Soapbox follow pack, via the addressable
+ // /:nip19 route. Encoded once per render (cheap).
+ const teamSoapboxNaddr = useMemo(
+ () =>
+ nip19.naddrEncode({
+ kind: TEAM_SOAPBOX_PACK.kind,
+ pubkey: TEAM_SOAPBOX_PACK.pubkey,
+ identifier: TEAM_SOAPBOX_PACK.identifier,
+ }),
+ [],
+ );
+
+ return (
+
+ {/* ── 1. Hero ──────────────────────────────────────────────────────── */}
+
+ {/* Map-tint overlay */}
+
+ {/* Soft orange halos */}
+
+
+
+
+
+ {/* Headline column */}
+
+
+ About {appName}
+
+ {/* Hero H1: verbatim CampaignsPage recipe. Bebas Neue at
+ italic, font-normal, with -webkit-text-stroke painting
+ the optical weight. Never font-bold. */}
+
+ How{' '}
+ {/* Orange highlighter span: same negative-margin trick
+ as CampaignsPage so the inner letter aligns with the
+ column edge despite the box padding. */}
+
+ {appName}
+
+ {/* Line break only on sm+ — on mobile the whole
+ headline fits on one line. */}
+
+ {' '}works.
+
+
+ {appName} is a censorship-resistant donation platform built on
+ Nostr and Bitcoin. No frozen bank accounts. No corporate
+ shut-downs. Just direct support from people who believe in your
+ cause.
+
+
+ {/* Trust chips */}
+
+ {['Decentralized', 'Open source', 'Censorship resistant'].map(
+ (label) => (
+ -
+
+ {label}
+
+ ),
+ )}
+
+
+ {/* CTAs */}
+
+
+
+ Read the Donor Guide
+
+
+
+
+ Read the Activist Guide
+
+
+
+
+
+ {/* Tilted preview card: sample campaign in the style of the
+ Soapbox Agora landing. Hidden on mobile so the hero
+ stays focused on the headline + CTAs. */}
+
+
+ {/* Orange halo behind the card */}
+
+
+ {/* Image header */}
+
+

+
+ {/* Country pill */}
+
+
+ 🇻🇪
+
+ Venezuela
+
+ {/* Public pill */}
+
+
+ Public
+
+
+ {/* Card body */}
+
+ {/* Org row */}
+
+
+ V
+
+
+ Venezolanos Libres
+
+
+
+
+ Free Venezuela's Political Prisoners
+
+
+ Legal defense and family support for 800+ political
+ prisoners detained by the Maduro regime.
+
+ {/* Progress bar */}
+
+ {/* Amount row */}
+
+
+
+ $8,420
+
+ raised
+
+
of $10,000
+
+
+ 247 donors · 12 countries
+
+ {/* Donate button */}
+
+
+ Donate Bitcoin
+
+
+
+
+
+
+
+
+
+ {/* ── 2. How it works, in three steps (light cream) ──────────────── */}
+
+
+ {/* ── 3. Two ways to get paid (white) ─────────────────────────────── */}
+
+
+
+
+
+ {/* Bitcoin Public Payments */}
+
}
+ title="Works with every wallet on earth."
+ description="Donations land at a regular Bitcoin address the activist controls. Anyone with any Bitcoin wallet can send. No new app, no new account, no learning curve."
+ bullets={[
+ 'Works in every Bitcoin wallet: Cash App, Coinbase, Strike, hardware',
+ 'Fastest settlement: Bitcoin confirmations only',
+ 'Verifiable on-chain: anyone can see what an activist has received',
+ ]}
+ tradeoffEmphasized
+ tradeoffTitle="Trade-off: Public on-chain."
+ tradeoffIntro={
+
+ Every donation is public on the Bitcoin blockchain and on
+ Nostr. {appName} is recommended only for above-ground
+ activism. If you or your donors require extreme privacy,
+ including from state actors, read the{' '}
+
+ Donor Guide
+ {' '}
+ and{' '}
+
+ Activist Guide
+ {' '}
+ before participating.
+
+ }
+ />
+ {/* Bitcoin Silent Payments */}
+
}
+ title="Unlinkable, on-chain, direct."
+ description="Donations are sent as BIP-352 silent payments. Each one lands at a fresh, unlinkable Bitcoin output that an observer staring at the blockchain can't tie back to the campaign."
+ bullets={[
+ "Donation trail can't be reconstructed on-chain",
+ 'Protects activists facing serious adversaries',
+ "Lands directly in the activist's wallet, no server in the middle",
+ ]}
+ tradeoffTitle="Trade-off: Silent payments are early."
+ tradeoffIntro={
+
+ The most private way to receive Bitcoin on-chain, but the
+ ecosystem is young and rough today:
+
+ }
+ tradeoffBullets={[
+
Few wallets support it.,
+
Receiving is slow.,
+
No push notifications.,
+
Wallets are still buggy.,
+
+ Donation counts aren't public.
+ ,
+ ]}
+ />
+
+
+ {/* No custody comparison block. The compare cards above show
+ the two options; this block answers the obvious next
+ question, "how is this actually different from existing
+ crowdfunding sites?", by name-checking the failure modes
+ of centralized and even other Bitcoin-based options. */}
+
+
+
+
+
+
+
+ No custody. No middleman.
+
+
+ {appName} never holds funds. Donations move
+ wallet-to-wallet on Bitcoin. There's no server standing
+ between donor and activist on either option. If {appName}{' '}
+ disappeared tomorrow, every campaign would keep working.
+
+
+
+
+ {/* Comparison grid */}
+
+
+
+
+
+ {/* ── 4. Need help? FAQ (cream, integrated as three chapters) ───── */}
+
+
+
+
+ {/* Three chapter mini-sections, rendered inline in page flow */}
+
+ {FAQ_CHAPTERS.map((chapter) => (
+
+ ))}
+
+
+
+
+ {/* ── 5. Pick the side you're on (white) ──────────────────────────── */}
+
+
+
+
+
+ }
+ accent="blue"
+ title="Support causes the banks won't."
+ description="Send Bitcoin directly to activists and movements anywhere in the world, without asking a payment processor for permission."
+ bullets={[
+ 'Use any Bitcoin wallet you already have.',
+ 'Donations land directly with the activist. No custodian, no middleman.',
+ 'For privacy, use a wallet that supports silent payments.',
+ ]}
+ cta="Read the Donor Guide"
+ />
+ }
+ accent="orange"
+ title="Get funded without permission."
+ description="Receive support directly from people who believe in your cause. No bank account, no application form, no company in the middle."
+ bullets={[
+ 'Start receiving donations as soon as you sign up.',
+ 'Pick which payment types to accept: public, silent payments, or both.',
+ 'For private cash-out, send to a silent-payments wallet first, then forward anywhere.',
+ ]}
+ cta="Read the Activist Guide"
+ />
+
+
+ {/* Page-closing 'Still stuck?' line: quiet pointer to the
+ Team Soapbox follow pack via the in-app /:nip19 route. */}
+
+ Still stuck?{' '}
+
+ Follow Team Soapbox on Nostr
+
+ . We triage questions there.
+
+
+
+
+ );
+}
+
+// ── Building blocks ───────────────────────────────────────────────────────
+
+/**
+ * Three FAQ chapters rendered in page flow. Category IDs match
+ * `helpContent.ts` so we can hand them to `HelpFAQSection` for
+ * rendering. Labels and descriptions are page-side copy, not the
+ * helpContent.ts labels (kept here so the chapter chrome stays
+ * editorial without coupling helpContent to the About page.
+ */
+const FAQ_CHAPTERS: Array<{
+ id: string;
+ number: string;
+ label: string;
+ description: string;
+}> = [
+ {
+ id: 'getting-started',
+ number: '01',
+ label: 'Getting started',
+ description: 'What Agora is, who built it, and what it costs.',
+ },
+ {
+ id: 'payments',
+ number: '02',
+ label: 'Bitcoin donations',
+ description: 'How payments work, why on-chain, why public, why these trade-offs.',
+ },
+ {
+ id: 'about-nostr',
+ number: '03',
+ label: 'About Nostr',
+ description: 'The open protocol Agora is built on, and how your account works.',
+ },
+];
+
+interface FAQChapterProps {
+ number: string;
+ title: string;
+ description: string;
+ categoryId: string;
+}
+
+/**
+ * A single chapter within the FAQ section: chapter numeral on the
+ * left, chapter heading + description, then the accordion items for
+ * the matching helpContent category underneath. Inline in page flow.
+ * no tabs, no JS state. Anchored via `id="faq-
"` so the chapter
+ * quick-jump strip can link directly to it.
+ */
+function FAQChapter({ number, title, description, categoryId }: FAQChapterProps) {
+ return (
+
+
+
+ {number}
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+
+ );
+}
+
+interface ComparisonItemProps {
+ heading: string;
+ body: string;
+ /** Defaults to light. On dark backgrounds, body copy switches to gray-300. */
+ theme?: 'light' | 'dark';
+}
+
+/**
+ * One cell in the "Unlike GoFundMe / GiveSendGo / Bitcoin" grid
+ * inside the No-Custody banner. Small heading + short body, no
+ * separating border (the parent grid gap is the separator).
+ */
+function ComparisonItem({ heading, body, theme = 'light' }: ComparisonItemProps) {
+ return (
+
+
+ {heading}
+
+
+ {body}
+
+
+ );
+}
+
+interface SectionHeaderProps {
+ eyebrow: string;
+ title: string;
+ lede?: string;
+ /** Light backgrounds use dark text; dark backgrounds invert. */
+ theme?: 'light' | 'dark';
+}
+
+function SectionHeader({
+ eyebrow,
+ title,
+ lede,
+ theme = 'light',
+}: SectionHeaderProps) {
+ return (
+
+
+ {eyebrow}
+
+ {/* Inter Bold, NOT Bebas Neue. The codebase's canonical section H2. */}
+
+ {title}
+
+ {lede && (
+
+ {lede}
+
+ )}
+
+ );
+}
+
+interface StepCardProps {
+ number: string;
+ image: string;
+ imageAlt: string;
+ title: string;
+ body: string;
+}
+
+function StepCard({ number, image, imageAlt, title, body }: StepCardProps) {
+ return (
+
+
+

+
+ {/* Step numeral: Bebas Neue is appropriate here as designed
+ signage, not a heading. font-normal, not bold. */}
+
+ {number}
+
+
+
+
+ );
+}
+
+interface RailCardProps {
+ accent: 'orange' | 'indigo';
+ kicker: string;
+ tagline: string;
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+ bullets: string[];
+ tradeoffTitle: string;
+ tradeoffIntro: React.ReactNode;
+ tradeoffBullets?: React.ReactNode[];
+ /** When true, the trade-off block uses an amber alert-style title icon. */
+ tradeoffEmphasized?: boolean;
+}
+
+function RailCard({
+ accent,
+ kicker,
+ tagline,
+ icon,
+ title,
+ description,
+ bullets,
+ tradeoffTitle,
+ tradeoffIntro,
+ tradeoffBullets,
+ tradeoffEmphasized,
+}: RailCardProps) {
+ const headerGradient =
+ accent === 'orange'
+ ? 'from-primary to-primary/80'
+ : 'from-indigo-600 to-indigo-700';
+ const taglineColor =
+ accent === 'orange' ? 'text-primary-foreground/85' : 'text-indigo-100';
+ const checkColor =
+ accent === 'orange' ? 'text-emerald-600' : 'text-indigo-600';
+
+ return (
+
+ {/* Gradient header strip */}
+
+
+ {icon}
+
+ {kicker}
+
+
+
{tagline}
+
+
+ {/* Body */}
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {bullets.map((b, i) => (
+ -
+
+ {b}
+
+ ))}
+
+
+ {/* Trade-off block */}
+
+
+ {tradeoffEmphasized ? (
+
+ ) : (
+
+ )}
+
+ {tradeoffTitle}
+
+
+
+ {tradeoffIntro}
+ {tradeoffBullets && tradeoffBullets.length > 0 && (
+
+ {tradeoffBullets.map((b, i) => (
+ -
+
+ {b}
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
+
+interface GuideCardProps {
+ to: string;
+ image: string;
+ imageAlt: string;
+ role: string;
+ icon: React.ReactNode;
+ accent: 'blue' | 'orange';
+ title: string;
+ description: string;
+ bullets: string[];
+ cta: string;
+}
+
+function GuideCard({
+ to,
+ image,
+ imageAlt,
+ role,
+ icon,
+ accent,
+ title,
+ description,
+ bullets,
+ cta,
+}: GuideCardProps) {
+ const accentText = accent === 'blue' ? 'text-blue-600' : 'text-primary';
+ const accentBg =
+ accent === 'blue'
+ ? 'bg-blue-600 hover:bg-blue-700'
+ : 'bg-primary hover:bg-primary/90';
+ return (
+
+ {/* Image header */}
+
+

+
+
+
+ {icon}
+
+
+ {role}
+
+
+
+
+ {/* Body */}
+
+
+ {title}
+
+
{description}
+
+ {bullets.map((b, i) => (
+ -
+
+ {b}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/pages/ActivistGuidePage.tsx b/src/pages/ActivistGuidePage.tsx
index 22e6d02e..ef9eaa17 100644
--- a/src/pages/ActivistGuidePage.tsx
+++ b/src/pages/ActivistGuidePage.tsx
@@ -1,20 +1,26 @@
import { useSeoMeta } from '@unhead/react';
-import { AlertTriangle } from 'lucide-react';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { GuideHero } from '@/components/GuideHero';
-import { GuideSectionCard } from '@/components/GuideSectionCard';
+import {
+ CalloutCard,
+ GuideProse,
+ GuideSteps,
+ GuideTLDR,
+ OptionGrid,
+ PaymentComparisonTable,
+} from '@/components/guide';
import { useAppContext } from '@/hooks/useAppContext';
import { DEFAULT_ACTION_COVERS } from '@/lib/defaultActionCovers';
-import { getActivistGuideSections } from '@/lib/helpContent';
+import { getActivistGuideBlocks, type GuideBlock } from '@/lib/helpContent';
import { HOPE_PALETTE } from '@/lib/hopePalette';
/**
- * Activist Guide — long-form companion to the Help page.
+ * Activist Guide. The long-form companion to the About 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.
+ * The page body is composed from a typed sequence of `GuideBlock`s
+ * defined in `src/lib/helpContent.ts`. Each block kind has a dedicated
+ * component; this page just hands each block to the right one. Linked
+ * from `/about` as one of the two large guide buttons.
*/
export function ActivistGuidePage() {
const { config } = useAppContext();
@@ -24,48 +30,48 @@ export function ActivistGuidePage() {
description: `How to receive donations on ${config.appName} and cash out privately.`,
});
- const sections = getActivistGuideSections(config.appName);
+ const blocks = getActivistGuideBlocks(config.appName);
return (
-
- {/* 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.
-
-
-
-
- {/* Sections */}
- {sections.map((section) => (
-
+
+ {blocks.map((block, i) => (
+
))}
);
}
+/** Dispatches a single block to the correct visual component. */
+function GuideBlockRenderer({ block }: { block: GuideBlock }) {
+ switch (block.kind) {
+ case 'tldr':
+ return
;
+ case 'steps':
+ return
;
+ case 'paymentComparison':
+ return
;
+ case 'callout':
+ return
;
+ case 'optionGrid':
+ return
;
+ case 'prose':
+ return
;
+ }
+}
+
/**
* 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,
+ * 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(
diff --git a/src/pages/CampaignsPage.tsx b/src/pages/CampaignsPage.tsx
index 4706fd23..d9ea3f83 100644
--- a/src/pages/CampaignsPage.tsx
+++ b/src/pages/CampaignsPage.tsx
@@ -218,7 +218,7 @@ export function CampaignsPage() {
asChild
className="rounded-full h-12 px-6 text-base border-white/30 bg-white/5 text-white hover:bg-white/10 hover:text-white hover:border-white/50 [&_svg]:size-[18px]"
>
-
+
How it works
diff --git a/src/pages/DonorGuidePage.tsx b/src/pages/DonorGuidePage.tsx
index 044d3834..1a1f1361 100644
--- a/src/pages/DonorGuidePage.tsx
+++ b/src/pages/DonorGuidePage.tsx
@@ -1,19 +1,25 @@
import { useSeoMeta } from '@unhead/react';
-import { AlertTriangle } from 'lucide-react';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { GuideHero } from '@/components/GuideHero';
-import { GuideSectionCard } from '@/components/GuideSectionCard';
+import {
+ CalloutCard,
+ GuideProse,
+ GuideSteps,
+ GuideTLDR,
+ OptionGrid,
+ PaymentComparisonTable,
+} from '@/components/guide';
import { useAppContext } from '@/hooks/useAppContext';
-import { getDonorGuideSections } from '@/lib/helpContent';
+import { getDonorGuideBlocks, type GuideBlock } from '@/lib/helpContent';
import { COOL_PALETTE } from '@/lib/hopePalette';
/**
- * Donor Guide — long-form companion to the Help page.
+ * Donor Guide. The long-form companion to the About 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.
+ * The page body is composed from a typed sequence of `GuideBlock`s
+ * defined in `src/lib/helpContent.ts`. Each block kind has a dedicated
+ * component; this page just hands each block to the right one. Linked
+ * from `/about` as one of the two large guide buttons.
*/
export function DonorGuidePage() {
const { config } = useAppContext();
@@ -23,46 +29,47 @@ export function DonorGuidePage() {
description: `How donating works on ${config.appName} and how to protect your privacy.`,
});
- const sections = getDonorGuideSections(config.appName);
+ const blocks = getDonorGuideBlocks(config.appName);
return (
-
- {/* 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.
-
-
-
-
- {/* Sections */}
- {sections.map((section) => (
-
+
+ {blocks.map((block, i) => (
+
))}
);
}
+/** Dispatches a single block to the correct visual component. */
+function GuideBlockRenderer({ block }: { block: GuideBlock }) {
+ switch (block.kind) {
+ case 'tldr':
+ return
;
+ case 'steps':
+ return
;
+ case 'paymentComparison':
+ return
;
+ case 'callout':
+ return
;
+ case 'optionGrid':
+ return
;
+ case 'prose':
+ return
;
+ }
+}
+
/**
* Hero images for the Donor Guide. Reuses the World Liberty Congress
- * event photos already in `/public/hero/` — they read as "community of
+ * 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.
diff --git a/src/pages/HelpPage.tsx b/src/pages/HelpPage.tsx
deleted file mode 100644
index 1f5b6b71..00000000
--- a/src/pages/HelpPage.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useSeoMeta } from '@unhead/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 { PageHeader } from '@/components/PageHeader';
-import { TeamSoapboxCard } from '@/components/TeamSoapboxCard';
-import { HelpFAQSection } from '@/components/HelpFAQSection';
-
-export function HelpPage() {
- const { config } = useAppContext();
-
- useSeoMeta({
- title: `Help | ${config.appName}`,
- 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."
- />
-
-
- {/* FAQ heading */}
-
-
Frequently Asked Questions
-
- Everything else you need to know about Nostr, {config.appName}, and how it all works.
-
-
-
- {/* FAQ accordion sections */}
-
-
- {/* Team Soapbox follow pack — at the end, after the FAQ */}
-
-
- {/* Privacy policy link */}
-
-
-
- Privacy Policy
-
-
-
- );
-}
-
-interface GuideButtonProps {
- to: string;
- icon: React.ReactNode;
- title: string;
- description: string;
-}
-
-function GuideButton({ to, icon, title, description }: GuideButtonProps) {
- return (
-
-
- {icon}
-
-
-
{title}
-
{description}
-
-
-
- );
-}