Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/Global/FAQs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
'use client'

import type { ReactNode } from 'react'

export type FAQsProps = {
heading: string
questions: Array<{
id: string
question: string
answer: string
/** Rich JSX answer body — rendered instead of `answer`, which still feeds SEO schemas */
answerContent?: ReactNode
redirectUrl?: string
redirectText?: string
calModal?: boolean
Expand Down Expand Up @@ -54,7 +58,7 @@ export function FAQsPanel({ heading, questions }: FAQsProps) {
</span>
</summary>
<div className="mt-4 text-lg font-semibold leading-6 text-n-1 md:text-xl">
<p className="whitespace-pre-line">{linkifyText(faq.answer)}</p>
{faq.answerContent ?? <p className="whitespace-pre-line">{linkifyText(faq.answer)}</p>}
{faq.calModal && (
<a
data-cal-link="kkonrad+hugo0/15min?duration=30"
Expand Down
17 changes: 15 additions & 2 deletions src/components/LandingPage/LandingPageClient.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client'

import { useFooterVisibility } from '@/context/footerVisibility'
import { Suspense, useEffect, useState, useRef, useCallback, type ReactNode } from 'react'
import { Suspense, useEffect, useMemo, useState, useRef, useCallback, type ReactNode } from 'react'
import { DropLink, FAQs, Hero, Marquee, NoFees, CardPioneers } from '@/components/LandingPage'
import { SupportedRailsFaqAnswer } from '@/components/LandingPage/SupportedRailsFaqAnswer'
import { SUPPORTED_RAILS_FAQ_ID } from '@/constants/faq.consts'
import TweetCarousel from '@/components/LandingPage/TweetCarousel'
import { StickyMobileCTA } from '@/components/LandingPage/StickyMobileCTA'
import underMaintenanceConfig from '@/config/underMaintenance.config'
Expand Down Expand Up @@ -51,6 +53,17 @@ export function LandingPageClient({
footerSlot,
}: LandingPageClientProps) {
const { isFooterVisible } = useFooterVisibility()

// Memoized: this component re-renders per scroll frame during the button
// animation — don't rebuild the FAQ array + rich answer element each time.
const faqQuestions = useMemo(
() =>
faqData.questions.map((q) =>
q.id === SUPPORTED_RAILS_FAQ_ID ? { ...q, answerContent: <SupportedRailsFaqAnswer /> } : q
),
[faqData.questions]
)

const [buttonVisible, setButtonVisible] = useState(true)
const [isScrollFrozen, setIsScrollFrozen] = useState(false)
const [buttonScale, setButtonScale] = useState(1)
Expand Down Expand Up @@ -215,7 +228,7 @@ export function LandingPageClient({
<NoFees />
</Suspense>
<Marquee {...marqueeProps} />
<FAQs heading={faqData.heading} questions={faqData.questions} marquee={faqData.marquee} />
<FAQs heading={faqData.heading} questions={faqQuestions} marquee={faqData.marquee} />
<Marquee {...marqueeProps} />
{footerSlot}
<StickyMobileCTA />
Expand Down
56 changes: 56 additions & 0 deletions src/components/LandingPage/SupportedRailsFaqAnswer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'

import ChainChip from '@/components/AddMoney/components/ChainChip'
import { CHAIN_LOGOS, OTHER_SUPPORTED_CHAINS, SUPPORTED_EVM_CHAINS, getSupportedTokens } from '@/constants/rhino.consts'
import { FIAT_RAILS } from '@/constants/faq.consts'
import { chainDisplayName } from '@/utils/chain-display.utils'

/**
* Rich answer body for the "which networks, tokens and banks?" landing FAQ item.
* Renders from the same rhino.consts constants as the add-money Choose Network
* drawer (and FIAT_RAILS shared with the plain-text SEO answer), so the FAQ
* always advertises exactly what the app supports.
*/
export function SupportedRailsFaqAnswer() {
return (
<div className="flex flex-col gap-5">
<div>
<p className="mb-2">
Crypto — one deposit address for all {SUPPORTED_EVM_CHAINS.length} EVM networks, plus{' '}
{OTHER_SUPPORTED_CHAINS.map(chainDisplayName).join(' and ')}:
</p>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div className="flex flex-wrap gap-1 rounded-sm border border-n-1 bg-white p-2">
{[...SUPPORTED_EVM_CHAINS, ...OTHER_SUPPORTED_CHAINS].map((chain) => (
<ChainChip key={chain} chainName={chainDisplayName(chain)} chainSymbol={CHAIN_LOGOS[chain]} />
))}
</div>
</div>
<div>
<p className="mb-2">Tokens:</p>
<div className="flex flex-wrap gap-1 rounded-sm border border-n-1 bg-white p-2">
{getSupportedTokens('EVM').map((token) => (
<ChainChip key={token.name} chainName={token.name} chainSymbol={token.logoUrl} />
))}
</div>
<p className="mt-2 text-base text-grey-1">
USDC & USDT on every network · ETH on EVM networks · Tron is USDT-only
</p>
</div>
<div>
<p className="mb-2">Banks & local payment apps:</p>
<ul className="flex flex-col gap-1.5">
{FIAT_RAILS.map((rail) => (
<li key={rail.name} className="flex items-baseline gap-2">
<span>{rail.flag}</span>
<span>{rail.name}</span>
<span className="text-base text-grey-1">
{rail.currency} · {rail.region}
</span>
</li>
))}
</ul>
</div>
<p className="text-base text-grey-1">Deposits are free — Peanut covers the gas.</p>
</div>
)
}
32 changes: 32 additions & 0 deletions src/constants/faq.consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { OTHER_SUPPORTED_CHAINS, SUPPORTED_EVM_CHAINS } from '@/constants/rhino.consts'
import { chainDisplayName } from '@/utils/chain-display.utils'

/**
* The "which networks, tokens and banks?" landing FAQ item. Question, fiat-rail
* facts and plain-text answer live here (server-safe, feeds the FAQPage JSON-LD
* via getLandingContent); the rich chip UI is rendered client-side by
* SupportedRailsFaqAnswer, matched by id. Chain names derive from rhino.consts
* and the visible rails render from FIAT_RAILS, so the public answer and the
* on-page UI share one source and can't drift from what the app supports.
*/
export const SUPPORTED_RAILS_FAQ_ID = 'supported-rails'

export const FIAT_RAILS = [
{ flag: '🇺🇸', name: 'ACH & Wire', currency: 'USD', region: 'United States' },
{ flag: '🇪🇺', name: 'SEPA', currency: 'EUR', region: '36 countries' },
{ flag: '🇬🇧', name: 'Faster Payments', currency: 'GBP', region: 'United Kingdom' },
{ flag: '🇲🇽', name: 'SPEI', currency: 'MXN', region: 'Mexico' },
{ flag: '🇦🇷', name: 'Mercado Pago', currency: 'ARS', region: 'Argentina' },
{ flag: '🇧🇷', name: 'Pix', currency: 'BRL', region: 'Brazil' },
] as const

const EVM_CHAIN_LIST = SUPPORTED_EVM_CHAINS.map(chainDisplayName).join(', ')
const OTHER_CHAIN_LIST = OTHER_SUPPORTED_CHAINS.map(chainDisplayName).join(' and ')
const FIAT_RAIL_LIST = FIAT_RAILS.map((rail) => `${rail.name} (${rail.currency}, ${rail.region})`).join(', ')

export const SUPPORTED_RAILS_FAQ_QUESTION = 'Which networks, tokens and banks does Peanut support?'

export const SUPPORTED_RAILS_FAQ_ANSWER =
`Crypto: deposit and withdraw USDC and USDT on ${SUPPORTED_EVM_CHAINS.length} EVM networks with a single address (${EVM_CHAIN_LIST}), plus ${OTHER_CHAIN_LIST}. ETH is also supported on EVM networks; Tron is USDT-only. ` +
`Banks & local payment apps: ${FIAT_RAIL_LIST}. ` +
'Deposits are free — Peanut covers the gas.'
28 changes: 28 additions & 0 deletions src/lib/landingContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
// component or route handler, never inside a 'use client' file.

import { readSingletonContentLocalized } from '@/lib/content'
import {
SUPPORTED_RAILS_FAQ_ANSWER,
SUPPORTED_RAILS_FAQ_ID,
SUPPORTED_RAILS_FAQ_QUESTION,
} from '@/constants/faq.consts'
import type { Locale } from '@/i18n/types'

interface LandingFrontmatter {
Expand Down Expand Up @@ -40,7 +45,30 @@ const DEFAULTS: LandingContent = {
marqueeMessages: [],
}

// Code-defined FAQ item advertising supported networks/tokens/bank rails.
// Lives in code (not the content MD) because its facts derive from
// rhino.consts — the same constants the add-money flow renders — so the
// public answer can't drift from what the app actually supports.
// LandingPageClient swaps in the rich chip UI by this id.
const SUPPORTED_RAILS_QUESTION = {
id: SUPPORTED_RAILS_FAQ_ID,
question: SUPPORTED_RAILS_FAQ_QUESTION,
answer: SUPPORTED_RAILS_FAQ_ANSWER,
}

// Insert right after the "What is Peanut?" question (falls back to the end).
function withSupportedRails(questions: LandingContent['faqData']['questions']) {
const idx = questions.findIndex((q) => /what is peanut\??/i.test(q.question))
const at = idx === -1 ? questions.length : idx + 1
return [...questions.slice(0, at), SUPPORTED_RAILS_QUESTION, ...questions.slice(at)]
}

export function getLandingContent(locale: Locale = 'en'): LandingContent {
const base = readLandingContent(locale)
return { ...base, faqData: { ...base.faqData, questions: withSupportedRails(base.faqData.questions) } }
}
Comment thread
Hugo0 marked this conversation as resolved.

function readLandingContent(locale: Locale): LandingContent {
const content = readSingletonContentLocalized<LandingFrontmatter>('landing', locale)
if (!content) return DEFAULTS

Expand Down
8 changes: 8 additions & 0 deletions src/utils/chain-display.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Display labels where plain title-case reads wrong.
const CHAIN_DISPLAY_OVERRIDES: Record<string, string> = {
BNB: 'BNB Chain',
}

/** rhino.consts chain key (e.g. 'ARBITRUM', 'BNB') → human display name */
export const chainDisplayName = (chain: string): string =>
CHAIN_DISPLAY_OVERRIDES[chain] ?? chain.charAt(0) + chain.slice(1).toLowerCase()
Loading