diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..845cc6e1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Codeowners per the FOC Engineering Workstreams proposal. +# https://app.notion.com/p/filecoindev/FOC-Engineering-Workstreams-342dc41950c1818f99d2e857a7f3cb2e +* @TippyFlitsUK @rjan90 diff --git a/.gitignore b/.gitignore index 4fc5fa81..fc1263e7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ next-env.d.ts # Local test fixtures, not part of the site dapper-labs-nba-topshots-cids.txt +CLAUDE.md +todo.md diff --git a/next.config.ts b/next.config.ts index d7543fb6..0416fa00 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,6 +19,17 @@ const nextConfig: NextConfig = { images: { qualities: [75, 100], }, + async redirects() { + return [ + // Page temporarily hidden; route code kept at src/app/service-providers. + // Remove this entry to restore it. + { + source: '/service-providers', + destination: '/', + permanent: false, + }, + ] + }, webpack: (config) => { config.module.rules.push(svgrRule) config.module.rules.push(markdownRule) diff --git a/package-lock.json b/package-lock.json index 6b7a4a59..68fbadd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@filecoin-foundation/ui-filecoin": "^0.9.0", - "@filoz/synapse-sdk": "^1.0.1", + "@filoz/synapse-sdk": "^1.1.1", "@headlessui/react": "^2.2.9", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.12", @@ -2259,9 +2259,9 @@ } }, "node_modules/@filoz/synapse-core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@filoz/synapse-core/-/synapse-core-0.7.0.tgz", - "integrity": "sha512-54G+yH1DrqXpCrtSZF5SYvNma3W2iA/LH6hLWmADy5tW1CkPlvtHZ5essqrJtlk3lfNRYMWk3+13ejzXxDAWWw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@filoz/synapse-core/-/synapse-core-0.7.1.tgz", + "integrity": "sha512-4vZZJbumv64ZKR+BjfhLJvP57ga/GhSHy/k2jIWuXInXGwM5qPmKvaCS7CkpzQwiSIXahO161DBNRLVH8SrVfQ==", "license": "Apache-2.0 OR MIT", "dependencies": { "dnum": "^2.15.0", @@ -2312,9 +2312,9 @@ } }, "node_modules/@filoz/synapse-core/node_modules/ox": { - "version": "0.14.29", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.29.tgz", - "integrity": "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==", + "version": "0.14.33", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", + "integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==", "funding": [ { "type": "github", @@ -2342,12 +2342,12 @@ } }, "node_modules/@filoz/synapse-sdk": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@filoz/synapse-sdk/-/synapse-sdk-1.0.1.tgz", - "integrity": "sha512-zUAFPVPit9CNcOfjzv4oH+zqj0FkyVHJkXbeWpuJo4ZQxlCTieILpgEn7zR5LFnGKBpukzM0iIzW1mOEqGZ6jA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@filoz/synapse-sdk/-/synapse-sdk-1.1.1.tgz", + "integrity": "sha512-m+DqCbjTID2VDBGa2iFQqmF7xjTVoANQjsdk54ikNSolZDLAvZK3TtoArlp0CKp9/L67bAw5ptm4SLJJPE+Rmw==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@filoz/synapse-core": "^0.7.0", + "@filoz/synapse-core": "^0.7.1", "multiformats": "^14.0.0" }, "peerDependencies": { @@ -5013,9 +5013,9 @@ } }, "node_modules/dot-prop": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", - "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz", + "integrity": "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw==", "license": "MIT", "dependencies": { "type-fest": "^5.0.0" @@ -5280,9 +5280,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -5643,9 +5643,9 @@ } }, "node_modules/idb-keyval": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.5.tgz", - "integrity": "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", "license": "Apache-2.0" }, "node_modules/import-fresh": { @@ -5903,9 +5903,9 @@ } }, "node_modules/kysely": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.2.tgz", - "integrity": "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==", + "version": "0.29.4", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.4.tgz", + "integrity": "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA==", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -7089,9 +7089,9 @@ "license": "MIT" }, "node_modules/multiformats": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-14.0.0.tgz", - "integrity": "sha512-iWK1RrAS58p2NDfeZFuSUSv3ZPewTIhsGbh/5NgeGGJwJmRljLxGtjRR3nkn+loG3zl+IrfR/W1590QnrSK+Gg==", + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-14.0.5.tgz", + "integrity": "sha512-vbIm83F2yZ1pWJGS0yl0ysracIvv56LtbrIyiIQHoLdYDJOMoLfVFsXhh9DUH4SFdkdkFhucyWniihsNzVEjkQ==", "license": "Apache-2.0 OR MIT" }, "node_modules/nanoid": { @@ -7350,9 +7350,9 @@ } }, "node_modules/p-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", - "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.1.tgz", + "integrity": "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==", "license": "MIT", "dependencies": { "yocto-queue": "^1.2.1" @@ -7380,9 +7380,9 @@ } }, "node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.4", @@ -8315,9 +8315,9 @@ "license": "MIT" }, "node_modules/type-fest": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", - "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" diff --git a/package.json b/package.json index 09ca7352..fb50a5c3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@filecoin-foundation/ui-filecoin": "^0.9.0", - "@filoz/synapse-sdk": "^1.0.1", + "@filoz/synapse-sdk": "^1.1.1", "@headlessui/react": "^2.2.9", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.12", diff --git a/src/app/(homepage)/data/filecoin-onchain-cloud-products.ts b/src/app/(homepage)/data/filecoin-onchain-cloud-products.ts index 13aa1a3e..39dd5a93 100644 --- a/src/app/(homepage)/data/filecoin-onchain-cloud-products.ts +++ b/src/app/(homepage)/data/filecoin-onchain-cloud-products.ts @@ -4,7 +4,6 @@ import { PATHS } from '@/constants/paths' import { FIL_BEAM_URL, FOC_URLS } from '@/constants/site-metadata' import aurora from '@/public/assets/aurora.webp' import spaceStation from '@/public/assets/space-station.webp' -import spiralGalaxy from '@/public/assets/spiral-galaxy.webp' import stellarExplosionNebula from '@/public/assets/stellar-explosion-nebula.webp' const CTA_TEXT = 'Learn more' @@ -49,14 +48,4 @@ export const filecoinOnchainCloudProducts: Array = [ alt: "View of colorful aurora over Earth's atmosphere seen from space with part of a spacecraft arm visible.", }, }, - { - title: 'Service Providers', - description: - 'A global network of operators running Filecoin Onchain Cloud nodes delivering resilient, high-availability storage infrastructure.', - cta: { href: PATHS.SERVICE_PROVIDERS.path, text: CTA_TEXT }, - image: { - data: spiralGalaxy, - alt: 'Bright spiral galaxy with glowing core and extended spiral arms filled with stars.', - }, - }, ] as const satisfies Array diff --git a/src/app/(homepage)/page.tsx b/src/app/(homepage)/page.tsx index 21ce3f19..0306dfdf 100644 --- a/src/app/(homepage)/page.tsx +++ b/src/app/(homepage)/page.tsx @@ -143,7 +143,7 @@ export default function Homepage() { title="Compose the Building Blocks" description="Modular services you can mix, match, and deploy, all built for openness, performance, and control." > - + {filecoinOnchainCloudProducts.map( ({ title, description, cta, image }) => ( } -export function AgentPrompt({ source }: AgentPromptProps) { +export function AgentPrompt({ source, cids }: AgentPromptProps) { const { copy, isCopied } = useCopyToClipboard() const plausible = usePlausible() + const prompt = buildAgentPrompt(cids) + async function handleCopy() { - const copied = await copy(AGENT_PROMPT) + const copied = await copy(prompt) if (copied) { - plausible(PLAUSIBLE_EVENTS.promptCopied, { props: { source } }) + plausible(PLAUSIBLE_EVENTS.promptCopied, { + props: { source, cidCount: cids?.length ?? 0 }, + }) } } return ( -
- - {AGENT_PROMPT} +
+ + {prompt} - + {isCopied ? 'Prompt copied to clipboard' : ''} diff --git a/src/app/ipfs2filecoin/components/CidListChecker.tsx b/src/app/ipfs2filecoin/components/CidListChecker.tsx index 14e25136..e800d1e7 100644 --- a/src/app/ipfs2filecoin/components/CidListChecker.tsx +++ b/src/app/ipfs2filecoin/components/CidListChecker.tsx @@ -1,75 +1,106 @@ '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 { Description, Field, Label, Textarea } from '@headlessui/react' import { usePlausible } from 'next-plausible' -import { useId, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' -import { PATHS } from '@/constants/paths' - -import { AgentPrompt } from './AgentPrompt' +import { CidListVerdict, getVerdict, type Verdict } from './CidListVerdict' 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 } -} +import { parseCidList } from '../utils/parse-cid-list' +import { pluralize } from '../utils/pluralize' + +/** + * One line, not three. Three full CIDs fill the field edge to edge and read as + * a list already pasted; a single dim example reads as the prompt it is. The + * field keeps its height from `min-h`, so it still invites a list. + */ +const PLACEHOLDER = + 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi' + +/** The free-check facts, broken out so they scan as features rather than fine print. */ +const CHECK_FACTS = [ + 'Free, no wallet required', + `Up to ${BROWSER_CHECK_ITEM_CAP.toLocaleString()} items`, + `${MAX_ITEM_SIZE_LABEL} per item`, + 'Runs in your browser', + 'Never sent anywhere', +] + +/** + * The submit shortcut accepts Cmd or Ctrl, so the chip has to name whichever + * one this visitor actually has. Resolved after mount rather than rendered on + * the server, which has no way to know: `null` until then keeps the first + * client render identical to the server's and avoids a hydration mismatch. + */ +function useShortcutLabel() { + const [label, setLabel] = useState(null) + + useEffect(() => { + const isApple = /mac|iphone|ipad|ipod/i.test( + navigator.platform || navigator.userAgent, + ) + setLabel(isApple ? '⌘ ↵' : 'Ctrl ↵') + }, []) -function pluralize(count: number, singular: string, plural = `${singular}s`) { - return count === 1 ? singular : plural + return label } -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`, - ) +/** + * What the parser makes of the list as it is typed, so the button confirms a + * result the user can already see rather than being the first sign of one. + */ +function describeProgress( + totalLines: number, + recognizedCount: number, +): string | null { + if (totalLines === 0) { + return null } - if (duplicateCount > 0) { - notes.push( - `${duplicateCount} ${pluralize(duplicateCount, 'duplicate')} removed`, - ) + if (recognizedCount === 0) { + return 'No CIDs recognized yet' + } + if (recognizedCount === totalLines) { + return `${recognizedCount.toLocaleString()} ${pluralize(recognizedCount, 'CID')}` } - return notes.length > 0 ? `${notes.join('. ')}.` : null + return `${recognizedCount.toLocaleString()} of ${totalLines.toLocaleString()} lines are CIDs` } export function CidListChecker() { - const inputId = useId() const plausible = usePlausible() const [value, setValue] = useState('') const [verdict, setVerdict] = useState(null) + const verdictRef = useRef(null) + + const shortcutLabel = useShortcutLabel() + const summary = useMemo(() => parseCidList(value), [value]) + const hasInput = value.trim().length > 0 + const progress = describeProgress( + summary.totalLines, + summary.uniqueCids.length, + ) + + /** + * The panel lands below the fold on a laptop, so a check would otherwise look + * like it did nothing. Aligning its bottom edge reveals the whole result while + * leaving the list itself in view, which `nearest` does not: for an element + * taller than the gap below it, the minimum scroll is barely any scroll. + */ + useEffect(() => { + if (verdict) { + verdictRef.current?.scrollIntoView({ + block: 'end', + behavior: 'smooth', + }) + } + }, [verdict]) function handleCheck() { - const summary = parseCidList(value) const next = getVerdict(summary) setVerdict(next) @@ -88,104 +119,88 @@ export function CidListChecker() { return (
- -