diff --git a/apps/web/app/(base-org)/developers/page.tsx b/apps/web/app/(base-org)/developers/page.tsx new file mode 100644 index 00000000000..9964bdb4612 --- /dev/null +++ b/apps/web/app/(base-org)/developers/page.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from 'next'; +import AnalyticsProvider from 'apps/web/contexts/Analytics'; +import Container from 'apps/web/src/components/base-org/Container'; +import { Hero } from 'apps/web/src/components/Developers/Hero'; +import { UseCases } from 'apps/web/src/components/Developers/UseCases'; +import { Customers } from 'apps/web/src/components/Developers/Customers'; +import { Testimonials } from 'apps/web/src/components/Developers/Testimonials'; +import { Tools } from 'apps/web/src/components/Developers/Tools'; +import { WhyBase } from 'apps/web/src/components/Developers/WhyBase'; +import { BottomCta } from 'apps/web/src/components/Developers/BottomCta'; +import { LiveDemo } from 'apps/web/src/components/Developers/LiveDemo'; + +export const metadata: Metadata = { + metadataBase: new URL('https://base.org'), + title: `Base | Developers`, + openGraph: { + title: `Base | Developers`, + url: `/developers`, + }, +}; + +export default async function Developers() { + return ( + + +
+ + + + + + + + +
+
+
+ ); +} diff --git a/apps/web/app/(base-org)/frames/names/page.tsx b/apps/web/app/(base-org)/frames/names/page.tsx deleted file mode 100644 index ff67ded0696..00000000000 --- a/apps/web/app/(base-org)/frames/names/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Metadata } from 'next'; -import Image from 'apps/web/node_modules/next/image'; -import Link from 'apps/web/node_modules/next/link'; -import initialFrameImage from 'apps/web/pages/api/basenames/frame/assets/initial-image.png'; -import { initialFrame } from 'apps/web/pages/api/basenames/frame/frameResponses'; - -export const metadata: Metadata = { - metadataBase: new URL('https://base.org'), - title: `Basenames | Frame`, - description: - 'Basenames are a core onchain building block that enables anyone to establish their identity on Base by registering human-readable names for their address(es). They are a fully onchain solution which leverages ENS infrastructure deployed on Base.', - openGraph: { - title: `Basenames | Frame`, - url: `/frames/names`, - images: [initialFrameImage.src], - }, - twitter: { - site: '@base', - card: 'summary_large_image', - }, - other: { - ...(initialFrame as Record), - }, -}; - -export default async function NameFrame() { - return ( -
-
-
- - Claim a basename today - -
-
-
- ); -} diff --git a/apps/web/app/(base-org)/build/page.tsx b/apps/web/app/(base-org)/resources/page.tsx similarity index 100% rename from apps/web/app/(base-org)/build/page.tsx rename to apps/web/app/(base-org)/resources/page.tsx diff --git a/apps/web/app/(basenames)/manage-names/page.tsx b/apps/web/app/(basenames)/manage-names/page.tsx index 1aec6958aab..1b93d045a9b 100644 --- a/apps/web/app/(basenames)/manage-names/page.tsx +++ b/apps/web/app/(basenames)/manage-names/page.tsx @@ -1,6 +1,5 @@ import ErrorsProvider from 'apps/web/contexts/Errors'; import type { Metadata } from 'next'; -import { initialFrame } from 'apps/web/pages/api/basenames/frame/frameResponses'; import NamesList from 'apps/web/src/components/Basenames/ManageNames/NamesList'; export const metadata: Metadata = { @@ -16,9 +15,6 @@ export const metadata: Metadata = { site: '@base', card: 'summary_large_image', }, - other: { - ...(initialFrame as Record), - }, }; export default async function Page() { diff --git a/apps/web/app/(basenames)/names/page.tsx b/apps/web/app/(basenames)/names/page.tsx index 52787ef2657..31312de3a18 100644 --- a/apps/web/app/(basenames)/names/page.tsx +++ b/apps/web/app/(basenames)/names/page.tsx @@ -6,7 +6,6 @@ import RegistrationFlow from 'apps/web/src/components/Basenames/RegistrationFlow import RegistrationValueProp from 'apps/web/src/components/Basenames/RegistrationValueProp'; import type { Metadata } from 'next'; import basenameCover from './basename_cover.png'; -import { initialFrame } from 'apps/web/pages/api/basenames/frame/frameResponses'; export const metadata: Metadata = { metadataBase: new URL('https://base.org'), @@ -22,9 +21,6 @@ export const metadata: Metadata = { site: '@base', card: 'summary_large_image', }, - other: { - ...(initialFrame as Record), - }, }; type PageProps = { searchParams?: { code?: string } }; diff --git a/apps/web/app/CryptoProviders.tsx b/apps/web/app/CryptoProviders.tsx index f25e43e6ec5..bc0927614ae 100644 --- a/apps/web/app/CryptoProviders.tsx +++ b/apps/web/app/CryptoProviders.tsx @@ -68,6 +68,7 @@ export default function CryptoProviders({ children }: CryptoProvidersProps) { {children} diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 6fff7cf42f4..f8a27495521 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -281,6 +281,11 @@ module.exports = extendBaseConfig( destination: '/build', permanent: true, }, + { + source: '/build', + destination: '/resources', + permanent: true, + }, { source: '/onchainfont', // just so the build doesn't fail in CI diff --git a/apps/web/package.json b/apps/web/package.json index 718ac4131ed..cbe5f306640 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,7 +15,7 @@ "dependencies": { "@coinbase/cookie-banner": "^1.0.3", "@coinbase/cookie-manager": "^1.1.1", - "@coinbase/onchainkit": "^0.35.2", + "@coinbase/onchainkit": "^0.36.0", "@datadog/browser-logs": "^5.23.3", "@datadog/browser-rum": "^5.23.3", "@frames.js/render": "^0.3.14", @@ -24,6 +24,7 @@ "@heroicons/react": "^2.1.3", "@lottiefiles/dotlottie-react": "^0.8.10", "@monogrid/gainmap-js": "^3.0.6", + "@number-flow/react": "^0.5.5", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-collapsible": "^1.1.0", "@radix-ui/react-popover": "^1.1.1", @@ -73,6 +74,8 @@ "recharts": "^2.12.7", "satori": "^0.10.14", "sharp": "^0.33.4", + "shiki": "^2.1.0", + "tailwindcss-animate": "^1.0.7", "three": "^0.168.0", "three-stdlib": "^2.33.0", "twemoji": "^14.0.2", @@ -80,7 +83,7 @@ "usehooks-ts": "^3.1.0", "uuid": "^10.0.0", "viem": "2.x", - "wagmi": "^2.11.3" + "wagmi": "^2.14.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.5.0", diff --git a/apps/web/pages/api/basenames/[name]/getBasenameRegistrationPrice.ts b/apps/web/pages/api/basenames/[name]/getBasenameRegistrationPrice.ts index 22b1fb58dd2..e686d871364 100644 --- a/apps/web/pages/api/basenames/[name]/getBasenameRegistrationPrice.ts +++ b/apps/web/pages/api/basenames/[name]/getBasenameRegistrationPrice.ts @@ -8,7 +8,7 @@ import { import { weiToEth } from 'apps/web/src/utils/weiToEth'; import { formatWei } from 'apps/web/src/utils/formatWei'; import { logger } from 'apps/web/src/utils/logger'; -import { CHAIN } from 'apps/web/pages/api/basenames/frame/constants'; +import { base } from 'viem/chains'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { name, years } = req.query; @@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) async function getBasenameRegistrationPrice(name: string, years: number): Promise { const client = createPublicClient({ - chain: CHAIN, + chain: base, transport: http(), }); try { @@ -40,7 +40,7 @@ async function getBasenameRegistrationPrice(name: string, years: number): Promis } const price = await client.readContract({ - address: REGISTER_CONTRACT_ADDRESSES[CHAIN.id], + address: REGISTER_CONTRACT_ADDRESSES[base.id], abi: REGISTER_CONTRACT_ABI, functionName: 'registerPrice', args: [normalizedName, secondsInYears(years)], diff --git a/apps/web/pages/api/basenames/[name]/isNameAvailable.ts b/apps/web/pages/api/basenames/[name]/isNameAvailable.ts index 7da37e45e57..b3f2d6b6da9 100644 --- a/apps/web/pages/api/basenames/[name]/isNameAvailable.ts +++ b/apps/web/pages/api/basenames/[name]/isNameAvailable.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'apps/web/node_modules/next/dist/shared/lib/utils'; import { getBasenameAvailable } from 'apps/web/src/utils/usernames'; -import { CHAIN } from 'apps/web/pages/api/basenames/frame/constants'; +import { base } from 'viem/chains'; export type IsNameAvailableResponse = { nameIsAvailable: boolean; @@ -9,7 +9,7 @@ export type IsNameAvailableResponse = { export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { name } = req.query; try { - const isNameAvailableResponse = await getBasenameAvailable(String(name), CHAIN); + const isNameAvailableResponse = await getBasenameAvailable(String(name), base); const responseData: IsNameAvailableResponse = { nameIsAvailable: isNameAvailableResponse, }; diff --git a/apps/web/pages/api/basenames/frame/01_inputSearchValue.ts b/apps/web/pages/api/basenames/frame/01_inputSearchValue.ts deleted file mode 100644 index 59581eb32ec..00000000000 --- a/apps/web/pages/api/basenames/frame/01_inputSearchValue.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'; -import { ActionType, ComponentType } from 'libs/base-ui/utils/logEvent'; -import logServerSideEvent, { generateDeviceId } from 'apps/web/src/utils/logServerSideEvent'; -import { logger } from 'apps/web/src/utils/logger'; -import { inputSearchValueFrame } from 'apps/web/pages/api/basenames/frame/frameResponses'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: `Search Screen — Method (${req.method}) Not Allowed` }); - } - - try { - const eventName = 'claim_initiated'; - const deviceId = generateDeviceId(req); - const eventProperties = { - action: ActionType.click, - context: 'basenames_claim_frame', - componentType: ComponentType.button, - }; - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - try { - return res.status(200).setHeader('Content-Type', 'text/html').send(inputSearchValueFrame); - } catch (error) { - logger.error('Could not process request:', error); - return res.status(500).json({ error: 'Internal Server Error' }); - } -} diff --git a/apps/web/pages/api/basenames/frame/02_validateSearchInputAndSetYears.ts b/apps/web/pages/api/basenames/frame/02_validateSearchInputAndSetYears.ts deleted file mode 100644 index c6e7890506f..00000000000 --- a/apps/web/pages/api/basenames/frame/02_validateSearchInputAndSetYears.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'; -import { FrameRequest } from '@coinbase/onchainkit/frame'; -import { ActionType, ComponentType } from 'libs/base-ui/utils/logEvent'; -import { formatDefaultUsername, validateEnsDomainName } from 'apps/web/src/utils/usernames'; -import logServerSideEvent, { generateDeviceId } from 'apps/web/src/utils/logServerSideEvent'; -import { logger } from 'apps/web/src/utils/logger'; -import type { IsNameAvailableResponse } from 'apps/web/pages/api/basenames/[name]/isNameAvailable'; -import { - retryInputSearchValueFrame, - setYearsFrame, -} from 'apps/web/pages/api/basenames/frame/frameResponses'; -import { DOMAIN } from 'apps/web/pages/api/basenames/frame/constants'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: `Set Years Screen — Method (${req.method}) Not Allowed` }); - } - - try { - const eventName = 'selected_name'; - const deviceId = generateDeviceId(req); - const eventProperties = { - action: ActionType.click, - context: 'basenames_claim_frame', - componentType: ComponentType.button, - }; - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - try { - const body = req.body as FrameRequest; - const { untrustedData } = body; - const targetName: string = encodeURIComponent(untrustedData.inputText); - - const { valid, message } = validateEnsDomainName(targetName); - if (!valid) { - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send(retryInputSearchValueFrame(message)); - } - - const isNameAvailableResponse = await fetch( - `${DOMAIN}/api/basenames/${targetName}/isNameAvailable`, - ); - const isNameAvailableResponseData = await isNameAvailableResponse.json(); - const { nameIsAvailable } = isNameAvailableResponseData as IsNameAvailableResponse; - if (!nameIsAvailable) { - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send(retryInputSearchValueFrame('Name unavailable')); - } - - const formattedTargetName = await formatDefaultUsername(targetName); - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send(setYearsFrame(targetName, formattedTargetName)); - } catch (error) { - return res.status(500).json({ error }); // TODO: figure out error state for the frame BAPP-452 - } -} diff --git a/apps/web/pages/api/basenames/frame/03_getPriceAndConfirm.ts b/apps/web/pages/api/basenames/frame/03_getPriceAndConfirm.ts deleted file mode 100644 index 546579c6770..00000000000 --- a/apps/web/pages/api/basenames/frame/03_getPriceAndConfirm.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'; -import { FrameRequest } from '@coinbase/onchainkit/frame'; -import { ActionType, ComponentType } from 'libs/base-ui/utils/logEvent'; -import logServerSideEvent, { generateDeviceId } from 'apps/web/src/utils/logServerSideEvent'; -import { logger } from 'apps/web/src/utils/logger'; -import { - confirmationFrame, - buttonIndexToYears, -} from 'apps/web/pages/api/basenames/frame/frameResponses'; -import { DOMAIN } from 'apps/web/pages/api/basenames/frame/constants'; - -type ButtonIndex = 1 | 2 | 3 | 4; -const validButtonIndexes: readonly ButtonIndex[] = [1, 2, 3, 4] as const; - -type GetBasenameRegistrationPriceResponseType = { - registrationPriceInWei: string; - registrationPriceInEth: string; -}; - -type ConfirmationFrameStateType = { - targetName: string; - formattedTargetName: string; -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: `Confirm Screen — Method (${req.method}) Not Allowed` }); - } - - try { - const eventName = 'selected_years'; - const deviceId = generateDeviceId(req); - const eventProperties = { - action: ActionType.click, - context: 'basenames_claim_frame', - componentType: ComponentType.button, - }; - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - const body = req.body as FrameRequest; - const { untrustedData } = body; - const messageState = JSON.parse( - decodeURIComponent(untrustedData.state), - ) as ConfirmationFrameStateType; - const targetName = encodeURIComponent(messageState.targetName); - const formattedTargetName = messageState.formattedTargetName; - - const buttonIndex = untrustedData.buttonIndex as ButtonIndex; - if (!validButtonIndexes.includes(buttonIndex)) { - return res.status(500).json({ error: 'Internal Server Error' }); - } - const targetYears = buttonIndexToYears[buttonIndex]; - - const getRegistrationPriceResponse = await fetch( - `${DOMAIN}/api/basenames/${targetName}/getBasenameRegistrationPrice?years=${targetYears}`, - ); - const getRegistrationPriceResponseData = await getRegistrationPriceResponse.json(); - const { registrationPriceInWei, registrationPriceInEth } = - getRegistrationPriceResponseData as GetBasenameRegistrationPriceResponseType; - - try { - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send( - confirmationFrame( - targetName, - formattedTargetName, - targetYears, - registrationPriceInWei, - registrationPriceInEth, - ), - ); - } catch (error) { - return res.status(500).json({ error: 'Internal Server Error' }); - } -} diff --git a/apps/web/pages/api/basenames/frame/04_txSubmitted.ts b/apps/web/pages/api/basenames/frame/04_txSubmitted.ts deleted file mode 100644 index 9b094a29abd..00000000000 --- a/apps/web/pages/api/basenames/frame/04_txSubmitted.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'; -import { FrameRequest, getFrameMessage } from '@coinbase/onchainkit/frame'; -import { ActionType, ComponentType } from 'libs/base-ui/utils/logEvent'; -import { getTransactionStatus } from 'apps/web/src/utils/frames/basenames'; -import logServerSideEvent, { generateDeviceId } from 'apps/web/src/utils/logServerSideEvent'; -import { logger } from 'apps/web/src/utils/logger'; -import { - txSucceededFrame, - txRevertedFrame, -} from 'apps/web/pages/api/basenames/frame/frameResponses'; -import { NEYNAR_API_KEY } from 'apps/web/pages/api/basenames/frame/constants'; -import { CHAIN } from 'apps/web/pages/api/basenames/frame/constants'; -import type { TxFrameStateType } from 'apps/web/pages/api/basenames/frame/tx'; - -if (!NEYNAR_API_KEY) { - throw new Error('missing NEYNAR_API_KEY'); -} - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: `TxSuccess Screen — Method (${req.method}) Not Allowed` }); - } - const deviceId = generateDeviceId(req); - - try { - const eventName = 'tx_submitted'; - const eventProperties = { - action: ActionType.click, - context: 'basenames_claim_frame', - componentType: ComponentType.button, - }; - - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - const body = req.body as FrameRequest; - const transactionId: string | undefined = body?.untrustedData?.transactionId; - let message; - let isValid; - let name; - - try { - if (body.trustedData) { - const result = await getFrameMessage(body, { - neynarApiKey: NEYNAR_API_KEY, - }); - isValid = result.isValid; - message = result.message; - if (!isValid) { - throw new Error('Message is not valid'); - } - if (!message) { - throw new Error('No message received'); - } - } - - const messageState = JSON.parse( - decodeURIComponent(message?.state?.serialized ?? body.untrustedData.state), - ) as TxFrameStateType; - if (!messageState) { - throw new Error('No message state received'); - } - name = messageState.targetName; - - if (!transactionId) { - throw new Error('transactionId is not valid'); - } - const txStatus = await getTransactionStatus(CHAIN, transactionId); - if (txStatus !== 'success') { - try { - const eventName = 'tx_reverted'; - const eventProperties = { - action: ActionType.process, - context: 'basenames_claim_frame', - componentType: ComponentType.service_worker, - }; - - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send(txRevertedFrame(txStatus as string, transactionId)); - } - - try { - const eventName = 'tx_succeeded'; - const eventProperties = { - action: ActionType.process, - context: 'basenames_claim_frame', - componentType: ComponentType.service_worker, - }; - - logServerSideEvent(eventName, deviceId, eventProperties); - } catch (error) { - logger.error('Could not log event:', error); - } - - return res - .status(200) - .setHeader('Content-Type', 'text/html') - .send(txSucceededFrame(name, transactionId)); - } catch (e) { - return res.status(500).json({ error: e }); - } -} diff --git a/apps/web/pages/api/basenames/frame/assets/initial-image.png b/apps/web/pages/api/basenames/frame/assets/initial-image.png deleted file mode 100644 index 0a45cf0aa75..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/initial-image.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/assets/registration-bg.png b/apps/web/pages/api/basenames/frame/assets/registration-bg.png deleted file mode 100644 index 69588270d3d..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/registration-bg.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/assets/registrationFrameImage.png.tsx b/apps/web/pages/api/basenames/frame/assets/registrationFrameImage.png.tsx deleted file mode 100644 index 2c8fe13fc80..00000000000 --- a/apps/web/pages/api/basenames/frame/assets/registrationFrameImage.png.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { ImageResponse } from '@vercel/og'; -import { NextRequest } from 'next/server'; -import { openGraphImageHeight, openGraphImageWidth } from 'apps/web/src/utils/opengraphs'; -import ImageRaw from 'apps/web/src/components/ImageRaw'; -import { DOMAIN } from 'apps/web/pages/api/basenames/frame/constants'; -import registrationImageBackground from 'apps/web/pages/api/basenames/frame/assets/registration-bg.png'; -import { getBasenameImage } from 'apps/web/src/utils/usernames'; - -export const config = { - runtime: 'edge', -}; -const secondaryFontColor = '#0052FF'; - -export default async function handler(request: NextRequest) { - const fontData = await fetch( - new URL('../../../../../src/fonts/CoinbaseDisplay-Regular.ttf', import.meta.url), - ).then(async (res) => res.arrayBuffer()); - - const url = new URL(request.url); - const username = url.searchParams.get('name') as string; - const profilePicture = getBasenameImage(username); - let imageSource = DOMAIN + profilePicture.src; - const years = url.searchParams.get('years'); - const priceInEth = url.searchParams.get('priceInEth'); - - return new ImageResponse( - ( -
-
-
- -
- - {username} - -
-
- {!years && ( - - How long do you want to register this name? - - )} - {years && ( - - Register for: {years} year{Number(years) > 1 ? 's' : ''} - - )} - {priceInEth && ( - - Cost: {priceInEth} ETH - - )} -
-
- ), - { - width: openGraphImageWidth, - height: openGraphImageHeight, - fonts: [ - { - name: 'Typewriter', - data: fontData, - style: 'normal', - }, - ], - }, - ); -} diff --git a/apps/web/pages/api/basenames/frame/assets/retry-search-image.png b/apps/web/pages/api/basenames/frame/assets/retry-search-image.png deleted file mode 100644 index 8009fea35fc..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/retry-search-image.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/assets/retrySearchFrameImage.png.tsx b/apps/web/pages/api/basenames/frame/assets/retrySearchFrameImage.png.tsx deleted file mode 100644 index 25bf043150b..00000000000 --- a/apps/web/pages/api/basenames/frame/assets/retrySearchFrameImage.png.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ImageResponse } from '@vercel/og'; -import { NextRequest } from 'next/server'; -import { openGraphImageHeight, openGraphImageWidth } from 'apps/web/src/utils/opengraphs'; -import { RawErrorStrings } from 'apps/web/src/utils/frames/basenames'; -import { DOMAIN } from 'apps/web/pages/api/basenames/frame/constants'; -import retrySearchImageBackground from 'apps/web/pages/api/basenames/frame/assets/retry-search-image.png'; - -export const config = { - runtime: 'edge', -}; - -const secondaryFontColor = '#0052FF'; -const divStyle = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', -}; - -const errorMap: Record = { - [RawErrorStrings.Unavailable]: ( -
- Sorry, that name is unavailable. -
- Search for another name -
- ), - [RawErrorStrings.TooShort]: ( -
- Sorry, that name is too short. -
- Search for another name -
- ), - [RawErrorStrings.TooLong]: ( -
- Sorry, that name is too long. -
- Search for another name -
- ), - [RawErrorStrings.DisallowedChars]: ( -
- Sorry, that name uses -
- disallowed characters. -
- Search for another name -
- ), - [RawErrorStrings.Invalid]: ( -
- Sorry, that name is invalid. -
- Search for another name -
- ), - [RawErrorStrings.InvalidUnderscore]: ( -
- Sorry, underscores are -
- only allowed at the start. -
- Search for another name -
- ), -} as const; - -export default async function handler(request: NextRequest) { - const fontData = await fetch( - new URL('../../../../../src/fonts/CoinbaseDisplay-Regular.ttf', import.meta.url), - ).then(async (res) => res.arrayBuffer()); - - const url = new URL(request.url); - const error = url.searchParams.get('error') as RawErrorStrings; - let errorMessage: JSX.Element | undefined; - if (error) { - errorMessage = errorMap[error] ?? ( -
- Sorry, unable to register that name. -
- Search for another name -
- ); - } - - return new ImageResponse( - ( -
-
- {errorMessage} -
-
- ), - { - width: openGraphImageWidth, - height: openGraphImageHeight, - fonts: [ - { - name: 'Typewriter', - data: fontData, - style: 'normal', - }, - ], - }, - ); -} diff --git a/apps/web/pages/api/basenames/frame/assets/search-image.png b/apps/web/pages/api/basenames/frame/assets/search-image.png deleted file mode 100644 index 3585804c2a5..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/search-image.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/assets/tx-failed.png b/apps/web/pages/api/basenames/frame/assets/tx-failed.png deleted file mode 100644 index 92d6124a14b..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/tx-failed.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/assets/tx-succeeded.png b/apps/web/pages/api/basenames/frame/assets/tx-succeeded.png deleted file mode 100644 index d963a7f12ce..00000000000 Binary files a/apps/web/pages/api/basenames/frame/assets/tx-succeeded.png and /dev/null differ diff --git a/apps/web/pages/api/basenames/frame/constants.ts b/apps/web/pages/api/basenames/frame/constants.ts deleted file mode 100644 index 464e5cad8c1..00000000000 --- a/apps/web/pages/api/basenames/frame/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isDevelopment } from 'apps/web/src/constants'; -import { base } from 'viem/chains'; - -export const DOMAIN = isDevelopment ? `http://localhost:3000` : 'https://www.base.org'; -export const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY; - -export const CHAIN = base; - -export const acceptedProtocols = { anonymous: 'vNext' }; diff --git a/apps/web/pages/api/basenames/frame/frameResponses.ts b/apps/web/pages/api/basenames/frame/frameResponses.ts deleted file mode 100644 index 05299b2297b..00000000000 --- a/apps/web/pages/api/basenames/frame/frameResponses.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { getFrameMetadata, getFrameHtmlResponse } from '@coinbase/onchainkit/frame'; -import { FrameMetadataResponse } from '@coinbase/onchainkit/frame/types'; -import initialImage from 'apps/web/pages/api/basenames/frame/assets/initial-image.png'; -import searchImage from 'apps/web/pages/api/basenames/frame/assets/search-image.png'; -import txSucceededImage from 'apps/web/pages/api/basenames/frame/assets/tx-succeeded.png'; -import txFailedImage from 'apps/web/pages/api/basenames/frame/assets/tx-failed.png'; -import { DOMAIN, acceptedProtocols } from 'apps/web/pages/api/basenames/frame/constants'; - -export const initialFrame: FrameMetadataResponse = getFrameMetadata({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - label: 'Claim', - }, - ], - image: { - src: `${DOMAIN}/${initialImage.src}`, - }, - postUrl: `${DOMAIN}/api/basenames/frame/01_inputSearchValue`, -}); - -export const inputSearchValueFrame = getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - label: 'Continue', - }, - ], - image: { - src: `${DOMAIN}/${searchImage.src}`, - }, - input: { - text: 'Search for a name', - }, - postUrl: `${DOMAIN}/api/basenames/frame/02_validateSearchInputAndSetYears`, -}); - -export const retryInputSearchValueFrame = (error?: string) => - getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - label: 'Search again', - }, - ], - image: { - src: `${DOMAIN}/api/basenames/frame/assets/retrySearchFrameImage.png?error=${error}`, - }, - input: { - text: 'Search for a name', - }, - postUrl: `${DOMAIN}/api/basenames/frame/02_validateSearchInputAndSetYears`, - }); - -export const buttonIndexToYears = { - 1: 1, - 2: 5, - 3: 10, - 4: 100, -}; - -export const setYearsFrame = (targetName: string, formattedTargetName: string) => - getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - label: '1 year', - }, - { - label: '5 years', - }, - { - label: '10 years', - }, - { - label: '100 years', - }, - ], - image: { - src: `${DOMAIN}/api/basenames/frame/assets/registrationFrameImage.png?name=${formattedTargetName}`, - }, - postUrl: `${DOMAIN}/api/basenames/frame/03_getPriceAndConfirm`, - state: { - targetName, - formattedTargetName, - }, - }); - -export const confirmationFrame = ( - targetName: string, - formattedTargetName: string, - targetYears: number, - registrationPriceInWei: string, - registrationPriceInEth: string, -) => - getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - action: 'tx', - label: `Claim name`, - target: `${DOMAIN}/api/basenames/frame/tx`, - postUrl: `${DOMAIN}/api/basenames/frame/04_txSubmitted`, - }, - ], - image: { - src: `${DOMAIN}/api/basenames/frame/assets/registrationFrameImage.png?name=${formattedTargetName}&years=${targetYears}&priceInEth=${registrationPriceInEth}`, - }, - postUrl: `${DOMAIN}/api/basenames/frame/04_txSubmitted`, - state: { - targetName, - formattedTargetName, - targetYears, - registrationPriceInWei, - registrationPriceInEth, - }, - }); - -export const txSucceededFrame = (name: string, transactionId: string) => - getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - action: 'link', - label: `Go to your profile`, - target: `${DOMAIN}/name/${name}`, - }, - { - action: 'link', - label: `View on block explorer`, - target: `https://basescan.org/tx/${transactionId}`, - }, - ], - image: { - src: `${DOMAIN}/${txSucceededImage.src}`, - }, - }); - -export const txRevertedFrame = (name: string, transactionId: string) => - getFrameHtmlResponse({ - isOpenFrame: true, - accepts: acceptedProtocols, - buttons: [ - { - action: 'link', - label: `View on block explorer`, - target: `https://basescan.org/tx/${transactionId}`, - }, - ], - image: { - src: `${DOMAIN}/${txFailedImage.src}`, - }, - }); diff --git a/apps/web/pages/api/basenames/frame/tx.ts b/apps/web/pages/api/basenames/frame/tx.ts deleted file mode 100644 index 8d539c7cce4..00000000000 --- a/apps/web/pages/api/basenames/frame/tx.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'; -import { - FrameRequest, - getFrameMessage, - FrameTransactionResponse, -} from '@coinbase/onchainkit/frame'; -import { encodeFunctionData, namehash } from 'viem'; -import L2ResolverAbi from 'apps/web/src/abis/L2Resolver'; -import RegistrarControllerABI from 'apps/web/src/abis/RegistrarControllerABI'; -import { formatBaseEthDomain } from 'apps/web/src/utils/usernames'; -import { - USERNAME_L2_RESOLVER_ADDRESSES, - USERNAME_REGISTRAR_CONTROLLER_ADDRESSES, -} from 'apps/web/src/addresses/usernames'; -import { CHAIN, NEYNAR_API_KEY } from 'apps/web/pages/api/basenames/frame/constants'; - -export type TxFrameStateType = { - targetName: string; - formattedTargetName: string; - targetYears: number; - registrationPriceInWei: string; - registrationPriceInEth: string; -}; - -const RESOLVER_ADDRESS = USERNAME_L2_RESOLVER_ADDRESSES[CHAIN.id]; -const REGISTRAR_CONTROLLER_ADDRESS = USERNAME_REGISTRAR_CONTROLLER_ADDRESSES[CHAIN.id]; - -if (!NEYNAR_API_KEY) { - throw new Error('missing NEYNAR_API_KEY'); -} - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: `Tx Screen — Method (${req.method}) Not Allowed` }); - } - - const body = req.body as FrameRequest; - let message; - let isValid; - let messageState; - let name; - let years; - let priceInWei; - let claimingAddress; - - try { - if (body.trustedData) { - const result = await getFrameMessage(body, { - neynarApiKey: NEYNAR_API_KEY, - }); - isValid = result.isValid; - message = result.message; - if (!isValid) { - throw new Error('Message is not valid'); - } - if (!message) { - throw new Error('No message received'); - } - } - - claimingAddress = (message?.address ?? body.untrustedData.address) as `0x${string}`; - if (!claimingAddress) { - throw new Error('No address received'); - } - - messageState = JSON.parse( - decodeURIComponent(message?.state?.serialized ?? body.untrustedData.state), - ) as TxFrameStateType; - if (!messageState) { - throw new Error('No message state received'); - } - name = messageState.targetName; - years = messageState.targetYears; - priceInWei = messageState.registrationPriceInWei; - } catch (e) { - return res.status(500).json({ error: e }); - } - - const addressData = encodeFunctionData({ - abi: L2ResolverAbi, - functionName: 'setAddr', - args: [namehash(formatBaseEthDomain(name, CHAIN.id)), claimingAddress], - }); - - const nameData = encodeFunctionData({ - abi: L2ResolverAbi, - functionName: 'setName', - args: [namehash(formatBaseEthDomain(name, CHAIN.id)), formatBaseEthDomain(name, CHAIN.id)], - }); - - const registerRequest = { - name, - owner: claimingAddress, - duration: secondsInYears(years), - resolver: RESOLVER_ADDRESS, - data: [addressData, nameData], - reverseRecord: true, - }; - - const data = encodeFunctionData({ - abi: RegistrarControllerABI, - functionName: 'register', - args: [registerRequest], - }); - - try { - const txData: FrameTransactionResponse = { - chainId: `eip155:${CHAIN.id}`, - method: 'eth_sendTransaction', - params: { - abi: [ - { - type: 'function', - name: 'register', - inputs: [ - { - name: 'request', - type: 'tuple', - internalType: 'struct RegistrarController.RegisterRequest', - components: [ - { - name: 'name', - type: 'string', - internalType: 'string', - }, - { - name: 'owner', - type: 'address', - internalType: 'address', - }, - { - name: 'duration', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'resolver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes[]', - internalType: 'bytes[]', - }, - { - name: 'reverseRecord', - type: 'bool', - internalType: 'bool', - }, - ], - }, - ], - outputs: [], - stateMutability: 'payable', - }, - ], - data, - to: REGISTRAR_CONTROLLER_ADDRESS, - value: priceInWei.toString(), - }, - }; - return res.status(200).json(txData); - } catch (error) { - return res.status(500).json({ error: 'Internal Server Error' }); - } -} - -function secondsInYears(years: number): bigint { - const secondsPerYear = 365.25 * 24 * 60 * 60; // .25 accounting for leap years - return BigInt(Math.round(years * secondsPerYear)); -} diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx index ff7db6a95ce..bbc5c106280 100644 --- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx +++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx @@ -149,7 +149,6 @@ export function ConnectWalletButton({ return (
diff --git a/apps/web/src/components/Developers/BottomCta/index.tsx b/apps/web/src/components/Developers/BottomCta/index.tsx new file mode 100644 index 00000000000..769eb87373c --- /dev/null +++ b/apps/web/src/components/Developers/BottomCta/index.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types'; +import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging'; +import { useCallback, useState } from 'react'; +import { Icon } from 'apps/web/src/components/Icon/Icon'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; + +export function BottomCta() { + const [hasCopied, setHasCopied] = useState(false); + + const handleCopy = useCallback(() => { + void navigator.clipboard.writeText('npm create onchain'); + setHasCopied(true); + setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds + }, []); + + return ( +
+
+ + Together, we're updating the Internet with a new dev platform. + + + Start building with a starter template or see documentation. + +
+ + + Documentation + +
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/Customers/index.tsx b/apps/web/src/components/Developers/Customers/index.tsx new file mode 100644 index 00000000000..bb46edddfc4 --- /dev/null +++ b/apps/web/src/components/Developers/Customers/index.tsx @@ -0,0 +1,63 @@ +'use client'; + +import logo from 'apps/web/src/components/base-org/shared/TopNavigation/assets/logo.svg'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; +import Image, { type StaticImageData } from 'next/image'; + +const LOGO_WIDTH = 200; // width of each logo in pixels +const LOGO_GAP = 40; // gap between logos in pixels +const TOTAL_LOGOS = 10; // reduced number of logos per group for better performance + +export function Customers() { + const logos = Array(TOTAL_LOGOS).fill(null); + + return ( +
+ + Powering the most consumer-friendly applications onchain. + + + {/* Auto-scrolling Logos */} +
+
+ {/* First set of logos */} + {logos.map((_, index) => ( + Base + ))} + {/* Duplicate set of logos for seamless loop */} + {logos.map((_, index) => ( + Base + ))} +
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/Hero/SearchModal.tsx b/apps/web/src/components/Developers/Hero/SearchModal.tsx new file mode 100644 index 00000000000..3efaf2fc813 --- /dev/null +++ b/apps/web/src/components/Developers/Hero/SearchModal.tsx @@ -0,0 +1,179 @@ +'use client'; + +import { Icon } from 'apps/web/src/components/Icon/Icon'; +import classNames from 'classnames'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useRef } from 'react'; +import Input from 'apps/web/src/components/Input'; +import { createPortal } from 'react-dom'; + +export function SearchModal({ + isOpen, + setIsOpen, +}: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +}) { + const searchInputRef = useRef(null); + const router = useRouter(); + + useEffect(() => { + if (isOpen) { + searchInputRef.current?.focus(); + } + }, [isOpen]); + + const handleCopyCreateOnchain = useCallback(() => { + const copyCreateOnchain = async () => { + try { + await navigator.clipboard.writeText('npm create onchain'); + } catch (error) { + console.error('Failed to copy to clipboard', error); + } + }; + + void copyCreateOnchain(); + }, []); + + const handleLaunchAgent = useCallback(() => { + window.open('https://replit.com/@CoinbaseDev/CDP-AgentKit#README.md', '_blank'); + }, []); + + const handleBuildOnchainStore = useCallback(() => { + window.open('https://onchain-commerce-template.vercel.app/', '_blank'); + }, []); + + const handleAgentKit = useCallback(() => { + router.push('/developers/agent-kit'); + }, [router]); + + const handleBaseWallet = useCallback(() => { + router.push('/developers/base-wallet'); + }, [router]); + + const handleBaseAppChains = useCallback(() => { + router.push('/developers/app-chains'); + }, [router]); + + const handleSearchInputFocus = useCallback(() => { + setIsOpen(true); + }, []); + + const handleSearchInputBlur = useCallback(() => { + setIsOpen(false); + }, []); + + if (!isOpen) { + return null; + } + + return createPortal( +
+
+
+ +
+
+
Quickstart
+ +
+
+
+ Start with a Template +
+ + +
+
+
Tools
+ + + +
+
+
+
+
, + document.body, + ); +} + +function ModalEntry({ + label, + icon, + rotateIcon, + onClick, +}: { + label: string; + icon: string; + rotateIcon?: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/apps/web/src/components/Developers/Hero/index.tsx b/apps/web/src/components/Developers/Hero/index.tsx new file mode 100644 index 00000000000..130cb4f78a5 --- /dev/null +++ b/apps/web/src/components/Developers/Hero/index.tsx @@ -0,0 +1,110 @@ +'use client'; + +import classNames from 'classnames'; +import Button from 'apps/web/src/components/base-org/Button'; +import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { useCallback, useEffect, useState } from 'react'; +import { SearchModal } from 'apps/web/src/components/Developers/Hero/SearchModal'; + +export function Hero() { + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'b') { + e.preventDefault(); + setIsSearchModalOpen(true); + } else if (e.key === 'Escape') { + setIsSearchModalOpen(false); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + const handleSearchClick = useCallback(() => { + setIsSearchModalOpen(true); + }, []); + + const handleLaunchAgent = useCallback(() => { + window.open('https://docs.cdp.coinbase.com/agentkit/docs/welcome', '_blank'); + }, []); + + const handleBuildOnchainStore = useCallback(() => { + window.open('https://onchain-commerce-template.vercel.app/', '_blank'); + }, []); + + const handleImplementSiwB = useCallback(() => { + window.open( + 'https://vocs-migration-mvp-one.vercel.app/dev-tools/identity/smart-wallet/quick-start', + '_blank', + ); + }, []); + + return ( +
+
+ + What do you want to build? + + +
+
+ +
+
+ ⌘ +
+
+ B +
+
+ +
+
+ + + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/LiveDemo/index.tsx b/apps/web/src/components/Developers/LiveDemo/index.tsx new file mode 100644 index 00000000000..1cf59551075 --- /dev/null +++ b/apps/web/src/components/Developers/LiveDemo/index.tsx @@ -0,0 +1,412 @@ +'use client'; + +// import { WalletAdvancedDefault } from '@coinbase/onchainkit/wallet'; +// import { Buy } from '@coinbase/onchainkit/buy'; +import { Checkout, CheckoutButton } from '@coinbase/onchainkit/checkout'; +import { SwapDefault } from '@coinbase/onchainkit/swap'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { createHighlighter } from 'shiki'; +import sun from './sun.svg'; +import moon from './moon.svg'; +import Image, { StaticImageData } from 'next/image'; +import { Icon } from 'apps/web/src/components/Icon/Icon'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; +import classNames from 'classnames'; +import { DynamicCryptoProviders } from 'apps/web/app/CryptoProviders.dynamic'; +import type { Token } from '@coinbase/onchainkit/token'; + +type Tab = 'onboard' | 'onramp' | 'pay' | 'swap' | 'earn'; + +const degenToken: Token[] = [ + { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: + 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }, +]; + +const ethToken: Token[] = [ + { + name: 'ETH', + address: '', + symbol: 'ETH', + decimals: 18, + image: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png', + chainId: 8453, + }, +]; + +const styles = ` + .code-snippet::-webkit-scrollbar { + width: 10px; + height: 10px; + } + .code-snippet::-webkit-scrollbar-track { + background: transparent; + } + .code-snippet::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + } + .code-snippet::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); + } + .code-snippet { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.1) transparent; + } + + /* Default theme (light) */ + .shiki, + .shiki span { + color: var(--shiki-light) !important; + background-color: var(--shiki-light-bg) !important; + font-style: var(--shiki-light-font-style) !important; + font-weight: var(--shiki-light-font-weight) !important; + text-decoration: var(--shiki-light-text-decoration) !important; + } + + /* Dark theme overrides */ + .dark .shiki, + .dark .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; + } +`; + +const codeSnippets = { + onboard: `import { + WalletAdvancedDefault, +} from '@coinbase/onchainkit/wallet'; + +function WalletAdvancedDefaultDemo() { + return +}`, + onramp: `import { Buy } from '@coinbase/onchainkit/buy'; +import type { Token } from '@coinbase/onchainkit/token'; + +function BuyDemo() { + const degenToken: Token = { + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }; + + return ( + + ); +}`, + pay: `import { + Checkout, + CheckoutButton, +} from '@coinbase/onchainkit/checkout'; + +function CheckoutDemo() { + return ( + + + + ) +}`, + swap: `import { SwapDefault } from '@coinbase/onchainkit/swap'; +import type { Token } from '@coinbase/onchainkit/token'; + +function SwapDemo() { + const { address } = useAccount(); + const ETHToken: Token = { + address: "", + chainId: 8453, + decimals: 18, + name: "Ethereum", + symbol: "ETH", + image: "", + }; + + const degenToken: Token[] = [{ + name: 'DEGEN', + address: '0x4ed4e862860bed51a9570b96d89af5e1b0efefed', + symbol: 'DEGEN', + decimals: 18, + image: 'https://d3r81g40ycuhqg.cloudfront.net/wallet/wais/3b/bf/3bbf118b5e6dc2f9e7fc607a6e7526647b4ba8f0bea87125f971446d57b296d2-MDNmNjY0MmEtNGFiZi00N2I0LWIwMTItMDUyMzg2ZDZhMWNm', + chainId: 8453, + }]; + + const swappableTokens: Token[] = [ETHToken, USDCToken]; + + return ( + + ) +}`, + earn: `import { Earn } from '@coinbase/onchainkit/earn'; + +function EarnDemo() { + return ; +}`, +}; + +export function LiveDemo() { + const [theme, setTheme] = useState<'light' | 'dark'>('dark'); + const [isMounted, setIsMounted] = useState(false); + const [highlightedCode, setHighlightedCode] = useState(''); + const [activeTab, setActiveTab] = useState('onboard'); + const [copied, setCopied] = useState(false); + + const buttonClasses = useMemo( + () => ({ + active: theme === 'dark' ? 'text-white' : 'text-dark-palette-backgroundAlternate', + inactive: + theme === 'dark' + ? 'text-dark-palette-foregroundMuted hover:text-white' + : 'text-dark-gray-50 hover:text-dark-palette-backgroundAlternate', + }), + [theme], + ); + + const demoComponent = useMemo(() => { + if (!isMounted) { + return null; + } + + switch (activeTab) { + case 'onboard': + return
WalletAdvancedDefault
+ // return ; + case 'onramp': + return
Buy
+ // return ; + case 'pay': + return ( + + + + ); + case 'swap': + return ; + case 'earn': + return
Earn yield
; + default: + return null; + } + }, [isMounted, activeTab]); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + if (!isMounted) { + return; + } + + async function highlightCode() { + const highlighter = await createHighlighter({ + themes: ['github-light', 'github-dark'], + langs: ['typescript'], + }); + + const code = highlighter.codeToHtml(codeSnippets[activeTab], { + lang: 'typescript', + themes: { + light: 'github-light', + dark: 'github-dark', + }, + defaultColor: false, + }); + + // Remove Shiki formatting + const cleanedCode = code.replace( + /]*class="([^"]*)"[^>]*>/, + (_match: string, className: string) => + `
`,
+      );
+
+      setHighlightedCode(cleanedCode);
+    }
+
+    void highlightCode();
+  }, [isMounted, activeTab]);
+
+  const handleCopy = useCallback(() => {
+    void navigator.clipboard.writeText(codeSnippets[activeTab]);
+    setCopied(true);
+    setTimeout(() => setCopied(false), 2000);
+  }, [activeTab]);
+
+  const toggleTheme = useCallback(() => {
+    setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
+  }, []);
+
+  if (!isMounted) {
+    return (
+      
+
+
+
+
Loading...
+
+
+
+
+ ); + } + + return ( +
+ +
+ Try it out! + + Experience how easy it is to build on Base. + +
+
+
+
+
+ + + + + +
+
+ +
+ + +
+
+ +
+
+ {demoComponent} +
+
+
+ {highlightedCode ? ( +
+ ) : ( +
+
Loading...
+
+ )} +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/LiveDemo/moon.svg b/apps/web/src/components/Developers/LiveDemo/moon.svg new file mode 100644 index 00000000000..5396efa109e --- /dev/null +++ b/apps/web/src/components/Developers/LiveDemo/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Developers/LiveDemo/sun.svg b/apps/web/src/components/Developers/LiveDemo/sun.svg new file mode 100644 index 00000000000..f8d9ed95b26 --- /dev/null +++ b/apps/web/src/components/Developers/LiveDemo/sun.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/src/components/Developers/Testimonials/index.tsx b/apps/web/src/components/Developers/Testimonials/index.tsx new file mode 100644 index 00000000000..8dd380c1a8a --- /dev/null +++ b/apps/web/src/components/Developers/Testimonials/index.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import classNames from 'classnames'; +import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging'; +import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; + +type Tab = 'build' | 'scale' | 'monetize'; + +type Testimonial = { + text: string; + author: string; + role: string; + tab: Tab; +}; + +const testimonials: Testimonial[] = [ + { + text: "Base's developer platform helped us launch our NFT marketplace in days instead of months.", + author: 'Sarah Chen', + role: 'CTO at ArtBlock', + tab: 'build', + }, + { + text: 'We scaled from 100 to 100k daily active users without changing our infrastructure.', + author: 'Michael Rodriguez', + role: 'Founder at GameFi', + tab: 'scale', + }, + { + text: 'Implementing Base Pay increased our revenue by 300% in the first month.', + author: 'David Kim', + role: 'CEO at DeFiPro', + tab: 'monetize', + }, +]; + +export function Testimonials() { + const [activeTab, setActiveTab] = useState('build'); + + const testimonialAnimation = useMemo( + () => ({ + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.3 }, + }), + [], + ); + + return ( +
+
+
+ {(['build', 'scale', 'monetize'] as const).map((tab) => ( + + ))} +
+ + + {testimonials + .filter((testimonial) => testimonial.tab === activeTab) + .map((testimonial) => ( + +
+ “{testimonial.text}” +
+ {testimonial.author} + {testimonial.role} +
+
+ + More stories + +
+ ))} +
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/Tools/agentKit.svg b/apps/web/src/components/Developers/Tools/agentKit.svg new file mode 100644 index 00000000000..142a285b5a1 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/agentKit.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/apps/web/src/components/Developers/Tools/baseNet.svg b/apps/web/src/components/Developers/Tools/baseNet.svg new file mode 100644 index 00000000000..18a98dd5975 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/baseNet.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/components/Developers/Tools/baseWallet.svg b/apps/web/src/components/Developers/Tools/baseWallet.svg new file mode 100644 index 00000000000..461789aa714 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/baseWallet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Developers/Tools/index.tsx b/apps/web/src/components/Developers/Tools/index.tsx new file mode 100644 index 00000000000..15ddfa5f7b9 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/index.tsx @@ -0,0 +1,88 @@ +'use client'; + +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; +import Image, { type StaticImageData } from 'next/image'; +import Link from 'next/link'; +import agentKit from 'apps/web/src/components/Developers/Tools/agentKit.svg'; +import baseNet from 'apps/web/src/components/Developers/Tools/baseNet.svg'; +import miniKit from 'apps/web/src/components/Developers/Tools/miniKit.svg'; +import baseWallet from 'apps/web/src/components/Developers/Tools/baseWallet.svg'; +import onchainKit from 'apps/web/src/components/Developers/Tools/onchainKit.svg'; +import verification from 'apps/web/src/components/Developers/Tools/verification.svg'; + +type ToolCardProps = { + title: string; + description: string; + icon: StaticImageData; + href: string; +}; + +export function Tools() { + return ( +
+ + The easiest and most rewarding way to build world-class onchain products. + +
+ + + + + + +
+
+ ); +} + +function ToolCard({ title, description, icon, href }: ToolCardProps) { + return ( + +
+ {title} +
+ + {title} + + + {description} + +
+
+ + ); +} diff --git a/apps/web/src/components/Developers/Tools/miniKit.svg b/apps/web/src/components/Developers/Tools/miniKit.svg new file mode 100644 index 00000000000..1f2af4ac60c --- /dev/null +++ b/apps/web/src/components/Developers/Tools/miniKit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/components/Developers/Tools/onchainKit.svg b/apps/web/src/components/Developers/Tools/onchainKit.svg new file mode 100644 index 00000000000..c4b9a945db2 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/onchainKit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/components/Developers/Tools/verification.svg b/apps/web/src/components/Developers/Tools/verification.svg new file mode 100644 index 00000000000..ed8c8040ba4 --- /dev/null +++ b/apps/web/src/components/Developers/Tools/verification.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Developers/UseCases/BaseAgent.tsx b/apps/web/src/components/Developers/UseCases/BaseAgent.tsx new file mode 100644 index 00000000000..115cc01bd9b --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/BaseAgent.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { Icon } from 'apps/web/src/components/Icon/Icon'; +import { motion, useInView } from 'framer-motion'; +import { useMemo, useRef } from 'react'; + +export function AnimatedBaseAgent() { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, amount: 0.4 }); + + const container = useMemo( + () => ({ + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.3, + delayChildren: 0.3, + }, + }, + }), + [], + ); + + const item = useMemo( + () => ({ + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0 }, + }), + [], + ); + + return ( +
+ {/* Header */} + + + + Based Agent + + + + + +
+ + + +
+ + + +
+ + + +
+ + +
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/Defi.tsx b/apps/web/src/components/Developers/UseCases/Defi.tsx new file mode 100644 index 00000000000..124071eaaf0 --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/Defi.tsx @@ -0,0 +1,82 @@ +'use client'; + +import Image, { StaticImageData } from 'next/image'; +import { useState, useEffect } from 'react'; +import NumberFlow from '@number-flow/react'; +import usdc from 'apps/web/public/images/partners/usdc.svg'; + +const initialEarned = 4124.39; +const apy = 6.97; + +export function AnimatedDefi() { + const [currentEarned, setCurrentEarned] = useState(initialEarned); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentEarned((prev) => { + const tier = Math.random() * 100; + let increment; + + if (tier < 50) { + increment = Math.random() * 15.4 + 0.5; + } else if (tier < 80) { + increment = Math.random() * 8.2 + 1.5; + } else if (tier < 95) { + increment = Math.random() * 5.75 + 2.5; + } else { + increment = Math.random() * 3.0 + 4.0; + } + + return prev + increment; + }); + }, 1000); + + return () => clearInterval(interval); + }, []); + + const percentageIncrease = ((currentEarned - initialEarned) / initialEarned) * 100; + + return ( +
+
+
+ USDC Icon +
+
+
APY
+
{apy}%
+
+
+
+ Total Earned +
+
+ $ + + + +{percentageIncrease.toFixed(2)}% + +
+
+ +
+
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/Gassless.tsx b/apps/web/src/components/Developers/UseCases/Gassless.tsx new file mode 100644 index 00000000000..8a7962a7f98 --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/Gassless.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export function AnimatedGasless() { + const [barHeights, setBarHeights] = useState([]); + + useEffect(() => { + const initialHeights = Array(16) + .fill(0) + .map( + () => Math.random() * 40 + 40, // Random values between 40% and 80% for initial state + ); + setBarHeights(initialHeights); + + const interval = setInterval(() => { + setBarHeights( + (prev) => prev.map(() => Math.random() * 100), // Random values between 0 and 100% ($0 to $1.5K) + ); + }, 2000); + + return () => clearInterval(interval); + }, []); + + return ( +
+
+

Gas sponsored

+ +
+
+ $1.5K + $1K + $500 + $0 +
+ +
+ {barHeights.map((height, index) => ( +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/Onboarding.tsx b/apps/web/src/components/Developers/UseCases/Onboarding.tsx new file mode 100644 index 00000000000..d25e613891d --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/Onboarding.tsx @@ -0,0 +1,80 @@ +'use client'; + +import classNames from 'classnames'; +import { useEffect, useState, useRef, useCallback } from 'react'; +import Image, { StaticImageData } from 'next/image'; +import logo from 'apps/web/src/components/base-org/shared/TopNavigation/assets/logo.svg'; + +export function AnimatedOnboarding() { + const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ + x: 0, + y: 0, + }); + const [isVisible, setIsVisible] = useState(false); + const topRectangleRef = useRef(null); + + const updateMousePosition = useCallback((e: React.MouseEvent) => { + const container = e.currentTarget.getBoundingClientRect(); + setMousePosition({ + x: e.clientX - container.left, + y: e.clientY - container.top, + }); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setIsVisible(true); + } + }); + }, + { + threshold: 0.5, // Trigger when 50% of the element is visible + }, + ); + + if (topRectangleRef.current) { + observer.observe(topRectangleRef.current); + } + + return () => { + if (topRectangleRef.current) { + observer.unobserve(topRectangleRef.current); + } + }; + }, []); + + return ( +
+
+
+ Base Logo + Coinbase Wallet +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/Payments.tsx b/apps/web/src/components/Developers/UseCases/Payments.tsx new file mode 100644 index 00000000000..367f16c31ab --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/Payments.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +type ButtonState = 'default' | 'loading' | 'success'; + +export function AnimatedPayment() { + const [buttonState, setButtonState] = useState('default'); + + useEffect(() => { + const animationCycle = () => { + setButtonState('loading'); + setTimeout(() => { + setButtonState('success'); + setTimeout(() => { + setButtonState('default'); + }, 800); + }, 1200); + }; + + const interval = setInterval(animationCycle, 4000); + return () => clearInterval(interval); + }, []); + + return ( +
+
+ + {/* Total amount with justified spacing */} +
+ Total + $69.42 +
+ + {/* */} + + {/* Pay with crypto button */} + +
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/Social.tsx b/apps/web/src/components/Developers/UseCases/Social.tsx new file mode 100644 index 00000000000..6c2a7deaadc --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/Social.tsx @@ -0,0 +1,77 @@ +'use client'; + +import LottieAnimation from 'apps/web/src/components/LottieAnimation'; +import { getBasenameAnimation } from 'apps/web/src/utils/usernames'; +import { motion } from 'framer-motion'; +import { useMemo } from 'react'; + +export function AnimatedSocial() { + const animation = getBasenameAnimation('basename'); + const heartAnimations = useMemo( + () => ({ + animate: { + scale: [1, 1.2, 1], + fill: ['transparent', '#ef4444', '#ef4444'], + }, + transition: { + duration: 1, + times: [0, 0.5, 1], + repeat: Infinity, + repeatDelay: 2, + }, + }), + [], + ); + + const buttonAnimations = useMemo( + () => ({ + initial: { opacity: 0, x: 20 }, + animate: { opacity: 1, x: 0 }, + transition: { + type: 'spring', + stiffness: 100, + delay: 0.1, + }, + }), + [], + ); + return ( +
+
+ +
+
+
+
+ + + + + + + Collect + +
+
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/UseCaseBlock.tsx b/apps/web/src/components/Developers/UseCases/UseCaseBlock.tsx new file mode 100644 index 00000000000..8b3f9546647 --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/UseCaseBlock.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging'; +import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types'; +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; + +export function UseCaseBlock({ + children, + title, + description, + href, +}: { + children: React.ReactNode; + title: string; + description: string; + href: string; +}) { + return ( +
+
+ {children} +
+
+ + {title} + + + {description} + +
+ + Get started + +
+ ); +} diff --git a/apps/web/src/components/Developers/UseCases/index.tsx b/apps/web/src/components/Developers/UseCases/index.tsx new file mode 100644 index 00000000000..7c9067c7af5 --- /dev/null +++ b/apps/web/src/components/Developers/UseCases/index.tsx @@ -0,0 +1,70 @@ +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; +import { UseCaseBlock } from 'apps/web/src/components/Developers/UseCases/UseCaseBlock'; +import { AnimatedOnboarding } from 'apps/web/src/components/Developers/UseCases/Onboarding'; +import { AnimatedPayment } from 'apps/web/src/components/Developers/UseCases/Payments'; +import { AnimatedBaseAgent } from 'apps/web/src/components/Developers/UseCases/BaseAgent'; +import { AnimatedSocial } from 'apps/web/src/components/Developers/UseCases/Social'; +import { AnimatedDefi } from 'apps/web/src/components/Developers/UseCases/Defi'; +import { AnimatedGasless } from 'apps/web/src/components/Developers/UseCases/Gassless'; + +export async function UseCases() { + return ( +
+
+ + Build, scale, and monetize. + + + Everything you need to launch onchain products. + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/apps/web/src/components/Developers/WhyBase/index.tsx b/apps/web/src/components/Developers/WhyBase/index.tsx new file mode 100644 index 00000000000..e370385f7d5 --- /dev/null +++ b/apps/web/src/components/Developers/WhyBase/index.tsx @@ -0,0 +1,72 @@ +import Title from 'apps/web/src/components/base-org/typography/Title'; +import { TitleLevel } from 'apps/web/src/components/base-org/typography/Title/types'; +import Image, { type StaticImageData } from 'next/image'; +import integration from 'apps/web/src/components/Developers/WhyBase/integration.svg'; +import support from 'apps/web/src/components/Developers/WhyBase/support.svg'; +import megaphone from 'apps/web/src/components/Developers/WhyBase/megaphone.svg'; +import security from 'apps/web/src/components/Developers/WhyBase/security.svg'; + +type ValuePropProps = { + title: string; + description: string; + icon: StaticImageData; +}; + +export function WhyBase() { + return ( +
+
+
+ + Built by Base with the Coinbase connection. + + + Grow faster with distribution through Base's social graph and integrations with + Coinbase products. + +
+ +
+ + + + +
+
+
+ ); +} + +function ValueProp({ title, description, icon }: ValuePropProps) { + return ( +
+
+
+ {title} + + {title} + +
+ + {description} + +
+
+ ); +} diff --git a/apps/web/src/components/Developers/WhyBase/integration.svg b/apps/web/src/components/Developers/WhyBase/integration.svg new file mode 100644 index 00000000000..43036132ee6 --- /dev/null +++ b/apps/web/src/components/Developers/WhyBase/integration.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Developers/WhyBase/megaphone.svg b/apps/web/src/components/Developers/WhyBase/megaphone.svg new file mode 100644 index 00000000000..4847c1cdddf --- /dev/null +++ b/apps/web/src/components/Developers/WhyBase/megaphone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Developers/WhyBase/security.svg b/apps/web/src/components/Developers/WhyBase/security.svg new file mode 100644 index 00000000000..7194dd824d5 --- /dev/null +++ b/apps/web/src/components/Developers/WhyBase/security.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Developers/WhyBase/support.svg b/apps/web/src/components/Developers/WhyBase/support.svg new file mode 100644 index 00000000000..07e531a4c8d --- /dev/null +++ b/apps/web/src/components/Developers/WhyBase/support.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Icon/Icon.tsx b/apps/web/src/components/Icon/Icon.tsx index 49e0a5fc45b..e6f8b576217 100644 --- a/apps/web/src/components/Icon/Icon.tsx +++ b/apps/web/src/components/Icon/Icon.tsx @@ -591,6 +591,24 @@ const ICONS: Record JSX.Element> = { /> ), + terminal: ({ color, width, height }: SvgProps) => ( + + + + + ), }; export function Icon({ name, color = 'white', width = '24', height = '24' }: IconProps) { diff --git a/apps/web/src/components/base-org/Button/index.tsx b/apps/web/src/components/base-org/Button/index.tsx index 928a4c1dff3..feeda18fc37 100644 --- a/apps/web/src/components/base-org/Button/index.tsx +++ b/apps/web/src/components/base-org/Button/index.tsx @@ -10,6 +10,7 @@ export type ButtonProps = ButtonHTMLAttributes & { variant?: ButtonVariants; size?: ButtonSizes; iconName?: IconProps['name']; + iconSize?: IconProps['width']; roundedFull?: boolean; fullWidth?: boolean; }; @@ -26,6 +27,9 @@ const variantStyles: Record = { // White outlined [ButtonVariants.Outlined]: 'bg-transparent text-white border border-white hover:bg-white hover:text-black active:bg-[#E3E7E9]', + + // Secondary Outlined + [ButtonVariants.SecondaryOutline]: 'bg-transparent border border-gray-muted hover:bg-white/10', }; const sizeStyles: Record = { @@ -51,13 +55,14 @@ export default function Button({ variant = ButtonVariants.Primary, size = ButtonSizes.Medium, iconName, + iconSize = sizeIconRatio[size], roundedFull = false, className, fullWidth = false, }: ButtonProps) { const buttonClasses = classNames( // Shared - base - 'text-md px-4 py-2 whitespace-nowrap', + 'text-base px-4 py-2 whitespace-nowrap', // Shared - layout 'flex items-center justify-center', @@ -82,8 +87,6 @@ export default function Button({ className, ); - const iconSize = sizeIconRatio[size]; - return (