-
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.
Merge pull request #27 from shapeshift/asset-selection-modal
feat: add asset select modal
- Loading branch information
Showing
16 changed files
with
121,263 additions
and
31 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 handleClick = useCallback(() => { | ||
onClick(asset) | ||
}, [asset, onClick]) | ||
|
||
const handleImageLoad = useCallback(() => { | ||
setImgLoaded(true) | ||
}, []) | ||
|
||
if (!asset) return null | ||
|
||
return ( | ||
<Button | ||
width='full' | ||
variant='ghost' | ||
onClick={handleClick} | ||
justifyContent='space-between' | ||
_focus={focus} | ||
height='auto' | ||
py={4} | ||
> | ||
<Flex gap={4} alignItems='center'> | ||
<SkeletonCircle isLoaded={imgLoaded}> | ||
<Avatar src={icon} size='sm' onLoad={handleImageLoad} /> | ||
</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,159 @@ | ||
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, useRef, 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 | ||
onClick: (asset: Asset) => void | ||
} | ||
|
||
export const AssetSelectModal: React.FC<AssetSelectModalProps> = ({ isOpen, onClose, onClick }) => { | ||
const [searchQuery, setSearchQuery] = useState('') | ||
const assets = useMemo(() => Object.values(AssetData), []) | ||
const [activeChain, setActiveChain] = useState<ChainId | 'All'>('All') | ||
const [searchTermAssets, setSearchTermAssets] = useState<Asset[]>([]) | ||
const iniitalRef = useRef(null) | ||
|
||
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 handleSearchQuery = useCallback((value: ChangeEvent<HTMLInputElement>) => { | ||
setSearchQuery(value.target.value) | ||
}, []) | ||
|
||
const handleChainClick = useCallback((chainId: ChainId | 'All') => { | ||
setActiveChain(chainId) | ||
}, []) | ||
|
||
const handleAllClick = useCallback(() => { | ||
setActiveChain('All') | ||
}, []) | ||
|
||
const handleClose = useCallback(() => { | ||
// Reset state on close | ||
setActiveChain('All') | ||
setSearchQuery('') | ||
onClose() | ||
}, [onClose]) | ||
|
||
const handleClick = useCallback( | ||
(asset: Asset) => { | ||
onClick(asset) | ||
handleClose() | ||
}, | ||
[handleClose, onClick], | ||
) | ||
|
||
useEffect(() => { | ||
if (filteredAssets && searching) { | ||
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.networkIcon ?? 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={handleClose} isCentered initialFocusRef={iniitalRef}> | ||
<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' | ||
ref={iniitalRef} | ||
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={handleAllClick} | ||
> | ||
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='xs' 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export enum AssetType { | ||
BUY = 'buy', | ||
SELL = 'sell', | ||
} |
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 |
---|---|---|
@@ -1,23 +1,20 @@ | ||
import { Avatar, Button, Text } from '@chakra-ui/react' | ||
import type { Asset } from 'types/Asset' | ||
|
||
type AssetSelectionProps = { | ||
onClick: () => void | ||
label: string | ||
assetIcon: string | ||
assetName: string | ||
asset?: Asset | ||
} | ||
|
||
export const AssetSelection: React.FC<AssetSelectionProps> = ({ | ||
label, | ||
onClick, | ||
assetIcon, | ||
assetName, | ||
}) => { | ||
export const AssetSelection: React.FC<AssetSelectionProps> = ({ label, onClick, asset }) => { | ||
return ( | ||
<Button flexDir='column' height='auto' py={4} gap={4} flex={1} onClick={onClick}> | ||
<Text color='text.subtle'>{label}</Text> | ||
<Avatar src={assetIcon} /> | ||
<Text>{assetName}</Text> | ||
<Avatar src={asset ? asset.icon : ''} /> | ||
<Text textOverflow='ellipsis' overflow='hidden' width='full'> | ||
{asset ? asset.name : 'Select Asset'} | ||
</Text> | ||
</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
Oops, something went wrong.