diff --git a/src/Routes/Routes.tsx b/src/Routes/Routes.tsx
index bfae8142182..80b56aeb570 100644
--- a/src/Routes/Routes.tsx
+++ b/src/Routes/Routes.tsx
@@ -1,5 +1,6 @@
import { LanguageTypeEnum } from 'constants/LanguageTypeEnum'
-import { useEffect, useMemo, useState } from 'react'
+import type { Location } from 'history'
+import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useDispatch } from 'react-redux'
import { matchPath, Redirect, Route, Switch, useLocation } from 'react-router-dom'
import { Layout } from 'components/Layout/Layout'
@@ -16,16 +17,9 @@ import { selectSelectedLocale } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
import { PrivateRoute } from './PrivateRoute'
-
-function useLocationBackground() {
- const location = useLocation<{ background: any }>()
- const background = location.state && location.state.background
- return { background, location }
-}
-
-export const Routes = () => {
+export const Routes = memo(() => {
const dispatch = useDispatch()
- const { background, location } = useLocationBackground()
+ const location = useLocation<{ background: Location }>()
const { connectDemo, state } = useWallet()
const { appRoutes } = useBrowserRouter()
const hasWallet = Boolean(state.walletInfo?.deviceId) || state.isLoadingLocalWallet
@@ -87,20 +81,20 @@ export const Routes = () => {
[appRoutes, hasWallet, isUnstableRoute, location],
)
+ const locationProps = useMemo(() => location.state?.background || location, [location])
+
+ const renderRedirect = useCallback(() => {
+ return shouldRedirectDemoRoute ? (
+
+ ) : null
+ }, [matchDemoPath?.params.appRoute, shouldRedirectDemoRoute])
+
return (
-
-
- {() => {
- return shouldRedirectDemoRoute ? (
-
- ) : null
- }}
-
+
+ {renderRedirect}
@@ -131,4 +125,4 @@ export const Routes = () => {
)
-}
+})
diff --git a/src/components/AccountDropdown/AccountDropdown.tsx b/src/components/AccountDropdown/AccountDropdown.tsx
index ee05740db75..e0ea104b977 100644
--- a/src/components/AccountDropdown/AccountDropdown.tsx
+++ b/src/components/AccountDropdown/AccountDropdown.tsx
@@ -27,7 +27,7 @@ import { UtxoAccountType } from '@shapeshiftoss/types'
import { chain } from 'lodash'
import isEmpty from 'lodash/isEmpty'
import sortBy from 'lodash/sortBy'
-import React, { type FC, useCallback, useEffect, useMemo, useState } from 'react'
+import React, { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import { useSelector } from 'react-redux'
import { bnOrZero } from 'lib/bignumber/bignumber'
@@ -76,254 +76,258 @@ const utxoAccountTypeToDisplayPriority = (accountType: UtxoAccountType | undefin
}
}
-export const AccountDropdown: FC = ({
- assetId,
- buttonProps,
- onChange: handleChange,
- disabled,
- defaultAccountId,
- listProps,
- autoSelectHighestBalance,
- boxProps,
- showLabel = true,
-}) => {
- const { chainId } = fromAssetId(assetId)
+export const AccountDropdown: FC = memo(
+ ({
+ assetId,
+ buttonProps,
+ onChange: handleChange,
+ disabled,
+ defaultAccountId,
+ listProps,
+ autoSelectHighestBalance,
+ boxProps,
+ showLabel = true,
+ }) => {
+ const { chainId } = fromAssetId(assetId)
- const color = useColorModeValue('black', 'white')
- const labelColor = useColorModeValue('gray.600', 'gray.500')
+ const color = useColorModeValue('black', 'white')
+ const labelColor = useColorModeValue('gray.600', 'gray.500')
- const filter = useMemo(() => ({ assetId }), [assetId])
- const accountIds = useAppSelector((s: ReduxState) =>
- selectPortfolioAccountIdsByAssetId(s, filter),
- )
+ const filter = useMemo(() => ({ assetId }), [assetId])
+ const accountIds = useAppSelector((s: ReduxState) =>
+ selectPortfolioAccountIdsByAssetId(s, filter),
+ )
- const translate = useTranslate()
- const asset = useAppSelector((s: ReduxState) => selectAssetById(s, assetId))
+ const translate = useTranslate()
+ const asset = useAppSelector((s: ReduxState) => selectAssetById(s, assetId))
- if (!asset) throw new Error(`AccountDropdown: no asset found for assetId ${assetId}!`)
+ if (!asset) throw new Error(`AccountDropdown: no asset found for assetId ${assetId}!`)
- const accountBalances = useSelector(selectPortfolioAccountBalancesBaseUnit)
- const accountMetadata = useSelector(selectPortfolioAccountMetadata)
- const highestUserCurrencyBalanceAccountId = useAppSelector(state =>
- selectHighestUserCurrencyBalanceAccountByAssetId(state, { assetId }),
- )
- const [selectedAccountId, setSelectedAccountId] = useState(
- defaultAccountId,
- )
+ const accountBalances = useSelector(selectPortfolioAccountBalancesBaseUnit)
+ const accountMetadata = useSelector(selectPortfolioAccountMetadata)
+ const highestUserCurrencyBalanceAccountId = useAppSelector(state =>
+ selectHighestUserCurrencyBalanceAccountByAssetId(state, { assetId }),
+ )
+ const [selectedAccountId, setSelectedAccountId] = useState(
+ defaultAccountId,
+ )
- // very suspicious of this
- // Poor man's componentDidUpdate until we figure out why this re-renders like crazy
- const previousSelectedAccountId = usePrevious(selectedAccountId)
- const isDropdownDisabled = disabled || accountIds.length <= 1
+ // very suspicious of this
+ // Poor man's componentDidUpdate until we figure out why this re-renders like crazy
+ const previousSelectedAccountId = usePrevious(selectedAccountId)
+ const isDropdownDisabled = disabled || accountIds.length <= 1
- /**
- * react on selectedAccountId change
- */
- useEffect(() => {
- if (isEmpty(accountMetadata)) return // not enough data to set an AccountId
- if (!selectedAccountId || previousSelectedAccountId === selectedAccountId) return // no-op, this would fire onChange an infuriating amount of times
- handleChange(selectedAccountId)
- }, [accountMetadata, previousSelectedAccountId, selectedAccountId, handleChange])
-
- /**
- * react on accountIds on first render
- */
- useEffect(() => {
- if (!accountIds.length) return
- const validatedAccountIdFromArgs = accountIds.find(accountId => accountId === defaultAccountId)
- const firstAccountId = accountIds[0]
- // Use the first accountId if we don't have a valid defaultAccountId
- const preSelectedAccountId =
- validatedAccountIdFromArgs ??
- (autoSelectHighestBalance ? highestUserCurrencyBalanceAccountId : undefined) ??
- firstAccountId
/**
- * assert asset the chainId of the accountId and assetId match
+ * react on selectedAccountId change
*/
- const accountIdChainId = fromAccountId(preSelectedAccountId).chainId
- const assetIdChainId = fromAssetId(assetId).chainId
- if (accountIdChainId !== assetIdChainId) {
- throw new Error('AccountDropdown: chainId mismatch!')
- }
- setSelectedAccountId(preSelectedAccountId)
- }, [
- assetId,
- accountIds,
- defaultAccountId,
- highestUserCurrencyBalanceAccountId,
- autoSelectHighestBalance,
- ])
+ useEffect(() => {
+ if (isEmpty(accountMetadata)) return // not enough data to set an AccountId
+ if (!selectedAccountId || previousSelectedAccountId === selectedAccountId) return // no-op, this would fire onChange an infuriating amount of times
+ handleChange(selectedAccountId)
+ }, [accountMetadata, previousSelectedAccountId, selectedAccountId, handleChange])
- const handleClick = useCallback((accountId: AccountId) => setSelectedAccountId(accountId), [])
+ /**
+ * react on accountIds on first render
+ */
+ useEffect(() => {
+ if (!accountIds.length) return
+ const validatedAccountIdFromArgs = accountIds.find(
+ accountId => accountId === defaultAccountId,
+ )
+ const firstAccountId = accountIds[0]
+ // Use the first accountId if we don't have a valid defaultAccountId
+ const preSelectedAccountId =
+ validatedAccountIdFromArgs ??
+ (autoSelectHighestBalance ? highestUserCurrencyBalanceAccountId : undefined) ??
+ firstAccountId
+ /**
+ * assert asset the chainId of the accountId and assetId match
+ */
+ const accountIdChainId = fromAccountId(preSelectedAccountId).chainId
+ const assetIdChainId = fromAssetId(assetId).chainId
+ if (accountIdChainId !== assetIdChainId) {
+ throw new Error('AccountDropdown: chainId mismatch!')
+ }
+ setSelectedAccountId(preSelectedAccountId)
+ }, [
+ assetId,
+ accountIds,
+ defaultAccountId,
+ highestUserCurrencyBalanceAccountId,
+ autoSelectHighestBalance,
+ ])
- /**
- * memoized view bits and bobs
- */
- const accountLabel = useMemo(
- () => selectedAccountId && accountIdToLabel(selectedAccountId),
- [selectedAccountId],
- )
+ const handleClick = useCallback((accountId: AccountId) => setSelectedAccountId(accountId), [])
- const accountNumber: number | undefined = useMemo(
- () => selectedAccountId && accountMetadata[selectedAccountId]?.bip44Params?.accountNumber,
- [accountMetadata, selectedAccountId],
- )
+ /**
+ * memoized view bits and bobs
+ */
+ const accountLabel = useMemo(
+ () => selectedAccountId && accountIdToLabel(selectedAccountId),
+ [selectedAccountId],
+ )
- const getAccountIdsSortedByUtxoAccountType = useCallback(
- (accountIds: AccountId[]): AccountId[] => {
- return sortBy(accountIds, accountId =>
- utxoAccountTypeToDisplayPriority(accountMetadata[accountId]?.accountType),
- )
- },
- [accountMetadata],
- )
+ const accountNumber: number | undefined = useMemo(
+ () => selectedAccountId && accountMetadata[selectedAccountId]?.bip44Params?.accountNumber,
+ [accountMetadata, selectedAccountId],
+ )
- const getAccountIdsSortedByBalance = useCallback(
- (accountIds: AccountId[]): AccountId[] =>
- chain(accountIds)
- .sortBy(accountIds, accountId =>
- bnOrZero(accountBalances?.[accountId]?.[assetId] ?? 0).toNumber(),
+ const getAccountIdsSortedByUtxoAccountType = useCallback(
+ (accountIds: AccountId[]): AccountId[] => {
+ return sortBy(accountIds, accountId =>
+ utxoAccountTypeToDisplayPriority(accountMetadata[accountId]?.accountType),
)
- .reverse()
- .value(),
- [accountBalances, assetId],
- )
+ },
+ [accountMetadata],
+ )
- const menuOptions = useMemo(() => {
- const makeTitle = (accountId: AccountId): string => {
- /**
- * for UTXO chains, we want the title to be the account type
- * for account-based chains, we want the title to be the asset name
- */
- const { chainNamespace } = fromChainId(chainId)
- switch (chainNamespace) {
- case CHAIN_NAMESPACE.Utxo: {
- return accountIdToLabel(accountId)
- }
- default: {
- return asset?.name ?? ''
+ const getAccountIdsSortedByBalance = useCallback(
+ (accountIds: AccountId[]): AccountId[] =>
+ chain(accountIds)
+ .sortBy(accountIds, accountId =>
+ bnOrZero(accountBalances?.[accountId]?.[assetId] ?? 0).toNumber(),
+ )
+ .reverse()
+ .value(),
+ [accountBalances, assetId],
+ )
+
+ const menuOptions = useMemo(() => {
+ const makeTitle = (accountId: AccountId): string => {
+ /**
+ * for UTXO chains, we want the title to be the account type
+ * for account-based chains, we want the title to be the asset name
+ */
+ const { chainNamespace } = fromChainId(chainId)
+ switch (chainNamespace) {
+ case CHAIN_NAMESPACE.Utxo: {
+ return accountIdToLabel(accountId)
+ }
+ default: {
+ return asset?.name ?? ''
+ }
}
}
- }
- /**
- * for UTXO-based chains, we can have many accounts for a single account number
- * e.g. account 0 can have legacy, segwit, and segwit native
- *
- * this allows us to render the multiple account varieties and their balances for
- * the native asset for UTXO chains, or a single row with the selected asset for
- * account based chains that support tokens
- */
- type AccountIdsByNumberAndType = {
- [k: number]: AccountId[]
- }
- const initial: AccountIdsByNumberAndType = {}
+ /**
+ * for UTXO-based chains, we can have many accounts for a single account number
+ * e.g. account 0 can have legacy, segwit, and segwit native
+ *
+ * this allows us to render the multiple account varieties and their balances for
+ * the native asset for UTXO chains, or a single row with the selected asset for
+ * account based chains that support tokens
+ */
+ type AccountIdsByNumberAndType = {
+ [k: number]: AccountId[]
+ }
+ const initial: AccountIdsByNumberAndType = {}
- const accountIdsByNumberAndType = accountIds.reduce((acc, accountId) => {
- const account = accountMetadata[accountId]
- if (!account) return acc
- const { accountNumber } = account.bip44Params
- if (!acc[accountNumber]) acc[accountNumber] = []
- acc[accountNumber].push(accountId)
- return acc
- }, initial)
+ const accountIdsByNumberAndType = accountIds.reduce((acc, accountId) => {
+ const account = accountMetadata[accountId]
+ if (!account) return acc
+ const { accountNumber } = account.bip44Params
+ if (!acc[accountNumber]) acc[accountNumber] = []
+ acc[accountNumber].push(accountId)
+ return acc
+ }, initial)
- return Object.entries(accountIdsByNumberAndType).map(([accountNumber, accountIds]) => {
- const sortedAccountIds = autoSelectHighestBalance
- ? getAccountIdsSortedByBalance(accountIds)
- : getAccountIdsSortedByUtxoAccountType(accountIds)
- return (
-
-
- {sortedAccountIds.map((iterAccountId, index) => (
- handleClick(iterAccountId)}
- isDisabled={disabled}
- {...listProps}
+ return Object.entries(accountIdsByNumberAndType).map(([accountNumber, accountIds]) => {
+ const sortedAccountIds = autoSelectHighestBalance
+ ? getAccountIdsSortedByBalance(accountIds)
+ : getAccountIdsSortedByUtxoAccountType(accountIds)
+ return (
+
+
- ))}
-
- )
- })
- }, [
- accountIds,
- chainId,
- asset?.name,
- asset?.precision,
- asset?.symbol,
- accountMetadata,
- autoSelectHighestBalance,
- getAccountIdsSortedByBalance,
- getAccountIdsSortedByUtxoAccountType,
- translate,
- accountBalances,
- assetId,
- selectedAccountId,
- disabled,
- listProps,
- handleClick,
- ])
+ {sortedAccountIds.map((iterAccountId, index) => (
+ handleClick(iterAccountId)}
+ isDisabled={disabled}
+ {...listProps}
+ />
+ ))}
+
+ )
+ })
+ }, [
+ accountIds,
+ chainId,
+ asset?.name,
+ asset?.precision,
+ asset?.symbol,
+ accountMetadata,
+ autoSelectHighestBalance,
+ getAccountIdsSortedByBalance,
+ getAccountIdsSortedByUtxoAccountType,
+ translate,
+ accountBalances,
+ assetId,
+ selectedAccountId,
+ disabled,
+ listProps,
+ handleClick,
+ ])
- /**
- * do NOT remove these checks, this is not a visual thing, this is a safety check!
- *
- * this component is responsible for selecting the correct account for operations where
- * we are sending funds, we need to be paranoid.
- */
- if (!accountIds.length) return null
- if (!isValidAccountNumber(accountNumber)) return null
- if (!menuOptions.length) return null
- if (!accountLabel) return null
+ /**
+ * do NOT remove these checks, this is not a visual thing, this is a safety check!
+ *
+ * this component is responsible for selecting the correct account for operations where
+ * we are sending funds, we need to be paranoid.
+ */
+ if (!accountIds.length) return null
+ if (!isValidAccountNumber(accountNumber)) return null
+ if (!menuOptions.length) return null
+ if (!accountLabel) return null
- return (
-
-
-
- )
-}
+
+
+ {translate('accounts.accountNumber', { accountNumber })}
+
+ {showLabel && (
+
+ {accountLabel}
+
+ )}
+
+
+
+
+
+ {menuOptions}
+
+
+
+
+
+ )
+ },
+)
diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx
index 215b590ccfe..b81b3ae3c7b 100644
--- a/src/components/AssetIcon.tsx
+++ b/src/components/AssetIcon.tsx
@@ -2,6 +2,7 @@ import type { AvatarProps } from '@chakra-ui/react'
import { Avatar, Circle, Flex, useColorModeValue, useMultiStyleConfig } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { fromAssetId } from '@shapeshiftoss/caip'
+import { memo } from 'react'
import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton'
import { selectAssetById, selectFeeAssetById } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
@@ -24,6 +25,19 @@ type AssetWithNetworkProps = {
assetId: AssetId
} & AvatarProps
+const before = {
+ content: '""',
+ width: '115%',
+ height: '115%',
+ backgroundColor: 'var(--chakra-colors-chakra-body-bg)',
+ borderRadius: 'full',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ transform: 'translate(-50%, -50%)',
+ zIndex: -1,
+}
+
const AssetWithNetwork: React.FC = ({ assetId, icon, src, ...rest }) => {
const asset = useAppSelector(state => selectAssetById(state, assetId ?? ''))
const feeAsset = useAppSelector(state => selectFeeAssetById(state, assetId))
@@ -44,25 +58,14 @@ const AssetWithNetwork: React.FC = ({ assetId, icon, src,
bg='none'
fontSize='inherit'
src={feeAsset?.networkIcon ?? feeAsset?.icon}
- _before={{
- content: '""',
- width: '115%',
- height: '115%',
- backgroundColor: 'var(--chakra-colors-chakra-body-bg)',
- borderRadius: 'full',
- position: 'absolute',
- left: '50%',
- top: '50%',
- transform: 'translate(-50%, -50%)',
- zIndex: -1,
- }}
+ _before={before}
/>
)}
)
}
-export const AssetIcon = ({ assetId, showNetworkIcon, src, ...rest }: AssetIconProps) => {
+export const AssetIcon = memo(({ assetId, showNetworkIcon, src, ...rest }: AssetIconProps) => {
const asset = useAppSelector(state => selectAssetById(state, assetId ?? ''))
const assetIconBg = useColorModeValue('gray.200', 'gray.700')
const assetIconColor = useColorModeValue('gray.500', 'gray.500')
@@ -117,7 +120,7 @@ export const AssetIcon = ({ assetId, showNetworkIcon, src, ...rest }: AssetIconP
{...rest}
/>
)
-}
+})
type WrappedIconProps = {
wrapColor?: string
diff --git a/src/components/AssetSearch/AssetList.tsx b/src/components/AssetSearch/AssetList.tsx
index cde32db0902..74d6d5291bb 100644
--- a/src/components/AssetSearch/AssetList.tsx
+++ b/src/components/AssetSearch/AssetList.tsx
@@ -1,7 +1,8 @@
import type { ListProps } from '@chakra-ui/react'
import { Center } from '@chakra-ui/react'
import type { FC } from 'react'
-import { useEffect, useRef } from 'react'
+import { useCallback, useEffect, useMemo, useRef } from 'react'
+import type { Size } from 'react-virtualized-auto-sizer'
import AutoSizer from 'react-virtualized-auto-sizer'
import { FixedSizeList } from 'react-window'
import { Text } from 'components/Text'
@@ -54,33 +55,47 @@ export const AssetList: FC = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assets])
- return (
-
- {({ height }) =>
- assets?.length === 0 ? (
+ const itemData = useMemo(
+ () => ({
+ assets,
+ handleClick,
+ disableUnsupported,
+ hideZeroBalanceAmounts,
+ }),
+ [assets, disableUnsupported, handleClick, hideZeroBalanceAmounts],
+ )
+
+ const renderContent = useCallback(
+ ({ height }: Size) => {
+ if (assets?.length === 0) {
+ return (
- ) : (
-
- {AssetRow}
-
)
}
+
+ return (
+
+ {AssetRow}
+
+ )
+ },
+ [assets.length, itemData],
+ )
+
+ return (
+
+ {renderContent}
)
}
diff --git a/src/components/AssetSearch/AssetRow.tsx b/src/components/AssetSearch/AssetRow.tsx
index 446c48a7938..41cebf6273c 100644
--- a/src/components/AssetSearch/AssetRow.tsx
+++ b/src/components/AssetSearch/AssetRow.tsx
@@ -1,6 +1,6 @@
import { Box, Button, Flex, Text, useColorModeValue } from '@chakra-ui/react'
import type { FC } from 'react'
-import { useMemo } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import type { ListChildComponentProps } from 'react-window'
import { Amount } from 'components/Amount/Amount'
import { AssetIcon } from 'components/AssetIcon'
@@ -17,74 +17,79 @@ import {
} from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
-export const AssetRow: FC> = ({
- data: { handleClick, disableUnsupported, assets, hideZeroBalanceAmounts },
- index,
- style,
-}) => {
- const color = useColorModeValue('gray.500', 'whiteAlpha.500')
- const {
- state: { isConnected, isDemoWallet, wallet },
- } = useWallet()
- const asset: Asset | undefined = assets[index]
- const assetId = asset?.assetId
- const filter = useMemo(() => ({ assetId }), [assetId])
- const isSupported = wallet && isAssetSupportedByWallet(assetId, wallet)
- const cryptoHumanBalance = useAppSelector(s =>
- selectPortfolioCryptoPrecisionBalanceByFilter(s, filter),
- )
- const userCurrencyBalance =
- useAppSelector(s => selectPortfolioUserCurrencyBalanceByAssetId(s, filter)) ?? '0'
- if (!asset) return null
+const focus = {
+ shadow: 'outline-inset',
+}
+
+export const AssetRow: FC> = memo(
+ ({ data: { handleClick, disableUnsupported, assets, hideZeroBalanceAmounts }, index, style }) => {
+ const color = useColorModeValue('gray.500', 'whiteAlpha.500')
+ const {
+ state: { isConnected, isDemoWallet, wallet },
+ } = useWallet()
+ const asset: Asset | undefined = assets[index]
+ const assetId = asset?.assetId
+ const filter = useMemo(() => ({ assetId }), [assetId])
+ const isSupported = wallet && isAssetSupportedByWallet(assetId, wallet)
+ const cryptoHumanBalance = useAppSelector(s =>
+ selectPortfolioCryptoPrecisionBalanceByFilter(s, filter),
+ )
+ const userCurrencyBalance =
+ useAppSelector(s => selectPortfolioUserCurrencyBalanceByAssetId(s, filter)) ?? '0'
+ const handleOnClick = useCallback(() => handleClick(asset), [asset, handleClick])
+
+ if (!asset) return null
- const hideAssetBalance = !!(hideZeroBalanceAmounts && bnOrZero(cryptoHumanBalance).isZero())
+ const hideAssetBalance = !!(hideZeroBalanceAmounts && bnOrZero(cryptoHumanBalance).isZero())
- return (
-
- )
-}
+ {isConnected && !isDemoWallet && !hideAssetBalance && (
+
+
+
+
+ )}
+
+ )
+ },
+)
diff --git a/src/components/AssetSearch/AssetSearch.tsx b/src/components/AssetSearch/AssetSearch.tsx
index acee0db01db..16f0c6bb05c 100644
--- a/src/components/AssetSearch/AssetSearch.tsx
+++ b/src/components/AssetSearch/AssetSearch.tsx
@@ -1,9 +1,8 @@
import { SearchIcon } from '@chakra-ui/icons'
import type { BoxProps, InputProps } from '@chakra-ui/react'
-import { Box, Input, InputGroup, InputLeftElement, SlideFade } from '@chakra-ui/react'
+import { Box, Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
import { isNft } from '@shapeshiftoss/caip'
-import { debounce } from 'lodash'
import intersection from 'lodash/intersection'
import uniq from 'lodash/uniq'
import type { FC, FormEvent } from 'react'
@@ -12,7 +11,6 @@ import { useForm } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router'
-import { Card } from 'components/Card/Card'
import type { Asset } from 'lib/asset-service'
import {
selectAssetsSortedByMarketCapUserCurrencyBalanceAndName,
@@ -23,33 +21,25 @@ import { useAppSelector } from 'state/store'
import { AssetList } from './AssetList'
import { ChainList } from './Chains/ChainList'
import { filterAssetsBySearchTerm } from './helpers/filterAssetsBySearchTerm/filterAssetsBySearchTerm'
-
export type AssetSearchProps = {
assets?: Asset[]
onClick?: (asset: Asset) => void
disableUnsupported?: boolean
- assetListAsDropdown?: boolean
- hideZeroBalanceAmounts?: boolean
formProps?: BoxProps
}
-
export const AssetSearch: FC = ({
assets: selectedAssets,
onClick,
disableUnsupported,
- assetListAsDropdown,
- hideZeroBalanceAmounts,
formProps,
}) => {
const translate = useTranslate()
const history = useHistory()
-
const chainIdsByMarketCap = useSelector(selectChainIdsByMarketCap)
const [activeChain, setActiveChain] = useState('All')
const assets = useAppSelector(
state => selectedAssets ?? selectAssetsSortedByMarketCapUserCurrencyBalanceAndName(state),
)
-
/**
* assets filtered by selected chain ids
*/
@@ -60,21 +50,17 @@ export const AssetSearch: FC = ({
: assets.filter(a => a.chainId === activeChain && !isNft(a.assetId)),
[activeChain, assets],
)
-
- const [isFocused, setIsFocused] = useState(false)
- const debounceBlur = debounce(() => setIsFocused(false), 150)
-
// If a custom click handler isn't provided navigate to the asset's page
- const defaultClickHandler = (asset: Asset) => {
- // AssetId has a `/` separator so the router will have to parse 2 variables
- // e.g., /assets/:chainId/:assetSubId
- const url = `/assets/${asset.assetId}`
- history.push(url)
- setIsFocused(false)
- }
-
+ const defaultClickHandler = useCallback(
+ (asset: Asset) => {
+ // AssetId has a `/` separator so the router will have to parse 2 variables
+ // e.g., /assets/:chainId/:assetSubId
+ const url = `/assets/${asset.assetId}`
+ history.push(url)
+ },
+ [history],
+ )
const handleClick = onClick ?? defaultClickHandler
-
const [searchTermAssets, setSearchTermAssets] = useState([])
const { register, watch } = useForm<{ search: string }>({
mode: 'onChange',
@@ -82,10 +68,8 @@ export const AssetSearch: FC = ({
search: '',
},
})
-
const searchString = watch('search')
const searching = useMemo(() => searchString.length > 0, [searchString])
-
useEffect(() => {
if (filteredAssets) {
setSearchTermAssets(
@@ -93,9 +77,7 @@ export const AssetSearch: FC = ({
)
}
}, [searchString, searching, filteredAssets])
-
const listAssets = searching ? searchTermAssets : filteredAssets
-
/**
* display a list of chain icon filters, based on a unique list of chain ids,
* derived from the output of the filterBy function, sorted by market cap
@@ -104,20 +86,18 @@ export const AssetSearch: FC = ({
() => intersection(chainIdsByMarketCap, uniq(assets.map(a => a.chainId))),
[chainIdsByMarketCap, assets],
)
-
- const inputProps: InputProps = {
- ...register('search'),
- type: 'text',
- placeholder: translate('common.searchAsset'),
- pl: 10,
- variant: 'filled',
- autoComplete: 'off',
- ...(() =>
- assetListAsDropdown
- ? { onBlur: debounceBlur, onFocus: () => setIsFocused(true) }
- : { autoFocus: true })(),
- }
-
+ const inputProps: InputProps = useMemo(
+ () => ({
+ ...register('search'),
+ type: 'text',
+ placeholder: translate('common.searchAsset'),
+ pl: 10,
+ variant: 'filled',
+ autoComplete: 'off',
+ autoFocus: true,
+ }),
+ [register, translate],
+ )
const handleChainClick = useCallback(
(e: React.MouseEvent) => (chainId: ChainId | 'All') => {
e.preventDefault()
@@ -126,33 +106,24 @@ export const AssetSearch: FC = ({
[],
)
- const searchElement: JSX.Element = (
- ) => e.preventDefault()}
- {...formProps}
- >
-
- {/* Override zIndex to prevent element displaying on overlay components */}
-
-
-
-
-
-
- )
+ const handleSubmit = useCallback((e: FormEvent) => e.preventDefault(), [])
- const assetSearchWithAssetList: JSX.Element = (
+ return (
<>
- {searchElement}
+
+
+ {/* Override zIndex to prevent element displaying on overlay components */}
+
+
+
+
+
+
{listAssets && (
= ({
)}
>
)
-
- const assetSearchWithAssetDropdown: JSX.Element = (
-
- {searchElement}
- {isFocused && (
-
-
-
-
-
-
-
-
-
- )}
-
- )
-
- return assetListAsDropdown ? assetSearchWithAssetDropdown : assetSearchWithAssetList
}
diff --git a/src/components/AssetSearch/Chains/ChainDropdown.tsx b/src/components/AssetSearch/Chains/ChainDropdown.tsx
index 4c0341074c7..2a32491c73b 100644
--- a/src/components/AssetSearch/Chains/ChainDropdown.tsx
+++ b/src/components/AssetSearch/Chains/ChainDropdown.tsx
@@ -10,7 +10,7 @@ import {
MenuOptionGroup,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
-import { useMemo } from 'react'
+import { useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { IconCircle } from 'components/IconCircle'
@@ -29,6 +29,8 @@ type ChainDropdownProps = {
buttonProps?: ButtonProps
} & Omit
+const width = { base: 'full', md: 'auto' }
+
export const ChainDropdown: React.FC = ({
chainIds,
chainId,
@@ -42,6 +44,7 @@ export const ChainDropdown: React.FC = ({
selectPortfolioTotalUserCurrencyBalanceExcludeEarnDupes,
)
const translate = useTranslate()
+
const renderChains = useMemo(() => {
return chainIds.map(chainId => (
@@ -50,18 +53,15 @@ export const ChainDropdown: React.FC = ({
))
}, [chainIds, includeBalance])
+ const onChange = useCallback((value: string | string[]) => onClick(value as ChainId), [onClick])
+
return (
- }
- {...buttonProps}
- >
+ } {...buttonProps}>
{chainId ? : translate('common.allChains')}
- onClick(value as ChainId)}>
+
{showAll && (
diff --git a/src/components/FilterGroup.tsx b/src/components/FilterGroup.tsx
index 40aa5ac48fd..30fcf8f63c4 100644
--- a/src/components/FilterGroup.tsx
+++ b/src/components/FilterGroup.tsx
@@ -17,7 +17,7 @@ import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io'
import { Text } from 'components/Text'
import { useToggle } from 'hooks/useToggle/useToggle'
-type Option = [string, string, ReactChild?]
+export type Option = [string, string, ReactChild?]
export const FilterGroup = ({
title,
diff --git a/src/components/Graph/Graph.tsx b/src/components/Graph/Graph.tsx
index b000ab89ac1..eee88f99a11 100644
--- a/src/components/Graph/Graph.tsx
+++ b/src/components/Graph/Graph.tsx
@@ -1,7 +1,8 @@
import { Center, Fade } from '@chakra-ui/react'
import { ParentSize } from '@visx/responsive'
+import type { ParentSizeProvidedProps } from '@visx/responsive/lib/components/ParentSize'
import { isEmpty } from 'lodash'
-import { useMemo } from 'react'
+import { useCallback } from 'react'
import type { BalanceChartData } from 'hooks/useBalanceChartData/useBalanceChartData'
import { GraphLoading } from './GraphLoading'
@@ -16,38 +17,38 @@ type GraphProps = {
isRainbowChart?: boolean
}
+const margin = {
+ top: 16,
+ right: 0,
+ bottom: 32,
+ left: 0,
+}
+
export const Graph: React.FC = ({ data, isLoaded, loading, color, isRainbowChart }) => {
- return useMemo(() => {
- const { total, rainbow } = data
- return (
-
- {parent => {
- const primaryChartProps = {
- height: parent.height,
- width: parent.width,
- color,
- margin: {
- top: 16,
- right: 0,
- bottom: 32,
- left: 0,
- },
- }
- return loading || !isLoaded ? (
-
-
-
-
-
- ) : !isEmpty(data) ? (
- isRainbowChart ? (
-
- ) : (
-
- )
- ) : null
- }}
-
- )
- }, [color, data, isLoaded, loading, isRainbowChart])
+ const { total, rainbow } = data
+ const renderGraph = useCallback(
+ ({ height, width }: ParentSizeProvidedProps) => {
+ return loading || !isLoaded ? (
+
+
+
+
+
+ ) : !isEmpty(data) ? (
+ isRainbowChart ? (
+
+ ) : (
+
+ )
+ ) : null
+ },
+ [color, data, isLoaded, isRainbowChart, loading, rainbow, total],
+ )
+ return {renderGraph}
}
diff --git a/src/components/Graph/PrimaryChart/PrimaryChart.tsx b/src/components/Graph/PrimaryChart/PrimaryChart.tsx
index 011070c72e6..dd7f170b031 100644
--- a/src/components/Graph/PrimaryChart/PrimaryChart.tsx
+++ b/src/components/Graph/PrimaryChart/PrimaryChart.tsx
@@ -6,10 +6,11 @@ import { LinearGradient } from '@visx/gradient'
import { ScaleSVG } from '@visx/responsive'
import { scaleLinear } from '@visx/scale'
import { AnimatedAreaSeries, AnimatedAxis, Tooltip, XYChart } from '@visx/xychart'
+import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'
import type { Numeric } from 'd3-array'
import { extent, max, min } from 'd3-array'
import dayjs from 'dayjs'
-import { useMemo } from 'react'
+import { useCallback, useMemo } from 'react'
import { Amount } from 'components/Amount/Amount'
import { RawText } from 'components/Text'
import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter'
@@ -24,19 +25,34 @@ export interface PrimaryChartProps {
data: HistoryData[]
width: number
height: number
- margin?: { top: number; right: number; bottom: number; left: number }
+ margin: { top: number; right: number; bottom: number; left: number }
color?: string
}
// accessors
const getStockValue = (d: HistoryData) => d?.price || 0
+const verticalCrosshairStyle = {
+ stroke: colors.blue[500],
+ strokeWidth: 2,
+ opacity: 0.5,
+ strokeDasharray: '5,2',
+ pointerEvents: 'none',
+}
+
+const tooltipStyle = { zIndex: 10 } // render over swapper TokenButton component
+
+const accessors = {
+ xAccessor: (d: HistoryData) => d.date,
+ yAccessor: (d: HistoryData) => d.price,
+}
+
export const PrimaryChart = ({
data,
width = 10,
height,
color = 'green.500',
- margin = { top: 0, right: 0, bottom: 0, left: 0 },
+ margin,
}: PrimaryChartProps) => {
const selectedLocale = useAppSelector(selectSelectedLocale)
@@ -50,24 +66,30 @@ export const PrimaryChart = ({
const tooltipColor = useColorModeValue(colors.gray[800], 'white')
// bounds
- const xMax = Math.max(width - margin.left - margin.right, 0)
- const yMax = Math.max(height - margin.top - margin.bottom, 0)
+ const xMax = useMemo(
+ () => Math.max(width - margin.left - margin.right, 0),
+ [margin.left, margin.right, width],
+ )
+ const yMax = useMemo(
+ () => Math.max(height - margin.top - margin.bottom, 0),
+ [height, margin.bottom, margin.top],
+ )
- const minPrice = Math.min(...data.map(getStockValue))
- const maxPrice = Math.max(...data.map(getStockValue))
+ const minPrice = useMemo(() => Math.min(...data.map(getStockValue)), [data])
+ const maxPrice = useMemo(() => Math.max(...data.map(getStockValue)), [data])
const priceScale = useMemo(() => {
return scaleLinear({
range: [yMax + margin.top - 32, margin.top + 32],
domain: [min(data, getStockValue) || 0, max(data, getStockValue) || 0],
})
- //
- }, [margin.top, yMax, data])
+ }, [yMax, margin.top, data])
+
+ const xyChartMargin = useMemo(
+ () => ({ top: 0, bottom: margin.bottom, left: 0, right: 0 }),
+ [margin],
+ )
- const accessors = {
- xAccessor: (d: HistoryData) => d.date,
- yAccessor: (d: HistoryData) => d.price,
- }
const labelColor = useColorModeValue(colors.gray[300], colors.gray[600])
const tickLabelProps = useMemo(
() => ({
@@ -98,21 +120,43 @@ export const PrimaryChart = ({
[yMax, margin.top, margin.bottom, minPrice, maxPrice],
)
+ const renderTooltip = useCallback(
+ ({ tooltipData }: RenderTooltipParams) => {
+ const { datum } = tooltipData?.nearestDatum!
+ const { date, price } = datum as HistoryData
+ return (
+
+
+
+ {dayjs(date).locale(selectedLocale).format('LLL')}
+
+
+ )
+ },
+ [selectedLocale, tooltipBg, tooltipBorder, tooltipColor],
+ )
+
+ const lineProps = useMemo(() => ({ stroke: chartColor }), [chartColor])
+ const tickLabelPropsFn = useCallback(() => tickLabelProps, [tickLabelProps])
+
return (
-
+
tickLabelProps}
+ tickLabelProps={tickLabelPropsFn}
numTicks={5}
labelOffset={16}
/>
@@ -121,45 +165,19 @@ export const PrimaryChart = ({
data={data}
fill='url(#area-gradient)'
fillOpacity={0.1}
- lineProps={{ stroke: chartColor }}
+ lineProps={lineProps}
offset={16}
{...accessors}
/>
{
- const { datum } = tooltipData?.nearestDatum!
- const { date, price } = datum as HistoryData
- return (
-
-
-
- {dayjs(date).locale(selectedLocale).format('LLL')}
-
-
- )
- }}
+ renderTooltip={renderTooltip}
/>
{
- const options = Object.freeze([
- { value: HistoryTimeframe.HOUR, label: 'graph.timeControls.1H' },
- { value: HistoryTimeframe.DAY, label: 'graph.timeControls.24H' },
- { value: HistoryTimeframe.WEEK, label: 'graph.timeControls.1W' },
- { value: HistoryTimeframe.MONTH, label: 'graph.timeControls.1M' },
- { value: HistoryTimeframe.YEAR, label: 'graph.timeControls.1Y' },
- { value: HistoryTimeframe.ALL, label: 'graph.timeControls.all' },
- ])
- return (
-
- )
-}
+export const TimeControls = memo(
+ ({ onChange, defaultTime, buttonGroupProps }: TimeControlsProps) => {
+ const options = Object.freeze([
+ { value: HistoryTimeframe.HOUR, label: 'graph.timeControls.1H' },
+ { value: HistoryTimeframe.DAY, label: 'graph.timeControls.24H' },
+ { value: HistoryTimeframe.WEEK, label: 'graph.timeControls.1W' },
+ { value: HistoryTimeframe.MONTH, label: 'graph.timeControls.1M' },
+ { value: HistoryTimeframe.YEAR, label: 'graph.timeControls.1Y' },
+ { value: HistoryTimeframe.ALL, label: 'graph.timeControls.all' },
+ ])
+ return (
+
+ )
+ },
+)
diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchButton.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchButton.tsx
index 8fc22d3f9dc..cf0bd9713fc 100644
--- a/src/components/Layout/Header/GlobalSearch/GlobalSearchButton.tsx
+++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchButton.tsx
@@ -74,7 +74,7 @@ export const GlobalSeachButton = () => {
const [assetResults, stakingResults, lpResults, txResults] = results
const flatResults = useMemo(() => [...results, sendResults].flat(), [results, sendResults])
const resultsCount = flatResults.length
- const isMac = /Mac/.test(navigator.userAgent)
+ const isMac = useMemo(() => /Mac/.test(navigator.userAgent), [])
const send = useModal('send')
useEffect(() => {
diff --git a/src/components/Layout/Header/Header.tsx b/src/components/Layout/Header/Header.tsx
index aeacea56cf6..16900d1a168 100644
--- a/src/components/Layout/Header/Header.tsx
+++ b/src/components/Layout/Header/Header.tsx
@@ -12,7 +12,7 @@ import {
} from '@chakra-ui/react'
import { useScroll } from 'framer-motion'
import { WalletConnectToDappsHeaderButton } from 'plugins/walletConnectToDapps/components/header/WalletConnectToDappsHeaderButton'
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { Text } from 'components/Text'
@@ -30,7 +30,7 @@ import { Notifications } from './NavBar/Notifications'
import { UserMenu } from './NavBar/UserMenu'
import { SideNavContent } from './SideNavContent'
-export const Header = () => {
+export const Header = memo(() => {
const { onToggle, isOpen, onClose } = useDisclosure()
const isDegradedState = useSelector(selectPortfolioLoadingStatus) === 'error'
@@ -42,7 +42,7 @@ export const Header = () => {
const bg = useColorModeValue('gray.100', 'gray.800')
const ref = useRef(null)
const [y, setY] = useState(0)
- const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
+ const height = useMemo(() => ref.current?.getBoundingClientRect()?.height ?? 0, [])
const { scrollY } = useScroll()
useEffect(() => {
return scrollY.onChange(() => setY(scrollY.get()))
@@ -71,7 +71,10 @@ export const Header = () => {
return () => document.removeEventListener('keydown', handleKeyPress)
}, [handleKeyPress])
- const handleBannerClick = () => dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true })
+ const handleBannerClick = useCallback(
+ () => dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }),
+ [dispatch],
+ )
return (
<>
@@ -159,4 +162,4 @@ export const Header = () => {
>
)
-}
+})
diff --git a/src/components/Layout/Header/NavBar/MainNavLink.tsx b/src/components/Layout/Header/NavBar/MainNavLink.tsx
index 89dd681dd73..866c66ce78f 100644
--- a/src/components/Layout/Header/NavBar/MainNavLink.tsx
+++ b/src/components/Layout/Header/NavBar/MainNavLink.tsx
@@ -1,9 +1,8 @@
import type { ButtonProps } from '@chakra-ui/react'
-import { Box, Button, forwardRef, Tag, Tooltip, useMediaQuery } from '@chakra-ui/react'
-import { memo, useMemo } from 'react'
+import { Box, Button, Tag, Tooltip, useMediaQuery } from '@chakra-ui/react'
+import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import type { NavLinkProps } from 'react-router-dom'
-import { matchPath, useLocation } from 'react-router-dom'
import { CircleIcon } from 'components/Icons/Circle'
import { breakpoints } from 'theme/theme'
@@ -14,22 +13,18 @@ type SidebarLinkProps = {
to?: NavLinkProps['to']
isCompact?: boolean
isNew?: boolean
+ isActive?: boolean
} & ButtonProps
export const MainNavLink = memo(
- forwardRef(({ isCompact, ...rest }: SidebarLinkProps, ref) => {
- const { href, label, isNew } = rest
+ ({ isCompact, onClick, isNew, label, isActive, ...rest }: SidebarLinkProps) => {
const [isLargerThan2xl] = useMediaQuery(`(min-width: ${breakpoints['2xl']})`, { ssr: false })
const translate = useTranslate()
- const location = useLocation()
- const isActive = useMemo(() => {
- const match = matchPath(location.pathname, {
- path: href,
- exact: false,
- strict: false,
- })
- return !!match
- }, [href, location.pathname])
+ const handleClick: React.MouseEventHandler = useCallback(
+ e => (isActive ? e.preventDefault() : onClick?.(e)),
+ [isActive, onClick],
+ )
+
return (
{label}
@@ -65,5 +60,5 @@ export const MainNavLink = memo(
)
- }),
+ },
)
diff --git a/src/components/Layout/Header/NavBar/MobileNavBar.tsx b/src/components/Layout/Header/NavBar/MobileNavBar.tsx
index 1151d27daa2..27d3883bfaa 100644
--- a/src/components/Layout/Header/NavBar/MobileNavBar.tsx
+++ b/src/components/Layout/Header/NavBar/MobileNavBar.tsx
@@ -1,49 +1,18 @@
-import { Button, Flex } from '@chakra-ui/react'
+import { Flex } from '@chakra-ui/react'
import { union } from 'lodash'
-import { useMemo } from 'react'
-import { useTranslate } from 'react-polyglot'
-import { Link as ReactRouterLink, useLocation } from 'react-router-dom'
import { routes } from 'Routes/RoutesCommon'
import { usePlugins } from 'context/PluginProvider/PluginProvider'
import { bnOrZero } from 'lib/bignumber/bignumber'
+import { MobileNavLink } from './MobileNavLink'
+
export const MobileNavBar = () => {
const { routes: pluginRoutes } = usePlugins()
- const translate = useTranslate()
const allRoutes = union(routes, pluginRoutes)
.filter(route => !route.disable && !route.hide && route.mobileNav)
// route mobileNav discriminated union narrowing is lost by the Array.prototype.sort() call
.sort((a, b) => bnOrZero(a.priority!).minus(b.priority!).toNumber())
- const location = useLocation()
- const renderMenu = useMemo(() => {
- return allRoutes.map(route => {
- const isActive = location?.pathname.includes(route?.path ?? '')
- return (
-
- {route.icon}
-
- {translate(route.shortLabel ?? route.label)}
-
-
- )
- })
- }, [allRoutes, location?.pathname, translate])
return (
{
paddingBottom='calc(env(safe-area-inset-bottom, 16px) - 16px)'
display={{ base: 'flex', md: 'none' }}
>
- {renderMenu}
+ {allRoutes.map(route => (
+
+ ))}
)
}
diff --git a/src/components/Layout/Header/NavBar/MobileNavLink.tsx b/src/components/Layout/Header/NavBar/MobileNavLink.tsx
new file mode 100644
index 00000000000..7522ede4666
--- /dev/null
+++ b/src/components/Layout/Header/NavBar/MobileNavLink.tsx
@@ -0,0 +1,43 @@
+import { Button, Flex } from '@chakra-ui/react'
+import { useMemo } from 'react'
+import { useTranslate } from 'react-polyglot'
+import { Link as ReactRouterLink, matchPath, useLocation } from 'react-router-dom'
+import type { Route } from 'Routes/helpers'
+
+export const MobileNavLink = ({ label, shortLabel, path, icon }: Route) => {
+ const translate = useTranslate()
+ const location = useLocation()
+ const isActive = useMemo(() => {
+ const match = matchPath(location.pathname, {
+ path,
+ exact: false,
+ strict: false,
+ })
+ return !!match
+ }, [path, location.pathname])
+
+ return (
+ isActive && e.preventDefault()}
+ _active={{ bg: 'transparent', svg: { color: 'blue.200' } }}
+ py={2}
+ width='full'
+ zIndex='sticky'
+ >
+ {icon}
+
+ {translate(shortLabel ?? label)}
+
+
+ )
+}
diff --git a/src/components/Layout/Header/NavBar/NavBar.tsx b/src/components/Layout/Header/NavBar/NavBar.tsx
index 91388d8f922..6ff61c16eb1 100644
--- a/src/components/Layout/Header/NavBar/NavBar.tsx
+++ b/src/components/Layout/Header/NavBar/NavBar.tsx
@@ -3,7 +3,7 @@ import { Divider, Stack, useColorModeValue, useMediaQuery } from '@chakra-ui/rea
import { union } from 'lodash'
import { useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
-import { Link as ReactRouterLink } from 'react-router-dom'
+import { Link as ReactRouterLink, matchPath, useLocation } from 'react-router-dom'
import type { Route } from 'Routes/helpers'
import { routes } from 'Routes/RoutesCommon'
import { YatBanner } from 'components/Banners/YatBanner'
@@ -26,6 +26,7 @@ export const NavBar = ({ isCompact, onClick, ...rest }: NavBarProps) => {
const isYatFeatureEnabled = useFeatureFlag('Yat')
const groupColor = useColorModeValue('gray.400', 'gray.600')
const dividerColor = useColorModeValue('gray.200', 'whiteAlpha.100')
+ const { pathname } = useLocation()
const navItemGroups = useMemo(() => {
const allRoutes = union(routes, pluginRoutes).filter(route =>
@@ -75,12 +76,19 @@ export const NavBar = ({ isCompact, onClick, ...rest }: NavBarProps) => {
label={translate(item.label)}
aria-label={translate(item.label)}
data-test={`navigation-${item.label.split('.')[1]}-button`}
+ isActive={
+ !!matchPath(pathname, {
+ path: item.path,
+ exact: false,
+ strict: false,
+ })
+ }
/>
))}
)
})
- }, [groupColor, isCompact, navItemGroups, onClick, translate])
+ }, [groupColor, isCompact, navItemGroups, onClick, pathname, translate])
return (
void }> = ({ onClick }) => {
if (isLocked) disconnect()
const hasWallet = Boolean(walletInfo?.deviceId)
- const handleConnect = () => {
+ const handleConnect = useCallback(() => {
onClick && onClick()
dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true })
- }
+ }, [dispatch, onClick])
return (
diff --git a/src/components/Layout/Header/NavBar/WalletConnectedMenu.tsx b/src/components/Layout/Header/NavBar/WalletConnectedMenu.tsx
index 5769ad07588..308c4d23c71 100644
--- a/src/components/Layout/Header/NavBar/WalletConnectedMenu.tsx
+++ b/src/components/Layout/Header/NavBar/WalletConnectedMenu.tsx
@@ -1,7 +1,7 @@
import { ChevronRightIcon, CloseIcon, RepeatIcon, WarningTwoIcon } from '@chakra-ui/icons'
import { Flex, MenuDivider, MenuGroup, MenuItem } from '@chakra-ui/react'
import { AnimatePresence } from 'framer-motion'
-import { useMemo } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Route, Switch, useLocation } from 'react-router-dom'
import {
@@ -14,41 +14,39 @@ import { WalletImage } from 'components/Layout/Header/NavBar/WalletImage'
import { RawText, Text } from 'components/Text'
import { SUPPORTED_WALLETS } from 'context/WalletProvider/config'
-export const WalletConnectedMenu = ({
- onDisconnect,
- onSwitchProvider,
- walletInfo,
- isConnected,
- type,
-}: WalletConnectedProps) => {
- const { navigateToRoute } = useMenuRoutes()
- const location = useLocation()
- const translate = useTranslate()
- const connectedWalletMenuRoutes = useMemo(
- () => type && SUPPORTED_WALLETS[type].connectedWalletMenuRoutes,
- [type],
- )
- const ConnectMenuComponent = useMemo(
- () => type && SUPPORTED_WALLETS[type].connectedMenuComponent,
- [type],
- )
+const ConnectedMenu = memo(
+ ({
+ connectedWalletMenuRoutes,
+ isConnected,
+ type,
+ walletInfo,
+ onDisconnect,
+ onSwitchProvider,
+ }: WalletConnectedProps & {
+ connectedWalletMenuRoutes: boolean
+ }) => {
+ const { navigateToRoute } = useMenuRoutes()
+ const translate = useTranslate()
+ const ConnectMenuComponent = useMemo(
+ () => type && SUPPORTED_WALLETS[type].connectedMenuComponent,
+ [type],
+ )
+
+ const handleClick = useCallback(() => {
+ if (!connectedWalletMenuRoutes) return
+ navigateToRoute(
+ (type && SUPPORTED_WALLETS[type])?.connectedWalletMenuInitialPath ??
+ WalletConnectedRoutes.Connected,
+ )
+ }, [connectedWalletMenuRoutes, navigateToRoute, type])
- const ConnectedMenu = () => {
return (
{walletInfo ? (
)
- }
+ },
+)
+
+export const WalletConnectedMenu = ({
+ onDisconnect,
+ onSwitchProvider,
+ walletInfo,
+ isConnected,
+ type,
+}: WalletConnectedProps) => {
+ const location = useLocation()
+
+ const connectedWalletMenuRoutes = useMemo(
+ () => type && SUPPORTED_WALLETS[type].connectedWalletMenuRoutes,
+ [type],
+ )
return (
-
+
- {connectedWalletMenuRoutes?.map(route => {
+ {connectedWalletMenuRoutes?.map((route, i) => {
const Component = route.component
return !Component ? null : (
}
diff --git a/src/components/Layout/Header/SideNav.tsx b/src/components/Layout/Header/SideNav.tsx
index 12c1bc847df..621c3272f63 100644
--- a/src/components/Layout/Header/SideNav.tsx
+++ b/src/components/Layout/Header/SideNav.tsx
@@ -1,9 +1,10 @@
import { chakra, Flex, useColorModeValue } from '@chakra-ui/react'
+import { memo } from 'react'
import { AppLoadingIcon } from './AppLoadingIcon'
import { SideNavContent } from './SideNavContent'
-export const SideNav = () => {
+export const SideNav = memo(() => {
const bgColor = useColorModeValue('white', 'blackAlpha.300')
const shadow = useColorModeValue('lg', 'none')
return (
@@ -30,4 +31,4 @@ export const SideNav = () => {
)
-}
+})
diff --git a/src/components/Layout/Header/SideNavContent.tsx b/src/components/Layout/Header/SideNavContent.tsx
index 09082c96eda..a675c36f59e 100644
--- a/src/components/Layout/Header/SideNavContent.tsx
+++ b/src/components/Layout/Header/SideNavContent.tsx
@@ -2,7 +2,7 @@ import { ChatIcon, CloseIcon, SettingsIcon } from '@chakra-ui/icons'
import type { FlexProps } from '@chakra-ui/react'
import { Box, Flex, IconButton, Stack, useMediaQuery } from '@chakra-ui/react'
import { WalletConnectToDappsHeaderButton } from 'plugins/walletConnectToDapps/components/header/WalletConnectToDappsHeaderButton'
-import { useCallback } from 'react'
+import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import { useModal } from 'hooks/useModal/useModal'
import { breakpoints } from 'theme/theme'
@@ -18,7 +18,7 @@ type HeaderContentProps = {
onClose?: () => void
} & FlexProps
-export const SideNavContent = ({ isCompact, onClose }: HeaderContentProps) => {
+export const SideNavContent = memo(({ isCompact, onClose }: HeaderContentProps) => {
const translate = useTranslate()
const [isLargerThanMd] = useMediaQuery(`(min-width: ${breakpoints['md']})`, { ssr: false })
const settings = useModal('settings')
@@ -28,13 +28,15 @@ export const SideNavContent = ({ isCompact, onClose }: HeaderContentProps) => {
const isWalletConnectToDappsEnabled =
isWalletConnectToDappsV1Enabled || isWalletConnectToDappsV2Enabled
- const handleClick = useCallback(
- (onClick?: () => void) => {
- onClose && onClose()
- onClick && onClick()
- },
- [onClose],
- )
+ const handleClickSettings = useCallback(() => {
+ settings.open({})
+ onClose && onClose()
+ }, [onClose, settings])
+
+ const handleClickSupport = useCallback(() => {
+ feedbackSupport.open({})
+ onClose && onClose()
+ }, [onClose, feedbackSupport])
return (
{
aria-label='Close Nav'
variant='ghost'
icon={}
- onClick={() => handleClick()}
+ onClick={onClose}
/>
- handleClick()} />
+
@@ -72,12 +74,12 @@ export const SideNavContent = ({ isCompact, onClose }: HeaderContentProps) => {
)}
- handleClick()} />
+
handleClick(() => settings.open({}))}
+ onClick={handleClickSettings}
label={translate('common.settings')}
leftIcon={}
data-test='navigation-settings-button'
@@ -85,11 +87,11 @@ export const SideNavContent = ({ isCompact, onClose }: HeaderContentProps) => {
handleClick(() => feedbackSupport.open({}))}
+ onClick={handleClickSupport}
label={translate('common.feedbackAndSupport')}
leftIcon={}
/>
)
-}
+})
diff --git a/src/components/Layout/Main.tsx b/src/components/Layout/Main.tsx
index 924123b026d..b9ebc78989c 100644
--- a/src/components/Layout/Main.tsx
+++ b/src/components/Layout/Main.tsx
@@ -2,7 +2,7 @@ import type { ContainerProps } from '@chakra-ui/react'
import { Box, Container, HStack, Stack, useColorModeValue } from '@chakra-ui/react'
import { useScroll } from 'framer-motion'
import type { ReactNode } from 'react'
-import { useEffect, useRef, useState } from 'react'
+import { memo, useEffect, useRef, useState } from 'react'
import { Breadcrumbs } from 'components/Breadcrumbs/Breadcrumbs'
import { NestedMenu } from 'components/NestedMenu/NestedMenu'
import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter'
@@ -15,56 +15,52 @@ export type MainProps = {
hideBreadcrumbs?: boolean
} & ContainerProps
-export const Main: React.FC = ({
- children,
- titleComponent,
- headerComponent,
- hideBreadcrumbs = false,
- ...rest
-}) => {
- const ref = useRef(null)
- const { currentRoute } = useBrowserRouter()
- const bg = useColorModeValue('white', 'gray.800')
- const borderColor = useColorModeValue('gray.100', 'gray.750')
- const [y, setY] = useState(0)
- const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
- const { scrollY } = useScroll()
- useEffect(() => {
- return scrollY.onChange(() => setY(scrollY.get()))
- }, [scrollY])
- return (
-
- {titleComponent && (
- height ? 'sm' : undefined}
- >
- <>
-
-
- {!hideBreadcrumbs && (
-
-
-
- )}
+export const Main: React.FC = memo(
+ ({ children, titleComponent, headerComponent, hideBreadcrumbs = false, ...rest }) => {
+ const ref = useRef(null)
+ const { currentRoute } = useBrowserRouter()
+ const bg = useColorModeValue('white', 'gray.800')
+ const borderColor = useColorModeValue('gray.100', 'gray.750')
+ const [y, setY] = useState(0)
+ const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
+ const { scrollY } = useScroll()
+ useEffect(() => {
+ return scrollY.onChange(() => setY(scrollY.get()))
+ }, [scrollY])
+ return (
+
+ {titleComponent && (
+ height ? 'sm' : undefined}
+ >
+ <>
+
+
+ {!hideBreadcrumbs && (
+
+
+
+ )}
- {titleComponent}
-
-
- {currentRoute && }
- >
-
- )}
- {headerComponent}
-
- {children}
-
-
- )
-}
+ {titleComponent}
+
+
+ {currentRoute && }
+ >
+
+ )}
+ {headerComponent}
+
+ {children}
+
+
+ )
+ },
+)
diff --git a/src/components/Layout/Seo.tsx b/src/components/Layout/Seo.tsx
index e695d259769..e9672e65036 100644
--- a/src/components/Layout/Seo.tsx
+++ b/src/components/Layout/Seo.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { memo } from 'react'
import { Helmet } from 'react-helmet-async'
import Thumb from 'assets/ss-thumb.jpg'
@@ -9,31 +9,33 @@ type SeoProps = {
name?: string
}
-export const SEO: React.FC = ({
- title,
- description = 'ShapeShift DAO | Your Web3 & DeFi Portal',
- type = 'website',
- name = 'ShapeShift',
-}) => {
- const publicUrl = window.location.origin
- return (
-
- {/* Standard metadata tags */}
- {title ? `${title} | ShapeShift` : 'ShapeShift'}
-
- {/* End standard metadata tags */}
- {/* Facebook tags */}
-
-
-
-
- {/* End Facebook tags */}
- {/* Twitter tags */}
-
-
-
-
- {/* End Twitter tags */}
-
- )
-}
+export const SEO: React.FC = memo(
+ ({
+ title,
+ description = 'ShapeShift DAO | Your Web3 & DeFi Portal',
+ type = 'website',
+ name = 'ShapeShift',
+ }) => {
+ const publicUrl = window.location.origin
+ return (
+
+ {/* Standard metadata tags */}
+ {title ? `${title} | ShapeShift` : 'ShapeShift'}
+
+ {/* End standard metadata tags */}
+ {/* Facebook tags */}
+
+
+
+
+ {/* End Facebook tags */}
+ {/* Twitter tags */}
+
+
+
+
+ {/* End Twitter tags */}
+
+ )
+ },
+)
diff --git a/src/components/MaybeChartUnavailable.tsx b/src/components/MaybeChartUnavailable.tsx
index 32a87e4006b..304c4abe382 100644
--- a/src/components/MaybeChartUnavailable.tsx
+++ b/src/components/MaybeChartUnavailable.tsx
@@ -1,12 +1,13 @@
import { Alert, AlertIcon } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
+import { memo } from 'react'
import { useTranslate } from 'react-polyglot'
import { useUnavailableBalanceChartDataAssetNames } from 'hooks/useBalanceChartData/utils'
type MaybeChartUnavailableProps = {
assetIds: AssetId[]
}
-export const MaybeChartUnavailable: React.FC = ({ assetIds }) => {
+export const MaybeChartUnavailable: React.FC = memo(({ assetIds }) => {
const translate = useTranslate()
const unavailableAssetNames = useUnavailableBalanceChartDataAssetNames(assetIds)
if (!unavailableAssetNames) return null
@@ -16,4 +17,4 @@ export const MaybeChartUnavailable: React.FC = ({ as
{translate('common.chartUnavailable', { unavailableAssetNames })}
)
-}
+})
diff --git a/src/components/Modals/AssetSearch/AssetSearchModal.tsx b/src/components/Modals/AssetSearch/AssetSearchModal.tsx
index d0e14ea3265..9bacd512609 100644
--- a/src/components/Modals/AssetSearch/AssetSearchModal.tsx
+++ b/src/components/Modals/AssetSearch/AssetSearchModal.tsx
@@ -8,7 +8,7 @@ import {
useMediaQuery,
} from '@chakra-ui/react'
import type { FC } from 'react'
-import { useCallback } from 'react'
+import { memo, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import type { AssetSearchProps } from 'components/AssetSearch/AssetSearch'
import { AssetSearch } from 'components/AssetSearch/AssetSearch'
@@ -70,17 +70,17 @@ export const AssetSearchModalBase: FC = ({
// multiple instances to prevent rerenders opening the modal in different parts of the app
-export const AssetSearchModal: FC = props => {
+export const AssetSearchModal: FC = memo(props => {
const assetSearch = useModal('assetSearch')
return
-}
+})
-export const SellAssetSearchModal: FC = props => {
+export const SellAssetSearchModal: FC = memo(props => {
const sellAssetSearch = useModal('sellAssetSearch')
return
-}
+})
-export const BuyAssetSearchModal: FC = props => {
+export const BuyAssetSearchModal: FC = memo(props => {
const buyAssetSearch = useModal('buyAssetSearch')
return
-}
+})
diff --git a/src/components/Modals/FeedbackSupport/FeedbackSupport.tsx b/src/components/Modals/FeedbackSupport/FeedbackSupport.tsx
index 9e622400f84..76290b41296 100644
--- a/src/components/Modals/FeedbackSupport/FeedbackSupport.tsx
+++ b/src/components/Modals/FeedbackSupport/FeedbackSupport.tsx
@@ -45,6 +45,7 @@ export const FeedbackAndSupport = () => {
)}
{
as={Link}
size='sm'
label={translate('common.submitFeatureRequest')}
+ // @ts-ignore
isExternal
href='https://shapeshift.canny.io/feature-requests'
/>
diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx
index 4f8eb75060c..d1dceb1db6d 100644
--- a/src/components/MultiHopTrade/MultiHopTrade.tsx
+++ b/src/components/MultiHopTrade/MultiHopTrade.tsx
@@ -1,7 +1,7 @@
import type { AssetId } from '@shapeshiftoss/caip'
import { ethAssetId, foxAssetId } from '@shapeshiftoss/caip'
import { AnimatePresence } from 'framer-motion'
-import { useEffect } from 'react'
+import { memo, useEffect } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { MemoryRouter, Route, Switch, useLocation } from 'react-router-dom'
import type { CardProps } from 'components/Card/Card'
@@ -22,37 +22,39 @@ export type TradeCardProps = {
defaultSellAssetId?: AssetId
} & CardProps
-export const MultiHopTrade = ({
- defaultBuyAssetId = foxAssetId,
- defaultSellAssetId = ethAssetId,
- ...cardProps
-}: TradeCardProps) => {
- const dispatch = useAppDispatch()
- const methods = useForm({ mode: 'onChange' })
+export const MultiHopTrade = memo(
+ ({
+ defaultBuyAssetId = foxAssetId,
+ defaultSellAssetId = ethAssetId,
+ ...cardProps
+ }: TradeCardProps) => {
+ const dispatch = useAppDispatch()
+ const methods = useForm({ mode: 'onChange' })
- const defaultBuyAsset = useAppSelector(state => selectAssetById(state, defaultBuyAssetId))
- const defaultSellAsset = useAppSelector(state => selectAssetById(state, defaultSellAssetId))
+ const defaultBuyAsset = useAppSelector(state => selectAssetById(state, defaultBuyAssetId))
+ const defaultSellAsset = useAppSelector(state => selectAssetById(state, defaultSellAssetId))
- useEffect(() => {
- dispatch(swappers.actions.clear())
- if (defaultSellAsset) dispatch(swappers.actions.setSellAsset(defaultSellAsset))
- if (defaultBuyAsset) dispatch(swappers.actions.setBuyAsset(defaultBuyAsset))
- }, [defaultBuyAsset, defaultSellAsset, dispatch])
+ useEffect(() => {
+ dispatch(swappers.actions.clear())
+ if (defaultSellAsset) dispatch(swappers.actions.setSellAsset(defaultSellAsset))
+ if (defaultBuyAsset) dispatch(swappers.actions.setBuyAsset(defaultBuyAsset))
+ }, [defaultBuyAsset, defaultSellAsset, dispatch])
- return (
-
-
-
-
-
-
-
-
-
- )
-}
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ },
+)
-const MultiHopRoutes = () => {
+const MultiHopRoutes = memo(() => {
const location = useLocation()
const dispatch = useAppDispatch()
@@ -80,4 +82,4 @@ const MultiHopRoutes = () => {
)
-}
+})
diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx
index 11842aba189..78542e8816f 100644
--- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx
@@ -1,4 +1,5 @@
import { ArrowDownIcon, ArrowForwardIcon, ArrowUpIcon } from '@chakra-ui/icons'
+import type { ResponsiveValue } from '@chakra-ui/react'
import {
Button,
Flex,
@@ -10,7 +11,8 @@ import {
} from '@chakra-ui/react'
import { KeplrHDWallet } from '@shapeshiftoss/hdwallet-keplr/dist/keplr'
import { getDefaultSlippagePercentageForSwapper } from 'constants/constants'
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import type { Property } from 'csstype'
+import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { useHistory } from 'react-router'
@@ -68,7 +70,12 @@ import { useSupportedAssets } from '../../hooks/useSupportedAssets'
import { SellAssetInput } from './components/SellAssetInput'
import { TradeQuotes } from './components/TradeQuotes/TradeQuotes'
-export const TradeInput = () => {
+const flexDir: ResponsiveValue = { base: 'column', md: 'row' }
+const marginHorizontal = { base: 0, md: -3 }
+const marginVertical = { base: -3, md: 0 }
+const percentOptions = [1]
+
+export const TradeInput = memo(() => {
useGetTradeQuotes()
const {
state: { wallet },
@@ -195,12 +202,32 @@ export const TradeInput = () => {
return quoteHasError || manualReceiveAddressIsValidating || isLoading || !isSellAmountEntered
}, [isLoading, isSellAmountEntered, manualReceiveAddressIsValidating, quoteHasError])
+ const rightRegion = useMemo(
+ () =>
+ activeQuote ? (
+ : }
+ aria-label='Expand Quotes'
+ onClick={toggleShowTradeQuotes}
+ />
+ ) : (
+ <>>
+ ),
+ [activeQuote, showTradeQuotes, toggleShowTradeQuotes],
+ )
+
+ const tradeQuotes = useMemo(
+ () => ,
+ [showTradeQuotes, sortedQuotes],
+ )
+
return (
-
+
{
{
fiatAmount={
isSellAmountEntered ? positiveOrZero(buyAmountAfterFeesUserCurrency).toFixed() : '0'
}
- percentOptions={[1]}
+ percentOptions={percentOptions}
showInputSkeleton={isLoading}
showFiatSkeleton={isLoading}
label={translate('trade.youGet')}
- rightRegion={
- activeQuote ? (
- : }
- aria-label='Expand Quotes'
- onClick={toggleShowTradeQuotes}
- />
- ) : (
- <>>
- )
- }
+ rightRegion={rightRegion}
>
- {Boolean(sortedQuotes.length) && (
-
- )}
+ {tradeQuotes}
{
)
-}
+})
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/DonationCheckbox.tsx b/src/components/MultiHopTrade/components/TradeInput/components/DonationCheckbox.tsx
index 662c0fa61cb..61a5be67b17 100644
--- a/src/components/MultiHopTrade/components/TradeInput/components/DonationCheckbox.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/components/DonationCheckbox.tsx
@@ -1,6 +1,6 @@
import { Checkbox, Stack } from '@chakra-ui/react'
import type { FC } from 'react'
-import { useCallback, useMemo } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip'
import { Row } from 'components/Row/Row'
@@ -18,43 +18,45 @@ type DonationCheckboxProps = {
isLoading: boolean
}
-export const DonationCheckbox: FC = ({ isLoading }): JSX.Element | null => {
- const translate = useTranslate()
- const dispatch = useAppDispatch()
- const willDonate = useAppSelector(selectWillDonate)
- const affiliateBps = useAppSelector(selectActiveQuoteDonationBps)
+export const DonationCheckbox: FC = memo(
+ ({ isLoading }): JSX.Element | null => {
+ const translate = useTranslate()
+ const dispatch = useAppDispatch()
+ const willDonate = useAppSelector(selectWillDonate)
+ const affiliateBps = useAppSelector(selectActiveQuoteDonationBps)
- const {
- number: { toFiat },
- } = useLocaleFormatter()
+ const {
+ number: { toFiat },
+ } = useLocaleFormatter()
- const potentialDonationAmountFiat = useAppSelector(selectPotentialDonationAmountUserCurrency)
+ const potentialDonationAmountFiat = useAppSelector(selectPotentialDonationAmountUserCurrency)
- const handleDonationToggle = useCallback(() => {
- dispatch(swappers.actions.toggleWillDonate())
- }, [dispatch])
+ const handleDonationToggle = useCallback(() => {
+ dispatch(swappers.actions.toggleWillDonate())
+ }, [dispatch])
- const donationOption: JSX.Element = useMemo(
- () => (
-
-
-
-
-
-
-
-
-
- {toFiat(potentialDonationAmountFiat ?? '0')}
-
-
- ),
- [translate, willDonate, handleDonationToggle, isLoading, toFiat, potentialDonationAmountFiat],
- )
+ const donationOption: JSX.Element = useMemo(
+ () => (
+
+
+
+
+
+
+
+
+
+ {toFiat(potentialDonationAmountFiat ?? '0')}
+
+
+ ),
+ [translate, willDonate, handleDonationToggle, isLoading, toFiat, potentialDonationAmountFiat],
+ )
- return affiliateBps !== undefined ? donationOption : null
-}
+ return affiliateBps !== undefined ? donationOption : null
+ },
+)
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx
index e7b11a2c093..2361bbbfb0f 100644
--- a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx
@@ -1,7 +1,7 @@
import { FormControl, FormLabel } from '@chakra-ui/react'
import { ethChainId } from '@shapeshiftoss/caip'
import type { FC } from 'react'
-import { useEffect, useMemo } from 'react'
+import { memo, useEffect, useMemo } from 'react'
import { useFormContext } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { AddressInput } from 'components/Modals/Send/AddressInput/AddressInput'
@@ -15,7 +15,7 @@ import { selectBuyAsset, selectManualReceiveAddress } from 'state/slices/swapper
import { swappers } from 'state/slices/swappersSlice/swappersSlice'
import { useAppDispatch, useAppSelector } from 'state/store'
-export const ManualAddressEntry: FC = (): JSX.Element | null => {
+export const ManualAddressEntry: FC = memo((): JSX.Element | null => {
const dispatch = useAppDispatch()
const {
@@ -58,6 +58,32 @@ export const ManualAddressEntry: FC = (): JSX.Element | null => {
dispatch(swappers.actions.setManualReceiveAddressIsValidating(isValidating))
}, [dispatch, isValidating])
+ const rules = useMemo(
+ () => ({
+ required: true,
+ validate: {
+ validateAddress: async (rawInput: string) => {
+ dispatch(swappers.actions.setManualReceiveAddress(undefined))
+ const value = rawInput.trim() // trim leading/trailing spaces
+ // this does not throw, everything inside is handled
+ const parseAddressInputWithChainIdArgs = {
+ assetId: buyAssetAssetId,
+ chainId: buyAssetChainId,
+ urlOrAddress: value,
+ disableUrlParsing: true,
+ }
+ const { address } = await parseAddressInputWithChainId(parseAddressInputWithChainIdArgs)
+ dispatch(swappers.actions.setManualReceiveAddress(address || undefined))
+ const invalidMessage = isYatSupported
+ ? 'common.invalidAddressOrYat'
+ : 'common.invalidAddress'
+ return address ? true : invalidMessage
+ },
+ },
+ }),
+ [buyAssetAssetId, buyAssetChainId, dispatch, isYatSupported],
+ )
+
const ManualReceiveAddressEntry: JSX.Element = useMemo(() => {
return (
@@ -68,35 +94,12 @@ export const ManualAddressEntry: FC = (): JSX.Element | null => {
{translate('trade.receiveAddressDescription', { chainName: buyAssetChainName })}
{
- dispatch(swappers.actions.setManualReceiveAddress(undefined))
- const value = rawInput.trim() // trim leading/trailing spaces
- // this does not throw, everything inside is handled
- const parseAddressInputWithChainIdArgs = {
- assetId: buyAssetAssetId,
- chainId: buyAssetChainId,
- urlOrAddress: value,
- disableUrlParsing: true,
- }
- const { address } = await parseAddressInputWithChainId(
- parseAddressInputWithChainIdArgs,
- )
- dispatch(swappers.actions.setManualReceiveAddress(address || undefined))
- const invalidMessage = isYatSupported
- ? 'common.invalidAddressOrYat'
- : 'common.invalidAddress'
- return address ? true : invalidMessage
- },
- },
- }}
+ rules={rules}
placeholder={translate('trade.addressPlaceholder', { chainName: buyAssetChainName })}
/>
)
- }, [buyAssetAssetId, buyAssetChainId, buyAssetChainName, dispatch, isYatSupported, translate])
+ }, [buyAssetChainName, rules, translate])
return shouldShowManualReceiveAddressInput ? ManualReceiveAddressEntry : null
-}
+})
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SellAssetInput.tsx b/src/components/MultiHopTrade/components/TradeInput/components/SellAssetInput.tsx
index f95015cbc5c..e4332fcef0d 100644
--- a/src/components/MultiHopTrade/components/TradeInput/components/SellAssetInput.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/components/SellAssetInput.tsx
@@ -1,5 +1,5 @@
import type { AccountId } from '@shapeshiftoss/caip'
-import { useCallback, useEffect, useState } from 'react'
+import { memo, useCallback, useEffect, useState } from 'react'
import { TradeAssetInput } from 'components/Trade/Components/TradeAssetInput'
import type { Asset } from 'lib/asset-service'
import { bnOrZero, positiveOrZero } from 'lib/bignumber/bignumber'
@@ -13,7 +13,9 @@ export type SellAssetInputProps = {
asset: Asset
}
-export const SellAssetInput = ({ accountId, asset, label }: SellAssetInputProps) => {
+const percentOptions = [1]
+
+export const SellAssetInput = memo(({ accountId, asset, label }: SellAssetInputProps) => {
const [sellAmountUserCurrencyHuman, setSellAmountUserCurrencyHuman] = useState('0')
const [sellAmountCryptoPrecision, setSellAmountCryptoPrecision] = useState('0')
const dispatch = useAppDispatch()
@@ -51,10 +53,10 @@ export const SellAssetInput = ({ accountId, asset, label }: SellAssetInputProps)
fiatAmount={positiveOrZero(sellAmountUserCurrencyHuman).toString()}
isSendMaxDisabled={false}
onChange={handleSellAssetInputChange}
- percentOptions={[1]}
+ percentOptions={percentOptions}
showInputSkeleton={false}
showFiatSkeleton={false}
label={label}
/>
)
-}
+})
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx
index 319f4da7147..e0442ec4c7b 100644
--- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx
+++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx
@@ -151,13 +151,22 @@ export const TradeQuoteLoaded: React.FC = ({
return borderColor
})()
+ const hoverProps = useMemo(
+ () => ({ borderColor: isActive ? activeSwapperColor : hoverColor }),
+ [activeSwapperColor, hoverColor, isActive],
+ )
+ const activeProps = useMemo(
+ () => ({ borderColor: isActive ? activeSwapperColor : focusColor }),
+ [activeSwapperColor, focusColor, isActive],
+ )
+
return totalReceiveAmountCryptoPrecision ? (
= ({ isOpen, sortedQuotes }) => {
+export const TradeQuotes: React.FC = memo(({ isOpen, sortedQuotes }) => {
const activeSwapperName = useAppSelector(selectActiveSwapperName)
const bestQuoteData = sortedQuotes[0]
- const quotes = sortedQuotes.map((quoteData, i) => {
- const { quote, swapperName } = quoteData
-
- // TODO(woodenfurniture): we may want to display per-swapper errors here
- if (!quote) return null
-
- // TODO(woodenfurniture): use quote ID when we want to support multiple quotes per swapper
- const isActive = activeSwapperName === swapperName
-
- return (
-
- )
- })
+ const quotes = useMemo(
+ () =>
+ sortedQuotes.map((quoteData, i) => {
+ const { quote, swapperName } = quoteData
+
+ // TODO(woodenfurniture): we may want to display per-swapper errors here
+ if (!quote) return null
+
+ // TODO(woodenfurniture): use quote ID when we want to support multiple quotes per swapper
+ const isActive = activeSwapperName === swapperName
+
+ return (
+
+ )
+ }),
+ [activeSwapperName, bestQuoteData, sortedQuotes],
+ )
return (
@@ -42,4 +47,4 @@ export const TradeQuotes: React.FC = ({ isOpen, sortedQuotes }
)
-}
+})
diff --git a/src/components/SelectAssets/SelectAssets.tsx b/src/components/SelectAssets/SelectAssets.tsx
index 9e930ba55da..a9dd8828f24 100644
--- a/src/components/SelectAssets/SelectAssets.tsx
+++ b/src/components/SelectAssets/SelectAssets.tsx
@@ -1,17 +1,20 @@
import { ArrowBackIcon } from '@chakra-ui/icons'
import { IconButton, ModalBody, ModalCloseButton, ModalHeader, Stack } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
+import { useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import { AssetSearch } from 'components/AssetSearch/AssetSearch'
import { SlideTransition } from 'components/SlideTransition'
+import type { Asset } from 'lib/asset-service'
type SelectAssetsProps = {
onClick(assetId: AssetId): void
onBack?: () => void
}
-export const SelectAssets = ({ onClick: handleClick, onBack: handleBack }: SelectAssetsProps) => {
+export const SelectAssets = ({ onClick, onBack: handleBack }: SelectAssetsProps) => {
const translate = useTranslate()
+ const handleClick = useCallback((asset: Asset) => onClick(asset.assetId), [onClick])
return (
@@ -30,7 +33,7 @@ export const SelectAssets = ({ onClick: handleClick, onBack: handleBack }: Selec
- handleClick(asset.assetId)} />
+
)
diff --git a/src/components/Trade/Components/AssetSelection.tsx b/src/components/Trade/Components/AssetSelection.tsx
index 5a7aab1b79b..1fc89c1d7af 100644
--- a/src/components/Trade/Components/AssetSelection.tsx
+++ b/src/components/Trade/Components/AssetSelection.tsx
@@ -7,6 +7,7 @@ import {
useColorModeValue,
} from '@chakra-ui/react'
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
+import { memo, useMemo } from 'react'
import type { AccountDropdownProps } from 'components/AccountDropdown/AccountDropdown'
import { AccountDropdown } from 'components/AccountDropdown/AccountDropdown'
import { AssetIcon } from 'components/AssetIcon'
@@ -38,6 +39,18 @@ type TradeAssetSelectProps = {
label: string
}
+const footerPadding = { padding: 0 }
+const buttonProps = {
+ width: 'full',
+ borderTopRadius: 0,
+ px: 4,
+ fontSize: 'xs',
+ py: 4,
+ height: 'auto',
+}
+const boxProps = { m: 0, p: 0 }
+const borderRadius = { base: 'xl' }
+
export const TradeAssetSelectWithAsset: React.FC = ({
onAccountIdChange: handleAccountIdChange,
accountId,
@@ -52,12 +65,16 @@ export const TradeAssetSelectWithAsset: React.FC = ({
const asset = useAppSelector(state => selectAssetById(state, assetId ?? ''))
const feeAsset = useAppSelector(state => selectFeeAssetByChainId(state, asset?.chainId ?? ''))
const networkName = feeAsset?.networkName || feeAsset?.name
+
+ const hoverProps = useMemo(() => ({ bg: hoverBg }), [hoverBg])
+ const activeProps = useMemo(() => ({ bg: focusBg }), [focusBg])
+
return (
@@ -65,8 +82,8 @@ export const TradeAssetSelectWithAsset: React.FC = ({
display='flex'
gap={1}
flexDir='column'
- _hover={{ bg: hoverBg }}
- _active={{ bg: focusBg }}
+ _hover={hoverProps}
+ _active={activeProps}
cursor='pointer'
py={2}
px={4}
@@ -86,20 +103,13 @@ export const TradeAssetSelectWithAsset: React.FC = ({
{assetId && (
-
+
@@ -109,14 +119,12 @@ export const TradeAssetSelectWithAsset: React.FC = ({
)
}
-export const TradeAssetSelect: React.FC = ({
- assetId,
- accountId,
- ...restAssetInputProps
-}) => {
- return assetId ? (
-
- ) : (
-
- )
-}
+export const TradeAssetSelect: React.FC = memo(
+ ({ assetId, accountId, ...restAssetInputProps }) => {
+ return assetId ? (
+
+ ) : (
+
+ )
+ },
+)
diff --git a/src/components/Trade/Components/RateGasRow.tsx b/src/components/Trade/Components/RateGasRow.tsx
index b870014faa5..1616461b99b 100644
--- a/src/components/Trade/Components/RateGasRow.tsx
+++ b/src/components/Trade/Components/RateGasRow.tsx
@@ -1,5 +1,5 @@
import { Stack } from '@chakra-ui/react'
-import { type FC } from 'react'
+import { type FC, memo } from 'react'
import { FaGasPump } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
@@ -18,68 +18,63 @@ type RateGasRowProps = {
isLoading?: boolean
isError?: boolean
}
-export const RateGasRow: FC = ({
- sellSymbol,
- buySymbol,
- rate,
- gasFee,
- isLoading,
- isError,
-}) => {
- const translate = useTranslate()
- switch (true) {
- case isLoading:
- return (
-
-
-
-
- )
- case !rate || isError:
- return (
-
-
-
-
-
- )
- default:
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
+export const RateGasRow: FC = memo(
+ ({ sellSymbol, buySymbol, rate, gasFee, isLoading, isError }) => {
+ const translate = useTranslate()
+ switch (true) {
+ case isLoading:
+ return (
+
+
+
+
+ )
+ case !rate || isError:
+ return (
+
+
+
+
+
+ )
+ default:
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ },
+)
diff --git a/src/components/Trade/Components/TradeAmountInput.tsx b/src/components/Trade/Components/TradeAmountInput.tsx
index 6ab28c15e54..7f583d8d5d7 100644
--- a/src/components/Trade/Components/TradeAmountInput.tsx
+++ b/src/components/Trade/Components/TradeAmountInput.tsx
@@ -13,8 +13,9 @@ import {
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
import { PairIcons } from 'features/defi/components/PairIcons/PairIcons'
import type { FocusEvent, PropsWithChildren } from 'react'
-import React, { useRef, useState } from 'react'
+import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
import type { FieldError } from 'react-hook-form'
+import type { NumberFormatValues } from 'react-number-format'
import NumberFormat from 'react-number-format'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
@@ -26,6 +27,8 @@ import { useToggle } from 'hooks/useToggle/useToggle'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { colors } from 'theme/colors'
+const cryptoInputStyle = { caretColor: colors.blue[200] }
+
const CryptoInput = (props: InputProps) => {
const translate = useTranslate()
return (
@@ -39,7 +42,7 @@ const CryptoInput = (props: InputProps) => {
textAlign='left'
variant='inline'
placeholder={translate('common.enterAmount')}
- style={{ caretColor: colors.blue[200] }}
+ style={cryptoInputStyle}
autoComplete='off'
{...props}
/>
@@ -72,156 +75,176 @@ export type TradeAmountInputProps = {
labelPostFix?: JSX.Element
} & PropsWithChildren
-export const TradeAmountInput: React.FC = ({
- assetId,
- assetSymbol,
- onChange = () => {},
- onMaxClick,
- onPercentOptionClick,
- cryptoAmount,
- isReadOnly,
- isSendMaxDisabled,
- fiatAmount,
- showFiatAmount = '0',
- balance,
- fiatBalance,
- errors,
- percentOptions = [0.25, 0.5, 0.75, 1],
- icons,
- children,
- showInputSkeleton,
- showFiatSkeleton,
- formControlProps,
- label,
- rightRegion,
- labelPostFix,
-}) => {
- const {
- number: { localeParts },
- } = useLocaleFormatter()
- const translate = useTranslate()
- const amountRef = useRef(null)
- const [isFiat, toggleIsFiat] = useToggle(false)
- const [isFocused, setIsFocused] = useState(false)
- const borderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100')
- const bgColor = useColorModeValue('white', 'gray.850')
- const focusBg = useColorModeValue('gray.50', 'gray.900')
- const focusBorder = useColorModeValue('blue.500', 'blue.400')
+const defaultPercentOptions = [0.25, 0.5, 0.75, 1]
- // Lower the decimal places when the integer is greater than 8 significant digits for better UI
- const cryptoAmountIntegerCount = bnOrZero(bnOrZero(cryptoAmount).toFixed(0)).precision(true)
- const formattedCryptoAmount = bnOrZero(cryptoAmountIntegerCount).isLessThanOrEqualTo(8)
- ? cryptoAmount
- : bnOrZero(cryptoAmount).toFixed(3)
+export const TradeAmountInput: React.FC = memo(
+ ({
+ assetId,
+ assetSymbol,
+ onChange,
+ onMaxClick,
+ onPercentOptionClick,
+ cryptoAmount,
+ isReadOnly,
+ isSendMaxDisabled,
+ fiatAmount,
+ showFiatAmount = '0',
+ balance,
+ fiatBalance,
+ errors,
+ percentOptions = defaultPercentOptions,
+ icons,
+ children,
+ showInputSkeleton,
+ showFiatSkeleton,
+ formControlProps,
+ label,
+ rightRegion,
+ labelPostFix,
+ }) => {
+ const {
+ number: { localeParts },
+ } = useLocaleFormatter()
+ const translate = useTranslate()
+ const amountRef = useRef(null)
+ const [isFiat, toggleIsFiat] = useToggle(false)
+ const [isFocused, setIsFocused] = useState(false)
+ const borderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100')
+ const bgColor = useColorModeValue('white', 'gray.850')
+ const focusBg = useColorModeValue('gray.50', 'gray.900')
+ const focusBorder = useColorModeValue('blue.500', 'blue.400')
- return (
-
-
- {label && (
-
-
- {label}
-
- {labelPostFix}
-
- )}
+ // Lower the decimal places when the integer is greater than 8 significant digits for better UI
+ const cryptoAmountIntegerCount = bnOrZero(bnOrZero(cryptoAmount).toFixed(0)).precision(true)
+ const formattedCryptoAmount = useMemo(
+ () =>
+ bnOrZero(cryptoAmountIntegerCount).isLessThanOrEqualTo(8)
+ ? cryptoAmount
+ : bnOrZero(cryptoAmount).toFixed(3),
+ [cryptoAmount, cryptoAmountIntegerCount],
+ )
+
+ const handleOnChange = useCallback(() => {
+ // onChange will send us the formatted value
+ // To get around this we need to get the value from the onChange using a ref
+ // Now when the max buttons are clicked the onChange will not fire
+ onChange?.(amountRef.current ?? '', isFiat)
+ }, [isFiat, onChange])
+
+ const handleOnBlur = useCallback(() => setIsFocused(false), [])
+ const handleOnFocus = useCallback((e: FocusEvent) => {
+ setIsFocused(true)
+ e.target.select()
+ }, [])
+
+ const hover = useMemo(
+ () => ({ bg: isReadOnly ? bgColor : focusBg }),
+ [bgColor, focusBg, isReadOnly],
+ )
+
+ const handleValueChange = useCallback((values: NumberFormatValues) => {
+ // This fires anytime value changes including setting it on max click
+ // Store the value in a ref to send when we actually want the onChange to fire
+ amountRef.current = values.value
+ }, [])
- {showFiatAmount && (
-
-
- {isFiat ? (
-
- ) : (
-
- )}
+ return (
+
+
+ {label && (
+
+
+ {label}
+
+ {labelPostFix}
+
+ )}
+
+ {showFiatAmount && (
+
+
+ {isFiat ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ {icons ? (
+
+ ) : (
+
+ )}
+
+
+
-
- )}
-
-
- {icons ? (
-
- ) : (
-
- )}
-
-
- {
- // This fires anytime value changes including setting it on max click
- // Store the value in a ref to send when we actually want the onChange to fire
- amountRef.current = values.value
- }}
- onChange={() => {
- // onChange will send us the formatted value
- // To get around this we need to get the value from the onChange using a ref
- // Now when the max buttons are clicked the onChange will not fire
- onChange(amountRef.current ?? '', isFiat)
- }}
- onBlur={() => setIsFocused(false)}
- onFocus={(e: FocusEvent) => {
- setIsFocused(true)
- e.target.select()
- }}
+ {rightRegion}
+
+
+
+ {balance && (
+
-
- {rightRegion}
+ )}
+ {onPercentOptionClick && (
+
+ )}
-
-
- {balance && (
-
- )}
- {onPercentOptionClick && (
-
- )}
-
- {errors && {errors?.message}}
- {children}
-
- )
-}
+ {errors && {errors?.message}}
+ {children}
+
+ )
+ },
+)
diff --git a/src/components/Trade/Components/TradeAssetInput.tsx b/src/components/Trade/Components/TradeAssetInput.tsx
index 296b83ccd0b..1c00b63950b 100644
--- a/src/components/Trade/Components/TradeAssetInput.tsx
+++ b/src/components/Trade/Components/TradeAssetInput.tsx
@@ -1,6 +1,6 @@
import { Skeleton, SkeletonCircle, Stack, useColorModeValue } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
-import React, { useMemo } from 'react'
+import React, { memo, useMemo } from 'react'
import type { AssetInputProps } from 'components/DeFi/components/AssetInput'
import { bnOrZero } from 'lib/bignumber/bignumber'
import {
@@ -55,14 +55,12 @@ export type TradeAssetInputProps = {
assetId?: AssetId
} & TradeAmountInputProps
-export const TradeAssetInput: React.FC = ({
- assetId,
- accountId,
- ...restAssetInputProps
-}) => {
- return assetId ? (
-
- ) : (
-
- )
-}
+export const TradeAssetInput: React.FC = memo(
+ ({ assetId, accountId, ...restAssetInputProps }) => {
+ return assetId ? (
+
+ ) : (
+
+ )
+ },
+)
diff --git a/src/components/Trade/SelectAsset.tsx b/src/components/Trade/SelectAsset.tsx
index ec39fb30fbb..2b3fe90b9ee 100644
--- a/src/components/Trade/SelectAsset.tsx
+++ b/src/components/Trade/SelectAsset.tsx
@@ -1,3 +1,4 @@
+import { useCallback } from 'react'
import type { RouteComponentProps } from 'react-router-dom'
import { AssetSearch } from 'components/AssetSearch/AssetSearch'
import { Card } from 'components/Card/Card'
@@ -14,9 +15,9 @@ type SelectAssetProps = {
} & RouteComponentProps
export const SelectAsset: React.FC = ({ assets, onClick, history }) => {
- const handleBack = () => {
+ const handleBack = useCallback(() => {
history.push(TradeRoutePaths.Input)
- }
+ }, [history])
return (
diff --git a/src/components/Trade/TradeConfirm/ReceiveSummary.tsx b/src/components/Trade/TradeConfirm/ReceiveSummary.tsx
index 47ce56407d9..f34ce076eed 100644
--- a/src/components/Trade/TradeConfirm/ReceiveSummary.tsx
+++ b/src/components/Trade/TradeConfirm/ReceiveSummary.tsx
@@ -8,7 +8,7 @@ import {
useDisclosure,
} from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
-import { type FC, useCallback, useMemo } from 'react'
+import { type FC, memo, useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { HelperTooltip } from 'components/HelperTooltip/HelperTooltip'
@@ -34,233 +34,237 @@ type ReceiveSummaryProps = {
donationAmount?: string
} & RowProps
-export const ReceiveSummary: FC = ({
- symbol,
- amountCryptoPrecision,
- intermediaryTransactionOutputs,
- fiatAmount,
- amountBeforeFeesCryptoPrecision,
- protocolFees,
- shapeShiftFee,
- slippage,
- swapperName,
- isLoading,
- donationAmount,
- ...rest
-}) => {
- const translate = useTranslate()
- const { isOpen, onToggle } = useDisclosure()
- const summaryBg = useColorModeValue('gray.50', 'gray.800')
- const borderColor = useColorModeValue('gray.100', 'gray.750')
- const hoverColor = useColorModeValue('black', 'white')
- const redColor = useColorModeValue('red.500', 'red.300')
- const greenColor = useColorModeValue('green.600', 'green.200')
- const textColor = useColorModeValue('gray.800', 'whiteAlpha.900')
+export const ReceiveSummary: FC = memo(
+ ({
+ symbol,
+ amountCryptoPrecision,
+ intermediaryTransactionOutputs,
+ fiatAmount,
+ amountBeforeFeesCryptoPrecision,
+ protocolFees,
+ shapeShiftFee,
+ slippage,
+ swapperName,
+ isLoading,
+ donationAmount,
+ ...rest
+ }) => {
+ const translate = useTranslate()
+ const { isOpen, onToggle } = useDisclosure()
+ const summaryBg = useColorModeValue('gray.50', 'gray.800')
+ const borderColor = useColorModeValue('gray.100', 'gray.750')
+ const hoverColor = useColorModeValue('black', 'white')
+ const redColor = useColorModeValue('red.500', 'red.300')
+ const greenColor = useColorModeValue('green.600', 'green.200')
+ const textColor = useColorModeValue('gray.800', 'whiteAlpha.900')
- const slippageAsPercentageString = bnOrZero(slippage).times(100).toString()
- const isAmountPositive = bnOrZero(amountCryptoPrecision).gt(0)
+ const slippageAsPercentageString = bnOrZero(slippage).times(100).toString()
+ const isAmountPositive = bnOrZero(amountCryptoPrecision).gt(0)
- const parseAmountDisplayMeta = useCallback((items: AmountDisplayMeta[]) => {
- return items
- .filter(({ amountCryptoBaseUnit }) => bnOrZero(amountCryptoBaseUnit).gt(0))
- .map(({ amountCryptoBaseUnit, asset }: AmountDisplayMeta) => ({
- symbol: asset.symbol,
- chainName: getChainAdapterManager().get(asset.chainId)?.getDisplayName(),
- amountCryptoPrecision: fromBaseUnit(amountCryptoBaseUnit, asset.precision),
- }))
- }, [])
+ const parseAmountDisplayMeta = useCallback((items: AmountDisplayMeta[]) => {
+ return items
+ .filter(({ amountCryptoBaseUnit }) => bnOrZero(amountCryptoBaseUnit).gt(0))
+ .map(({ amountCryptoBaseUnit, asset }: AmountDisplayMeta) => ({
+ symbol: asset.symbol,
+ chainName: getChainAdapterManager().get(asset.chainId)?.getDisplayName(),
+ amountCryptoPrecision: fromBaseUnit(amountCryptoBaseUnit, asset.precision),
+ }))
+ }, [])
- const protocolFeesParsed = useMemo(
- () => (protocolFees ? parseAmountDisplayMeta(Object.values(protocolFees)) : undefined),
- [protocolFees, parseAmountDisplayMeta],
- )
+ const protocolFeesParsed = useMemo(
+ () => (protocolFees ? parseAmountDisplayMeta(Object.values(protocolFees)) : undefined),
+ [protocolFees, parseAmountDisplayMeta],
+ )
- const intermediaryTransactionOutputsParsed = useMemo(
- () =>
- intermediaryTransactionOutputs
- ? parseAmountDisplayMeta(intermediaryTransactionOutputs)
- : undefined,
- [intermediaryTransactionOutputs, parseAmountDisplayMeta],
- )
+ const intermediaryTransactionOutputsParsed = useMemo(
+ () =>
+ intermediaryTransactionOutputs
+ ? parseAmountDisplayMeta(intermediaryTransactionOutputs)
+ : undefined,
+ [intermediaryTransactionOutputs, parseAmountDisplayMeta],
+ )
- const hasProtocolFees = useMemo(
- () => protocolFeesParsed && protocolFeesParsed.length > 0,
- [protocolFeesParsed],
- )
+ const hasProtocolFees = useMemo(
+ () => protocolFeesParsed && protocolFeesParsed.length > 0,
+ [protocolFeesParsed],
+ )
- const hasIntermediaryTransactionOutputs = useMemo(
- () => intermediaryTransactionOutputsParsed && intermediaryTransactionOutputsParsed.length > 0,
- [intermediaryTransactionOutputsParsed],
- )
+ const hasIntermediaryTransactionOutputs = useMemo(
+ () => intermediaryTransactionOutputsParsed && intermediaryTransactionOutputsParsed.length > 0,
+ [intermediaryTransactionOutputsParsed],
+ )
- return (
- <>
-
-
-
-
- {isOpen ? : }
-
-
-
-
-
-
-
- {fiatAmount && (
+ return (
+ <>
+
+
+
+
+ {isOpen ? : }
+
+
+
+
-
+
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {swapperName}
-
-
-
-
- {amountBeforeFeesCryptoPrecision && (
-
-
-
-
-
+ {fiatAmount && (
-
+
-
-
- )}
- {hasProtocolFees && (
+ )}
+
+
+
+
+
-
+
-
+
- {protocolFeesParsed?.map(({ amountCryptoPrecision, symbol, chainName }) => (
-
-
-
- ))}
-
-
- )}
- {shapeShiftFee && (
-
-
-
+
+ {swapperName}
+
-
-
-
-
-
- )}
- {donationAmount && (
-
-
+ {amountBeforeFeesCryptoPrecision && (
+
-
+
-
-
-
-
-
-
-
- )}
- <>
-
-
-
-
-
-
-
+
-
+
+
+
+ )}
+ {hasProtocolFees && (
+
+
+
+
+
+
+
+ {protocolFeesParsed?.map(({ amountCryptoPrecision, symbol, chainName }) => (
+
+
+
+ ))}
+
+
+ )}
+ {shapeShiftFee && (
+
+
+
+
+
+
+
+
+
- {isAmountPositive &&
- hasIntermediaryTransactionOutputs &&
- intermediaryTransactionOutputsParsed?.map(
- ({ amountCryptoPrecision, symbol, chainName }) => (
-
-
-
- ),
- )}
-
-
-
- >
-
-
- >
- )
-}
+
+
+ )}
+ {donationAmount && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ <>
+
+
+
+
+
+
+
+
+
+
+ {isAmountPositive &&
+ hasIntermediaryTransactionOutputs &&
+ intermediaryTransactionOutputsParsed?.map(
+ ({ amountCryptoPrecision, symbol, chainName }) => (
+
+
+
+ ),
+ )}
+
+
+
+ >
+
+
+ >
+ )
+ },
+)
diff --git a/src/components/TransactionHistory/TransactionHistoryList.tsx b/src/components/TransactionHistory/TransactionHistoryList.tsx
index 0fd843c8528..8fef40b6f34 100644
--- a/src/components/TransactionHistory/TransactionHistoryList.tsx
+++ b/src/components/TransactionHistory/TransactionHistoryList.tsx
@@ -1,4 +1,5 @@
import { Center } from '@chakra-ui/react'
+import { memo } from 'react'
import InfiniteScroll from 'react-infinite-scroller'
import { Card } from 'components/Card/Card'
import { CircularProgress } from 'components/CircularProgress/CircularProgress'
@@ -12,34 +13,33 @@ type TransactionHistoryListProps = {
useCompactMode?: boolean
}
-export const TransactionHistoryList: React.FC = ({
- txIds,
- useCompactMode = false,
-}) => {
- const { next, data, hasMore } = useInfiniteScroll(txIds)
+export const TransactionHistoryList: React.FC = memo(
+ ({ txIds, useCompactMode = false }) => {
+ const { next, data, hasMore } = useInfiniteScroll(txIds)
- return data.length ? (
-
-
-
-
- }
- >
-
-
-
- ) : (
-
-
-
- )
-}
+ return data.length ? (
+
+
+
+
+ }
+ >
+
+
+
+ ) : (
+
+
+
+ )
+ },
+)
diff --git a/src/components/TransactionHistory/TransactionsGroupByDate.tsx b/src/components/TransactionHistory/TransactionsGroupByDate.tsx
index 97cf0eeb262..e3e0a5d2a08 100644
--- a/src/components/TransactionHistory/TransactionsGroupByDate.tsx
+++ b/src/components/TransactionHistory/TransactionsGroupByDate.tsx
@@ -1,6 +1,6 @@
import { Stack, StackDivider, useColorModeValue } from '@chakra-ui/react'
import dayjs from 'dayjs'
-import { useMemo } from 'react'
+import { memo, useMemo } from 'react'
import { TransactionDate } from 'components/TransactionHistoryRows/TransactionDate'
import { TransactionRow } from 'components/TransactionHistoryRows/TransactionRow'
import { useResizeObserver } from 'hooks/useResizeObserver/useResizeObserver'
@@ -18,50 +18,49 @@ type TransactionGroup = {
txIds: TxId[]
}
-export const TransactionsGroupByDate: React.FC = ({
- txIds,
- useCompactMode = false,
-}) => {
- const { setNode, entry } = useResizeObserver()
- const transactions = useAppSelector(state => selectTxDateByIds(state, txIds))
- const borderTopColor = useColorModeValue('gray.100', 'gray.750')
- const txRows = useMemo(() => {
- const groups: TransactionGroup[] = []
- for (let index = 0; index < transactions.length; index++) {
- const transaction = transactions[index]
- const transactionDate = dayjs(transaction.date * 1000)
- .startOf('day')
- .unix()
- const group = groups.find(g => g.date === transactionDate)
- if (group) {
- group.txIds.push(transaction.txId)
- } else {
- groups.push({ date: transactionDate, txIds: [transaction.txId] })
+export const TransactionsGroupByDate: React.FC = memo(
+ ({ txIds, useCompactMode = false }) => {
+ const { setNode, entry } = useResizeObserver()
+ const transactions = useAppSelector(state => selectTxDateByIds(state, txIds))
+ const borderTopColor = useColorModeValue('gray.100', 'gray.750')
+ const txRows = useMemo(() => {
+ const groups: TransactionGroup[] = []
+ for (let index = 0; index < transactions.length; index++) {
+ const transaction = transactions[index]
+ const transactionDate = dayjs(transaction.date * 1000)
+ .startOf('day')
+ .unix()
+ const group = groups.find(g => g.date === transactionDate)
+ if (group) {
+ group.txIds.push(transaction.txId)
+ } else {
+ groups.push({ date: transactionDate, txIds: [transaction.txId] })
+ }
}
- }
- return groups
- }, [transactions])
+ return groups
+ }, [transactions])
- const renderTxRows = useMemo(() => {
- return txRows.map((group: TransactionGroup) => (
-
- {!useCompactMode && }
- {group.txIds?.map((txId: TxId, index: number) => (
-
- ))}
-
- ))
- }, [entry?.contentRect.width, txRows, useCompactMode])
+ const renderTxRows = useMemo(() => {
+ return txRows.map((group: TransactionGroup) => (
+
+ {!useCompactMode && }
+ {group.txIds?.map((txId: TxId, index: number) => (
+
+ ))}
+
+ ))
+ }, [entry?.contentRect.width, txRows, useCompactMode])
- return (
- }>
- {renderTxRows}
-
- )
-}
+ return (
+ }>
+ {renderTxRows}
+
+ )
+ },
+)
diff --git a/src/components/TransactionHistoryRows/TransactionRow.tsx b/src/components/TransactionHistoryRows/TransactionRow.tsx
index e00634f19d8..d5784c305b5 100644
--- a/src/components/TransactionHistoryRows/TransactionRow.tsx
+++ b/src/components/TransactionHistoryRows/TransactionRow.tsx
@@ -3,7 +3,7 @@ import { Box, forwardRef, useColorModeValue } from '@chakra-ui/react'
import { TradeType, TransferType } from '@shapeshiftoss/unchained-client'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
-import { useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { TransactionMethod } from 'components/TransactionHistoryRows/TransactionMethod'
import { TransactionReceive } from 'components/TransactionHistoryRows/TransactionReceive'
import { TransactionSend } from 'components/TransactionHistoryRows/TransactionSend'
@@ -34,6 +34,48 @@ type TxRowProps = {
disableCollapse?: boolean
} & BoxProps
+const TransactionType = ({
+ txDetails,
+ showDateAndGuide,
+ useCompactMode,
+ isOpen,
+ parentWidth,
+ toggleOpen,
+}: {
+ txDetails: TxDetails
+ showDateAndGuide: boolean
+ useCompactMode: boolean
+ isOpen: boolean
+ parentWidth: number
+ toggleOpen: () => void
+}): JSX.Element => {
+ const props: TransactionRowProps = useMemo(
+ () => ({
+ txDetails,
+ showDateAndGuide,
+ compactMode: useCompactMode,
+ toggleOpen,
+ isOpen,
+ parentWidth,
+ }),
+ [isOpen, parentWidth, showDateAndGuide, toggleOpen, txDetails, useCompactMode],
+ )
+
+ switch (txDetails.type) {
+ case TransferType.Send:
+ return
+ case TransferType.Receive:
+ return
+ case TradeType.Trade:
+ case TradeType.Refund:
+ return
+ case 'method':
+ return
+ default:
+ return
+ }
+}
+
export const TransactionRow = forwardRef(
(
{
@@ -48,52 +90,36 @@ export const TransactionRow = forwardRef(
ref,
) => {
const [isOpen, setIsOpen] = useState(initOpen)
- const toggleOpen = () => (disableCollapse ? null : setIsOpen(!isOpen))
+ const toggleOpen = useCallback(
+ () => (disableCollapse ? null : setIsOpen(!isOpen)),
+ [disableCollapse, isOpen],
+ )
const rowHoverBg = useColorModeValue('gray.100', 'gray.750')
const borderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100')
const txDetails = useTxDetails(txId)
- const renderTransactionType = (
- txDetails: TxDetails,
- showDateAndGuide: boolean,
- useCompactMode: boolean,
- ): JSX.Element => {
- const props: TransactionRowProps = {
- txDetails,
- showDateAndGuide,
- compactMode: useCompactMode,
- toggleOpen,
- isOpen,
- parentWidth,
- }
+ const backgroundProps = useMemo(() => ({ bg: rowHoverBg }), [rowHoverBg])
- switch (txDetails.type) {
- case TransferType.Send:
- return
- case TransferType.Receive:
- return
- case TradeType.Trade:
- case TradeType.Refund:
- return
- case 'method':
- return
- default:
- return
- }
- }
return (
- {renderTransactionType(txDetails, showDateAndGuide, useCompactMode)}
+
)
},
diff --git a/src/hooks/useBalanceChartData/useBalanceChartData.ts b/src/hooks/useBalanceChartData/useBalanceChartData.ts
index 49cf491c152..0f01ca31bd8 100644
--- a/src/hooks/useBalanceChartData/useBalanceChartData.ts
+++ b/src/hooks/useBalanceChartData/useBalanceChartData.ts
@@ -5,13 +5,10 @@ import { HistoryTimeframe } from '@shapeshiftoss/types'
import { TransferType, TxStatus } from '@shapeshiftoss/unchained-client'
import type { BigNumber } from 'bignumber.js'
import dayjs from 'dayjs'
-import fill from 'lodash/fill'
import head from 'lodash/head'
import intersection from 'lodash/intersection'
import isEmpty from 'lodash/isEmpty'
import last from 'lodash/last'
-import reduce from 'lodash/reduce'
-import reverse from 'lodash/reverse'
import values from 'lodash/values'
import without from 'lodash/without'
import { useEffect, useMemo, useState } from 'react'
@@ -39,9 +36,7 @@ import { useAppSelector } from 'state/store'
import { excludeTransaction } from './cosmosUtils'
import { CHART_ASSET_ID_BLACKLIST, makeBalanceChartData } from './utils'
-type BalanceByAssetId = {
- [k: AssetId]: BigNumber // map of asset to base units
-}
+type BalanceByAssetId = Record // map of asset to base units
type BucketBalance = {
crypto: BalanceByAssetId
@@ -73,14 +68,25 @@ type MakeBucketsArgs = {
balances: AssetBalancesById
}
+// PERF: limit buckets to a limited number to prevent performance issues
+const NUM_BUCKETS = 100
+
// adjust this to give charts more or less granularity
export const timeframeMap: Record = {
- [HistoryTimeframe.HOUR]: { count: 60, duration: 1, unit: 'minute' },
- [HistoryTimeframe.DAY]: { count: 289, duration: 5, unit: 'minutes' },
- [HistoryTimeframe.WEEK]: { count: 168, duration: 1, unit: 'hours' },
- [HistoryTimeframe.MONTH]: { count: 362, duration: 2, unit: 'hours' },
- [HistoryTimeframe.YEAR]: { count: 365, duration: 1, unit: 'days' },
- [HistoryTimeframe.ALL]: { count: 262, duration: 1, unit: 'weeks' },
+ [HistoryTimeframe.HOUR]: { count: NUM_BUCKETS, duration: 60 / NUM_BUCKETS, unit: 'minute' },
+ [HistoryTimeframe.DAY]: {
+ count: NUM_BUCKETS,
+ duration: (24 * 60) / NUM_BUCKETS,
+ unit: 'minutes',
+ },
+ [HistoryTimeframe.WEEK]: { count: NUM_BUCKETS, duration: (7 * 24) / NUM_BUCKETS, unit: 'hours' },
+ [HistoryTimeframe.MONTH]: {
+ count: NUM_BUCKETS,
+ duration: (31 * 24) / NUM_BUCKETS,
+ unit: 'hours',
+ },
+ [HistoryTimeframe.YEAR]: { count: NUM_BUCKETS, duration: 365 / NUM_BUCKETS, unit: 'days' },
+ [HistoryTimeframe.ALL]: { count: NUM_BUCKETS, duration: 1, unit: 'weeks' },
}
type MakeBuckets = (args: MakeBucketsArgs) => MakeBucketsReturn
@@ -99,9 +105,14 @@ export const makeBuckets: MakeBuckets = args => {
return acc
}, {})
- const makeReducer = (duration: number, unit: dayjs.ManipulateType) => {
- const now = dayjs()
- return (acc: Bucket[], _cur: unknown, idx: number) => {
+ const now = dayjs()
+
+ const meta = timeframeMap[timeframe]
+ const { count, duration, unit } = meta
+
+ const buckets = Array(count)
+ .fill(undefined)
+ .map((_value, idx) => {
const end = now.subtract(idx * duration, unit)
const start = end.subtract(duration, unit).add(1, 'second')
const txs: Tx[] = []
@@ -110,15 +121,10 @@ export const makeBuckets: MakeBuckets = args => {
crypto: assetBalances,
fiat: zeroAssetBalances,
}
- const bucket = { start, end, txs, rebases, balance }
- acc.push(bucket)
- return acc
- }
- }
+ return { start, end, txs, rebases, balance }
+ })
+ .reverse()
- const meta = timeframeMap[timeframe]
- const { count, duration, unit } = meta
- const buckets = reverse(reduce(fill(Array(count), 0), makeReducer(duration, unit), []))
return { buckets, meta }
}
diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx
index 11d88c5be30..615d8aed0a6 100644
--- a/src/pages/Dashboard/Dashboard.tsx
+++ b/src/pages/Dashboard/Dashboard.tsx
@@ -1,4 +1,6 @@
+import type { StackDirection } from '@chakra-ui/react'
import { Stack } from '@chakra-ui/react'
+import { memo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Route, Switch, useRouteMatch } from 'react-router'
import { Main } from 'components/Layout/Main'
@@ -16,7 +18,10 @@ import { Portfolio } from './Portfolio'
import { RewardsDashboard } from './RewardsDashboard'
import { WalletDashboard } from './WalletDashboard'
-export const Dashboard = () => {
+const direction: StackDirection = { base: 'column', xl: 'row' }
+const maxWidth = { base: 'full', xl: 'sm' }
+
+export const Dashboard = memo(() => {
const translate = useTranslate()
const isDefiDashboardEnabled = useFeatureFlag('DefiDashboard')
const { path } = useRouteMatch()
@@ -58,19 +63,14 @@ export const Dashboard = () => {
return (
-
+
-
+
)
-}
+})
diff --git a/src/pages/Dashboard/DashboardSidebar.tsx b/src/pages/Dashboard/DashboardSidebar.tsx
index e4a6411b288..8a2d7085907 100644
--- a/src/pages/Dashboard/DashboardSidebar.tsx
+++ b/src/pages/Dashboard/DashboardSidebar.tsx
@@ -1,5 +1,6 @@
import { Flex, Image } from '@chakra-ui/react'
import { btcAssetId, dogeAssetId, foxAssetId } from '@shapeshiftoss/caip'
+import { memo } from 'react'
import OnRamperLogo from 'assets/on-ramper.png'
import SaversVaultTop from 'assets/savers-vault-top.png'
import { AssetIcon } from 'components/AssetIcon'
@@ -68,19 +69,17 @@ const promoData: PromoItem[] = [
},
]
-export const DashboardSidebar = () => {
+const display = { base: 'none', xl: 'block' }
+
+export const DashboardSidebar = memo(() => {
const { MultiHopTrades } = useAppSelector(selectFeatureFlags)
return (
- {MultiHopTrades ? (
-
- ) : (
-
- )}
+ {MultiHopTrades ? : }
)
-}
+})
diff --git a/src/pages/Dashboard/EarnDashboard.tsx b/src/pages/Dashboard/EarnDashboard.tsx
index 18a6bff1075..166d14c2a30 100644
--- a/src/pages/Dashboard/EarnDashboard.tsx
+++ b/src/pages/Dashboard/EarnDashboard.tsx
@@ -1,15 +1,19 @@
import { ArrowForwardIcon } from '@chakra-ui/icons'
import { Button, Flex, Heading } from '@chakra-ui/react'
+import { memo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Link as NavLink } from 'react-router-dom'
import { DeFiEarn } from 'components/StakingVaults/DeFiEarn'
import { RawText } from 'components/Text'
+const alignItems = { base: 'flex-start', md: 'center' }
+const padding = { base: 4, xl: 0 }
+
const EarnHeader = () => {
const translate = useTranslate()
return (
-
+
{translate('defi.myPositions')}
{
)
}
-export const EarnDashboard = () => {
+export const EarnDashboard = memo(() => {
return } />
-}
+})
diff --git a/src/pages/Dashboard/Portfolio.tsx b/src/pages/Dashboard/Portfolio.tsx
index a5a94d925fc..ce6ab2ac3d2 100644
--- a/src/pages/Dashboard/Portfolio.tsx
+++ b/src/pages/Dashboard/Portfolio.tsx
@@ -9,7 +9,7 @@ import {
Switch,
} from '@chakra-ui/react'
import type { HistoryTimeframe } from '@shapeshiftoss/types'
-import { useCallback, useMemo, useState } from 'react'
+import { memo, useCallback, useMemo, useState } from 'react'
import { Amount } from 'components/Amount/Amount'
import { BalanceChart } from 'components/BalanceChart/BalanceChart'
import { Card } from 'components/Card/Card'
@@ -31,7 +31,7 @@ import { useAppSelector } from 'state/store'
import { AccountTable } from './components/AccountList/AccountTable'
import { PortfolioBreakdown } from './PortfolioBreakdown'
-export const Portfolio = () => {
+export const Portfolio = memo(() => {
const userChartTimeframe = useAppSelector(selectChartTimeframe)
const [timeframe, setTimeframe] = useState(userChartTimeframe)
const handleTimeframeChange = useTimeframeChange(setTimeframe)
@@ -143,4 +143,4 @@ export const Portfolio = () => {
)
-}
+})
diff --git a/src/pages/Dashboard/PortfolioBreakdown.tsx b/src/pages/Dashboard/PortfolioBreakdown.tsx
index ad202bb3a67..5324c77a7c3 100644
--- a/src/pages/Dashboard/PortfolioBreakdown.tsx
+++ b/src/pages/Dashboard/PortfolioBreakdown.tsx
@@ -1,5 +1,5 @@
import { Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'
-import { useMemo } from 'react'
+import { memo, useMemo } from 'react'
import { useHistory } from 'react-router'
import { Amount } from 'components/Amount/Amount'
import { Card } from 'components/Card/Card'
@@ -52,7 +52,7 @@ const BreakdownCard: React.FC = ({
)
}
-export const PortfolioBreakdown = () => {
+export const PortfolioBreakdown = memo(() => {
const history = useHistory()
const earnUserCurrencyBalance = useAppSelector(selectEarnBalancesUserCurrencyAmountFull).toFixed()
const claimableRewardsUserCurrencyBalanceFilter = useMemo(() => ({}), [])
@@ -96,4 +96,4 @@ export const PortfolioBreakdown = () => {
/>
)
-}
+})
diff --git a/src/pages/Dashboard/RecentTransactions.tsx b/src/pages/Dashboard/RecentTransactions.tsx
index 6106ccd2518..c4542b0aa61 100644
--- a/src/pages/Dashboard/RecentTransactions.tsx
+++ b/src/pages/Dashboard/RecentTransactions.tsx
@@ -1,4 +1,5 @@
import { Button } from '@chakra-ui/react'
+import { memo } from 'react'
import { useTranslate } from 'react-polyglot'
import { useSelector } from 'react-redux'
import { NavLink } from 'react-router-dom'
@@ -11,26 +12,30 @@ import { selectLastNTxIds } from 'state/slices/selectors'
type RecentTransactionProps = { limit?: number; viewMoreLink?: boolean } & CardProps
-export const RecentTransactions: React.FC = ({
- limit = 10,
- viewMoreLink,
- ...rest
-}) => {
- const recentTxIds = useSelector((state: ReduxState) => selectLastNTxIds(state, limit))
- const translate = useTranslate()
- return (
-
-
-
-
-
- {viewMoreLink && (
-
- {translate('common.viewAll')}
-
- )}
-
-
-
- )
-}
+export const RecentTransactions: React.FC = memo(
+ ({ limit = 10, viewMoreLink, ...rest }) => {
+ const recentTxIds = useSelector((state: ReduxState) => selectLastNTxIds(state, limit))
+ const translate = useTranslate()
+ return (
+
+
+
+
+
+ {viewMoreLink && (
+
+ {translate('common.viewAll')}
+
+ )}
+
+
+
+ )
+ },
+)
diff --git a/src/pages/Dashboard/RewardsDashboard.tsx b/src/pages/Dashboard/RewardsDashboard.tsx
index 166b712206a..9650f212ce3 100644
--- a/src/pages/Dashboard/RewardsDashboard.tsx
+++ b/src/pages/Dashboard/RewardsDashboard.tsx
@@ -1,14 +1,18 @@
import { ArrowForwardIcon } from '@chakra-ui/icons'
import { Button, Flex, Heading } from '@chakra-ui/react'
+import { memo } from 'react'
import { useTranslate } from 'react-polyglot'
import { Link as NavLink } from 'react-router-dom'
import { DeFiEarn } from 'components/StakingVaults/DeFiEarn'
import { RawText } from 'components/Text'
+const alignItems = { base: 'flex-start', md: 'center' }
+const padding = { base: 4, xl: 0 }
+
const RewardsHeader = () => {
const translate = useTranslate()
return (
-
+
{translate('defi.myRewards')}
{
)
}
-export const RewardsDashboard = () => {
+export const RewardsDashboard = memo(() => {
return } />
-}
+})
diff --git a/src/pages/Dashboard/WalletDashboard.tsx b/src/pages/Dashboard/WalletDashboard.tsx
index 2b3d8bd9abd..a81f90e2b1f 100644
--- a/src/pages/Dashboard/WalletDashboard.tsx
+++ b/src/pages/Dashboard/WalletDashboard.tsx
@@ -1,22 +1,22 @@
+import type { StackDirection } from '@chakra-ui/react'
import { Stack } from '@chakra-ui/react'
+import { memo } from 'react'
import { DashboardSidebar } from './DashboardSidebar'
import { Portfolio } from './Portfolio'
-export const WalletDashboard = () => {
+const direction: StackDirection = { base: 'column', '2xl': 'row' }
+const maxWidth = { base: 'full', xl: 'sm' }
+
+export const WalletDashboard = memo(() => {
return (
-
+
-
+
)
-}
+})
diff --git a/src/pages/Dashboard/components/AccountList/AccountTable.tsx b/src/pages/Dashboard/components/AccountList/AccountTable.tsx
index ac400472158..355a7364b99 100644
--- a/src/pages/Dashboard/components/AccountList/AccountTable.tsx
+++ b/src/pages/Dashboard/components/AccountList/AccountTable.tsx
@@ -1,6 +1,6 @@
import { Stack, Stat, StatArrow, StatNumber, useColorModeValue } from '@chakra-ui/react'
import { range } from 'lodash'
-import { useCallback, useMemo } from 'react'
+import { memo, useCallback, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import type { Column, Row } from 'react-table'
@@ -15,7 +15,7 @@ import { selectPortfolioAccountRows, selectPortfolioLoading } from 'state/slices
type RowProps = Row
-export const AccountTable = () => {
+export const AccountTable = memo(() => {
const loading = useSelector(selectPortfolioLoading)
const rowData = useSelector(selectPortfolioAccountRows)
const textColor = useColorModeValue('black', 'white')
@@ -121,4 +121,4 @@ export const AccountTable = () => {
onRowClick={handleRowClick}
/>
)
-}
+})
diff --git a/src/pages/Trade/Trade.tsx b/src/pages/Trade/Trade.tsx
index b21b9dfc6cc..9468ba2c522 100644
--- a/src/pages/Trade/Trade.tsx
+++ b/src/pages/Trade/Trade.tsx
@@ -1,4 +1,5 @@
import { Container, Stack } from '@chakra-ui/react'
+import { memo } from 'react'
import foxPageBg from 'assets/foxpage-bg.png'
import { Main } from 'components/Layout/Main'
import { MultiHopTrade } from 'components/MultiHopTrade/MultiHopTrade'
@@ -7,7 +8,10 @@ import { TradeCard } from 'pages/Dashboard/TradeCard'
import { selectFeatureFlags } from 'state/slices/selectors'
import { useAppSelector } from 'state/store'
-export const Trade = () => {
+const maxWidth = { base: '100%', lg: 'container.sm' }
+const padding = { base: 0, md: 8 }
+
+export const Trade = memo(() => {
const { MultiHopTrades } = useAppSelector(selectFeatureFlags)
return (
{
backgroundRepeat='no-repeat'
>
-
+
{MultiHopTrades ? : }
@@ -39,4 +38,4 @@ export const Trade = () => {
)
-}
+})
diff --git a/src/pages/TransactionHistory/DownloadButton.tsx b/src/pages/TransactionHistory/DownloadButton.tsx
index 404a95f6f98..dd22892b640 100644
--- a/src/pages/TransactionHistory/DownloadButton.tsx
+++ b/src/pages/TransactionHistory/DownloadButton.tsx
@@ -2,7 +2,7 @@ import { Button, useMediaQuery } from '@chakra-ui/react'
import { TransferType } from '@shapeshiftoss/unchained-client'
import dayjs from 'dayjs'
import fileDownload from 'js-file-download'
-import { useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import { Text } from 'components/Text'
import { getTransfers, getTxType } from 'hooks/useTxDetails/useTxDetails'
@@ -41,6 +41,8 @@ const jsonToCsv = (fields: Record, rows: ReportRow[]): string =>
return `${csvRows}\r\n`
}
+const buttonMargin = [3, 3, 6]
+
export const DownloadButton = ({ txIds }: { txIds: TxId[] }) => {
const [isLoading, setIsLoading] = useState(false)
const [isLargerThanLg] = useMediaQuery(`(min-width: ${breakpoints['lg']})`, { ssr: false })
@@ -48,22 +50,25 @@ export const DownloadButton = ({ txIds }: { txIds: TxId[] }) => {
const assets = useAppSelector(selectAssets)
const marketData = useAppSelector(selectSelectedCurrencyMarketDataSortedByMarketCap)
const translate = useTranslate()
- const fields = {
- txid: translate('transactionHistory.csv.txid'),
- type: translate('transactionHistory.csv.type'),
- status: translate('transactionHistory.csv.status'),
- timestamp: translate('transactionHistory.csv.timestamp'),
- minerFee: translate('transactionHistory.csv.minerFee'),
- minerFeeCurrency: translate('transactionHistory.csv.minerFeeCurrency'),
- inputAmount: translate('transactionHistory.csv.inputAmount'),
- inputCurrency: translate('transactionHistory.csv.inputCurrency'),
- inputAddress: translate('transactionHistory.csv.inputAddress'),
- outputAmount: translate('transactionHistory.csv.outputAmount'),
- outputCurrency: translate('transactionHistory.csv.outputCurrency'),
- outputAddress: translate('transactionHistory.csv.outputAddress'),
- }
+ const fields = useMemo(
+ () => ({
+ txid: translate('transactionHistory.csv.txid'),
+ type: translate('transactionHistory.csv.type'),
+ status: translate('transactionHistory.csv.status'),
+ timestamp: translate('transactionHistory.csv.timestamp'),
+ minerFee: translate('transactionHistory.csv.minerFee'),
+ minerFeeCurrency: translate('transactionHistory.csv.minerFeeCurrency'),
+ inputAmount: translate('transactionHistory.csv.inputAmount'),
+ inputCurrency: translate('transactionHistory.csv.inputCurrency'),
+ inputAddress: translate('transactionHistory.csv.inputAddress'),
+ outputAmount: translate('transactionHistory.csv.outputAmount'),
+ outputCurrency: translate('transactionHistory.csv.outputCurrency'),
+ outputAddress: translate('transactionHistory.csv.outputAddress'),
+ }),
+ [translate],
+ )
- const generateCSV = () => {
+ const generateCSV = useCallback(() => {
setIsLoading(true)
const report: ReportRow[] = []
@@ -121,11 +126,11 @@ export const DownloadButton = ({ txIds }: { txIds: TxId[] }) => {
} finally {
setIsLoading(false)
}
- }
+ }, [allTxs, assets, fields, marketData, translate, txIds])
return isLargerThanLg ? (
{
+const headingPadding = [2, 3, 6]
+
+export const TransactionHistory = memo(() => {
const translate = useTranslate()
const { path } = useRouteMatch()
const inputRef = useRef(null)
@@ -46,7 +48,7 @@ export const TransactionHistory = () => {
-
+
@@ -67,4 +69,4 @@ export const TransactionHistory = () => {
)
-}
+})
diff --git a/src/pages/TransactionHistory/TransactionHistoryFilter.tsx b/src/pages/TransactionHistory/TransactionHistoryFilter.tsx
index 522e7c085d3..b3dddd2a1a0 100644
--- a/src/pages/TransactionHistory/TransactionHistoryFilter.tsx
+++ b/src/pages/TransactionHistory/TransactionHistoryFilter.tsx
@@ -15,13 +15,14 @@ import {
} from '@chakra-ui/react'
import { TradeType, TransferType } from '@shapeshiftoss/unchained-client'
import dayjs from 'dayjs'
-import { useRef, useState } from 'react'
+import { memo, useCallback, useMemo, useRef, useState } from 'react'
import type { FieldValues } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import { IoOptionsOutline } from 'react-icons/io5'
import { useTranslate } from 'react-polyglot'
import { Text } from 'components/Text'
+import type { Option } from '../../components/FilterGroup'
import { FilterGroup } from '../../components/FilterGroup'
import { DatePicker } from './components/DatePicker/DatePicker'
@@ -40,153 +41,169 @@ type TransactionHistoryFilterProps = {
hasAppliedFilter?: boolean
}
-export const TransactionHistoryFilter = ({
- setFilters,
- resetFilters,
- hasAppliedFilter = false,
-}: TransactionHistoryFilterProps) => {
- const [isOpen, setIsOpen] = useState(false)
- const popoverRef = useRef(null)
- /**
- * Popover default outside click detector didn't play well with
- * react-datepicker, but making it controlled and
- * passing a new detector to popover content,
- * solved the problem.
- */
- useOutsideClick({
- ref: popoverRef,
- handler: () => {
- setIsOpen(false)
- },
- })
- const translate = useTranslate()
- const { control, handleSubmit, watch, reset } = useForm({ mode: 'onChange' })
- const onSubmit = (values: FieldValues) => {
- const { fromDate, toDate, dayRange, types } = values
- let filterSet = {
- fromDate,
- toDate,
- dayRange,
- types,
- }
- if (!!dayRange && dayRange !== customRangeOption) {
- const today = dayjs().endOf('day')
- filterSet.fromDate = today.subtract(dayRange, 'day').unix()
- filterSet.toDate = today.unix()
- } else if (dayRange === customRangeOption) {
- if (fromDate) {
- filterSet.fromDate = dayjs(fromDate).startOf('day').unix()
- }
- if (toDate) {
- filterSet.toDate = dayjs(toDate).endOf('day').unix()
- }
- }
- setFilters(filterSet)
- }
- const popoverContentBg = useColorModeValue('gray.100', 'gray.700')
- const onResetFilters = () => {
- reset()
- resetFilters()
- }
- const dayRangeSelectedOption = watch(FilterFormFields.DayRange)
- const RangeCustomComponent = () => {
- return dayRangeSelectedOption === customRangeOption ? (
-
-
-
-
-
- ) : null
- }
- return (
-
- <>
-
-
- }
- onClick={() => setIsOpen(state => !state)}
- >
-
-
- }
- onClick={e => {
- e.stopPropagation()
- onResetFilters()
- }}
- />
-
-
-
-
-
-
+export const TransactionHistoryFilter = memo(
+ ({ setFilters, resetFilters, hasAppliedFilter = false }: TransactionHistoryFilterProps) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const popoverRef = useRef(null)
+ /**
+ * Popover default outside click detector didn't play well with
+ * react-datepicker, but making it controlled and
+ * passing a new detector to popover content,
+ * solved the problem.
+ */
+ useOutsideClick({
+ ref: popoverRef,
+ handler: () => {
+ setIsOpen(false)
+ },
+ })
+ const translate = useTranslate()
+ const { control, handleSubmit, watch, reset } = useForm({ mode: 'onChange' })
+ const onSubmit = useCallback(
+ (values: FieldValues) => {
+ const { fromDate, toDate, dayRange, types } = values
+ let filterSet = {
+ fromDate,
+ toDate,
+ dayRange,
+ types,
+ }
+ if (!!dayRange && dayRange !== customRangeOption) {
+ const today = dayjs().endOf('day')
+ filterSet.fromDate = today.subtract(dayRange, 'day').unix()
+ filterSet.toDate = today.unix()
+ } else if (dayRange === customRangeOption) {
+ if (fromDate) {
+ filterSet.fromDate = dayjs(fromDate).startOf('day').unix()
+ }
+ if (toDate) {
+ filterSet.toDate = dayjs(toDate).endOf('day').unix()
+ }
+ }
+ setFilters(filterSet)
+ },
+ [setFilters],
+ )
+ const popoverContentBg = useColorModeValue('gray.100', 'gray.700')
+ const onResetFilters = useCallback(() => {
+ reset()
+ resetFilters()
+ }, [reset, resetFilters])
+ const dayRangeSelectedOption = watch(FilterFormFields.DayRange)
+ const RangeCustomComponent = useCallback(() => {
+ return dayRangeSelectedOption === customRangeOption ? (
+
+
+
+
+
+ ) : null
+ }, [control, dayRangeSelectedOption])
+
+ const handleClose = useCallback(() => setIsOpen(false), [setIsOpen])
+ const handleToggle = useCallback(() => setIsOpen(state => !state), [setIsOpen])
+
+ const handleResetStopPagination: React.MouseEventHandler = useCallback(
+ e => {
+ e.stopPropagation()
+ onResetFilters()
+ },
+ [onResetFilters],
+ )
+
+ const dayRangeOptions: Option[] = useMemo(
+ () => [
+ ['transactionHistory.filters.custom', customRangeOption, ],
+ ['transactionHistory.filters.10days', '10'],
+ ['transactionHistory.filters.30days', '30'],
+ ['transactionHistory.filters.90days', '90'],
+ ],
+ [RangeCustomComponent],
+ )
+
+ const categoriesOptions: Option[] = useMemo(
+ () => [
+ ['transactionHistory.filters.send', TransferType.Send],
+ ['transactionHistory.filters.trade', TradeType.Trade],
+ ['transactionHistory.filters.receive', TransferType.Receive],
+ ],
+ [],
+ )
+
+ return (
+
+ <>
+
+
onResetFilters()}
+ variant='ghost-filled'
+ leftIcon={}
+ onClick={handleToggle}
>
-
+
-
-
-
-
-
- >
-
- )
-}
+
+
+
+
+ >
+
+ )
+ },
+)
diff --git a/src/pages/TransactionHistory/TransactionHistorySearch.tsx b/src/pages/TransactionHistory/TransactionHistorySearch.tsx
index 2112e223a4d..b8694bf88b5 100644
--- a/src/pages/TransactionHistory/TransactionHistorySearch.tsx
+++ b/src/pages/TransactionHistory/TransactionHistorySearch.tsx
@@ -1,20 +1,26 @@
import { SearchIcon } from '@chakra-ui/icons'
import { Input, InputGroup, InputLeftElement, useColorModeValue } from '@chakra-ui/react'
-import { forwardRef } from 'react'
+import { forwardRef, useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
+const inputGroupMargin = [2, 3, 6]
+
export const TransactionHistorySearch = forwardRef<
HTMLInputElement,
{ handleInputChange: Function }
>(({ handleInputChange }, ref) => {
const translate = useTranslate()
+ const handleOnChange: React.ChangeEventHandler = useCallback(
+ e => handleInputChange(e.target.value),
+ [handleInputChange],
+ )
return (
-
+
handleInputChange(e.target.value)}
+ onChange={handleOnChange}
type='text'
placeholder={translate('common.search')}
pl={10}
diff --git a/src/pages/TransactionHistory/components/DatePicker/DatePicker.tsx b/src/pages/TransactionHistory/components/DatePicker/DatePicker.tsx
index 567ddef1fa7..d518f920043 100644
--- a/src/pages/TransactionHistory/components/DatePicker/DatePicker.tsx
+++ b/src/pages/TransactionHistory/components/DatePicker/DatePicker.tsx
@@ -2,6 +2,7 @@ import 'react-datepicker/dist/react-datepicker.css'
import './DatePicker.css'
import { Input, InputGroup, InputLeftElement, useColorModeValue } from '@chakra-ui/react'
+import { useCallback } from 'react'
import ReactDatePicker from 'react-datepicker'
import type { Control } from 'react-hook-form'
import { useController } from 'react-hook-form'
@@ -11,6 +12,7 @@ export const DatePicker = ({ control, name }: { control: Control; name: string }
const {
field: { onChange, value },
} = useController({ control, name })
+ const handleFormatWeekDat = useCallback((day: string) => day.slice(0, 1), [])
return (
@@ -25,7 +27,7 @@ export const DatePicker = ({ control, name }: { control: Control; name: string }
name={name}
placeholderText='00/00/0000'
autoComplete='off'
- formatWeekDay={(day: string) => day.slice(0, 1)}
+ formatWeekDay={handleFormatWeekDat}
/>
)
diff --git a/src/pages/TransactionHistory/hooks/useFilters.tsx b/src/pages/TransactionHistory/hooks/useFilters.tsx
index 729f700238b..56fded496d2 100644
--- a/src/pages/TransactionHistory/hooks/useFilters.tsx
+++ b/src/pages/TransactionHistory/hooks/useFilters.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useCallback, useState } from 'react'
type TransactionFilterType = {
fromDate: number | null
@@ -15,7 +15,7 @@ const initialState = {
export const useFilters = () => {
const [filters, setFilters] = useState(initialState)
- const resetFilters = () => setFilters(initialState)
+ const resetFilters = useCallback(() => setFilters(initialState), [])
return {
filters,