Commit 7bf74102 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: profile listing sidebar (#4809)

* Add sell header to Bag

* split bag content to its own file

* empty tag state

* continue button

* file re-arranging and add profile select row content

* update padding

* better null check
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 0017e2fc
...@@ -10,7 +10,6 @@ import { useLocation } from 'react-router-dom' ...@@ -10,7 +10,6 @@ import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks' import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
const Cart = lazy(() => import('nft/components/profile/modal/ListingTag'))
const Bag = lazy(() => import('nft/components/bag/Bag')) const Bag = lazy(() => import('nft/components/bag/Bag'))
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal')) const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
...@@ -30,7 +29,6 @@ export default function TopLevelModals() { ...@@ -30,7 +29,6 @@ export default function TopLevelModals() {
<ConnectedAccountBlocked account={account} isOpen={open} /> <ConnectedAccountBlocked account={account} isOpen={open} />
{useTokensFlag() === TokensVariant.Enabled && {useTokensFlag() === TokensVariant.Enabled &&
(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />} (location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
<Cart />
<Bag /> <Bag />
{useNftFlag() === NftVariant.Enabled && <TransactionCompleteModal />} {useNftFlag() === NftVariant.Enabled && <TransactionCompleteModal />}
</> </>
......
import { style } from '@vanilla-extract/css' import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css' import { breakpoints, sprinkles } from 'nft/css/sprinkles.css'
import { breakpoints, sprinkles, vars } from 'nft/css/sprinkles.css'
export const bagContainer = style([ export const bagContainer = style([
sprinkles({ sprinkles({
...@@ -35,27 +34,3 @@ export const assetsContainer = style([ ...@@ -35,27 +34,3 @@ export const assetsContainer = style([
scrollbarWidth: 'none', scrollbarWidth: 'none',
}, },
]) ])
export const header = style([
subhead,
sprinkles({
color: 'textPrimary',
justifyContent: 'space-between',
}),
{
lineHeight: '24px',
},
])
export const clearAll = style([
sprinkles({
color: 'textTertiary',
cursor: 'pointer',
fontWeight: 'semibold',
}),
{
':hover': {
color: vars.color.blue400,
},
},
])
...@@ -3,21 +3,28 @@ import { formatEther } from '@ethersproject/units' ...@@ -3,21 +3,28 @@ import { formatEther } from '@ethersproject/units'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { parseEther } from 'ethers/lib/utils' import { parseEther } from 'ethers/lib/utils'
import { BagFooter } from 'nft/components/bag/BagFooter' import { BagFooter } from 'nft/components/bag/BagFooter'
import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow' import ListingModal from 'nft/components/bag/profile/ListingModal'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal' import { Portal } from 'nft/components/common/Portal'
import { Center, Column, Row } from 'nft/components/Flex' import { Center, Column } from 'nft/components/Flex'
import { BagCloseIcon, LargeBagIcon } from 'nft/components/icons' import { LargeBagIcon, LargeTagIcon } from 'nft/components/icons'
import { Overlay } from 'nft/components/modals/Overlay' import { Overlay } from 'nft/components/modals/Overlay'
import { subhead } from 'nft/css/common.css' import { buttonTextMedium, commonButtonStyles, subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useSendTransaction, useTransactionResponse, useWalletBalance } from 'nft/hooks' import {
useBag,
useIsMobile,
useProfilePageState,
useSellAsset,
useSendTransaction,
useTransactionResponse,
useWalletBalance,
} from 'nft/hooks'
import { fetchRoute } from 'nft/queries' import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, RouteResponse, TxStateType } from 'nft/types' import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import { buildSellObject } from 'nft/utils/buildSellObject' import { buildSellObject } from 'nft/utils/buildSellObject'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice' import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice' import { fetchPrice } from 'nft/utils/fetchPrice'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute' import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { sortUpdatedAssets } from 'nft/utils/updatedAssets' import { sortUpdatedAssets } from 'nft/utils/updatedAssets'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
...@@ -25,14 +32,27 @@ import { useQuery, useQueryClient } from 'react-query' ...@@ -25,14 +32,27 @@ import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import * as styles from './Bag.css' import * as styles from './Bag.css'
import { BagContent } from './BagContent'
import { BagHeader } from './BagHeader'
import { ProfileBagContent } from './profile/ProfileBagContent'
const EmptyState = () => { const EmptyState = () => {
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/profile')
return ( return (
<Center height="full"> <Center height="full">
<Column gap="12"> <Column gap={isProfilePage ? '16' : '12'}>
<Center> <Center>
{isProfilePage ? (
<LargeTagIcon color={themeVars.colors.textTertiary} />
) : (
<LargeBagIcon color={themeVars.colors.textTertiary} /> <LargeBagIcon color={themeVars.colors.textTertiary} />
)}
</Center> </Center>
{isProfilePage ? (
<span className={subhead}>No NFTs Selected</span>
) : (
<Column gap="16"> <Column gap="16">
<Center className={subhead} style={{ lineHeight: '24px' }}> <Center className={subhead} style={{ lineHeight: '24px' }}>
Your bag is empty Your bag is empty
...@@ -41,6 +61,7 @@ const EmptyState = () => { ...@@ -41,6 +61,7 @@ const EmptyState = () => {
Selected NFTs will appear here Selected NFTs will appear here
</Center> </Center>
</Column> </Column>
)}
</Column> </Column>
</Center> </Center>
) )
...@@ -64,52 +85,21 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => ( ...@@ -64,52 +85,21 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
/> />
) )
interface BagHeaderProps {
numberOfAssets: number
toggleBag: () => void
resetFlow: () => void
}
const BagHeader = ({ numberOfAssets, toggleBag, resetFlow }: BagHeaderProps) => {
return (
<Column gap="4" paddingX="32" marginBottom="20">
<Row className={styles.header}>
My bag
<Box display="flex" padding="2" color="textSecondary" cursor="pointer" onClick={toggleBag}>
<BagCloseIcon />
</Box>
</Row>
{numberOfAssets > 0 && (
<Box fontSize="14" fontWeight="normal" style={{ lineHeight: '20px' }} color="textPrimary">
{roundAndPluralize(numberOfAssets, 'NFT')} ·{' '}
<Box
as="span"
className={styles.clearAll}
onClick={() => {
resetFlow()
}}
>
Clear all
</Box>
</Box>
)}
</Column>
)
}
const Bag = () => { const Bag = () => {
const { account } = useWeb3React() const { account } = useWeb3React()
const bagStatus = useBag((s) => s.bagStatus) const bagStatus = useBag((s) => s.bagStatus)
const setBagStatus = useBag((s) => s.setBagStatus) const setBagStatus = useBag((s) => s.setBagStatus)
const markAssetAsReviewed = useBag((s) => s.markAssetAsReviewed)
const didOpenUnavailableAssets = useBag((s) => s.didOpenUnavailableAssets) const didOpenUnavailableAssets = useBag((s) => s.didOpenUnavailableAssets)
const setDidOpenUnavailableAssets = useBag((s) => s.setDidOpenUnavailableAssets) const setDidOpenUnavailableAssets = useBag((s) => s.setDidOpenUnavailableAssets)
const bagIsLocked = useBag((s) => s.isLocked) const bagIsLocked = useBag((s) => s.isLocked)
const setLocked = useBag((s) => s.setLocked) const setLocked = useBag((s) => s.setLocked)
const reset = useBag((s) => s.reset) const reset = useBag((s) => s.reset)
const resetSellAssets = useSellAsset((state) => state.reset)
const sellAssets = useSellAsset((state) => state.sellAssets)
const setProfilePageState = useProfilePageState((state) => state.setProfilePageState)
const profilePageState = useProfilePageState((state) => state.state)
const uncheckedItemsInBag = useBag((s) => s.itemsInBag) const uncheckedItemsInBag = useBag((s) => s.itemsInBag)
const setItemsInBag = useBag((s) => s.setItemsInBag) const setItemsInBag = useBag((s) => s.setItemsInBag)
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
const bagExpanded = useBag((s) => s.bagExpanded) const bagExpanded = useBag((s) => s.bagExpanded)
const toggleBag = useBag((s) => s.toggleBag) const toggleBag = useBag((s) => s.toggleBag)
const setTotalEthPrice = useBag((s) => s.setTotalEthPrice) const setTotalEthPrice = useBag((s) => s.setTotalEthPrice)
...@@ -121,7 +111,7 @@ const Bag = () => { ...@@ -121,7 +111,7 @@ const Bag = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/profile') const isProfilePage = pathname.startsWith('/profile')
const isNFTPage = pathname.startsWith('/nfts') const isNFTPage = pathname.startsWith('/nfts')
const shouldShowBag = isNFTPage && !isProfilePage const shouldShowBag = isNFTPage || isProfilePage
const isMobile = useIsMobile() const isMobile = useIsMobile()
const sendTransaction = useSendTransaction((state) => state.sendTransaction) const sendTransaction = useSendTransaction((state) => state.sendTransaction)
...@@ -255,31 +245,6 @@ const Bag = () => { ...@@ -255,31 +245,6 @@ const Bag = () => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state)) useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, []) }, [])
const { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems } = useMemo(() => {
const unchangedAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.ADDED_TO_BAG || item.status === BagItemStatus.REVIEWED)
.map((item) => item.asset)
const priceChangedAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)
.map((item) => item.asset)
const unavailableAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.UNAVAILABLE)
.map((item) => item.asset)
const availableItems = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE)
return { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems }
}, [itemsInBag])
useEffect(() => {
const hasAssetsInReview = priceChangedAssets.length > 0
const hasAssets = itemsInBag.length > 0
if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) {
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG)
}
}, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus])
useEffect(() => { useEffect(() => {
if (bagIsLocked && !isOpen) setModalIsOpen(true) if (bagIsLocked && !isOpen) setModalIsOpen(true)
}, [bagIsLocked, isOpen]) }, [bagIsLocked, isOpen])
...@@ -307,7 +272,7 @@ const Bag = () => { ...@@ -307,7 +272,7 @@ const Bag = () => {
setTotalUsdPrice(totalUsdPrice) setTotalUsdPrice(totalUsdPrice)
}, [totalEthPrice, totalUsdPrice, setTotalEthPrice, setTotalUsdPrice]) }, [totalEthPrice, totalUsdPrice, setTotalEthPrice, setTotalUsdPrice])
const hasAssetsToShow = itemsInBag.length > 0 || unavailableAssets.length > 0 const hasAssetsToShow = itemsInBag.length > 0
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => { const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
const scrollTop = event.currentTarget.scrollTop const scrollTop = event.currentTarget.scrollTop
...@@ -322,47 +287,22 @@ const Bag = () => { ...@@ -322,47 +287,22 @@ const Bag = () => {
{bagExpanded && shouldShowBag ? ( {bagExpanded && shouldShowBag ? (
<Portal> <Portal>
<Column zIndex={isMobile || isOpen ? 'modal' : '3'} className={styles.bagContainer}> <Column zIndex={isMobile || isOpen ? 'modal' : '3'} className={styles.bagContainer}>
<BagHeader numberOfAssets={itemsInBag.length} toggleBag={toggleBag} resetFlow={reset} /> {!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
{itemsInBag.length === 0 && bagStatus === BagStatus.ADDING_TO_BAG && <EmptyState />} <>
<BagHeader
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
toggleBag={toggleBag}
resetFlow={isProfilePage ? resetSellAssets : reset}
isProfilePage={isProfilePage}
/>
{(!isProfilePage && itemsInBag.length === 0 && bagStatus === BagStatus.ADDING_TO_BAG) ||
(isProfilePage && sellAssets.length === 0 && <EmptyState />)}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} /> <ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12"> <Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
<Column display={priceChangedAssets.length > 0 || unavailableAssets.length > 0 ? 'flex' : 'none'}> {isProfilePage ? <ProfileBagContent /> : <BagContent />}
{unavailableAssets.length > 0 && (
<UnavailableAssetsHeaderRow
assets={unavailableAssets}
usdPrice={fetchedPriceData}
clearUnavailableAssets={() => setItemsInBag(availableItems)}
didOpenUnavailableAssets={didOpenUnavailableAssets}
setDidOpenUnavailableAssets={setDidOpenUnavailableAssets}
isMobile={isMobile}
/>
)}
{priceChangedAssets.map((asset, index) => (
<PriceChangeBagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
markAssetAsReviewed={markAssetAsReviewed}
top={index === 0 && unavailableAssets.length === 0}
isMobile={isMobile}
/>
))}
</Column>
<Column gap="8">
{unchangedAssets.map((asset) => (
<BagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
removeAsset={removeAssetFromBag}
showRemove={true}
isMobile={isMobile}
/>
))}
</Column>
</Column> </Column>
<ScrollingIndicator show={userCanScroll && scrollProgress < 100} /> <ScrollingIndicator show={userCanScroll && scrollProgress < 100} />
{hasAssetsToShow && ( {hasAssetsToShow && !isProfilePage && (
<BagFooter <BagFooter
balance={balance} balance={balance}
sufficientBalance={sufficientBalance} sufficientBalance={sufficientBalance}
...@@ -374,6 +314,26 @@ const Bag = () => { ...@@ -374,6 +314,26 @@ const Bag = () => {
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)} assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
/> />
)} )}
{sellAssets.length !== 0 && isProfilePage && (
<Box
marginTop="32"
marginX="28"
paddingY="10"
className={`${buttonTextMedium} ${commonButtonStyles}`}
backgroundColor="accentAction"
textAlign="center"
onClick={() => {
isMobile && toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
}}
>
Continue
</Box>
)}
</>
) : (
<ListingModal />
)}
</Column> </Column>
{isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />} {isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />}
</Portal> </Portal>
......
import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow'
import { Column } from 'nft/components/Flex'
import { useBag, useIsMobile } from 'nft/hooks'
import { BagItemStatus, BagStatus } from 'nft/types'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { useEffect, useMemo } from 'react'
import { useQuery } from 'react-query'
export const BagContent = () => {
const bagStatus = useBag((s) => s.bagStatus)
const setBagStatus = useBag((s) => s.setBagStatus)
const markAssetAsReviewed = useBag((s) => s.markAssetAsReviewed)
const didOpenUnavailableAssets = useBag((s) => s.didOpenUnavailableAssets)
const setDidOpenUnavailableAssets = useBag((s) => s.setDidOpenUnavailableAssets)
const uncheckedItemsInBag = useBag((s) => s.itemsInBag)
const setItemsInBag = useBag((s) => s.setItemsInBag)
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
const isMobile = useIsMobile()
const itemsInBag = useMemo(() => {
return recalculateBagUsingPooledAssets(uncheckedItemsInBag)
}, [uncheckedItemsInBag])
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
const { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems } = useMemo(() => {
const unchangedAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.ADDED_TO_BAG || item.status === BagItemStatus.REVIEWED)
.map((item) => item.asset)
const priceChangedAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)
.map((item) => item.asset)
const unavailableAssets = itemsInBag
.filter((item) => item.status === BagItemStatus.UNAVAILABLE)
.map((item) => item.asset)
const availableItems = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE)
return { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems }
}, [itemsInBag])
useEffect(() => {
const hasAssetsInReview = priceChangedAssets.length > 0
const hasAssets = itemsInBag.length > 0
if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) {
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG)
}
}, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus])
return (
<>
<Column display={priceChangedAssets.length > 0 || unavailableAssets.length > 0 ? 'flex' : 'none'}>
{unavailableAssets.length > 0 && (
<UnavailableAssetsHeaderRow
assets={unavailableAssets}
usdPrice={fetchedPriceData}
clearUnavailableAssets={() => setItemsInBag(availableItems)}
didOpenUnavailableAssets={didOpenUnavailableAssets}
setDidOpenUnavailableAssets={setDidOpenUnavailableAssets}
isMobile={isMobile}
/>
)}
{priceChangedAssets.map((asset, index) => (
<PriceChangeBagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
markAssetAsReviewed={markAssetAsReviewed}
top={index === 0 && unavailableAssets.length === 0}
isMobile={isMobile}
/>
))}
</Column>
<Column gap="8">
{unchangedAssets.map((asset) => (
<BagRow
key={asset.id}
asset={asset}
usdPrice={fetchedPriceData}
removeAsset={removeAssetFromBag}
showRemove={true}
isMobile={isMobile}
/>
))}
</Column>
</>
)
}
import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css'
import { sprinkles, vars } from 'nft/css/sprinkles.css'
export const header = style([
subhead,
sprinkles({
color: 'textPrimary',
justifyContent: 'space-between',
}),
{
lineHeight: '24px',
},
])
export const clearAll = style([
sprinkles({
color: 'textTertiary',
cursor: 'pointer',
fontWeight: 'semibold',
}),
{
':hover': {
color: vars.color.blue400,
},
},
])
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { BagCloseIcon } from 'nft/components/icons'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import * as styles from './BagHeader.css'
interface BagHeaderProps {
numberOfAssets: number
toggleBag: () => void
resetFlow: () => void
isProfilePage: boolean
}
export const BagHeader = ({ numberOfAssets, toggleBag, resetFlow, isProfilePage }: BagHeaderProps) => {
return (
<Column gap="4" paddingX="32" marginBottom="20">
<Row className={styles.header}>
{isProfilePage ? 'Sell NFTs' : 'My bag'}
<Box display="flex" padding="2" color="textSecondary" cursor="pointer" onClick={toggleBag}>
<BagCloseIcon />
</Box>
</Row>
{numberOfAssets > 0 && (
<Box fontSize="14" fontWeight="normal" style={{ lineHeight: '20px' }} color="textPrimary">
{roundAndPluralize(numberOfAssets, 'NFT')} ·{' '}
<Box
as="span"
className={styles.clearAll}
onClick={() => {
resetFlow()
}}
>
Clear all
</Box>
</Box>
)}
</Column>
)
}
import { style } from '@vanilla-extract/css' import { style } from '@vanilla-extract/css'
import { buttonTextSmall } from 'nft/css/common.css'
import { sprinkles } from 'nft/css/sprinkles.css' import { sprinkles } from 'nft/css/sprinkles.css'
export const tagAssetImage = style([ export const tagAssetImage = style([
sprinkles({ sprinkles({
borderRadius: '8', borderRadius: '8',
height: '52', height: '56',
width: '52', width: '56',
marginRight: '12', marginX: '12',
marginLeft: '8',
cursor: 'pointer',
}),
{
boxSizing: 'border-box',
},
])
export const tagAssetName = style([
sprinkles({
fontWeight: 'medium',
overflow: 'hidden',
marginRight: 'auto',
marginTop: '4',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
])
export const tagAssetCollectionName = style([
sprinkles({
fontWeight: 'normal',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}), }),
{
maxWidth: '65%',
},
])
export const tagAssetRowBottom = style([
sprinkles({
width: 'full',
display: 'flex',
flexWrap: 'nowrap',
marginRight: '4',
}),
{
marginTop: '-10px',
},
]) ])
export const removeAsset = style([ export const removeAsset = style([
...@@ -56,8 +17,8 @@ export const removeAsset = style([ ...@@ -56,8 +17,8 @@ export const removeAsset = style([
cursor: 'pointer', cursor: 'pointer',
}), }),
{ {
bottom: '-12px', bottom: '-4px',
left: '22px', left: '24px',
}, },
]) ])
...@@ -67,16 +28,14 @@ export const removeIcon = style([ ...@@ -67,16 +28,14 @@ export const removeIcon = style([
}), }),
]) ])
export const tagAssetInfo = style([ export const removeBagRowButton = style([
buttonTextSmall,
sprinkles({ sprinkles({
fontSize: '14', background: 'backgroundInteractive',
color: 'textPrimary', color: 'textPrimary',
display: 'flex', paddingX: '14',
flexWrap: 'wrap', paddingY: '12',
width: 'full', borderRadius: '12',
overflowX: 'hidden', cursor: 'pointer',
}), }),
{
lineHeight: '17px',
},
]) ])
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { bodySmall, subhead } from 'nft/css/common.css'
import { useIsMobile, useSellAsset } from 'nft/hooks'
import { WalletAsset } from 'nft/types'
import { useState } from 'react'
import * as styles from './ProfileAssetRow.css'
const ProfileAssetRow = ({ asset }: { asset: WalletAsset }) => {
const removeAsset = useSellAsset((state) => state.removeSellAsset)
const isMobile = useIsMobile()
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
return (
<Row paddingY="8" position="relative" onMouseEnter={handleHover} onMouseLeave={handleHover}>
<div>
<Box
display={isMobile ? 'flex' : 'none'}
className={styles.removeAsset}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
removeAsset(asset)
}}
>
<img className={styles.removeIcon} src={'/nft/svgs/minusCircle.svg'} alt="Remove item" />
</Box>
<img className={styles.tagAssetImage} src={asset.image_url} alt={asset.name} />
</div>
<Column gap="4" overflow="hidden" flexWrap="nowrap">
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" className={subhead}>
{asset.name ?? `#${asset.tokenId}`}
</Box>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" className={bodySmall}>
{asset.collection?.name}
</Box>
</Column>
{hovered && !isMobile && (
<Box
marginLeft="auto"
marginRight="0"
className={styles.removeBagRowButton}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
removeAsset(asset)
}}
>
Remove
</Box>
)}
</Row>
)
}
export default ProfileAssetRow
import { Column } from 'nft/components/Flex'
import { useSellAsset } from 'nft/hooks'
import ProfileAssetRow from './ProfileAssetRow'
export const ProfileBagContent = () => {
const sellAssets = useSellAsset((state) => state.sellAssets)
return (
<Column>
{sellAssets.length ? sellAssets.map((asset, index) => <ProfileAssetRow asset={asset} key={index} />) : null}
</Column>
)
}
...@@ -1396,6 +1396,19 @@ export const LargeBagIcon = (props: SVGProps) => ( ...@@ -1396,6 +1396,19 @@ export const LargeBagIcon = (props: SVGProps) => (
</svg> </svg>
) )
export const LargeTagIcon = (props: SVGProps) => (
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M55.6215 80.6815L55.621 80.6819C55.0823 81.2212 54.4427 81.649 53.7386 81.9409C53.0345 82.2328 52.2797 82.383 51.5175 82.383C50.7553 82.383 50.0006 82.2328 49.2965 81.9409C48.5923 81.649 47.9527 81.2212 47.414 80.6819L47.413 80.681L17.7 51.0025V17.7H51.0029L80.714 47.411C80.7141 47.4111 80.7142 47.4112 80.7143 47.4113C81.7943 48.498 82.4006 49.9679 82.4006 51.5C82.4006 53.032 81.7944 54.5017 80.7146 55.5884C80.7144 55.5886 80.7142 55.5888 80.714 55.589L55.6215 80.6815Z"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="34" cy="34" r="3" fill="currentColor" />
</svg>
)
export const CircularCloseIcon = (props: SVGProps) => ( export const CircularCloseIcon = (props: SVGProps) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="8" cy="8" r="8" fill="#293249" /> <circle cx="8" cy="8" r="8" fill="#293249" />
......
import clsx from 'clsx' import clsx from 'clsx'
import ms from 'ms.macro' import ms from 'ms.macro'
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
import { getListingState } from 'nft/components/bag/profile/utils'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { SortDropdown } from 'nft/components/common/SortDropdown' import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
...@@ -41,8 +43,6 @@ import { ListingMarkets } from 'nft/utils/listNfts' ...@@ -41,8 +43,6 @@ import { ListingMarkets } from 'nft/utils/listNfts'
import { pluralize } from 'nft/utils/roundAndPluralize' import { pluralize } from 'nft/utils/roundAndPluralize'
import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react' import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import { ListingButton } from '../modal/ListingButton'
import { getListingState } from '../modal/utils'
import * as styles from './ListPage.css' import * as styles from './ListPage.css'
const SelectMarketplacesModal = ({ const SelectMarketplacesModal = ({
......
import { style } from '@vanilla-extract/css'
import { breakpoints, sprinkles } from 'nft/css/sprinkles.css'
export const tagContainer = style([
sprinkles({
borderRadius: '20',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'backgroundOutline',
}),
{
'@media': {
[`screen and (max-width: ${breakpoints.md}px)`]: {
borderRadius: '0',
},
},
},
])
export const tagAssets = style([
sprinkles({
maxHeight: 'inherit',
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.md}px)`]: {
maxHeight: '55vh',
},
},
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])
export const closeIcon = style({
transform: 'rotate(90deg)',
float: 'right',
paddingTop: '1px',
})
export const orderButton = style([
sprinkles({
width: 'full',
paddingY: '12',
paddingX: '0',
}),
{
':hover': {
boxShadow: 'none',
},
},
])
import Loader from 'components/Loader'
import { Box } from 'nft/components/Box'
import { Column } from 'nft/components/Flex'
import { CloseDropDownIcon } from 'nft/components/icons'
import { bodySmall, buttonMedium, headlineSmall } from 'nft/css/common.css'
import { useBag, useIsMobile, useProfilePageState, useSellAsset } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { lazy, Suspense } from 'react'
import { useLocation } from 'react-router-dom'
import * as styles from './ListingTag.css'
const CartSellAssetRow = lazy(() => import('./TagAssetRow'))
const ListingModal = lazy(() => import('./ListingModal'))
const Cart = () => {
const { pathname } = useLocation()
const isProfilePage = pathname.startsWith('/profile')
const sellAssets = useSellAsset((state) => state.sellAssets)
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const sellPageState = useProfilePageState((state) => state.state)
const toggleCart = useBag((state) => state.toggleBag)
const isMobile = useIsMobile()
const bagExpanded = useBag((s) => s.bagExpanded)
return (
<Box
zIndex={{ sm: '3', md: '2' }}
width={{ sm: 'full', md: 'auto' }}
height={{ sm: 'full', md: 'auto' }}
position="fixed"
left={{ sm: '0', md: 'unset' }}
right={{ sm: 'unset', md: '0' }}
top={{ sm: '0', md: 'unset' }}
display={bagExpanded && isProfilePage ? 'flex' : 'none'}
>
<Suspense fallback={<Loader />}>
<Column
marginTop={{ sm: '0', md: '4' }}
marginRight={{ sm: '0', md: '20' }}
className={styles.tagContainer}
width={{ sm: 'full', md: '288' }}
height={{ sm: 'full', md: 'auto' }}
backgroundColor="backgroundSurface"
marginLeft="0"
justifyContent="flex-start"
>
{sellPageState === ProfilePageStateType.LISTING ? (
<ListingModal />
) : (
<>
<BagHeader bagQuantity={sellAssets.length} />
<Column
overflowX="hidden"
overflowY="auto"
position="relative"
paddingTop="6"
paddingBottom="6"
height="full"
className={styles.tagAssets}
>
{sellAssets.length
? sellAssets.map((asset, index) => <CartSellAssetRow asset={asset} key={index} />)
: null}
</Column>
<Box padding="12">
<Box
as="button"
className={`${buttonMedium} ${styles.orderButton}`}
disabled={sellAssets.length === 0}
onClick={() => {
isMobile && toggleCart()
setSellPageState(ProfilePageStateType.LISTING)
}}
>
Continue
</Box>
</Box>
</>
)}
</Column>
</Suspense>
</Box>
)
}
const BagHeader = ({ bagQuantity }: { bagQuantity: number }) => {
const toggleCart = useBag((state) => state.toggleBag)
const resetSellAssets = useSellAsset((state) => state.reset)
const isMobile = useIsMobile()
return (
<Box position="relative" zIndex="2" paddingTop="20" paddingLeft="12" paddingRight="12">
{isMobile ? (
<Box
as="button"
border="none"
color="textSecondary"
background="black"
className={styles.closeIcon}
onClick={toggleCart}
>
<CloseDropDownIcon />
</Box>
) : null}
<Box className={headlineSmall} paddingTop="0" paddingBottom="8" paddingX="0" margin="0">
{'Selected items'}
</Box>
{bagQuantity > 0 ? (
<Box className={bodySmall} paddingTop="0" paddingBottom="8" paddingX="0" marginY="0" marginX="auto">
{bagQuantity} {bagQuantity === 1 ? 'NFT' : 'NFTs'}
<Box as="span" position="relative" paddingRight="2" paddingLeft="4" style={{ fontSize: '8px', top: '-2px' }}>
&#x2022;
</Box>
<Box as="span" color="blue400" onClick={resetSellAssets} cursor="pointer" paddingLeft="2">
Remove all
</Box>
</Box>
) : null}
</Box>
)
}
export default Cart
import { Box } from 'nft/components/Box'
import { bodySmall, subheadSmall } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks'
import { WalletAsset } from 'nft/types'
import { useEffect, useRef, useState } from 'react'
import * as styles from './TagAssetRow.css'
const CartSellAssetRow = ({ asset }: { asset: WalletAsset }) => {
const removeAsset = useSellAsset((state) => state.removeSellAsset)
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
const assetRowRef = useRef<HTMLDivElement>()
useEffect(() => {
if (hovered && assetRowRef.current && assetRowRef.current.matches(':hover') === false) setHovered(false)
}, [hovered])
return (
<Box display="flex" padding="4" marginBottom="4" borderRadius="8" position="relative">
<Box
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={() => {
removeAsset(asset)
}}
>
<Box visibility={hovered ? 'visible' : 'hidden'} className={styles.removeAsset}>
<img className={styles.removeIcon} src={'/nft/svgs/minusCircle.svg'} alt="Remove item" />
</Box>
<img className={styles.tagAssetImage} src={asset.image_url} alt={asset.name} />
</Box>
<Box className={styles.tagAssetInfo}>
<Box className={`${subheadSmall} ${styles.tagAssetName}`}>{asset.name || `#${asset.tokenId}`}</Box>
<Box className={styles.tagAssetRowBottom}>
<Box className={`${bodySmall} ${styles.tagAssetCollectionName}`}>{asset.collection?.name}</Box>
</Box>
</Box>
</Box>
)
}
export default CartSellAssetRow
...@@ -25,7 +25,7 @@ export const headlineLarge = sprinkles({ fontWeight: 'normal', fontSize: '36', l ...@@ -25,7 +25,7 @@ export const headlineLarge = sprinkles({ fontWeight: 'normal', fontSize: '36', l
export const headlineMedium = sprinkles({ fontWeight: 'normal', fontSize: '28', lineHeight: '36' }) export const headlineMedium = sprinkles({ fontWeight: 'normal', fontSize: '28', lineHeight: '36' })
export const headlineSmall = sprinkles({ fontWeight: 'normal', fontSize: '20', lineHeight: '28' }) export const headlineSmall = sprinkles({ fontWeight: 'normal', fontSize: '20', lineHeight: '28' })
export const subhead = sprinkles({ fontWeight: 'medium', fontSize: '16', lineHeight: '16' }) export const subhead = sprinkles({ fontWeight: 'medium', fontSize: '16', lineHeight: '24' })
export const subheadSmall = sprinkles({ fontWeight: 'medium', fontSize: '14', lineHeight: '14' }) export const subheadSmall = sprinkles({ fontWeight: 'medium', fontSize: '14', lineHeight: '14' })
export const body = sprinkles({ fontWeight: 'normal', fontSize: '16', lineHeight: '24' }) export const body = sprinkles({ fontWeight: 'normal', fontSize: '16', lineHeight: '24' })
...@@ -37,7 +37,7 @@ export const buttonTextLarge = sprinkles({ fontWeight: 'semibold', fontSize: '20 ...@@ -37,7 +37,7 @@ export const buttonTextLarge = sprinkles({ fontWeight: 'semibold', fontSize: '20
export const buttonTextMedium = sprinkles({ fontWeight: 'semibold', fontSize: '16', lineHeight: '20' }) export const buttonTextMedium = sprinkles({ fontWeight: 'semibold', fontSize: '16', lineHeight: '20' })
export const buttonTextSmall = sprinkles({ fontWeight: 'semibold', fontSize: '14', lineHeight: '16' }) export const buttonTextSmall = sprinkles({ fontWeight: 'semibold', fontSize: '14', lineHeight: '16' })
const commonButtonStyles = style([ export const commonButtonStyles = style([
sprinkles({ sprinkles({
borderRadius: '12', borderRadius: '12',
transition: '250', transition: '250',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment