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}

+
+ + + ); +}