Commit 4b1b6098 authored by aballerr's avatar aballerr Committed by GitHub

feat: Details implementation (#5059)

* Details Page Update
parent 53a6acc1
...@@ -93,7 +93,7 @@ export const CollectionRow = ({ ...@@ -93,7 +93,7 @@ export const CollectionRow = ({
<Box className={styles.primaryText}>{collection.name}</Box> <Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />} {collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row> </Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats?.total_supply)} items</Box> <Box className={styles.secondaryText}>{putCommas(collection?.stats?.total_supply ?? 0)} items</Box>
</Column> </Column>
</Row> </Row>
{collection.stats?.floor_price ? ( {collection.stats?.floor_price ? (
......
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { Trait } from 'nft/hooks/useCollectionFilters' import { GenieCollection, Trait } from 'nft/types'
import { GenieCollection } from 'nft/types'
import { useLazyLoadQuery } from 'react-relay' import { useLazyLoadQuery } from 'react-relay'
import { CollectionQuery } from './__generated__/CollectionQuery.graphql' import { CollectionQuery } from './__generated__/CollectionQuery.graphql'
...@@ -122,8 +121,8 @@ export function useCollectionQuery(address: string): GenieCollection | undefined ...@@ -122,8 +121,8 @@ export function useCollectionQuery(address: string): GenieCollection | undefined
: {}, : {},
traits, traits,
// marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports // marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports
imageUrl: queryCollection?.image?.url, imageUrl: queryCollection?.image?.url ?? '',
twitter: queryCollection?.twitterName ?? undefined, twitterUrl: queryCollection?.twitterName ?? '',
instagram: queryCollection?.instagramName ?? undefined, instagram: queryCollection?.instagramName ?? undefined,
discordUrl: queryCollection?.discordUrl ?? undefined, discordUrl: queryCollection?.discordUrl ?? undefined,
externalUrl: queryCollection?.homepageUrl ?? undefined, externalUrl: queryCollection?.homepageUrl ?? undefined,
......
import { parseEther } from '@ethersproject/units' import { parseEther } from '@ethersproject/units'
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { Trait } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types'
import { useLazyLoadQuery } from 'react-relay' import { useLazyLoadQuery } from 'react-relay'
...@@ -141,14 +140,14 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset, ...@@ -141,14 +140,14 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
}) })
: undefined, : undefined,
}, },
owner: asset?.ownerAddress ?? undefined, owner: { address: asset?.ownerAddress ?? '' },
creator: { creator: {
profile_img_url: asset?.creator?.profileImage?.url, profile_img_url: asset?.creator?.profileImage?.url ?? '',
address: asset?.creator?.address, address: asset?.creator?.address ?? '',
}, },
metadataUrl: asset?.metadataUrl ?? undefined, metadataUrl: asset?.metadataUrl ?? '',
traits: asset?.traits?.map((trait) => { traits: asset?.traits?.map((trait) => {
return { trait_type: trait.name ?? undefined, trait_value: trait.value ?? undefined } as Trait return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
}), }),
}, },
{ {
......
...@@ -29,6 +29,7 @@ import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTx ...@@ -29,6 +29,7 @@ import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTx
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query' import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import * as styles from './Bag.css' import * as styles from './Bag.css'
import { BagContent } from './BagContent' import { BagContent } from './BagContent'
...@@ -41,6 +42,15 @@ interface SeparatorProps { ...@@ -41,6 +42,15 @@ interface SeparatorProps {
show?: boolean show?: boolean
} }
const DetailsPageBackground = styled.div`
position: fixed;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(12px);
top: 72px;
width: 100%;
height: 100%;
`
const ScrollingIndicator = ({ top, show }: SeparatorProps) => ( const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
<Box <Box
marginX="16" marginX="16"
...@@ -82,6 +92,8 @@ const Bag = () => { ...@@ -82,6 +92,8 @@ const Bag = () => {
const shouldShowBag = isNFTPage || isProfilePage const shouldShowBag = isNFTPage || isProfilePage
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isDetailsPage = pathname.includes('/nfts/asset/')
const sendTransaction = useSendTransaction((state) => state.sendTransaction) const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state) const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState) const setTransactionState = useSendTransaction((state) => state.setState)
...@@ -304,7 +316,11 @@ const Bag = () => { ...@@ -304,7 +316,11 @@ const Bag = () => {
<ListingModal /> <ListingModal />
)} )}
</Column> </Column>
{isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />} {isDetailsPage ? (
<DetailsPageBackground onClick={toggleBag} />
) : (
isOpen && <Overlay onClick={() => (!bagIsLocked ? setModalIsOpen(false) : undefined)} />
)}
</Portal> </Portal>
) : null} ) : null}
</> </>
......
...@@ -79,7 +79,6 @@ export const detailsName = style([ ...@@ -79,7 +79,6 @@ export const detailsName = style([
export const eventDetail = style([ export const eventDetail = style([
subhead, subhead,
sprinkles({ sprinkles({
marginBottom: '4',
gap: '8', gap: '8',
}), }),
{ {
......
...@@ -48,7 +48,7 @@ const initialFilterState = { ...@@ -48,7 +48,7 @@ const initialFilterState = {
[ActivityEventType.CancelListing]: false, [ActivityEventType.CancelListing]: false,
} }
const reduceFilters = (state: typeof initialFilterState, action: { eventType: ActivityEventType }) => { export const reduceFilters = (state: typeof initialFilterState, action: { eventType: ActivityEventType }) => {
return { ...state, [action.eventType]: !state[action.eventType] } return { ...state, [action.eventType]: !state[action.eventType] }
} }
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
ActivityListingIcon, ActivityListingIcon,
ActivitySaleIcon, ActivitySaleIcon,
ActivityTransferIcon, ActivityTransferIcon,
CancelListingIcon,
RarityVerifiedIcon, RarityVerifiedIcon,
} from 'nft/components/icons' } from 'nft/components/icons'
import { import {
...@@ -157,7 +158,7 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel ...@@ -157,7 +158,7 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel
) )
} }
const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => { export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => {
return ( return (
<Box <Box
as="img" as="img"
...@@ -204,8 +205,9 @@ interface EventCellProps { ...@@ -204,8 +205,9 @@ interface EventCellProps {
eventType: ActivityEventType eventType: ActivityEventType
eventTimestamp?: number eventTimestamp?: number
eventTransactionHash?: string eventTransactionHash?: string
eventOnly?: boolean
price?: string price?: string
isMobile: boolean isMobile?: boolean
} }
const renderEventIcon = (eventType: ActivityEventType) => { const renderEventIcon = (eventType: ActivityEventType) => {
...@@ -216,6 +218,8 @@ const renderEventIcon = (eventType: ActivityEventType) => { ...@@ -216,6 +218,8 @@ const renderEventIcon = (eventType: ActivityEventType) => {
return <ActivitySaleIcon width={16} height={16} /> return <ActivitySaleIcon width={16} height={16} />
case ActivityEventType.Transfer: case ActivityEventType.Transfer:
return <ActivityTransferIcon width={16} height={16} /> return <ActivityTransferIcon width={16} height={16} />
case ActivityEventType.CancelListing:
return <CancelListingIcon width={16} height={16} />
default: default:
return null return null
} }
...@@ -237,13 +241,20 @@ const eventColors = (eventType: ActivityEventType) => { ...@@ -237,13 +241,20 @@ const eventColors = (eventType: ActivityEventType) => {
[ActivityEventType.Listing]: 'gold', [ActivityEventType.Listing]: 'gold',
[ActivityEventType.Sale]: 'green', [ActivityEventType.Sale]: 'green',
[ActivityEventType.Transfer]: 'violet', [ActivityEventType.Transfer]: 'violet',
[ActivityEventType.CancelListing]: 'error', [ActivityEventType.CancelListing]: 'accentFailure',
} }
return activityEvents[eventType] as 'gold' | 'green' | 'violet' | 'accentFailure' return activityEvents[eventType] as 'gold' | 'green' | 'violet' | 'accentFailure'
} }
export const EventCell = ({ eventType, eventTimestamp, eventTransactionHash, price, isMobile }: EventCellProps) => { export const EventCell = ({
eventType,
eventTimestamp,
eventTransactionHash,
eventOnly,
price,
isMobile,
}: EventCellProps) => {
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price]) const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
return ( return (
<Column height="full" justifyContent="center" gap="4"> <Column height="full" justifyContent="center" gap="4">
...@@ -251,7 +262,7 @@ export const EventCell = ({ eventType, eventTimestamp, eventTransactionHash, pri ...@@ -251,7 +262,7 @@ export const EventCell = ({ eventType, eventTimestamp, eventTransactionHash, pri
{renderEventIcon(eventType)} {renderEventIcon(eventType)}
{ActivityEventTypeDisplay[eventType]} {ActivityEventTypeDisplay[eventType]}
</Row> </Row>
{eventTimestamp && isValidDate(eventTimestamp) && !isMobile && ( {eventTimestamp && isValidDate(eventTimestamp) && !isMobile && !eventOnly && (
<Row className={styles.eventTime}> <Row className={styles.eventTime}>
{getTimeDifference(eventTimestamp.toString())} {getTimeDifference(eventTimestamp.toString())}
{eventTransactionHash && <ExternalLinkIcon transactionHash={eventTransactionHash} />} {eventTransactionHash && <ExternalLinkIcon transactionHash={eventTransactionHash} />}
...@@ -301,9 +312,10 @@ interface RankingProps { ...@@ -301,9 +312,10 @@ interface RankingProps {
rarity: TokenRarity rarity: TokenRarity
collectionName: string collectionName: string
rarityVerified: boolean rarityVerified: boolean
details?: boolean
} }
const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => { const Ranking = ({ details, rarity, collectionName, rarityVerified }: RankingProps) => {
const rarityProviderLogo = getRarityProviderLogo(rarity.source) const rarityProviderLogo = getRarityProviderLogo(rarity.source)
return ( return (
......
...@@ -76,8 +76,8 @@ const MobileSocialsPopover = ({ ...@@ -76,8 +76,8 @@ const MobileSocialsPopover = ({
</Box> </Box>
</MobileSocialsIcon> </MobileSocialsIcon>
) : null} ) : null}
{collectionStats.twitter ? ( {collectionStats.twitterUrl ? (
<MobileSocialsIcon href={'https://twitter.com/' + collectionStats.twitter}> <MobileSocialsIcon href={'https://twitter.com/' + collectionStats.twitterUrl}>
<Box margin="auto" paddingTop="6"> <Box margin="auto" paddingTop="6">
<TwitterIcon <TwitterIcon
fill={themeVars.colors.textSecondary} fill={themeVars.colors.textSecondary}
...@@ -161,8 +161,8 @@ const CollectionName = ({ ...@@ -161,8 +161,8 @@ const CollectionName = ({
/> />
</SocialsIcon> </SocialsIcon>
) : null} ) : null}
{collectionStats.twitter ? ( {collectionStats.twitterUrl ? (
<SocialsIcon href={'https://twitter.com/' + collectionStats.twitter}> <SocialsIcon href={'https://twitter.com/' + collectionStats.twitterUrl}>
<TwitterIcon <TwitterIcon
fill={themeVars.colors.textSecondary} fill={themeVars.colors.textSecondary}
color={themeVars.colors.textSecondary} color={themeVars.colors.textSecondary}
...@@ -186,7 +186,7 @@ const CollectionName = ({ ...@@ -186,7 +186,7 @@ const CollectionName = ({
</Row> </Row>
{isMobile && {isMobile &&
(collectionStats.discordUrl || (collectionStats.discordUrl ||
collectionStats.twitter || collectionStats.twitterUrl ||
collectionStats.instagram || collectionStats.instagram ||
collectionStats.externalUrl) && ( collectionStats.externalUrl) && (
<MobileSocialsPopover <MobileSocialsPopover
......
import { ActivityEventResponse } from 'nft/types'
import { shortenAddress } from 'nft/utils/address'
import { formatEthPrice } from 'nft/utils/currency'
import { getTimeDifference } from 'nft/utils/date'
import { putCommas } from 'nft/utils/putCommas'
import styled from 'styled-components/macro'
import { EventCell } from '../collection/ActivityCells'
import { MarketplaceIcon } from '../collection/ActivityCells'
const TR = styled.tr`
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
width: 100%;
&:last-child {
border-bottom: none;
}
`
const TH = styled.th`
color: ${({ theme }) => theme.textSecondary};
font-weight: 600;
font-size: 14px;
line-height: 20px;
width: 20%;
@media (max-width: 960px) {
&:nth-child(4) {
display: none;
}
}
@media (max-width: 720px) {
&:nth-child(2) {
display: none;
}
}
`
const Table = styled.table`
border-collapse: collapse;
text-align: left;
width: 100%;
`
const TD = styled.td`
height: 56px;
padding: 8px 0px;
text-align: left;
vertical-align: middle;
width: 20%;
@media (max-width: 960px) {
&:nth-child(4) {
display: none;
}
}
@media (max-width: 720px) {
&:nth-child(2) {
display: none;
}
}
`
const PriceContainer = styled.div`
align-items: center;
display: flex;
gap: 8px;
`
const Link = styled.a`
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
`
const ActivityContainer = styled.div`
max-height: 310px;
overflow: auto;
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color: ${({ theme }) => `${theme.backgroundOutline} transparent`};
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
width: 4px;
}
::-webkit-scrollbar-thumb {
background: ${({ theme }) => theme.backgroundOutline};
border-radius: 8px;
}
`
const AssetActivity = ({ eventsData }: { eventsData: ActivityEventResponse | undefined }) => {
return (
<ActivityContainer id="activityContainer">
<Table>
<thead>
<TR>
<TH>Event</TH>
<TH>Price</TH>
<TH>By</TH>
<TH>To</TH>
<TH>Time</TH>
</TR>
</thead>
<tbody>
{eventsData?.events &&
eventsData.events.map((event, index) => {
const { eventTimestamp, eventType, fromAddress, marketplace, price, toAddress, transactionHash } = event
const formattedPrice = price ? putCommas(formatEthPrice(price)).toString() : null
return (
<TR key={index}>
<TD>
<EventCell
eventType={eventType}
eventTimestamp={eventTimestamp}
eventTransactionHash={transactionHash}
eventOnly
/>
</TD>
<TD>
{formattedPrice && (
<PriceContainer>
{marketplace && <MarketplaceIcon marketplace={marketplace} />}
{formattedPrice} ETH
</PriceContainer>
)}
</TD>
<TD>
{fromAddress && (
<Link
href={`https://etherscan.io/address/${fromAddress}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenAddress(fromAddress, 2, 4)}
</Link>
)}
</TD>
<TD>
{toAddress && (
<Link
href={`https://etherscan.io/address/${toAddress}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenAddress(toAddress, 2, 4)}
</Link>
)}
</TD>
<TD>{eventTimestamp && getTimeDifference(eventTimestamp.toString())}</TD>
</TR>
)
})}
</tbody>
</Table>
</ActivityContainer>
)
}
export default AssetActivity
...@@ -6,19 +6,18 @@ import { sprinkles, vars } from '../../css/sprinkles.css' ...@@ -6,19 +6,18 @@ import { sprinkles, vars } from '../../css/sprinkles.css'
export const image = style([ export const image = style([
sprinkles({ borderRadius: '20', height: 'full', alignSelf: 'center' }), sprinkles({ borderRadius: '20', height: 'full', alignSelf: 'center' }),
{ {
width: 'calc(90vh - 165px)', maxHeight: 'calc(90vh - 165px)',
height: 'calc(90vh - 165px)', minHeight: 400,
maxHeight: '678px', maxWidth: 780,
maxWidth: '678px',
boxShadow: `0px 20px 50px var(--shadow), 0px 10px 50px rgba(70, 115, 250, 0.2)`, boxShadow: `0px 20px 50px var(--shadow), 0px 10px 50px rgba(70, 115, 250, 0.2)`,
'@media': { '@media': {
'(max-width: 1024px)': { '(max-width: 1024px)': {
maxHeight: '64vh', maxHeight: '64vh',
maxWidth: '64vh',
}, },
'(max-width: 640px)': { '(max-width: 640px)': {
minHeight: 280,
maxHeight: '56vh', maxHeight: '56vh',
maxWidth: '56vh', maxWidth: '100%',
}, },
}, },
}, },
...@@ -81,8 +80,6 @@ export const columns = style([ ...@@ -81,8 +80,6 @@ export const columns = style([
]) ])
export const column = style({ export const column = style({
maxWidth: '50%',
width: '50%',
alignSelf: 'center', alignSelf: 'center',
'@media': { '@media': {
'(max-width: 1024px)': { '(max-width: 1024px)': {
......
This diff is collapsed.
import useCopyClipboard from 'hooks/useCopyClipboard'
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { putCommas } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address'
import { useCallback } from 'react'
import { Copy } from 'react-feather'
import styled from 'styled-components/macro'
const Details = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 40px;
@media (max-width: 600px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media (max-width: 450px) {
grid-template-columns: 1fr 1fr;
}
`
const Header = styled.div`
color: ${({ theme }) => theme.textSecondary};
font-size: 14px;
line-height: 20px;
`
const Body = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 14px;
line-height: 20px;
margin-top: 8px;
`
const Center = styled.span`
align-items: center;
cursor: pointer;
display: flex;
gap: 8px;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
`
const CreatorLink = styled.a`
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
`
const CopyIcon = styled(Copy)`
cursor: pointer;
`
const GridItem = ({ header, body }: { header: string; body: React.ReactNode }) => {
return (
<div>
<Header>{header}</Header>
<Body>{body}</Body>
</div>
)
}
const stringShortener = (text: string) => `${text.substring(0, 4)}...${text.substring(text.length - 4, text.length)}`
const DetailsContainer = ({ asset, collection }: { asset: GenieAsset; collection: CollectionInfoForAsset }) => {
const { address, tokenId, tokenType, creator } = asset
const { totalSupply } = collection
const [, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(address ?? '')
}, [address, setCopied])
return (
<Details>
<GridItem
header="Contract address"
body={
<Center onClick={copy}>
{shortenAddress(address, 2, 4)} <CopyIcon size={13} />
</Center>
}
/>
<GridItem header="Token ID" body={tokenId.length > 9 ? stringShortener(tokenId) : tokenId} />
<GridItem header="Token standard" body={tokenType} />
<GridItem header="Blockchain" body="Ethereum" />
<GridItem header="Total supply" body={`${putCommas(totalSupply ?? 0)}`} />
<GridItem
header="Creator"
body={
creator?.address && (
<CreatorLink
href={`https://etherscan.io/address/${creator.address}`}
rel="noopener noreferrer"
target="_blank"
>
{shortenAddress(creator.address, 2, 4)}
</CreatorLink>
)
}
/>
</Details>
)
}
export default DetailsContainer
import { useState } from 'react'
import { ChevronDown, ChevronUp } from 'react-feather'
import styled, { css } from 'styled-components/macro'
const Header = styled.div<{ isOpen: boolean }>`
display: flex;
border-radius: ${({ isOpen }) => (isOpen ? '16px 16px 0px 0px' : '16px')};
justify-content: space-between;
background-color: ${({ theme }) => theme.backgroundSurface};
padding: 14px 20px;
cursor: pointer;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-top: 28px;
width: 100%;
align-items: center;
&:hover {
background-color: ${({ theme }) => theme.stateOverlayHover};
}
&:active {
background-color: ${({ theme }) => theme.stateOverlayPressed};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => css`background-color ${duration.medium} ${timing.ease}`};
`
const PrimaryHeader = styled.span`
display: flex;
align-items: center;
gap: 16px;
color: ${({ theme }) => theme.textPrimary};
font-weight: 500;
line-height: 28px;
font-size: 20px;
`
const SecondaryHeader = styled.span`
font-size: 12px;
color: ${({ theme }) => theme.textSecondary};
`
const SecondaryHeaderContainer = styled.span`
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
color: ${({ theme }) => theme.textPrimary};
`
const ContentContainer = styled.div`
padding: 20px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-top: none;
border-radius: 0px 0px 16px 16px;
background-color: ${({ theme }) => theme.backgroundSurface}; ;
`
const InfoContainer = ({
children,
primaryHeader,
secondaryHeader,
defaultOpen,
}: {
children: JSX.Element
primaryHeader: string
secondaryHeader: React.ReactNode
defaultOpen?: boolean
}) => {
const [isOpen, setIsOpen] = useState(!!defaultOpen)
return (
<div>
<Header isOpen={isOpen} onClick={() => setIsOpen(!isOpen)}>
<PrimaryHeader>
{primaryHeader} <SecondaryHeader>{secondaryHeader}</SecondaryHeader>
</PrimaryHeader>
<SecondaryHeaderContainer>{isOpen ? <ChevronUp /> : <ChevronDown />}</SecondaryHeaderContainer>
</Header>
{isOpen && <ContentContainer>{children}</ContentContainer>}
</div>
)
}
export default InfoContainer
import { style } from '@vanilla-extract/css'
import { sprinkles } from '../../css/sprinkles.css'
export const grid = style([
sprinkles({ gap: '16', display: 'grid' }),
{
gridTemplateColumns: 'repeat(4, 1fr)',
'@media': {
'(max-width: 1536px)': {
gridTemplateColumns: 'repeat(3, 1fr)',
},
'(max-width: 640px)': {
gridTemplateColumns: 'repeat(2, 1fr)',
},
},
},
])
import { Trait } from 'nft/hooks'
import qs from 'query-string'
import { badge } from '../../css/common.css'
import { Box } from '../Box'
import { Column } from '../Flex'
import * as styles from './Traits.css'
const TraitRow: React.FC<Trait> = ({ trait_type, trait_value }: Trait) => (
<Column backgroundColor="backgroundSurface" padding="16" gap="4" borderRadius="12">
<Box
as="span"
className={badge}
color="textSecondary"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
style={{ textTransform: 'uppercase' }}
maxWidth={{ sm: '120', md: '160' }}
>
{trait_type}
</Box>
<Box
as="span"
color="textPrimary"
fontSize="16"
fontWeight="normal"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
maxWidth={{ sm: '120', md: '160' }}
>
{trait_value}
</Box>
</Column>
)
export const Traits = ({ traits, collectionAddress }: { traits: Trait[]; collectionAddress: string }) => (
<div className={styles.grid}>
{traits.length === 0
? 'No traits'
: traits.map((item) => {
const params = qs.stringify(
{ traits: [`("${item.trait_type}","${item.trait_value}")`] },
{
arrayFormat: 'comma',
}
)
return (
<a
key={`${item.trait_type}-${item.trait_value}`}
href={`#/nfts/collection/${collectionAddress}?${params}`}
style={{ textDecoration: 'none' }}
>
<TraitRow trait_type={item.trait_type} trait_value={item.trait_value} />
</a>
)
})}
</div>
)
import { GenieAsset, Trait } from 'nft/types'
import qs from 'query-string'
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
const Grid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 16px;
max-width: 780px;
@media (max-width: 960px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media (max-width: 420px) {
grid-template-columns: 1fr 1fr;
}
`
const GridItemContainer = styled(Link)`
background-color: ${({ theme }) => theme.backgroundInteractive};
border-radius: 12px;
cursor: pointer;
padding: 12px;
text-decoration: none;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
min-width: 0;
`
const TraitType = styled.div`
color: ${({ theme }) => theme.textSecondary};
font-weight: 600;
font-size: 10px;
line-height: 12px;
white-space: nowrap;
width: 100%;
`
const TraitValue = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 16px;
line-height: 24px;
margin-top: 4px;
display: inline-block;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
`
const GridItem = ({ trait, collectionAddress }: { trait: Trait; collectionAddress: string }) => {
const { trait_type, trait_value } = trait
const params = qs.stringify(
{ traits: [`("${trait_type}","${trait_value}")`] },
{
arrayFormat: 'comma',
}
)
return (
<GridItemContainer to={`/nfts/collection/${collectionAddress}?${params}`}>
<TraitType>{trait_type}</TraitType>
<TraitValue>{trait_value}</TraitValue>
</GridItemContainer>
)
}
const TraitsContainer = ({ asset }: { asset: GenieAsset }) => {
const traits = useMemo(() => asset.traits?.sort((a, b) => a.trait_type.localeCompare(b.trait_type)), [asset])
return (
<Grid>
{traits?.map((trait) => {
return <GridItem key={trait.trait_type} trait={trait} collectionAddress={asset.address} />
})}
</Grid>
)
}
export default TraitsContainer
...@@ -1495,11 +1495,10 @@ export const EmptyNFTWalletIcon = (props: SVGProps) => ( ...@@ -1495,11 +1495,10 @@ export const EmptyNFTWalletIcon = (props: SVGProps) => (
) )
export const CancelListingIcon = (props: SVGProps) => ( export const CancelListingIcon = (props: SVGProps) => (
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path <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" d="M12.6667 6L13.3933 6.72667C13.6417 6.97648 13.7811 7.31442 13.7811 7.66667C13.7811 8.01891 13.6417 8.35685 13.3933 8.60667L8.61333 13.3867C8.4895 13.5106 8.34245 13.609 8.18059 13.6761C8.01872 13.7432 7.84522 13.7777 7.67 13.7777C7.49478 13.7777 7.32128 13.7432 7.15941 13.6761C6.99755 13.609 6.8505 13.5106 6.72667 13.3867L6 12.6667M8.8 2.13333L7.66667 1H1V7.66667L2.13333 8.8M13.7811 1L1.10498 13.6761M4.33333 4.33333H4.33889"
stroke="currentColor" stroke="currentColor"
strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
......
...@@ -5,6 +5,7 @@ import { useDetailsQuery } from 'graphql/data/nft/Details' ...@@ -5,6 +5,7 @@ import { useDetailsQuery } from 'graphql/data/nft/Details'
import { AssetDetails } from 'nft/components/details/AssetDetails' import { AssetDetails } from 'nft/components/details/AssetDetails'
import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
import { fetchSingleAsset } from 'nft/queries' import { fetchSingleAsset } from 'nft/queries'
import { CollectionStatsFetcher } from 'nft/queries'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
...@@ -12,8 +13,20 @@ import styled from 'styled-components/macro' ...@@ -12,8 +13,20 @@ import styled from 'styled-components/macro'
const AssetContainer = styled.div` const AssetContainer = styled.div`
display: flex; display: flex;
padding-right: 116px; width: 100%;
padding-left: 116px; justify-content: center;
gap: 60px;
padding: 48px 40px 0 40px;
`
const AssetPriceDetailsContainer = styled.div`
min-width: 360px;
position: relative;
padding-right: 100px;
@media (max-width: 960px) {
display: none;
}
` `
const Asset = () => { const Asset = () => {
...@@ -37,6 +50,10 @@ const Asset = () => { ...@@ -37,6 +50,10 @@ const Asset = () => {
[data, gqlData, isNftGraphQl] [data, gqlData, isNftGraphQl]
) )
const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () =>
CollectionStatsFetcher(contractAddress)
)
return ( return (
<> <>
<Trace <Trace
...@@ -46,8 +63,10 @@ const Asset = () => { ...@@ -46,8 +63,10 @@ const Asset = () => {
> >
{asset && collection ? ( {asset && collection ? (
<AssetContainer> <AssetContainer>
<AssetDetails collection={collection} asset={asset} /> <AssetDetails collection={collection} asset={asset} collectionStats={collectionStats} />
<AssetPriceDetailsContainer>
<AssetPriceDetails collection={collection} asset={asset} /> <AssetPriceDetails collection={collection} asset={asset} />
</AssetPriceDetailsContainer>
</AssetContainer> </AssetContainer>
) : ( ) : (
<div>Holder for loading ...</div> <div>Holder for loading ...</div>
......
...@@ -3,15 +3,19 @@ import { ActivityEventResponse, ActivityFilter } from '../../types' ...@@ -3,15 +3,19 @@ import { ActivityEventResponse, ActivityFilter } from '../../types'
export const ActivityFetcher = async ( export const ActivityFetcher = async (
contractAddress: string, contractAddress: string,
filters?: ActivityFilter, filters?: ActivityFilter,
cursor?: string cursor?: string,
limit?: string
): Promise<ActivityEventResponse> => { ): Promise<ActivityEventResponse> => {
const filterParam = const filterParam =
filters && filters.eventTypes filters && filters.eventTypes
? `&${filters.eventTypes?.map((eventType) => `event_types[]=${eventType}`).join('&')}` ? `&${filters.eventTypes?.map((eventType) => `event_types[]=${eventType}`).join('&')}`
: '' : ''
const url = `${
process.env.REACT_APP_GENIE_V3_API_URL const tokenId = filters?.token_id ? `&token_id=${filters?.token_id}` : ''
}/collections/${contractAddress}/activity?limit=25${filterParam}${cursor ? `&cursor=${cursor}` : ''}`
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/collections/${contractAddress}/activity?limit=${
limit ? limit : '25'
}${filterParam}${cursor ? `&cursor=${cursor}` : ''}${tokenId}`
const r = await fetch(url, { const r = await fetch(url, {
method: 'GET', method: 'GET',
......
import { CollectionInfoForAsset, GenieAsset } from '../../types' import { CollectionInfoForAsset, GenieAsset } from '../../types'
interface ReponseTrait {
trait_type: string
value: string
}
export const fetchSingleAsset = async ({ export const fetchSingleAsset = async ({
contractAddress, contractAddress,
tokenId, tokenId,
...@@ -10,5 +15,9 @@ export const fetchSingleAsset = async ({ ...@@ -10,5 +15,9 @@ export const fetchSingleAsset = async ({
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/assetDetails?address=${contractAddress}&tokenId=${tokenId}` const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/assetDetails?address=${contractAddress}&tokenId=${tokenId}`
const r = await fetch(url) const r = await fetch(url)
const data = await r.json() const data = await r.json()
return [data.asset[0], data.collection] const asset = data.asset[0]
asset.traits = asset.traits.map((trait: ReponseTrait) => ({ trait_type: trait.trait_type, trait_value: trait.value }))
return [asset, data.collection]
} }
...@@ -51,7 +51,7 @@ export enum ActivityEventTypeDisplay { ...@@ -51,7 +51,7 @@ export enum ActivityEventTypeDisplay {
'LISTING' = 'Listed', 'LISTING' = 'Listed',
'SALE' = 'Sold', 'SALE' = 'Sold',
'TRANSFER' = 'Transferred', 'TRANSFER' = 'Transferred',
'CANCEL_LISTING' = 'Cancelled', 'CANCEL_LISTING' = 'Cancellation',
} }
export enum OrderStatus { export enum OrderStatus {
...@@ -65,6 +65,7 @@ export interface ActivityFilter { ...@@ -65,6 +65,7 @@ export interface ActivityFilter {
collectionAddress?: string collectionAddress?: string
eventTypes?: ActivityEventType[] eventTypes?: ActivityEventType[]
marketplaces?: Markets[] marketplaces?: Markets[]
token_id?: string
} }
export interface ActivityEventResponse { export interface ActivityEventResponse {
......
import { Trait } from 'nft/hooks/useCollectionFilters'
import { Deprecated_SellOrder, SellOrder } from '../sell' import { Deprecated_SellOrder, SellOrder } from '../sell'
export interface OpenSeaCollection { export interface OpenSeaCollection {
...@@ -43,13 +41,6 @@ export interface OpenSeaAsset { ...@@ -43,13 +41,6 @@ export interface OpenSeaAsset {
collection?: OpenSeaCollection collection?: OpenSeaCollection
} }
interface OpenSeaUser {
user?: null
profile_img_url?: string
address?: string
config?: string
}
export enum TokenType { export enum TokenType {
ERC20 = 'ERC20', ERC20 = 'ERC20',
ERC721 = 'ERC721', ERC721 = 'ERC721',
...@@ -77,6 +68,14 @@ export interface Rarity { ...@@ -77,6 +68,14 @@ export interface Rarity {
providers?: { provider: string; rank?: number; url?: string; score?: number }[] providers?: { provider: string; rank?: number; url?: string; score?: number }[]
} }
export interface Trait {
trait_type: string
trait_value: string
display_type?: any
max_value?: any
trait_count?: number
order?: any
}
export interface GenieAsset { export interface GenieAsset {
id?: string // This would be a random id created and assigned by front end id?: string // This would be a random id created and assigned by front end
address: string address: string
...@@ -96,9 +95,14 @@ export interface GenieAsset { ...@@ -96,9 +95,14 @@ export interface GenieAsset {
totalCount?: number // The totalCount from the query to /assets totalCount?: number // The totalCount from the query to /assets
collectionIsVerified?: boolean collectionIsVerified?: boolean
rarity?: Rarity rarity?: Rarity
owner?: string owner: {
creator: OpenSeaUser address: string
metadataUrl?: string }
metadataUrl: string
creator: {
address: string
profile_img_url: string
}
traits?: Trait[] traits?: Trait[]
} }
...@@ -122,8 +126,8 @@ export interface GenieCollection { ...@@ -122,8 +126,8 @@ export interface GenieCollection {
} }
traits?: Record<string, Trait[]> traits?: Record<string, Trait[]>
marketplaceCount?: { marketplace: string; count: number }[] marketplaceCount?: { marketplace: string; count: number }[]
imageUrl?: string imageUrl: string
twitter?: string twitterUrl?: string
instagram?: string instagram?: string
discordUrl?: string discordUrl?: string
externalUrl?: string externalUrl?: string
......
...@@ -20,7 +20,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<num ...@@ -20,7 +20,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<num
export function useUsdPrice(asset: GenieAsset): string | undefined { export function useUsdPrice(asset: GenieAsset): string | undefined {
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {}) const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
return fetchedPriceData && asset.priceInfo.ETHPrice
? (parseFloat(formatEther(asset.priceInfo.ETHPrice)) * fetchedPriceData).toString() return fetchedPriceData && asset?.priceInfo?.ETHPrice
: undefined ? (parseFloat(formatEther(asset?.priceInfo?.ETHPrice)) * fetchedPriceData).toString()
: ''
} }
export const putCommas = (value?: number) => { export const putCommas = (value: number) => {
try { try {
if (!value) return value if (!value) return value
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
......
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