diff --git a/src/nft/components/common/Loading/LoadingSparkle.css.ts b/src/nft/components/common/Loading/LoadingSparkle.css.ts
new file mode 100644
index 0000000000..1492b967a6
--- /dev/null
+++ b/src/nft/components/common/Loading/LoadingSparkle.css.ts
@@ -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,
+ },
+ },
+})
diff --git a/src/nft/components/common/Loading/LoadingSparkle.tsx b/src/nft/components/common/Loading/LoadingSparkle.tsx
new file mode 100644
index 0000000000..eaa987d7d1
--- /dev/null
+++ b/src/nft/components/common/Loading/LoadingSparkle.tsx
@@ -0,0 +1,25 @@
+import { themeVars } from 'nft/css/sprinkles.css'
+
+import * as styles from './LoadingSparkle.css'
+
+export const LoadingSparkle = () => {
+ return (
+
+ )
+}
diff --git a/src/nft/components/sell/select/SelectPage.css.ts b/src/nft/components/sell/select/SelectPage.css.ts
new file mode 100644
index 0000000000..2e2cf0fa83
--- /dev/null
+++ b/src/nft/components/sell/select/SelectPage.css.ts
@@ -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',
+})
diff --git a/src/nft/components/sell/select/SelectPage.tsx b/src/nft/components/sell/select/SelectPage.tsx
index c9e3ff7985..1abae004c1 100644
--- a/src/nft/components/sell/select/SelectPage.tsx
+++ b/src/nft/components/sell/select/SelectPage.tsx
@@ -1,3 +1,383 @@
-export const SelectPage = () => {
- return
Select NFTs to list
+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
+
+
+
+
+
+
+
+
+
+
+ ) : null
+ }
+ dataLength={displayAssets.length}
+ style={{ overflow: 'unset' }}
+ >
+
+ {displayAssets && displayAssets.length
+ ? displayAssets.map((asset, index) => )
+ : null}
+
+
+
+
+ {sellAssets.length > 0 && (
+
+ {sellAssets.length} selected item{sellAssets.length === 1 ? '' : 's'}
+
+ Clear
+
+ setSellPageState(SellPageStateType.LISTING)}
+ lineHeight="16"
+ borderRadius="12"
+ padding="8"
+ >
+ Continue
+
+
+ )}
+
+ )
+}
+
+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 (
+
+
+
+
+
+ {asset.name ? asset.name : `#${asset.tokenId}`}
+
+
+ {asset.collection?.name}
+ {asset.collectionIsVerified ? : null}
+
+
+ Last:
+ {asset.lastPrice ? (
+ <>
+ {formatEth(asset.lastPrice)}
+ ETH
+ >
+ ) : (
+
+ —
+
+ )}
+
+
+ Floor:
+ {asset.floorPrice ? (
+ <>
+ {formatEth(asset.floorPrice)}
+ ETH
+ >
+ ) : (
+
+ —
+
+ )}
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ handleSelect()
+ }}
+ >
+ {isSelected ? 'Remove' : 'Select'}
+
+
+
+
+ )
+}
+
+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 (
+
+ {isAllSelected ? 'Deselect all' : 'Select all'}
+
+ )
+}
+
+const CollectionSearch = ({
+ searchText,
+ setSearchText,
+}: {
+ searchText: string
+ setSearchText: Dispatch>
+}) => {
+ return (
+ ) => setSearchText(e.currentTarget.value)}
+ />
+ )
}
diff --git a/src/nft/css/sprinkles.css.ts b/src/nft/css/sprinkles.css.ts
index 21abd0bf39..2d96c06127 100644
--- a/src/nft/css/sprinkles.css.ts
+++ b/src/nft/css/sprinkles.css.ts
@@ -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,
diff --git a/src/nft/pages/sell/sell.css.ts b/src/nft/pages/sell/sell.css.ts
index 9655623fcc..8300fa9553 100644
--- a/src/nft/pages/sell/sell.css.ts
+++ b/src/nft/pages/sell/sell.css.ts
@@ -24,6 +24,9 @@ export const mobileSellWrapper = style([
width: { sm: 'full', md: 'auto' },
overflowY: 'scroll',
}),
+ {
+ scrollbarWidth: 'none',
+ },
])
export const mobileSellHeader = style([