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:
Jack Short 2022-10-11 09:53:33 -07:00 committed by GitHub
parent 8ceabd513c
commit d704e78223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 18 deletions

@ -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')}`
}