chore: Merging details part 3 (#4637)

* Merging details part 3



Co-authored-by: Alex Ball <alexball@UNISWAP-MAC-038.local>
This commit is contained in:
aballerr 2022-09-19 09:32:11 -04:00 committed by GitHub
parent 02b617d297
commit e7d498c95e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 257 additions and 42 deletions

@ -1,22 +1,61 @@
import { BagItem } from 'nft/types' import { v4 as uuidv4 } from 'uuid'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
interface BagState { import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from '../types'
type BagState = {
itemsInBag: BagItem[] itemsInBag: BagItem[]
addAssetToBag: (asset: UpdatedGenieAsset) => void
removeAssetFromBag: (asset: UpdatedGenieAsset) => void
isLocked: boolean
setLocked: (isLocked: boolean) => void
bagExpanded: boolean bagExpanded: boolean
toggleBag: () => void toggleBag: () => void
} }
export const useBag = create<BagState>()( export const useBag = create<BagState>()(
devtools( devtools(
(set) => ({ (set, get) => ({
bagExpanded: false, bagExpanded: false,
itemsInBag: [],
toggleBag: () => toggleBag: () =>
set(({ bagExpanded }) => ({ set(({ bagExpanded }) => ({
bagExpanded: !bagExpanded, bagExpanded: !bagExpanded,
})), })),
isLocked: false,
setLocked: (_isLocked) =>
set(() => ({
isLocked: _isLocked,
})),
itemsInBag: [],
addAssetToBag: (asset) =>
set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag }
const assetWithId = { asset: { id: uuidv4(), ...asset }, status: BagItemStatus.ADDED_TO_BAG }
if (itemsInBag.length === 0)
return {
itemsInBag: [assetWithId],
bagStatus: BagStatus.ADDING_TO_BAG,
}
else
return {
itemsInBag: [...itemsInBag, assetWithId],
bagStatus: BagStatus.ADDING_TO_BAG,
}
}),
removeAssetFromBag: (asset) => {
set(({ itemsInBag }) => {
if (get().isLocked) return { 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 }
itemsCopy.splice(index, 1)
return { itemsInBag: itemsCopy }
})
},
}), }),
{ name: 'useBag' } { name: 'useBag' }
) )

@ -0,0 +1,31 @@
import { useEffect, useState } from 'react'
const MINUTE = 1000 * 60
const HOUR = MINUTE * 60
const DAY = 24 * HOUR
const getReturnValues = (countDown: number): [number, number, number, number] => {
// calculate time left
const days = Math.floor(countDown / DAY)
const hours = Math.floor((countDown % DAY) / HOUR)
const minutes = Math.floor((countDown % HOUR) / MINUTE)
const seconds = Math.floor((countDown % MINUTE) / 1000)
return [days, hours, minutes, seconds]
}
export const useTimeout = (targetDate: Date) => {
const countDownDate = new Date(targetDate).getTime()
const [countDown, setCountDown] = useState<number>(countDownDate - new Date().getTime())
useEffect(() => {
const interval = setInterval(() => {
setCountDown(countDownDate - new Date().getTime())
}, 1000)
return () => clearInterval(interval)
}, [countDownDate])
return getReturnValues(countDown)
}

@ -35,7 +35,7 @@ export const marketplace = sprinkles({ borderRadius: '4' })
export const tab = style([ export const tab = style([
subhead, subhead,
sprinkles({ color: 'darkGray', border: 'none', padding: '0', background: 'transparent' }), sprinkles({ color: 'darkGray', border: 'none', padding: '0', background: 'transparent', cursor: 'pointer' }),
{ {
selectors: { selectors: {
'&[data-active="true"]': { '&[data-active="true"]': {

@ -1,3 +1,4 @@
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx' import clsx from 'clsx'
import useENSName from 'hooks/useENSName' import useENSName from 'hooks/useENSName'
import qs from 'query-string' import qs from 'query-string'
@ -7,22 +8,27 @@ import { useQuery } from 'react-query'
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import { useSpring } from 'react-spring/web' import { useSpring } from 'react-spring/web'
import { MouseoverTooltip } from '../../../components/Tooltip/index'
import { AnimatedBox, Box } from '../../components/Box' import { AnimatedBox, Box } from '../../components/Box'
import { CollectionProfile } from '../../components/details/CollectionProfile' import { CollectionProfile } from '../../components/details/CollectionProfile'
import { Details } from '../../components/details/Details' import { Details } from '../../components/details/Details'
import { Traits } from '../../components/details/Traits' import { Traits } from '../../components/details/Traits'
import { Center, Column, Row } from '../../components/Flex' import { Center, Column, Row } from '../../components/Flex'
import { CloseDropDownIcon, CornerDownLeftIcon, ShareIcon } from '../../components/icons' import { CloseDropDownIcon, CornerDownLeftIcon, Eth2Icon, ShareIcon, SuspiciousIcon } from '../../components/icons'
import { ExpandableText } from '../../components/layout/ExpandableText' import { ExpandableText } from '../../components/layout/ExpandableText'
import { header2 } from '../../css/common.css' import { badge, bodySmall, caption, header2, subhead } from '../../css/common.css'
import { themeVars } from '../../css/sprinkles.css' import { themeVars } from '../../css/sprinkles.css'
import { useBag } from '../../hooks' import { useBag } from '../../hooks'
import { useTimeout } from '../../hooks/useTimeout'
import { fetchSingleAsset } from '../../queries' import { fetchSingleAsset } from '../../queries'
import { CollectionInfoForAsset, GenieAsset } from '../../types' import { CollectionInfoForAsset, GenieAsset, SellOrder } from '../../types'
import { shortenAddress } from '../../utils/address' import { shortenAddress } from '../../utils/address'
import { formatEthPrice } from '../../utils/currency'
import { isAssetOwnedByUser } from '../../utils/isAssetOwnedByUser'
import { isAudio } from '../../utils/isAudio' import { isAudio } from '../../utils/isAudio'
import { isVideo } from '../../utils/isVideo' import { isVideo } from '../../utils/isVideo'
import { rarityProviderLogo } from '../../utils/rarity' import { fallbackProvider, rarityProviderLogo } from '../../utils/rarity'
import { toSignificant } from '../../utils/toSignificant'
import * as styles from './Asset.css' import * as styles from './Asset.css'
const AudioPlayer = ({ const AudioPlayer = ({
@ -49,6 +55,28 @@ const AudioPlayer = ({
) )
} }
const formatter = Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'short' })
const CountdownTimer = ({ sellOrder }: { sellOrder: SellOrder }) => {
const { date, expires } = useMemo(() => {
const date = new Date(sellOrder.orderClosingDate)
return {
date,
expires: formatter.format(date),
}
}, [sellOrder])
const [days, hours, minutes, seconds] = useTimeout(date)
return (
<MouseoverTooltip text={<Box fontSize="12">Expires {expires}</Box>}>
<Box as="span" fontWeight="normal" className={caption} color="darkGray">
Expires: {days !== 0 ? `${days} days` : ''} {hours !== 0 ? `${hours} hours` : ''} {minutes} minutes {seconds}{' '}
seconds
</Box>
</MouseoverTooltip>
)
}
const AssetView = ({ const AssetView = ({
mediaType, mediaType,
asset, asset,
@ -85,6 +113,9 @@ const Asset = () => {
) )
const { pathname, search } = useLocation() const { pathname, search } = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const addAssetToBag = useBag((state) => state.addAssetToBag)
const removeAssetFromBag = useBag((state) => state.removeAssetFromBag)
const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
const [creatorAddress, setCreatorAddress] = useState('') const [creatorAddress, setCreatorAddress] = useState('')
const [ownerAddress, setOwnerAddress] = useState('') const [ownerAddress, setOwnerAddress] = useState('')
@ -98,13 +129,11 @@ const Asset = () => {
gridWidthOffset: bagExpanded ? 324 : 0, gridWidthOffset: bagExpanded ? 324 : 0,
}) })
const [showTraits, setShowTraits] = useState(true) const [showTraits, setShowTraits] = useState(true)
const [isSelected, setSelected] = useState(false)
const [isOwned, setIsOwned] = useState(false)
const { account: address, provider } = useWeb3React()
useEffect(() => { const { rarityProvider, rarityLogo } = useMemo(
if (asset.creator) setCreatorAddress(asset.creator.address)
if (asset.owner) setOwnerAddress(asset.owner)
}, [asset])
const { rarityProvider } = useMemo(
() => () =>
asset.rarity asset.rarity
? { ? {
@ -126,6 +155,29 @@ const Asset = () => {
return MediaType.Image return MediaType.Image
}, [asset]) }, [asset])
useEffect(() => {
if (asset.creator) setCreatorAddress(asset.creator.address)
if (asset.owner) setOwnerAddress(asset.owner)
}, [asset])
useEffect(() => {
setSelected(
!!itemsInBag.find((item) => item.asset.tokenId === asset.tokenId && item.asset.address === asset.address)
)
}, [asset, itemsInBag])
useEffect(() => {
if (provider) {
isAssetOwnedByUser({
tokenId: asset.tokenId,
userAddress: address || '',
assetAddress: asset.address,
tokenType: asset.tokenType,
provider,
}).then(setIsOwned)
}
}, [asset, address, provider])
return ( return (
<AnimatedBox <AnimatedBox
style={{ style={{
@ -149,15 +201,43 @@ const Asset = () => {
</Column> </Column>
<Column className={clsx(styles.column, styles.columnRight)} width="full"> <Column className={clsx(styles.column, styles.columnRight)} width="full">
<Column> <Column>
<Row marginBottom="8" alignItems="center" justifyContent={rarityProvider ? 'space-between' : 'flex-end'}> <Row
marginBottom="8"
alignItems="center"
textAlign="center"
justifyContent={rarityProvider ? 'space-between' : 'flex-end'}
>
{rarityProvider && (
<MouseoverTooltip
text={
<Row gap="4">
<img src={rarityLogo} width={16} alt={rarityProvider.provider} />
Ranking by{' '}
{asset.rarity?.primaryProvider === 'Genie' ? fallbackProvider : asset.rarity?.primaryProvider}
</Row>
}
>
<Center
paddingLeft="6"
paddingRight="4"
className={badge}
backgroundColor="lightGray"
color="blackBlue"
borderRadius="4"
>
#{rarityProvider.rank} <img src="/nft/svgs/rarity.svg" height={15} width={15} alt="Rarity rank" />
</Center>
</MouseoverTooltip>
)}
<Row gap="12"> <Row gap="12">
<Center <Center
as="button" as="button"
padding="0" padding="0"
border="none" border="none"
background="transparent" background="transparent"
cursor="pointer"
onClick={async () => { onClick={async () => {
await navigator.clipboard.writeText(window.location.hostname + pathname) await navigator.clipboard.writeText(`${window.location.hostname}/#${pathname}`)
}} }}
> >
<ShareIcon /> <ShareIcon />
@ -173,13 +253,13 @@ const Asset = () => {
cursor="pointer" cursor="pointer"
onClick={() => { onClick={() => {
if (!parsed.origin || parsed.origin === 'collection') { if (!parsed.origin || parsed.origin === 'collection') {
navigate(`/nft/collection/${asset.address}`, undefined) navigate(`/nfts/collection/${asset.address}`)
} else if (parsed.origin === 'sell') { } else if (parsed.origin === 'sell') {
navigate('/nft/sell', undefined) navigate('/nfts/sell', undefined)
} else if (parsed.origin === 'explore') { } else if (parsed.origin === 'explore') {
navigate(`/nft`, undefined) navigate(`/nfts`, undefined)
} else if (parsed.origin === 'activity') { } else if (parsed.origin === 'activity') {
navigate(`/nft/collection/${asset.address}/activity`, undefined) navigate(`/nfts/collection/${asset.address}/activity`, undefined)
} }
}} }}
> >
@ -192,6 +272,14 @@ const Asset = () => {
</Row> </Row>
</Row> </Row>
<Row as="h1" marginTop="0" marginBottom="12" gap="2" className={header2}> <Row as="h1" marginTop="0" marginBottom="12" gap="2" className={header2}>
{asset.openseaSusFlag && (
<Box marginTop="8">
<MouseoverTooltip text={<Box fontWeight="normal">Reported for suspicious activity on OpenSea</Box>}>
<SuspiciousIcon height="30" width="30" viewBox="0 0 16 17" />
</MouseoverTooltip>
</Box>
)}
{asset.name || `${collection.collectionName} #${asset.tokenId}`} {asset.name || `${collection.collectionName} #${asset.tokenId}`}
</Row> </Row>
{collection.collectionDescription ? ( {collection.collectionDescription ? (
@ -252,28 +340,85 @@ const Asset = () => {
</a> </a>
) : null} ) : null}
</Row> </Row>
<Row gap="32" marginBottom="20">
<button data-active={showTraits} onClick={() => setShowTraits(true)} className={styles.tab}>
Traits
</button>
<button data-active={!showTraits} onClick={() => setShowTraits(false)} className={styles.tab}>
Details
</button>
</Row>
{showTraits ? (
<Traits collectionAddress={asset.address} traits={asset.traits ?? []} />
) : (
<Details
contractAddress={contractAddress}
tokenId={tokenId}
tokenType={asset.tokenType}
blockchain="Ethereum"
metadataUrl={asset.externalLink}
totalSupply={collection.totalSupply}
/>
)}
</Column> </Column>
{asset.priceInfo && !isOwned ? (
<Row
marginTop="8"
marginBottom="40"
justifyContent="space-between"
borderRadius="12"
paddingTop="16"
paddingBottom="16"
paddingLeft="16"
paddingRight="24"
background="accentActiveSoft"
>
<Column justifyContent="flex-start" gap="8">
<Row gap="12" as="a" target="_blank" rel="norefferer">
<a href={asset.sellorders[0].marketplaceUrl} rel="noreferrer" target="_blank">
<img
className={styles.marketplace}
src={`/nft/svgs/marketplaces/${asset.sellorders[0].marketplace}.svg`}
height={16}
width={16}
alt="Markeplace"
/>
</a>
<Row as="span" className={subhead} color="blackBlue">
{formatEthPrice(asset.priceInfo.ETHPrice)} <Eth2Icon />
</Row>
<Box as="span" color="darkGray" className={bodySmall}>
${toSignificant(asset.priceInfo.USDPrice)}
</Box>
</Row>
{asset.sellorders?.[0].orderClosingDate ? <CountdownTimer sellOrder={asset.sellorders[0]} /> : null}
</Column>
<Box
as="button"
paddingTop="14"
paddingBottom="14"
fontWeight="medium"
textAlign="center"
fontSize="14"
style={{ width: '244px' }}
color={isSelected ? 'genieBlue' : 'explicitWhite'}
border="none"
borderRadius="12"
background={isSelected ? 'explicitWhite' : 'genieBlue'}
transition="250"
boxShadow={{ hover: 'elevation' }}
onClick={() => {
if (isSelected) {
removeAssetFromBag(asset)
} else addAssetToBag(asset)
setSelected((x) => !x)
}}
>
{isSelected ? 'Added to Bag' : 'Buy Now'}
</Box>
</Row>
) : null}
<Row gap="32" marginBottom="20">
<button data-active={showTraits} onClick={() => setShowTraits(true)} className={styles.tab}>
Traits
</button>
<button data-active={!showTraits} onClick={() => setShowTraits(false)} className={styles.tab}>
Details
</button>
</Row>
{showTraits ? (
<Traits collectionAddress={asset.address} traits={asset.traits ?? []} />
) : (
<Details
contractAddress={contractAddress}
tokenId={tokenId}
tokenType={asset.tokenType}
blockchain="Ethereum"
metadataUrl={asset.externalLink}
totalSupply={collection.totalSupply}
/>
)}
</Column> </Column>
</div> </div>
</AnimatedBox> </AnimatedBox>