diff --git a/packages/web/src/components/buy-sell-modal/components/TokenDropdown.tsx b/packages/web/src/components/buy-sell-modal/components/TokenDropdown.tsx index 5cbcdb0a365..b0ebb3fc4bd 100644 --- a/packages/web/src/components/buy-sell-modal/components/TokenDropdown.tsx +++ b/packages/web/src/components/buy-sell-modal/components/TokenDropdown.tsx @@ -1,5 +1,6 @@ -import { useMemo, useCallback, useRef, useState } from 'react' +import { useMemo, useCallback, useRef, useState, useEffect } from 'react' +import { transformArtistCoinToTokenInfo } from '@audius/common/api' import type { TokenInfo } from '@audius/common/store' import { IconCaretDown, @@ -14,11 +15,15 @@ import { import { useTheme } from '@emotion/react' import Select, { components } from 'react-select' import type { SingleValue, OptionProps, InputProps } from 'react-select' +import { useDebounce } from 'react-use' +import { useArtistCoins } from '~/api/tan-query/coins/useArtistCoins' import zIndex from 'utils/zIndex' import { TokenIcon } from '../TokenIcon' +const DEBOUNCE_MS = 300 + type TokenOption = { value: string label: string @@ -27,7 +32,6 @@ type TokenOption = { type TokenDropdownProps = { selectedToken: TokenInfo - availableTokens: TokenInfo[] onTokenChange?: (token: TokenInfo) => void disabled?: boolean } @@ -117,37 +121,76 @@ const CustomInput = (props: InputProps) => { export const TokenDropdown = ({ selectedToken, - availableTokens, onTokenChange, disabled = false }: TokenDropdownProps) => { const wrapperRef = useRef(null) const { color, spacing } = useTheme() const [isOpen, setIsOpen] = useState(false) + const [isSearching, setIsSearching] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [debouncedQuery, setDebouncedQuery] = useState('') + + useDebounce( + () => { + setDebouncedQuery(searchQuery) + setIsSearching(false) + }, + DEBOUNCE_MS, + [searchQuery] + ) + + const { + data: artistCoins, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isPending + } = useArtistCoins( + { + pageSize: 10, + query: debouncedQuery + }, + { + enabled: isOpen + } + ) const handleTokenSelect = useCallback( (option: SingleValue) => { if (option) { onTokenChange?.(option.tokenInfo) setIsOpen(false) + setSearchQuery('') } }, [onTokenChange] ) + const handleInputChange = useCallback((newValue: string) => { + setIsSearching(true) + setSearchQuery(newValue) + return newValue + }, []) + + const handleMenuScrollToBottom = useCallback(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + const options: TokenOption[] = useMemo(() => { - return availableTokens - .map((token) => ({ - value: token.symbol, - label: token.name ?? token.symbol, - tokenInfo: token - })) - .sort((a, b) => a.label.localeCompare(b.label)) - }, [availableTokens]) + if (!artistCoins) return [] + return artistCoins.map(transformArtistCoinToTokenInfo).map((token) => ({ + value: token.symbol, + label: token.name ?? token.symbol, + tokenInfo: token + })) + }, [artistCoins]) const selectedOption = useMemo( () => - options.find((option) => option.value === selectedToken.symbol) || { + options.find((option) => option.value === selectedToken.symbol) ?? { value: selectedToken.symbol, label: selectedToken.name ?? selectedToken.symbol, tokenInfo: selectedToken @@ -155,6 +198,13 @@ export const TokenDropdown = ({ [options, selectedToken] ) + // Reset search when dropdown closes + useEffect(() => { + if (!isOpen) { + setSearchQuery('') + } + }, [isOpen]) + return (