Commit a53e773e authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

refactor: shelve Details redesign for now (#7077)

* shelve Details redesign for now

* remove feature flag enum

* no longer used utils
parent 5307d113
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useBaseEnabledFlag } from 'featureFlags/flags/baseEnabled'
import { useForceUniswapXOnFlag } from 'featureFlags/flags/forceUniswapXOn'
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { useRoutingAPIForPriceFlag } from 'featureFlags/flags/priceRoutingApi'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { UniswapXVariant, useUniswapXFlag } from 'featureFlags/flags/uniswapx'
......@@ -207,12 +206,6 @@ export default function FeatureFlagModal() {
<X size={24} />
</CloseButton>
</Header>
<FeatureFlagOption
variant={DetailsV2Variant}
value={useDetailsV2Flag()}
featureFlag={FeatureFlag.detailsV2}
label="Use the new details page for nfts"
/>
<FeatureFlagOption
variant={UniswapXVariant}
value={useUniswapXFlag()}
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useDetailsV2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.detailsV2)
}
export function useDetailsV2Enabled(): boolean {
return useDetailsV2Flag() === BaseVariant.Enabled
}
export { BaseVariant as DetailsV2Variant }
......@@ -9,7 +9,6 @@ export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
detailsV2 = 'details_v2',
debounceSwapQuote = 'debounce_swap_quote',
uniswapXEnabled = 'uniswapx_enabled', // enables sending dutch_limit config to routing-api
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
......
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
import { Row } from 'nft/components/Flex'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { SupportedTimePeriodsType, TimePeriodSwitcher } from './TimePeriodSwitcher'
const TableContentContainer = styled(Row)`
height: 568px;
justify-content: space-between;
align-items: flex-start;
`
export const ActivityTableContent = () => {
const [timePeriod, setTimePeriod] = useState<SupportedTimePeriodsType>(HistoryDuration.Week)
return (
<TableContentContainer>
<span>Activity Content</span>
<TimePeriodSwitcher activeTimePeriod={timePeriod} setTimePeriod={setTimePeriod} />
</TableContentContainer>
)
}
import { Trans } from '@lingui/macro'
import { formatNumber } from '@uniswap/conedison/format'
import { ButtonPrimary } from 'components/Button'
import Loader from 'components/Icons/LoadingSpinner'
import { useBuyAssetCallback } from 'nft/hooks/useFetchAssets'
import { GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { OfferButton } from './OfferButton'
import { ButtonStyles } from './shared'
const StyledBuyButton = styled(ButtonPrimary)`
display: flex;
flex-direction: row;
padding: 16px 24px;
gap: 8px;
line-height: 24px;
white-space: nowrap;
${ButtonStyles}
`
const Price = styled.div`
color: ${({ theme }) => theme.accentTextLightSecondary};
`
export const BuyButton = ({ asset, onDataPage }: { asset: GenieAsset; onDataPage?: boolean }) => {
const { fetchAndPurchaseSingleAsset, isLoading: isLoadingRoute } = useBuyAssetCallback()
const price = asset.sellorders?.[0]?.price.value
if (!price) {
return <OfferButton />
}
return (
<>
<StyledBuyButton disabled={isLoadingRoute} onClick={() => fetchAndPurchaseSingleAsset(asset)}>
{isLoadingRoute ? (
<>
<Trans>Fetching Route</Trans>
<Loader size="24px" stroke="white" />
</>
) : (
<>
<Trans>Buy</Trans>
<Price>{formatNumber(price)} ETH</Price>
</>
)}
</StyledBuyButton>
{onDataPage && <OfferButton smallVersion />}
</>
)
}
import { TEST_NFT_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { DataPage } from './DataPage'
it('data page loads with header showing', () => {
const { asFragment } = render(<DataPage asset={TEST_NFT_ASSET} showDataHeader={true} />)
expect(asFragment()).toMatchSnapshot()
})
// The header is hidden via opacity: 0 to maintain its spacing, so it still exists in the DOM
// Therefore we can not check for its non-existence and instead rely on comparing the full generated snapshots
it('data page loads without header showing', () => {
const { asFragment } = render(<DataPage asset={TEST_NFT_ASSET} showDataHeader={false} />)
expect(asFragment()).toMatchSnapshot()
})
import Column from 'components/Column'
import Row from 'components/Row'
import { GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { DataPageDescription } from './DataPageDescription'
import { DataPageHeader } from './DataPageHeader'
import { DataPageTable } from './DataPageTable'
import { DataPageTraits } from './DataPageTraits'
const DataPagePaddingContainer = styled.div`
padding: 24px 64px;
height: 100vh;
width: 100%;
@media screen and (max-width: ${BREAKPOINTS.md}px) {
height: 100%;
}
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
padding: 24px 48px;
}
@media screen and (max-width: ${BREAKPOINTS.xs}px) {
padding: 24px 20px;
}
`
const DataPageContainer = styled(Column)`
height: 100%;
width: 100%;
gap: 36px;
max-width: ${({ theme }) => theme.maxWidth};
margin: 0 auto;
`
const HeaderContainer = styled.div<{ showDataHeader?: boolean }>`
position: sticky;
top: ${({ theme }) => `${theme.navHeight}px`};
padding-top: 16px;
backdrop-filter: blur(12px);
z-index: 1;
transition: ${({ theme }) => `opacity ${theme.transition.duration.fast}`};
opacity: ${({ showDataHeader }) => (showDataHeader ? '1' : '0')};
@media screen and (max-width: ${BREAKPOINTS.md}px) {
display: none;
}
`
const ContentContainer = styled(Row)`
gap: 24px;
padding-bottom: 45px;
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
flex-wrap: wrap;
}
`
const LeftColumn = styled(Column)`
gap: 24px;
width: 100%;
align-self: flex-start;
`
export const DataPage = ({ asset, showDataHeader }: { asset: GenieAsset; showDataHeader: boolean }) => {
return (
<DataPagePaddingContainer>
<DataPageContainer>
<HeaderContainer showDataHeader={showDataHeader}>
<DataPageHeader asset={asset} />
</HeaderContainer>
<ContentContainer>
<LeftColumn>
{!!asset.traits?.length && <DataPageTraits asset={asset} />}
<DataPageDescription />
</LeftColumn>
<DataPageTable asset={asset} />
</ContentContainer>
</DataPageContainer>
</DataPagePaddingContainer>
)
}
import { Trans } from '@lingui/macro'
import styled from 'styled-components/macro'
import { Tab, TabbedComponent } from './TabbedComponent'
const DescriptionContentContainer = styled.div`
height: 252px;
`
const DescriptionContent = () => {
return <DescriptionContentContainer>Description Content</DescriptionContentContainer>
}
const DetailsContent = () => {
return <DescriptionContentContainer>Details Content</DescriptionContentContainer>
}
enum DescriptionTabsKeys {
Description = 'description',
Details = 'details',
}
const DescriptionTabs: Map<string, Tab> = new Map([
[
DescriptionTabsKeys.Description,
{
title: <Trans>Description</Trans>,
key: DescriptionTabsKeys.Description,
content: <DescriptionContent />,
},
],
[
DescriptionTabsKeys.Details,
{
title: <Trans>Details</Trans>,
key: DescriptionTabsKeys.Details,
content: <DetailsContent />,
},
],
])
export const DataPageDescription = () => {
return <TabbedComponent tabs={DescriptionTabs} />
}
import { TEST_NFT_ASSET, TEST_SELL_ORDER } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { DataPageHeader } from './DataPageHeader'
it('Header loads with asset with no sell orders', () => {
const { asFragment } = render(<DataPageHeader asset={TEST_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
it('Header loads with asset with a sell order', () => {
const assetWithOrder = {
...TEST_NFT_ASSET,
sellorders: [TEST_SELL_ORDER],
}
const { asFragment } = render(<DataPageHeader asset={assetWithOrder} />)
expect(asFragment()).toMatchSnapshot()
})
import Column from 'components/Column'
import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons'
import { GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { BuyButton } from './BuyButton'
const HeaderContainer = styled(Row)`
gap: 24px;
`
const AssetImage = styled.img`
width: 96px;
height: 96px;
border-radius: 20px;
object-fit: cover;
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
display: none;
}
`
const AssetText = styled(Column)`
gap: 4px;
margin-right: auto;
`
export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
return (
<HeaderContainer>
<AssetImage src={asset.imageUrl} />
<AssetText>
<Row gap="4px">
<ThemedText.SubHeaderSmall>{asset.collectionName}</ThemedText.SubHeaderSmall>
<VerifiedIcon width="16px" height="16px" />
</Row>
<ThemedText.HeadlineMedium>
{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}
</ThemedText.HeadlineMedium>
</AssetText>
<Row justifySelf="flex-end" width="min-content" gap="12px">
<BuyButton asset={asset} onDataPage />
</Row>
</HeaderContainer>
)
}
import { TEST_NFT_ASSET, TEST_OFFER, TEST_SELL_ORDER } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { ListingsTableContent } from './ListingsTableContent'
import { OffersTableContent } from './OffersTableContent'
it('data page offers table content loads with a given asset', () => {
const assetWithOffer = {
...TEST_NFT_ASSET,
offers: [TEST_OFFER],
}
const { asFragment } = render(<OffersTableContent asset={assetWithOffer} />)
expect(asFragment()).toMatchSnapshot()
})
it('data page listings table content loads with a given asset', () => {
const assetWithOrder = {
...TEST_NFT_ASSET,
sellorders: [TEST_SELL_ORDER],
}
const { asFragment } = render(<ListingsTableContent asset={assetWithOrder} />)
expect(asFragment()).toMatchSnapshot()
})
import { Trans } from '@lingui/macro'
import { GenieAsset } from 'nft/types'
import { useMemo } from 'react'
import { ActivityTableContent } from './ActivityTableContent'
import { ListingsTableContent } from './ListingsTableContent'
import { OffersTableContent } from './OffersTableContent'
import { Tab, TabbedComponent } from './TabbedComponent'
export enum TableTabsKeys {
Activity = 'activity',
Offers = 'offers',
Listings = 'listings',
}
export const DataPageTable = ({ asset }: { asset: GenieAsset }) => {
const TableTabs: Map<string, Tab> = useMemo(
() =>
new Map([
[
TableTabsKeys.Activity,
{
title: <Trans>Activity</Trans>,
key: TableTabsKeys.Activity,
content: <ActivityTableContent />,
},
],
[
TableTabsKeys.Offers,
{
title: <Trans>Offers</Trans>,
key: TableTabsKeys.Offers,
content: <OffersTableContent asset={asset} />,
count: 11, // TODO Replace Placeholder with real data
},
],
[
TableTabsKeys.Listings,
{
title: <Trans>Listings</Trans>,
key: TableTabsKeys.Listings,
content: <ListingsTableContent asset={asset} />,
count: asset.sellorders?.length,
},
],
]),
[asset]
)
return <TabbedComponent tabs={TableTabs} />
}
import { TEST_NFT_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { DataPageTraits } from './DataPageTraits'
it('data page trait component does not load with asset with no traits', () => {
const { asFragment } = render(<DataPageTraits asset={TEST_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
// TODO(NFT-1114): add test for trait component with asset with traits when rarity is not randomly generated
// while rarities are randomly generated, snapshots will never match
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
import { useSubscribeScrollState } from 'nft/hooks'
import { GenieAsset } from 'nft/types'
import { useMemo } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Scrim } from './shared'
import { Tab, TabbedComponent } from './TabbedComponent'
import { TraitRow } from './TraitRow'
const TraitsHeaderContainer = styled(Row)`
padding: 0px 12px;
`
const TraitsHeader = styled(ThemedText.SubHeaderSmall)<{
$flex?: number
$justifyContent?: string
hideOnSmall?: boolean
}>`
display: flex;
line-height: 20px;
color: ${({ theme }) => theme.textSecondary};
flex: ${({ $flex }) => $flex ?? 1};
justify-content: ${({ $justifyContent }) => $justifyContent};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: ${({ hideOnSmall }) => (hideOnSmall ? 'none' : 'flex')};
}
`
const TraitRowContainer = styled.div`
position: relative;
`
const TraitRowScrollableContainer = styled.div`
overflow-y: auto;
overflow-x: hidden;
max-height: 412px;
width: calc(100% + 6px);
${ScrollBarStyles}
`
const TraitsContent = ({ asset }: { asset: GenieAsset }) => {
const { userCanScroll, scrollRef, scrollProgress, scrollHandler } = useSubscribeScrollState()
// This is needed to prevent rerenders when handling scrolls
const traitRows = useMemo(() => {
return asset.traits?.map((trait) => (
<TraitRow collectionAddress={asset.address} trait={trait} key={trait.trait_type + ':' + trait.trait_value} />
))
}, [asset.address, asset.traits])
return (
<Column>
<TraitsHeaderContainer>
<TraitsHeader $flex={3}>
<Trans>Trait</Trans>
</TraitsHeader>
<TraitsHeader $flex={2}>
<Trans>Floor price</Trans>
</TraitsHeader>
<TraitsHeader hideOnSmall={true}>
<Trans>Quantity</Trans>
</TraitsHeader>
<TraitsHeader $flex={1.5} $justifyContent="flex-end">
<Trans>Rarity</Trans>
</TraitsHeader>
</TraitsHeaderContainer>
<TraitRowContainer>
{scrollProgress > 0 && <Scrim />}
<TraitRowScrollableContainer ref={scrollRef} onScroll={scrollHandler}>
{traitRows}
</TraitRowScrollableContainer>
{userCanScroll && scrollProgress !== 100 && <Scrim isBottom={true} />}
</TraitRowContainer>
</Column>
)
}
enum TraitTabsKeys {
Traits = 'traits',
}
export const DataPageTraits = ({ asset }: { asset: GenieAsset }) => {
const TraitTabs: Map<string, Tab> = useMemo(
() =>
new Map([
[
TraitTabsKeys.Traits,
{
title: <Trans>Traits</Trans>,
key: TraitTabsKeys.Traits,
content: <TraitsContent asset={asset} />,
count: asset.traits?.length,
},
],
]),
[asset]
)
return <TabbedComponent tabs={TraitTabs} />
}
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import Row from 'components/Row'
import { Unicon } from 'components/Unicon'
import useENSAvatar from 'hooks/useENSAvatar'
import useENSName from 'hooks/useENSName'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset } from 'nft/types'
import { getLinkForTrait } from 'nft/utils'
import { ReactNode, useReducer } from 'react'
import { ChevronDown, DollarSign } from 'react-feather'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ClickableStyle, EllipsisStyle, ExternalLink, LinkStyle, ThemedText } from 'theme'
import { isAddress, shortenAddress } from 'utils'
import { ExplorerDataType } from 'utils/getExplorerLink'
import { getExplorerLink } from 'utils/getExplorerLink'
const StyledBubble = styled(Row)`
background-color: ${({ theme }) => theme.backgroundSurface};
padding: 10px 12px 10px 8px;
border-radius: 20px;
max-width: 144px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
max-width: 169px;
}
`
const StyledLabelMedium = styled.div`
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.textPrimary};
${EllipsisStyle}
`
const StyledIcon = styled(Row)`
width: 24px;
height: 24px;
flex-shrink: 0;
color: ${({ theme }) => theme.accentAction};
border-radius: 100%;
overflow: hidden;
justify-content: center;
align-items: center;
`
const StyledLink = styled(Link)`
${ClickableStyle}
${LinkStyle}
`
const ConditionalLinkWrapper = ({
isExternal,
href,
children,
}: {
isExternal?: boolean
href: string
children: ReactNode
}) => {
return isExternal ? (
<ExternalLink href={href}>{children}</ExternalLink>
) : (
<StyledLink to={href}>{children}</StyledLink>
)
}
const InfoBubble = ({
title,
info,
icon,
href,
isExternal,
}: {
title: ReactNode
info: string
icon: ReactNode
href: string
isExternal?: boolean
}) => {
return (
<Column gap="sm">
<ThemedText.Caption color="textSecondary">{title}</ThemedText.Caption>
<ConditionalLinkWrapper isExternal={isExternal} href={href}>
<StyledBubble gap="sm">
<StyledIcon>{icon}</StyledIcon>
<StyledLabelMedium>{info}</StyledLabelMedium>
</StyledBubble>
</ConditionalLinkWrapper>
</Column>
)
}
const InfoChipDropdown = styled.button`
padding: 10px;
background-color: ${({ theme }) => theme.backgroundSurface};
color: ${({ theme }) => theme.textSecondary};
border-radius: 100%;
border: none;
cursor: pointer;
`
const InfoChipDropdownContainer = styled(Column)`
height: 100%;
margin-top: auto;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const Break = styled(Column)`
flex-basis: 100%;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const InfoChipsContainer = styled(Row)`
gap: 4px;
width: 100%;
flex-wrap: wrap;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
gap: 12px;
flex-wrap: nowrap;
}
`
const StyledChevron = styled(ChevronDown)<{ isOpen: boolean }>`
transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
will-change: transform;
transition: transform ${({ theme }) => theme.transition.duration.medium};
`
export const InfoChips = ({ asset }: { asset: GenieAsset }) => {
const { chainId } = useWeb3React()
const isMobile = useIsMobile()
const [showExtraInfoChips, toggleShowExtraInfoChips] = useReducer((s) => !s, false)
const shouldShowExtraInfoChips = !isMobile || showExtraInfoChips
const topTrait = asset?.traits?.[0]
const traitCollectionAddress = topTrait && getLinkForTrait(topTrait, asset.address)
const isChecksummedAddress = isAddress(asset.ownerAddress)
const checksummedAddress = isChecksummedAddress ? isChecksummedAddress : undefined
const { ENSName } = useENSName(checksummedAddress)
const { avatar } = useENSAvatar(checksummedAddress)
const shortenedAddress = asset.ownerAddress ? shortenAddress(asset.ownerAddress) : ''
const addressToDisplay = ENSName ?? shortenedAddress
const avatarToDisplay = avatar ? (
<img src={avatar} width={24} height={24} />
) : (
<Unicon size={24} address={asset.ownerAddress ?? ''} />
)
return (
<Column gap="sm">
<InfoChipsContainer justify="center">
<InfoBubble
title={<Trans>Owner</Trans>}
info={addressToDisplay}
icon={avatarToDisplay}
href={getExplorerLink(chainId ?? 1, asset.ownerAddress ?? '', ExplorerDataType.ADDRESS)}
isExternal={true}
/>
{traitCollectionAddress && (
<>
<InfoBubble
title={<Trans>Trait Floor</Trans>}
info="5.3 ETH"
icon={<DollarSign size={20} />}
href={traitCollectionAddress}
/>
<InfoChipDropdownContainer>
<InfoChipDropdown onClick={toggleShowExtraInfoChips}>
<StyledChevron isOpen={showExtraInfoChips} size={20} display="block" />
</InfoChipDropdown>
</InfoChipDropdownContainer>
{shouldShowExtraInfoChips && (
<>
<Break />
<InfoBubble
title={<Trans>Top Trait</Trans>}
info={topTrait.trait_value}
icon=""
href={traitCollectionAddress}
/>
</>
)}
</>
)}
</InfoChipsContainer>
</Column>
)
}
import { TEST_NFT_ASSET, TEST_NFT_COLLECTION_INFO_FOR_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { LandingPage } from './LandingPage'
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn()
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
})
window.IntersectionObserver = mockIntersectionObserver
})
describe('LandingPage', () => {
const mockSetShowDataHeader = jest.fn()
it('renders it correctly', () => {
const { asFragment } = render(
<LandingPage
asset={TEST_NFT_ASSET}
collection={TEST_NFT_COLLECTION_INFO_FOR_ASSET}
setShowDataHeader={mockSetShowDataHeader}
/>
)
expect(asFragment()).toMatchSnapshot()
})
})
import Column, { ColumnCenter } from 'components/Column'
import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons'
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { useEffect, useRef } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { BuyButton } from './BuyButton'
import { InfoChips } from './InfoChips'
import { MediaRenderer } from './MediaRenderer'
const MAX_WIDTH = 560
const LandingPageContainer = styled.div`
display: flex;
flex-direction: column;
min-height: ${({ theme }) => `calc(100vh - ${theme.navHeight}px - ${theme.mobileBottomBarHeight}px)`};
align-items: center;
padding: 22px 20px 0px;
gap: 26px;
width: 100%;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
gap: 64px;
padding-top: 28px;
}
@media screen and (min-width: ${BREAKPOINTS.md}px) {
min-height: ${({ theme }) => `calc(100vh - ${theme.navHeight}px )`};
}
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
flex-direction: row;
padding-top: 0px;
padding-bottom: ${({ theme }) => `${theme.navHeight}px`};
gap: 80px;
justify-content: center;
}
`
const InfoContainer = styled(ColumnCenter)`
gap: 40px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
width: ${MAX_WIDTH}px;
}
`
const StyledHeadlineText = styled.div`
font-weight: 600;
font-size: 20px;
line-height: 28px;
text-align: center;
color: ${({ theme }) => theme.textPrimary};
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
line-height: 44px;
font-size: 36px;
}
`
const StyledSubheaderText = styled.div`
font-weight: 500;
font-size: 14px;
line-height: 20px;
text-align: center;
color: ${({ theme }) => theme.textSecondary};
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
line-height: 24px;
font-size: 16px;
}
`
const InfoDetailsContainer = styled(Column)`
gap: 4px;
align-items: center;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
${StyledHeadlineText} {
line-height: 44px;
font-size: 36px;
}
${StyledSubheaderText} {
line-height: 24px;
font-size: 16px;
}
}
`
const MediaContainer = styled.div`
position: relative;
width: 100%;
height: 100%;
filter: drop-shadow(0px 12px 20px rgba(0, 0, 0, 0.1));
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
width: ${MAX_WIDTH}px;
height: ${MAX_WIDTH}px;
}
`
interface LandingPageProps {
asset: GenieAsset
collection: CollectionInfoForAsset
setShowDataHeader: (showDataHeader: boolean) => void
}
export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPageProps) => {
const intersectionRef = useRef<HTMLDivElement>(null)
const observableRef = useRef(
new IntersectionObserver((entries) => {
if (!entries[0].isIntersecting) {
setShowDataHeader(true)
} else {
setShowDataHeader(false)
}
})
)
// Checks if the intersectionRef is in the viewport
// If it is not in the viewport, the data page header becomes visible
useEffect(() => {
const cachedRef = intersectionRef.current
const observer = observableRef.current
if (cachedRef && observer) {
observer.observe(cachedRef)
return () => observer.unobserve(cachedRef)
}
return
}, [intersectionRef, observableRef, setShowDataHeader])
return (
<LandingPageContainer>
<MediaContainer ref={intersectionRef}>
<MediaRenderer asset={asset} />
</MediaContainer>
<InfoContainer>
<InfoDetailsContainer>
<Row justify="center" gap="4px" align="center">
<StyledSubheaderText>{collection.collectionName}</StyledSubheaderText>
{collection.isVerified && <VerifiedIcon width="16px" height="16px" />}
</Row>
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
</InfoDetailsContainer>
<InfoChips asset={asset} />
<BuyButton asset={asset} />
</InfoContainer>
</LandingPageContainer>
)
}
import { Trans } from '@lingui/macro'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { AddToBagIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset } from 'nft/types'
import { useTheme } from 'styled-components/macro'
import { TableTabsKeys } from './DataPageTable'
import { TableContentComponent } from './TableContentComponent'
import { ContentRow, HeaderRow } from './TableRowComponent'
export const ListingsTableContent = ({ asset }: { asset: GenieAsset }) => {
const isMobile = useIsMobile()
const theme = useTheme()
const headers = <HeaderRow type={TableTabsKeys.Listings} is1155={asset.tokenType === NftStandard.Erc1155} />
const contentRows = (asset.sellorders || []).map((offer, index) => (
<ContentRow
key={'offer_' + index}
content={offer}
buttonCTA={isMobile ? <AddToBagIcon color={theme.textSecondary} /> : <Trans>Add to Bag</Trans>}
is1155={asset.tokenType === NftStandard.Erc1155}
/>
))
return <TableContentComponent headerRow={headers} contentRows={contentRows} type={TableTabsKeys.Offers} />
}
import {
TEST_AUDIO_NFT_ASSET,
TEST_EMBEDDED_NFT_ASSET,
TEST_NFT_ASSET,
TEST_VIDEO_NFT_ASSET,
} from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { MediaRenderer } from './MediaRenderer'
describe('Media renderer', () => {
it('renders image nft correctly', () => {
const { asFragment } = render(<MediaRenderer asset={TEST_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders an embedded nft correctly', () => {
const { asFragment } = render(<MediaRenderer asset={TEST_EMBEDDED_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders a video nft correctly', () => {
const { asFragment } = render(<MediaRenderer asset={TEST_VIDEO_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders an audio nft correctly', () => {
const { asFragment } = render(<MediaRenderer asset={TEST_AUDIO_NFT_ASSET} />)
expect(asFragment()).toMatchSnapshot()
})
})
import { GenieAsset } from 'nft/types'
import { isAudio, isVideo } from 'nft/utils'
import { useState } from 'react'
import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
const MediaStyle = css`
position: relative;
object-fit: contain;
height: 100%;
width: 100%;
aspect-ratio: 1;
z-index: 1;
`
const StyledImage = styled.img`
${MediaStyle}
`
const StyledVideo = styled.video`
${MediaStyle}
`
const MediaShadow = styled.img`
object-fit: contain;
height: 100%;
aspect-ratio: 1;
border-radius: 20px;
filter: blur(25px);
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
filter: blur(50px);
}
`
const MediaShadowContainer = styled.div`
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
`
const StyledEmbed = styled.div`
position: relative;
overflow: hidden;
width: 100%;
padding-top: 100%;
z-index: 1;
`
const StyledIFrame = styled.iframe`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
`
const AudioContainer = styled.div`
position: relative;
`
const StyledAudio = styled.audio`
position: absolute;
left: 0;
bottom: 0;
z-index: 2;
width: 100%;
`
const AudioPlayer = ({ asset, onError }: { asset: GenieAsset; onError: () => void }) => {
return (
<AudioContainer>
<StyledImage
src={asset.imageUrl}
alt={asset.name ?? asset.collectionName + ' #' + asset.tokenId}
onError={onError}
/>
<StyledAudio controls src={asset.animationUrl} onError={onError} />
</AudioContainer>
)
}
const EmbeddedMediaPlayer = ({ asset, onError }: { asset: GenieAsset; onError: () => void }) => {
return (
<StyledEmbed>
<StyledIFrame
title={asset.name ?? `${asset.collectionName} #${asset.tokenId}`}
src={asset.animationUrl}
frameBorder={0}
sandbox="allow-scripts"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
onError={onError}
/>
</StyledEmbed>
)
}
const ContentNotAvailable = styled(ThemedText.BodySmall)`
display: flex;
background-color: ${({ theme }) => theme.backgroundSurface};
color: ${({ theme }) => theme.textSecondary};
align-items: center;
justify-content: center;
${MediaStyle}
`
// TODO: when assets query is moved to nxyz update with mediaType from the query
enum MediaType {
Audio = 'audio',
Video = 'video',
Image = 'image',
Raw = 'raw',
}
function assetMediaType(asset: GenieAsset): MediaType {
if (isAudio(asset.animationUrl ?? '')) {
return MediaType.Audio
} else if (isVideo(asset.animationUrl ?? '')) {
return MediaType.Video
} else if (asset.animationUrl) {
return MediaType.Raw
}
return MediaType.Image
}
const RenderMediaShadow = ({ imageUrl }: { imageUrl?: string }) => {
const [contentNotAvailable, setContentNotAvailable] = useState(false)
if (!imageUrl || contentNotAvailable) {
return null
}
return (
<MediaShadowContainer>
<MediaShadow src={imageUrl} onError={() => setContentNotAvailable(true)} />
</MediaShadowContainer>
)
}
const RenderMediaType = ({ asset }: { asset: GenieAsset }) => {
const [contentNotAvailable, setContentNotAvailable] = useState(false)
if (contentNotAvailable) {
return <ContentNotAvailable>Content not available</ContentNotAvailable>
}
switch (assetMediaType(asset)) {
case MediaType.Image:
return (
<StyledImage
src={asset.imageUrl}
alt={asset.name || asset.collectionName}
onError={() => setContentNotAvailable(true)}
/>
)
case MediaType.Video:
return (
<StyledVideo
src={asset.animationUrl}
autoPlay
controls
muted
loop
onError={() => setContentNotAvailable(true)}
/>
)
case MediaType.Audio:
return <AudioPlayer asset={asset} onError={() => setContentNotAvailable(true)} />
case MediaType.Raw:
return <EmbeddedMediaPlayer asset={asset} onError={() => setContentNotAvailable(true)} />
}
}
export const MediaRenderer = ({ asset }: { asset: GenieAsset }) => (
<>
<RenderMediaType asset={asset} />
<RenderMediaShadow imageUrl={asset.imageUrl} />
</>
)
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
import { DataPage } from './DataPage'
import { LandingPage } from './LandingPage'
interface NftDetailsProps {
asset: GenieAsset
collection: CollectionInfoForAsset
}
const DetailsBackground = styled.div<{ backgroundImage: string }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: ${({ backgroundImage }) => `url(${backgroundImage})`};
filter: blur(100px);
opacity: ${({ theme }) => (theme.darkMode ? 0.2 : 0.24)};
`
const DetailsContentContainer = styled.div`
z-index: ${Z_INDEX.hover};
width: 100%;
`
export const NftDetails = ({ asset, collection }: NftDetailsProps) => {
const [showDataHeader, setShowDataHeader] = useState(false)
return (
<>
{asset.imageUrl && <DetailsBackground backgroundImage={asset.imageUrl} />}
<DetailsContentContainer>
<LandingPage asset={asset} collection={collection} setShowDataHeader={setShowDataHeader} />
<DataPage asset={asset} showDataHeader={showDataHeader} />
</DetailsContentContainer>
</>
)
}
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import { HandHoldingDollarIcon } from 'nft/components/icons'
import styled from 'styled-components/macro'
import { ButtonStyles } from './shared'
const MakeOfferButtonSmall = styled(ButtonPrimary)`
padding: 16px;
${ButtonStyles}
`
const MakeOfferButtonLarge = styled(ButtonGray)`
white-space: nowrap;
${ButtonStyles}
`
export const OfferButton = ({ smallVersion }: { smallVersion?: boolean }) => {
if (smallVersion) {
return (
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
)
}
return (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)
}
import { Trans } from '@lingui/macro'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset } from 'nft/types'
import { Check } from 'react-feather'
import { useTheme } from 'styled-components/macro'
import { TEST_OFFER } from 'test-utils/nft/fixtures'
import { TableTabsKeys } from './DataPageTable'
import { TableContentComponent } from './TableContentComponent'
import { ContentRow, HeaderRow } from './TableRowComponent'
export const OffersTableContent = ({ asset }: { asset: GenieAsset }) => {
// TODO(NFT-1114) Replace with real offer data when BE supports
const mockOffers = new Array(11).fill(TEST_OFFER)
const isMobile = useIsMobile()
const theme = useTheme()
const headers = <HeaderRow type={TableTabsKeys.Offers} is1155={asset.tokenType === NftStandard.Erc1155} />
const contentRows = mockOffers.map((offer, index) => (
<ContentRow
key={'offer_' + index}
content={offer}
buttonCTA={isMobile ? <Check color={theme.textSecondary} height="20px" width="20px" /> : <Trans>Accept</Trans>}
is1155={asset.tokenType === NftStandard.Erc1155}
/>
))
return <TableContentComponent headerRow={headers} contentRows={contentRows} type={TableTabsKeys.Offers} />
}
import { Trans } from '@lingui/macro'
import Row from 'components/Row'
import { Trait } from 'nft/types'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
const RarityBar = styled.div<{ $color?: string }>`
background: ${({ $color, theme }) => $color ?? theme.backgroundOutline};
width: 2px;
height: 10px;
border-radius: 2px;
`
interface RarityValue {
threshold: number
color: string
caption: React.ReactNode
}
enum RarityLevel {
VeryCommon = 'Very Common',
Common = 'Common',
Rare = 'Rare',
VeryRare = 'Very Rare',
ExtremelyRare = 'Extremely Rare',
}
const RarityLevels: { [key in RarityLevel]: RarityValue } = {
[RarityLevel.VeryCommon]: {
threshold: 0.8,
color: colors.gray500,
caption: <Trans>Very common</Trans>,
},
[RarityLevel.Common]: {
threshold: 0.6,
color: colors.green300,
caption: <Trans>Common</Trans>,
},
[RarityLevel.Rare]: {
threshold: 0.4,
color: colors.blueVibrant,
caption: <Trans>Rare</Trans>,
},
[RarityLevel.VeryRare]: {
threshold: 0.2,
color: colors.purpleVibrant,
caption: <Trans>Very rare</Trans>,
},
[RarityLevel.ExtremelyRare]: {
threshold: 0,
color: colors.magentaVibrant,
caption: <Trans>Extremely rare</Trans>,
},
}
export function getRarityLevel(rarity: number) {
switch (true) {
case rarity > RarityLevels[RarityLevel.VeryCommon].threshold:
return RarityLevels[RarityLevel.VeryCommon]
case rarity > RarityLevels[RarityLevel.Common].threshold:
return RarityLevels[RarityLevel.Common]
case rarity > RarityLevels[RarityLevel.Rare].threshold:
return RarityLevels[RarityLevel.Rare]
case rarity > RarityLevels[RarityLevel.VeryRare].threshold:
return RarityLevels[RarityLevel.VeryRare]
case rarity >= RarityLevels[RarityLevel.ExtremelyRare].threshold:
return RarityLevels[RarityLevel.ExtremelyRare]
default:
return RarityLevels[RarityLevel.VeryCommon]
}
}
export const RarityGraph = ({ trait, rarity }: { trait: Trait; rarity: number }) => {
const rarityLevel = getRarityLevel(rarity)
return (
<Row gap="1.68px" justify="flex-end">
{Array.from({ length: 20 }).map((_, index) => (
<RarityBar
key={trait.trait_value + '_bar_' + index}
$color={index * 0.05 <= 1 - rarity ? rarityLevel?.color : undefined}
/>
))}
</Row>
)
}
import Row from 'components/Row'
import { useState } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { containerStyles } from './shared'
const TabbedComponentContainer = styled.div`
${containerStyles}
`
const TabsRow = styled(Row)`
gap: 32px;
width: 100;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
`
const Tab = styled(ThemedText.SubHeader)<{ isActive: boolean; numTabs: number }>`
color: ${({ theme, isActive }) => (isActive ? theme.textPrimary : theme.textTertiary)};
line-height: 24px;
cursor: ${({ numTabs }) => (numTabs > 1 ? 'pointer' : 'default')};
&:hover {
opacity: ${({ numTabs, theme }) => numTabs > 1 && theme.opacity.hover};
}
`
const TabNumBubble = styled(ThemedText.UtilityBadge)`
background: ${({ theme }) => theme.backgroundOutline};
border-radius: 4px;
padding: 2px 4px;
color: ${({ theme }) => theme.textSecondary};
line-height: 12px;
`
export interface Tab {
title: React.ReactNode
key: string
content: JSX.Element
count?: number
}
interface TabbedComponentProps {
tabs: Map<string, Tab>
defaultTabKey?: string
}
export const TabbedComponent = ({ tabs, defaultTabKey }: TabbedComponentProps) => {
const firstKey = tabs.keys().next().value
const [activeKey, setActiveKey] = useState(defaultTabKey ?? firstKey)
const activeContent = tabs.get(activeKey)?.content
const tabArray = Array.from(tabs.values())
return (
<TabbedComponentContainer>
<TabsRow>
{tabArray.map((tab) => (
<Tab
isActive={activeKey === tab.key}
numTabs={tabArray.length}
onClick={() => setActiveKey(tab.key)}
key={tab.key}
>
<Row gap="8px">
{tab.title}
{!!tab.count && <TabNumBubble>{tab.count > 10 ? '10+' : tab.count}</TabNumBubble>}
</Row>
</Tab>
))}
</TabsRow>
{activeContent}
</TabbedComponentContainer>
)
}
import { ScrollBarStyles } from 'components/Common'
import { useSubscribeScrollState } from 'nft/hooks'
import styled from 'styled-components/macro'
import { TableTabsKeys } from './DataPageTable'
import { Scrim } from './shared'
const TableRowsContainer = styled.div`
position: relative;
`
const TableRowScrollableContainer = styled.div`
overflow-y: auto;
overflow-x: hidden;
max-height: 264px;
${ScrollBarStyles}
`
const TableHeaderRowContainer = styled.div<{ userCanScroll: boolean }>`
margin-right: ${({ userCanScroll }) => (userCanScroll ? '11px' : '0')};
`
const TableRowContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
&:last-child {
border-bottom: none;
}
`
interface TableContentComponentProps {
headerRow: React.ReactNode
contentRows: React.ReactNode[]
type: TableTabsKeys
}
export const TableContentComponent = ({ headerRow, contentRows, type }: TableContentComponentProps) => {
const { userCanScroll, scrollRef, scrollProgress, scrollHandler } = useSubscribeScrollState()
return (
<>
<TableHeaderRowContainer userCanScroll={userCanScroll}>{headerRow}</TableHeaderRowContainer>
<TableRowsContainer>
{scrollProgress > 0 && <Scrim />}
<TableRowScrollableContainer ref={scrollRef} onScroll={scrollHandler}>
{contentRows.map((row, index) => (
<TableRowContainer key={type + '_row_' + index}>{row}</TableRowContainer>
))}
</TableRowScrollableContainer>
{userCanScroll && scrollProgress !== 100 && <Scrim isBottom={true} />}
</TableRowsContainer>
</>
)
}
import { Trans } from '@lingui/macro'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { OrderType } from 'graphql/data/__generated__/types-and-hooks'
import { useScreenSize } from 'hooks/useScreenSize'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { HomeSearchIcon } from 'nft/components/icons'
import { Offer, SellOrder } from 'nft/types'
import { formatEth, getMarketplaceIcon, timeUntil } from 'nft/utils'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
import { TableTabsKeys } from './DataPageTable'
const TableCell = styled.div<{ $flex?: number; $justifyContent?: string; $color?: string; hideOnSmall?: boolean }>`
display: flex;
flex: ${({ $flex }) => $flex ?? 1};
justify-content: ${({ $justifyContent }) => $justifyContent};
color: ${({ $color }) => $color};
flex-shrink: 0;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: ${({ hideOnSmall }) => (hideOnSmall ? 'none' : 'flex')};
}
`
const ActionButton = styled.div`
cursor: pointer;
white-space: nowrap;
${OpacityHoverState}
`
const USDPrice = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
line-height: 20px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
@media screen and (min-width: ${BREAKPOINTS.lg}px) and (max-width: ${BREAKPOINTS.xl - 1}px) {
display: none;
}
`
const Link = styled(ExternalLink)`
height: 20px;
`
const PriceCell = ({ price }: { price: number }) => {
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const parsedAmount = tryParseCurrencyAmount(price.toString(), nativeCurrency)
const usdValue = useStablecoinValue(parsedAmount)
return (
<Row gap="8px">
<ThemedText.LabelSmall color="textPrimary" lineHeight="16px">
{formatEth(price)}
</ThemedText.LabelSmall>
<USDPrice>{formatCurrencyAmount(usdValue, NumberType.FiatTokenPrice)}</USDPrice>
</Row>
)
}
export const HeaderRow = ({ type, is1155 }: { type: TableTabsKeys; is1155?: boolean }) => {
const screenSize = useScreenSize()
const isMobile = !screenSize['sm']
const isLargeScreen = screenSize['lg'] && !screenSize['xl']
const reducedPriceWidth = isMobile || isLargeScreen
return (
<Row gap="12px" padding="6px 6px 6px 0px">
<HomeSearchIcon />
<TableCell $flex={reducedPriceWidth ? 1 : 1.75}>
<ThemedText.SubHeaderSmall color="textSecondary">
<Trans>Price</Trans>
</ThemedText.SubHeaderSmall>
</TableCell>
{is1155 && (
<TableCell $flex={0.5}>
<ThemedText.SubHeaderSmall color="textSecondary">
<Trans>Quantity</Trans>
</ThemedText.SubHeaderSmall>
</TableCell>
)}
{(type === TableTabsKeys.Offers || is1155) && (
<TableCell hideOnSmall={true}>
<ThemedText.SubHeaderSmall color="textSecondary">
{type === TableTabsKeys.Offers ? <Trans>From</Trans> : <Trans>Seller</Trans>}
</ThemedText.SubHeaderSmall>
</TableCell>
)}
<TableCell $justifyContent="flex-end">
<ThemedText.SubHeaderSmall color="textSecondary">
<Trans>Expires in</Trans>
</ThemedText.SubHeaderSmall>
</TableCell>
{/* An empty cell is needed in the headers for proper vertical alignment with the action buttons */}
<TableCell $flex={isMobile ? 0.25 : 1}>&nbsp;</TableCell>
</Row>
)
}
export const ContentRow = ({
content,
buttonCTA,
is1155,
}: {
content: Offer | SellOrder
buttonCTA: React.ReactNode
is1155?: boolean
}) => {
const screenSize = useScreenSize()
const isMobile = !screenSize['sm']
const date = content.endAt && new Date(content.endAt)
const isSellOrder = 'type' in content && content.type === OrderType.Listing
const reducedPriceWidth = isMobile || (screenSize['lg'] && !screenSize['xl'])
return (
<Row gap="12px" padding="16px 6px 16px 0px">
<Link href={content.marketplaceUrl}>{getMarketplaceIcon(content.marketplace, '20')}</Link>
{content.price && (
<TableCell $flex={reducedPriceWidth ? 1 : 1.75}>
<PriceCell price={content.price.value} />
</TableCell>
)}
{is1155 && (
<TableCell $flex={0.5} $justifyContent="center">
<ThemedText.SubHeaderSmall color="textPrimary">{content.quantity}</ThemedText.SubHeaderSmall>
</TableCell>
)}
{(!isSellOrder || is1155) && (
<TableCell hideOnSmall={true}>
<Link href={`https://etherscan.io/address/${content.maker}`}>
<ThemedText.LabelSmall color="textPrimary">{shortenAddress(content.maker)}</ThemedText.LabelSmall>
</Link>
</TableCell>
)}
<TableCell $justifyContent="flex-end">
<ThemedText.LabelSmall color="textPrimary">
{date ? timeUntil(date) : <Trans>Never</Trans>}
</ThemedText.LabelSmall>
</TableCell>
<TableCell $flex={isMobile ? 0.25 : 1} $justifyContent="center">
<ActionButton>
<ThemedText.LabelSmall color="textSecondary">{buttonCTA}</ThemedText.LabelSmall>
</ActionButton>
</TableCell>
</Row>
)
}
import userEvent from '@testing-library/user-event'
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
import { act, render, screen } from 'test-utils/render'
import { TimePeriodSwitcher } from './TimePeriodSwitcher'
describe('NFT Details Activity Time Period Switcher', () => {
const mockSetTimePeriod = jest.fn()
it('renders when week is selected', () => {
render(<TimePeriodSwitcher activeTimePeriod={HistoryDuration.Week} setTimePeriod={mockSetTimePeriod} />)
expect(screen.queryByTestId('activity-time-period-switcher')?.textContent).toBe('1 week')
})
it('renders when month is selected', () => {
render(<TimePeriodSwitcher activeTimePeriod={HistoryDuration.Month} setTimePeriod={mockSetTimePeriod} />)
expect(screen.queryByTestId('activity-time-period-switcher')?.textContent).toBe('1 month')
})
it('renders when year is selected', () => {
render(<TimePeriodSwitcher activeTimePeriod={HistoryDuration.Year} setTimePeriod={mockSetTimePeriod} />)
expect(screen.queryByTestId('activity-time-period-switcher')?.textContent).toBe('1 year')
})
it('renders when all time is selected', () => {
render(<TimePeriodSwitcher activeTimePeriod={HistoryDuration.Max} setTimePeriod={mockSetTimePeriod} />)
expect(screen.queryByTestId('activity-time-period-switcher')?.textContent).toBe('All time')
})
it('renders dropdown when clicked', async () => {
render(<TimePeriodSwitcher activeTimePeriod={HistoryDuration.Max} setTimePeriod={mockSetTimePeriod} />)
await act(() => userEvent.click(screen.getByTestId('activity-time-period-switcher')))
expect(screen.queryByTestId('activity-time-period-switcher-dropdown')).toBeTruthy()
})
})
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Dispatch, ReactNode, SetStateAction, useReducer, useRef } from 'react'
import { Check, ChevronDown } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
const SwitcherAndDropdownWrapper = styled.div`
position: relative;
`
const SwitcherWrapper = styled(Row)`
gap: 4px;
padding: 8px;
cursor: pointer;
border-radius: 12px;
width: 92px;
justify-content: space-between;
user-select: none;
background: ${({ theme }) => theme.backgroundInteractive};
${OpacityHoverState}
`
const Chevron = styled(ChevronDown)<{ $isOpen: boolean }>`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.textSecondary};
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform ${({ theme }) => theme.transition.duration.fast};
`
const TimeDropdownMenu = styled(Column)`
background-color: ${({ theme }) => theme.backgroundSurface};
box-shadow: ${({ theme }) => theme.deepShadow};
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
padding: 10px 8px;
gap: 8px;
position: absolute;
top: 42px;
z-index: ${Z_INDEX.dropdown}};
right: 0px;
width: 240px;
`
const DropdownContent = styled(Row)`
gap: 4px;
padding: 10px 8px;
width: 100%;
justify-content: space-between;
border-radius: 8px;
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.stateOverlayHover};
}
`
const supportedTimePeriods = [
HistoryDuration.Week,
HistoryDuration.Month,
HistoryDuration.Year,
HistoryDuration.Max,
] as const
export type SupportedTimePeriodsType = (typeof supportedTimePeriods)[number]
const supportedTimePeriodsData: Record<SupportedTimePeriodsType, ReactNode> = {
[HistoryDuration.Week]: <Trans>1 week</Trans>,
[HistoryDuration.Month]: <Trans>1 month</Trans>,
[HistoryDuration.Year]: <Trans>1 year</Trans>,
[HistoryDuration.Max]: <Trans>All time</Trans>,
}
export const TimePeriodSwitcher = ({
activeTimePeriod,
setTimePeriod,
}: {
activeTimePeriod: SupportedTimePeriodsType
setTimePeriod: Dispatch<SetStateAction<SupportedTimePeriodsType>>
}) => {
const theme = useTheme()
const [isOpen, toggleIsOpen] = useReducer((isOpen) => !isOpen, false)
const menuRef = useRef<HTMLDivElement>(null)
useOnClickOutside(menuRef, () => {
isOpen && toggleIsOpen()
})
return (
<SwitcherAndDropdownWrapper ref={menuRef}>
<SwitcherWrapper onClick={toggleIsOpen} data-testid="activity-time-period-switcher">
<ThemedText.LabelSmall lineHeight="16px" color="textPrimary">
{supportedTimePeriodsData[activeTimePeriod]}
</ThemedText.LabelSmall>
<Chevron $isOpen={isOpen} />
</SwitcherWrapper>
{isOpen && (
<TimeDropdownMenu data-testid="activity-time-period-switcher-dropdown">
{supportedTimePeriods.map((timePeriod) => (
<DropdownContent
key={timePeriod}
onClick={() => {
setTimePeriod(timePeriod)
toggleIsOpen()
}}
>
<ThemedText.BodyPrimary lineHeight="24px">{supportedTimePeriodsData[timePeriod]}</ThemedText.BodyPrimary>
<Check size="16px" color={theme.accentActive} opacity={activeTimePeriod === timePeriod ? 1 : 0} />
</DropdownContent>
))}
</TimeDropdownMenu>
)}
</SwitcherAndDropdownWrapper>
)
}
import Column from 'components/Column'
import Row from 'components/Row'
import { Trait } from 'nft/types'
import { formatEth, getLinkForTrait } from 'nft/utils'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { getRarityLevel, RarityGraph } from './RarityGraph'
const TraitRowLink = styled(Link)`
text-decoration: none;
`
const SubheaderTiny = styled.div<{ $color?: string }>`
font-size: 10px;
line-height: 16px;
font-weight: 600;
color: ${({ theme, $color }) => ($color ? $color : theme.textSecondary)};
`
const SubheaderTinyHidden = styled(SubheaderTiny)`
opacity: 0;
`
const TraitRowContainer = styled(Row)`
padding: 12px 18px 12px 12px;
border-radius: 12px;
cursor: pointer;
text-decoration: none;
&:hover {
background: ${({ theme }) => theme.hoverDefault};
${SubheaderTinyHidden} {
opacity: 1;
}
}
`
const TraitColumnValue = styled(Column)<{ $flex?: number; $alignItems?: string }>`
gap: 4px;
flex: ${({ $flex }) => $flex ?? 3};
align-items: ${({ $alignItems }) => $alignItems};
`
const TraitRowValue = styled(ThemedText.BodySmall)<{ $flex?: number; $justifyContent?: string; hideOnSmall?: boolean }>`
display: flex;
line-height: 20px;
padding-top: 20px;
flex: ${({ $flex }) => $flex ?? 1};
justify-content: ${({ $justifyContent }) => $justifyContent};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: ${({ hideOnSmall }) => (hideOnSmall ? 'none' : 'flex')};
}
`
export const TraitRow = ({ trait, collectionAddress }: { trait: Trait; collectionAddress: string }) => {
// TODO(NFT-1114): Replace with actual rarity, count, and floor price when BE supports
// rarity eventually should be number of items with this trait / total number of items, smaller rarity means more rare
const randomRarity = Math.random()
const rarityLevel = getRarityLevel(randomRarity)
return (
<TraitRowLink to={getLinkForTrait(trait, collectionAddress)}>
<TraitRowContainer>
<TraitColumnValue>
<SubheaderTiny>{trait.trait_type}</SubheaderTiny>
<ThemedText.BodyPrimary lineHeight="20px">{trait.trait_value}</ThemedText.BodyPrimary>
</TraitColumnValue>
<TraitRowValue $flex={2}>{formatEth(randomRarity * 1000)} ETH</TraitRowValue>
<TraitRowValue hideOnSmall={true}>{Math.round(randomRarity * 10000)}</TraitRowValue>
<TraitColumnValue $flex={1.5} $alignItems="flex-end">
<SubheaderTinyHidden $color={rarityLevel.color}>{rarityLevel.caption}</SubheaderTinyHidden>
<RarityGraph trait={trait} rarity={randomRarity} />
</TraitColumnValue>
</TraitRowContainer>
</TraitRowLink>
)
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`data page trait component does not load with asset with no traits 1`] = `
<DocumentFragment>
.c1 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c2 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c6 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 8px;
}
.c4 {
color: #0D111C;
}
.c9 {
color: #7780A0;
}
.c7 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c0 {
background: #FFFFFF;
border: 1px solid #D2D9EE;
border-radius: 16px;
padding: 16px 20px;
width: 100%;
-webkit-align-self: flex-start;
-ms-flex-item-align: start;
align-self: flex-start;
}
.c3 {
gap: 32px;
width: 100;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #D2D9EE;
}
.c5 {
color: #0D111C;
line-height: 24px;
cursor: default;
}
.c8 {
padding: 0px 12px;
}
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
line-height: 20px;
color: #7780A0;
-webkit-flex: 3;
-ms-flex: 3;
flex: 3;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
line-height: 20px;
color: #7780A0;
-webkit-flex: 2;
-ms-flex: 2;
flex: 2;
}
.c12 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
line-height: 20px;
color: #7780A0;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
}
.c13 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
line-height: 20px;
color: #7780A0;
-webkit-flex: 1.5;
-ms-flex: 1.5;
flex: 1.5;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
}
.c14 {
position: relative;
}
.c15 {
overflow-y: auto;
overflow-x: hidden;
max-height: 412px;
width: calc(100% + 6px);
-webkit-scrollbar-width: thin;
-moz-scrollbar-width: thin;
-ms-scrollbar-width: thin;
scrollbar-width: thin;
-webkit-scrollbar-color: #D2D9EE transparent;
-moz-scrollbar-color: #D2D9EE transparent;
-ms-scrollbar-color: #D2D9EE transparent;
scrollbar-color: #D2D9EE transparent;
height: 100%;
}
.c15::-webkit-scrollbar {
background: transparent;
width: 4px;
overflow-y: scroll;
}
.c15::-webkit-scrollbar-thumb {
background: #D2D9EE;
border-radius: 8px;
}
@media screen and (max-width:640px) {
.c10 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
}
@media screen and (max-width:640px) {
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
}
@media screen and (max-width:640px) {
.c12 {
display: none;
}
}
@media screen and (max-width:640px) {
.c13 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
}
<div
class="c0"
>
<div
class="c1 c2 c3"
>
<div
class="c4 c5 css-rjqmed"
>
<div
class="c1 c6"
>
Traits
</div>
</div>
</div>
<div
class="c7"
>
<div
class="c1 c2 c8"
>
<div
class="c9 c10 css-1aekuku"
>
Trait
</div>
<div
class="c9 c11 css-1aekuku"
>
Floor price
</div>
<div
class="c9 c12 css-1aekuku"
>
Quantity
</div>
<div
class="c9 c13 css-1aekuku"
>
Rarity
</div>
</div>
<div
class="c14"
>
<div
class="c15"
/>
</div>
</div>
</div>
</DocumentFragment>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Media renderer renders a video nft correctly 1`] = `
<DocumentFragment>
.c0 {
position: relative;
object-fit: contain;
height: 100%;
width: 100%;
aspect-ratio: 1;
z-index: 1;
}
@media screen and (min-width:1280px) {
}
<video
autoplay=""
class="c0"
controls=""
loop=""
src="https://openseauserdata.com/files/5af92728200027caa4f3f5ae87a486a7.mp4"
/>
.c1 {
object-fit: contain;
height: 100%;
aspect-ratio: 1;
border-radius: 20px;
-webkit-filter: blur(25px);
filter: blur(25px);
}
.c0 {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@media screen and (min-width:1280px) {
.c1 {
-webkit-filter: blur(50px);
filter: blur(50px);
}
}
<div
class="c0"
>
<img
class="c1"
src="https://i.seadn.io/gae/tkDbNhjjBZV2PmYaJbJOOigywZCrlcyGRxeQFkZS1YZyihyG5GoWNWj3N9f1T7YVuaxOqdxhfJylC9ejtoCvdgBE932vd7jorVqA?w=500&auto=format"
/>
</div>
</DocumentFragment>
`;
exports[`Media renderer renders an audio nft correctly 1`] = `
<DocumentFragment>
.c1 {
position: relative;
object-fit: contain;
height: 100%;
width: 100%;
aspect-ratio: 1;
z-index: 1;
}
.c0 {
position: relative;
}
.c2 {
position: absolute;
left: 0;
bottom: 0;
z-index: 2;
width: 100%;
}
@media screen and (min-width:1280px) {
}
<div
class="c0"
>
<img
alt="Death Row Session: Vol. 2 (420 Edition) #320"
class="c1"
src="https://i.seadn.io/gae/Kze9SBqn_6O0qrHKxspo1gRkkDV2A5EmTeWtvdS-dNxBsvi_wPXUYjc6De0sUC-DYzL093102mUftenWxwWuTelqsdw-ngoBC3o2XFU?w=500&auto=format"
/>
<audio
class="c2"
controls=""
src="https://openseauserdata.com/files/4a22253e44e10baa11484a2e43efefda.mp3"
/>
</div>
.c1 {
object-fit: contain;
height: 100%;
aspect-ratio: 1;
border-radius: 20px;
-webkit-filter: blur(25px);
filter: blur(25px);
}
.c0 {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@media screen and (min-width:1280px) {
.c1 {
-webkit-filter: blur(50px);
filter: blur(50px);
}
}
<div
class="c0"
>
<img
class="c1"
src="https://i.seadn.io/gae/Kze9SBqn_6O0qrHKxspo1gRkkDV2A5EmTeWtvdS-dNxBsvi_wPXUYjc6De0sUC-DYzL093102mUftenWxwWuTelqsdw-ngoBC3o2XFU?w=500&auto=format"
/>
</div>
</DocumentFragment>
`;
exports[`Media renderer renders an embedded nft correctly 1`] = `
<DocumentFragment>
.c0 {
position: relative;
overflow: hidden;
width: 100%;
padding-top: 100%;
z-index: 1;
}
.c1 {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
@media screen and (min-width:1280px) {
}
<div
class="c0"
>
<iframe
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
class="c1"
frameborder="0"
sandbox="allow-scripts"
src="https://tokens.mathcastles.xyz/terraforms/token-html/7202"
title="Level 13 at {28, 3}"
/>
</div>
.c1 {
object-fit: contain;
height: 100%;
aspect-ratio: 1;
border-radius: 20px;
-webkit-filter: blur(25px);
filter: blur(25px);
}
.c0 {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@media screen and (min-width:1280px) {
.c1 {
-webkit-filter: blur(50px);
filter: blur(50px);
}
}
<div
class="c0"
>
<img
class="c1"
src="https://cdn.center.app/v2/1/06ff92279474add6ce06176e2a65447396edf786d169d8ccc03fddfa45ce004f/bb01f8a2f093ea4619498dae58fc19e5ba3fa38a84cabf92948994609489d566.png"
/>
</div>
</DocumentFragment>
`;
exports[`Media renderer renders image nft correctly 1`] = `
<DocumentFragment>
.c0 {
position: relative;
object-fit: contain;
height: 100%;
width: 100%;
aspect-ratio: 1;
z-index: 1;
}
@media screen and (min-width:1280px) {
}
<img
alt="Azuki #3318"
class="c0"
src="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png"
/>
.c1 {
object-fit: contain;
height: 100%;
aspect-ratio: 1;
border-radius: 20px;
-webkit-filter: blur(25px);
filter: blur(25px);
}
.c0 {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@media screen and (min-width:1280px) {
.c1 {
-webkit-filter: blur(50px);
filter: blur(50px);
}
}
<div
class="c0"
>
<img
class="c1"
src="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png"
/>
</div>
</DocumentFragment>
`;
import styled, { css } from 'styled-components/macro'
import { opacify } from 'theme/utils'
export const containerStyles = css`
background: ${({ theme }) => theme.backgroundSurface};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
padding: 16px 20px;
width: 100%;
align-self: flex-start;
`
// Scrim that fades out the top and bottom of the scrollable container, isBottom changes the direction and placement of the fade
export const Scrim = styled.div<{ isBottom?: boolean }>`
position: absolute;
pointer-events: none;
height: 88px;
left: 0px;
right: 6px;
${({ isBottom }) =>
isBottom
? 'bottom: 0px'
: `
top: 0px;
transform: matrix(1, 0, 0, -1, 0, 0);
`};
background: ${({ theme }) =>
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
display: flex;
`
export const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`
......@@ -1350,41 +1350,3 @@ export const UniswapMagentaIcon = (props: SVGProps) => (
/>
</svg>
)
export const HomeSearchIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M17.898 7.57097L11.7212 2.49102C10.7237 1.67268 9.27795 1.67185 8.28045 2.49102L2.10379 7.57016C1.83796 7.78932 1.79877 8.18268 2.01794 8.45018C2.2371 8.71768 2.63213 8.75437 2.89796 8.53604L3.54209 8.00605V15.0002C3.54209 17.0152 4.65209 18.1252 6.66709 18.1252H13.3338C15.3488 18.1252 16.4588 17.0152 16.4588 15.0002V8.00605L17.1029 8.53604C17.2195 8.63187 17.3604 8.67845 17.5004 8.67845C17.6804 8.67845 17.8596 8.601 17.9829 8.451C18.2029 8.1835 18.1638 7.79014 17.898 7.57097ZM15.2088 15.0002C15.2088 16.3143 14.6479 16.8752 13.3338 16.8752H6.66709C5.35292 16.8752 4.79209 16.3143 4.79209 15.0002V6.97852L9.07462 3.45771C9.61045 3.01688 10.3913 3.01688 10.9271 3.45771L15.2096 6.97934V15.0002H15.2088ZM6.45875 10.7643C6.45875 12.4493 7.82958 13.8202 9.51458 13.8202C10.1312 13.8202 10.7038 13.6335 11.1838 13.3176L12.4746 14.6085C12.5962 14.7302 12.7563 14.7918 12.9163 14.7918C13.0763 14.7918 13.2363 14.731 13.358 14.6085C13.6021 14.3644 13.6021 13.9685 13.358 13.7243L12.0663 12.4326C12.3813 11.9518 12.568 11.3794 12.568 10.7627C12.568 9.07854 11.1971 7.70688 9.51295 7.70688C7.82962 7.70854 6.45875 9.07933 6.45875 10.7643ZM11.3196 10.7643C11.3196 11.7602 10.5096 12.5702 9.51458 12.5702C8.51875 12.5702 7.70875 11.7602 7.70875 10.7643C7.70875 9.7685 8.51875 8.9585 9.51458 8.9585C10.5096 8.9585 11.3196 9.7685 11.3196 10.7643Z"
fill="#7780A0"
/>
</svg>
)
export const AddToBagIcon = (props: SVGProps) => (
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8.51389 18.25H5.44444C4.6467 18.25 4 17.653 4 16.9167V7.58333C4 6.84695 4.6467 6.25 5.44444 6.25H14.5556C15.3533 6.25 16 6.84695 16 7.58333V10.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 6.25L7 5.45C7 4.60131 7.31607 3.78737 7.87868 3.18726C8.44129 2.58714 9.20435 2.25 10 2.25C10.7956 2.25 11.5587 2.58714 12.1213 3.18726C12.6839 3.78737 13 4.60131 13 5.45L13 6.25"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M11 15.25H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14 12.25L14 18.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
export const HandHoldingDollarIcon = (props: SVGProps) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M5 22H3C2.448 22 2 21.552 2 21V17C2 16.448 2.448 16 3 16H5C5.552 16 6 16.448 6 17V21C6 21.552 5.552 22 5 22ZM19.66 16.02C19.43 16.02 19.2 16.08 18.98 16.21L16.71 17.5699C16.5 18.7999 15.42 19.75 14.12 19.75H11C10.59 19.75 10.25 19.41 10.25 19C10.25 18.59 10.59 18.25 11 18.25H14.12C14.74 18.25 15.25 17.75 15.25 17.12C15.25 16.5 14.74 16 14.12 16H9C7.9 16 7 16.9 7 18V20C7 21.1 7.9 22 9 22H14.6C15.51 22 16.39 21.69 17.1 21.12L20.5 18.4C20.82 18.15 21 17.76 21 17.36C21 16.58 20.36 16.02 19.66 16.02ZM18 7.5C18 10.809 15.309 13.5 12 13.5C8.691 13.5 6 10.809 6 7.5C6 4.191 8.691 1.5 12 1.5C15.309 1.5 18 4.191 18 7.5ZM14.25 8.91199C14.25 7.96999 13.626 7.14894 12.731 6.91394L11.646 6.63403C11.535 6.60503 11.438 6.53894 11.363 6.43994C11.29 6.34394 11.25 6.21901 11.25 6.08801C11.25 5.77901 11.48 5.52698 11.764 5.52698H12.237C12.497 5.52698 12.717 5.74102 12.748 6.02502C12.792 6.43702 13.157 6.73194 13.575 6.68994C13.987 6.64594 14.284 6.27504 14.24 5.86304C14.146 4.99204 13.531 4.307 12.737 4.099V3.99902C12.737 3.58502 12.401 3.24902 11.987 3.24902C11.573 3.24902 11.237 3.58502 11.237 3.99902V4.10803C10.384 4.34703 9.75201 5.13904 9.75201 6.08704C9.75201 6.54404 9.90101 6.99004 10.169 7.34204C10.442 7.70604 10.833 7.96898 11.272 8.08398L12.357 8.36401C12.59 8.42501 12.753 8.65003 12.753 8.91003C12.753 9.06303 12.696 9.20696 12.593 9.31396C12.536 9.37296 12.416 9.46997 12.239 9.46997H11.766C11.506 9.46997 11.286 9.25605 11.255 8.97205C11.211 8.56005 10.847 8.26401 10.428 8.30701C10.016 8.35101 9.719 8.72203 9.763 9.13403C9.856 9.99303 10.456 10.671 11.236 10.889V11C11.236 11.414 11.572 11.75 11.986 11.75C12.4 11.75 12.736 11.414 12.736 11V10.9C13.085 10.808 13.408 10.6281 13.67 10.3571C14.044 9.96906 14.25 9.45499 14.25 8.91199Z"
fill="white"
/>
</svg>
)
import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus, GenieAsset } from 'nft/types'
import {
buildNftTradeInput,
buildNftTradeInputFromBagItems,
filterUpdatedAssetsByState,
recalculateBagUsingPooledAssets,
} from 'nft/utils'
import { BagStatus } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { compareAssetsWithTransactionRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
......@@ -106,48 +100,3 @@ export function useFetchAssets(): () => Promise<void> {
tokenTradeInput,
])
}
export const useBuyAssetCallback = () => {
const { account } = useWeb3React()
const [fetchGqlRoute] = useNftRouteLazyQuery()
const purchaseAssets = usePurchaseAssets()
const [isLoading, setIsLoading] = useState(false)
const fetchAndPurchaseSingleAsset = useCallback(
async (asset: GenieAsset) => {
setIsLoading(true)
fetchGqlRoute({
variables: {
senderAddress: account ? account : '',
nftTrades: buildNftTradeInput([asset]),
tokenTrades: undefined,
},
pollInterval: 0,
fetchPolicy: 'no-cache',
onCompleted: (data) => {
setIsLoading(false)
if (!data.nftRoute || !data.nftRoute.route) {
return
}
const { route, routeResponse } = buildRouteResponse(data.nftRoute, false)
const { updatedAssets } = compareAssetsWithTransactionRoute([asset], route)
const { priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
const invalidData = priceChanged.length > 0 || unavailable.length > 0
if (invalidData) {
return
}
purchaseAssets(routeResponse, updatedAssets, false)
},
})
},
[account, fetchGqlRoute, purchaseAssets]
)
return useMemo(() => ({ fetchAndPurchaseSingleAsset, isLoading }), [fetchAndPurchaseSingleAsset, isLoading])
}
import { InterfacePageName } from '@uniswap/analytics-events'
import { Trace } from 'analytics'
import { useDetailsV2Enabled } from 'featureFlags/flags/nftDetails'
import { useNftAssetDetails } from 'graphql/data/nft/Details'
import { AssetDetails } from 'nft/components/details/AssetDetails'
import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading'
import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
import { NftDetails } from 'nft/components/details/detailsV2/NftDetails'
import { useParams } from 'react-router-dom'
import styled from 'styled-components/macro'
......@@ -39,11 +37,10 @@ const AssetPriceDetailsContainer = styled.div`
const AssetPage = () => {
const { tokenId = '', contractAddress = '' } = useParams()
const { data, loading } = useNftAssetDetails(contractAddress, tokenId)
const detailsV2Enabled = useDetailsV2Enabled()
const [asset, collection] = data
if (loading && !detailsV2Enabled) return <AssetDetailsLoading />
if (loading) return <AssetDetailsLoading />
return (
<>
<Trace
......@@ -52,16 +49,12 @@ const AssetPage = () => {
shouldLogImpression
>
{!!asset && !!collection ? (
detailsV2Enabled ? (
<NftDetails asset={asset} collection={collection} />
) : (
<AssetContainer>
<AssetDetails collection={collection} asset={asset} />
<AssetPriceDetailsContainer>
<AssetPriceDetails collection={collection} asset={asset} />
</AssetPriceDetailsContainer>
</AssetContainer>
)
<AssetContainer>
<AssetDetails collection={collection} asset={asset} />
<AssetPriceDetailsContainer>
<AssetPriceDetails collection={collection} asset={asset} />
</AssetPriceDetailsContainer>
</AssetContainer>
) : null}
</Trace>
</>
......
This diff is collapsed.
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