diff --git a/apps/web/src/components/Basenames/RegistrationForm/index.tsx b/apps/web/src/components/Basenames/RegistrationForm/index.tsx index 5cb624a1d03..f54e1641a17 100644 --- a/apps/web/src/components/Basenames/RegistrationForm/index.tsx +++ b/apps/web/src/components/Basenames/RegistrationForm/index.tsx @@ -31,6 +31,10 @@ import { formatEtherPrice } from 'apps/web/src/utils/formatEtherPrice'; import { formatUsdPrice } from 'apps/web/src/utils/formatUsdPrice'; import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit'; import { Button, ButtonSizes, ButtonVariants } from 'apps/web/src/components/Button/Button'; +import { + useSwitchToBasenameChain, + useYearSelectionCallbacks, +} from 'apps/web/src/components/Basenames/basenameFormUtils'; export default function RegistrationForm() { const { chain: connectedChain, address } = useAccount(); @@ -41,10 +45,7 @@ export default function RegistrationForm() { const { basenameChain } = useBasenameChain(); const { switchChain } = useSwitchChain(); - const switchToIntendedNetwork = useCallback( - () => switchChain({ chainId: basenameChain.id }), - [basenameChain.id, switchChain], - ); + const switchToIntendedNetwork = useSwitchToBasenameChain(switchChain, basenameChain.id); const { selectedName, @@ -72,17 +73,10 @@ export default function RegistrationForm() { setLearnMoreAboutDiscountsModalOpen((open) => !open); }, [logEventWithContext, setLearnMoreAboutDiscountsModalOpen]); - const increment = useCallback(() => { - logEventWithContext('registration_form_increment_year', ActionType.click); - - setYears((n) => n + 1); - }, [logEventWithContext, setYears]); - - const decrement = useCallback(() => { - logEventWithContext('registration_form_decement_year', ActionType.click); - - setYears((n) => (n > 1 ? n - 1 : n)); - }, [logEventWithContext, setYears]); + const { increment, decrement } = useYearSelectionCallbacks(setYears, { + onIncrement: () => logEventWithContext('registration_form_increment_year', ActionType.click), + onDecrement: () => logEventWithContext('registration_form_decrement_year', ActionType.click), + }); const ethUsdPrice = useEthPriceFromUniswap(); const { data: initialPrice } = useNameRegistrationPrice(selectedName, years); diff --git a/apps/web/src/components/Basenames/RenewalForm/index.tsx b/apps/web/src/components/Basenames/RenewalForm/index.tsx index 1679c715748..d260e002370 100644 --- a/apps/web/src/components/Basenames/RenewalForm/index.tsx +++ b/apps/web/src/components/Basenames/RenewalForm/index.tsx @@ -14,6 +14,10 @@ import { RenewalButton } from './RenewalButton'; import { formatUsdPrice } from 'apps/web/src/utils/formatUsdPrice'; import { formatEtherPrice } from 'apps/web/src/utils/formatEtherPrice'; import YearSelector from 'apps/web/src/components/Basenames/YearSelector'; +import { + useSwitchToBasenameChain, + useYearSelectionCallbacks, +} from 'apps/web/src/components/Basenames/basenameFormUtils'; export default function RenewalForm() { const { chain: connectedChain, address } = useAccount(); @@ -24,23 +28,14 @@ export default function RenewalForm() { const { years, setYears, renewBasename, price, isPending, expirationDate } = useRenewal(); - const switchToIntendedNetwork = useCallback( - () => switchChain({ chainId: basenameChain.id }), - [basenameChain.id, switchChain], - ); + const switchToIntendedNetwork = useSwitchToBasenameChain(switchChain, basenameChain.id); const isOnSupportedNetwork = useMemo( () => connectedChain && supportedChainIds.includes(connectedChain.id), [connectedChain], ); - const increment = useCallback(() => { - setYears((n) => n + 1); - }, [setYears]); - - const decrement = useCallback(() => { - setYears((n) => (n > 1 ? n - 1 : n)); - }, [setYears]); + const { increment, decrement } = useYearSelectionCallbacks(setYears); const ethUsdPrice = useEthPriceFromUniswap(); diff --git a/apps/web/src/components/Basenames/basenameFormUtils.ts b/apps/web/src/components/Basenames/basenameFormUtils.ts new file mode 100644 index 00000000000..8dbe5df0a1c --- /dev/null +++ b/apps/web/src/components/Basenames/basenameFormUtils.ts @@ -0,0 +1,36 @@ +import { useCallback } from 'react'; + +/** + * Creates a callback to switch to a specific chain + */ +export function useSwitchToBasenameChain( + switchChain: (params: { chainId: number }) => void, + chainId: number, +) { + return useCallback(() => switchChain({ chainId }), [chainId, switchChain]); +} + +/** + * Creates callbacks for incrementing and decrementing year selection + */ +export function useYearSelectionCallbacks( + setYears: (fn: (n: number) => number) => void, + options?: { + onIncrement?: () => void; + onDecrement?: () => void; + }, +) { + const { onIncrement, onDecrement } = options ?? {}; + + const increment = useCallback(() => { + onIncrement?.(); + setYears((n) => n + 1); + }, [setYears, onIncrement]); + + const decrement = useCallback(() => { + onDecrement?.(); + setYears((n) => (n > 1 ? n - 1 : n)); + }, [setYears, onDecrement]); + + return { increment, decrement }; +} diff --git a/apps/web/src/components/Builders/AgentKit/Hero.tsx b/apps/web/src/components/Builders/AgentKit/Hero.tsx index b44ea0075a2..3d32c045fcd 100644 --- a/apps/web/src/components/Builders/AgentKit/Hero.tsx +++ b/apps/web/src/components/Builders/AgentKit/Hero.tsx @@ -1,5 +1,3 @@ -'use client'; - 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'; @@ -9,16 +7,14 @@ import { Demo } from 'apps/web/src/components/Builders/AgentKit/Demo'; import { AGENTKIT_DOCS_LINK } from 'apps/web/src/components/Builders/AgentKit/links'; import { Icon } from 'apps/web/src/components/Icon/Icon'; import Image, { StaticImageData } from 'next/image'; -import { useCallback, useState } from 'react'; +import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard'; +import { useCallback } from 'react'; export function Hero() { - const [hasCopied, setHasCopied] = useState(false); - + const { hasCopied, copyToClipboard } = useCopyToClipboard(); const handleCopy = useCallback(() => { - void navigator.clipboard.writeText('npx create-agentkit-app'); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds - }, []); + copyToClipboard('npx create-agentkit-app'); + }, [copyToClipboard]); return (
diff --git a/apps/web/src/components/Builders/MiniKit/Hero.tsx b/apps/web/src/components/Builders/MiniKit/Hero.tsx index 6c5c3aece47..cbdf2fc54d0 100644 --- a/apps/web/src/components/Builders/MiniKit/Hero.tsx +++ b/apps/web/src/components/Builders/MiniKit/Hero.tsx @@ -1,7 +1,4 @@ -'use client'; - import Image, { StaticImageData } from 'next/image'; -import { useCallback, useState } from 'react'; 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'; @@ -9,18 +6,17 @@ import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/Bu import { HeaderAnimation } from 'apps/web/src/components/Builders/MiniKit/HeaderAnimation'; import minikit from 'apps/web/src/components/Builders/MiniKit/minikit.svg'; import { Icon } from 'apps/web/src/components/Icon/Icon'; +import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard'; +import { useCallback } from 'react'; export const GET_STARTED_URL = 'https://docs.base.org/builderkits/minikit/overview'; const MINIKIT_COMMAND = 'npx create-onchain --mini'; export function Hero() { - const [hasCopied, setHasCopied] = useState(false); - + const { hasCopied, copyToClipboard } = useCopyToClipboard(); const handleCopy = useCallback(() => { - void navigator.clipboard.writeText(MINIKIT_COMMAND); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds - }, []); + copyToClipboard(MINIKIT_COMMAND); + }, [copyToClipboard]); return (
diff --git a/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx b/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx index 3d88f10da29..b841a497aed 100644 --- a/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx +++ b/apps/web/src/components/Builders/Onchainkit/CtaBanner.tsx @@ -1,22 +1,18 @@ -'use client'; - import { ButtonVariants } from 'apps/web/src/components/base-org/Button/types'; import { CtaBanner as DefaultCtaBanner } from 'apps/web/src/components/Builders/Shared/CtaBanner'; import { ButtonWithLinkAndEventLogging } from 'apps/web/src/components/Button/ButtonWithLinkAndEventLogging'; -import { useCallback, useState } from 'react'; import { Icon } from 'apps/web/src/components/Icon/Icon'; +import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard'; +import { useCallback } from 'react'; const ONCHAINKIT_DOCS_LINK = 'https://docs.base.org/builderkits/onchainkit/getting-started'; const ONCHAINKIT_AI_DOCS_LINK = 'https://docs.base.org/builderkits/onchainkit/llms.txt'; export function CtaBanner() { - const [hasCopied, setHasCopied] = useState(false); - + const { hasCopied, copyToClipboard } = useCopyToClipboard(); const handleCopy = useCallback(() => { - void navigator.clipboard.writeText('npm create onchain'); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds - }, []); + copyToClipboard('npm create onchain'); + }, [copyToClipboard]); return ( { - void navigator.clipboard.writeText('npm create onchain'); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds - }, []); + copyToClipboard('npm create onchain'); + }, [copyToClipboard]); return (
diff --git a/apps/web/src/components/Builders/Shared/BottomCta/index.tsx b/apps/web/src/components/Builders/Shared/BottomCta/index.tsx index 410c65364f6..87369f13d9e 100644 --- a/apps/web/src/components/Builders/Shared/BottomCta/index.tsx +++ b/apps/web/src/components/Builders/Shared/BottomCta/index.tsx @@ -1,19 +1,15 @@ -'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 { CtaBanner } from 'apps/web/src/components/Builders/Shared/BottomCta/CtaBanner'; +import { useCopyToClipboard } from 'apps/web/src/hooks/useCopyToClipboard'; +import { useCallback } from 'react'; export function BottomCta() { - const [hasCopied, setHasCopied] = useState(false); - + const { hasCopied, copyToClipboard } = useCopyToClipboard(); const handleCopy = useCallback(() => { - void navigator.clipboard.writeText('npm create onchain'); - setHasCopied(true); - setTimeout(() => setHasCopied(false), 2000); // Reset after 2 seconds - }, []); + copyToClipboard('npm create onchain'); + }, [copyToClipboard]); return ( (null); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const copyToClipboard = useCallback( + async (text: string) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + try { + await navigator.clipboard.writeText(text); + setHasCopied(true); + + timeoutRef.current = setTimeout(() => { + setHasCopied(false); + timeoutRef.current = null; + }, timeout); + } catch (error) { + // Silently fail - clipboard access may be restricted + console.warn('Failed to copy to clipboard:', error); + } + }, + [timeout], + ); + + return { hasCopied, copyToClipboard }; +} diff --git a/apps/web/src/utils/formatWei.ts b/apps/web/src/utils/formatWei.ts deleted file mode 100644 index 5ae0c2ce818..00000000000 --- a/apps/web/src/utils/formatWei.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { formatEther, parseEther } from 'viem'; - -export function formatWei(wei?: bigint): bigint | '...' { - if (wei === undefined) { - return '...'; - } - - const priceInEth = formatEther(wei); - return parseEther(priceInEth.toString()); -} - diff --git a/apps/web/src/utils/logEvent.ts b/apps/web/src/utils/logEvent.ts deleted file mode 100644 index cf75328cf73..00000000000 --- a/apps/web/src/utils/logEvent.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare const window: Window & - typeof globalThis & { - ClientAnalytics: { - logEvent: (name: string, event: unknown) => void; - }; - }; - -export default function logEvent(name: string, event: unknown) { - if (window.ClientAnalytics) { - window.ClientAnalytics?.logEvent(name, event); - } -} - -export function identify(event: unknown) { - if (window.ClientAnalytics) { - window.ClientAnalytics?.logEvent('identify', event); - } -} diff --git a/apps/web/src/utils/weiToEth.ts b/apps/web/src/utils/weiToEth.ts deleted file mode 100644 index 7efceffc7f8..00000000000 --- a/apps/web/src/utils/weiToEth.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { formatEther } from 'viem'; - -export function weiToEth(wei?: bigint): number | '...' { - if (wei === undefined) { - return '...'; - } - const eth = parseFloat(formatEther(wei)); - if (eth < 0.001) { - return parseFloat(eth.toFixed(4)); - } else { - return parseFloat(eth.toFixed(3)); - } -}