diff --git a/src/components/NavBar/SuggestionRow.tsx b/src/components/NavBar/SuggestionRow.tsx index bd77bcd4e3..cec3d6e1e9 100644 --- a/src/components/NavBar/SuggestionRow.tsx +++ b/src/components/NavBar/SuggestionRow.tsx @@ -93,7 +93,7 @@ export const CollectionRow = ({ {collection.name} {collection.isVerified && } - {putCommas(collection.stats?.total_supply)} items + {putCommas(collection?.stats?.total_supply ?? 0)} items {collection.stats?.floor_price ? ( diff --git a/src/graphql/data/nft/Collection.ts b/src/graphql/data/nft/Collection.ts index 068d0b0c84..2cc4a4aaa1 100644 --- a/src/graphql/data/nft/Collection.ts +++ b/src/graphql/data/nft/Collection.ts @@ -1,6 +1,5 @@ import graphql from 'babel-plugin-relay/macro' -import { Trait } from 'nft/hooks/useCollectionFilters' -import { GenieCollection } from 'nft/types' +import { GenieCollection, Trait } from 'nft/types' import { useLazyLoadQuery } from 'react-relay' import { CollectionQuery } from './__generated__/CollectionQuery.graphql' @@ -122,8 +121,8 @@ export function useCollectionQuery(address: string): GenieCollection | undefined : {}, traits, // marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports - imageUrl: queryCollection?.image?.url, - twitter: queryCollection?.twitterName ?? undefined, + imageUrl: queryCollection?.image?.url ?? '', + twitterUrl: queryCollection?.twitterName ?? '', instagram: queryCollection?.instagramName ?? undefined, discordUrl: queryCollection?.discordUrl ?? undefined, externalUrl: queryCollection?.homepageUrl ?? undefined, diff --git a/src/graphql/data/nft/Details.ts b/src/graphql/data/nft/Details.ts index 847be2a2ff..43906d1ee4 100644 --- a/src/graphql/data/nft/Details.ts +++ b/src/graphql/data/nft/Details.ts @@ -1,6 +1,5 @@ import { parseEther } from '@ethersproject/units' import graphql from 'babel-plugin-relay/macro' -import { Trait } from 'nft/hooks' import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types' import { useLazyLoadQuery } from 'react-relay' @@ -141,14 +140,14 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset, }) : undefined, }, - owner: asset?.ownerAddress ?? undefined, + owner: { address: asset?.ownerAddress ?? '' }, creator: { - profile_img_url: asset?.creator?.profileImage?.url, - address: asset?.creator?.address, + profile_img_url: asset?.creator?.profileImage?.url ?? '', + address: asset?.creator?.address ?? '', }, - metadataUrl: asset?.metadataUrl ?? undefined, + metadataUrl: asset?.metadataUrl ?? '', 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 ?? '' } }), }, { diff --git a/src/nft/components/bag/Bag.tsx b/src/nft/components/bag/Bag.tsx index 37225db86e..7c10303de8 100644 --- a/src/nft/components/bag/Bag.tsx +++ b/src/nft/components/bag/Bag.tsx @@ -29,6 +29,7 @@ import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTx import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useQuery, useQueryClient } from 'react-query' import { useLocation } from 'react-router-dom' +import styled from 'styled-components/macro' import * as styles from './Bag.css' import { BagContent } from './BagContent' @@ -41,6 +42,15 @@ interface SeparatorProps { 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 shouldShowBag = isNFTPage || isProfilePage const isMobile = useIsMobile() + const isDetailsPage = pathname.includes('/nfts/asset/') + const sendTransaction = useSendTransaction((state) => state.sendTransaction) const transactionState = useSendTransaction((state) => state.state) const setTransactionState = useSendTransaction((state) => state.setState) @@ -304,7 +316,11 @@ const Bag = () => { )} - {isOpen && (!bagIsLocked ? setModalIsOpen(false) : undefined)} />} + {isDetailsPage ? ( + + ) : ( + isOpen && (!bagIsLocked ? setModalIsOpen(false) : undefined)} /> + )} ) : null} diff --git a/src/nft/components/collection/Activity.css.ts b/src/nft/components/collection/Activity.css.ts index 34fbfb4df0..ecb5fa8f98 100644 --- a/src/nft/components/collection/Activity.css.ts +++ b/src/nft/components/collection/Activity.css.ts @@ -79,7 +79,6 @@ export const detailsName = style([ export const eventDetail = style([ subhead, sprinkles({ - marginBottom: '4', gap: '8', }), { diff --git a/src/nft/components/collection/Activity.tsx b/src/nft/components/collection/Activity.tsx index dbfada9a29..1f193907ee 100644 --- a/src/nft/components/collection/Activity.tsx +++ b/src/nft/components/collection/Activity.tsx @@ -48,7 +48,7 @@ const initialFilterState = { [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] } } diff --git a/src/nft/components/collection/ActivityCells.tsx b/src/nft/components/collection/ActivityCells.tsx index d13b67714e..2739e48d2a 100644 --- a/src/nft/components/collection/ActivityCells.tsx +++ b/src/nft/components/collection/ActivityCells.tsx @@ -10,6 +10,7 @@ import { ActivityListingIcon, ActivitySaleIcon, ActivityTransferIcon, + CancelListingIcon, RarityVerifiedIcon, } from 'nft/components/icons' import { @@ -157,7 +158,7 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel ) } -const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => { +export const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => { return ( { @@ -216,6 +218,8 @@ const renderEventIcon = (eventType: ActivityEventType) => { return case ActivityEventType.Transfer: return + case ActivityEventType.CancelListing: + return default: return null } @@ -237,13 +241,20 @@ const eventColors = (eventType: ActivityEventType) => { [ActivityEventType.Listing]: 'gold', [ActivityEventType.Sale]: 'green', [ActivityEventType.Transfer]: 'violet', - [ActivityEventType.CancelListing]: 'error', + [ActivityEventType.CancelListing]: '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]) return ( @@ -251,7 +262,7 @@ export const EventCell = ({ eventType, eventTimestamp, eventTransactionHash, pri {renderEventIcon(eventType)} {ActivityEventTypeDisplay[eventType]} - {eventTimestamp && isValidDate(eventTimestamp) && !isMobile && ( + {eventTimestamp && isValidDate(eventTimestamp) && !isMobile && !eventOnly && ( {getTimeDifference(eventTimestamp.toString())} {eventTransactionHash && } @@ -301,9 +312,10 @@ interface RankingProps { rarity: TokenRarity collectionName: string rarityVerified: boolean + details?: boolean } -const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => { +const Ranking = ({ details, rarity, collectionName, rarityVerified }: RankingProps) => { const rarityProviderLogo = getRarityProviderLogo(rarity.source) return ( diff --git a/src/nft/components/collection/CollectionStats.tsx b/src/nft/components/collection/CollectionStats.tsx index c81f7f9a53..9e89ca6e55 100644 --- a/src/nft/components/collection/CollectionStats.tsx +++ b/src/nft/components/collection/CollectionStats.tsx @@ -76,8 +76,8 @@ const MobileSocialsPopover = ({ ) : null} - {collectionStats.twitter ? ( - + {collectionStats.twitterUrl ? ( + ) : null} - {collectionStats.twitter ? ( - + {collectionStats.twitterUrl ? ( + {isMobile && (collectionStats.discordUrl || - collectionStats.twitter || + collectionStats.twitterUrl || collectionStats.instagram || collectionStats.externalUrl) && ( `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 ( + + + + + + + + + + + + + {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 ( + + + + + + + + + + ) + })} + +
EventPriceByToTime
+ + + {formattedPrice && ( + + {marketplace && } + {formattedPrice} ETH + + )} + + {fromAddress && ( + + {shortenAddress(fromAddress, 2, 4)} + + )} + + {toAddress && ( + + {shortenAddress(toAddress, 2, 4)} + + )} + {eventTimestamp && getTimeDifference(eventTimestamp.toString())}
+
+ ) +} + +export default AssetActivity diff --git a/src/nft/components/details/AssetDetails.css.ts b/src/nft/components/details/AssetDetails.css.ts index ef772afb9b..bd0a26e536 100644 --- a/src/nft/components/details/AssetDetails.css.ts +++ b/src/nft/components/details/AssetDetails.css.ts @@ -6,19 +6,18 @@ import { sprinkles, vars } from '../../css/sprinkles.css' export const image = style([ sprinkles({ borderRadius: '20', height: 'full', alignSelf: 'center' }), { - width: 'calc(90vh - 165px)', - height: 'calc(90vh - 165px)', - maxHeight: '678px', - maxWidth: '678px', + maxHeight: 'calc(90vh - 165px)', + minHeight: 400, + maxWidth: 780, boxShadow: `0px 20px 50px var(--shadow), 0px 10px 50px rgba(70, 115, 250, 0.2)`, '@media': { '(max-width: 1024px)': { maxHeight: '64vh', - maxWidth: '64vh', }, '(max-width: 640px)': { + minHeight: 280, maxHeight: '56vh', - maxWidth: '56vh', + maxWidth: '100%', }, }, }, @@ -81,8 +80,6 @@ export const columns = style([ ]) export const column = style({ - maxWidth: '50%', - width: '50%', alignSelf: 'center', '@media': { '(max-width: 1024px)': { diff --git a/src/nft/components/details/AssetDetails.tsx b/src/nft/components/details/AssetDetails.tsx index 63080bc81e..fed3a21794 100644 --- a/src/nft/components/details/AssetDetails.tsx +++ b/src/nft/components/details/AssetDetails.tsx @@ -1,38 +1,217 @@ -import { useWeb3React } from '@web3-react/core' -import { sendAnalyticsEvent } from 'analytics' -import { EventName, PageName } from 'analytics/constants' -import { useTrace } from 'analytics/Trace' -import clsx from 'clsx' +import Resource from 'components/Tokens/TokenDetails/Resource' import { MouseoverTooltip } from 'components/Tooltip/index' -import useENSName from 'hooks/useENSName' -import { AnimatedBox, Box } from 'nft/components/Box' -import { CollectionProfile } from 'nft/components/details/CollectionProfile' -import { Details } from 'nft/components/details/Details' -import { Traits } from 'nft/components/details/Traits' -import { Center, Column, Row } from 'nft/components/Flex' -import { CloseDropDownIcon, CornerDownLeftIcon, Eth2Icon, ShareIcon, SuspiciousIcon } from 'nft/components/icons' -import { ExpandableText } from 'nft/components/layout/ExpandableText' -import { badge, bodySmall, caption, headlineMedium, subhead } from 'nft/css/common.css' -import { themeVars } from 'nft/css/sprinkles.css' -import { useBag } from 'nft/hooks' -import { useTimeout } from 'nft/hooks/useTimeout' -import { CollectionInfoForAsset, Deprecated_SellOrder, GenieAsset, SellOrder } from 'nft/types' -import { useUsdPrice } from 'nft/utils' +import { Box } from 'nft/components/Box' +import { reduceFilters } from 'nft/components/collection/Activity' +import { LoadingSparkle } from 'nft/components/common/Loading/LoadingSparkle' +import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' +import { Center } from 'nft/components/Flex' +import { VerifiedIcon } from 'nft/components/icons' +import { ActivityFetcher } from 'nft/queries/genie/ActivityFetcher' +import { ActivityEventResponse, ActivityEventType } from 'nft/types' +import { CollectionInfoForAsset, GenieAsset, GenieCollection } from 'nft/types' import { shortenAddress } from 'nft/utils/address' import { formatEthPrice } from 'nft/utils/currency' -import { isAssetOwnedByUser } from 'nft/utils/isAssetOwnedByUser' import { isAudio } from 'nft/utils/isAudio' import { isVideo } from 'nft/utils/isVideo' -import { fallbackProvider, rarityProviderLogo } from 'nft/utils/rarity' -import { toSignificant } from 'nft/utils/toSignificant' -import qs from 'query-string' -import { useEffect, useMemo, useState } from 'react' -import ReactMarkdown from 'react-markdown' -import { Link, useLocation, useNavigate } from 'react-router-dom' -import { useSpring } from 'react-spring' +import { putCommas } from 'nft/utils/putCommas' +import { fallbackProvider, getRarityProviderLogo } from 'nft/utils/rarity' +import { useCallback, useMemo, useReducer, useState } from 'react' +import InfiniteScroll from 'react-infinite-scroll-component' +import { useInfiniteQuery, useQuery } from 'react-query' +import { Link as RouterLink } from 'react-router-dom' +import styled, { css } from 'styled-components/macro' -import { SUSPICIOUS_TEXT } from '../collection/Card' +import AssetActivity from './AssetActivity' import * as styles from './AssetDetails.css' +import DetailsContainer from './DetailsContainer' +import InfoContainer from './InfoContainer' +import TraitsContainer from './TraitsContainer' + +const OpacityTransition = css` + &:hover { + opacity: ${({ theme }) => theme.opacity.hover}; + } + + &:active { + opacity: ${({ theme }) => theme.opacity.click}; + } + + transition: ${({ + theme: { + transition: { duration, timing }, + }, + }) => `opacity ${duration.medium} ${timing.ease}`}; +` + +const CollectionHeader = styled.span` + display: flex; + align-items: center; + font-size: 16px; + line-height: 24px; + color: ${({ theme }) => theme.textPrimary}; + margin-top: 28px; + text-decoration: none; + ${OpacityTransition}; +` + +const AssetPriceDetailsContainer = styled.div` + margin-top: 20px; + display: none; + @media (max-width: 960px) { + display: block; + } +` + +const AssetHeader = styled.div` + display: flex; + align-items: center; + font-size: 36px; + line-height: 36px; + color: ${({ theme }) => theme.textPrimary}; + margin-top: 8px; +` + +const MediaContainer = styled.div` + display: flex; + justify-content: center; +` + +const Column = styled.div` + display: flex; + flex-direction: column; + width: 100%; + max-width: 780px; +` + +const AddressTextLink = styled.a` + display: inline-block; + color: ${({ theme }) => theme.textSecondary}; + text-decoration: none; + max-width: 100%; + word-wrap: break-word; + ${OpacityTransition}; +` + +const SocialsContainer = styled.div` + display: flex; + gap: 16px; + margin-top: 20px; +` + +const DescriptionText = styled.div` + margin-top: 8px; + font-size: 14px; + line-height: 20px; +` + +const RarityWrap = styled.span` + display: flex; + color: ${({ theme }) => theme.textSecondary}; + padding: 2px 4px; + border-radius: 4px; + align-items: center; + gap: 4px; +` + +const EmptyActivitiesContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + color: ${({ theme }) => theme.textPrimary}; + font-size: 28px; + line-height: 36px; + padding: 56px 0px; +` + +const Link = styled(RouterLink)` + color: ${({ theme }) => theme.accentAction}; + text-decoration: none; + font-size: 14px; + line-height: 16px; + margin-top: 12px; + cursor: pointer; + ${OpacityTransition}; +` + +const DefaultLink = styled(RouterLink)` + text-decoration: none; +` + +const ActivitySelectContainer = styled.div` + display: flex; + gap: 8px; + margin-bottom: 34px; + overflow-x: auto; + + // Firefox scrollbar styling + scrollbar-width: thin; + scrollbar-color: ${({ theme }) => `${theme.backgroundOutline} transparent`}; + + // safari and chrome scrollbar styling + ::-webkit-scrollbar { + background: transparent; + height: 4px; + } + ::-webkit-scrollbar-track { + margin-top: 40px; + } + ::-webkit-scrollbar-thumb { + background: ${({ theme }) => theme.backgroundOutline}; + border-radius: 8px; + } + + @media (max-width: 720px) { + padding-bottom: 8px; + } +` + +const ContentNotAvailable = styled.div` + display: flex; + background-color: ${({ theme }) => theme.backgroundSurface}; + color: ${({ theme }) => theme.textSecondary}; + font-size: 14px; + line-height: 20px; + align-items: center; + justify-content: center; + border-radius: 12px; + width: 450px; + height: 450px; +` + +const FilterBox = styled.div<{ isActive?: boolean }>` + box-sizing: border-box; + background-color: ${({ theme }) => theme.backgroundInteractive}; + color: ${({ theme }) => theme.textPrimary}; + padding: 12px 16px; + border-radius: 12px; + cursor: pointer; + box-sizing: border-box; + border: ${({ isActive, theme }) => (isActive ? `1px solid ${theme.accentActive}` : undefined)}; + ${OpacityTransition}; +` + +const ByText = styled.span` + font-size: 14px; + line-height: 20px; +` + +const Img = styled.img` + background-color: white; +` + +const HoverImageContainer = styled.div` + display: flex; + margin-right: 4px; +` + +const HoverContainer = styled.div` + display: flex; +` + +const ContainerText = styled.span` + font-size: 14px; +` const AudioPlayer = ({ imageUrl, @@ -58,26 +237,11 @@ const AudioPlayer = ({ ) } -const formatter = Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'short' }) - -const CountdownTimer = ({ sellOrder }: { sellOrder: Deprecated_SellOrder | SellOrder }) => { - const { date, expires } = useMemo(() => { - const date = new Date((sellOrder as Deprecated_SellOrder).orderClosingDate ?? (sellOrder as SellOrder).endAt) - return { - date, - expires: formatter.format(date), - } - }, [sellOrder]) - const [days, hours, minutes, seconds] = useTimeout(date) - - return ( - Expires {expires}
}> - - Expires: {days !== 0 ? `${days} days` : ''} {hours !== 0 ? `${hours} hours` : ''} {minutes} minutes {seconds}{' '} - seconds - - - ) +const initialFilterState = { + [ActivityEventType.Listing]: true, + [ActivityEventType.Sale]: true, + [ActivityEventType.Transfer]: false, + [ActivityEventType.CancelListing]: false, } const AssetView = ({ @@ -112,46 +276,19 @@ enum MediaType { interface AssetDetailsProps { asset: GenieAsset collection: CollectionInfoForAsset + collectionStats: GenieCollection | undefined } -export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { - const { pathname, search } = useLocation() - const navigate = useNavigate() - const addAssetsToBag = useBag((state) => state.addAssetsToBag) - const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag) - const itemsInBag = useBag((state) => state.itemsInBag) - const bagExpanded = useBag((state) => state.bagExpanded) - const [creatorAddress, setCreatorAddress] = useState('') - const [ownerAddress, setOwnerAddress] = useState('') +export const AssetDetails = ({ asset, collection, collectionStats }: AssetDetailsProps) => { const [dominantColor] = useState<[number, number, number]>([0, 0, 0]) - const creatorEnsName = useENSName(creatorAddress) - const ownerEnsName = useENSName(ownerAddress) - const parsed = qs.parse(search) - const { gridWidthOffset } = useSpring({ - gridWidthOffset: bagExpanded ? 324 : 0, - }) - const [showTraits, setShowTraits] = useState(true) - const [isSelected, setSelected] = useState(false) - const [isOwned, setIsOwned] = useState(false) - const { account: address, provider } = useWeb3React() - const trace = useTrace({ page: PageName.NFT_DETAILS_PAGE }) - - const eventProperties = { - collection_address: asset.address, - token_id: asset.tokenId, - token_type: asset.tokenType, - ...trace, - } - - const { rarityProvider, rarityLogo } = useMemo( + const { rarityProvider } = useMemo( () => asset.rarity ? { rarityProvider: asset?.rarity?.providers?.find( ({ provider: _provider }) => _provider === asset.rarity?.primaryProvider ), - rarityLogo: rarityProviderLogo[asset.rarity.primaryProvider] || '', } : {}, [asset.rarity] @@ -166,282 +303,217 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { return MediaType.Image }, [asset]) - useEffect(() => { - if (asset.creator) setCreatorAddress(asset.creator.address ?? '') - if (asset.owner) setOwnerAddress(asset.owner) - }, [asset]) + const { address: contractAddress, tokenId: token_id } = 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) + const { data: priceData } = useQuery( + [ + 'collectionActivity', + { + contractAddress, + }, + ], + async ({ pageParam = '' }) => { + return await ActivityFetcher( + contractAddress, + { + token_id, + eventTypes: [ActivityEventType.Sale], + }, + pageParam, + '1' + ) + }, + { + getNextPageParam: (lastPage) => { + return lastPage.events?.length === 25 ? lastPage.cursor : undefined + }, + refetchInterval: 15000, + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + refetchOnMount: false, } - }, [asset, address, provider]) + ) - const USDPrice = useUsdPrice(asset) + const lastSalePrice = priceData?.events[0]?.price ?? null + const formattedEthprice = formatEthPrice(lastSalePrice ?? '') || 0 + const formattedPrice = lastSalePrice ? putCommas(formattedEthprice).toString() : null + const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState) + + const Filter = useCallback( + function ActivityFilter({ eventType }: { eventType: ActivityEventType }) { + const isActive = activeFilters[eventType] + + return ( + filtersDispatch({ eventType })}> + {eventType === ActivityEventType.CancelListing + ? 'Cancellations' + : eventType.charAt(0) + eventType.slice(1).toLowerCase() + 's'} + + ) + }, + [activeFilters] + ) + + const { + data: eventsData, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isSuccess, + } = useInfiniteQuery( + [ + 'collectionActivity', + { + contractAddress, + activeFilters, + token_id, + }, + ], + async ({ pageParam = '' }) => { + return await ActivityFetcher( + contractAddress, + { + token_id, + eventTypes: Object.keys(activeFilters) + .map((key) => key as ActivityEventType) + .filter((key) => activeFilters[key]), + }, + pageParam + ) + }, + { + getNextPageParam: (lastPage) => { + return lastPage.events?.length === 25 ? lastPage.cursor : undefined + }, + refetchInterval: 15000, + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + refetchOnMount: false, + } + ) + + const rarity = asset?.rarity?.providers?.length ? asset?.rarity?.providers?.[0] : undefined + const [showHolder, setShowHolder] = useState(false) + const rarityProviderLogo = getRarityProviderLogo(rarity?.provider) + const events = useMemo( + () => (isSuccess ? eventsData?.pages.map((page) => page.events).flat() : null), + [isSuccess, eventsData] + ) return ( - `calc(100% - ${x}px)`), - }} - className={styles.container} - > -
- - {assetMediaType === MediaType.Image ? ( - {asset.name - ) : ( - - )} - - - - + + {asset.imageUrl === undefined || showHolder ? ( + Content not available yet + ) : assetMediaType === MediaType.Image ? ( + {asset.name setShowHolder(true)} + /> + ) : ( + + )} + + + + {collection.collectionName} {collectionStats?.isVerified && } + + + + {asset.name ?? `${asset.collectionName} #${asset.tokenId}`} + + + + + + cardLogo + + + {collectionStats?.rarityVerified + ? `Verified by ${collectionStats?.name}` + : `Ranking by ${rarity.provider === 'Genie' ? fallbackProvider : rarity.provider}`} + + + } + placement="top" > - {rarityProvider && ( - - {rarityProvider.provider} - Ranking by{' '} - {asset.rarity?.primaryProvider === 'Genie' ? fallbackProvider : asset.rarity?.primaryProvider} - - } - > -
- #{rarityProvider.rank} Rarity rank + Rarity: {putCommas(rarity.score)} + + ) : null + } + > + + + + <> + + + + + + + {events && events.length > 0 ? ( + +
- - )} - -
{ - await navigator.clipboard.writeText(`${window.location.hostname}/#${pathname}`) - }} - > - -
- -
{ - if (!parsed.origin || parsed.origin === 'collection') { - navigate(`/nfts/collection/${asset.address}`) - } else if (parsed.origin === 'profile') { - navigate('/profile', undefined) - } else if (parsed.origin === 'explore') { - navigate(`/nfts`, undefined) - } else if (parsed.origin === 'activity') { - navigate(`/nfts/collection/${asset.address}/activity`, undefined) - } - }} - > - {parsed.origin ? ( - - ) : ( - - )} -
-
- - - {asset.susFlag && ( - - {SUSPICIOUS_TEXT}}> - - - - )} - - {asset.name || `${collection.collectionName} #${asset.tokenId}`} - - {collection.collectionDescription ? ( - - - - ) : null} - - {ownerAddress.length > 0 && ( - - - - )} - - - - - - {creatorAddress ? ( - - - - ) : null} - -
- - {asset.priceInfo && asset.sellorders && !isOwned ? ( - - - - - Markeplace - - - {formatEthPrice(asset.priceInfo.ETHPrice)} - - {USDPrice && ( - - ${toSignificant(USDPrice)} - - )} - - {(asset.sellorders?.[0] as Deprecated_SellOrder).orderClosingDate || - (asset.sellorders?.[0] as SellOrder).endAt ? ( - - ) : null} - - { - if (isSelected) { - removeAssetsFromBag([asset]) - } else { - addAssetsToBag([asset]) - sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { ...eventProperties }) - } - setSelected((x) => !x) - }} - > - {isSelected ? 'Added to Bag' : 'Buy Now'} - - - ) : null} - - - - - {showTraits ? ( - + + ) : ( -
+ +
No activities yet
+ View collection items{' '} +
)} - -
-
+ + + + <> + By + {asset?.creator && asset.creator?.address && ( + + {shortenAddress(asset.creator.address, 2, 4)} + + )} + + {collection.collectionDescription} + + {collectionStats?.externalUrl && } + {collectionStats?.twitterUrl && ( + + )} + {collectionStats?.discordUrl && } + + + + + + + ) } diff --git a/src/nft/components/details/AssetPriceDetails.tsx b/src/nft/components/details/AssetPriceDetails.tsx index 5ca3444e69..d7ecdc8f09 100644 --- a/src/nft/components/details/AssetPriceDetails.tsx +++ b/src/nft/components/details/AssetPriceDetails.tsx @@ -1,14 +1,14 @@ import { useWeb3React } from '@web3-react/core' -import { sendAnalyticsEvent } from 'analytics' -import { EventName, PageName } from 'analytics/constants' -import { useTrace } from 'analytics/Trace' +import useCopyClipboard from 'hooks/useCopyClipboard' import { CancelListingIcon, MinusIcon, PlusIcon } from 'nft/components/icons' import { useBag } from 'nft/hooks' import { CollectionInfoForAsset, Deprecated_SellOrder, GenieAsset, SellOrder, TokenType } from 'nft/types' import { ethNumberStandardFormatter, formatEthPrice, getMarketplaceIcon, timeLeft, useUsdPrice } from 'nft/utils' +import { shortenAddress } from 'nft/utils/address' import { useMemo } from 'react' +import { Upload } from 'react-feather' import { useNavigate } from 'react-router-dom' -import styled, { useTheme } from 'styled-components/macro' +import styled, { css, useTheme } from 'styled-components/macro' import { ThemedText } from 'theme' interface AssetPriceDetailsProps { @@ -16,8 +16,40 @@ interface AssetPriceDetailsProps { collection: CollectionInfoForAsset } +const hoverState = css` + :hover::after { + border-radius: 12px; + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: ${({ theme }) => theme.stateOverlayHover}; + z-index: 0; + } + + :active::after { + border-radius: 12px; + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: ${({ theme }) => theme.stateOverlayPressed}; + z-index: 0; + } +` + const Container = styled.div` - margin-left: 86px; + width: 100%; + + @media (min-width: 960px) { + position: fixed; + width: 360px; + margin-top: -6px; + } ` const BestPriceContainer = styled.div` @@ -28,7 +60,6 @@ const BestPriceContainer = styled.div` background-color: ${({ theme }) => theme.backgroundSurface}; border: 1px solid ${({ theme }) => theme.backgroundOutline}; border-radius: 16px; - width: 320px; ` const HeaderRow = styled.div` @@ -59,11 +90,17 @@ const BuyNowButton = styled.div<{ assetInBag: boolean; margin: boolean; useAccen margin-top: ${({ margin }) => (margin ? '12px' : '0px')}; text-align: center; cursor: pointer; + + ${hoverState} +` + +const BuyNowButtonContainer = styled.div` + position: relative; ` const Erc1155BuyNowButton = styled.div` - display: flex; - flex-direction: row; + display: grid; + grid-template-columns: 1fr 1fr 1fr; width: 100%; background-color: ${({ theme }) => theme.backgroundSurface}; border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`}; @@ -74,6 +111,9 @@ const Erc1155BuyNowButton = styled.div` justify-content: space-between; overflow-x: hidden; ` +const Tertiary = styled(ThemedText.BodySecondary)` + color: ${({ theme }) => theme.textTertiary}; +` const Erc1155BuyNowText = styled.div` display: flex; @@ -88,12 +128,32 @@ const Erc1155ChangeButton = styled(Erc1155BuyNowText)<{ remove: boolean }>` color: ${({ theme, remove }) => (remove ? theme.accentFailure : theme.accentAction)}; cursor: pointer; - :hover { - background-color: ${({ theme, remove }) => (remove ? theme.accentFailure : theme.accentAction)}; - color: ${({ theme }) => theme.textPrimary}; + ${hoverState} + + &:hover::after { + border-radius: 0px; } ` +const UploadLink = styled.a` + color: ${({ theme }) => theme.textSecondary}; + cursor: pointer; + + &:hover { + opacity: ${({ theme }) => theme.opacity.hover}; + } + + &:active { + opacity: ${({ theme }) => theme.opacity.click}; + } + + transition: ${({ + theme: { + transition: { duration, timing }, + }, + }) => `opacity ${duration.medium} ${timing.ease}`}; +` + const NotForSaleContainer = styled.div` display: flex; flex-direction: column; @@ -111,10 +171,40 @@ const DiscoveryContainer = styled.div` align-items: center; ` +const OwnerText = styled.a` + font-size: 14px; + line-height: 20px; + color: ${({ theme }) => theme.textSecondary}; + 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 OwnerInformationContainer = styled.div` + color: ${({ theme }) => theme.textSecondary}; + display: flex; + justify-content: space-between; + padding: 0 8px; + margin-bottom: 20px; +` + export const OwnerContainer = ({ asset }: { asset: GenieAsset }) => { const listing = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined - const expirationDate = listing - ? new Date((listing as Deprecated_SellOrder).orderClosingDate ?? (listing as SellOrder).endAt) + const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined + const expirationDate = cheapestOrder + ? new Date((cheapestOrder as Deprecated_SellOrder).orderClosingDate ?? (cheapestOrder as SellOrder).endAt) : undefined const USDPrice = useUsdPrice(asset) @@ -190,26 +280,27 @@ export const NotForSale = ({ collection }: { collection: CollectionInfoForAsset ) } +const SubHeader = styled(ThemedText.SubHeader)` + color: ${({ theme }) => theme.textPrimary}; +` + 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 as Deprecated_SellOrder).orderClosingDate ?? (cheapestOrder as SellOrder).endAt) : undefined + const itemsInBag = useBag((s) => s.itemsInBag) const addAssetsToBag = useBag((s) => s.addAssetsToBag) const removeAssetsFromBag = useBag((s) => s.removeAssetsFromBag) + const toggleBag = useBag((s) => s.toggleBag) + const bagExpanded = useBag((s) => s.bagExpanded) const USDPrice = useUsdPrice(asset) const isErc1555 = asset.tokenType === TokenType.ERC1155 - - const trace = useTrace({ page: PageName.NFT_DETAILS_PAGE }) - const eventProperties = { - collection_address: asset.address, - token_id: asset.tokenId, - token_type: asset.tokenType, - ...trace, - } + const [, setCopied] = useCopyClipboard() const { quantity, assetInBag } = useMemo(() => { return { @@ -222,8 +313,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) } }, [asset, itemsInBag]) - const isOwner = - asset.owner && typeof asset.owner === 'string' ? account?.toLowerCase() === asset.owner.toLowerCase() : false + const isOwner = asset.owner ? account?.toLowerCase() === asset.owner?.address?.toLowerCase() : false if (isOwner) { return @@ -231,6 +321,28 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) return ( + + + {asset.tokenType === 'ERC1155' ? ( + '' + ) : ( + Seller: {isOwner ? 'you' : asset.owner.address && shortenAddress(asset.owner.address, 2, 4)} + )} + + { + setCopied(window.location.href) + }} + target="_blank" + > + + + + {cheapestOrder && asset.priceInfo ? ( @@ -241,7 +353,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) - {formatEthPrice(asset.priceInfo.ETHPrice)} + {formatEthPrice(asset.priceInfo.ETHPrice)} ETH {USDPrice && ( @@ -249,34 +361,48 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) )} - {expirationDate && ( - Sale ends: {timeLeft(expirationDate)} - )} - {!isErc1555 || !assetInBag ? ( - { - assetInBag ? removeAssetsFromBag([asset]) : addAssetsToBag([asset]) - !assetInBag && sendAnalyticsEvent(EventName.NFT_BUY_ADDED, { ...eventProperties }) - }} - > - {assetInBag ? 'Remove' : 'Buy Now'} - - ) : ( - - removeAssetsFromBag([asset])}> - - - - {quantity} - - addAssetsToBag([asset])}> - - - - )} + {expirationDate && Sale ends: {timeLeft(expirationDate)}} +
+ {!isErc1555 || !assetInBag ? ( + + { + assetInBag ? removeAssetsFromBag([asset]) : addAssetsToBag([asset]) + if (!assetInBag && !isErc1555 && !bagExpanded) { + toggleBag() + } + }} + > + + {assetInBag ? 'Remove' : 'Buy Now'} + + + + ) : ( + + + removeAssetsFromBag([asset])}> + + + + + + + {quantity} + + + + + addAssetsToBag([asset])}> + + + + + )} +
) : ( diff --git a/src/nft/components/details/DetailsContainer.tsx b/src/nft/components/details/DetailsContainer.tsx new file mode 100644 index 0000000000..52baade320 --- /dev/null +++ b/src/nft/components/details/DetailsContainer.tsx @@ -0,0 +1,132 @@ +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 ( +
+
{header}
+ {body} +
+ ) +} + +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 ( +
+ + {shortenAddress(address, 2, 4)} + + } + /> + 9 ? stringShortener(tokenId) : tokenId} /> + + + + + {shortenAddress(creator.address, 2, 4)} + + ) + } + /> +
+ ) +} + +export default DetailsContainer diff --git a/src/nft/components/details/InfoContainer.tsx b/src/nft/components/details/InfoContainer.tsx new file mode 100644 index 0000000000..d38187ca18 --- /dev/null +++ b/src/nft/components/details/InfoContainer.tsx @@ -0,0 +1,89 @@ +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 ( +
+
setIsOpen(!isOpen)}> + + {primaryHeader} {secondaryHeader} + + {isOpen ? : } +
+ {isOpen && {children}} +
+ ) +} + +export default InfoContainer diff --git a/src/nft/components/details/Traits.css.ts b/src/nft/components/details/Traits.css.ts deleted file mode 100644 index 56f7f525da..0000000000 --- a/src/nft/components/details/Traits.css.ts +++ /dev/null @@ -1,18 +0,0 @@ -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)', - }, - }, - }, -]) diff --git a/src/nft/components/details/Traits.tsx b/src/nft/components/details/Traits.tsx deleted file mode 100644 index e8b935fd03..0000000000 --- a/src/nft/components/details/Traits.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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_type, trait_value }: Trait) => ( - - - {trait_type} - - - - {trait_value} - - -) - -export const Traits = ({ traits, collectionAddress }: { traits: Trait[]; collectionAddress: string }) => ( -
- {traits.length === 0 - ? 'No traits' - : traits.map((item) => { - const params = qs.stringify( - { traits: [`("${item.trait_type}","${item.trait_value}")`] }, - { - arrayFormat: 'comma', - } - ) - - return ( - - - - ) - })} -
-) diff --git a/src/nft/components/details/TraitsContainer.tsx b/src/nft/components/details/TraitsContainer.tsx new file mode 100644 index 0000000000..c2faf5b725 --- /dev/null +++ b/src/nft/components/details/TraitsContainer.tsx @@ -0,0 +1,97 @@ +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 ( + + {trait_type} + {trait_value} + + ) +} + +const TraitsContainer = ({ asset }: { asset: GenieAsset }) => { + const traits = useMemo(() => asset.traits?.sort((a, b) => a.trait_type.localeCompare(b.trait_type)), [asset]) + + return ( + + {traits?.map((trait) => { + return + })} + + ) +} + +export default TraitsContainer diff --git a/src/nft/components/icons.tsx b/src/nft/components/icons.tsx index 6131ac8f24..1939553afe 100644 --- a/src/nft/components/icons.tsx +++ b/src/nft/components/icons.tsx @@ -1495,11 +1495,10 @@ export const EmptyNFTWalletIcon = (props: SVGProps) => ( ) export const CancelListingIcon = (props: SVGProps) => ( - + diff --git a/src/nft/pages/asset/Asset.tsx b/src/nft/pages/asset/Asset.tsx index aef391d689..3525010ec1 100644 --- a/src/nft/pages/asset/Asset.tsx +++ b/src/nft/pages/asset/Asset.tsx @@ -5,6 +5,7 @@ import { useDetailsQuery } from 'graphql/data/nft/Details' import { AssetDetails } from 'nft/components/details/AssetDetails' import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' import { fetchSingleAsset } from 'nft/queries' +import { CollectionStatsFetcher } from 'nft/queries' import { useMemo } from 'react' import { useQuery } from 'react-query' import { useParams } from 'react-router-dom' @@ -12,8 +13,20 @@ import styled from 'styled-components/macro' const AssetContainer = styled.div` display: flex; - padding-right: 116px; - padding-left: 116px; + width: 100%; + 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 = () => { @@ -37,6 +50,10 @@ const Asset = () => { [data, gqlData, isNftGraphQl] ) + const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () => + CollectionStatsFetcher(contractAddress) + ) + return ( <> { > {asset && collection ? ( - - + + + + ) : (
Holder for loading ...
diff --git a/src/nft/queries/genie/ActivityFetcher.ts b/src/nft/queries/genie/ActivityFetcher.ts index 7fc120aadf..f22cc9b031 100644 --- a/src/nft/queries/genie/ActivityFetcher.ts +++ b/src/nft/queries/genie/ActivityFetcher.ts @@ -3,15 +3,19 @@ import { ActivityEventResponse, ActivityFilter } from '../../types' export const ActivityFetcher = async ( contractAddress: string, filters?: ActivityFilter, - cursor?: string + cursor?: string, + limit?: string ): Promise => { const filterParam = filters && filters.eventTypes ? `&${filters.eventTypes?.map((eventType) => `event_types[]=${eventType}`).join('&')}` : '' - const url = `${ - process.env.REACT_APP_GENIE_V3_API_URL - }/collections/${contractAddress}/activity?limit=25${filterParam}${cursor ? `&cursor=${cursor}` : ''}` + + const tokenId = filters?.token_id ? `&token_id=${filters?.token_id}` : '' + + 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, { method: 'GET', diff --git a/src/nft/queries/genie/SingleAssetFetcher.ts b/src/nft/queries/genie/SingleAssetFetcher.ts index 5f9e208dc8..337187b6f4 100644 --- a/src/nft/queries/genie/SingleAssetFetcher.ts +++ b/src/nft/queries/genie/SingleAssetFetcher.ts @@ -1,5 +1,10 @@ import { CollectionInfoForAsset, GenieAsset } from '../../types' +interface ReponseTrait { + trait_type: string + value: string +} + export const fetchSingleAsset = async ({ contractAddress, tokenId, @@ -10,5 +15,9 @@ export const fetchSingleAsset = async ({ const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/assetDetails?address=${contractAddress}&tokenId=${tokenId}` const r = await fetch(url) 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] } diff --git a/src/nft/types/collection/collection.ts b/src/nft/types/collection/collection.ts index 50c669aa72..016d14260d 100644 --- a/src/nft/types/collection/collection.ts +++ b/src/nft/types/collection/collection.ts @@ -51,7 +51,7 @@ export enum ActivityEventTypeDisplay { 'LISTING' = 'Listed', 'SALE' = 'Sold', 'TRANSFER' = 'Transferred', - 'CANCEL_LISTING' = 'Cancelled', + 'CANCEL_LISTING' = 'Cancellation', } export enum OrderStatus { @@ -65,6 +65,7 @@ export interface ActivityFilter { collectionAddress?: string eventTypes?: ActivityEventType[] marketplaces?: Markets[] + token_id?: string } export interface ActivityEventResponse { diff --git a/src/nft/types/common/common.ts b/src/nft/types/common/common.ts index ffcf7ccb0a..5a9ee3edaa 100644 --- a/src/nft/types/common/common.ts +++ b/src/nft/types/common/common.ts @@ -1,5 +1,3 @@ -import { Trait } from 'nft/hooks/useCollectionFilters' - import { Deprecated_SellOrder, SellOrder } from '../sell' export interface OpenSeaCollection { @@ -43,13 +41,6 @@ export interface OpenSeaAsset { collection?: OpenSeaCollection } -interface OpenSeaUser { - user?: null - profile_img_url?: string - address?: string - config?: string -} - export enum TokenType { ERC20 = 'ERC20', ERC721 = 'ERC721', @@ -77,6 +68,14 @@ export interface Rarity { 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 { id?: string // This would be a random id created and assigned by front end address: string @@ -96,9 +95,14 @@ export interface GenieAsset { totalCount?: number // The totalCount from the query to /assets collectionIsVerified?: boolean rarity?: Rarity - owner?: string - creator: OpenSeaUser - metadataUrl?: string + owner: { + address: string + } + metadataUrl: string + creator: { + address: string + profile_img_url: string + } traits?: Trait[] } @@ -122,8 +126,8 @@ export interface GenieCollection { } traits?: Record marketplaceCount?: { marketplace: string; count: number }[] - imageUrl?: string - twitter?: string + imageUrl: string + twitterUrl?: string instagram?: string discordUrl?: string externalUrl?: string diff --git a/src/nft/utils/fetchPrice.ts b/src/nft/utils/fetchPrice.ts index cd93d67e1f..46faee1b4c 100644 --- a/src/nft/utils/fetchPrice.ts +++ b/src/nft/utils/fetchPrice.ts @@ -20,7 +20,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise fetchPrice(), {}) - return fetchedPriceData && asset.priceInfo.ETHPrice - ? (parseFloat(formatEther(asset.priceInfo.ETHPrice)) * fetchedPriceData).toString() - : undefined + + return fetchedPriceData && asset?.priceInfo?.ETHPrice + ? (parseFloat(formatEther(asset?.priceInfo?.ETHPrice)) * fetchedPriceData).toString() + : '' } diff --git a/src/nft/utils/putCommas.ts b/src/nft/utils/putCommas.ts index e45a0b243b..bd0b114aa0 100644 --- a/src/nft/utils/putCommas.ts +++ b/src/nft/utils/putCommas.ts @@ -1,4 +1,4 @@ -export const putCommas = (value?: number) => { +export const putCommas = (value: number) => { try { if (!value) return value return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')