-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
006787d
commit c613ebd
Showing
13 changed files
with
121,135 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import type { ListProps } from '@chakra-ui/react' | ||
import type { FC } from 'react' | ||
import { useCallback } from 'react' | ||
import { Virtuoso } from 'react-virtuoso' | ||
import type { Asset } from 'types/Asset' | ||
|
||
import { AssetRow } from './AssetRow' | ||
|
||
const style = { height: '400px' } | ||
|
||
export type AssetData = { | ||
assets: Asset[] | ||
handleClick: (asset: Asset) => void | ||
} | ||
|
||
type AssetListProps = AssetData & ListProps | ||
|
||
export const AssetList: FC<AssetListProps> = ({ assets, handleClick }) => { | ||
const renderItemContent = useCallback( | ||
(index: number, asset: Asset) => { | ||
return <AssetRow {...asset} key={index} onClick={handleClick} /> | ||
}, | ||
[handleClick], | ||
) | ||
|
||
return ( | ||
<Virtuoso | ||
style={style} | ||
data={assets} | ||
totalCount={assets.length} | ||
itemContent={renderItemContent} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Avatar, Box, Button, Flex, SkeletonCircle, Text } from '@chakra-ui/react' | ||
import type { FC } from 'react' | ||
import { memo, useCallback, useState } from 'react' | ||
import { middleEllipsis } from 'lib/utils' | ||
import type { Asset } from 'types/Asset' | ||
|
||
const focus = { | ||
shadow: 'outline-inset', | ||
} | ||
|
||
type AssetRowProps = { | ||
onClick: (asset: Asset) => void | ||
} & Asset | ||
|
||
export const AssetRow: FC<AssetRowProps> = memo(({ onClick, ...asset }) => { | ||
const [imgLoaded, setImgLoaded] = useState(false) | ||
const { name, icon, symbol, id } = asset | ||
const handleOnClick = useCallback(() => { | ||
onClick(asset) | ||
}, [asset, onClick]) | ||
|
||
const handleImgLoad = useCallback(() => { | ||
setImgLoaded(true) | ||
}, []) | ||
|
||
if (!asset) return null | ||
|
||
return ( | ||
<Button | ||
width='full' | ||
variant='ghost' | ||
onClick={handleOnClick} | ||
justifyContent='space-between' | ||
_focus={focus} | ||
height='auto' | ||
py={4} | ||
> | ||
<Flex gap={4} alignItems='center'> | ||
<SkeletonCircle isLoaded={imgLoaded}> | ||
<Avatar src={icon} size='sm' onLoad={handleImgLoad} /> | ||
</SkeletonCircle> | ||
<Box textAlign='left'> | ||
<Text | ||
lineHeight={1} | ||
textOverflow='ellipsis' | ||
whiteSpace='nowrap' | ||
maxWidth='200px' | ||
overflow='hidden' | ||
color='text.base' | ||
fontSize='sm' | ||
mb={1} | ||
> | ||
{name} | ||
</Text> | ||
<Flex alignItems='center' gap={2}> | ||
<Text fontWeight='normal' fontSize='xs' color='text.subtle'> | ||
{symbol} | ||
</Text> | ||
{id && ( | ||
<Text fontWeight='normal' fontSize='xs' color='text.subtle'> | ||
{middleEllipsis(id)} | ||
</Text> | ||
)} | ||
</Flex> | ||
</Box> | ||
</Flex> | ||
</Button> | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { | ||
Button, | ||
CloseButton, | ||
Flex, | ||
Input, | ||
Modal, | ||
ModalBody, | ||
ModalContent, | ||
ModalHeader, | ||
ModalOverlay, | ||
Text, | ||
} from '@chakra-ui/react' | ||
import type { ChainId } from '@shapeshiftoss/caip' | ||
import type { ChangeEvent } from 'react' | ||
import { useCallback, useEffect, useMemo, useState } from 'react' | ||
import AssetData from 'lib/generatedAssetData.json' | ||
import { isNft } from 'lib/utils' | ||
import type { Asset } from 'types/Asset' | ||
|
||
import { AssetList } from './AssetList' | ||
import type { ChainRow } from './ChainButton' | ||
import { ChainButton } from './ChainButton' | ||
import { filterAssetsBySearchTerm } from './helpers/filterAssetsBySearchTerm' | ||
|
||
type AssetSelectModalProps = { | ||
isOpen: boolean | ||
onClose: () => void | ||
} | ||
|
||
export const AssetSelectModal: React.FC<AssetSelectModalProps> = ({ isOpen, onClose }) => { | ||
const [searchQuery, setSearchQuery] = useState('') | ||
const assets = Object.values(AssetData) | ||
const [activeChain, setActiveChain] = useState<ChainId | 'All'>('All') | ||
const [searchTermAssets, setSearchTermAssets] = useState<Asset[]>([]) | ||
|
||
const filteredAssets = useMemo( | ||
() => | ||
activeChain === 'All' | ||
? assets.filter(a => !isNft(a.assetId)) | ||
: assets.filter(a => a.chainId === activeChain && !isNft(a.assetId)), | ||
[activeChain, assets], | ||
) | ||
|
||
const searching = useMemo(() => searchQuery.length > 0, [searchQuery]) | ||
|
||
const handleClick = useCallback((asset: Asset) => { | ||
console.info(asset.assetId) | ||
}, []) | ||
|
||
const handleSearchQuery = useCallback((value: ChangeEvent<HTMLInputElement>) => { | ||
setSearchQuery(value.target.value) | ||
}, []) | ||
|
||
const handleChainClick = useCallback((chainId: ChainId | 'All') => { | ||
setActiveChain(chainId) | ||
}, []) | ||
|
||
const handleAllChain = useCallback(() => { | ||
setActiveChain('All') | ||
}, []) | ||
|
||
useEffect(() => { | ||
if (filteredAssets) { | ||
setSearchTermAssets( | ||
searching ? filterAssetsBySearchTerm(searchQuery, filteredAssets) : filteredAssets, | ||
) | ||
} | ||
}, [searchQuery, searching, filteredAssets]) | ||
|
||
const listAssets = searching ? searchTermAssets : filteredAssets | ||
|
||
const uniqueChainIds: ChainRow[] = assets.reduce((accumulator, currentAsset: Asset) => { | ||
const existingEntry = accumulator.find( | ||
(entry: ChainRow) => entry.chainId === currentAsset.chainId, | ||
) | ||
|
||
if (!existingEntry) { | ||
accumulator.push({ | ||
chainId: currentAsset.chainId, | ||
icon: currentAsset.icon, | ||
name: currentAsset.networkName ?? currentAsset.name, | ||
}) | ||
} | ||
|
||
return accumulator | ||
}, []) | ||
|
||
const renderRows = useMemo(() => { | ||
return <AssetList assets={listAssets} handleClick={handleClick} /> | ||
}, [handleClick, listAssets]) | ||
|
||
const renderChains = useMemo(() => { | ||
return uniqueChainIds.map(chain => ( | ||
<ChainButton | ||
key={chain.chainId} | ||
isActive={chain.chainId === activeChain} | ||
onClick={handleChainClick} | ||
{...chain} | ||
/> | ||
)) | ||
}, [activeChain, handleChainClick, uniqueChainIds]) | ||
|
||
return ( | ||
<Modal isOpen={isOpen} onClose={onClose} isCentered> | ||
<ModalOverlay /> | ||
<ModalContent> | ||
<ModalHeader | ||
display='flex' | ||
flexDir='column' | ||
gap={2} | ||
borderBottomWidth={1} | ||
borderColor='border.base' | ||
> | ||
<Flex alignItems='center' justifyContent='space-between'> | ||
<Text fontWeight='bold' fontSize='md'> | ||
Select asset | ||
</Text> | ||
<CloseButton position='relative' /> | ||
</Flex> | ||
<Input | ||
size='lg' | ||
placeholder='Search name or paste address' | ||
onChange={handleSearchQuery} | ||
/> | ||
<Flex mt={4} flexWrap='wrap' gap={2} justifyContent='space-between'> | ||
<Button | ||
size='lg' | ||
isActive={activeChain === 'All'} | ||
variant='outline' | ||
fontSize='sm' | ||
px={2} | ||
onClick={handleAllChain} | ||
> | ||
All | ||
</Button> | ||
{renderChains} | ||
</Flex> | ||
</ModalHeader> | ||
<ModalBody px={2} py={0}> | ||
{renderRows} | ||
</ModalBody> | ||
</ModalContent> | ||
</Modal> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Avatar, IconButton } from '@chakra-ui/react' | ||
import type { ChainId } from '@shapeshiftoss/caip' | ||
import { useCallback, useMemo } from 'react' | ||
export type ChainRow = { | ||
chainId: ChainId | ||
icon: string | ||
name: string | ||
} | ||
|
||
type ChainButtonProps = { | ||
onClick: (chainId: ChainId) => void | ||
isActive?: boolean | ||
} & ChainRow | ||
|
||
export const ChainButton: React.FC<ChainButtonProps> = ({ | ||
name, | ||
chainId, | ||
icon, | ||
onClick, | ||
isActive, | ||
}) => { | ||
const chainIcon = useMemo(() => { | ||
return <Avatar size='sm' src={icon} /> | ||
}, [icon]) | ||
|
||
const handleClick = useCallback(() => { | ||
onClick(chainId) | ||
}, [chainId, onClick]) | ||
|
||
return ( | ||
<IconButton | ||
isActive={isActive} | ||
variant='outline' | ||
size='lg' | ||
aria-label={name} | ||
icon={chainIcon} | ||
onClick={handleClick} | ||
/> | ||
) | ||
} |
21 changes: 21 additions & 0 deletions
21
src/components/AssetSelectModal/helpers/filterAssetsBySearchTerm.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { fromAssetId } from '@shapeshiftoss/caip' | ||
import { matchSorter } from 'match-sorter' | ||
import { isEthAddress } from 'lib/utils' | ||
import type { Asset } from 'types/Asset' | ||
|
||
export const filterAssetsBySearchTerm = (search: string, assets: Asset[]) => { | ||
if (!assets) return [] | ||
|
||
const searchLower = search.toLowerCase() | ||
|
||
if (isEthAddress(search)) { | ||
return assets.filter( | ||
asset => fromAssetId(asset?.assetId).assetReference.toLowerCase() === searchLower, | ||
) | ||
} | ||
|
||
return matchSorter(assets, search, { | ||
keys: ['name', 'symbol'], | ||
threshold: matchSorter.rankings.CONTAINS, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.