Commit 5dc0df21 authored by Jack Short's avatar Jack Short Committed by GitHub

feat: initial bag port (#4665)

* initial bag port

* adding add to bag

* adding remove

* addressing comments

* reenable bag on disconnect when reviewing price changes
parent 7f2cc9a3
......@@ -10,6 +10,7 @@ import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
const Cart = lazy(() => import('nft/components/sell/modal/ListingTag'))
const Bag = lazy(() => import('nft/components/bag/Bag'))
export default function TopLevelModals() {
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
......@@ -28,6 +29,7 @@ export default function TopLevelModals() {
{useTokensFlag() === TokensVariant.Enabled &&
(location.pathname.includes('/pool') || location.pathname.includes('/swap')) && <TokensBanner />}
<Cart />
<Bag />
</>
)
}
import { style } from '@vanilla-extract/css'
import { subhead } from 'nft/css/common.css'
import { breakpoints, sprinkles, vars } from 'nft/css/sprinkles.css'
export const bagContainer = style([
sprinkles({
position: 'fixed',
top: { sm: '0', md: '72' },
width: 'full',
height: 'full',
right: '0',
background: 'lightGray',
color: 'blackBlue',
paddingTop: '20',
paddingBottom: '24',
zIndex: { sm: 'offcanvas', md: '3' },
}),
{
'@media': {
[`(min-width: ${breakpoints.md}px)`]: {
width: '316px',
height: 'calc(100vh - 72px)',
},
},
},
])
export const assetsContainer = style([
sprinkles({
paddingX: '32',
maxHeight: 'full',
overflowY: 'scroll',
}),
{
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])
export const header = style([
subhead,
sprinkles({
color: 'blackBlue',
justifyContent: 'space-between',
}),
{
lineHeight: '24px',
},
])
export const clearAll = style([
sprinkles({
color: 'placeholder',
cursor: 'pointer',
fontWeight: 'semibold',
}),
{
':hover': {
color: vars.color.blue400,
},
},
])
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { useWeb3React } from '@web3-react/core'
import { parseEther } from 'ethers/lib/utils'
import { BagFooter } from 'nft/components/bag/BagFooter'
import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/components/bag/BagRow'
import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal'
import { Center, Column, Row } from 'nft/components/Flex'
import { BagCloseIcon, LargeBagIcon } from 'nft/components/icons'
import { Overlay } from 'nft/components/modals/Overlay'
import { subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useWalletBalance } from 'nft/hooks'
import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus } from 'nft/types'
import { buildSellObject } from 'nft/utils/buildSellObject'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { sortUpdatedAssets } from 'nft/utils/updatedAssets'
import { useEffect, useMemo, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom'
import * as styles from './Bag.css'
const EmptyState = () => {
return (
<Center height="full">
<Column gap="12">
<Center>
<LargeBagIcon color={themeVars.colors.placeholder} />
</Center>
<Column gap="16">
<Center className={subhead} style={{ lineHeight: '24px' }}>
Your bag is empty
</Center>
<Center fontSize="12" fontWeight="normal" color="darkGray" style={{ lineHeight: '16px' }}>
Selected NFTs will appear here
</Center>
</Column>
</Column>
</Center>
)
}
interface SeparatorProps {
top?: boolean
show?: boolean
}
const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
<Box
marginX="16"
borderWidth="1px"
borderStyle="solid"
borderColor="transparent"
borderTopColor={top ? 'transparent' : 'medGray'}
borderBottomColor={top ? 'medGray' : 'transparent'}
opacity={show ? '1' : '0'}
transition="250"
/>
)
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="darkGray" cursor="pointer" onClick={toggleBag}>
<BagCloseIcon />
</Box>
</Row>
{numberOfAssets > 0 && (
<Box fontSize="14" fontWeight="normal" style={{ lineHeight: '20px' }} color="blackBlue">
{roundAndPluralize(numberOfAssets, 'NFT')} ·{' '}
<Box
as="span"
className={styles.clearAll}
onClick={() => {
resetFlow()
}}
>
Clear all
</Box>
</Box>
)}
</Column>
)
}
const Bag = () => {
const { account } = useWeb3React()
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 bagIsLocked = useBag((s) => s.isLocked)
const setLocked = useBag((s) => s.setLocked)
const reset = useBag((s) => s.reset)
const uncheckedItemsInBag = useBag((s) => s.itemsInBag)
const setItemsInBag = useBag((s) => s.setItemsInBag)
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
const bagExpanded = useBag((s) => s.bagExpanded)
const toggleBag = useBag((s) => s.toggleBag)
const { address, balance: balanceInEth, provider } = useWalletBalance()
const isConnected = !!provider && !!address
const { pathname } = useLocation()
const isNFTSellPage = pathname.startsWith('/nfts/sell')
const isNFTPage = pathname.startsWith('/nfts')
const shouldShowBag = isNFTPage && !isNFTSellPage
const isMobile = useIsMobile()
const queryClient = useQueryClient()
const itemsInBag = useMemo(() => {
return recalculateBagUsingPooledAssets(uncheckedItemsInBag)
}, [uncheckedItemsInBag])
const ethSellObject = useMemo(
() =>
buildSellObject(
itemsInBag
.reduce(
(ethTotal, bagItem) => ethTotal.add(BigNumber.from(bagItem.asset.priceInfo.ETHPrice)),
BigNumber.from(0)
)
.toString()
),
[itemsInBag]
)
const [isOpen, setModalIsOpen] = useState(false)
const [userCanScroll, setUserCanScroll] = useState(false)
const [scrollProgress, setScrollProgress] = useState(0)
const scrollRef = (node: HTMLDivElement) => {
if (node !== null) {
const canScroll = node.scrollHeight > node.clientHeight
canScroll !== userCanScroll && setUserCanScroll(canScroll)
}
}
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
const { data: routingData, refetch } = useQuery(
['assetsRoute'],
() =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsInBag.map((item) => item.asset),
senderAddress: account ?? '',
}),
{
enabled: false,
}
)
const { totalEthPrice, totalUsdPrice } = useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
? total.add(
BigNumber.from(
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
)
)
: total,
BigNumber.from(0)
)
const totalUsdPrice = fetchedPriceData ? parseFloat(formatEther(totalEthPrice)) * fetchedPriceData : undefined
return { totalEthPrice, totalUsdPrice }
}, [itemsInBag, fetchedPriceData])
const { balance, sufficientBalance } = useMemo(() => {
const balance: BigNumber = parseEther(balanceInEth.toString())
const sufficientBalance = isConnected ? BigNumber.from(balance).gte(totalEthPrice) : true
return { balance, sufficientBalance }
}, [balanceInEth, totalEthPrice, isConnected])
useEffect(() => {
if (routingData && bagStatus === BagStatus.FETCHING_ROUTE) {
const updatedAssets = combineBuyItemsWithTxRoute(
itemsInBag.map((item) => item.asset),
routingData.route
)
const priceChangedAssets = updatedAssets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets)
const unavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const unchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = unchangedAssets.length > 0
const hasAssetsInReview = priceChangedAssets.length > 0
const hasUnavailableAssets = unavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...unavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...priceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...unchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
} else if (routingData && bagStatus === BagStatus.FETCHING_FINAL_ROUTE) {
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [routingData])
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 hasUnavailableAssets = unavailableAssets.length > 0
const hasAssets = itemsInBag.length > 0
if (bagStatus === BagStatus.ADDING_TO_BAG) {
isOpen && setModalIsOpen(false)
queryClient.setQueryData('assetsRoute', undefined)
}
if (bagStatus === BagStatus.FETCHING_ROUTE) {
hasUnavailableAssets && setItemsInBag(itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE))
setLocked(true)
refetch()
}
if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) {
queryClient.setQueryData('assetsRoute', undefined)
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG)
}
if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE) {
hasUnavailableAssets && setItemsInBag(itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE))
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setLocked(true)
refetch()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bagStatus, itemsInBag, priceChangedAssets, unavailableAssets])
useEffect(() => {
if (bagIsLocked && !isOpen) setModalIsOpen(true)
}, [bagIsLocked, isOpen])
useEffect(() => {
bagExpanded && toggleBag()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
const hasAssetsToShow = itemsInBag.length > 0 || unavailableAssets.length > 0
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
const scrollTop = event.currentTarget.scrollTop
const containerHeight = event.currentTarget.clientHeight
const scrollHeight = event.currentTarget.scrollHeight
setScrollProgress(scrollTop ? ((scrollTop + containerHeight) / scrollHeight) * 100 : 0)
}
return (
<>
{bagExpanded && shouldShowBag ? (
<Portal>
<Column className={styles.bagContainer}>
<BagHeader numberOfAssets={itemsInBag.length} toggleBag={toggleBag} resetFlow={reset} />
{itemsInBag.length === 0 && bagStatus === BagStatus.ADDING_TO_BAG && <EmptyState />}
<ScrollingIndicator top show={userCanScroll && scrollProgress > 0} />
<Column ref={scrollRef} className={styles.assetsContainer} onScroll={scrollHandler} gap="12">
<Column display={priceChangedAssets.length > 0 || unavailableAssets.length > 0 ? 'flex' : 'none'}>
{unavailableAssets.length > 0 && (
<UnavailableAssetsHeaderRow
assets={unavailableAssets}
clearUnavailableAssets={() => setItemsInBag(availableItems)}
didOpenUnavailableAssets={didOpenUnavailableAssets}
setDidOpenUnavailableAssets={setDidOpenUnavailableAssets}
isMobile={isMobile}
/>
)}
{priceChangedAssets.map((asset, index) => (
<PriceChangeBagRow
key={asset.id}
asset={asset}
markAssetAsReviewed={markAssetAsReviewed}
top={index === 0 && unavailableAssets.length === 0}
isMobile={isMobile}
/>
))}
</Column>
<Column gap="8">
{unchangedAssets.map((asset) => (
<BagRow
key={asset.id}
asset={asset}
removeAsset={removeAssetFromBag}
showRemove={true}
isMobile={isMobile}
/>
))}
</Column>
</Column>
<ScrollingIndicator show={userCanScroll && scrollProgress < 100} />
{hasAssetsToShow && (
<BagFooter
balance={balance}
sufficientBalance={sufficientBalance}
isConnected={isConnected}
totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus}
setBagStatus={setBagStatus}
fetchReview={() => {
setBagStatus(BagStatus.FETCHING_ROUTE)
}}
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
/>
)}
</Column>
{isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />}
</Portal>
) : null}
</>
)
}
export default Bag
import { style } from '@vanilla-extract/css'
import { body } from 'nft/css/common.css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const footerContainer = sprinkles({
marginTop: '20',
paddingX: '16',
})
export const footer = style([
sprinkles({
background: 'lightGray',
color: 'blackBlue',
paddingX: '16',
paddingY: '12',
borderBottomLeftRadius: '12',
borderBottomRightRadius: '12',
}),
])
export const warningContainer = style([
sprinkles({
paddingY: '12',
borderTopLeftRadius: '12',
borderTopRightRadius: '12',
justifyContent: 'center',
fontSize: '12',
fontWeight: 'semibold',
}),
{
color: '#EEB317',
background: '#EEB3173D',
lineHeight: '12px',
},
])
export const payButton = style([
body,
sprinkles({
background: 'blue400',
color: 'blackBlue',
border: 'none',
borderRadius: '12',
paddingY: '12',
fontWeight: 'semibold',
cursor: 'pointer',
justifyContent: 'center',
gap: '16',
}),
{
':disabled': {
opacity: '0.6',
cursor: 'auto',
},
},
])
export const ethPill = style([
sprinkles({
background: 'lightGray',
gap: '8',
paddingY: '4',
paddingLeft: '4',
paddingRight: '12',
fontSize: '20',
fontWeight: 'semibold',
borderRadius: 'round',
}),
{
lineHeight: '24px',
},
])
import { BigNumber } from '@ethersproject/bignumber'
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
import Loader from 'components/Loader'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { bodySmall, header2, subheadSmall } from 'nft/css/common.css'
import { BagStatus } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils/currency'
import { useModalIsOpen, useToggleWalletModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import * as styles from './BagFooter.css'
interface BagFooterProps {
balance: BigNumber
isConnected: boolean
sufficientBalance: boolean
totalEthPrice: BigNumber
totalUsdPrice: number | undefined
bagStatus: BagStatus
setBagStatus: (status: BagStatus) => void
fetchReview: () => void
assetsAreInReview: boolean
}
const PENDING_BAG_STATUSES = [
BagStatus.FETCHING_ROUTE,
BagStatus.CONFIRMING_IN_WALLET,
BagStatus.FETCHING_FINAL_ROUTE,
BagStatus.PROCESSING_TRANSACTION,
]
export const BagFooter = ({
balance,
isConnected,
sufficientBalance,
totalEthPrice,
totalUsdPrice,
bagStatus,
setBagStatus,
fetchReview,
assetsAreInReview,
}: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal()
const walletModalIsOpen = useModalIsOpen(ApplicationModal.WALLET)
const isPending = PENDING_BAG_STATUSES.includes(bagStatus) || walletModalIsOpen
const isDisabled = isConnected && (isPending || !sufficientBalance || assetsAreInReview)
const showWarning = isConnected && (!sufficientBalance || bagStatus === BagStatus.WARNING)
return (
<Column className={styles.footerContainer}>
{showWarning && (
<Row className={styles.warningContainer}>
{!sufficientBalance
? `Insufficient funds (${formatWeiToDecimal(balance.toString())} ETH)`
: `Something went wrong. Please try again.`}
</Row>
)}
<Column
borderTopLeftRadius={showWarning ? '0' : '12'}
borderTopRightRadius={showWarning ? '0' : '12'}
className={styles.footer}
>
<Box marginBottom="8" style={{ lineHeight: '20px', opacity: '0.54' }} className={subheadSmall}>
Total
</Box>
<Column marginBottom="16">
<Row justifyContent="space-between">
<Box className={header2}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box>
<Row className={styles.ethPill}>
<Box as="img" src={ethereumLogoUrl} alt="Ethereum" width="24" height="24" />
ETH
</Row>
</Row>
<Box fontWeight="normal" style={{ lineHeight: '20px', opacity: '0.6' }} className={bodySmall}>
{`${ethNumberStandardFormatter(totalUsdPrice, true)}`}
</Box>
</Column>
<Row
as="button"
color="explicitWhite"
className={styles.payButton}
disabled={isDisabled}
onClick={() => {
if (!isConnected) {
toggleWalletModal()
} else if (bagStatus === BagStatus.ADDING_TO_BAG) {
fetchReview()
} else if (bagStatus === BagStatus.CONFIRM_REVIEW || bagStatus === BagStatus.WARNING) {
setBagStatus(BagStatus.FETCHING_FINAL_ROUTE)
}
}}
>
{isPending && <Loader size="20px" stroke="white" />}
{!isConnected || walletModalIsOpen
? 'Connect wallet'
: bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET
? 'Proceed in wallet'
: bagStatus === BagStatus.PROCESSING_TRANSACTION
? 'Transaction pending'
: 'Pay'}
</Row>
</Column>
</Column>
)
}
import { style } from '@vanilla-extract/css'
import { bodySmall, buttonTextSmall } from 'nft/css/common.css'
import { sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
export const bagRow = style([
sprinkles({
color: 'blackBlue',
padding: '4',
gap: '12',
cursor: 'pointer',
height: 'full',
borderRadius: '12',
}),
{
marginLeft: '-4px',
marginRight: '-4px',
':hover': {
background: themeVars.colors.darkGray10,
},
},
])
export const unavailableAssetsContainer = sprinkles({
background: 'none',
gap: '12',
color: 'blackBlue',
paddingY: '16',
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'transparent',
borderTopColor: 'medGray',
borderBottomColor: 'medGray',
height: 'full',
})
export const priceChangeColumn = sprinkles({
background: 'none',
gap: '8',
color: 'blackBlue',
paddingY: '16',
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'transparent',
borderBottomColor: 'medGray',
height: 'full',
cursor: 'pointer',
})
export const priceChangeRow = style([
sprinkles({
color: 'placeholder',
gap: '4',
fontSize: '14',
fontWeight: 'normal',
}),
{
lineHeight: '20px',
},
])
export const unavailableAssetRow = style([
sprinkles({
gap: '12',
color: 'blackBlue',
paddingX: '12',
paddingY: '4',
}),
{
':hover': {
background: themeVars.colors.lightGrayButton,
},
},
])
export const priceChangeButton = style([
bodySmall,
sprinkles({
width: 'full',
paddingY: '8',
color: 'explicitWhite',
fontWeight: 'semibold',
textAlign: 'center',
borderRadius: '12',
}),
{
':hover': {
color: themeVars.colors.placeholder,
},
},
])
export const keepButton = style([
priceChangeButton,
sprinkles({
backgroundColor: 'blue400',
}),
{
':hover': {
background: `linear-gradient(rgba(76, 130, 251, 0.24), rgba(76, 130, 251, .24)), linear-gradient(${vars.color.blue400}, ${vars.color.blue400})`,
},
},
])
export const removeButton = style([
priceChangeButton,
sprinkles({
backgroundColor: 'lightGrayButton',
}),
{
':hover': {
background: `linear-gradient(rgba(76, 130, 251, 0.24), rgba(76, 130, 251, .24)), linear-gradient(${vars.color.lightGrayButton}, ${vars.color.lightGrayButton})`,
},
},
])
export const bagRowImage = sprinkles({
width: '72',
height: '72',
borderRadius: '8',
})
export const grayscaleImage = style({
filter: 'grayscale(100%)',
})
export const unavailableImage = style([
grayscaleImage,
sprinkles({
width: '44',
height: '44',
borderRadius: '8',
}),
])
export const removeAssetOverlay = style([
sprinkles({
position: 'absolute',
right: '4',
top: '4',
}),
])
export const bagRowPrice = style([
sprinkles({
gap: '4',
fontSize: '16',
fontWeight: 'normal',
}),
{
lineHeight: '24px',
},
])
export const assetName = style([
sprinkles({
fontSize: '14',
fontWeight: 'semibold',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
{
lineHeight: '20px',
},
])
export const collectionName = style([
buttonTextSmall,
sprinkles({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}),
{
lineHeight: '20px',
},
])
export const icon = sprinkles({
flexShrink: '0',
})
export const previewImageGrid = style([
sprinkles({
display: 'grid',
}),
{
gridTemplateColumns: 'repeat(5, 13px)',
},
])
export const toolTip = sprinkles({
color: 'darkGray',
display: 'flex',
flexShrink: '0',
})
import { BigNumber } from '@ethersproject/bignumber'
import clsx from 'clsx'
import { TimedLoader } from 'nft/components/bag/TimedLoader'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import {
ChevronDownBagIcon,
ChevronUpBagIcon,
CircularCloseIcon,
CloseTimerIcon,
SquareArrowDownIcon,
SquareArrowUpIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { loadingBlock } from 'nft/css/loading.css'
import { GenieAsset, UpdatedGenieAsset } from 'nft/types'
import { getAssetHref } from 'nft/utils/asset'
import { formatWeiToDecimal } from 'nft/utils/currency'
import { MouseEvent, useEffect, useReducer, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import * as styles from './BagRow.css'
const NoContentContainer = () => (
<Box position="relative" background="loadingBackground" className={styles.bagRowImage}>
<Box
position="absolute"
textAlign="center"
left="1/2"
top="1/2"
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
color="grey500"
fontSize="12"
fontWeight="normal"
>
Image
<br />
not
<br />
available
</Box>
</Box>
)
interface BagRowProps {
asset: UpdatedGenieAsset
removeAsset: (asset: GenieAsset) => void
showRemove?: boolean
grayscale?: boolean
isMobile: boolean
}
export const BagRow = ({ asset, removeAsset, showRemove, grayscale, isMobile }: BagRowProps) => {
const [cardHovered, setCardHovered] = useState(false)
const [loadedImage, setImageLoaded] = useState(false)
const [noImageAvailable, setNoImageAvailable] = useState(!asset.smallImageUrl)
const handleCardHover = () => setCardHovered(!cardHovered)
const assetCardRef = useRef<HTMLDivElement>(null)
if (cardHovered && assetCardRef.current && assetCardRef.current.matches(':hover') === false) setCardHovered(false)
return (
<Link to={getAssetHref(asset)} style={{ textDecoration: 'none' }}>
<Row ref={assetCardRef} className={styles.bagRow} onMouseEnter={handleCardHover} onMouseLeave={handleCardHover}>
<Box position="relative" display="flex">
<Box
display={showRemove ? 'block' : 'none'}
className={styles.removeAssetOverlay}
onClick={(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
removeAsset(asset)
}}
transition="250"
style={{ opacity: cardHovered || isMobile ? '1' : '0' }}
zIndex="1"
>
<CircularCloseIcon />
</Box>
{!noImageAvailable && (
<Box
as="img"
src={asset.smallImageUrl}
alt={asset.name}
className={clsx(styles.bagRowImage, grayscale && !cardHovered && styles.grayscaleImage)}
onLoad={() => {
setImageLoaded(true)
}}
onError={() => {
setNoImageAvailable(true)
}}
visibility={loadedImage ? 'visible' : 'hidden'}
/>
)}
{!loadedImage && <Box position="absolute" className={`${styles.bagRowImage} ${loadingBlock}`} />}
{noImageAvailable && <NoContentContainer />}
</Box>
<Column
overflow="hidden"
height="full"
justifyContent="space-between"
color={grayscale ? 'darkGray' : 'blackBlue'}
>
<Column>
<Row overflow="hidden" whiteSpace="nowrap" gap="2">
<Box className={styles.assetName}>{asset.name || asset.tokenId}</Box>
</Row>
<Row overflow="hidden" whiteSpace="nowrap" gap="2">
<Box className={styles.collectionName}>{asset.collectionName}</Box>
{asset.collectionIsVerified && <VerifiedIcon className={styles.icon} />}
</Row>
</Column>
<Row className={styles.bagRowPrice}>
{`${formatWeiToDecimal(
asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice
)} ETH`}
</Row>
</Column>
</Row>
</Link>
)
}
interface PriceChangeBagRowProps {
asset: UpdatedGenieAsset
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
top?: boolean
isMobile: boolean
}
export const PriceChangeBagRow = ({ asset, markAssetAsReviewed, top, isMobile }: PriceChangeBagRowProps) => {
const isPriceIncrease = BigNumber.from(asset.updatedPriceInfo?.ETHPrice).gt(BigNumber.from(asset.priceInfo.ETHPrice))
return (
<Column className={styles.priceChangeColumn} borderTopColor={top ? 'medGray' : 'transparent'}>
<Row className={styles.priceChangeRow}>
{isPriceIncrease ? <SquareArrowUpIcon /> : <SquareArrowDownIcon />}
<Box>{`Price ${isPriceIncrease ? 'increased' : 'decreased'} from ${formatWeiToDecimal(
asset.priceInfo.ETHPrice
)} ETH`}</Box>
</Row>
<BagRow asset={asset} removeAsset={() => undefined} isMobile={isMobile} />
<Row gap="12" justifyContent="space-between">
<Box
className={styles.removeButton}
onClick={(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
markAssetAsReviewed(asset, false)
}}
>
Remove
</Box>
<Box
className={styles.keepButton}
onClick={(e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
markAssetAsReviewed(asset, true)
}}
>
Keep
</Box>
</Row>
</Column>
)
}
interface UnavailableAssetsHeaderRowProps {
assets?: UpdatedGenieAsset[]
clearUnavailableAssets: () => void
didOpenUnavailableAssets: boolean
setDidOpenUnavailableAssets: (didOpen: boolean) => void
isMobile: boolean
}
interface UnavailableAssetsPreviewProps {
assets: UpdatedGenieAsset[]
}
const ASSET_PREVIEW_WIDTH = 32
const ASSET_PREVIEW_OFFSET = 20
const UnavailableAssetsPreview = ({ assets }: UnavailableAssetsPreviewProps) => (
<Column
display="grid"
style={{
gridTemplateColumns: `repeat(${assets.length}, 20px)`,
width: `${ASSET_PREVIEW_WIDTH + (assets.length - 1) * ASSET_PREVIEW_OFFSET}px`,
}}
>
{assets.map((asset, index) => (
<Box
key={`preview${index}`}
as="img"
src={asset.smallImageUrl}
width="32"
height="32"
borderStyle="solid"
borderWidth="1px"
borderColor="lightGray"
borderRadius="4"
style={{ zIndex: assets.length - index }}
className={styles.grayscaleImage}
/>
))}
</Column>
)
export const UnavailableAssetsHeaderRow = ({
assets,
clearUnavailableAssets,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isMobile,
}: UnavailableAssetsHeaderRowProps) => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const timerLimit = 8
const [timeLeft, setTimeLeft] = useState(timerLimit)
useEffect(() => {
if (!timeLeft) {
if (!didOpenUnavailableAssets) {
clearUnavailableAssets()
setDidOpenUnavailableAssets(false)
}
return
}
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1)
}, 1000)
return () => clearInterval(intervalId)
}, [timeLeft, clearUnavailableAssets, didOpenUnavailableAssets, setDidOpenUnavailableAssets])
if (!assets || assets.length === 0) return null
return (
<Column className={styles.unavailableAssetsContainer}>
<Row className={styles.priceChangeRow} justifyContent="space-between">
No longer available for sale
{!didOpenUnavailableAssets && (
<Row
position="relative"
width="20"
height="20"
color="blackBlue"
justifyContent="center"
cursor="pointer"
onClick={() => clearUnavailableAssets()}
>
<TimedLoader />
<CloseTimerIcon />
</Row>
)}
</Row>
{assets.length === 1 && <BagRow asset={assets[0]} removeAsset={() => undefined} grayscale isMobile={isMobile} />}
{assets.length > 1 && (
<Column>
<Row
justifyContent="space-between"
marginBottom={isOpen ? '12' : '0'}
cursor="pointer"
onClick={() => {
!didOpenUnavailableAssets && setDidOpenUnavailableAssets(true)
toggleOpen()
}}
>
<Row gap="12" fontSize="14" color="darkGray" fontWeight="normal" style={{ lineHeight: '20px' }}>
{!isOpen && <UnavailableAssetsPreview assets={assets.slice(0, 5)} />}
{`${assets.length} unavailable NFTs`}
</Row>
<Row color="darkGray">{isOpen ? <ChevronUpBagIcon /> : <ChevronDownBagIcon />}</Row>
</Row>
<Column gap="8">
{isOpen &&
assets.map((asset) => (
<BagRow key={asset.id} asset={asset} removeAsset={() => undefined} grayscale isMobile={isMobile} />
))}
</Column>
</Column>
)}
</Column>
)
}
import { Box } from 'nft/components/Box'
import styled, { keyframes } from 'styled-components/macro'
const dash = keyframes`
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
`
const Circle = styled.circle`
stroke-dasharray: 1000;
stroke-dashoffset: 0;
-webkit-animation: ${dash} linear;
animation: ${dash} linear;
animation-duration: 160s;
stroke: ${({ theme }) => theme.accentActive};
`
export const TimedLoader = () => {
const stroke = 1.5
return (
<Box display="flex" position="absolute">
<svg height="18px" width="18px">
<Circle
strokeWidth={`${stroke}`}
strokeLinecap="round"
style={{
transform: 'rotate(90deg)',
transformOrigin: '50% 50%',
}}
fill="transparent"
r="8px"
cx="9px"
cy="9px"
/>
</svg>
</Box>
)
}
......@@ -42,23 +42,24 @@ const baseHref = (asset: GenieAsset) => `/#/nfts/asset/${asset.address}/${asset.
/* -------- ASSET CARD -------- */
interface CardProps {
asset: GenieAsset
selected: boolean
children: ReactNode
}
const Container = ({ asset, children }: CardProps) => {
const Container = ({ asset, selected, children }: CardProps) => {
const [hovered, toggleHovered] = useReducer((s) => !s, false)
const [href, setHref] = useState(baseHref(asset))
const providerValue = useMemo(
() => ({
asset,
selected: false,
selected,
hovered,
toggleHovered,
href,
setHref,
}),
[asset, hovered, href]
[asset, hovered, selected, href]
)
const assetRef = useRef<HTMLDivElement>(null)
......@@ -412,12 +413,13 @@ const TertiaryInfo = ({ children }: { children: ReactNode }) => {
interface ButtonProps {
children: ReactNode
quantity: number
selectedChildren: ReactNode
onClick: (e: MouseEvent) => void
onSelectedClick: (e: MouseEvent) => void
}
const Button = ({ children, selectedChildren, onClick, onSelectedClick }: ButtonProps) => {
const Button = ({ children, quantity, selectedChildren, onClick, onSelectedClick }: ButtonProps) => {
const [buttonHovered, toggleButtonHovered] = useReducer((s) => !s, false)
const { asset, selected, setHref } = useCardContext()
const buttonRef = useRef<HTMLDivElement>(null)
......@@ -490,7 +492,7 @@ const Button = ({ children, selectedChildren, onClick, onSelectedClick }: Button
>
<MinusIconLarge width="32" height="32" />
</Column>
<Box className={`${styles.erc1155QuantityText} ${subheadSmall}`}></Box>
<Box className={`${styles.erc1155QuantityText} ${subheadSmall}`}>{quantity.toString()}</Box>
<Column
as="button"
className={styles.erc1155PlusButton}
......
import { BigNumber } from '@ethersproject/bignumber'
import * as Card from 'nft/components/collection/Card'
import { useBag } from 'nft/hooks'
import { GenieAsset, UniformHeight } from 'nft/types'
import { formatWeiToDecimal } from 'nft/utils/currency'
import { isAudio } from 'nft/utils/isAudio'
import { isVideo } from 'nft/utils/isVideo'
import { MouseEvent, useMemo } from 'react'
import * as Card from './Card'
enum AssetMediaType {
Image,
Video,
......@@ -14,6 +16,7 @@ enum AssetMediaType {
interface CollectionAssetProps {
asset: GenieAsset
isMobile: boolean
uniformHeight: UniformHeight
setUniformHeight: (u: UniformHeight) => void
mediaShouldBePlaying: boolean
......@@ -22,11 +25,31 @@ interface CollectionAssetProps {
export const CollectionAsset = ({
asset,
isMobile,
uniformHeight,
setUniformHeight,
mediaShouldBePlaying,
setCurrentTokenPlayingMedia,
}: CollectionAssetProps) => {
const { addAssetToBag, removeAssetFromBag, itemsInBag, bagExpanded, toggleBag } = useBag((state) => ({
addAssetToBag: state.addAssetToBag,
removeAssetFromBag: state.removeAssetFromBag,
itemsInBag: state.itemsInBag,
bagExpanded: state.bagExpanded,
toggleBag: state.toggleBag,
}))
const { quantity, isSelected } = useMemo(() => {
return {
quantity: itemsInBag.filter(
(x) => x.asset.tokenType === 'ERC1155' && x.asset.tokenId === asset.tokenId && x.asset.address === asset.address
).length,
isSelected: itemsInBag.some(
(item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
),
}
}, [asset, itemsInBag])
const { notForSale, assetMediaType } = useMemo(() => {
let notForSale = true
let assetMediaType = AssetMediaType.Image
......@@ -45,7 +68,7 @@ export const CollectionAsset = ({
}, [asset])
return (
<Card.Container asset={asset}>
<Card.Container asset={asset} selected={isSelected}>
{assetMediaType === AssetMediaType.Image ? (
<Card.Image uniformHeight={uniformHeight} setUniformHeight={setUniformHeight} />
) : assetMediaType === AssetMediaType.Video ? (
......@@ -82,12 +105,16 @@ export const CollectionAsset = ({
</Card.SecondaryRow>
</Card.InfoContainer>
<Card.Button
quantity={quantity}
selectedChildren={'Remove'}
onClick={(e: MouseEvent) => {
e.preventDefault()
addAssetToBag(asset)
!bagExpanded && !isMobile && toggleBag()
}}
onSelectedClick={(e: MouseEvent) => {
e.preventDefault()
removeAssetFromBag(asset)
}}
>
{'Buy now'}
......
......@@ -122,6 +122,7 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
<CollectionAsset
key={asset.address + asset.tokenId}
asset={asset}
isMobile={isMobile}
uniformHeight={uniformHeight}
setUniformHeight={setUniformHeight}
mediaShouldBePlaying={asset.tokenId === currentTokenPlayingMedia}
......
......@@ -148,6 +148,7 @@ export const vars = createGlobalTheme(':root', {
...themeVars.colors,
genieBlue: '#4C82FB',
fallbackGradient: 'linear-gradient(270deg, #D1D5DB 0%, #F6F6F6 100%)',
loadingBackground: '#24272e',
dropShadow: '0px 4px 16px rgba(70, 115, 250, 0.4)',
green: '#209853',
orange: '#FA2C38',
......
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from '../types'
type BagState = {
interface BagState {
bagStatus: BagStatus
setBagStatus: (state: BagStatus) => void
itemsInBag: BagItem[]
setItemsInBag: (items: BagItem[]) => void
addAssetToBag: (asset: UpdatedGenieAsset) => void
removeAssetFromBag: (asset: UpdatedGenieAsset) => void
isLocked: boolean
setLocked: (isLocked: boolean) => void
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
didOpenUnavailableAssets: boolean
setDidOpenUnavailableAssets: (didOpen: boolean) => void
bagExpanded: boolean
toggleBag: () => void
isLocked: boolean
setLocked: (isLocked: boolean) => void
reset: () => void
}
export const useBag = create<BagState>()(
devtools(
(set, get) => ({
bagStatus: BagStatus.ADDING_TO_BAG,
setBagStatus: (newBagStatus) =>
set(() => ({
bagStatus: newBagStatus,
})),
markAssetAsReviewed: (asset, toKeep) =>
set(({ itemsInBag }) => {
if (itemsInBag.length === 0) return { itemsInBag: [] }
const itemsInBagCopy = [...itemsInBag]
const index = itemsInBagCopy.findIndex((item) => item.asset.id === asset.id)
if (!toKeep && index !== -1) itemsInBagCopy.splice(index, 1)
else if (index !== -1) {
itemsInBagCopy[index].status = BagItemStatus.REVIEWED
}
return {
itemsInBag: itemsInBagCopy,
}
}),
didOpenUnavailableAssets: false,
setDidOpenUnavailableAssets: (didOpen) =>
set(() => ({
didOpenUnavailableAssets: didOpen,
})),
bagExpanded: false,
toggleBag: () =>
set(({ bagExpanded }) => ({
......@@ -28,9 +57,13 @@ export const useBag = create<BagState>()(
isLocked: _isLocked,
})),
itemsInBag: [],
setItemsInBag: (items) =>
set(() => ({
itemsInBag: items,
})),
addAssetToBag: (asset) =>
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag }
if (get().isLocked) return { itemsInBag: get().itemsInBag }
const assetWithId = { asset: { id: uuidv4(), ...asset }, status: BagItemStatus.ADDED_TO_BAG }
if (itemsInBag.length === 0)
return {
......@@ -45,17 +78,28 @@ export const useBag = create<BagState>()(
}),
removeAssetFromBag: (asset) => {
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag }
if (get().isLocked) return { itemsInBag: get().itemsInBag }
if (itemsInBag.length === 0) return { itemsInBag: [] }
const itemsCopy = [...itemsInBag]
const index = itemsCopy.findIndex((n) =>
asset.id ? n.asset.id === asset.id : n.asset.tokenId === asset.tokenId && n.asset.address === asset.address
)
if (index === -1) return { itemsInBag }
if (index === -1) return { itemsInBag: get().itemsInBag }
itemsCopy.splice(index, 1)
return { itemsInBag: itemsCopy }
})
},
reset: () =>
set(() => {
if (!get().isLocked)
return {
bagStatus: BagStatus.ADDING_TO_BAG,
itemsInBag: [],
didOpenUnavailableAssets: false,
isLocked: false,
}
else return {}
}),
}),
{ name: 'useBag' }
)
......
import { AnimatedBox, Box } from 'nft/components/Box'
import { CollectionNfts, CollectionStats, Filters } from 'nft/components/collection'
import { Column, Row } from 'nft/components/Flex'
import { useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css'
import { CollectionStatsFetcher } from 'nft/queries'
import { useEffect } from 'react'
......@@ -10,6 +10,7 @@ import { useParams } from 'react-router-dom'
import { useSpring } from 'react-spring/web'
const FILTER_WIDTH = 332
const BAG_WIDTH = 324
const Collection = () => {
const { contractAddress } = useParams()
......@@ -17,6 +18,7 @@ const Collection = () => {
const isMobile = useIsMobile()
const [isFiltersExpanded] = useFiltersExpanded()
const setMarketCount = useCollectionFilters((state) => state.setMarketCount)
const isBagExpanded = useBag((state) => state.bagExpanded)
const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () =>
CollectionStatsFetcher(contractAddress as string)
......@@ -24,7 +26,13 @@ const Collection = () => {
const { gridX, gridWidthOffset } = useSpring({
gridX: isFiltersExpanded ? FILTER_WIDTH : 0,
gridWidthOffset: isFiltersExpanded ? FILTER_WIDTH : 0,
gridWidthOffset: isFiltersExpanded
? isBagExpanded
? BAG_WIDTH + FILTER_WIDTH
: FILTER_WIDTH
: isBagExpanded
? BAG_WIDTH
: 0,
})
useEffect(() => {
......
......@@ -85,7 +85,7 @@ export interface GenieAsset {
currentUsdPrice: string
imageUrl: string
animationUrl: string
marketplace: string
marketplace: Markets
name: string
priceInfo: PriceInfo
openseaSusFlag: boolean
......@@ -181,3 +181,9 @@ export interface DropDownOption {
reverseIndex?: number
reverseOnClick?: () => void
}
export enum DetailsOrigin {
COLLECTION = 'collection',
SELL = 'sell',
EXPLORE = 'explore',
}
import { DetailsOrigin, GenieAsset } from 'nft/types'
export const getAssetHref = (asset: GenieAsset, origin?: DetailsOrigin) => {
return `/nfts/asset/${asset.address}/${asset.tokenId}${origin ? `?origin=${origin}` : ''}`
}
import { BigNumber } from '@ethersproject/bignumber'
import { GenieAsset, Markets } from '../types'
import { BagItem, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types'
export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
let amountToBuy: BigNumber = BigNumber.from(0)
......@@ -43,3 +42,77 @@ export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
return price.toString()
}
export const calcAvgGroupPoolPrice = (asset: GenieAsset, numberOfAssets: number) => {
let total = BigNumber.from(0)
for (let i = 0; i < numberOfAssets; i++) {
const price = BigNumber.from(calcPoolPrice(asset, i))
total = total.add(price)
}
return total.div(numberOfAssets).toString()
}
export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[]) => {
if (
!uncheckedItemsInBag.some(
(item) => item.asset.marketplace === Markets.NFTX || item.asset.marketplace === Markets.NFT20
)
)
return uncheckedItemsInBag
const isPooledMarket = (market: Markets) => market === Markets.NFTX || market === Markets.NFT20
const itemsInBag = [...uncheckedItemsInBag]
const possibleMarkets = itemsInBag.reduce((markets, item) => {
const asset = item.asset
const market = asset.marketplace
if (!isPooledMarket(market)) return markets
const key = asset.address + asset.marketplace
if (Object.keys(markets).includes(key)) {
markets[key].push(asset.tokenId)
} else {
markets[key] = [asset.tokenId]
}
return markets
}, {} as { [key: string]: [string] })
const updatedPriceMarkets = itemsInBag.reduce((markets, item) => {
const asset = item.asset
const market = asset.marketplace
if (!asset.updatedPriceInfo) return markets
if (!isPooledMarket(market)) return markets
const key = asset.address + asset.marketplace
if (Object.keys(markets).includes(key)) {
markets[key] = [markets[key][0] + 1, asset]
} else {
markets[key] = [1, asset]
}
return markets
}, {} as { [key: string]: [number, UpdatedGenieAsset] })
const calculatedAvgPoolPrices = Object.keys(updatedPriceMarkets).reduce((prices, key) => {
prices[key] = calcAvgGroupPoolPrice(updatedPriceMarkets[key][1], updatedPriceMarkets[key][0])
return prices
}, {} as { [key: string]: string })
itemsInBag.forEach((item) => {
if (isPooledMarket(item.asset.marketplace)) {
const asset = item.asset
const isPriceChangedAsset = !!asset.updatedPriceInfo
const calculatedPrice = isPriceChangedAsset
? calculatedAvgPoolPrices[asset.address + asset.marketplace]
: calcPoolPrice(asset, possibleMarkets[asset.address + asset.marketplace].indexOf(item.asset.tokenId))
if (isPriceChangedAsset && item.asset.updatedPriceInfo)
item.asset.updatedPriceInfo.ETHPrice = item.asset.updatedPriceInfo.basePrice = calculatedPrice
else item.asset.currentEthPrice = item.asset.priceInfo.ETHPrice = calculatedPrice
}
})
return itemsInBag
}
import { BigNumber } from '@ethersproject/bignumber'
import { UpdatedGenieAsset } from 'nft/types'
export const updatedAssetPriceDifference = (asset: UpdatedGenieAsset) => {
if (!asset.updatedPriceInfo) return BigNumber.from(0)
return BigNumber.from(asset.updatedPriceInfo.ETHPrice).sub(BigNumber.from(asset.priceInfo.ETHPrice))
}
export const sortUpdatedAssets = (x: UpdatedGenieAsset, y: UpdatedGenieAsset) => {
return updatedAssetPriceDifference(x).gt(updatedAssetPriceDifference(y)) ? -1 : 1
}
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