diff --git a/.env.local.sample b/.env.local.sample index babd5ea..578026a 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -3,4 +3,10 @@ NEXT_PUBLIC_ONRAMPER_URL=https://buy.onramper.com/?apiKey=pk_prod_01HWMA66BYRB22 NEXT_PUBLIC_STRAPI_URL=https://strapi.shapeshift.com NEXT_PUBLIC_WEGLOT_API_KEY=obtain-from-engineering -STRAPI_API_TOKEN=obtain-from-engineering \ No newline at end of file +STRAPI_API_TOKEN=obtain-from-engineering + +# Public Reown project ID shared with the swap-widget service. Set before building. +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=69a04035cb588540ee2eb062c394a87a + +# ShapeShift registered affiliate code; confirm the payout account before configuring. +NEXT_PUBLIC_SHAPESHIFT_PARTNER_CODE= diff --git a/app/[lang]/_components/Footer.tsx b/app/[lang]/_components/Footer.tsx index 97b4bc4..de75263 100644 --- a/app/[lang]/_components/Footer.tsx +++ b/app/[lang]/_components/Footer.tsx @@ -30,7 +30,7 @@ export function Footer(): ReactNode { {/* Middle row: Menu items */}
-
+
{Object.entries(footerLinks).map(([category, links]) => (

{category}

diff --git a/app/[lang]/_components/header/DesktopHeader.tsx b/app/[lang]/_components/header/DesktopHeader.tsx index e2e06f5..d9f00ce 100644 --- a/app/[lang]/_components/header/DesktopHeader.tsx +++ b/app/[lang]/_components/header/DesktopHeader.tsx @@ -15,6 +15,7 @@ import { dAppUrl, headerTabs } from '@/app/[lang]/_utils/constants' import { containerAnimation } from './animations' import { DAOExpand } from './DaoExpand' +import { DevelopersExpand } from './DevelopersExpand' import { LanguageExpand } from './LanguageExpand' import { ProductsExpand } from './ProductsExpand' import { ResourcesExpand } from './ResourcesExpand' @@ -41,6 +42,7 @@ export function DesktopHeader({ className, switchLanguageAction, currentLanguage const tabContent: Record = useMemo( () => ({ products: , + developers: , resources: , dao: , language: , @@ -85,11 +87,18 @@ export function DesktopHeader({ className, switchLanguageAction, currentLanguage key={tab.name} onMouseEnter={() => setCurrentTab(tab.value)} className={cl( - 'cursor-pointer p-4 text-sm font-medium transition-colors', + 'text-sm font-medium transition-colors', + tab.value === 'developers' ? '' : 'cursor-pointer p-4', currentTab && currentTab !== tab.value ? 'text-gray-500' : 'text-white' )} > - {tab.name} + {tab.value === 'developers' ? ( + + {tab.name} + + ) : ( + tab.name + )}
))} diff --git a/app/[lang]/_components/header/DevelopersExpand.tsx b/app/[lang]/_components/header/DevelopersExpand.tsx new file mode 100644 index 0000000..70c38a0 --- /dev/null +++ b/app/[lang]/_components/header/DevelopersExpand.tsx @@ -0,0 +1,75 @@ +import { motion } from 'framer-motion' + +import { Button } from '@/app/[lang]/_components/Button' +import { LocalizedLink } from '@/app/[lang]/_components/LocalizedLink' +import { appDevelopers, developerDocsUrl } from '@/app/[lang]/_utils/constants' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import { expandAnimation } from './animations' + +import type { TAppLink } from '@/app/[lang]/_utils/constants' +import type { ReactNode } from 'react' + +function DeveloperItem({ + name, + href, + target, + description, + icon, + onClick, +}: TAppLink & { onClick: () => void }): ReactNode { + return ( + +
{icon}
+
+ {name} + {description && ( + {description} + )} +
+
+ ) +} + +export function DevelopersExpand({ setCurrentTab }: { setCurrentTab: (tab: string) => void }): ReactNode { + return ( + +
+

+ {DEVELOPERS_DICT.expand.titleLine1}
+ {DEVELOPERS_DICT.expand.titleLine2} +

+

{DEVELOPERS_DICT.expand.description}

+
+ +
+
+ {appDevelopers.map((developerResource) => ( + setCurrentTab('empty')} + key={developerResource.name} + name={developerResource.name} + href={developerResource.href} + description={developerResource.description} + icon={developerResource.icon} + target={developerResource.target} + /> + ))} +
+
+
+ ) +} diff --git a/app/[lang]/_components/header/MobileHeader.tsx b/app/[lang]/_components/header/MobileHeader.tsx index a4e8270..7db3717 100644 --- a/app/[lang]/_components/header/MobileHeader.tsx +++ b/app/[lang]/_components/header/MobileHeader.tsx @@ -11,7 +11,7 @@ import { IconClose } from '@/app/[lang]/_icons/IconClose' import { IconMenu } from '@/app/[lang]/_icons/IconMenu' import { IconPlanet } from '@/app/[lang]/_icons/IconPlanet' import { ShapeshiftLogo } from '@/app/[lang]/_icons/ShapeshiftLogo' -import { appDao, appProducts, appResources, headerTabs } from '@/app/[lang]/_utils/constants' +import { appDao, appDevelopers, appProducts, appResources, headerTabs } from '@/app/[lang]/_utils/constants' import { SUPPORTED_LANGUAGES } from '@/app/[lang]/_utils/i18nconfig' import type { TAppLink } from '@/app/[lang]/_utils/constants' @@ -23,6 +23,11 @@ const mobileTabs: { name: string; value: string; items: TAppLink[] }[] = [ value: 'products', items: appProducts.slice(0, 6), }, + { + name: 'Developers', + value: 'developers', + items: appDevelopers.slice(0, 6), + }, { name: 'Resources', value: 'resources', @@ -111,17 +116,42 @@ export function MobileHeader({
{headerTabs.map((tab) => (
-
- + ) : ( + + )} {expandedSection === tab.value && ( diff --git a/app/[lang]/_icons/IconSettings.tsx b/app/[lang]/_icons/IconSettings.tsx new file mode 100644 index 0000000..0c04aa4 --- /dev/null +++ b/app/[lang]/_icons/IconSettings.tsx @@ -0,0 +1,18 @@ +import type { ReactNode, SVGProps } from 'react' + +export function IconSettings(props: SVGProps): ReactNode { + return ( + + + + + ) +} diff --git a/app/[lang]/_utils/constants.tsx b/app/[lang]/_utils/constants.tsx index a29174b..434045a 100644 --- a/app/[lang]/_utils/constants.tsx +++ b/app/[lang]/_utils/constants.tsx @@ -39,6 +39,10 @@ export const dAppUrl = const docsUrl = 'https://github.com/shapeshift/web' +export const developerDocsUrl = 'https://api.shapeshift.com/docs' + +export const developerDocsSectionUrl = (section: string): string => `${developerDocsUrl}${section}` + const githubUrl = 'https://github.com/shapeshift' export const footerButtonTitle = 'Donate' @@ -226,6 +230,51 @@ export const appResources: TAppLink[] = [ }, ] +export const appDevelopers: TAppLink[] = [ + { + name: 'Swap Widget SDK', + href: developerDocsSectionUrl('#tag/swap-widget-sdk'), + description: 'Integrate the drop-in swap widget with built-in UI.', + icon: , + target: '_blank', + }, + { + name: 'Widget sandbox', + href: 'https://widget.shapeshift.com/', + description: 'Configure and preview the drop-in swap widget.', + icon: , + target: '_blank', + }, + { + name: 'Swap API Doc', + href: developerDocsSectionUrl('#tag/swaps'), + description: 'Get rates, executable quotes, and swap status.', + icon: , + target: '_blank', + }, + { + name: 'Supported Assets', + href: developerDocsSectionUrl('#tag/supported-assets'), + description: 'List assets and fetch asset details by CAIP-19 ID.', + icon: , + target: '_blank', + }, + { + name: 'Supported Chains', + href: developerDocsSectionUrl('#tag/supported-chains'), + description: 'Review the chains available through the public API.', + icon: , + target: '_blank', + }, + { + name: 'Partners portal', + href: 'https://dashboard.affiliate.shapeshift.com/', + description: 'Get your affiliate code and configure the widget.', + icon: , + target: '_blank', + }, +] + export const appDao: TAppLink[] = [ { name: 'FOX Token', @@ -314,12 +363,14 @@ export const appProducts: TAppLink[] = [ export const headerTabs = [ { name: 'Products', href: '/products', value: 'products' }, + { name: 'Developers', href: '/developers', value: 'developers' }, { name: 'Resources', href: '/resources', value: 'resources' }, { name: 'DAO', href: '/dao', value: 'dao' }, ] export const footerLinks: Record = { Products: appProducts, + Developers: appDevelopers, Resources: appResources, DAO: appDao, Connect: [ diff --git a/app/[lang]/_utils/dictionary/developers.ts b/app/[lang]/_utils/dictionary/developers.ts new file mode 100644 index 0000000..d05457b --- /dev/null +++ b/app/[lang]/_utils/dictionary/developers.ts @@ -0,0 +1,82 @@ +export const DEVELOPERS_DICT = { + expand: { + titleLine1: 'Build with', + titleLine2: 'ShapeShift.', + description: 'Explore API documentation, integration guides, and reference material for crypto applications.', + ctaButton: 'View API Docs', + }, + page: { + widget: { + title: 'Your brand. Make it feel native.', + description: + 'Choose a theme, set the default assets, and preview a swap experience that feels built into your product.', + ctaButton: 'Open the widget sandbox', + }, + api: { + title: 'Skip the UI and build your own', + description: + 'Use the same routing engine through a REST API. We return unsigned transactions for your users to sign and broadcast.', + endpoints: [ + { + method: 'GET /v1/assets', + title: 'List chains and assets', + description: 'Enumerate what your users can trade, by CAIP-19 ID.', + }, + { + method: 'GET /v1/swap/rates', + title: 'Fetch rates for a pair', + description: 'One call returns a rate from every available swapper. No transaction yet.', + }, + { + method: 'POST /v1/swap/quote', + title: 'Get an executable quote', + description: 'Returns transaction data. Your user signs it in their own wallet, funds never touch us.', + }, + ], + }, + economics: { + title: 'How the revenue share works', + description: 'One partner code follows every swap to on-chain settlement. No invoices or payout schedules.', + }, + stats: { + chains: { value: '48+', title: 'Supported chains' }, + assets: { value: '30,000+', title: 'Tradable assets' }, + volume: { value: '$1.7B+', title: 'Lifetime swap volume' }, + }, + faq: { + title: 'Questions partners ask', + items: [ + { + question: 'Which chains are supported?', + answer: + 'Bitcoin, Ethereum and the major L2s (Arbitrum, Base, Optimism), Solana, Avalanche, BNB Chain, Cosmos, and more. 48+ chains today, with new routes added as the underlying aggregators support them. The full, current list is served by the API: https://api.shapeshift.com/v1/chains', + hasChainsLink: true, + }, + { + question: 'What does it cost to integrate?', + answer: + 'Nothing. There is no license fee or API key charge. Your affiliate fee is added on top of the protocol fee and paid directly to you.', + hasChainsLink: false, + }, + { + question: 'Who holds user funds?', + answer: + 'Swaps are non-custodial end to end. Users sign transactions from their own wallets, and funds move directly through the underlying protocols.', + hasChainsLink: false, + }, + { + question: 'What support do partners get?', + hasChainsLink: false, + answer: + 'Support does not stop at launch. Partners get a direct channel to our integration engineers for the lifetime of the integration, on both technical and marketing questions, plus example repos and a staging affiliate ID for testing.', + }, + ], + }, + cta: { + title: 'Integrate multichain swaps in your product', + description: 'Configure the Widget, preview your theme, create a partner code, and start earning on every swap.', + ctaPrimary: 'Configure the Widget', + ctaSecondary: 'Talk with us', + }, + }, +} as const diff --git a/app/[lang]/developers/README.md b/app/[lang]/developers/README.md new file mode 100644 index 0000000..5f8657f --- /dev/null +++ b/app/[lang]/developers/README.md @@ -0,0 +1,35 @@ +# Developers Directory + +This directory contains the `/developers` landing page: the entry point for dApps, chains, and wallets integrating ShapeShift's swap widget or REST API. + +## Directory Structure + +- **layout.tsx**: Metadata (title/description/OG/Twitter) for the page, plus `DevelopersResourceHints` (DNS prefetch/preconnect for `widget.shapeshift.com`). +- **page.tsx**: Assembles all sections below in order. +- **_components/**: One component per page section. + +## Page sections, in order + +1. **Hero** — the real, live `@shapeshiftoss/swap-widget` embedded in the page (not a mock), plus the primary CTAs (Try the Widget / Talk with us). +2. **Stats** — chains / assets / lifetime volume (static snapshot figures, not live). +3. **Partner logos** — scrolling row of protocols ShapeShift routes across. +4. **Widget** — feature copy + an interactive live theme-color preview (`LiveThemeSwitcher`). +5. **Why ShapeShift** — the routing pitch: compare protocols, return one best route. +6. **API** — clickable endpoint list with a live-switching, typewriter-animated code sample panel. +7. **Economics** — how the affiliate revenue share works, step by step. +8. **Launch path** — the three questions most teams ask before choosing an integration path. +9. **FAQ** — accordion. +10. **Closing CTA** band. + +## Technical Implementation + +- Most copy lives in `app/[lang]/_utils/dictionary/developers.ts` under `DEVELOPERS_DICT.page`, **except** WhyShapeShift, WidgetSection's feature ring, ApiSection's code panels, EconomicsSection's milestones, and LaunchPath's Q&A, which hardcode their copy directly in JSX (illustration- or interaction-heavy sections where copy, visuals, and behavior are tightly coupled). +- `DevelopersHero.tsx` embeds the real `@shapeshiftoss/swap-widget` React SDK (dynamically imported, `ssr: false`) with `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID`. Set this public Reown project ID before building (Next.js embeds it in the client bundle); the existing swap-widget service uses the value documented in `.env.local.sample`. Configure it in each Railway environment before promoting this page. Verify the website origin is allowed in Reown and test wallet connection before release. Set `NEXT_PUBLIC_SHAPESHIFT_PARTNER_CODE` to ShapeShift's registered affiliate code before release so website swaps are attributed to its payout account; verify the code via `/v1/partner/{code}`. +- Client components (interactive state, refs, or the widget's own client-only requirements): `DevelopersHero`, `DevelopersWidgetSection`, `DevelopersApiSection`, `DevelopersFaq`, `DevelopersPartnerLogos`. Everything else is a server component. +- Reuses existing shared components (`Button`, `LocalizedLink`) and Tailwind color tokens from `tailwind.config.ts`. + +## Development Guidelines + +- Keep all partner/API links pointed at real, live URLs (`https://api.shapeshift.com/docs`, `https://widget.shapeshift.com/`) — never placeholder `#` hrefs. +- Match existing Tailwind color tokens (`bg-blue`, `bg-blueLight`, `bg-mint`, `bg-secondBg`, `border-stroke`, etc.) rather than introducing new hex values. +- The `@shapeshiftoss/swap-widget` SDK and its peer dependencies (`wagmi`, `@reown/appkit*`, `@solana/wallet-adapter-*`, etc.) are the only dependency this page adds to the repo. `next.config.ts`'s `webpack` block and `middleware.ts`'s CSP scoping (to `/developers` only) exist solely to support it — see the comments in both files before changing either. diff --git a/app/[lang]/developers/_components/DevelopersApiSection.tsx b/app/[lang]/developers/_components/DevelopersApiSection.tsx new file mode 100644 index 0000000..d50bc33 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersApiSection.tsx @@ -0,0 +1,238 @@ +'use client' + +import { motion, useInView, useReducedMotion } from 'framer-motion' +import { useEffect, useRef, useState } from 'react' + +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +const codePanels = [ + { + label: 'GET /v1/assets', + lines: [ + '$ # chains', + 'curl "https://api.shapeshift.com/v1/chains"', + '', + '# assets, filtered by chain', + 'curl --get "https://api.shapeshift.com/v1/assets" \\', + ' --data-urlencode "chainId=eip155:1" \\', + ' --data-urlencode "limit=100"', + '', + '# response · 200', + '{', + ' "assets": [{', + ' "assetId": "eip155:1/slip44:60",', + ' "chainId": "eip155:1",', + ' "name": "Ethereum",', + ' "symbol": "ETH",', + ' "precision": 18,', + ' "icon": "https://…/eth@2x.png"', + ' }, …],', + ' "timestamp": 1754241000000', + '}', + ], + }, + { + label: 'GET /v1/swap/rates', + lines: [ + '$ curl --get "https://api.shapeshift.com/v1/swap/rates" \\', + ' --data-urlencode "sellAssetId=eip155:1/slip44:60" \\', + ' --data-urlencode "buyAssetId=bip122:000000000019d6689c085ae165831e93/slip44:0" \\', + ' --data-urlencode "sellAmountCryptoBaseUnit=1000000000000000000" \\', + ' -H "X-Partner-Code: your-partner-code"', + '', + '# response · 200', + '{', + ' "rates": [{', + ' "swapperName": "THORChain",', + ' "rate": "0.04829",', + ' "buyAmountCryptoBaseUnit": "4829000",', + ' "steps": 1,', + ' "estimatedExecutionTimeMs": 60000,', + ' "affiliateBps": "10"', + ' }, …one entry per swapper…],', + ' "expiresAt": 1754241060000', + '}', + ], + }, + { + label: 'POST /v1/swap/quote', + lines: [ + '$ curl -X POST "https://api.shapeshift.com/v1/swap/quote" \\', + ' -H "X-Partner-Code: your-partner-code" \\', + ' -H "Content-Type: application/json" \\', + " -d '{", + ' "sellAssetId": "eip155:1/slip44:60",', + ' "buyAssetId": "bip122:00000000...93/slip44:0",', + ' "sellAmountCryptoBaseUnit": "1000000000000000000",', + ' "receiveAddress": "bc1qar0s...f5mdq",', + ' "swapperName": "Relay"', + " }'", + '', + '# response · 200', + '{', + ' "quoteId": "0f8e2b1a-…",', + ' "swapperName": "Relay",', + ' "rate": "0.04829",', + ' "affiliateBps": "10",', + ' "steps": [{', + ' "transactionData": {', + ' "to": "0xdef1c0de…", "data": "0x…",', + ' "value": "1000000000000000000"', + ' }', + ' }],', + ' "expiresAt": 1754241060000', + '}', + ], + }, +] + +function TypedCodePanel({ activeTab }: { activeTab: number }): ReactNode { + const panelRef = useRef(null) + const isInView = useInView(panelRef, { amount: 0.25, once: true }) + const shouldReduceMotion = useReducedMotion() + const source = codePanels[activeTab].lines.join('\n') + const [visibleCharacters, setVisibleCharacters] = useState(0) + const [hasCopied, setHasCopied] = useState(false) + + useEffect(() => { + if (!isInView) { + setVisibleCharacters(0) + return undefined + } + if (shouldReduceMotion) { + setVisibleCharacters(source.length) + return undefined + } + + setVisibleCharacters(0) + const interval = window.setInterval(() => { + setVisibleCharacters((value) => { + if (value >= source.length) { + window.clearInterval(interval) + return value + } + return Math.min(value + 12, source.length) + }) + }, 12) + + return () => window.clearInterval(interval) + }, [activeTab, isInView, shouldReduceMotion, source.length]) + + useEffect(() => setHasCopied(false), [activeTab]) + + return ( +
+
+
+ + + +
+ {codePanels[activeTab].label} + +
+
+        {source.slice(0, visibleCharacters)}
+        {visibleCharacters < source.length ? (
+          
+        ) : null}
+      
+
+ ) +} + +export function DevelopersApiSection(): ReactNode { + const { api } = DEVELOPERS_DICT.page + const [activeTab, setActiveTab] = useState(0) + + return ( +
+

{api.title}

+

{api.description}

+ +
+
+ {api.endpoints.map((endpoint, index) => ( + + ))} +
+
+
+ + +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersCta.tsx b/app/[lang]/developers/_components/DevelopersCta.tsx new file mode 100644 index 0000000..f6052b8 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersCta.tsx @@ -0,0 +1,56 @@ +import Image from 'next/image' + +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +export function DevelopersCta(): ReactNode { + const { cta } = DEVELOPERS_DICT.page + + return ( +
+
+ {''} + {/* Sized and positioned to match the fox's own real usage on the official site + (app/[lang]/dao/fox-token/page.tsx: h-[398px] in a lg:h-[552px] banner, ~72% of the + container's height, bottom-right at a 5% inset, hidden below the lg breakpoint — + not a guessed scale. */} + +
+
+

+ {cta.title} +

+

{cta.description}

+
+
+
+
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx b/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx new file mode 100644 index 0000000..7ccb57f --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersEconomicsSection.tsx @@ -0,0 +1,144 @@ +import { Button } from '@/app/[lang]/_components/Button' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +const milestones = [ + { + icon: '', + label: 'Your product', + title: 'Create your partner code', + description: 'One parameter in the Widget or API.', + }, + { + icon: '⇄', + label: 'ShapeShift', + title: 'We route every swap', + description: 'The trade stays attributed to your product.', + }, + { + icon: '$', + label: 'Your wallet', + title: 'Your fee settles on-chain', + description: 'Your share arrives with the transaction.', + }, +] as const + +function MilestoneMarker({ index }: { index: number }): ReactNode { + return ( +
+ {milestones[index].icon} +
+ ) +} + +function MilestoneCopy({ index }: { index: number }): ReactNode { + const milestone = milestones[index] + + return ( +
+
{milestone.label}
+

{milestone.title}

+

{milestone.description}

+
+ ) +} + +export function DevelopersEconomicsSection(): ReactNode { + const { economics } = DEVELOPERS_DICT.page + + return ( +
+
+

+ {economics.title} +

+

+ {economics.description} +

+
+ +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersFaq.tsx b/app/[lang]/developers/_components/DevelopersFaq.tsx new file mode 100644 index 0000000..07dda1b --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersFaq.tsx @@ -0,0 +1,105 @@ +'use client' + +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' +import { useState } from 'react' + +import { AnimatedPlusMinusIcon } from '@/app/[lang]/_components/QuestionSection' +import { developerDocsSectionUrl } from '@/app/[lang]/_utils/constants' +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +// Mirrors QuestionSection's accordion exactly, but accepts a ReactNode answer instead of a +// plain string, so the chains question can end in a real button instead of a raw URL. +function FaqItem({ question, answer }: { question: string; answer: ReactNode }): ReactNode { + const [isOpen, setIsOpen] = useState(false) + const shouldReduceMotion = useReducedMotion() + + return ( +
+ + + {isOpen ? ( + +
{answer}
+
+ ) : null} +
+
+ ) +} + +export function DevelopersFaq(): ReactNode { + const { items } = DEVELOPERS_DICT.page.faq + + return ( +
+
+

+ {DEVELOPERS_DICT.page.faq.title} +

+

+ {'Straight answers for product and engineering teams evaluating the integration.'} +

+
+
+ {items.map((item) => ( + +

+ { + 'Bitcoin, Ethereum and the major L2s (Arbitrum, Base, Optimism), Solana, Avalanche, BNB Chain, Cosmos, and more. 48+ chains today, with new routes added as the underlying aggregators support them.' + } +

+ + {'View supported chains'} + {'→'} + + + ) : ( + item.answer + ) + } + /> + ))} +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersHero.tsx b/app/[lang]/developers/_components/DevelopersHero.tsx new file mode 100644 index 0000000..7ddc534 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersHero.tsx @@ -0,0 +1,162 @@ +'use client' + +import '@shapeshiftoss/swap-widget/style.css' +import { animate, motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion' +import dynamic from 'next/dynamic' +import { useEffect, useRef } from 'react' + +import { Button } from '@/app/[lang]/_components/Button' + +import type { ReactNode } from 'react' + +// Loaded client-side only, per the SDK's own docs: the widget initializes Reown AppKit at load, +// which reads browser-only state and has no meaningful server-rendered output. +const SwapWidget = dynamic(async () => (await import('@shapeshiftoss/swap-widget')).SwapWidget, { + ssr: false, + loading: () =>
, +}) + +function SwapWidgetEmbed(): ReactNode { + return ( + <> + {/* + Upstream bug in @shapeshiftoss/swap-widget's own ≤600px CSS: .ssw-modal only sets a + max-height there, so its flex children (chain sidebar + token list) have no definite space + to distribute — the virtualized token list resolves to 0px tall and renders zero rows. + Giving the modal a real height (still capped well under the viewport) fixes the flex + cascade without changing anything about its layout/content. + */} + + console.log('Success:', txHash)} + onSwapError={(error) => console.error('Error:', error)} + theme={'dark'} + /> + + ) +} + +const PALETTE = ['#386FF9aa', '#9D63ECaa', '#70E1B1aa', '#06B6D4aa'] + +export function DevelopersHero(): ReactNode { + const shouldReduceMotion = useReducedMotion() + const glowRef = useRef(null) + const glowAccent = useMotionValue(PALETTE[0]) + + useEffect(() => { + if (shouldReduceMotion) return undefined + const controls = animate(glowAccent, [...PALETTE, PALETTE[0]], { + duration: PALETTE.length * 3.2, + repeat: Infinity, + ease: 'linear', + }) + + const el = glowRef.current + const observer = el + ? new IntersectionObserver(([entry]) => { + if (entry?.isIntersecting) controls.play() + else controls.pause() + }) + : null + if (el && observer) observer.observe(el) + + return () => { + controls.stop() + observer?.disconnect() + } + }, [shouldReduceMotion, glowAccent]) + + const glowBackground = useMotionTemplate`linear-gradient(135deg, ${glowAccent}, #9D63EC77 48%, #70E1B166)` + + return ( +
+
+ +

+ {'Every chain your users need, in one place.'} +

+

+ {'Embed the Widget or call the API. ShapeShift handles the routing across every chain you need.'} +

+
+
+
+ {['48+ chains', '18 routing protocols'].map((benefit, index) => ( + + {index > 0 ? {'/'} : null} + {benefit} + + ))} +
+
+
+ {'Earn on every attributed swap'} + {'Set 0 to 100 bps'} +
+

{'Your partner fee settles directly on-chain.'}

+
+ + {'Need more control? Build with the API'} + {'→'} + +
+ + + + {/* Reserve room for the quote selector and network fee before an amount is entered. + A minimum height keeps the widget top-aligned without clipping or scrolling taller + states. Allow the SDK's fixed-width card to shrink on narrow screens. */} +
+ +
+
+
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersLaunchPath.tsx b/app/[lang]/developers/_components/DevelopersLaunchPath.tsx new file mode 100644 index 0000000..4184f4c --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersLaunchPath.tsx @@ -0,0 +1,68 @@ +import { Button } from '@/app/[lang]/_components/Button' + +import type { ReactNode } from 'react' + +const integrationAnswers = [ + { + question: 'How long does integration take?', + answer: + 'You can configure and preview the Widget in minutes. Production timing depends on your wallet flow, supported assets, and QA. The API requires your team to build the interface and transaction flow.', + }, + { + question: 'Who owns what?', + answer: + 'ShapeShift maintains routing and returns executable transactions. You own the product experience, wallet connection, transaction signing, support flow, and final integration testing.', + }, + { + question: 'How do partner earnings work?', + answer: + 'Create a partner code in the Portal, add it to the Widget or API, and monitor attributed swaps there. Fees settle directly to your configured wallet on-chain. There is no balance to withdraw.', + }, +] as const + +export function DevelopersLaunchPath(): ReactNode { + return ( +
+
+
+
+

+ {'Know what shipping involves'} +

+

+ {'The three answers most teams need before choosing an integration path.'} +

+
+
+
+
+ +
+ {integrationAnswers.map((item) => ( +
+
{item.question}
+
{item.answer}
+
+ ))} +
+ +
+ {'No integration fee'} + {'Non-custodial execution'} + {'Documentation and Discord support'} +
+
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx b/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx new file mode 100644 index 0000000..5e2fdca --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersPartnerLogos.tsx @@ -0,0 +1,91 @@ +'use client' + +import Image from 'next/image' + +import { Carousel } from '@/app/[lang]/_components/Carousel' + +import type { ReactNode } from 'react' + +const imageLogos = [ + { + key: 'thorchain', + alt: 'THORChain', + src: '/landing/thorchain.png', + width: 192, + height: 40, + }, + { + key: 'chainflip', + alt: 'Chainflip', + src: '/landing/chainflip.png', + width: 238, + height: 40, + }, + { + key: 'cowSwap', + alt: 'CoW Swap', + src: '/landing/cow-swap.png', + width: 218, + height: 40, + }, + { + key: 'relay', + alt: 'Relay', + src: '/landing/relay.png', + width: 158, + height: 40, + }, + { key: 'Ox', alt: '0x', src: '/landing/0x.png', width: 65, height: 40 }, + { + key: 'mayaProtocol', + alt: 'MAYAChain', + src: '/landing/maya-protocol.png', + width: 171, + height: 40, + }, + { + key: 'butterNetwork', + alt: 'Butter Network', + src: '/landing/butter-network.png', + width: 231, + height: 40, + }, + { + key: 'jupiter', + alt: 'Jupiter', + src: '/landing/jupiter.png', + width: 150, + height: 40, + }, +] as const + +function ProtocolRow(): ReactNode { + return ( +
+ {imageLogos.map(({ key, alt, src, width, height }) => ( +
+ {alt} +
+ ))} +
+ ) +} + +export function DevelopersPartnerLogos(): ReactNode { + return ( +
+
+ + + +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersResourceHints.tsx b/app/[lang]/developers/_components/DevelopersResourceHints.tsx new file mode 100644 index 0000000..4d470e1 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersResourceHints.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react' + +export function DevelopersResourceHints(): ReactNode { + return ( + <> + + + + ) +} diff --git a/app/[lang]/developers/_components/DevelopersStats.tsx b/app/[lang]/developers/_components/DevelopersStats.tsx new file mode 100644 index 0000000..b52f58c --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersStats.tsx @@ -0,0 +1,27 @@ +import { DEVELOPERS_DICT } from '@/app/[lang]/_utils/dictionary/developers' + +import type { ReactNode } from 'react' + +export function DevelopersStats(): ReactNode { + const stats = Object.values(DEVELOPERS_DICT.page.stats) + + return ( +
+
+ {stats.map((stat) => ( +
+
+ {stat.value} +
+
{stat.title}
+
+ ))} +
+
+ ) +} diff --git a/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx b/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx new file mode 100644 index 0000000..7f1a284 --- /dev/null +++ b/app/[lang]/developers/_components/DevelopersWhyShapeShift.tsx @@ -0,0 +1,74 @@ +import { ShapeshiftLogo } from '@/app/[lang]/_icons/ShapeshiftLogo' + +import type { ReactNode } from 'react' + +function RouterIllustration(): ReactNode { + return ( +
+
+