diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBox.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBox.tsx deleted file mode 100644 index 8efef82a2..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBox.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import type { ComboBoxSelectTokenProps } from '@/dex/components/ComboBoxSelectToken/types' - -import React, { useEffect, useRef, useState } from 'react' -import { t } from '@ui-kit/lib/i18n' -import chunk from 'lodash/chunk' -import styled from 'styled-components' - -import { breakpoints } from '@ui/utils/responsive' -import useStore from '@/dex/store/useStore' - -import { RCEditClear } from '@ui/images' -import { StyledInput } from '@ui/InputComp/styles' -import Box from '@ui/Box/Box' -import Checkbox from '@ui/Checkbox' -import ComboBoxListChunk from '@/dex/components/ComboBoxSelectToken/ComboBoxListChunk' -import Icon from '@ui/Icon' -import IconButton from '@ui/IconButton/IconButton' -import InputProvider from '@ui/InputComp/InputProvider' -import Popover from '@ui/Popover/Popover' -import Spinner from '@ui/Spinner' -import SpinnerWrapper from '@ui/Spinner/SpinnerWrapper' -import { Token } from '@/dex/types/main.types' - -const ComboBox = ({ - testId, - dialogClose, - blockchainId, - listBoxHeight, - showBalances, - result, - selectedToken, - showCheckboxHideSmallPools, - showInpSearch, - tokens, - handleInpChange, - handleOnSelectChange, -}: Pick< - ComboBoxSelectTokenProps, - | 'testId' - | 'blockchainId' - | 'listBoxHeight' - | 'showBalances' - | 'showCheckboxHideSmallPools' - | 'showInpSearch' - | 'tokens' -> & { - dialogClose: () => void - result: Token[] | undefined - selectedToken: string - handleInpChange(filterValue: string, tokens: Token[] | undefined): void - handleOnSelectChange(selectedAddress: string): void -}) => { - const topContentRef = useRef(null) - const inputRef = useRef(null) - const listRef = useRef(null) - const popoverRef = useRef(null) - - const [topContentHeight, setTopContentHeight] = useState() - - const filterValue = useStore((state) => state.selectToken.filterValue) - const hideSmallPools = useStore((state) => state.poolList.formValues.hideSmallPools) - const setPoolListFormValues = useStore((state) => state.quickSwap.setPoolListFormValues) - - useEffect(() => { - if (topContentRef?.current) { - setTopContentHeight(topContentRef.current.getBoundingClientRect().height) - } - }, [topContentRef]) - - return ( - <> - {}}> - - {(showInpSearch || showCheckboxHideSmallPools) && ( - - {showInpSearch && ( - - - - ) => - handleInpChange(value, tokens) - } - onKeyDown={(evt) => { - // scroll to first item on list - if (evt.key === 'ArrowDown' && result && result.length > 0 && listRef.current) { - const visibleList = listRef.current.querySelector('.visible') - const firstButton = visibleList?.querySelector?.('button') - - if (firstButton) { - firstButton.focus() - // setTimeout needed or else it over scroll element - setTimeout(() => firstButton.scrollIntoView(), 200) - } - } else if (evt.key === 'Escape') { - dialogClose() - } - }} - /> - handleOnSelectChange('')} - > - - - - - - - - )} - - {/* CHECKBOX */} - {showCheckboxHideSmallPools && ( - - {t`Hide tokens from very small pools`} - - )} - - )} - - {/* LIST */} - - {Array.isArray(result) && result.length > 0 ? ( - chunk(result, 30).map((tokens, idx) => ( - - )) - ) : !!filterValue ? ( - {t`No token found for "${filterValue}"`} - ) : ( - - - - )} - - - - - ) -} - -const ComboBoxSearchInpWrapper = styled(InputProvider)` - align-items: center; - display: grid; - grid-template-columns: auto 1fr auto; - grid-column-gap: var(--spacing-2); - position: relative; - transition: 3s; -` - -const ComboBoxSearchInpClearBtn = styled(IconButton)` - display: none; - min-width: 1.5625rem; //25px - opacity: 1; - padding: 0; - - &.show { - display: inline-block; - } - - .svg-tooltip { - position: relative; - top: 2px; - } -` - -const ComboBoxCheckbox = styled(Checkbox)` - margin-left: var(--spacing-3); -` - -const ComboBoxListWrapper = styled.div<{ boxHeight: string; topContentHeight: number | undefined }>` - overflow-x: hidden; - overflow-y: scroll; - ${({ topContentHeight }) => { - if (topContentHeight) { - return `height: calc(100vh - ${topContentHeight}px);` - } - }}; - - @media (min-width: ${breakpoints.sm}rem) { - height: ${({ boxHeight }) => boxHeight}; - } -` - -const ComboBoxListNoResult = styled.li` - text-align: center; - padding: var(--spacing-wide) var(--spacing-2); -` - -export default ComboBox diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListChunk.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListChunk.tsx deleted file mode 100644 index 7db5c528a..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListChunk.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { ComboBoxSelectTokenProps } from '@/dex/components/ComboBoxSelectToken/types' - -import React, { useRef } from 'react' -import styled from 'styled-components' - -import useIntersectionObserver from 'ui/src/hooks/useIntersectionObserver' - -import SelectTokenListItem from '@/dex/components/ComboBoxSelectToken/ComboBoxListItem' -import { Token } from '@/dex/types/main.types' - -const SelectTokenListChunk = ({ - testId, - blockchainId, - inputRef, - showBalances, - selectedToken, - tokens, - dialogClose, - handleOnSelectChange, -}: Pick & { - inputRef?: React.RefObject - selectedToken: string - tokens: Token[] - dialogClose: () => void - handleOnSelectChange(selectedToken: string): void -}) => { - const ref = useRef(null) - const entry = useIntersectionObserver(ref, { freezeOnceVisible: false }) - - const isVisible = !!entry?.isIntersecting - - return ( - { - // scroll up/down list - const activeElement = document.activeElement - - if (evt.key === 'ArrowUp') { - const previousButton = activeElement?.parentElement?.previousElementSibling - if (previousButton) { - previousButton.querySelector('button')?.focus() - } else if (inputRef?.current) { - inputRef.current.focus() - } - } else if (evt.key === 'ArrowDown') { - const nextButton = activeElement?.parentElement?.nextElementSibling - if (nextButton) { - nextButton.querySelector('button')?.focus() - } - } else if (evt.key === 'Escape') { - dialogClose() - } - }} - > - {isVisible && - tokens.map((item) => ( - - ))} - - ) -} - -const ItemsWrapper = styled.ul<{ count: number }>` - height: ${({ count }) => `${count * 50}px`}; -` - -export default SelectTokenListChunk diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListItem.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListItem.tsx deleted file mode 100644 index 5e57c4279..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxListItem.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { ComboBoxSelectTokenProps } from '@/dex/components/ComboBoxSelectToken/types' - -import React from 'react' -import styled from 'styled-components' - -import { focusVisible } from '@ui/utils' -import { shortenTokenAddress } from '@/dex/utils' - -import Box from '@ui/Box' -import Button from '@ui/Button' -import Chip from '@ui/Typography/Chip' -import SelectTokenListItemUserBalance from '@/dex/components/ComboBoxSelectToken/ComboBoxTokenUserBalance' -import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon' -import { Token } from '@/dex/types/main.types' - -const ComboBoxListItem = ({ - blockchainId, - testId, - showBalances, - selectedToken, - handleOnSelectChange, - ...item -}: Pick & - Token & { - selectedToken: string - handleOnSelectChange(selectedToken: string): void - }) => ( -
  • - handleOnSelectChange(item.address)} - > - - - - - {item.symbol} - - {shortenTokenAddress(item.address)} - - - {showBalances && } - -
  • -) - -const ItemButton = styled(Button)` - ${focusVisible}; - - &.focus-visible, - &.active { - color: var(--box--primary--color); - background-color: var(--table_detail_row--active--background-color); - } - align-items: center; - border: none; - display: grid; - font-family: inherit; - padding: 0 var(--spacing-3); - grid-column-gap: var(--spacing-2); - grid-template-columns: auto 1fr auto; - height: 50px; - width: 100%; -` - -const IconWrapper = styled.div` - min-width: 1.875rem; // 30px; - text-align: left; -` - -const LabelText = styled.div` - overflow: hidden; - - font-size: var(--font-size-4); - font-weight: var(--font-weight--bold); - line-height: 1; - text-overflow: ellipsis; - text-transform: initial; -` - -const LabelTextWrapper = styled(Box)` - overflow: hidden; - text-overflow: ellipsis; -` - -export default ComboBoxListItem diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedToken.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedToken.tsx deleted file mode 100644 index c41c3bdfe..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedToken.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react' -import styled from 'styled-components' - -import { shortenTokenAddress } from '@/dex/utils' - -import { Chip } from '@ui/Typography' -import Box from '@ui/Box' -import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon' -import { Token } from '@/dex/types/main.types' - -const ComboBoxSelectedToken = ({ - blockchainId, - selected, - testId, -}: { - blockchainId: string - selected: Token - testId: string | undefined -}) => ( - <> - - - - - - {selected.symbol} - {' '} - {selected?.haveSameTokenName && {shortenTokenAddress(selected.address)}} - - -) - -const AddressChip = styled(Chip)` - margin-top: var(--spacing-1); -` - -const SelectedLabelText = styled.span` - overflow: hidden; - - font-size: var(--input_button--font-size); - text-overflow: ellipsis; - white-space: nowrap; -` - -const TokenIconWrapper = styled.div` - display: inline-block; - min-height: 1.25rem; // 20px - min-width: 1.25rem; // 20px -` - -const LabelTextWrapper = styled(Box)` - overflow: hidden; - text-overflow: ellipsis; -` - -export default ComboBoxSelectedToken diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedTokenButton.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedTokenButton.tsx deleted file mode 100644 index 654cd7d87..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxSelectedTokenButton.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { AriaButtonProps } from 'react-aria' -import type { ButtonProps } from '@ui/Button/types' - -import { useButton } from 'react-aria' -import React, { useRef } from 'react' -import styled from 'styled-components' - -import ButtonComp from '@ui/Button' - -const ComboBoxSelectedTokenButton = (props: React.PropsWithChildren & ButtonProps>) => { - const ref = useRef(null) - const { buttonProps } = useButton(props, ref) - const { children, onPress, ...rest } = props - - return ( - - {children} - - ) -} - -const StyledComboBoxButton = styled(ButtonComp)` - align-items: center; - display: grid; - padding-right: var(--spacing-2); - height: 100%; - - text-transform: var(--input_button--text-transform); - - color: var(--input_button--color); - background-color: var(--input--background-color); - border: 0.5px solid var(--input_button--border-color); - box-shadow: inset -2px -2px 0px 0.25px var(--box--primary--shadow-color); - - grid-template-columns: auto 1fr auto; - - transition: - background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, - color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - - &:disabled { - opacity: 0.7; - } - - &:hover:not(:disabled) { - color: var(--input_button--hover--color); - background-color: var(--input_button--hover--background-color); - } -` - -export default ComboBoxSelectedTokenButton diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxTokenUserBalance.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxTokenUserBalance.tsx deleted file mode 100644 index 40179dc7a..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/ComboBoxTokenUserBalance.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' - -import { FORMAT_OPTIONS, formatNumber } from '@ui/utils' -import useStore from '@/dex/store/useStore' - -import Box from '@ui/Box' -import Spinner from '@ui/Spinner' -import TextCaption from '@ui/TextCaption' - -const ComboBoxTokenUserBalance = ({ tokenAddress }: { tokenAddress: string }) => { - const userBalancesMapper = useStore((state) => state.userBalances.userBalancesMapper) - const usdRatesMapper = useStore((state) => state.usdRates.usdRatesMapper) - const userBalance = userBalancesMapper[tokenAddress] - const userBalanceUsdRate = usdRatesMapper[tokenAddress] - const userBalanceUsd = +(userBalance ?? '0') * +(userBalanceUsdRate ?? '0') - - return ( - <> - {typeof userBalance === 'undefined' ? ( - - ) : ( - -
    {formatNumber(userBalance)}
    - {userBalanceUsd > 0 ? {formatNumber(userBalanceUsd, FORMAT_OPTIONS.USD)} : null} -
    - )} - - ) -} - -export default ComboBoxTokenUserBalance diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/index.tsx b/apps/main/src/dex/components/ComboBoxSelectToken/index.tsx deleted file mode 100644 index b6acbb558..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react' -import { useFilter } from 'react-aria' -import { useOverlayTriggerState } from 'react-stately' -import styled from 'styled-components' -import { delayAction } from '@/dex/utils' -import useStore from '@/dex/store/useStore' -import ComboBox from '@/dex/components/ComboBoxSelectToken/ComboBox' -import ComboBoxSelectedToken from '@/dex/components/ComboBoxSelectToken/ComboBoxSelectedToken' -import ComboBoxSelectedTokenButton from '@/dex/components/ComboBoxSelectToken/ComboBoxSelectedTokenButton' -import ModalDialog from '@ui/Dialog' -import Spinner, { SpinnerWrapper } from '@ui/Spinner' -import { Token } from '@/dex/types/main.types' -import { filterTokens } from '@ui-kit/utils' - -const ComboBoxTokens = ({ - disabled, - blockchainId, - listBoxHeight, - selectedToken, - showBalances, - showCheckboxHideSmallPools, - showSearch, - testId, - title, - tokens = [], - onOpen, - onSelectionChange, -}: { - disabled?: boolean - blockchainId: string - listBoxHeight?: string - selectedToken: Token | undefined - showBalances?: boolean - showCheckboxHideSmallPools?: boolean - showSearch?: boolean - testId?: string - title: string - tokens: Token[] | undefined - onOpen?: () => void - onSelectionChange: (selectedAddress: React.Key) => void -}) => { - const { endsWith } = useFilter({ sensitivity: 'base' }) - const overlayTriggerState = useOverlayTriggerState({}) - - const filterValue = useStore((state) => state.selectToken.filterValue) - const isMobile = useStore((state) => state.isMobile) - const setStateByKey = useStore((state) => state.selectToken.setStateByKey) - - const [result, setResult] = useState() - - const handleInpChange = useCallback( - (filterValue: string, tokens: Token[] | undefined) => { - setStateByKey('filterValue', filterValue) - const result = filterValue && tokens && tokens.length > 0 ? filterTokens(filterValue, tokens, endsWith) : tokens - setResult(result) - }, - [endsWith, setStateByKey], - ) - - const handleOnSelectChange = (tokenAddress: React.Key) => { - onSelectionChange(tokenAddress) - handleClose() - } - - const handleOpen = () => { - if (typeof onOpen === 'function') onOpen() - - setResult(tokens) - setStateByKey('filterValue', '') - overlayTriggerState.open() - } - - const handleClose = () => { - if (isMobile) { - delayAction(overlayTriggerState.close) - } else { - overlayTriggerState.close() - } - } - - // update result if tokens list changed. - useEffect(() => { - if (Array.isArray(tokens) && tokens.length > 0) { - if (filterValue) { - handleInpChange(filterValue, tokens) - } else { - setResult(tokens) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tokens]) - - return selectedToken ? ( - <> - - - - {overlayTriggerState.isOpen && ( - - - - )} - - ) : ( - - - - ) -} - -const StyledSpinnerWrapper = styled(SpinnerWrapper)` - height: 100%; - border: 0.5px solid var(--input_button--border-color); - box-shadow: inset -2px -2px 0 0.25px var(--box--primary--shadow-color); -` - -export default ComboBoxTokens diff --git a/apps/main/src/dex/components/ComboBoxSelectToken/types.ts b/apps/main/src/dex/components/ComboBoxSelectToken/types.ts deleted file mode 100644 index da05ab064..000000000 --- a/apps/main/src/dex/components/ComboBoxSelectToken/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Token } from '@/dex/types/main.types' - -export type ComboBoxSelectTokenProps = { - disabled?: boolean - blockchainId: string - listBoxHeight?: string - selectedToken: Token | undefined - showBalances?: boolean - showCheckboxHideSmallPools?: boolean - showInpSearch?: boolean - testId?: string - title: string - tokens: Token[] | undefined - onSelectionChange(selectedAddress: string): void -} diff --git a/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBox.tsx b/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBox.tsx deleted file mode 100644 index 9ce8b939c..000000000 --- a/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBox.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import type { ComboBoxStateOptions } from 'react-stately' - -import { useButton, useComboBox, useFocusRing } from 'react-aria' -import { useComboBoxState } from '@react-stately/combobox' -import React, { ChangeEvent, useCallback, useMemo, useRef } from 'react' -import styled from 'styled-components' -import { t } from '@ui-kit/lib/i18n' - -import debounce from 'lodash/debounce' - -import Icon from '@ui/Icon' -import InputWrapper from '@ui/InputComp' -import { ListBox } from '@ui/DialogComboBox' -import { StyledInput } from '@ui/InputComp/styles' -import { ReactComponent as EditClearSymbolic } from '@ui/images/edit-clear-symbolic.svg' -import IconButton from '@ui/IconButton' -import Box from '@ui/Box' -import Popover from '@ui/Popover' - -type Props = { - activeKey: string - onClose?: () => void - quickList?: React.ReactNode - listBoxHeight?: string - isListboxOpenPermanently: boolean - showSearch: boolean -} - -function ComboBox({ listBoxHeight, onClose, showSearch, ...props }: ComboBoxStateOptions & Props) { - const { focusProps } = useFocusRing() - const state = useComboBoxState(props) - const closeButtonRef = useRef(null) - const topContentRef = useRef(null) - const inputRef = useRef(null) - const listBoxRef = useRef(null) - const popoverRef = useRef(null) - - const topContentHeight = topContentRef?.current?.getBoundingClientRect()?.height - - const { listBoxProps } = useComboBox( - { - ...props, - inputRef, - listBoxRef, - popoverRef, - }, - state, - ) - - const handleBtnClickClear = () => { - if (inputRef.current) { - inputRef.current.value = '' - if (typeof props.onInputChange === 'function') props.onInputChange('') - } - } - - const handleInpChange = useCallback( - (evt: ChangeEvent) => { - if (typeof props.onInputChange === 'function') { - props.onInputChange(evt.target.value) - } - }, - [props], - ) - - const debounceInpChange = useMemo(() => debounce(handleInpChange, 700), [handleInpChange]) - - const { buttonProps: closeButtonProps } = useButton( - { - onPress: () => { - if (typeof onClose === 'function') onClose() - }, - }, - closeButtonRef, - ) - - return ( - - {showSearch && ( - -
    - - - - - - - - - - - -

    {t`Type the full token address to add a new token`}

    -
    - {props.quickList} -
    - )} - - - -
    - ) -} - -const ListBoxWrapper = styled.div<{ boxHeight: string; topContentHeight: number | undefined }>` - padding-top: var(--spacing-3); - - ${({ topContentHeight }) => { - if (topContentHeight) { - return `height: calc(100vh - ${topContentHeight}px - 1px);` - } - }}; - - @media (min-width: 28.125rem) { - height: ${({ boxHeight }) => boxHeight}; - } -` - -const StyledBox = styled(Box)` - padding-top: var(--spacing-narrow); - p { - font-style: italic; - font-size: var(--font-size-1); - margin: var(--spacing-2) auto var(--spacing-2); - } -` - -const Header = styled(Box)` - margin: 1rem 0.5rem 0 1rem; -` - -const StyledIconButton = styled(IconButton)` - display: none; - min-width: 1.5625rem; //25px - opacity: 1; - padding: 0; - - &.show { - display: inline-block; - } - - .svg-tooltip { - position: relative; - top: 2px; - } -` - -const StyledInputWrapper = styled(InputWrapper)` - align-items: center; - display: grid; - grid-template-columns: auto 1fr auto; - grid-column-gap: var(--spacing-2); - position: relative; - transition: 3s; -` - -export default ComboBox diff --git a/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBoxTokenPicker.tsx b/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBoxTokenPicker.tsx deleted file mode 100644 index f84104798..000000000 --- a/apps/main/src/dex/components/PageCreatePool/SelectTokenModal/ComboBoxTokenPicker.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { CreateQuickListToken, CreateToken } from '@/dex/components/PageCreatePool/types' -import { t } from '@ui-kit/lib/i18n' -import { useButton } from '@react-aria/button' -import { useFilter } from '@react-aria/i18n' -import { Key, useMemo, useRef, useState } from 'react' -import { useOverlayTriggerState } from '@react-stately/overlays' -import { Item } from '@react-stately/collections' -import styled from 'styled-components' -import { breakpoints } from '@ui/utils/responsive' -import { delayAction, shortenTokenAddress } from '@/dex/utils' -import useStore from '@/dex/store/useStore' -import { STABLESWAP } from '@/dex/components/PageCreatePool/constants' -import ComboBox from '@/dex/components/PageCreatePool/SelectTokenModal/ComboBox' -import Box from '@ui/Box' -import Button from '@ui/Button' -import ModalDialog from '@/dex/components/PageCreatePool/ConfirmModal/ModalDialog' -import Spinner, { SpinnerWrapper } from '@ui/Spinner' -import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon' -import { Chip } from '@ui/Typography' -import LazyItem from '@ui/LazyItem' -import Checkbox from '@ui/Checkbox' -import { ChainId, CurveApi } from '@/dex/types/main.types' -import { filterTokens } from '@ui-kit/utils' - -type Props = { - curve: CurveApi - chainId: ChainId - disabledKeys?: string[] - haveSigner: boolean - blockchainId: string - selectedAddress: string - tokens: CreateToken[] - onSelectionChange: (selectedAddress: React.Key) => void -} - -type TokenQueryType = '' | 'LOADING' | 'ERROR' | 'DISABLED' - -const ComboBoxTokenPicker = ({ - curve, - disabledKeys, - chainId, - blockchainId, - selectedAddress, - tokens = [], - onSelectionChange, -}: Props) => { - const networks = useStore((state) => state.networks.networks) - const visibleTokens = useRef<{ [k: string]: boolean }>({}) - const overlayTriggerState = useOverlayTriggerState({}) - const openButtonRef = useRef(null) - const { buttonProps: openButtonProps } = useButton({ onPress: () => overlayTriggerState.open() }, openButtonRef) - const { endsWith } = useFilter({ sensitivity: 'base' }) - - const isMobile = useStore((state) => state.isMobile) - const isMdUp = useStore((state) => state.isMdUp) - const nativeToken = useStore((state) => state.networks.nativeToken[chainId]) - const updateUserAddedTokens = useStore((state) => state.createPool.updateUserAddedTokens) - const { loading } = useStore((state) => state.tokens) - const { basePools, basePoolsLoading } = useStore((state) => state.pools) - const { swapType } = useStore((state) => state.createPool) - - const [filterValue, setFilterValue] = useState('') - const [filterBasepools, setFilterBasepools] = useState(false) - const [tokenQueryStatus, setTokenQueryStatus] = useState('') - - const quickList = [ - { - address: nativeToken?.wrappedAddress ?? '', - haveSameTokenName: false, - symbol: nativeToken?.wrappedSymbol ?? '', - }, - ...networks[chainId].createQuickList, - ] - - if (!overlayTriggerState.isOpen) { - visibleTokens.current = {} - } - - const verifyTokens = async () => { - if (disabledKeys?.some((item) => item.toLowerCase() === filterValue.toLowerCase())) { - setTokenQueryStatus('DISABLED') - return - } - - setTokenQueryStatus('LOADING') - - try { - const token = await curve.getCoinsData([filterValue]) - const isBasePool = basePools[chainId].some( - (basepool) => basepool.token.toLowerCase() === filterValue.toLowerCase(), - ) - updateUserAddedTokens(filterValue, token[0].symbol, false, isBasePool) - } catch (error) { - console.log(error) - setTokenQueryStatus('ERROR') - } - } - - // handles search/filtering - const items = useMemo(() => { - const basePoolsFilteredTokens = filterBasepools - ? tokens.filter((item) => - basePools[chainId].some((basepool) => basepool.token.toLowerCase() === item.address.toLowerCase()), - ) - : tokens - const enabledTokens = disabledKeys - ? basePoolsFilteredTokens.filter( - (item) => !disabledKeys.some((i) => i.toLowerCase() === item.address.toLowerCase()), - ) - : basePoolsFilteredTokens - - const filteredResults = filterTokens(filterValue, enabledTokens, endsWith) - setTokenQueryStatus('') - if (filterValue.length === 42 && filteredResults.length === 0) { - verifyTokens() - } - - return filteredResults - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterValue, tokens, disabledKeys, filterBasepools]) - - const selectedToken = useMemo( - () => (selectedAddress ? tokens.find((userToken) => userToken.address === selectedAddress) : null), - [selectedAddress, tokens], - ) - - const handleClose = () => { - setFilterValue('') - isMobile ? delayAction(overlayTriggerState.close) : overlayTriggerState.close() - } - - const handleOnSelectChange = (tokenAddress: Key | null) => { - if (tokenAddress) { - onSelectionChange(tokenAddress) - setFilterBasepools(false) - } - handleClose() - } - - return chainId || basePoolsLoading ? ( - <> - - {selectedToken ? ( - <> - - - - {selectedToken.symbol}{' '} - {selectedToken.basePool && swapType === STABLESWAP && {t`BASE`}} - - - {shortenTokenAddress(selectedToken.address)} - - ) : ( - - {t`Select a token`} - - )} - - {overlayTriggerState.isOpen && ( - - - {quickList.map(({ address, symbol }: CreateQuickListToken) => ( - item.toLowerCase() === address.toLowerCase())} - variant="icon-outlined" - onClick={() => handleOnSelectChange(address)} - > - {' '} - {symbol} - - ))} - setFilterBasepools(!filterBasepools)} - > - View Basepools - - - } - onSelectionChange={handleOnSelectChange} - > - {tokenQueryStatus === '' ? ( - items.length > 0 ? ( - (item: CreateToken) => ( - - - - - - {item.symbol}{' '} - {item.basePool && swapType === STABLESWAP && {t`BASE`}} - - {item.userAddedToken && User added} - - {shortenTokenAddress(item.address)} - - - ) - ) : loading || basePoolsLoading ? ( - - - - - - ) : ( - - - - {t`Search generated no results`} - - - - ) // loading - ) : tokenQueryStatus === 'LOADING' ? ( - - - - - - ) : tokenQueryStatus === 'ERROR' ? ( - // no search resuslts - - - - - {t`No token found for address ${shortenTokenAddress(filterValue)}`} - - - - ) : ( - // disabled token - - - - - {networks[chainId].createDisabledTokens.some( - (token) => token.toLowerCase() === filterValue.toLowerCase(), - ) ? ( - {t`${filterValue} is a disabled token in Pool Creation`} - ) : ( - {t`${filterValue} is a disabled token in this pool configuration.`} - )} - - - - )} - - - )} - - ) : ( - - - - ) -} - -const ItemWrapper = styled(LazyItem)` - align-items: center; - display: grid; - grid-column-gap: var(--spacing-2); - grid-template-columns: auto 1fr auto; - width: 100%; -` - -const StyledQuickListTokenIcon = styled(TokenIcon)` - margin-right: 0.25rem; -` - -const ButtonTokenIcon = styled(TokenIcon)` - margin-right: 0.25rem; -` - -const QuickListButton = styled(Button)` - margin: 0.25rem; - padding: 0.5rem 1rem 0.5rem 1rem; - text-transform: none; -` - -const StyledCheckbox = styled(Checkbox)` - margin: auto 1.25rem auto auto; -` - -const QuickListWrapper = styled.div` - display: none; - @media (min-width: 28.125rem) { - display: flex; - margin: 0 0.75rem; - flex-wrap: wrap; - } -` - -const StyledSpinnerWrapper = styled(SpinnerWrapper)` - display: flex; - align-items: center; - padding: var(--spacing-2) var(--spacing-narrow); - height: 100%; - - text-transform: var(--input_button--text-transform); - - background: var(--layout--home--background-color); - color: var(--page--text-color); - border: 1px solid var(--nav_button--border-color); - box-shadow: 3px 3px 0 var(--box--primary--shadow-color); -` - -const StyledSearchSpinnerWrapper = styled(SpinnerWrapper)` - display: flex; - align-items: center; - padding: var(--spacing-2) var(--spacing-narrow); - height: 100%; - - text-transform: var(--input_button--text-transform); - - color: var(--page--text-color); -` - -const SelectedLabelText = styled.span` - overflow: hidden; - display: flex; - justify-content: center; - - font-size: var(--font-size-3); - font-weight: var(--semi-bold); - line-height: 1; - text-overflow: ellipsis; - white-space: nowrap; -` - -const SelectedLabelAddress = styled.span` - font-size: var(--font-size-2); - font-weight: var(--semi-bold); - line-height: 1; - margin-left: auto; -` - -const PlaceholderSelectedLabelText = styled(SelectedLabelText)` - font-weight: var(--semi-bold); - font-size: var(--font-size-2); -` - -const LabelText = styled.div` - display: flex; - align-items: center; - overflow: hidden; - - font-size: var(--font-size-4); - font-weight: var(--font-weight--bold); - line-height: 1; - text-overflow: ellipsis; -` - -const UserAddedText = styled.div` - font-size: var(--font-size-1); -` - -const ErrorText = styled.div` - font-size: var(--font-size-2); - text-align: center; - margin: auto; -` - -const LabelTextWrapper = styled(Box)` - display: flex; - flex-direction: column; - overflow: hidden; - text-overflow: ellipsis; - padding-top: var(--spacing-1); - padding-bottom: var(--spacing-1); -` - -const ComboBoxButton = styled(Button)` - display: flex; - align-items: center; - padding: var(--spacing-2) var(--spacing-narrow); - height: 100%; - min-height: 2.75rem; - - text-transform: var(--input_button--text-transform); - - background: var(--dialog--background-color); - color: var(--page--text-color); - border: 1px solid var(--nav_button--border-color); - box-shadow: 3px 3px 0 var(--box--primary--shadow-color); - - grid-template-columns: auto 1fr auto; - - transition: - background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, - color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - - &:hover { - color: var(--button--color); - border: 1px solid var(--button--background-color); - } -` - -const StyledModalDialog = styled(ModalDialog)` - position: fixed; - width: 100%; - - @media (min-width: ${breakpoints.lg}rem) { - position: relative; - } -` - -const BasepoolLabel = styled(Chip)` - margin: auto 0 auto var(--spacing-1); - font-weight: var(--bold); - font-size: var(--font-size-1); - padding: 2px; - background-color: var(--warning-400); - color: var(--black); - letter-spacing: 0; - height: 15px; -` - -export default ComboBoxTokenPicker diff --git a/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectToken.tsx b/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectToken.tsx index ab46da811..e405cbc9e 100644 --- a/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectToken.tsx +++ b/apps/main/src/dex/components/PageCreatePool/TokensInPool/SelectToken.tsx @@ -20,7 +20,7 @@ import { TOKEN_G, TOKEN_H, } from '@/dex/components/PageCreatePool/constants' -import ComboBoxTokenPicker from '@/dex/components/PageCreatePool/SelectTokenModal/ComboBoxTokenPicker' +import SelectTokenButton from './SelectTokenButton' import Box from '@ui/Box' import Checkbox from '@ui/Checkbox' import Icon from '@ui/Icon' @@ -101,7 +101,7 @@ const SelectToken = ({ )} - void +} + +const ComboBoxTokenPicker = ({ + curve, + disabledKeys, + chainId, + blockchainId, + selectedAddress, + tokens = [], + onSelectionChange, +}: Props) => { + const networks = useStore((state) => state.networks.networks) + const visibleTokens = useRef<{ [k: string]: boolean }>({}) + const overlayTriggerState = useOverlayTriggerState({}) + const openButtonRef = useRef(null) + const { buttonProps: openButtonProps } = useButton({ onPress: () => overlayTriggerState.open() }, openButtonRef) + const { endsWith } = useFilter({ sensitivity: 'base' }) + + const isMobile = useStore((state) => state.isMobile) + const nativeToken = useStore((state) => state.networks.nativeToken[chainId]) + + const userAddedTokens = useStore((state) => state.createPool.userAddedTokens) + const updateUserAddedTokens = useStore((state) => state.createPool.updateUserAddedTokens) + + const { basePools, basePoolsLoading } = useStore((state) => state.pools) + const { swapType } = useStore((state) => state.createPool) + + const [error, setError] = useState('') + const [filterValue, setFilterValue] = useState('') + const [filterBasepools, setFilterBasepools] = useState(false) + + const favorites = [ + { + address: nativeToken?.wrappedAddress ?? '', + symbol: nativeToken?.wrappedSymbol ?? '', + }, + ...networks[chainId].createQuickList, + ].map(({ address, symbol }) => ({ + chain: blockchainId, + address: address as `0x${string}`, + symbol, + label: '', + volume: 0, + })) + + if (!overlayTriggerState.isOpen) { + visibleTokens.current = {} + } + + // handles search/filtering + const options = useMemo(() => { + const allTokens = filterBasepools + ? tokens.filter((item) => + basePools[chainId].some((basepool) => basepool.token.toLowerCase() === item.address.toLowerCase()), + ) + : tokens + + const filteredResults = filterTokens(filterValue, allTokens, endsWith) + + return filteredResults.map((token) => ({ + chain: blockchainId, + address: token.address as `0x${string}`, + symbol: token.symbol, + label: [token.basePool ? 'Base pool' : '', token.userAddedToken ? 'User added' : ''] + .filter((x) => x !== '') + .join(' - '), + volume: token.volume ?? 0, + })) + }, [filterBasepools, tokens, filterValue, endsWith, basePools, chainId, blockchainId]) + + useEffect(() => { + async function updateUserAddedToken() { + const filterValueLowerCase = filterValue.toLocaleLowerCase() + + // If user input is an address and there's 0 results, add user token + if ( + filterValueLowerCase.length === 42 && + options.length === 0 && + !(userAddedTokens ?? []).some((x) => x.address.toLocaleLowerCase() === filterValueLowerCase) + ) { + try { + const token = await curve.getCoinsData([filterValueLowerCase]) + const isBasePool = basePools[chainId].some( + (basepool) => basepool.token.toLowerCase() === filterValueLowerCase, + ) + + updateUserAddedTokens(filterValueLowerCase, token[0].symbol, false, isBasePool) + } catch (error) { + console.log(error) + setError(error) + } + } + } + updateUserAddedToken() + }, [basePools, chainId, curve, filterValue, options, updateUserAddedTokens, userAddedTokens]) + + const selectedToken = useMemo( + () => (selectedAddress ? tokens.find((userToken) => userToken.address === selectedAddress) : null), + [selectedAddress, tokens], + ) + + const handleClose = () => { + setFilterValue('') + isMobile ? delayAction(overlayTriggerState.close) : overlayTriggerState.close() + } + + return chainId || basePoolsLoading ? ( + <> + + {selectedToken ? ( + <> + + + + {selectedToken.symbol}{' '} + {selectedToken.basePool && swapType === STABLESWAP && {t`BASE`}} + + + {shortenTokenAddress(selectedToken.address)} + + ) : ( + + {t`Select a token`} + + )} + + {overlayTriggerState.isOpen && ( + setFilterBasepools(!filterBasepools)} + > + View Basepools + + } + onClose={handleClose} + onToken={({ address }) => { + onSelectionChange(address) + setFilterBasepools(false) + handleClose() + }} + onSearch={setFilterValue} + /> + )} + + ) : ( + + + + ) +} + +const ButtonTokenIcon = styled(TokenIcon)` + margin-right: 0.25rem; +` + +const StyledSpinnerWrapper = styled(SpinnerWrapper)` + display: flex; + align-items: center; + padding: var(--spacing-2) var(--spacing-narrow); + height: 100%; + + text-transform: var(--input_button--text-transform); + + background: var(--layout--home--background-color); + color: var(--page--text-color); + border: 1px solid var(--nav_button--border-color); + box-shadow: 3px 3px 0 var(--box--primary--shadow-color); +` + +const SelectedLabelText = styled.span` + overflow: hidden; + display: flex; + justify-content: center; + + font-size: var(--font-size-3); + font-weight: var(--semi-bold); + line-height: 1; + text-overflow: ellipsis; + white-space: nowrap; +` + +const SelectedLabelAddress = styled.span` + font-size: var(--font-size-2); + font-weight: var(--semi-bold); + line-height: 1; + margin-left: auto; +` + +const PlaceholderSelectedLabelText = styled(SelectedLabelText)` + font-weight: var(--semi-bold); + font-size: var(--font-size-2); +` + +const LabelTextWrapper = styled(Box)` + display: flex; + flex-direction: column; + overflow: hidden; + text-overflow: ellipsis; + padding-top: var(--spacing-1); + padding-bottom: var(--spacing-1); +` + +const ComboBoxButton = styled(Button)` + display: flex; + align-items: center; + padding: var(--spacing-2) var(--spacing-narrow); + height: 100%; + min-height: 2.75rem; + + text-transform: var(--input_button--text-transform); + + background: var(--dialog--background-color); + color: var(--page--text-color); + border: 1px solid var(--nav_button--border-color); + box-shadow: 3px 3px 0 var(--box--primary--shadow-color); + + grid-template-columns: auto 1fr auto; + + transition: + background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, + color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + + &:hover { + color: var(--button--color); + border: 1px solid var(--button--background-color); + } +` + +const BasepoolLabel = styled(Chip)` + margin: auto 0 auto var(--spacing-1); + font-weight: var(--bold); + font-size: var(--font-size-1); + padding: 2px; + background-color: var(--warning-400); + color: var(--black); + letter-spacing: 0; + height: 15px; +` + +export default ComboBoxTokenPicker diff --git a/apps/main/src/dex/components/PageCreatePool/types.ts b/apps/main/src/dex/components/PageCreatePool/types.ts index f14f02f3f..4db67a538 100644 --- a/apps/main/src/dex/components/PageCreatePool/types.ts +++ b/apps/main/src/dex/components/PageCreatePool/types.ts @@ -35,7 +35,7 @@ export type CreateQuickListToken = { } export type BasePoolToken = { - namne: string + name: string token: string pool: string } diff --git a/apps/main/src/dex/components/PagePool/Swap/index.tsx b/apps/main/src/dex/components/PagePool/Swap/index.tsx index 2cc67ffc1..dac6944c9 100644 --- a/apps/main/src/dex/components/PagePool/Swap/index.tsx +++ b/apps/main/src/dex/components/PagePool/Swap/index.tsx @@ -30,17 +30,16 @@ import Icon from '@ui/Icon' import IconButton from '@ui/IconButton' import InputProvider, { InputDebounced, InputMaxBtn } from '@ui/InputComp' import Stepper from '@ui/Stepper' -import TokenComboBox from '@/dex/components/ComboBoxSelectToken' import TransferActions from '@/dex/components/PagePool/components/TransferActions' import TxInfoBar from '@ui/TxInfoBar' import WarningModal from '@/dex/components/PagePool/components/WarningModal' import { Balances, CurveApi, PoolAlert, PoolData, TokensMapper } from '@/dex/types/main.types' import { notify } from '@ui-kit/features/connect-wallet' +import { TokenSelector } from '@ui-kit/features/select-token' const Swap = ({ chainIdPoolId, curve, - blockchainId, maxSlippage, poolAlert, poolData, @@ -52,7 +51,6 @@ const Swap = ({ userPoolBalancesLoading, }: Pick & { chainIdPoolId: string - blockchainId: string poolAlert: PoolAlert | null maxSlippage: string seed: Seed @@ -88,15 +86,32 @@ const Swap = ({ const poolId = poolData?.pool?.id const haveSigner = !!signerAddress + const userFromBalance = userPoolBalances?.[formValues.fromAddress] const userToBalance = userPoolBalances?.[formValues.toAddress] + const fromUsdRate = usdRatesMapper[formValues.fromAddress] const toUsdRate = usdRatesMapper[formValues.toAddress] - const { selectList, swapTokensMapper } = useMemo( - () => getSwapTokens(tokensMapper, poolDataCacheOrApi), - [poolDataCacheOrApi, tokensMapper], - ) + const { selectList, swapTokensMapper } = useMemo(() => { + const { selectList, swapTokensMapper } = getSwapTokens(tokensMapper, poolDataCacheOrApi) + + return { + selectList: selectList + .filter((token) => !!token) + .map((token) => ({ + chain: network?.networkId ?? '', + address: token?.address as `0x${string}`, + symbol: token?.symbol, + label: '', + volume: token?.volume ?? 0, + })), + swapTokensMapper, + } + }, [poolDataCacheOrApi, tokensMapper, network?.networkId]) + + const fromToken = selectList.find((x) => x.address.toLocaleLowerCase() == formValues.fromAddress) + const toToken = selectList.find((x) => x.address.toLocaleLowerCase() == formValues.toAddress) const updateFormValues = useCallback( (updatedFormValues: Partial, isGetMaxFrom: boolean | null, updatedMaxSlippage: string | null) => { @@ -365,16 +380,16 @@ const Swap = ({ updateFormValues({ isFrom: true, fromAmount: '', toAmount: '' }, true, null) }} /> - { - const val = value as string + showSettings={false} + compact + onToken={(token) => { + const val = token.address const cFormValues = cloneDeep(formValues) if (val === formValues.toAddress) { cFormValues.toAddress = formValues.fromAddress @@ -448,16 +463,16 @@ const Swap = ({ updateFormValues({ isFrom: false, toAmount, fromAmount: '' }, null, '') }} /> - { - const val = value as string + showSettings={false} + compact + onToken={(token) => { + const val = token.address const cFormValues = cloneDeep(formValues) if (val === formValues.fromAddress) { cFormValues.fromAddress = formValues.toAddress diff --git a/apps/main/src/dex/components/PagePool/index.tsx b/apps/main/src/dex/components/PagePool/index.tsx index 710b6f551..c6194ff1a 100644 --- a/apps/main/src/dex/components/PagePool/index.tsx +++ b/apps/main/src/dex/components/PagePool/index.tsx @@ -252,7 +252,6 @@ const Transfer = (pageTransferProps: PageTransferProps) => { { const updatePath = useCallback( (updatedSearchParams: Partial) => { - const { filterKey, hideSmallPools, searchText, sortBy, sortByOrder } = { + const { filterKey, searchText, sortBy, sortByOrder } = { ...parsedSearchParams, ...updatedSearchParams, } const searchPath = new URLSearchParams( [ [SEARCH.filter, filterKey && filterKey !== 'all' ? filterKey : ''], - [SEARCH.hideSmallPools, hideSmallPools ? '' : 'false'], [SEARCH.sortBy, sortBy && sortBy !== defaultSortBy ? sortBy : ''], [SEARCH.order, sortByOrder && sortByOrder !== 'desc' ? sortByOrder : ''], [SEARCH.search, searchText ? encodeURIComponent(searchText) : ''], @@ -93,7 +91,6 @@ const Page: NextPage = () => { const paramFilterKey = (searchParams.get(SEARCH.filter) || 'all').toLowerCase() const paramSortBy = (searchParams.get(SEARCH.sortBy) || defaultSortBy).toLowerCase() const paramOrder = (searchParams.get(SEARCH.order) || 'desc').toLowerCase() - const paramHideSmallPools = searchParams.get(SEARCH.hideSmallPools) || 'true' const searchText = decodeURIComponent(searchParams.get(SEARCH.search) || '') // validate filter key @@ -101,7 +98,6 @@ const Page: NextPage = () => { if ((paramFilterKey === 'user' && !!curve && !curve?.signerAddress) || !foundFilterKey) { updatePath({ filterKey: 'all', - hideSmallPools: paramHideSmallPools === 'true', sortBy: paramSortBy as SortKey, sortByOrder: paramOrder as Order, searchText: searchText, @@ -109,7 +105,6 @@ const Page: NextPage = () => { } else { setParsedSearchParams({ filterKey: paramFilterKey as FilterKey, - hideSmallPools: paramHideSmallPools === 'true', searchText, sortBy: (Object.keys(TABLE_LABEL).find((k) => k.toLowerCase() === paramSortBy) ?? defaultSortBy) as SortKey, sortByOrder: (['desc', 'asc'].find((k) => k.toLowerCase() === paramOrder) ?? 'desc') as Order, diff --git a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableCheckboxHideSmallPools.tsx b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableCheckboxHideSmallPools.tsx index cb4ed0784..c4e6b4e46 100644 --- a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableCheckboxHideSmallPools.tsx +++ b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableCheckboxHideSmallPools.tsx @@ -1,26 +1,21 @@ -import type { SearchParams } from '@/dex/components/PagePoolList/types' - import React from 'react' import { t } from '@ui-kit/lib/i18n' import Checkbox from '@ui/Checkbox' import { PoolData } from '@/dex/types/main.types' +import { useUserProfileStore } from '@ui-kit/features/user-profile' + +const TableCheckboxHideSmallPools = ({ poolDatasCachedOrApi }: { poolDatasCachedOrApi: PoolData[] }) => { + const isDisabled = poolDatasCachedOrApi.length < 10 + + const hideSmallPools = useUserProfileStore((state) => state.hideSmallPools) + const setHideSmallPools = useUserProfileStore((state) => state.setHideSmallPools) -const TableCheckboxHideSmallPools = ({ - searchParams, - poolDatasCachedOrApi, - updatePath, -}: { - searchParams: SearchParams - poolDatasCachedOrApi: PoolData[] - updatePath(updatedSearchParams: Partial): void -}) => { - const isDisabled = searchParams.filterKey === 'user' || poolDatasCachedOrApi.length < 10 return ( updatePath({ hideSmallPools: val })} + isSelected={isDisabled ? false : hideSmallPools} + onChange={(val) => setHideSmallPools(val)} > {t`Hide very small pools`} diff --git a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx index 33f7ef115..5eb305a8b 100644 --- a/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx +++ b/apps/main/src/dex/components/PagePoolList/components/TableSettings/TableSettings.tsx @@ -101,11 +101,7 @@ const TableSettings = ({ /> - + ) : ( @@ -116,11 +112,7 @@ const TableSettings = ({ updateRouteFilterKey={(filterKey) => updatePath({ filterKey: filterKey as FilterKey })} /> - + )} diff --git a/apps/main/src/dex/components/PagePoolList/index.tsx b/apps/main/src/dex/components/PagePoolList/index.tsx index 67618ece7..ec04642c4 100644 --- a/apps/main/src/dex/components/PagePoolList/index.tsx +++ b/apps/main/src/dex/components/PagePoolList/index.tsx @@ -15,6 +15,7 @@ import TableHeadMobile from '@/dex/components/PagePoolList/components/TableHeadM import TableSettings from '@/dex/components/PagePoolList/components/TableSettings/TableSettings' import TableRowNoResult from '@/dex/components/PagePoolList/components/TableRowNoResult' import { PoolRow } from '@/dex/components/PagePoolList/components/PoolRow' +import { useUserProfileStore } from '@ui-kit/features/user-profile' const PoolList = ({ rChainId, @@ -46,6 +47,7 @@ const PoolList = ({ const fetchPoolsRewardsApy = useStore((state) => state.pools.fetchPoolsRewardsApy) const setFormValues = useStore((state) => state.poolList.setFormValues) const { initCampaignRewards, initiated } = useStore((state) => state.campaigns) + const hideSmallPools = useUserProfileStore((state) => state.hideSmallPools) const [showDetail, setShowDetail] = useState('') @@ -103,6 +105,7 @@ const PoolList = ({ rChainId, isLite, searchParams, + hideSmallPools, typeof poolDataMapper !== 'undefined' ? poolDatas : undefined, poolDatasCached, rewardsApyMapper ?? {}, @@ -115,19 +118,20 @@ const PoolList = ({ ) }, [ + setFormValues, + rChainId, isLite, - campaignRewardsMapper, + hideSmallPools, poolDataMapper, poolDatas, poolDatasCached, - rChainId, rewardsApyMapper, - setFormValues, + volumeMapper, + volumeMapperCached, tvlMapper, tvlMapperCached, userPoolList, - volumeMapper, - volumeMapperCached, + campaignRewardsMapper, ], ) @@ -147,7 +151,7 @@ const PoolList = ({ updateFormValues(searchParams) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReady, isReadyWithApiData, chainId, signerAddress, searchParams]) + }, [isReady, isReadyWithApiData, chainId, signerAddress, searchParams, hideSmallPools]) // init campaignRewardsMapper useEffect(() => { diff --git a/apps/main/src/dex/components/PagePoolList/types.ts b/apps/main/src/dex/components/PagePoolList/types.ts index 76286f161..d93a69429 100644 --- a/apps/main/src/dex/components/PagePoolList/types.ts +++ b/apps/main/src/dex/components/PagePoolList/types.ts @@ -56,7 +56,6 @@ export type PoolListTableLabel = { export type SearchParams = { filterKey: FilterKey - hideSmallPools: boolean searchText: string sortBy: SortKey sortByOrder: Order diff --git a/apps/main/src/dex/components/PageRouterSwap/index.tsx b/apps/main/src/dex/components/PageRouterSwap/index.tsx index 0415c8185..855218608 100644 --- a/apps/main/src/dex/components/PageRouterSwap/index.tsx +++ b/apps/main/src/dex/components/PageRouterSwap/index.tsx @@ -13,7 +13,6 @@ import { NETWORK_TOKEN, REFRESH_INTERVAL } from '@/dex/constants' import { formatNumber } from '@ui/utils' import { getActiveStep, getStepStatus } from '@ui/Stepper/helpers' import { getTokensMapperStr } from '@/dex/store/createTokensSlice' -import { getTokensObjList } from '@/dex/store/createQuickSwapSlice' import { getChainSignerActiveKey } from '@/dex/utils' import usePageVisibleInterval from '@/dex/hooks/usePageVisibleInterval' import useSelectToList from '@/dex/components/PageRouterSwap/components/useSelectToList' @@ -34,12 +33,12 @@ import InputProvider, { InputDebounced, InputMaxBtn } from '@ui/InputComp' import FormConnectWallet from '@/dex/components/FormConnectWallet' import RouterSwapAlerts from '@/dex/components/PageRouterSwap/components/RouterSwapAlerts' import Stepper from '@ui/Stepper' -import TokenComboBox from '@/dex/components/ComboBoxSelectToken' import TxInfoBar from '@ui/TxInfoBar' import WarningModal from '@/dex/components/PagePool/components/WarningModal' import { useUserProfileStore } from '@ui-kit/features/user-profile' -import { ChainId, CurveApi, Token, TokensMapper } from '@/dex/types/main.types' +import { ChainId, CurveApi, TokensMapper } from '@/dex/types/main.types' import { notify } from '@ui-kit/features/connect-wallet' +import { TokenSelector } from '@ui-kit/features/select-token' const QuickSwap = ({ pageLoaded, @@ -63,7 +62,8 @@ const QuickSwap = ({ const curve = useStore((state) => state.curve) const { chainId, signerAddress } = curve ?? {} const { tokensNameMapper } = useTokensNameMapper(rChainId) - const { selectToList, selectToListStr } = useSelectToList(rChainId) + const { selectToList } = useSelectToList(rChainId) + const selectFromList = useStore((state) => state.quickSwap.selectFromList[chainSignerActiveKey]) const chainSignerActiveKey = getChainSignerActiveKey(rChainId, signerAddress) const activeKey = useStore((state) => state.quickSwap.activeKey) const formEstGas = useStore((state) => state.quickSwap.formEstGas[activeKey]) @@ -72,9 +72,8 @@ const QuickSwap = ({ const isLoadingApi = useStore((state) => state.isLoadingApi) const isPageVisible = useStore((state) => state.isPageVisible) const routesAndOutput = useStore((state) => state.quickSwap.routesAndOutput[activeKey]) - const isHideSmallPools = useStore((state) => state.poolList.formValues.hideSmallPools) + const hideSmallPools = useUserProfileStore((state) => state.hideSmallPools) const isMaxLoading = useStore((state) => state.quickSwap.isMaxLoading) - const selectFromList = useStore((state) => state.quickSwap.selectFromList[chainSignerActiveKey]) const tokensMapperNonSmallTvl = useStore((state) => state.tokens.tokensMapperNonSmallTvl[rChainId] ?? {}) const userBalancesMapper = useStore((state) => state.userBalances.userBalancesMapper) const userBalancesLoading = useStore((state) => state.userBalances.loading) @@ -98,30 +97,54 @@ const QuickSwap = ({ const isReady = pageLoaded && !isLoadingApi && isPageVisible const haveSigner = !!signerAddress + const userFromBalance = userBalancesMapper[fromAddress] const userToBalance = userBalancesMapper[toAddress] + const fromUsdRate = usdRatesMapper[fromAddress] const toUsdRate = usdRatesMapper[toAddress] - const fromToken = tokensNameMapper[fromAddress] ?? '' - const toToken = tokensNameMapper[toAddress] ?? '' const tokensMapperNonSmallTvlStr = useMemo( () => getTokensMapperStr(tokensMapperNonSmallTvl), [tokensMapperNonSmallTvl], ) - const selectFromTokensList = useMemo( - () => getTokensObjList(selectFromList ?? selectToList, tokensMapper), - // eslint-disable-next-line react-hooks/exhaustive-deps - [selectFromList, selectToListStr, tokensMapperStr], + function getTokensObjList(tokensList: string[] | undefined, tokensMapper: TokensMapper | undefined) { + if (!tokensList || tokensList.length === 0 || !tokensMapper || Object.keys(tokensMapper).length === 0) return [] + return tokensList.map((address) => tokensMapper[address]) + } + + const tokensFrom = useMemo( + () => + getTokensObjList(selectFromList ?? selectToList, tokensMapper) + .filter((token) => !!token) + .map((token) => ({ + chain: network?.networkId ?? '', + address: token?.address as `0x${string}`, + symbol: token?.symbol, + label: '', + volume: token?.volume ?? 0, + })), + [selectFromList, selectToList, tokensMapper, network?.networkId], ) - const selectToTokensList = useMemo( - () => getTokensObjList(selectToList, tokensMapper), - // eslint-disable-next-line react-hooks/exhaustive-deps - [selectToListStr, tokensMapperStr], + const tokensTo = useMemo( + () => + getTokensObjList(selectToList, tokensMapper) + .filter((token) => !!token) + .map((token) => ({ + chain: network?.networkId ?? '', + address: token?.address as `0x${string}`, + symbol: token?.symbol, + label: '', + volume: token?.volume ?? 0, + })), + [selectToList, tokensMapper, network?.networkId], ) + const fromToken = tokensFrom.find((x) => x.address.toLocaleLowerCase() == fromAddress) + const toToken = tokensTo.find((x) => x.address.toLocaleLowerCase() == toAddress) + const updateFormValues = useCallback( ( updatedFormValues: Partial, @@ -346,9 +369,9 @@ const QuickSwap = ({ // toToken list useEffect(() => { - setSelectToList(isReady ? curve : null, isHideSmallPools ? tokensMapperNonSmallTvl : tokensMapper) + setSelectToList(isReady ? curve : null, hideSmallPools ? tokensMapperNonSmallTvl : tokensMapper) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isHideSmallPools, isReady, tokensMapperStr, tokensMapperNonSmallTvlStr, volumesMapper]) + }, [hideSmallPools, isReady, tokensMapperStr, tokensMapperNonSmallTvlStr, volumesMapper]) // re-fetch data usePageVisibleInterval(() => fetchData(), REFRESH_INTERVAL['15s'], isPageVisible) @@ -362,8 +385,8 @@ const QuickSwap = ({ isReady ? formStatus : { ...formStatus, formProcessing: true }, formValues, searchedParams, - toToken, - fromToken, + toToken?.address ?? '', + fromToken?.address ?? '', ) setSteps(updatedSteps) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -407,20 +430,15 @@ const QuickSwap = ({ testId="max" onClick={() => updateFormValues({ isFrom: true, toAmount: '' }, true)} /> - setSelectFromList(curve, selectToList)} - onSelectionChange={(value) => { - const fromAddress = value as string + + { + const fromAddress = token.address const toAddress = fromAddress === searchedParams.toAddress ? searchedParams.fromAddress : searchedParams.toAddress resetFormErrors() @@ -466,18 +484,14 @@ const QuickSwap = ({ value={formValues.toAmount} onChange={(toAmount) => updateFormValues({ isFrom: false, toAmount, fromAmount: '' })} /> - { - const toAddress = value as string + { + const toAddress = token.address const fromAddress = toAddress === searchedParams.fromAddress ? searchedParams.toAddress : searchedParams.fromAddress resetFormErrors() diff --git a/apps/main/src/dex/entities/token/index.ts b/apps/main/src/dex/entities/token/index.ts deleted file mode 100644 index 99b409687..000000000 --- a/apps/main/src/dex/entities/token/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib' diff --git a/apps/main/src/dex/entities/token/lib.ts b/apps/main/src/dex/entities/token/lib.ts deleted file mode 100644 index c32af8149..000000000 --- a/apps/main/src/dex/entities/token/lib.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMemo } from 'react' -import type { Address } from 'viem' -import useTokensMapper from '@/dex/hooks/useTokensMapper' -import useStore from '@/dex/store/useStore' -import { Token } from '@/dex/types/main.types' -import { ChainId } from '@/dex/types/main.types' - -export const useTokens = (addresses: (Address | undefined)[]): { data: (Token | undefined)[] } => { - const chainId = useStore((state) => state.curve?.chainId ?? (0 as ChainId)) - const { tokensMapper } = useTokensMapper(chainId) - - const tokensKey = JSON.stringify(addresses) - - const tokens = useMemo( - () => addresses.map((address) => (address ? tokensMapper[address] : undefined)), - // eslint-disable-next-line react-hooks/exhaustive-deps - [tokensKey, tokensMapper], - ) - - return { data: tokens } -} - -export const useTokensUSDRates = (tokens: (Address | undefined)[]): { data: (number | undefined)[] } => { - const usdRatesMapper = useStore((state) => state.usdRates.usdRatesMapper) - - const tokensKey = JSON.stringify(tokens) - - const usdRates = useMemo( - () => tokens.map((token) => (token ? usdRatesMapper[token] : undefined)), - // eslint-disable-next-line react-hooks/exhaustive-deps - [tokensKey, usdRatesMapper], - ) - - return { data: usdRates } -} diff --git a/apps/main/src/dex/features/add-gauge-reward-token/ui/TokenSelector.tsx b/apps/main/src/dex/features/add-gauge-reward-token/ui/TokenSelector.tsx index a65d5f816..20e16eea8 100644 --- a/apps/main/src/dex/features/add-gauge-reward-token/ui/TokenSelector.tsx +++ b/apps/main/src/dex/features/add-gauge-reward-token/ui/TokenSelector.tsx @@ -3,12 +3,13 @@ import React, { useEffect, useMemo } from 'react' import { useFormContext } from 'react-hook-form' import { type Address, isAddressEqual, zeroAddress } from 'viem' import type { AddRewardFormValues } from '@/dex/features/add-gauge-reward-token/types' -import { FlexItemToken, StyledTokenComboBox, SubTitle } from '@/dex/features/add-gauge-reward-token/ui' +import { FlexItemToken, SubTitle } from '@/dex/features/add-gauge-reward-token/ui' import { useGaugeRewardsDistributors } from '@/dex/entities/gauge' import { NETWORK_TOKEN } from '@/dex/constants' import useTokensMapper from '@/dex/hooks/useTokensMapper' import useStore from '@/dex/store/useStore' import { ChainId, Token } from '@/dex/types/main.types' +import { TokenSelector as TokenSelectorUIKit } from '@ui-kit/features/select-token' export const TokenSelector: React.FC<{ chainId: ChainId; poolId: string; disabled: boolean }> = ({ chainId, @@ -20,6 +21,7 @@ export const TokenSelector: React.FC<{ chainId: ChainId; poolId: string; disable const network = useStore((state) => state.networks.networks[chainId]) const rewardTokenId = watch('rewardTokenId') const { tokensMapper } = useTokensMapper(chainId) + const { data: gaugeRewardsDistributors, isSuccess: isGaugeRewardsDistributorsSuccess } = useGaugeRewardsDistributors({ chainId, poolId, @@ -27,16 +29,26 @@ export const TokenSelector: React.FC<{ chainId: ChainId; poolId: string; disable const filteredTokens = useMemo(() => { const gaugeRewardTokens = Object.keys(gaugeRewardsDistributors || {}) - return Object.values(tokensMapper).filter( - (token): token is Token => - token !== undefined && - token.decimals === 18 && - !aliasesCrv && - ![...gaugeRewardTokens, zeroAddress, NETWORK_TOKEN, aliasesCrv].some((rewardToken) => - isAddressEqual(rewardToken as Address, token.address as Address), - ), - ) - }, [gaugeRewardsDistributors, tokensMapper, aliasesCrv]) + return Object.values(tokensMapper) + .filter( + (token): token is Token => + token !== undefined && + token.decimals === 18 && + !aliasesCrv && + ![...gaugeRewardTokens, zeroAddress, NETWORK_TOKEN, aliasesCrv].some((rewardToken) => + isAddressEqual(rewardToken as Address, token.address as Address), + ), + ) + .map((token) => ({ + chain: network?.networkId ?? '', + address: token?.address as `0x${string}`, + symbol: token?.symbol, + label: '', + volume: token?.volume ?? 0, + })) + }, [gaugeRewardsDistributors, tokensMapper, aliasesCrv, network.networkId]) + + const selectedToken = filteredTokens.find((x) => x.address === rewardTokenId) useEffect(() => { if (!isGaugeRewardsDistributorsSuccess) return @@ -54,17 +66,17 @@ export const TokenSelector: React.FC<{ chainId: ChainId; poolId: string; disable return ( {t`Token`} - { - setValue('rewardTokenId', value as Address, { shouldValidate: true }) + disabled={disabled || filteredTokens.length === 0} + onToken={(token) => { + setValue('rewardTokenId', token.address, { shouldValidate: true }) + }} + sx={{ + width: '100%', + height: '100%', }} - disabled={disabled} /> ) diff --git a/apps/main/src/dex/features/add-gauge-reward-token/ui/styled.tsx b/apps/main/src/dex/features/add-gauge-reward-token/ui/styled.tsx index 298025075..b16e9de8c 100644 --- a/apps/main/src/dex/features/add-gauge-reward-token/ui/styled.tsx +++ b/apps/main/src/dex/features/add-gauge-reward-token/ui/styled.tsx @@ -1,6 +1,5 @@ import Button from '@ui/Button' import styled from 'styled-components' -import TokenComboBox from '@/dex/components/ComboBoxSelectToken' export const FlexItemToken = styled.div` flex: 0 0 auto; @@ -22,7 +21,3 @@ export const StyledButton = styled(Button)` max-width: 100%; box-sizing: border-box; ` - -export const StyledTokenComboBox = styled(TokenComboBox)` - height: var(--height-medium); -` diff --git a/apps/main/src/dex/features/deposit-gauge-reward/ui/AmountTokenInput.tsx b/apps/main/src/dex/features/deposit-gauge-reward/ui/AmountTokenInput.tsx index 786fd93fc..79d037657 100644 --- a/apps/main/src/dex/features/deposit-gauge-reward/ui/AmountTokenInput.tsx +++ b/apps/main/src/dex/features/deposit-gauge-reward/ui/AmountTokenInput.tsx @@ -1,6 +1,6 @@ import { InputDebounced, InputMaxBtn } from '@ui/InputComp' import { t } from '@ui-kit/lib/i18n' -import { useCallback, useMemo, type Key } from 'react' +import { useCallback, useMemo } from 'react' import { useFormContext } from 'react-hook-form' import { Address, isAddressEqual } from 'viem' import { NETWORK_TOKEN } from '@/dex/constants' @@ -12,7 +12,6 @@ import { FlexItemMaxBtn, FlexItemToken, StyledInputProvider, - StyledTokenComboBox, } from '@/dex/features/deposit-gauge-reward/ui/styled' import { useDepositRewardApproveIsMutating, @@ -20,16 +19,16 @@ import { useGaugeRewardsDistributors, } from '@/dex/entities/gauge' import { useIsSignerConnected, useSignerAddress, useTokensBalances } from '@/dex/entities/signer' -import { useTokens } from '@/dex/entities/token' import { FlexContainer } from '@ui/styled-containers' import { ChainId, Token } from '@/dex/types/main.types' import { formatNumber } from '@ui/utils' +import { type TokenOption, TokenSelector } from '@ui-kit/features/select-token' export const AmountTokenInput: React.FC<{ chainId: ChainId poolId: string }> = ({ chainId, poolId }) => { - const { setValue, getValues, formState, watch, setError, clearErrors } = useFormContext() + const { setValue, getValues, formState, watch } = useFormContext() const rewardTokenId = watch('rewardTokenId') const amount = watch('amount') const epoch = watch('epoch') @@ -39,10 +38,10 @@ export const AmountTokenInput: React.FC<{ const isMaxLoading = useStore((state) => state.quickSwap.isMaxLoading) const { networkId } = useStore((state) => state.networks.networks[chainId]) + const userBalancesMapper = useStore((state) => state.userBalances.userBalancesMapper) + const tokenPrices = useStore((state) => state.usdRates.usdRatesMapper) + const { tokensMapper } = useTokensMapper(chainId) - const { - data: [token], - } = useTokens([rewardTokenId]) const { data: rewardDistributors, isPending: isPendingRewardDistributors } = useGaugeRewardsDistributors({ chainId, @@ -57,30 +56,40 @@ export const AmountTokenInput: React.FC<{ isLoading: isTokenBalancesLoading, } = useTokensBalances([rewardTokenId]) - const filteredTokens = useMemo(() => { + const filteredTokens = useMemo(() => { if (isPendingRewardDistributors || !rewardDistributors || !signerAddress) return [] const activeRewardTokens = Object.entries(rewardDistributors) .filter(([_, distributor]) => isAddressEqual(distributor as Address, signerAddress)) .map(([tokenId]) => tokenId) - const filteredTokens = Object.values(tokensMapper).filter( - (token): token is Token => - token !== undefined && - activeRewardTokens.some((rewardToken) => isAddressEqual(rewardToken as Address, token.address as Address)), - ) + const filteredTokens = Object.values(tokensMapper) + .filter( + (token): token is Token => + token !== undefined && + activeRewardTokens.some((rewardToken) => isAddressEqual(rewardToken as Address, token.address as Address)), + ) + .map((token) => ({ + chain: networkId ?? '', + address: token?.address as `0x${string}`, + symbol: token?.symbol, + label: '', + volume: token?.volume ?? 0, + })) const rewardTokenId = getValues('rewardTokenId') if ( rewardTokenId && filteredTokens.length > 0 && - !filteredTokens.some((token) => isAddressEqual(token.address as Address, rewardTokenId)) + !filteredTokens.some((token) => isAddressEqual(token.address, rewardTokenId)) ) { - setValue('rewardTokenId', filteredTokens[0].address as Address, { shouldValidate: true }) + setValue('rewardTokenId', filteredTokens[0].address, { shouldValidate: true }) } return filteredTokens - }, [isPendingRewardDistributors, rewardDistributors, signerAddress, tokensMapper, getValues, setValue]) + }, [isPendingRewardDistributors, rewardDistributors, signerAddress, tokensMapper, getValues, networkId, setValue]) + + const token = filteredTokens.find((x) => x.address === rewardTokenId) const onChangeAmount = useCallback( (amount: string) => { @@ -90,9 +99,9 @@ export const AmountTokenInput: React.FC<{ ) const onChangeToken = useCallback( - (value: Key) => { - if (rewardTokenId && isAddressEqual(value as Address, rewardTokenId)) return - setValue('rewardTokenId', value as Address, { shouldValidate: true }) + (value: TokenOption) => { + if (rewardTokenId && isAddressEqual(value.address, rewardTokenId)) return + setValue('rewardTokenId', value.address, { shouldValidate: true }) setValue('step', DepositRewardStep.APPROVAL, { shouldValidate: true }) }, [rewardTokenId, setValue], @@ -142,19 +151,13 @@ export const AmountTokenInput: React.FC<{ /> - diff --git a/apps/main/src/dex/features/deposit-gauge-reward/ui/HelperFields.tsx b/apps/main/src/dex/features/deposit-gauge-reward/ui/HelperFields.tsx index c876f7a99..ab5b9d888 100644 --- a/apps/main/src/dex/features/deposit-gauge-reward/ui/HelperFields.tsx +++ b/apps/main/src/dex/features/deposit-gauge-reward/ui/HelperFields.tsx @@ -1,18 +1,25 @@ +import { useMemo } from 'react' import { useFormContext } from 'react-hook-form' import FieldHelperUsdRate from '@/dex/components/FieldHelperUsdRate' import { type DepositRewardFormValues } from '@/dex/features/deposit-gauge-reward/types' -import { useTokensUSDRates } from '@/dex/entities/token' import { FlexContainer } from '@ui/styled-containers' import { ChainId } from '@/dex/types/main.types' +import useStore from '@/dex/store/useStore' export const HelperFields: React.FC<{ chainId: ChainId; poolId: string }> = ({ chainId, poolId }) => { const { watch } = useFormContext() const rewardTokenId = watch('rewardTokenId') const amount = watch('amount') - const { - data: [tokenUsdRate], - } = useTokensUSDRates([rewardTokenId]) + const usdRatesMapper = useStore((state) => state.usdRates.usdRatesMapper) + const tokens = [rewardTokenId] + const tokensKey = JSON.stringify(tokens) + + const [tokenUsdRate] = useMemo( + () => tokens.map((token) => (token ? usdRatesMapper[token] : undefined)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [tokensKey, usdRatesMapper], + ) return ( diff --git a/apps/main/src/dex/features/deposit-gauge-reward/ui/styled.tsx b/apps/main/src/dex/features/deposit-gauge-reward/ui/styled.tsx index ded92772a..f6de191d1 100644 --- a/apps/main/src/dex/features/deposit-gauge-reward/ui/styled.tsx +++ b/apps/main/src/dex/features/deposit-gauge-reward/ui/styled.tsx @@ -1,6 +1,5 @@ import InputProvider from '@ui/InputComp' import styled from 'styled-components' -import TokenComboBox from '@/dex/components/ComboBoxSelectToken' export const StyledInputProvider = styled(InputProvider)` width: 100%; @@ -21,10 +20,6 @@ export const FlexItemToken = styled.div` flex: 0 0 120px; ` -export const StyledTokenComboBox = styled(TokenComboBox)` - height: var(--height-x-large); -` - export const EpochLabel = styled.label` align-self: center; font-size: var(--font-size-2); diff --git a/apps/main/src/dex/store/createGlobalSlice.ts b/apps/main/src/dex/store/createGlobalSlice.ts index e178f6b6f..a4f34a73b 100644 --- a/apps/main/src/dex/store/createGlobalSlice.ts +++ b/apps/main/src/dex/store/createGlobalSlice.ts @@ -202,9 +202,6 @@ const createGlobalSlice = (set: SetState, get: GetState): GlobalSl return } - // default hideSmallPools to false if poolIds length < 10 - state.poolList.setStateByKey('formValues', { ...state.poolList.formValues, hideSmallPools: poolIds.length > 10 }) - // TODO: Temporary code to determine if there is an issue with getting base APY from Kava Api (https://api.curve.fi/api/getFactoryAPYs-kava) const failedFetching24hOldVprice: { [poolAddress: string]: boolean } = chainId === 2222 ? await curvejsApi.network.getFailedFetching24hOldVprice() : {} diff --git a/apps/main/src/dex/store/createNetworksSlice.ts b/apps/main/src/dex/store/createNetworksSlice.ts index 359b38f14..f4be5341a 100644 --- a/apps/main/src/dex/store/createNetworksSlice.ts +++ b/apps/main/src/dex/store/createNetworksSlice.ts @@ -175,7 +175,6 @@ const defaultNetworks = Object.entries({ }, [Chain.Kava]: { poolFilters: ['all', 'usd', 'btc', 'kava', 'crypto', 'tricrypto', 'stableng', 'others', 'user'], - poolListFormValuesDefault: { hideSmallPools: false }, // remove if Kava have > 10 pools swap: { fromAddress: '0x765277eebeca2e31912c9946eae1021199b39c61', toAddress: '0xb44a9b6905af7c801311e8f4e76932ee959c663c', diff --git a/apps/main/src/dex/store/createPoolListSlice.ts b/apps/main/src/dex/store/createPoolListSlice.ts index 6a29eb1e7..126cfec0e 100644 --- a/apps/main/src/dex/store/createPoolListSlice.ts +++ b/apps/main/src/dex/store/createPoolListSlice.ts @@ -67,9 +67,9 @@ export type PoolListSlice = { filterBySearchText

    (searchText: string, poolDatas: P[], highlightResult?: boolean): P[] filterSmallTvl

    (poolDatas: P[], tvlMapper: TvlMapper, chainId: ChainId): P[] sortFn

    (sortKey: SortKey, order: Order, poolDatas: P[], rewardsApyMapper: RewardsApyMapper, volumeMapper: VolumeMapper, tvlMapper: TvlMapper, campaignRewardsMapper: CampaignRewardsMapper): P[] - setSortAndFilterData(rChainId: ChainId, searchParams: SearchParams, poolDatas: PoolData[], rewardsApyMapper: RewardsApyMapper, volumeMapper: VolumeMapper, tvlMapper: TvlMapper, userPoolList: UserPoolListMapper, campaignRewardsMapper: CampaignRewardsMapper): Promise + setSortAndFilterData(rChainId: ChainId, searchParams: SearchParams, hideSmallPools: boolean, poolDatas: PoolData[], rewardsApyMapper: RewardsApyMapper, volumeMapper: VolumeMapper, tvlMapper: TvlMapper, userPoolList: UserPoolListMapper, campaignRewardsMapper: CampaignRewardsMapper): Promise setSortAndFilterCachedData(rChainId: ChainId, searchParams: SearchParams, poolDatasCached: PoolDataCache[], volumeMapperCached: { [poolId:string]: { value: string } }, tvlMapperCached: { [poolId:string]: { value: string } }): void - setFormValues(rChainId: ChainId, isLite: boolean, searchParams: SearchParams, poolDatas: PoolData[] | undefined, poolDatasCached: PoolDataCache[] | undefined, rewardsApyMapper: RewardsApyMapper | undefined, volumeMapper: VolumeMapper | undefined, volumeMapperCached: ValueMapperCached | undefined, tvlMapper: TvlMapper | undefined, tvlMapperCached: ValueMapperCached | undefined, userPoolList: UserPoolListMapper | undefined, campaignRewardsMapper: CampaignRewardsMapper): void + setFormValues(rChainId: ChainId, isLite: boolean, searchParams: SearchParams, hideSmallPools: boolean, poolDatas: PoolData[] | undefined, poolDatasCached: PoolDataCache[] | undefined, rewardsApyMapper: RewardsApyMapper | undefined, volumeMapper: VolumeMapper | undefined, volumeMapperCached: ValueMapperCached | undefined, tvlMapper: TvlMapper | undefined, tvlMapperCached: ValueMapperCached | undefined, userPoolList: UserPoolListMapper | undefined, campaignRewardsMapper: CampaignRewardsMapper): void setStateByActiveKey(key: StateKey, activeKey: string, value: T): void setStateByKey(key: StateKey, value: T): void @@ -159,7 +159,10 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi networks: { networks }, } = get() const { hideSmallPoolsTvl } = networks[chainId] - return poolDatas.filter(({ pool }) => +(tvlMapper?.[pool.id]?.value || '0') > hideSmallPoolsTvl) + + return poolDatas.length < 10 + ? poolDatas + : poolDatas.filter(({ pool }) => +(tvlMapper?.[pool.id]?.value || '0') > hideSmallPoolsTvl) }, sortFn: (sortKey, order, poolDatas, rewardsApyMapper, tvlMapper, volumeMapper, campaignRewardsMapper) => { if (poolDatas.length === 0) { @@ -213,6 +216,7 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi setSortAndFilterData: async ( rChainId, searchParams, + hideSmallPools, poolDatas, rewardsApyMapper, volumeMapper, @@ -224,7 +228,7 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi pools, [sliceKey]: { activeKey, formStatus, result: storedResults, ...sliceState }, } = get() - const { hideSmallPools, searchText, filterKey, sortBy, sortByOrder } = searchParams + const { searchText, filterKey, sortBy, sortByOrder } = searchParams let tablePoolDatas: PoolData[] = [...poolDatas] @@ -316,6 +320,7 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi rChainId, isLite, searchParams, + hideSmallPools, poolDatas, poolDatasCached = [], rewardsApyMapper = {}, @@ -336,19 +341,14 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi isLoading: typeof storedResults[activeKey] === 'undefined', }) - const { hideSmallPools, searchText, filterKey, sortBy, sortByOrder } = searchParams + const { searchText, filterKey, sortBy, sortByOrder } = searchParams const isDefaultSearchParams = - hideSmallPools && - searchText === '' && - filterKey === 'all' && - sortBy === (isLite ? 'tvl' : 'volume') && - sortByOrder === 'desc' + searchText === '' && filterKey === 'all' && sortBy === (isLite ? 'tvl' : 'volume') && sortByOrder === 'desc' // update form values formValues = { ...formValues, - hideSmallPools, searchTextByTokensAndAddresses: {}, searchTextByOther: {}, } @@ -377,6 +377,7 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi sliceState.setSortAndFilterData( rChainId, searchParams, + hideSmallPools, poolDatas, rewardsApyMapper, volumeMapper, @@ -423,14 +424,14 @@ const createPoolListSlice = (set: SetState, get: GetState): PoolLi }) export function getPoolListActiveKey(chainId: ChainId, searchParams: SearchParams) { - const { filterKey, hideSmallPools, searchText, sortBy, sortByOrder } = searchParams + const { filterKey, searchText, sortBy, sortByOrder } = searchParams let parsedSearchText = searchText if (searchText && searchText.length > 20) { parsedSearchText = chunk(searchText, 5) .map((group) => group[0]) .join('') } - return `${chainId}-${filterKey}-${hideSmallPools}-${sortBy}-${sortByOrder}-${parsedSearchText}` + return `${chainId}-${filterKey}-${sortBy}-${sortByOrder}-${parsedSearchText}` } export default createPoolListSlice diff --git a/apps/main/src/dex/store/createQuickSwapSlice.ts b/apps/main/src/dex/store/createQuickSwapSlice.ts index d7cf1856b..b89d189c1 100644 --- a/apps/main/src/dex/store/createQuickSwapSlice.ts +++ b/apps/main/src/dex/store/createQuickSwapSlice.ts @@ -11,7 +11,6 @@ import type { import cloneDeep from 'lodash/cloneDeep' import isEqual from 'lodash/isEqual' -import orderBy from 'lodash/orderBy' import { DEFAULT_FORM_STATUS, DEFAULT_FORM_VALUES, sortTokensByGasFees } from '@/dex/components/PageRouterSwap/utils' import { NETWORK_TOKEN } from '@/dex/constants' @@ -410,11 +409,9 @@ const createQuickSwapSlice = (set: SetState, get: GetState): Quick const { chainId, signerAddress } = curve - const selectToList = orderBy( - Object.entries(tokensMapper).map(([_, v]) => v!), - ({ volume }) => (typeof volume !== 'undefined' ? +volume : 0), - ['desc'], - ).map(({ address }) => address) + const selectToList = Object.entries(tokensMapper) + .map(([_, v]) => v!) + .map(({ address }) => address) sliceState.setStateByActiveKey('selectToList', chainId.toString(), selectToList) @@ -641,9 +638,4 @@ function getRouterWarningModal( return null } -export function getTokensObjList(tokensList: string[] | undefined, tokensMapper: TokensMapper | undefined) { - if (!tokensList || tokensList.length === 0 || !tokensMapper || Object.keys(tokensMapper).length === 0) return [] - return tokensList.map((address) => tokensMapper[address]) -} - export default createQuickSwapSlice diff --git a/apps/main/src/dex/store/createSelectTokenSlice.tsx b/apps/main/src/dex/store/createSelectTokenSlice.tsx deleted file mode 100644 index 2af60b9ba..000000000 --- a/apps/main/src/dex/store/createSelectTokenSlice.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { GetState, SetState } from 'zustand' -import type { State } from '@/dex/store/useStore' -import cloneDeep from 'lodash/cloneDeep' -import { Token } from '@/dex/types/main.types' -import { filterTokens } from '@ui-kit/utils' - -type StateKey = keyof typeof DEFAULT_STATE - -type SliceState = { - filterValue: string - selectTokensResult: Token[] -} - -type FilterOptions = { - showSearch?: boolean - endsWith(string: string, substring: string): boolean -} - -const sliceKey = 'selectToken' - -export type SelectTokenSlice = { - [sliceKey]: SliceState & { - filterFn(filterValue: string, tokens: Token[], filterOptions: FilterOptions): Token[] - setFilterValue(filterValue: string, tokens: Token[], filterOptions: FilterOptions): void - - setStateByActiveKey(key: StateKey, activeKey: string, value: T): void - setStateByKey(key: StateKey, value: T): void - setStateByKeys(SliceState: Partial): void - resetState(): void - } -} - -const DEFAULT_STATE: SliceState = { - filterValue: '', - selectTokensResult: [], -} - -const createSelectTokenSlice = (set: SetState, get: GetState): SelectTokenSlice => ({ - [sliceKey]: { - ...DEFAULT_STATE, - - filterFn: (filterValue, tokens, { endsWith }) => filterTokens(filterValue, tokens, endsWith), - setFilterValue: (filterValue, tokens, filterOptions) => { - get()[sliceKey].setStateByKey('filterValue', filterValue) - - // filter result - let result = tokens - - if (filterValue && filterOptions.showSearch) { - result = get()[sliceKey].filterFn(filterValue, tokens, filterOptions) - } - get()[sliceKey].setStateByKey('selectTokensResult', result) - }, - - // slice helpers - setStateByActiveKey: (key, activeKey, value) => { - get().setAppStateByActiveKey(sliceKey, key, activeKey, value) - }, - setStateByKey: (key, value) => { - get().setAppStateByKey(sliceKey, key, value) - }, - setStateByKeys: (sliceState) => { - get().setAppStateByKeys(sliceKey, sliceState) - }, - resetState: () => { - get().resetAppState(sliceKey, cloneDeep(DEFAULT_STATE)) - }, - }, -}) - -export default createSelectTokenSlice diff --git a/apps/main/src/dex/store/useStore.ts b/apps/main/src/dex/store/useStore.ts index aad305f4a..adb1f566a 100644 --- a/apps/main/src/dex/store/useStore.ts +++ b/apps/main/src/dex/store/useStore.ts @@ -21,7 +21,6 @@ import createLockedCrvSlice, { LockedCrvSlice } from '@/dex/store/createLockedCr import createPoolSwapSlice, { PoolSwapSlice } from '@/dex/store/createPoolSwapSlice' import createCreatePoolSlice, { CreatePoolSlice } from '@/dex/store/createCreatePoolSlice' import createIntegrationsSlice, { IntegrationsSlice } from '@/dex/store/createIntegrationsSlice' -import createSelectTokenSlice, { SelectTokenSlice } from '@/dex/store/createSelectTokenSlice' import createDeployGaugeSlice, { DeployGaugeSlice } from '@/dex/store/createDeployGaugeSlice' import createPoolDepositSlice, { PoolDepositSlice } from '@/dex/store/createPoolDepositSlice' import createPoolWithdrawSlice, { PoolWithdrawSlice } from '@/dex/store/createPoolWithdrawSlice' @@ -45,7 +44,6 @@ export type State = GlobalSlice & LockedCrvSlice & CreatePoolSlice & IntegrationsSlice & - SelectTokenSlice & DeployGaugeSlice & CampaignRewardsSlice @@ -68,7 +66,6 @@ const store = (set: SetState, get: GetState): State => ({ ...createLockedCrvSlice(set, get), ...createCreatePoolSlice(set, get), ...createIntegrationsSlice(set, get), - ...createSelectTokenSlice(set, get), ...createDeployGaugeSlice(set, get), ...createCampaignRewardsSlice(set, get), }) diff --git a/packages/curve-ui-kit/src/features/select-token/SelectToken.stories.tsx b/packages/curve-ui-kit/src/features/select-token/SelectToken.stories.tsx new file mode 100644 index 000000000..fd98c1d77 --- /dev/null +++ b/packages/curve-ui-kit/src/features/select-token/SelectToken.stories.tsx @@ -0,0 +1,206 @@ +import { useState } from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import { Button, Stack, Typography } from '@mui/material' +import { TokenSelector } from './' +import type { TokenOption } from './types' + +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' + +const { Spacing } = SizesAndSpaces + +const defaultTokens: TokenOption[] = [ + { + chain: 'ethereum', + address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + symbol: 'ETH', + label: 'Ethereum', + volume: 1, + }, + { + chain: 'ethereum', + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + label: 'Circle Dollar', + volume: 2, + }, + { + chain: 'ethereum', + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + symbol: 'USDT', + label: 'Tether Dollar', + volume: 3, + }, + { + chain: 'ethereum', + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + label: 'Maker DAI', + volume: 4, + }, +] + +const defaultBalances = { + [defaultTokens[0].address]: '32', + [defaultTokens[1].address]: '1000.00', + [defaultTokens[3].address]: '2000.00', +} + +const defaultTokenPrices = { + [defaultTokens[0].address]: 2600, + [defaultTokens[1].address]: 0.996, + [defaultTokens[2].address]: 1.01, +} + +const defaultFavorites = [defaultTokens[0], defaultTokens[1]] + +const defaultDisabledTokens = [defaultTokens[2].address] + +const TokenSelectorComponent = ({ + selectedToken: selectedTokenInit, + ...props +}: React.ComponentProps) => { + const [selectedToken, setSelectedToken] = useState(selectedTokenInit) + + return +} + +const meta: Meta = { + title: 'UI Kit/Features/TokenSelector', + component: TokenSelectorComponent, + args: { + selectedToken: defaultTokens[0], + tokens: defaultTokens, + favorites: defaultFavorites, + balances: defaultBalances, + tokenPrices: defaultTokenPrices, + disabled: false, + showSearch: true, + showSettings: true, + compact: false, + error: '', + disabledTokens: defaultDisabledTokens, + disableSorting: false, + onToken: (token) => console.log('Selected token:', token), + }, + argTypes: { + tokens: { + control: 'object', + description: 'Array of token options to display in selector', + }, + favorites: { + control: 'object', + description: 'Array of favorite token options to display in selector', + }, + balances: { + control: 'object', + description: 'Record of token balances by address', + }, + tokenPrices: { + control: 'object', + description: 'Record of token prices in USD by address', + }, + disabled: { + control: 'boolean', + description: 'Disables the token selector button and modal', + }, + showSearch: { + control: 'boolean', + description: 'Shows search input in token selector modal', + }, + showSettings: { + control: 'boolean', + description: 'Shows settings button in token selector modal footer', + }, + compact: { + control: 'boolean', + description: 'Renders the modal in a compact size', + }, + selectedToken: { + control: 'object', + description: 'Currently selected token', + }, + error: { + control: 'text', + description: 'Custom error message to display in the token selector modal', + }, + disabledTokens: { + control: 'object', + description: 'Array of token addresses that should be disabled in the selector', + }, + disableSorting: { + control: 'boolean', + description: 'Disable automatic sorting so you can apply your own in the tokens property', + }, + customOptions: { + control: false, + description: 'Adds extra custom options to the modal, below the favorites', + }, + + onToken: { + action: 'token selected', + description: 'Callback when a token is selected', + }, + }, +} + +type Story = StoryObj + +export const Default: Story = { + parameters: { + docs: { + description: { + component: 'TokenSelector allows selecting a token from a list with search, favorites and balances', + story: 'Default view showing token selector button that opens modal', + }, + }, + }, +} + +export const NoSelectedToken: Story = { + args: { + selectedToken: undefined, + disabled: true, + }, + parameters: { + docs: { + description: { + story: 'Token selector with no token selected initially', + }, + }, + }, +} + +export const WithError: Story = { + args: { + error: 'Failed to load tokens. Please try again later.', + }, + parameters: { + docs: { + description: { + story: 'Token selector displaying an error message', + }, + }, + }, +} + +export const WithCustomOptions: Story = { + args: { + customOptions: ( + + Custom Options + + + ), + }, + parameters: { + docs: { + description: { + story: 'Token selector with custom options displayed below favorites', + }, + }, + }, +} + +export default meta diff --git a/packages/curve-ui-kit/src/features/select-token/index.ts b/packages/curve-ui-kit/src/features/select-token/index.ts new file mode 100644 index 000000000..5f1acf4f0 --- /dev/null +++ b/packages/curve-ui-kit/src/features/select-token/index.ts @@ -0,0 +1,2 @@ +export { TokenSelector } from './ui/TokenSelector' +export * from './types' diff --git a/packages/curve-ui-kit/src/features/select-token/types.ts b/packages/curve-ui-kit/src/features/select-token/types.ts new file mode 100644 index 000000000..7f12fd1f6 --- /dev/null +++ b/packages/curve-ui-kit/src/features/select-token/types.ts @@ -0,0 +1,9 @@ +import { Address } from '@ui-kit/utils' + +export type TokenOption = { + chain: string + address: Address + symbol: string + label: string + volume: number +} diff --git a/packages/curve-ui-kit/src/features/select-token/ui/TokenSelectButton.tsx b/packages/curve-ui-kit/src/features/select-token/ui/TokenSelectButton.tsx new file mode 100644 index 000000000..ad0892e29 --- /dev/null +++ b/packages/curve-ui-kit/src/features/select-token/ui/TokenSelectButton.tsx @@ -0,0 +1,95 @@ +import CircularProgress from '@mui/material/CircularProgress' +import Select from '@mui/material/Select' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import type { SxProps } from '@mui/system' + +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' +import { TokenIcon } from '@ui-kit/shared/ui/TokenIcon' + +const { Spacing, ButtonSize, MinWidth } = SizesAndSpaces + +import type { TokenOption } from '../types' + +const ButtonContent = ({ token, disabled }: { token: TokenOption; disabled: boolean }) => ( + + + {token.symbol} + +) + +const Spinner = () => ( + theme.palette.text.secondary, + }} + /> +) + +export type TokenSelectButtonCallbacks = { + onClick: () => void +} + +export type TokenSelectButtonProps = { + token?: TokenOption + disabled: boolean +} + +export type Props = TokenSelectButtonProps & + TokenSelectButtonCallbacks & { + sx?: SxProps + } + +/** The token selector is Select but acts like a button, so it's a bit unique */ +export const TokenSelectButton = ({ token, disabled, onClick, sx }: Props) => ( +