From c4a9825c8e25b442fd1c09cb674648cb4b789080 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:33:01 -0400 Subject: [PATCH 01/26] feat(ipfs2filecoin): add IPFS to Filecoin campaign landing page Adds filecoin.cloud/ipfs2filecoin, the front door for the IPFS-to-Filecoin mini-campaign, ported from Jenni's wireframe in FilOzone/FOC-PM onto the site's existing ui-filecoin components. The page is both the campaign's front door and its measuring instrument. It does three things: takes a pasted CID list, produces a deposit estimate from a declared volume, and routes to the agent door or to contact. Interactive parts run entirely client-side, with no wallet and no network calls, so a check costs the visitor nothing and sends us nothing: - CID list checker: validates, dedupes, and steers lists over the 500-item browser cap to the agent prompt. - Deposit estimator: reproduces the PRD's worked example exactly ($128.08 deposit, $5.25 refundable for 1 TiB over two years) from documented constants rather than hardcoded figures. - Agent prompt with copy-to-clipboard, the primary conversion. Plausible events cover the funnel the PRD asks for: CID List Checked, Estimate Viewed, and Agent Prompt Copied, the last being the headline number. Also serves a draft migration brief at /ipfs2filecoin/llms.txt and advertises it via , so an agent pointed at the human URL discovers the brief without being told the convention. The brief opens with a stage 0 ingress check, per the 2026-07-28 validation run that found runs die there after the expensive commP pass. The page is unlinked from site navigation, reachable by URL and sitemap only, so campaign attribution stays clean. --- .../ipfs2filecoin/components/AgentPrompt.tsx | 42 +++ .../components/CidListChecker.tsx | 191 +++++++++++++ .../components/ComparisonTable.tsx | 56 ++++ .../components/CostEstimator.tsx | 225 +++++++++++++++ src/app/ipfs2filecoin/constants/migration.ts | 40 +++ src/app/ipfs2filecoin/constants/seo.ts | 5 + src/app/ipfs2filecoin/data/faqs.tsx | 109 +++++++ src/app/ipfs2filecoin/data/limits.ts | 31 ++ .../ipfs2filecoin/data/pricing-comparison.ts | 37 +++ src/app/ipfs2filecoin/data/reasons.ts | 22 ++ src/app/ipfs2filecoin/data/steps.ts | 26 ++ src/app/ipfs2filecoin/llms.txt/route.ts | 121 ++++++++ src/app/ipfs2filecoin/page.tsx | 267 ++++++++++++++++++ src/app/ipfs2filecoin/utils/estimate-cost.ts | 78 +++++ .../utils/generate-structured-data.ts | 23 ++ src/app/ipfs2filecoin/utils/parse-cid-list.ts | 40 +++ src/constants/paths.ts | 4 + 17 files changed, 1317 insertions(+) create mode 100644 src/app/ipfs2filecoin/components/AgentPrompt.tsx create mode 100644 src/app/ipfs2filecoin/components/CidListChecker.tsx create mode 100644 src/app/ipfs2filecoin/components/ComparisonTable.tsx create mode 100644 src/app/ipfs2filecoin/components/CostEstimator.tsx create mode 100644 src/app/ipfs2filecoin/constants/migration.ts create mode 100644 src/app/ipfs2filecoin/constants/seo.ts create mode 100644 src/app/ipfs2filecoin/data/faqs.tsx create mode 100644 src/app/ipfs2filecoin/data/limits.ts create mode 100644 src/app/ipfs2filecoin/data/pricing-comparison.ts create mode 100644 src/app/ipfs2filecoin/data/reasons.ts create mode 100644 src/app/ipfs2filecoin/data/steps.ts create mode 100644 src/app/ipfs2filecoin/llms.txt/route.ts create mode 100644 src/app/ipfs2filecoin/page.tsx create mode 100644 src/app/ipfs2filecoin/utils/estimate-cost.ts create mode 100644 src/app/ipfs2filecoin/utils/generate-structured-data.ts create mode 100644 src/app/ipfs2filecoin/utils/parse-cid-list.ts diff --git a/src/app/ipfs2filecoin/components/AgentPrompt.tsx b/src/app/ipfs2filecoin/components/AgentPrompt.tsx new file mode 100644 index 00000000..1569d9f6 --- /dev/null +++ b/src/app/ipfs2filecoin/components/AgentPrompt.tsx @@ -0,0 +1,42 @@ +'use client' + +import { Button } from '@filecoin-foundation/ui-filecoin/Button' +import { usePlausible } from 'next-plausible' + +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' + +import { AGENT_PROMPT, PLAUSIBLE_EVENTS } from '../constants/migration' + +type AgentPromptProps = { + /** Where on the page the prompt was copied from, so the two spots stay distinguishable. */ + source: 'verdict' | 'agent-door' +} + +export function AgentPrompt({ source }: AgentPromptProps) { + const { copy, isCopied } = useCopyToClipboard() + const plausible = usePlausible() + + async function handleCopy() { + const copied = await copy(AGENT_PROMPT) + + if (copied) { + plausible(PLAUSIBLE_EVENTS.promptCopied, { props: { source } }) + } + } + + return ( +
+ + {AGENT_PROMPT} + + + + + + {isCopied ? 'Prompt copied to clipboard' : ''} + +
+ ) +} diff --git a/src/app/ipfs2filecoin/components/CidListChecker.tsx b/src/app/ipfs2filecoin/components/CidListChecker.tsx new file mode 100644 index 00000000..14e25136 --- /dev/null +++ b/src/app/ipfs2filecoin/components/CidListChecker.tsx @@ -0,0 +1,191 @@ +'use client' + +import { Button } from '@filecoin-foundation/ui-filecoin/Button' +import { SmartTextLink } from '@filecoin-foundation/ui-filecoin/TextLink/SmartTextLink' +import { Field, Label, Textarea } from '@headlessui/react' +import { usePlausible } from 'next-plausible' +import { useId, useState } from 'react' + +import { PATHS } from '@/constants/paths' + +import { AgentPrompt } from './AgentPrompt' +import { + BROWSER_CHECK_ITEM_CAP, + MAX_ITEM_SIZE_LABEL, + PLAUSIBLE_EVENTS, +} from '../constants/migration' +import { type CidListSummary, parseCidList } from '../utils/parse-cid-list' + +const PLACEHOLDER = [ + 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi', + 'bafkreieq5jui4j25lacwomsqgjeswwl3y5zcdrresptwgmfylxo2depppq', + 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG', +].join('\n') + +type Verdict = + | { kind: 'empty' } + | { kind: 'unreadable'; summary: CidListSummary } + | { kind: 'ok'; summary: CidListSummary } + | { kind: 'over-cap'; summary: CidListSummary } + +function getVerdict(summary: CidListSummary): Verdict { + if (summary.totalLines === 0) { + return { kind: 'empty' } + } + if (summary.uniqueCids.length === 0) { + return { kind: 'unreadable', summary } + } + if (summary.uniqueCids.length > BROWSER_CHECK_ITEM_CAP) { + return { kind: 'over-cap', summary } + } + return { kind: 'ok', summary } +} + +function pluralize(count: number, singular: string, plural = `${singular}s`) { + return count === 1 ? singular : plural +} + +function buildNotes({ invalidCount, duplicateCount }: CidListSummary) { + const notes: Array = [] + + if (invalidCount > 0) { + notes.push( + `${invalidCount} ${pluralize(invalidCount, 'line')} could not be read as a CID and will be skipped`, + ) + } + if (duplicateCount > 0) { + notes.push( + `${duplicateCount} ${pluralize(duplicateCount, 'duplicate')} removed`, + ) + } + + return notes.length > 0 ? `${notes.join('. ')}.` : null +} + +export function CidListChecker() { + const inputId = useId() + const plausible = usePlausible() + const [value, setValue] = useState('') + const [verdict, setVerdict] = useState(null) + + function handleCheck() { + const summary = parseCidList(value) + const next = getVerdict(summary) + setVerdict(next) + + if (next.kind === 'empty') { + return + } + + plausible(PLAUSIBLE_EVENTS.cidListChecked, { + props: { + outcome: next.kind, + cidCount: summary.uniqueCids.length, + }, + }) + } + + return ( +
+ + +