feat: details owner actions (#4851)
* feat: initial pass at price details container * feat: nft details owners actions * adding check for bad types
This commit is contained in:
parent
8ceabd513c
commit
d704e78223
@ -13,7 +13,6 @@ import { badge, bodySmall, caption, headlineMedium, subhead } from 'nft/css/comm
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { useTimeout } from 'nft/hooks/useTimeout'
|
||||
import { fetchSingleAsset } from 'nft/queries'
|
||||
import { CollectionInfoForAsset, GenieAsset, SellOrder } from 'nft/types'
|
||||
import { shortenAddress } from 'nft/utils/address'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
@ -25,7 +24,6 @@ import { toSignificant } from 'nft/utils/toSignificant'
|
||||
import qs from 'query-string'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useQuery } from 'react-query'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useSpring } from 'react-spring'
|
||||
|
||||
@ -107,14 +105,11 @@ enum MediaType {
|
||||
}
|
||||
|
||||
interface AssetDetailsProps {
|
||||
tokenId: string
|
||||
contractAddress: string
|
||||
asset: GenieAsset
|
||||
collection: CollectionInfoForAsset
|
||||
}
|
||||
|
||||
export const AssetDetails = ({ tokenId, contractAddress }: AssetDetailsProps) => {
|
||||
const { data } = useQuery(['assetDetail', contractAddress, tokenId], () =>
|
||||
fetchSingleAsset({ contractAddress, tokenId })
|
||||
)
|
||||
export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
const { pathname, search } = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const addAssetToBag = useBag((state) => state.addAssetToBag)
|
||||
@ -127,8 +122,6 @@ export const AssetDetails = ({ tokenId, contractAddress }: AssetDetailsProps) =>
|
||||
const creatorEnsName = useENSName(creatorAddress)
|
||||
const ownerEnsName = useENSName(ownerAddress)
|
||||
const parsed = qs.parse(search)
|
||||
const asset = useMemo(() => (data ? data[0] : ({} as GenieAsset)), [data])
|
||||
const collection = useMemo(() => (data ? data[1] : ({} as CollectionInfoForAsset)), [data])
|
||||
const { gridWidthOffset } = useSpring({
|
||||
gridWidthOffset: bagExpanded ? 324 : 0,
|
||||
})
|
||||
@ -415,8 +408,8 @@ export const AssetDetails = ({ tokenId, contractAddress }: AssetDetailsProps) =>
|
||||
<Traits collectionAddress={asset.address} traits={asset.traits ?? []} />
|
||||
) : (
|
||||
<Details
|
||||
contractAddress={contractAddress}
|
||||
tokenId={tokenId}
|
||||
contractAddress={asset.address}
|
||||
tokenId={asset.tokenId}
|
||||
tokenType={asset.tokenType}
|
||||
blockchain="Ethereum"
|
||||
metadataUrl={asset.externalLink}
|
||||
|
@ -1,3 +1,206 @@
|
||||
export const AssetPriceDetails = () => {
|
||||
return <div>Holder for price details</div>
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { CancelListingIcon } from 'nft/components/icons'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
|
||||
import { ethNumberStandardFormatter, formatEthPrice, getMarketplaceIcon, timeLeft } from 'nft/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
interface AssetPriceDetailsProps {
|
||||
asset: GenieAsset
|
||||
collection: CollectionInfoForAsset
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-left: 86px;
|
||||
`
|
||||
|
||||
const BestPriceContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
width: 320px;
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const PriceRow = styled.div`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
`
|
||||
|
||||
const MarketplaceIcon = styled.img`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
`
|
||||
|
||||
const BuyNowButton = styled.div<{ assetInBag: boolean; margin: boolean; useAccentColor: boolean }>`
|
||||
width: 100%;
|
||||
background-color: ${({ theme, assetInBag, useAccentColor }) =>
|
||||
assetInBag ? theme.accentFailure : useAccentColor ? theme.accentAction : theme.backgroundInteractive};
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
margin-top: ${({ margin }) => (margin ? '12px' : '0px')};
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const NotForSaleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 48px 18px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const DiscoveryContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => {
|
||||
const listing = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined
|
||||
const expirationDate = listing ? new Date(listing.orderClosingDate) : undefined
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<BestPriceContainer>
|
||||
<HeaderRow>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight={'24px'}>
|
||||
{listing ? 'Your Price' : 'List for Sale'}
|
||||
</ThemedText.SubHeader>
|
||||
{listing && <MarketplaceIcon alt={listing.marketplace} src={getMarketplaceIcon(listing.marketplace)} />}
|
||||
</HeaderRow>
|
||||
<PriceRow>
|
||||
{listing ? (
|
||||
<>
|
||||
<ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}>
|
||||
{formatEthPrice(asset.priceInfo.ETHPrice)}
|
||||
</ThemedText.MediumHeader>
|
||||
<ThemedText.BodySecondary lineHeight={'24px'}>
|
||||
{ethNumberStandardFormatter(asset.priceInfo.USDPrice, true, true)}
|
||||
</ThemedText.BodySecondary>
|
||||
</>
|
||||
) : (
|
||||
<ThemedText.BodySecondary fontSize="14px" lineHeight={'20px'}>
|
||||
Get the best price for your NFT by selling with Uniswap.
|
||||
</ThemedText.BodySecondary>
|
||||
)}
|
||||
</PriceRow>
|
||||
{expirationDate && (
|
||||
<ThemedText.BodySecondary fontSize={'14px'}>Sale ends: {timeLeft(expirationDate)}</ThemedText.BodySecondary>
|
||||
)}
|
||||
{!listing ? (
|
||||
<BuyNowButton assetInBag={false} margin={true} useAccentColor={true} onClick={() => navigate('/profile')}>
|
||||
<ThemedText.SubHeader lineHeight={'20px'}>List</ThemedText.SubHeader>
|
||||
</BuyNowButton>
|
||||
) : (
|
||||
<>
|
||||
<BuyNowButton assetInBag={false} margin={true} useAccentColor={false} onClick={() => navigate('/profile')}>
|
||||
<ThemedText.SubHeader lineHeight={'20px'}>Adjust listing</ThemedText.SubHeader>
|
||||
</BuyNowButton>
|
||||
<BuyNowButton assetInBag={true} margin={false} useAccentColor={false} onClick={() => navigate('/profile')}>
|
||||
<ThemedText.SubHeader lineHeight={'20px'}>Cancel listing</ThemedText.SubHeader>
|
||||
</BuyNowButton>
|
||||
</>
|
||||
)}
|
||||
</BestPriceContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const NotForSale = ({ collection }: { collection: CollectionInfoForAsset }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<BestPriceContainer>
|
||||
<NotForSaleContainer>
|
||||
<CancelListingIcon width="79px" height="79px" color={theme.textTertiary} />
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
|
||||
Not for sale
|
||||
</ThemedText.SubHeader>
|
||||
<DiscoveryContainer>
|
||||
<ThemedText.BodySecondary fontSize="14px" lineHeight="20px">
|
||||
Discover similar NFTs for sale in
|
||||
</ThemedText.BodySecondary>
|
||||
<ThemedText.Link lineHeight="20px">{collection.collectionName}</ThemedText.Link>
|
||||
</DiscoveryContainer>
|
||||
</NotForSaleContainer>
|
||||
</BestPriceContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) => {
|
||||
const { account } = useWeb3React()
|
||||
const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined
|
||||
const expirationDate = cheapestOrder ? new Date(cheapestOrder.orderClosingDate) : undefined
|
||||
const itemsInBag = useBag((s) => s.itemsInBag)
|
||||
const addAssetToBag = useBag((s) => s.addAssetToBag)
|
||||
const removeAssetFromBag = useBag((s) => s.removeAssetFromBag)
|
||||
|
||||
const assetInBag = useMemo(() => {
|
||||
return itemsInBag.some((item) => item.asset.tokenId === asset.tokenId && item.asset.address === asset.address)
|
||||
}, [itemsInBag, asset])
|
||||
|
||||
const isOwner =
|
||||
asset.owner && typeof asset.owner === 'string' ? account?.toLowerCase() === asset.owner.toLowerCase() : false
|
||||
|
||||
if (isOwner) {
|
||||
return <OwnerContainer asset={asset} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{cheapestOrder && asset.priceInfo ? (
|
||||
<BestPriceContainer>
|
||||
<HeaderRow>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight={'24px'}>
|
||||
Best Price
|
||||
</ThemedText.SubHeader>
|
||||
<MarketplaceIcon alt={cheapestOrder.marketplace} src={getMarketplaceIcon(cheapestOrder.marketplace)} />
|
||||
</HeaderRow>
|
||||
<PriceRow>
|
||||
<ThemedText.MediumHeader fontSize={'28px'} lineHeight={'36px'}>
|
||||
{formatEthPrice(asset.priceInfo.ETHPrice)}
|
||||
</ThemedText.MediumHeader>
|
||||
<ThemedText.BodySecondary lineHeight={'24px'}>
|
||||
{ethNumberStandardFormatter(asset.priceInfo.USDPrice, true, true)}
|
||||
</ThemedText.BodySecondary>
|
||||
</PriceRow>
|
||||
{expirationDate && (
|
||||
<ThemedText.BodySecondary fontSize={'14px'}>Sale ends: {timeLeft(expirationDate)}</ThemedText.BodySecondary>
|
||||
)}
|
||||
<BuyNowButton
|
||||
assetInBag={assetInBag}
|
||||
margin={true}
|
||||
useAccentColor={true}
|
||||
onClick={() => (assetInBag ? removeAssetFromBag(asset) : addAssetToBag(asset))}
|
||||
>
|
||||
<ThemedText.SubHeader lineHeight={'20px'}>{assetInBag ? 'Remove' : 'Buy Now'}</ThemedText.SubHeader>
|
||||
</BuyNowButton>
|
||||
</BestPriceContainer>
|
||||
) : (
|
||||
<NotForSale collection={collection} />
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -1511,3 +1511,15 @@ export const EmptyNFTWalletIcon = (props: SVGProps) => (
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const CancelListingIcon = (props: SVGProps) => (
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M71 31L75.36 35.36C76.85 36.8589 77.6863 38.8865 77.6863 41C77.6863 43.1135 76.85 45.1411 75.36 46.64L46.68 75.32C45.937 76.0638 45.0547 76.6539 44.0835 77.0565C43.1123 77.4591 42.0713 77.6663 41.02 77.6663C39.9687 77.6663 38.9277 77.4591 37.9565 77.0565C36.9853 76.6539 36.103 76.0638 35.36 75.32L31 71M47.8 7.8L41 1H1V41L7.8 47.8M77.6863 1L1.62987 77.0565M21 21H21.0333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { AssetDetails } from 'nft/components/details/AssetDetails'
|
||||
import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
|
||||
import { fetchSingleAsset } from 'nft/queries'
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@ -11,12 +14,30 @@ const AssetContainer = styled.div`
|
||||
|
||||
const Asset = () => {
|
||||
const { tokenId = '', contractAddress = '' } = useParams()
|
||||
const { data } = useQuery(
|
||||
['assetDetail', contractAddress, tokenId],
|
||||
() => fetchSingleAsset({ contractAddress, tokenId }),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const asset = useMemo(() => (data ? data[0] : undefined), [data])
|
||||
const collection = useMemo(() => (data ? data[1] : undefined), [data])
|
||||
|
||||
return (
|
||||
<>
|
||||
{asset && collection ? (
|
||||
<AssetContainer>
|
||||
<AssetDetails tokenId={tokenId} contractAddress={contractAddress} />
|
||||
<AssetPriceDetails />
|
||||
<AssetDetails collection={collection} asset={asset} />
|
||||
<AssetPriceDetails collection={collection} asset={asset} />
|
||||
</AssetContainer>
|
||||
) : (
|
||||
<div>Holder for loading ...</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -24,3 +24,7 @@ export const getAssetHref = (asset: GenieAsset | WalletAsset, origin?: DetailsOr
|
||||
: (asset as WalletAsset).asset_contract.address
|
||||
return `/nfts/asset/${address}/${asset.tokenId}${origin ? `?origin=${origin}` : ''}`
|
||||
}
|
||||
|
||||
export const getMarketplaceIcon = (marketplace: string) => {
|
||||
return `/nft/svgs/marketplaces/${marketplace}.svg`
|
||||
}
|
||||
|
@ -10,5 +10,6 @@ export * from './listNfts'
|
||||
export * from './putCommas'
|
||||
export * from './rarity'
|
||||
export * from './roundAndPluralize'
|
||||
export * from './timeSince'
|
||||
export * from './transactionResponse'
|
||||
export * from './updatedAssets'
|
||||
|
@ -22,3 +22,18 @@ export function timeSince(date: Date, min?: boolean) {
|
||||
|
||||
return roundAndPluralize(interval, 'sec')
|
||||
}
|
||||
|
||||
const MINUTE = 1000 * 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = 24 * HOUR
|
||||
|
||||
export const timeLeft = (targetDate: Date): string => {
|
||||
const countDown = new Date(targetDate).getTime() - new Date().getTime()
|
||||
const days = Math.floor(countDown / DAY)
|
||||
const hours = Math.floor((countDown % DAY) / HOUR)
|
||||
const minutes = Math.floor((countDown % HOUR) / MINUTE)
|
||||
|
||||
return `${days !== 0 ? roundAndPluralize(days, 'day') : ''} ${
|
||||
hours !== 0 ? roundAndPluralize(hours, 'hour') : ''
|
||||
} ${roundAndPluralize(minutes, 'minute')}`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user