feat: migrate select page assets (#4618)

* add main nft sell page

* remove background

* more precise naming

* Add wallet assets for select page

* update styles while without filter bar

* remove unnecessary useeffect

* deprecate old stlye

* move to common props

* add round helper fn

* use react router link

Co-authored-by: Charlie <charlie@uniswap.org>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2022-09-14 11:07:58 -07:00 committed by GitHub
parent 86f3b5a036
commit c38b5c0ce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 505 additions and 3 deletions

@ -0,0 +1,32 @@
import { keyframes, style } from '@vanilla-extract/css'
const pathAnim = keyframes({
'0%': {
opacity: '0.2',
},
'100%': {
opacity: '1',
},
})
const pathAnimCommonProps = {
animationDirection: 'alternate',
animationTimingFunction: 'linear',
animation: `0.5s infinite ${pathAnim}`,
}
export const path = style({
selectors: {
'&:nth-child(1)': {
...pathAnimCommonProps,
},
'&:nth-child(2)': {
animationDelay: '0.1s',
...pathAnimCommonProps,
},
'&:nth-child(3)': {
animationDelay: '0.2s',
...pathAnimCommonProps,
},
},
})

@ -0,0 +1,25 @@
import { themeVars } from 'nft/css/sprinkles.css'
import * as styles from './LoadingSparkle.css'
export const LoadingSparkle = () => {
return (
<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.8281 10.834C17.6159 9.88757 17.4333 9.13689 17.291 8.58997C17.1751 8.11971 16.9497 7.68339 16.6331 7.31649C16.2834 6.97574 15.8515 6.731 15.3791 6.60601C14.8206 6.43175 14.0365 6.24408 13.0457 6.04301C12.7771 5.98134 12.6321 5.83926 12.6321 5.61405C12.6286 5.56049 12.6369 5.50679 12.6564 5.45676C12.6758 5.40674 12.7061 5.36159 12.7449 5.32448C12.8303 5.25054 12.9343 5.20142 13.0457 5.18241C13.8337 5.03623 14.6127 4.84557 15.3791 4.61136C15.8508 4.48054 16.282 4.23345 16.6331 3.89283C16.9497 3.52593 17.1751 3.08961 17.291 2.61935C17.4468 2.06885 17.6258 1.32623 17.8281 0.39145C17.8791 0.123349 18.016 0 18.2416 0C18.4671 0 18.6095 0.131392 18.6739 0.39145C18.8726 1.32623 19.0516 2.06885 19.211 2.61935C19.3304 3.08988 19.5585 3.52599 19.8769 3.89283C20.229 4.23347 20.6611 4.48052 21.1336 4.61136C21.8997 4.84657 22.6788 5.03724 23.467 5.18241C23.576 5.18865 23.6782 5.23739 23.7515 5.31813C23.8249 5.39887 23.8635 5.50515 23.8591 5.61405C23.8591 5.83926 23.7275 5.98134 23.467 6.04301C22.6802 6.19136 21.9014 6.37926 21.1336 6.60601C20.6605 6.73103 20.2276 6.97574 19.8769 7.31649C19.5585 7.68333 19.3304 8.11943 19.211 8.58997C19.0472 9.12617 18.8672 9.88757 18.6739 10.834C18.6095 11.1021 18.4671 11.2281 18.2416 11.2281C18.016 11.2281 17.8737 11.0967 17.8281 10.834Z"
fill={themeVars.colors.darkGray}
className={styles.path}
/>
<path
d="M32.4448 19.8364C32.2192 18.6679 32.0079 17.7366 31.8166 17.0452C31.6743 16.4509 31.3902 15.8999 30.9885 15.4396C30.5445 15.0113 29.9992 14.7027 29.4037 14.5425C28.4447 14.2629 27.4703 14.0397 26.4853 13.874C26.1398 13.8226 25.9656 13.6426 25.9656 13.3369C25.9618 13.2674 25.9723 13.1978 25.9964 13.1324C26.0204 13.0671 26.0576 13.0074 26.1055 12.9569C26.2105 12.8597 26.3433 12.7977 26.4853 12.7798C27.4694 12.6134 28.4437 12.3931 29.4037 12.1198C30.0021 11.9667 30.549 11.6571 30.9885 11.2228C31.389 10.7583 31.6729 10.2049 31.8166 9.60859C32.0079 8.91721 32.2192 7.98584 32.4448 6.81449C32.4458 6.74608 32.4607 6.67858 32.4886 6.61609C32.5164 6.5536 32.5567 6.49743 32.6068 6.45096C32.657 6.40448 32.7161 6.3687 32.7806 6.34574C32.845 6.32278 32.9134 6.31313 32.9816 6.31739C33.2672 6.31739 33.4528 6.48309 33.5213 6.81449C33.7479 7.98584 33.9573 8.91721 34.1495 9.60859C34.2907 10.2036 34.5706 10.7567 34.9662 11.2228C35.404 11.6593 35.9516 11.9693 36.551 12.1198C37.515 12.3926 38.493 12.6129 39.4808 12.7798C39.5532 12.7803 39.6248 12.7958 39.6909 12.8254C39.757 12.855 39.8162 12.8981 39.8648 12.9518C39.9134 13.0055 39.9503 13.0687 39.9732 13.1375C39.9961 13.2063 40.0044 13.279 39.9977 13.3512C39.9977 13.6569 39.8263 13.8369 39.4808 13.8883C38.4922 14.0545 37.514 14.2778 36.551 14.5568C35.9543 14.7142 35.4084 15.0232 34.9662 15.4539C34.5695 15.916 34.2894 16.4666 34.1495 17.0595C33.9592 17.7509 33.7498 18.6813 33.5213 19.8507C33.4528 20.185 33.2758 20.3507 32.9816 20.3507C32.912 20.3546 32.8424 20.3442 32.777 20.32C32.7116 20.2958 32.6519 20.2585 32.6015 20.2103C32.5512 20.1621 32.5113 20.104 32.4843 20.0396C32.4573 19.9753 32.4439 19.9062 32.4448 19.8364Z"
fill={themeVars.colors.darkGray}
className={styles.path}
/>
<path
d="M12.6196 40.6995C12.5109 40.7023 12.4027 40.6829 12.3013 40.6422C12.2 40.6016 12.1075 40.5405 12.0294 40.4627C11.8673 40.3009 11.7574 40.0919 11.7145 39.8639C11.418 38.0492 11.1251 36.5322 10.8358 35.3128C10.6133 34.2663 10.2461 33.258 9.74531 32.3187C9.32818 31.5649 8.73049 30.9334 8.00907 30.4841C7.132 29.9745 6.18128 29.6124 5.19297 29.4117C4.03371 29.1449 2.58684 28.8918 0.852359 28.6523C0.616735 28.6254 0.397657 28.5147 0.23303 28.3393C0.0804808 28.1721 -0.00302245 27.9506 0.000117795 27.7214C-0.00358202 27.4921 0.0799998 27.2703 0.23303 27.1035C0.396979 26.9271 0.616416 26.8162 0.852359 26.7905C2.5886 26.5618 4.03812 26.3114 5.20091 26.0392C6.20136 25.8286 7.16449 25.4614 8.05671 24.9505C8.7902 24.5064 9.40286 23.8792 9.83794 23.1268C10.3473 22.1993 10.7151 21.1968 10.9284 20.1545C11.2054 18.9369 11.4701 17.4162 11.7224 15.5925C11.7547 15.3608 11.8619 15.147 12.0268 14.9855C12.1058 14.9052 12.1997 14.8418 12.3028 14.7993C12.406 14.7567 12.5164 14.7359 12.6276 14.7378C12.7392 14.7338 12.8506 14.7526 12.9551 14.7933C13.0596 14.834 13.1552 14.8956 13.2363 14.9747C13.398 15.1367 13.5079 15.3456 13.5513 15.5735C13.8583 17.3972 14.1565 18.9178 14.4459 20.1354C14.6712 21.1811 15.0382 22.189 15.5363 23.1295C15.9558 23.8807 16.5531 24.5108 17.2726 24.9614C18.1474 25.4733 19.095 25.8407 20.0807 26.0501C21.2347 26.3223 22.6789 26.5727 24.4134 26.8014C24.6493 26.8271 24.8688 26.938 25.0327 27.1144C25.185 27.2815 25.2677 27.5033 25.263 27.7323C25.2662 27.9634 25.1839 28.1872 25.0327 28.3583C24.8722 28.5382 24.6503 28.6474 24.4134 28.6632C22.9509 28.7659 21.4954 28.9559 20.0542 29.232C19.046 29.4136 18.075 29.7698 17.1826 30.2854C16.4365 30.7497 15.8216 31.4065 15.3987 32.1908C14.8949 33.1535 14.531 34.187 14.3188 35.2584C14.04 36.5068 13.7842 38.0429 13.5513 39.8666C13.5238 40.1013 13.4192 40.3193 13.2548 40.4845C13.1681 40.5618 13.0673 40.6207 12.9583 40.6576C12.8492 40.6945 12.7341 40.7087 12.6196 40.6995Z"
fill={themeVars.colors.darkGray}
className={styles.path}
/>
</svg>
)
}

@ -0,0 +1,62 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, vars } from '../../../css/sprinkles.css'
export const section = style([
sprinkles({
paddingLeft: { sm: '16', lg: '0' },
paddingRight: { sm: '16', lg: '0' },
}),
{ maxWidth: '1000px', margin: '0 auto' },
])
export const filterRowIcon = sprinkles({ color: { default: 'darkGray', hover: 'blue' } })
export const buttonSelected = style({
border: `2px solid ${vars.color.genieBlue}`,
})
export const ethIcon = style({
marginBottom: '-3px',
})
export const collectionName = style([
sprinkles({
fontWeight: 'normal',
overflow: 'hidden',
paddingRight: '14',
}),
{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '180px',
},
])
export const verifiedBadge = style({
height: '12px',
width: '12px',
marginLeft: '2px',
marginBottom: '-2px',
boxSizing: 'border-box',
})
/* From [contractAddress] */
export const dropDown = style({
width: '190px',
})
export const activeDropDown = style({
boxShadow: vars.color.dropShadow,
})
export const activeDropDownItems = style({
boxShadow: '0 14px 16px 0 rgba(70, 115, 250, 0.4)',
})
export const collectionFilterBubbleText = style({
whiteSpace: 'nowrap',
maxWidth: '100px',
textOverflow: 'ellipsis',
overflow: 'hidden',
})

@ -1,3 +1,383 @@
export const SelectPage = () => {
return <div>Select NFTs to list</div>
import clsx from 'clsx'
import { AnimatedBox, Box } from 'nft/components/Box'
import { assetList } from 'nft/components/collection/CollectionNfts.css'
import { LoadingSparkle } from 'nft/components/common/Loading/LoadingSparkle'
import { Center, Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import { subhead, subheadSmall } from 'nft/css/common.css'
import { useBag, useIsMobile, useSellAsset, useSellPageState, useWalletBalance, useWalletCollections } from 'nft/hooks'
import { fetchMultipleCollectionStats, fetchWalletAssets, OSCollectionsFetcher } from 'nft/queries'
import { SellPageStateType, WalletAsset } from 'nft/types'
import { Dispatch, FormEvent, SetStateAction, useEffect, useMemo, useReducer, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useInfiniteQuery, useQuery } from 'react-query'
import { Link } from 'react-router-dom'
import * as styles from './SelectPage.css'
const formatEth = (price: number) => {
if (price > 1000000) {
return `${Math.round(price / 1000000)}M`
} else if (price > 1000) {
return `${Math.round(price / 1000)}K`
} else {
return `${Math.round(price * 100 + Number.EPSILON) / 100}`
}
}
function roundFloorPrice(price?: number, n?: number) {
return price ? Math.round(price * Math.pow(10, n ?? 3) + Number.EPSILON) / Math.pow(10, n ?? 3) : 0
}
export const SelectPage = () => {
const { address } = useWalletBalance()
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
const { data: ownerCollections } = useQuery(
['ownerCollections', address],
() => OSCollectionsFetcher({ params: { asset_owner: address, offset: '0', limit: '300' } }),
{
refetchOnWindowFocus: false,
}
)
const ownerCollectionsAddresses = useMemo(() => ownerCollections?.map(({ address }) => address), [ownerCollections])
const { data: collectionStats } = useQuery(
['ownerCollectionStats', ownerCollectionsAddresses],
() => fetchMultipleCollectionStats({ addresses: ownerCollectionsAddresses ?? [] }),
{
refetchOnWindowFocus: false,
}
)
const {
data: ownerAssetsData,
fetchNextPage,
hasNextPage,
isSuccess,
} = useInfiniteQuery(
['ownerAssets', address, collectionFilters],
async ({ pageParam = 0 }) => {
return await fetchWalletAssets({
ownerAddress: address ?? '',
collectionAddresses: collectionFilters,
pageParam,
})
},
{
getNextPageParam: (lastPage, pages) => {
return lastPage?.flat().length === 25 ? pages.length : null
},
refetchOnWindowFocus: false,
refetchOnMount: false,
}
)
const ownerAssets = useMemo(() => (isSuccess ? ownerAssetsData?.pages.flat() : null), [isSuccess, ownerAssetsData])
const walletAssets = useWalletCollections((state) => state.walletAssets)
const setWalletAssets = useWalletCollections((state) => state.setWalletAssets)
const displayAssets = useWalletCollections((state) => state.displayAssets)
const setDisplayAssets = useWalletCollections((state) => state.setDisplayAssets)
const setWalletCollections = useWalletCollections((state) => state.setWalletCollections)
const sellAssets = useSellAsset((state) => state.sellAssets)
const reset = useSellAsset((state) => state.reset)
const setSellPageState = useSellPageState((state) => state.setSellPageState)
const [searchText, setSearchText] = useState('')
useEffect(() => {
setWalletAssets(ownerAssets?.flat() ?? [])
}, [ownerAssets, setWalletAssets])
useEffect(() => {
ownerCollections && setWalletCollections(ownerCollections)
}, [ownerCollections, setWalletCollections])
const listFilter = useWalletCollections((state) => state.listFilter)
useEffect(() => {
if (searchText) {
const filtered = walletAssets.filter((asset) => asset.name?.toLowerCase().includes(searchText.toLowerCase()))
setDisplayAssets(filtered, listFilter)
} else {
setDisplayAssets(walletAssets, listFilter)
}
}, [searchText, walletAssets, listFilter, setDisplayAssets])
useEffect(() => {
if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) {
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.floorPrice
collection.floorPrice = roundFloorPrice(floorPrice)
}
setWalletCollections(ownerCollectionsCopy)
}
}, [collectionStats, ownerCollections, setWalletCollections])
return (
// Column style is temporary while we move over the filters bar that adjust width
<Column style={{ width: 'calc(100vw - 32px)' }}>
<Row
alignItems="flex-start"
position="relative"
paddingLeft={{ sm: '0', md: '52' }}
paddingRight={{ sm: '0', md: '72' }}
paddingTop={{ sm: '16', md: '40' }}
>
<AnimatedBox paddingX="16" flexShrink="0" width="full">
<Row gap="8" flexWrap="nowrap">
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
<SelectAllButton />
</Row>
<InfiniteScroll
next={fetchNextPage}
hasMore={hasNextPage ?? false}
loader={
hasNextPage ? (
<Center>
<LoadingSparkle />
</Center>
) : null
}
dataLength={displayAssets.length}
style={{ overflow: 'unset' }}
>
<div className={assetList}>
{displayAssets && displayAssets.length
? displayAssets.map((asset, index) => <WalletAssetDisplay asset={asset} key={index} />)
: null}
</div>
</InfiniteScroll>
</AnimatedBox>
</Row>
{sellAssets.length > 0 && (
<Row
display={{ sm: 'flex', md: 'none' }}
position="fixed"
bottom="60"
left="16"
height="56"
borderRadius="12"
paddingX="16"
paddingY="12"
style={{ background: '#0d0e0ef2', width: 'calc(100% - 32px)', lineHeight: '24px' }}
className={subhead}
>
{sellAssets.length}&nbsp; selected item{sellAssets.length === 1 ? '' : 's'}
<Box
fontWeight="semibold"
fontSize="14"
cursor="pointer"
color="genieBlue"
marginRight="20"
marginLeft="auto"
onClick={reset}
lineHeight="16"
>
Clear
</Box>
<Box
marginRight="0"
fontWeight="medium"
fontSize="14"
cursor="pointer"
backgroundColor="genieBlue"
onClick={() => setSellPageState(SellPageStateType.LISTING)}
lineHeight="16"
borderRadius="12"
padding="8"
>
Continue
</Box>
</Row>
)}
</Column>
)
}
export const WalletAssetDisplay = ({ asset }: { asset: WalletAsset }) => {
const sellAssets = useSellAsset((state) => state.sellAssets)
const selectSellAsset = useSellAsset((state) => state.selectSellAsset)
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
const cartExpanded = useBag((state) => state.bagExpanded)
const toggleCart = useBag((state) => state.toggleBag)
const isMobile = useIsMobile()
const [boxHovered, toggleBoxHovered] = useReducer((state) => {
return !state
}, false)
const [buttonHovered, toggleButtonHovered] = useReducer((state) => {
return !state
}, false)
const isSelected = useMemo(() => {
return sellAssets.some((item) => asset.id === item.id)
}, [asset, sellAssets])
const handleSelect = () => {
isSelected ? removeSellAsset(asset) : selectSellAsset(asset)
if (
!cartExpanded &&
!sellAssets.find(
(x) => x.tokenId === asset.tokenId && x.asset_contract.address === asset.asset_contract.address
) &&
!isMobile
)
toggleCart()
}
return (
<Link
to={`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}?origin=sell`}
style={{ textDecoration: 'none' }}
>
<Column
color={'blackBlue'}
className={subheadSmall}
onMouseEnter={toggleBoxHovered}
onMouseLeave={toggleBoxHovered}
>
<Box
as="img"
alt={asset.name}
width="full"
borderTopLeftRadius="20"
borderTopRightRadius="20"
src={asset.image_url || '/nft/svgs/image-placeholder.svg'}
style={{ aspectRatio: '1' }}
/>
<Column
position="relative"
borderBottomLeftRadius="20"
borderBottomRightRadius="20"
transition="250"
backgroundColor={boxHovered ? 'medGray' : 'lightGray'}
paddingY="12"
paddingX="12"
>
<Box className={subheadSmall} overflow="hidden" textOverflow="ellipsis" marginTop="4" lineHeight="20">
{asset.name ? asset.name : `#${asset.tokenId}`}
</Box>
<Box fontSize="12" marginTop="4" lineHeight="16" overflow="hidden" textOverflow="ellipsis">
{asset.collection?.name}
{asset.collectionIsVerified ? <VerifiedIcon className={styles.verifiedBadge} /> : null}
</Box>
<Box as="span" fontSize="12" lineHeight="16" color="darkGray" marginTop="8">
Last:&nbsp;
{asset.lastPrice ? (
<>
{formatEth(asset.lastPrice)}
&nbsp;ETH
</>
) : (
<Box as="span" marginLeft="6">
&mdash;
</Box>
)}
</Box>
<Box as="span" fontSize="12" lineHeight="16" color="darkGray" marginTop="4">
Floor:&nbsp;
{asset.floorPrice ? (
<>
{formatEth(asset.floorPrice)}
&nbsp;ETH
</>
) : (
<Box as="span" marginLeft="8">
&mdash;
</Box>
)}
</Box>
<Box
marginTop="12"
textAlign="center"
width="full"
borderRadius="12"
paddingY="8"
transition="250"
color={buttonHovered ? 'blackBlue' : isSelected ? 'red400' : 'genieBlue'}
backgroundColor={buttonHovered ? (isSelected ? 'red400' : 'genieBlue') : 'lightGray'}
className={subheadSmall}
onMouseEnter={toggleButtonHovered}
onMouseLeave={toggleButtonHovered}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect()
}}
>
{isSelected ? 'Remove' : 'Select'}
</Box>
</Column>
</Column>
</Link>
)
}
const SelectAllButton = () => {
const [isAllSelected, setIsAllSelected] = useState(false)
const displayAssets = useWalletCollections((state) => state.displayAssets)
const selectSellAsset = useSellAsset((state) => state.selectSellAsset)
const resetSellAssets = useSellAsset((state) => state.reset)
useEffect(() => {
if (!isAllSelected) resetSellAssets()
if (isAllSelected) {
displayAssets.forEach((asset) => selectSellAsset(asset))
}
}, [displayAssets, isAllSelected, resetSellAssets, selectSellAsset])
const toggleAllSelected = () => {
setIsAllSelected(!isAllSelected)
}
return (
<Box
display="flex"
flexShrink="0"
flexDirection="row"
alignItems="center"
marginLeft={{ sm: '8', md: 'auto' }}
borderRadius="12"
backgroundColor="medGray"
fontWeight="medium"
height="44"
paddingTop="12"
paddingBottom="12"
paddingRight="16"
paddingLeft="16"
cursor="pointer"
color="blackBlue"
onClick={toggleAllSelected}
className={clsx(`${subheadSmall} ${isAllSelected ? styles.buttonSelected : null}`)}
>
{isAllSelected ? 'Deselect all' : 'Select all'}
</Box>
)
}
const CollectionSearch = ({
searchText,
setSearchText,
}: {
searchText: string
setSearchText: Dispatch<SetStateAction<string>>
}) => {
return (
<Box
as="input"
borderColor={{ default: 'medGray', focus: 'genieBlue' }}
borderWidth="1px"
borderStyle="solid"
borderRadius="8"
padding="12"
backgroundColor="white"
fontSize="14"
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
placeholder="Search by name"
value={searchText}
width="full"
onChange={(e: FormEvent<HTMLInputElement>) => setSearchText(e.currentTarget.value)}
/>
)
}

@ -359,7 +359,7 @@ const unresponsiveProperties = defineProperties({
overflowX: overflow,
overflowY: overflow,
boxShadow: { ...themeVars.shadows, none: 'none', dropShadow: vars.color.dropShadow },
lineHeight: ['1', 'auto'],
lineHeight: { '1': '1', auto: 'auto', '16': '16px', '20': '20px', '28': '28px', '36': '36px' },
transition: vars.time,
transitionDuration: vars.time,
animationDuration: vars.time,

@ -24,6 +24,9 @@ export const mobileSellWrapper = style([
width: { sm: 'full', md: 'auto' },
overflowY: 'scroll',
}),
{
scrollbarWidth: 'none',
},
])
export const mobileSellHeader = style([