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([