Commit b963d3b2 authored by Jack Short's avatar Jack Short Committed by GitHub

feat: cards v2 (#6048)

* initial setup

* forgot border

* cta

* rough draft

* adding marketplace container and selector

* removing last sale

* details link

* marketplace icons

* removing hovered

* adding icons

* removing unused exports

* fixing hover

* not linking to details

* linting

* mobile

* moving cards to component

* linting

* profile cards

* deleting cards

* fixing imports

* actually fixing imports

* tryingn to fix linting errors

* disabling module export for this file

* fixing build

* seems to hate uppercase C

* fixing tests

* passing data-testid correctly

* tertiary info

* removing the extra times

* button states

* adjusting tertiary

* pointer-events

* border animation

* variance bug

* cryptopunks

* unavailable for listing

* disabled cta

* set heihgt

* animated slide up

* animation

* badge changes

* shadows

* ran yarn

* removing eslint comment

* removing types and hooks

* removing from cache

* small tweaks

* removing unused tertiary info

* initial comment addressing

* more comments

* translations

* refactoring file structure

* removing trans tag

* reverting to what it prev was

* text-shadow

* eslint ignore

* updating size test
parent 799edfb4
...@@ -33,7 +33,7 @@ describe('Testing nfts', () => { ...@@ -33,7 +33,7 @@ describe('Testing nfts', () => {
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`) cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-filter')).first().click() cy.get(getTestSelector('nft-filter')).first().click()
cy.get(getTestSelector('nft-collection-filter-buy-now')).click() cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
cy.get(getTestSelector('nft-details-link')).first().click() cy.get(getTestSelector('nft-collection-asset')).first().click()
cy.get(getTestSelector('nft-details-traits')).should('exist') cy.get(getTestSelector('nft-details-traits')).should('exist')
cy.get(getTestSelector('nft-details-activity')).should('exist') cy.get(getTestSelector('nft-details-activity')).should('exist')
cy.get(getTestSelector('nft-details-description')).should('exist') cy.get(getTestSelector('nft-details-description')).should('exist')
......
...@@ -16,12 +16,12 @@ try { ...@@ -16,12 +16,12 @@ try {
} }
// The last recorded size for these assets, as reported by `yarn build`. // The last recorded size for these assets, as reported by `yarn build`.
const LAST_SIZE_MAIN_KB = 374 const LAST_SIZE_MAIN_KB = 381
// This is the async-loaded js, called <number>.<hash>.js, with a matching css file. // This is the async-loaded js, called <number>.<hash>.js, with a matching css file.
const LAST_SIZE_ENTRY_KB = 1432 const LAST_SIZE_ENTRY_KB = 1432
const SIZE_TOLERANCE_KB = 5 const SIZE_TOLERANCE_KB = 10
const jsEntrypoints = entrypoints.filter((entrypoint) => entrypoint.endsWith('js')) const jsEntrypoints = entrypoints.filter((entrypoint) => entrypoint.endsWith('js'))
assert(jsEntrypoints.length === 3) assert(jsEntrypoints.length === 3)
......
...@@ -495,8 +495,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo ...@@ -495,8 +495,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
...eventProperties, ...eventProperties,
} }
console.log(bagStatus)
return ( return (
<FooterContainer> <FooterContainer>
<Footer> <Footer>
......
...@@ -4,7 +4,7 @@ import clsx from 'clsx' ...@@ -4,7 +4,7 @@ import clsx from 'clsx'
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
import { TimedLoader } from 'nft/components/bag/TimedLoader' import { TimedLoader } from 'nft/components/bag/TimedLoader'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Suspicious } from 'nft/components/collection/Card' import { Suspicious } from 'nft/components/card/icons'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { import {
ChevronDownBagIcon, ChevronDownBagIcon,
......
import Column from 'components/Column'
import Row from 'components/Row'
import { StyledImage } from 'nft/components/card/media'
import { ReactNode } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
const BORDER_RADIUS = '12'
const StyledDetailsRelativeContainer = styled.div`
position: relative;
height: 84px;
`
const StyledDetailsContainer = styled(Column)`
position: absolute;
width: 100%;
padding: 16px 8px 0px;
justify-content: space-between;
gap: 8px;
height: 84px;
background: ${({ theme }) => theme.backgroundSurface};
will-change: transform;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} transform`};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
height: 112px;
transform: translateY(-28px);
}
`
const StyledActionButton = styled(ThemedText.BodySmall)<{
selected: boolean
isDisabled: boolean
}>`
position: absolute;
display: flex;
padding: 8px 0px;
bottom: -32px;
left: 8px;
right: 8px;
color: ${({ theme, isDisabled }) => (isDisabled ? theme.textPrimary : theme.accentTextLightPrimary)};
background: ${({ theme, selected, isDisabled }) =>
selected ? theme.accentCritical : isDisabled ? theme.backgroundInteractive : theme.accentAction};
transition: ${({ theme }) =>
`${theme.transition.duration.medium} ${theme.transition.timing.ease} bottom, ${theme.transition.duration.medium} ${theme.transition.timing.ease} visibility`};
will-change: transform;
border-radius: 8px;
justify-content: center;
font-weight: 600 !important;
line-height: 16px;
visibility: hidden;
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
visibility: visible;
bottom: 8px;
}
&:before {
background-size: 100%;
border-radius: inherit;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
}
&:hover:before {
background-color: ${({ theme, isDisabled }) => !isDisabled && theme.stateOverlayHover};
}
&:active:before {
background-color: ${({ theme, isDisabled }) => !isDisabled && theme.stateOverlayPressed};
}
`
const ActionButton = ({
isDisabled,
isSelected,
clickActionButton,
children,
}: {
isDisabled: boolean
isSelected: boolean
clickActionButton: (e: React.MouseEvent) => void
children: ReactNode
}) => {
return (
<StyledActionButton
selected={isSelected}
isDisabled={isDisabled}
onClick={(e: React.MouseEvent) => (isDisabled ? undefined : clickActionButton(e))}
>
{children}
</StyledActionButton>
)
}
const StyledCardContainer = styled.div<{ selected: boolean; isDisabled: boolean }>`
position: relative;
border-radius: ${BORDER_RADIUS}px;
background-color: ${({ theme }) => theme.backgroundSurface};
overflow: hidden;
box-shadow: 0px 0px 8px rgba(51, 53, 72, 0.04), 1px 2px 4px rgba(51, 53, 72, 0.12);
box-sizing: border-box;
-webkit-box-sizing: border-box;
isolation: isolate;
:after {
content: '';
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
border: ${({ selected, theme }) => (selected ? '3px' : !theme.darkMode ? '0px' : '1px')} solid;
border-radius: ${BORDER_RADIUS}px;
border-color: ${({ theme, selected }) => (selected ? theme.accentAction : theme.backgroundOutline)};
pointer-events: none;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
will-change: border;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
${({ selected, theme }) => selected && `border-color: ${theme.accentCritical}`};
}
}
:hover::after {
${({ selected, theme }) => selected && `border-color: ${theme.accentCritical}`};
}
:hover {
${StyledActionButton} {
visibility: visible;
bottom: 8px;
}
${StyledDetailsContainer} {
height: 112px;
transform: translateY(-28px);
}
${StyledImage} {
transform: scale(1.15);
}
}
`
const CardContainer = ({
isSelected,
isDisabled,
children,
testId,
onClick,
}: {
isSelected: boolean
isDisabled: boolean
children: ReactNode
testId?: string
onClick?: (e: React.MouseEvent) => void
}) => {
return (
<StyledCardContainer
selected={isSelected}
isDisabled={isDisabled}
draggable={false}
data-testid={testId}
onClick={onClick}
>
{children}
</StyledCardContainer>
)
}
const StyledLink = styled(Link)`
text-decoration: none;
`
const Container = ({
isSelected,
isDisabled,
detailsHref,
doNotLinkToDetails = false,
testId,
onClick,
children,
}: {
isSelected: boolean
isDisabled: boolean
detailsHref: string
doNotLinkToDetails: boolean
testId?: string
children: ReactNode
onClick?: (e: React.MouseEvent) => void
}) => {
return (
<CardContainer isSelected={isSelected} isDisabled={isDisabled} testId={testId} onClick={onClick}>
<StyledLink to={doNotLinkToDetails ? '' : detailsHref}>{children}</StyledLink>
</CardContainer>
)
}
const DetailsRelativeContainer = ({ children }: { children: ReactNode }) => {
return <StyledDetailsRelativeContainer>{children}</StyledDetailsRelativeContainer>
}
const DetailsContainer = ({ children }: { children: ReactNode }) => {
return <StyledDetailsContainer>{children}</StyledDetailsContainer>
}
const StyledInfoContainer = styled(Column)`
gap: 4px;
overflow: hidden;
width: 100%;
padding: 0px 8px;
height: 48px;
`
const InfoContainer = ({ children }: { children: ReactNode }) => {
return <StyledInfoContainer>{children}</StyledInfoContainer>
}
const StyledPrimaryRow = styled(Row)`
gap: 8px;
justify-content: space-between;
`
const PrimaryRow = ({ children }: { children: ReactNode }) => <StyledPrimaryRow>{children}</StyledPrimaryRow>
const StyledPrimaryDetails = styled(Row)`
justify-items: center;
overflow: hidden;
white-space: nowrap;
gap: 8px;
`
const PrimaryDetails = ({ children }: { children: ReactNode }) => (
<StyledPrimaryDetails>{children}</StyledPrimaryDetails>
)
const PrimaryInfoContainer = styled(ThemedText.BodySmall)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 600 !important;
line-height: 20px;
`
const PrimaryInfo = ({ children }: { children: ReactNode }) => {
return <PrimaryInfoContainer>{children}</PrimaryInfoContainer>
}
const StyledSecondaryRow = styled(Row)`
justify-content: space-between;
`
const SecondaryRow = ({ children }: { children: ReactNode }) => <StyledSecondaryRow>{children}</StyledSecondaryRow>
const StyledSecondaryDetails = styled(Row)`
overflow: hidden;
white-space: nowrap;
`
const SecondaryDetails = ({ children }: { children: ReactNode }) => (
<StyledSecondaryDetails>{children}</StyledSecondaryDetails>
)
const SecondaryInfoContainer = styled(ThemedText.BodyPrimary)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 24px;
`
const SecondaryInfo = ({ children }: { children: ReactNode }) => {
return <SecondaryInfoContainer>{children}</SecondaryInfoContainer>
}
export {
ActionButton,
Container,
DetailsContainer,
DetailsRelativeContainer,
InfoContainer,
PrimaryDetails,
PrimaryInfo,
PrimaryRow,
SecondaryDetails,
SecondaryInfo,
SecondaryRow,
}
import { Trans } from '@lingui/macro'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { getMarketplaceIcon } from 'nft/components/card/utils'
import { CollectionSelectedAssetIcon } from 'nft/components/icons'
import { Markets } from 'nft/types'
import { putCommas } from 'nft/utils'
import { AlertTriangle, Check, Tag } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const StyledMarketplaceContainer = styled.div<{ isText?: boolean }>`
position: absolute;
display: flex;
top: 12px;
left: 12px;
height: 32px;
width: ${({ isText }) => (isText ? 'auto' : '32px')};
padding: ${({ isText }) => (isText ? '0px 8px' : '0px')};
background: rgba(93, 103, 133, 0.24);
color: ${({ theme }) => theme.accentTextLightPrimary};
justify-content: center;
align-items: center;
border-radius: 32px;
z-index: 2;
`
const ListPriceRowContainer = styled(Row)`
gap: 6px;
color: ${({ theme }) => theme.accentTextLightPrimary};
font-size: 14px;
font-weight: 600;
line-height: 16px;
text-shadow: 1px 1px 3px rgba(51, 53, 72, 0.54);
`
export const MarketplaceContainer = ({
isSelected,
marketplace,
tokenType,
listedPrice,
}: {
isSelected: boolean
marketplace?: Markets
tokenType?: NftStandard
listedPrice?: string
}) => {
if (isSelected) {
if (!marketplace) {
return (
<StyledMarketplaceContainer>
<Check size={20} />
</StyledMarketplaceContainer>
)
}
return (
<StyledMarketplaceContainer>
<CollectionSelectedAssetIcon width="20px" height="20px" viewBox="0 0 20 20" />
</StyledMarketplaceContainer>
)
}
if (listedPrice) {
return (
<StyledMarketplaceContainer isText={true}>
<ListPriceRowContainer>
<Tag size={20} />
{listedPrice} ETH
</ListPriceRowContainer>
</StyledMarketplaceContainer>
)
}
if (!marketplace || tokenType === NftStandard.Erc1155) {
return null
}
return <StyledMarketplaceContainer>{getMarketplaceIcon(marketplace)}</StyledMarketplaceContainer>
}
const SuspiciousIcon = styled(AlertTriangle)`
width: 16px;
height: 16px;
color: ${({ theme }) => theme.accentFailure};
`
interface RankingProps {
provider: { url?: string; rank?: number }
}
const RarityLogoContainer = styled(Row)`
margin-right: 8px;
width: 16px;
`
const RarityText = styled(ThemedText.BodySmall)`
display: flex;
`
const RarityInfo = styled(ThemedText.Caption)`
flex-shrink: 0;
color: ${({ theme }) => theme.textSecondary};
background: ${({ theme }) => theme.backgroundInteractive};
padding: 4px 6px;
border-radius: 4px;
font-weight: 700 !important;
line-height: 12px;
text-align: right;
cursor: pointer;
`
export const Ranking = ({ provider }: RankingProps) => {
if (!provider.rank) {
return null
}
return (
<RarityInfo>
<MouseoverTooltip
text={
<Row>
<RarityLogoContainer>
<img src="/nft/svgs/gem.svg" width={16} height={16} />
</RarityLogoContainer>
<RarityText>Ranking by Rarity Sniper</RarityText>
</Row>
}
placement="top"
>
# {putCommas(provider.rank)}
</MouseoverTooltip>
</RarityInfo>
)
}
const SuspiciousIconContainer = styled(Row)`
flex-shrink: 0;
`
export const Suspicious = () => {
return (
<MouseoverTooltip
text={
<ThemedText.BodySmall>
<Trans>Blocked on OpenSea</Trans>
</ThemedText.BodySmall>
}
placement="top"
>
<SuspiciousIconContainer>
<SuspiciousIcon />
</SuspiciousIconContainer>
</MouseoverTooltip>
)
}
import * as Card from 'nft/components/card/containers'
import { MarketplaceContainer } from 'nft/components/card/icons'
import { MediaContainer } from 'nft/components/card/media'
import { detailsHref, getNftDisplayComponent, useSelectAsset } from 'nft/components/card/utils'
import { useBag } from 'nft/hooks'
import { GenieAsset, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
import { floorFormatter } from 'nft/utils'
import { ReactNode } from 'react'
import { shallow } from 'zustand/shallow'
interface NftCardProps {
asset: GenieAsset | WalletAsset
display: NftCardDisplayProps
isSelected: boolean
isDisabled: boolean
selectAsset: () => void
unselectAsset: () => void
onClick?: () => void
doNotLinkToDetails?: boolean
mediaShouldBePlaying: boolean
uniformAspectRatio?: UniformAspectRatio
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
renderedHeight?: number
setRenderedHeight?: (renderedHeight: number | undefined) => void
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
testId?: string
}
export interface NftCardDisplayProps {
primaryInfo?: ReactNode
primaryInfoIcon?: ReactNode
primaryInfoRight?: ReactNode
secondaryInfo?: ReactNode
selectedInfo?: ReactNode
notSelectedInfo?: ReactNode
disabledInfo?: ReactNode
}
export const NftCard = ({
asset,
display,
isSelected,
selectAsset,
unselectAsset,
isDisabled,
onClick,
doNotLinkToDetails = false,
mediaShouldBePlaying,
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
setCurrentTokenPlayingMedia,
testId,
}: NftCardProps) => {
const clickActionButton = useSelectAsset(selectAsset, unselectAsset, isSelected, isDisabled, onClick)
const { bagExpanded, setBagExpanded } = useBag(
(state) => ({
bagExpanded: state.bagExpanded,
setBagExpanded: state.setBagExpanded,
}),
shallow
)
const collectionNft = 'marketplace' in asset
const profileNft = 'asset_contract' in asset
const tokenType = collectionNft ? asset.tokenType : profileNft ? asset.asset_contract.tokenType : undefined
const marketplace = collectionNft ? asset.marketplace : undefined
const listedPrice =
profileNft && !isDisabled && asset.floor_sell_order_price ? floorFormatter(asset.floor_sell_order_price) : undefined
return (
<Card.Container
isSelected={isSelected}
isDisabled={isDisabled}
detailsHref={detailsHref(asset)}
doNotLinkToDetails={doNotLinkToDetails}
testId={testId}
onClick={() => {
if (bagExpanded) setBagExpanded({ bagExpanded: false })
}}
>
<MediaContainer isDisabled={isDisabled}>
<MarketplaceContainer
isSelected={isSelected}
marketplace={marketplace}
tokenType={tokenType}
listedPrice={listedPrice}
/>
{getNftDisplayComponent(
asset,
mediaShouldBePlaying,
setCurrentTokenPlayingMedia,
uniformAspectRatio,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight
)}
</MediaContainer>
<Card.DetailsRelativeContainer>
<Card.DetailsContainer>
<Card.InfoContainer>
<Card.PrimaryRow>
<Card.PrimaryDetails>
<Card.PrimaryInfo>{display.primaryInfo}</Card.PrimaryInfo>
{display.primaryInfoIcon}
</Card.PrimaryDetails>
{display.primaryInfoRight}
</Card.PrimaryRow>
<Card.SecondaryRow>
<Card.SecondaryDetails>
<Card.SecondaryInfo>{display.secondaryInfo}</Card.SecondaryInfo>
</Card.SecondaryDetails>
</Card.SecondaryRow>
</Card.InfoContainer>
</Card.DetailsContainer>
</Card.DetailsRelativeContainer>
<Card.ActionButton clickActionButton={clickActionButton} isDisabled={isDisabled} isSelected={isSelected}>
{isSelected ? display.selectedInfo : isDisabled ? display.disabledInfo : display.notSelectedInfo}
</Card.ActionButton>
</Card.Container>
)
}
import { Trans } from '@lingui/macro'
import Row from 'components/Row'
import { getHeightFromAspectRatio, getMediaAspectRatio, handleUniformAspectRatio } from 'nft/components/card/utils'
import { UniformAspectRatio, UniformAspectRatios } from 'nft/types'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { Pause, Play } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { colors } from 'theme/colors'
const StyledImageContainer = styled.div<{ isDisabled?: boolean }>`
position: relative;
pointer-events: auto;
&:hover {
opacity: ${({ isDisabled, theme }) => (isDisabled ? theme.opacity.disabled : theme.opacity.enabled)};
}
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`
export const MediaContainer = ({ isDisabled, children }: { isDisabled: boolean; children: ReactNode }) => {
return <StyledImageContainer isDisabled={isDisabled}>{children}</StyledImageContainer>
}
interface ImageProps {
src?: string
uniformAspectRatio?: UniformAspectRatio
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
renderedHeight?: number
setRenderedHeight?: (renderedHeight: number | undefined) => void
}
const StyledMediaContainer = styled(Row)`
overflow: hidden;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
`
export const StyledImage = styled.img<{
imageLoading: boolean
$aspectRatio?: string
$hidden?: boolean
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} transform`};
will-change: transform;
object-fit: contain;
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
background: ${({ theme, imageLoading }) =>
imageLoading && `linear-gradient(270deg, ${theme.backgroundOutline} 0%, ${theme.backgroundSurface} 100%)`};
`
export const NftImage = ({
src,
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
}: ImageProps) => {
const [noContent, setNoContent] = useState(!src)
const [loaded, setLoaded] = useState(false)
if (noContent) {
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
}
return (
<StyledMediaContainer>
<StyledImage
src={src}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
imageLoading={!loaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setLoaded(true)
}}
/>
</StyledMediaContainer>
)
}
interface MediaProps {
isAudio?: boolean
mediaSrc?: string
tokenId?: string
shouldPlay: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}
const PlaybackButton = styled.div<{ pauseButton?: boolean }>`
display: ${({ pauseButton }) => (pauseButton ? 'block' : 'none')};
color: ${({ theme }) => theme.accentAction};
position: absolute;
height: 40px;
width: 40px;
z-index: 1;
margin-left: calc(100% - 50px);
transform: translateY(-76px);
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: block;
}
${StyledImageContainer}:hover & {
display: block;
}
`
const StyledVideo = styled.video<{
$aspectRatio?: string
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
`
const StyledInnerMediaContainer = styled(Row)`
position: absolute;
left: 0px;
top: 0px;
`
const StyledAudio = styled.audio`
width: 100%;
height: 100%;
`
export const NftPlayableMedia = ({
isAudio,
src,
mediaSrc,
tokenId,
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
shouldPlay,
setCurrentTokenPlayingMedia,
}: MediaProps & ImageProps) => {
const mediaRef = useRef<HTMLVideoElement>(null)
const [noContent, setNoContent] = useState(!src)
const [imageLoaded, setImageLoaded] = useState(false)
useEffect(() => {
if (shouldPlay && mediaRef.current) {
mediaRef.current.play()
} else if (!shouldPlay && mediaRef.current) {
mediaRef.current.pause()
}
}, [shouldPlay])
if (noContent) {
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
}
return (
<>
<StyledMediaContainer>
<StyledImage
src={src}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
imageLoading={!imageLoaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setImageLoaded(true)
}}
$hidden={shouldPlay && !isAudio}
/>
</StyledMediaContainer>
{shouldPlay ? (
<>
<PlaybackButton pauseButton={true}>
<Pause
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(undefined)
}}
/>
</PlaybackButton>
<StyledInnerMediaContainer>
{isAudio ? (
<StyledAudio
ref={mediaRef}
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
}}
>
<source src={mediaSrc} />
</StyledAudio>
) : (
<StyledVideo
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
ref={mediaRef}
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
}}
loop
playsInline
>
<source src={mediaSrc} />
</StyledVideo>
)}
</StyledInnerMediaContainer>
</>
) : (
<PlaybackButton>
<Play
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(tokenId)
}}
/>
</PlaybackButton>
)}
</>
)
}
const NoContentContainerBackground = styled.div<{ $height?: number }>`
position: relative;
width: 100%;
height: ${({ $height }) => ($height ? `${$height}px` : 'auto')};
padding-top: 100%;
background: ${({ theme }) =>
`linear-gradient(90deg, ${theme.backgroundSurface} 0%, ${theme.backgroundInteractive} 95.83%)`};
`
const NoContentText = styled(ThemedText.BodyPrimary)`
position: absolute;
text-align: center;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
color: ${colors.gray500};
`
const NoContentContainer = ({ height }: { height?: number }) => (
<>
<NoContentContainerBackground $height={height}>
<NoContentText>
<Trans>Content not</Trans>
<br />
<Trans>available yet</Trans>
</NoContentText>
</NoContentContainerBackground>
</>
)
import { NftImage, NftPlayableMedia } from 'nft/components/card/media'
import {
LarvaLabsMarketplaceIcon,
LooksRareIcon,
Nft20Icon,
NftXIcon,
OpenSeaMarketplaceIcon,
SudoSwapIcon,
X2y2Icon,
} from 'nft/components/icons'
import { GenieAsset, Markets, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
import { isAudio, isVideo } from 'nft/utils'
import { ReactNode, useCallback } from 'react'
enum AssetMediaType {
Image,
Video,
Audio,
}
function getAssetImageUrl(asset: GenieAsset | WalletAsset) {
return asset.imageUrl || asset.smallImageUrl
}
function getAssetMediaUrl(asset: GenieAsset | WalletAsset) {
return asset.animationUrl
}
export function detailsHref(asset: GenieAsset | WalletAsset) {
if ('address' in asset) return `/nfts/asset/${asset.address}/${asset.tokenId}?origin=collection`
if ('asset_contract' in asset) return `/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}?origin=profile`
return '/nfts/profile'
}
function getAssetMediaType(asset: GenieAsset | WalletAsset) {
let assetMediaType = AssetMediaType.Image
if (asset.animationUrl) {
if (isAudio(asset.animationUrl)) {
assetMediaType = AssetMediaType.Audio
} else if (isVideo(asset.animationUrl)) {
assetMediaType = AssetMediaType.Video
}
}
return assetMediaType
}
export function getNftDisplayComponent(
asset: GenieAsset | WalletAsset,
mediaShouldBePlaying: boolean,
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void,
uniformAspectRatio?: UniformAspectRatio,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void,
renderedHeight?: number,
setRenderedHeight?: (renderedHeight: number | undefined) => void
) {
switch (getAssetMediaType(asset)) {
case AssetMediaType.Image:
return (
<NftImage
src={getAssetImageUrl(asset)}
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
)
case AssetMediaType.Video:
return (
<NftPlayableMedia
src={getAssetImageUrl(asset)}
mediaSrc={getAssetMediaUrl(asset)}
tokenId={asset.tokenId}
shouldPlay={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
)
case AssetMediaType.Audio:
return (
<NftPlayableMedia
isAudio={true}
src={getAssetImageUrl(asset)}
mediaSrc={getAssetMediaUrl(asset)}
tokenId={asset.tokenId}
shouldPlay={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
)
}
}
export function useSelectAsset(
selectAsset: () => void,
unselectAsset: () => void,
isSelected: boolean,
isDisabled: boolean,
onClick?: () => void
) {
return useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
if (isDisabled) {
return
}
if (onClick) {
onClick()
return
}
return isSelected ? unselectAsset() : selectAsset()
},
[selectAsset, isDisabled, onClick, unselectAsset, isSelected]
)
}
export function getMarketplaceIcon(market: Markets): ReactNode {
switch (market) {
case Markets.Opensea:
return <OpenSeaMarketplaceIcon />
case Markets.LooksRare:
return <LooksRareIcon />
case Markets.X2Y2:
return <X2y2Icon />
case Markets.Sudoswap:
return <SudoSwapIcon />
case Markets.NFT20:
return <Nft20Icon />
case Markets.NFTX:
return <NftXIcon />
case Markets.Cryptopunks:
return <LarvaLabsMarketplaceIcon />
default:
return null
}
}
export const handleUniformAspectRatio = (
uniformAspectRatio: UniformAspectRatio,
e: React.SyntheticEvent<HTMLElement, Event>,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void,
renderedHeight?: number,
setRenderedHeight?: (renderedHeight: number | undefined) => void
) => {
if (uniformAspectRatio !== UniformAspectRatios.square && setUniformAspectRatio) {
const height = e.currentTarget.clientHeight
const width = e.currentTarget.clientWidth
const aspectRatio = width / height
if (
(!renderedHeight || renderedHeight !== height) &&
aspectRatio < 1 &&
uniformAspectRatio !== UniformAspectRatios.square &&
setRenderedHeight
) {
setRenderedHeight(height)
}
const variance = 0.05
if (uniformAspectRatio === UniformAspectRatios.unset) {
setUniformAspectRatio(aspectRatio >= 1 ? UniformAspectRatios.square : aspectRatio)
} else if (aspectRatio > uniformAspectRatio + variance || aspectRatio < uniformAspectRatio - variance) {
setUniformAspectRatio(UniformAspectRatios.square)
setRenderedHeight && setRenderedHeight(undefined)
}
}
}
export function getHeightFromAspectRatio(
uniformAspectRatio: UniformAspectRatio,
renderedHeight?: number
): number | undefined {
return uniformAspectRatio === UniformAspectRatios.square || uniformAspectRatio === UniformAspectRatios.unset
? undefined
: renderedHeight
}
export function getMediaAspectRatio(
uniformAspectRatio?: UniformAspectRatio,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
): string {
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
}
import { BigNumber } from '@ethersproject/bignumber'
import { t, Trans } from '@lingui/macro'
import Column from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import {
MinusIconLarge,
PauseButtonIcon,
PlayButtonIcon,
PlusIconLarge,
PoolIcon,
RarityVerifiedIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset, Rarity, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils'
import { floorFormatter } from 'nft/utils/numbers'
import {
createContext,
MouseEvent,
ReactNode,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react'
import { AlertTriangle, Pause, Play } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { colors } from 'theme/colors'
import { opacify } from 'theme/utils'
/* -------- ASSET CONTEXT -------- */
export interface CardContextProps {
asset: GenieAsset | WalletAsset
hovered: boolean
selected: boolean
href: string
setHref: (href: string) => void
addAssetToBag: () => void
removeAssetFromBag: () => void
}
const CardContext = createContext<CardContextProps | undefined>(undefined)
const BORDER_RADIUS = '12'
const useCardContext = () => {
const context = useContext(CardContext)
if (!context) throw new Error('Must use context inside of provider')
return context
}
export enum AssetMediaType {
Image,
Video,
Audio,
}
const useNotForSale = (asset: GenieAsset) =>
useMemo(() => {
let notForSale = true
notForSale = asset.notForSale || BigNumber.from(asset.priceInfo ? asset.priceInfo.ETHPrice : 0).lt(0)
return notForSale
}, [asset])
const useAssetMediaType = (asset: GenieAsset | WalletAsset) =>
useMemo(() => {
let assetMediaType = AssetMediaType.Image
if (asset.animationUrl) {
if (isAudio(asset.animationUrl)) {
assetMediaType = AssetMediaType.Audio
} else if (isVideo(asset.animationUrl)) {
assetMediaType = AssetMediaType.Video
}
}
return assetMediaType
}, [asset])
const baseHref = (asset: GenieAsset | WalletAsset) => {
if ('address' in asset) return `/#/nfts/asset/${asset.address}/${asset.tokenId}?origin=collection`
if ('asset_contract' in asset) return `/#/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}?origin=profile`
return '/#/nfts/profile'
}
const DetailsLinkContainer = styled.a`
display: flex;
align-items: center;
flex-shrink: 0;
text-decoration: none;
font-size: 14px;
font-weight: 500;
border: 1px solid;
color: ${({ theme }) => theme.accentAction};
border-color: ${({ theme }) => theme.accentActionSoft};
padding: 2px 6px;
border-radius: 6px;
${OpacityHoverState};
`
const SuspiciousIcon = styled(AlertTriangle)`
width: 16px;
height: 16px;
color: ${({ theme }) => theme.accentFailure};
`
const Erc1155ControlsRow = styled.div`
position: absolute;
display: flex;
width: 100%;
bottom: 12px;
z-index: 2;
justify-content: center;
`
const Erc1155ControlsContainer = styled.div`
display: flex;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: ${BORDER_RADIUS}px;
overflow: hidden;
`
const Erc1155ControlsDisplay = styled(ThemedText.HeadlineSmall)`
display: flex;
padding: 6px 8px;
width: 60px;
background: ${({ theme }) => theme.backgroundBackdrop};
justify-content: center;
cursor: default;
`
const Erc1155ControlsInput = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 40px;
background: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
:hover {
color: ${({ theme }) => theme.accentAction};
}
`
const RankingContainer = styled.div`
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
`
const StyledImageContainer = styled.div<{ isDisabled?: boolean }>`
position: relative;
pointer-events: auto;
&:hover {
opacity: ${({ isDisabled, theme }) => (isDisabled ? theme.opacity.disabled : theme.opacity.enabled)};
}
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`
const CardContainer = styled.div<{ selected: boolean }>`
position: relative;
border-radius: ${BORDER_RADIUS}px;
background-color: ${({ theme }) => theme.backgroundSurface};
overflow: hidden;
padding-bottom: 12px;
border-radius: 16px;
box-shadow: rgba(0, 0, 0, 10%) 0px 4px 12px;
box-sizing: border-box;
-webkit-box-sizing: border-box;
:after {
content: '';
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
border: ${({ selected }) => (selected ? '2px' : '1px')} solid;
border-radius: 16px;
border-color: ${({ theme, selected }) => (selected ? theme.accentAction : opacify(12, colors.gray500))};
pointer-events: none;
}
`
/* -------- ASSET CARD -------- */
interface CardProps {
asset: GenieAsset | WalletAsset
selected: boolean
addAssetToBag: () => void
removeAssetFromBag: () => void
children: ReactNode
isDisabled?: boolean
onClick?: () => void
}
const Container = ({
asset,
selected,
addAssetToBag,
removeAssetFromBag,
children,
isDisabled,
onClick,
}: CardProps) => {
const [hovered, toggleHovered] = useReducer((s) => !s, false)
const [href, setHref] = useState(baseHref(asset))
const providerValue = useMemo(
() => ({
asset,
selected,
hovered,
toggleHovered,
href,
setHref,
addAssetToBag,
removeAssetFromBag,
}),
[asset, hovered, selected, href, addAssetToBag, removeAssetFromBag]
)
const assetRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (hovered && assetRef.current?.matches(':hover') === false) toggleHovered()
}, [hovered])
const handleAssetInBag = (e: MouseEvent) => {
if (!asset.notForSale) {
e.preventDefault()
!selected ? addAssetToBag() : removeAssetFromBag()
}
}
const toggleHover = useCallback(() => toggleHovered(), [])
return (
<CardContext.Provider value={providerValue}>
<CardContainer
selected={selected}
ref={assetRef}
draggable={false}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
onClick={isDisabled ? () => null : onClick ?? handleAssetInBag}
>
{children}
</CardContainer>
</CardContext.Provider>
)
}
const ImageContainer = ({ children, isDisabled = false }: { children: ReactNode; isDisabled?: boolean }) => (
<StyledImageContainer isDisabled={isDisabled}>{children}</StyledImageContainer>
)
const handleUniformAspectRatio = (
uniformAspectRatio: UniformAspectRatio,
e: React.SyntheticEvent<HTMLElement, Event>,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void,
renderedHeight?: number,
setRenderedHeight?: (renderedHeight: number | undefined) => void
) => {
if (uniformAspectRatio !== UniformAspectRatios.square && setUniformAspectRatio) {
const height = e.currentTarget.clientHeight
const width = e.currentTarget.clientWidth
const aspectRatio = width / height
if (
(!renderedHeight || renderedHeight !== height) &&
aspectRatio < 1 &&
uniformAspectRatio !== UniformAspectRatios.square &&
setRenderedHeight
) {
setRenderedHeight(height)
}
if (uniformAspectRatio === UniformAspectRatios.unset) {
setUniformAspectRatio(aspectRatio >= 1 ? UniformAspectRatios.square : aspectRatio)
} else if (uniformAspectRatio !== aspectRatio) {
setUniformAspectRatio(UniformAspectRatios.square)
setRenderedHeight && setRenderedHeight(undefined)
}
}
}
function getHeightFromAspectRatio(uniformAspectRatio: UniformAspectRatio, renderedHeight?: number): number | undefined {
return uniformAspectRatio === UniformAspectRatios.square || uniformAspectRatio === UniformAspectRatios.unset
? undefined
: renderedHeight
}
function getMediaAspectRatio(
uniformAspectRatio?: UniformAspectRatio,
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
): string {
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
}
interface ImageProps {
uniformAspectRatio?: UniformAspectRatio
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
renderedHeight?: number
setRenderedHeight?: (renderedHeight: number | undefined) => void
}
const StyledMediaContainer = styled(Row)`
overflow: hidden;
border-top-left-radius: ${BORDER_RADIUS}px;
border-top-right-radius: ${BORDER_RADIUS}px;
`
const StyledImage = styled.img<{
hovered: boolean
imageLoading: boolean
$aspectRatio?: string
$hidden?: boolean
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} transform`};
will-change: transform;
object-fit: contain;
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
transform: ${({ hovered }) => hovered && 'scale(1.15)'};
background: ${({ theme, imageLoading }) =>
imageLoading && `linear-gradient(270deg, ${theme.backgroundOutline} 0%, ${theme.backgroundSurface} 100%)`};
`
const Image = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
}: ImageProps) => {
const { hovered, asset } = useCardContext()
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
const [loaded, setLoaded] = useState(false)
const isMobile = useIsMobile()
if (noContent) {
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
}
return (
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!loaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setLoaded(true)
}}
/>
</StyledMediaContainer>
)
}
interface MediaProps {
shouldPlay: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}
const PlaybackButton = styled.div`
position: absolute;
height: 40px;
width: 40px;
z-index: 1;
margin-left: calc(100% - 50px);
transform: translateY(-56px);
`
const StyledVideo = styled.video<{
$aspectRatio?: string
}>`
width: 100%;
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
`
const StyledInnerMediaContainer = styled(Row)`
position: absolute;
left: 0px;
top: 0px;
`
const Video = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
shouldPlay,
setCurrentTokenPlayingMedia,
}: MediaProps & ImageProps) => {
const vidRef = useRef<HTMLVideoElement>(null)
const { hovered, asset } = useCardContext()
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
const [imageLoaded, setImageLoaded] = useState(false)
const isMobile = useIsMobile()
if (shouldPlay) {
vidRef.current?.play()
} else {
vidRef.current?.pause()
}
if (noContent) {
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
}
return (
<>
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
alt={asset.name || asset.tokenId}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!imageLoaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setImageLoaded(true)
}}
$hidden={shouldPlay}
/>
</StyledMediaContainer>
{shouldPlay ? (
<>
<PlaybackButton>
<Pause
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(undefined)
}}
/>
</PlaybackButton>
<StyledInnerMediaContainer>
<StyledVideo
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
ref={vidRef}
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
}}
loop
playsInline
>
<source src={asset.animationUrl} />
</StyledVideo>
</StyledInnerMediaContainer>
</>
) : (
<PlaybackButton>
{((!isMobile && hovered) || isMobile) && (
<Play
size="24px"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(asset.tokenId)
}}
/>
)}
</PlaybackButton>
)}
</>
)
}
const StyledAudio = styled.audio`
width: 100%;
height: 100%;
`
const Audio = ({
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
renderedHeight,
setRenderedHeight,
shouldPlay,
setCurrentTokenPlayingMedia,
}: MediaProps & ImageProps) => {
const audRef = useRef<HTMLAudioElement>(null)
const { hovered, asset } = useCardContext()
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
const [imageLoaded, setImageLoaded] = useState(false)
const isMobile = useIsMobile()
if (shouldPlay) {
audRef.current?.play()
} else {
audRef.current?.pause()
}
if (noContent) {
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
}
return (
<>
<StyledMediaContainer>
<StyledImage
src={asset.imageUrl || asset.smallImageUrl}
alt={asset.name || asset.tokenId}
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
hovered={hovered && !isMobile}
imageLoading={!imageLoaded}
draggable={false}
onError={() => setNoContent(true)}
onLoad={(e) => {
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
setImageLoaded(true)
setImageLoaded(true)
}}
/>
</StyledMediaContainer>
{shouldPlay ? (
<>
<PlaybackButton>
<PauseButtonIcon
width="100%"
height="100%"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(undefined)
}}
/>
</PlaybackButton>
<StyledInnerMediaContainer>
<StyledAudio
ref={audRef}
onEnded={(e) => {
e.preventDefault()
setCurrentTokenPlayingMedia(undefined)
}}
>
<source src={asset.animationUrl} />
</StyledAudio>
</StyledInnerMediaContainer>
</>
) : (
<PlaybackButton>
{((!isMobile && hovered) || isMobile) && (
<PlayButtonIcon
width="100%"
height="100%"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setCurrentTokenPlayingMedia(asset.tokenId)
}}
/>
)}
</PlaybackButton>
)}
</>
)
}
/* -------- CARD DETAILS CONTAINER -------- */
interface CardDetailsContainerProps {
children: ReactNode
}
const StyledDetailsContainer = styled(Column)`
position: relative;
padding: 12px 12px 0px;
justify-content: space-between;
transition: ${({ theme }) => `${theme.transition.duration.medium}`};
`
const DetailsContainer = ({ children }: CardDetailsContainerProps) => {
return <StyledDetailsContainer>{children}</StyledDetailsContainer>
}
const StyledInfoContainer = styled.div`
overflow: hidden;
width: 100%;
`
const InfoContainer = ({ children }: { children: ReactNode }) => {
return <StyledInfoContainer>{children}</StyledInfoContainer>
}
const TruncatedTextRow = styled(ThemedText.BodySmall)`
display: flex;
padding: 2px;
white-space: pre;
text-overflow: ellipsis;
display: block;
overflow: hidden;
`
const AssetNameRow = styled(TruncatedTextRow)`
color: ${({ theme }) => theme.textPrimary};
font-size: 16px !important;
font-weight: 400;
`
interface ProfileNftDetailsProps {
asset: WalletAsset
hideDetails: boolean
}
const PrimaryRowContainer = styled.div`
overflow: hidden;
width: 100%;
flex-wrap: nowrap;
`
const FloorPriceRow = styled(TruncatedTextRow)`
font-size: 16px;
font-weight: 600;
line-height: 20px;
`
const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
const assetName = () => {
if (!asset.name && !asset.tokenId) return
return asset.name ? asset.name : `#${asset.tokenId}`
}
const shouldShowUserListedPrice = !asset.notForSale && asset.asset_contract.tokenType !== NftStandard.Erc1155
return (
<PrimaryRowContainer>
<PrimaryRow>
<PrimaryDetails>
<TruncatedTextRow color="textSecondary">
{!!asset.asset_contract.name && <span>{asset.asset_contract.name}</span>}
</TruncatedTextRow>
{asset.collectionIsVerified && <VerifiedIcon height="18px" width="18px" />}
</PrimaryDetails>
{!hideDetails && <DetailsLink />}
</PrimaryRow>
<Row>
<AssetNameRow>{assetName()}</AssetNameRow>
{asset.susFlag && <Suspicious />}
</Row>
<FloorPriceRow>
{shouldShowUserListedPrice && asset.floor_sell_order_price
? `${floorFormatter(asset.floor_sell_order_price)} ETH`
: ' '}
</FloorPriceRow>
</PrimaryRowContainer>
)
}
const StyledPrimaryRow = styled(Row)`
gap: 8px;
justify-content: space-between;
`
const PrimaryRow = ({ children }: { children: ReactNode }) => <StyledPrimaryRow>{children}</StyledPrimaryRow>
const StyledPrimaryDetails = styled(Row)`
justify-items: center;
overflow: hidden;
white-space: nowrap;
`
const PrimaryDetails = ({ children }: { children: ReactNode }) => (
<StyledPrimaryDetails>{children}</StyledPrimaryDetails>
)
const PrimaryInfoContainer = styled.div`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 400;
font-size: 16px;
line-height: 24px;
`
const PrimaryInfo = ({ children }: { children: ReactNode }) => {
return <PrimaryInfoContainer>{children}</PrimaryInfoContainer>
}
const StyledSecondaryRow = styled(Row)`
height: 20px;
justify-content: space-between;
margin-top: 6px;
`
const SecondaryRow = ({ children }: { children: ReactNode }) => <StyledSecondaryRow>{children}</StyledSecondaryRow>
const StyledSecondaryDetails = styled(Row)`
overflow: hidden;
white-space: nowrap;
`
const SecondaryDetails = ({ children }: { children: ReactNode }) => (
<StyledSecondaryDetails>{children}</StyledSecondaryDetails>
)
const SecondaryInfoContainer = styled.div`
color: ${({ theme }) => theme.textPrimary};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 20px;
`
const SecondaryInfo = ({ children }: { children: ReactNode }) => {
return <SecondaryInfoContainer>{children}</SecondaryInfoContainer>
}
const TertiaryInfoContainer = styled.div`
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const TertiaryInfo = ({ children }: { children: ReactNode }) => {
return <TertiaryInfoContainer>{children}</TertiaryInfoContainer>
}
interface Erc1155ControlsInterface {
quantity: string
}
const Erc1155Controls = ({ quantity }: Erc1155ControlsInterface) => {
const { addAssetToBag, removeAssetFromBag } = useCardContext()
return (
<Erc1155ControlsRow>
<Erc1155ControlsContainer>
<Erc1155ControlsInput
onClick={(e: MouseEvent) => {
e.stopPropagation()
removeAssetFromBag()
}}
>
<MinusIconLarge width="24px" height="24px" />
</Erc1155ControlsInput>
<Erc1155ControlsDisplay>{quantity}</Erc1155ControlsDisplay>
<Erc1155ControlsInput
onClick={(e: MouseEvent) => {
e.stopPropagation()
addAssetToBag()
}}
>
<PlusIconLarge width="24px" height="24px" />
</Erc1155ControlsInput>
</Erc1155ControlsContainer>
</Erc1155ControlsRow>
)
}
const StyledMarketplaceIcon = styled.img`
display: inline-block;
width: 16px;
height: 16px;
border-radius: 4px;
flex-shrink: 0;
margin-left: 8px;
vertical-align: top;
`
const MarketplaceIcon = ({ marketplace }: { marketplace: string }) => {
return <StyledMarketplaceIcon alt={marketplace} src={`/nft/svgs/marketplaces/${marketplace}.svg`} />
}
const DetailsLink = () => {
const { asset } = useCardContext()
return (
<DetailsLinkContainer
href={baseHref(asset)}
onClick={(e: MouseEvent) => {
e.stopPropagation()
}}
>
<div data-testid="nft-details-link">Details</div>
</DetailsLinkContainer>
)
}
/* -------- RANKING CARD -------- */
interface RankingProps {
rarity: Rarity
provider: { url?: string; rank?: number }
rarityVerified: boolean
rarityLogo?: string
}
const RarityLogoContainer = styled(Row)`
margin-right: 4px;
width: 16px;
`
const RarityText = styled(ThemedText.BodySmall)`
display: flex;
`
const RarityInfo = styled(Row)`
height: 16px;
border-radius: 4px;
color: ${({ theme }) => theme.textPrimary};
background: ${({ theme }) => theme.backgroundInteractive};
font-size: 10px;
font-weight: 600;
padding: 0px 4px;
line-height: 12px;
letter-spacing: 0.04em;
backdrop-filter: blur(6px);
`
const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps) => {
const { asset } = useCardContext()
return (
<>
{provider.rank && (
<RankingContainer>
<MouseoverTooltip
text={
<Row>
<RarityLogoContainer>
<img src={rarityLogo} alt="cardLogo" width={16} height={16} />
</RarityLogoContainer>
<RarityText>
{rarityVerified
? `Verified by ${
('collectionName' in asset && asset.collectionName) ||
('asset_contract' in asset && asset.asset_contract?.name)
}`
: `Ranking by ${rarity.primaryProvider === 'Genie' ? fallbackProvider : rarity.primaryProvider}`}
</RarityText>
</Row>
}
placement="top"
>
<RarityInfo>
<Row padding="2px 0px">{putCommas(provider.rank)}</Row>
<Row>{rarityVerified ? <RarityVerifiedIcon /> : null}</Row>
</RarityInfo>
</MouseoverTooltip>
</RankingContainer>
)}
</>
)
}
const SUSPICIOUS_TEXT = t`Blocked on OpenSea`
const SuspiciousIconContainer = styled(Row)`
flex-shrink: 0;
margin-left: 4px;
`
const PoolIconContainer = styled(SuspiciousIconContainer)`
color: ${({ theme }) => theme.textSecondary};
`
const Suspicious = () => {
return (
<MouseoverTooltip text={<ThemedText.BodySmall>{SUSPICIOUS_TEXT}</ThemedText.BodySmall>} placement="top">
<SuspiciousIconContainer>
<SuspiciousIcon />
</SuspiciousIconContainer>
</MouseoverTooltip>
)
}
const Pool = () => {
return (
<MouseoverTooltip
text={
<ThemedText.BodySmall>
This NFT is part of a liquidity pool. Buying this will increase the price of the remaining pooled NFTs.
</ThemedText.BodySmall>
}
placement="top"
>
<PoolIconContainer>
<PoolIcon width="20" height="20" />
</PoolIconContainer>
</MouseoverTooltip>
)
}
const NoContentContainerBackground = styled.div<{ height?: number }>`
position: relative;
width: 100%;
height: ${({ height }) => (height ? `${height}px` : 'auto')};
padding-top: 100%;
background: ${({ theme }) =>
`linear-gradient(90deg, ${theme.backgroundSurface} 0%, ${theme.backgroundInteractive} 95.83%)`};
`
const NoContentText = styled(ThemedText.BodyPrimary)`
position: absolute;
text-align: center;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
color: ${colors.gray500};
`
const NoContentContainer = ({ height }: { height?: number }) => (
<>
<NoContentContainerBackground height={height}>
<NoContentText>
<Trans>Content not</Trans>
<br />
<Trans>available yet</Trans>
</NoContentText>
</NoContentContainerBackground>
</>
)
export {
Audio,
Container,
DetailsContainer,
DetailsLink,
Erc1155Controls,
Image,
ImageContainer,
InfoContainer,
MarketplaceIcon,
Pool,
PrimaryDetails,
PrimaryInfo,
PrimaryRow,
ProfileNftDetails,
Ranking,
SecondaryDetails,
SecondaryInfo,
SecondaryRow,
Suspicious,
SUSPICIOUS_TEXT,
TertiaryInfo,
useAssetMediaType,
useNotForSale,
Video,
}
...@@ -2,20 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -2,20 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics' import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfacePageName, NFTEventName } from '@uniswap/analytics-events' import { InterfacePageName, NFTEventName } from '@uniswap/analytics-events'
import { MouseoverTooltip } from 'components/Tooltip' import { NftCard, NftCardDisplayProps } from 'nft/components/card'
import Tooltip from 'components/Tooltip' import { Ranking as RankingContainer, Suspicious as SuspiciousContainer } from 'nft/components/card/icons'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box'
import { bodySmall } from 'nft/css/common.css'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { GenieAsset, isPooledMarket, UniformAspectRatio } from 'nft/types' import { GenieAsset, UniformAspectRatio } from 'nft/types'
import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils' import { formatWeiToDecimal } from 'nft/utils'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useMemo } from 'react'
import styled from 'styled-components/macro'
import { useAssetMediaType, useNotForSale } from './Card'
import { AssetMediaType } from './Card'
import * as Card from './Card'
interface CollectionAssetProps { interface CollectionAssetProps {
asset: GenieAsset asset: GenieAsset
...@@ -29,25 +21,11 @@ interface CollectionAssetProps { ...@@ -29,25 +21,11 @@ interface CollectionAssetProps {
setRenderedHeight: (renderedHeight: number | undefined) => void setRenderedHeight: (renderedHeight: number | undefined) => void
} }
const TOOLTIP_TIMEOUT = 2000
const StyledContainer = styled.div`
position: absolute;
bottom: 12px;
left: 0px;
display: flex;
justify-content: center;
width: 100%;
z-index: 2;
pointer-events: none;
`
export const CollectionAsset = ({ export const CollectionAsset = ({
asset, asset,
isMobile, isMobile,
mediaShouldBePlaying, mediaShouldBePlaying,
setCurrentTokenPlayingMedia, setCurrentTokenPlayingMedia,
rarityVerified,
uniformAspectRatio, uniformAspectRatio,
setUniformAspectRatio, setUniformAspectRatio,
renderedHeight, renderedHeight,
...@@ -56,7 +34,6 @@ export const CollectionAsset = ({ ...@@ -56,7 +34,6 @@ export const CollectionAsset = ({
const bagManuallyClosed = useBag((state) => state.bagManuallyClosed) const bagManuallyClosed = useBag((state) => state.bagManuallyClosed)
const addAssetsToBag = useBag((state) => state.addAssetsToBag) const addAssetsToBag = useBag((state) => state.addAssetsToBag)
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag) const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
const usedSweep = useBag((state) => state.usedSweep)
const itemsInBag = useBag((state) => state.itemsInBag) const itemsInBag = useBag((state) => state.itemsInBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
const setBagExpanded = useBag((state) => state.setBagExpanded) const setBagExpanded = useBag((state) => state.setBagExpanded)
...@@ -73,21 +50,8 @@ export const CollectionAsset = ({ ...@@ -73,21 +50,8 @@ export const CollectionAsset = ({
} }
}, [asset, itemsInBag]) }, [asset, itemsInBag])
const [showTooltip, setShowTooltip] = useState(false) const notForSale = asset.notForSale || BigNumber.from(asset.priceInfo ? asset.priceInfo.ETHPrice : 0).lt(0)
const isSelectedRef = useRef(isSelected) const provider = asset?.rarity?.providers ? asset.rarity.providers[0] : undefined
const notForSale = useNotForSale(asset)
const assetMediaType = useAssetMediaType(asset)
const { provider, rarityLogo } = useMemo(() => {
return {
provider: asset?.rarity?.providers?.find(
({ provider: _provider }) => _provider === asset.rarity?.primaryProvider
),
rarityLogo: rarityProviderLogo[asset.rarity?.primaryProvider ?? 0] ?? '',
}
}, [asset])
const handleAddAssetToBag = useCallback(() => { const handleAddAssetToBag = useCallback(() => {
if (BigNumber.from(asset.priceInfo?.ETHPrice ?? 0).gt(0)) { if (BigNumber.from(asset.priceInfo?.ETHPrice ?? 0).gt(0)) {
addAssetsToBag([asset]) addAssetsToBag([asset])
...@@ -103,122 +67,37 @@ export const CollectionAsset = ({ ...@@ -103,122 +67,37 @@ export const CollectionAsset = ({
} }
}, [addAssetsToBag, asset, bagExpanded, bagManuallyClosed, isMobile, setBagExpanded, trace]) }, [addAssetsToBag, asset, bagExpanded, bagManuallyClosed, isMobile, setBagExpanded, trace])
useEffect(() => {
if (isSelected !== isSelectedRef.current && !usedSweep) {
setShowTooltip(true)
isSelectedRef.current = isSelected
const tooltipTimer = setTimeout(() => {
setShowTooltip(false)
}, TOOLTIP_TIMEOUT)
return () => {
clearTimeout(tooltipTimer)
}
}
isSelectedRef.current = isSelected
return undefined
}, [isSelected, isSelectedRef, usedSweep])
const handleRemoveAssetFromBag = useCallback(() => { const handleRemoveAssetFromBag = useCallback(() => {
removeAssetsFromBag([asset]) removeAssetsFromBag([asset])
}, [asset, removeAssetsFromBag]) }, [asset, removeAssetsFromBag])
const display: NftCardDisplayProps = useMemo(() => {
return {
primaryInfo: asset.name ? asset.name : `#${asset.tokenId}`,
primaryInfoIcon: asset.susFlag ? <SuspiciousContainer /> : null,
primaryInfoRight: asset.rarity && provider ? <RankingContainer provider={provider} /> : null,
secondaryInfo: notForSale ? '' : `${formatWeiToDecimal(asset.priceInfo.ETHPrice, true)} ETH`,
selectedInfo: <Trans>Remove from bag</Trans>,
notSelectedInfo: <Trans>Add to bag</Trans>,
disabledInfo: <Trans>Not listed</Trans>,
}
}, [asset.name, asset.priceInfo.ETHPrice, asset.rarity, asset.susFlag, asset.tokenId, notForSale, provider])
return ( return (
<Card.Container <NftCard
asset={asset} asset={asset}
selected={isSelected} display={display}
addAssetToBag={handleAddAssetToBag} isSelected={isSelected}
removeAssetFromBag={handleRemoveAssetFromBag} isDisabled={Boolean(asset.notForSale)}
> selectAsset={handleAddAssetToBag}
<Card.ImageContainer isDisabled={asset.notForSale}> unselectAsset={handleRemoveAssetFromBag}
<StyledContainer data-testid="nft-collection-asset"> mediaShouldBePlaying={mediaShouldBePlaying}
<Tooltip uniformAspectRatio={uniformAspectRatio}
text={ setUniformAspectRatio={setUniformAspectRatio}
<Box as="span" className={bodySmall} color="textPrimary"> renderedHeight={renderedHeight}
{isSelected ? <Trans>Added to bag</Trans> : <Trans>Removed from bag</Trans>} setRenderedHeight={setRenderedHeight}
</Box> setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
} testId="nft-collection-asset"
show={showTooltip} />
style={{ display: 'block' }}
offsetX={0}
offsetY={0}
hideArrow={true}
placement="bottom"
showInline
/>
</StyledContainer>
{asset.rarity && provider && (
<Card.Ranking
rarity={asset.rarity}
provider={provider}
rarityVerified={!!rarityVerified}
rarityLogo={rarityLogo}
/>
)}
<MouseoverTooltip
text={
<Box as="span" className={bodySmall} color="textPrimary">
<Trans>This item is not for sale</Trans>
</Box>
}
placement="bottom"
offsetX={0}
offsetY={-50}
style={{ display: 'block' }}
hideArrow={true}
disableHover={!asset.notForSale}
timeout={isMobile ? TOOLTIP_TIMEOUT : undefined}
>
{assetMediaType === AssetMediaType.Image ? (
<Card.Image
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
) : assetMediaType === AssetMediaType.Video ? (
<Card.Video
shouldPlay={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
) : (
<Card.Audio
shouldPlay={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
uniformAspectRatio={uniformAspectRatio}
setUniformAspectRatio={setUniformAspectRatio}
renderedHeight={renderedHeight}
setRenderedHeight={setRenderedHeight}
/>
)}
</MouseoverTooltip>
</Card.ImageContainer>
<Card.DetailsContainer>
<Card.InfoContainer>
<Card.PrimaryRow>
<Card.PrimaryDetails>
<Card.PrimaryInfo>{asset.name ? asset.name : `#${asset.tokenId}`}</Card.PrimaryInfo>
{asset.susFlag && <Card.Suspicious />}
</Card.PrimaryDetails>
<Card.DetailsLink />
</Card.PrimaryRow>
<Card.SecondaryRow>
<Card.SecondaryDetails>
<Card.SecondaryInfo>
{notForSale ? '' : `${formatWeiToDecimal(asset.priceInfo.ETHPrice, true)} ETH`}
</Card.SecondaryInfo>
{isPooledMarket(asset.marketplace) && <Card.Pool />}
</Card.SecondaryDetails>
{asset.tokenType !== NftStandard.Erc1155 && asset.marketplace && (
<Card.MarketplaceIcon marketplace={asset.marketplace} />
)}
</Card.SecondaryRow>
</Card.InfoContainer>
</Card.DetailsContainer>
</Card.Container>
) )
} }
...@@ -67,15 +67,6 @@ export const VerifiedIcon = (props: SVGProps) => { ...@@ -67,15 +67,6 @@ export const VerifiedIcon = (props: SVGProps) => {
) )
} }
export const PoolIcon = (props: SVGProps) => (
<svg {...props} fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.13184 15.1834C3.13184 15.5098 3.34528 15.7233 3.67174 15.7233H16.2464C16.5854 15.7233 16.8679 15.4596 16.8679 15.108C16.8679 14.7565 16.5854 14.4928 16.2464 14.4928H4.53809C4.40625 14.4928 4.3623 14.4489 4.3623 14.317V10.9019L7.02414 9.35749C7.23758 9.55838 7.52009 9.68394 7.84026 9.68394C8.11021 9.68394 8.36761 9.58977 8.5685 9.43283L9.74874 10.4373C9.7048 10.5628 9.67969 10.701 9.67969 10.8391C9.67969 11.5045 10.2133 12.0382 10.8725 12.0382C11.5317 12.0382 12.0716 11.5045 12.0716 10.8391C12.0716 10.7889 12.0653 10.7324 12.059 10.6821L14.9343 8.96198C15.1226 9.09382 15.3549 9.16915 15.5998 9.16915C16.2589 9.16915 16.7988 8.62925 16.7988 7.97008C16.7988 7.3109 16.2589 6.771 15.5998 6.771C14.9406 6.771 14.4007 7.3109 14.4007 7.97008C14.4007 8.05169 14.4132 8.1333 14.4258 8.20864L11.607 9.89739C11.4061 9.74044 11.1487 9.64627 10.8725 9.64627C10.6779 9.64627 10.4895 9.69022 10.32 9.77811L9.02051 8.6732C9.02679 8.61042 9.03306 8.54764 9.03306 8.48486C9.03306 7.82568 8.49944 7.28578 7.84026 7.28578C7.17481 7.28578 6.64118 7.82568 6.64118 8.48486C6.64118 8.5037 6.64118 8.52253 6.64118 8.54136L4.3623 9.85972V4.44817C4.3623 4.10289 4.09863 3.82666 3.74707 3.82666C3.39551 3.82666 3.13184 4.10289 3.13184 4.44817V15.1834Z"
fill="currentColor"
/>
</svg>
)
export const XMarkIcon = (props: SVGProps) => ( export const XMarkIcon = (props: SVGProps) => (
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" {...props}> <svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" {...props}>
<path <path
...@@ -189,26 +180,6 @@ export const CrossIcon = (props: SVGProps) => ( ...@@ -189,26 +180,6 @@ export const CrossIcon = (props: SVGProps) => (
</svg> </svg>
) )
export const PlayButtonIcon = (props: SVGProps) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle opacity="0.4" cx="12" cy="12" r="12" fill="#70757A" />
<path
d="M8.67188 17.2653C8.93555 17.2653 9.16783 17.1712 9.45661 17.0017L16.8708 12.7139C17.4107 12.4 17.6367 12.1551 17.6367 11.7596C17.6367 11.3641 17.4107 11.1256 16.8708 10.8054L9.45661 6.51758C9.16783 6.34807 8.93555 6.25391 8.67188 6.25391C8.15709 6.25391 7.79297 6.64941 7.79297 7.27093V16.2483C7.79297 16.8761 8.15709 17.2653 8.67188 17.2653Z"
fill="white"
/>
</svg>
)
export const PauseButtonIcon = (props: SVGProps) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle opacity="0.4" cx="12" cy="12" r="12" fill="#70757A" />
<path
d="M8.88067 16.9757H10.356C10.9524 16.9757 11.26 16.6681 11.26 16.0717V7.43331C11.26 6.82436 10.9524 6.53557 10.356 6.5293H8.88067C8.28427 6.5293 7.97666 6.83691 7.97666 7.43331V16.0717C7.97038 16.6681 8.278 16.9757 8.88067 16.9757ZM13.6519 16.9757H15.1209C15.7173 16.9757 16.0249 16.6681 16.0249 16.0717V7.43331C16.0249 6.82436 15.7173 6.5293 15.1209 6.5293H13.6519C13.0492 6.5293 12.7479 6.83691 12.7479 7.43331V16.0717C12.7479 16.6681 13.0492 16.9757 13.6519 16.9757Z"
fill="white"
/>
</svg>
)
export const ArrowsIcon = () => ( export const ArrowsIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
...@@ -493,19 +464,6 @@ export const GovernanceIcon = (props: SVGProps) => ( ...@@ -493,19 +464,6 @@ export const GovernanceIcon = (props: SVGProps) => (
</svg> </svg>
) )
export const MinusIconLarge = (props: SVGProps) => (
<svg {...props} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
export const PlusIconLarge = (props: SVGProps) => (
<svg {...props} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 12H19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
export const ChevronLeftIcon = (props: SVGProps) => ( export const ChevronLeftIcon = (props: SVGProps) => (
<svg width="8" height="16" viewBox="0 0 8 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> <svg width="8" height="16" viewBox="0 0 8 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M7 1L1 7L7 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M7 1L1 7L7 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
...@@ -742,3 +700,106 @@ export const ListingModalWindowClosed = (props: SVGProps) => ( ...@@ -742,3 +700,106 @@ export const ListingModalWindowClosed = (props: SVGProps) => (
<circle cx="8" cy="8" r="7" stroke="#333D59" strokeWidth="2" /> <circle cx="8" cy="8" r="7" stroke="#333D59" strokeWidth="2" />
</svg> </svg>
) )
export const OpenSeaMarketplaceIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24 16.06V14.806C24 14.69 23.888 14.606 23.778 14.638L17.856 16.35C17.824 16.358 17.796 16.376 17.774 16.4C17.1396 17.1008 16.6005 17.571 16.4578 17.6955L16.448 17.704C16.08 18.016 15.624 18.186 15.144 18.186H14.093C13.4894 18.186 13 17.6966 13 17.093C13 16.4894 13.4894 16 14.093 16H14.704C14.748 16 14.79 15.984 14.822 15.956L15.042 15.754C15.136 15.668 15.248 15.564 15.382 15.43C15.3933 15.4187 15.4047 15.4073 15.4163 15.3958C15.4868 15.3256 15.5621 15.2505 15.636 15.168C15.724 15.082 15.81 14.986 15.89 14.892C16.024 14.748 16.152 14.598 16.286 14.44C16.382 14.336 16.47 14.218 16.556 14.1C16.652 13.988 16.746 13.862 16.834 13.742C16.8666 13.6941 16.9013 13.6457 16.9367 13.5963C16.9708 13.5486 17.0057 13.5 17.04 13.45C17.104 13.354 17.168 13.252 17.222 13.156C17.39 12.896 17.532 12.618 17.652 12.34C17.707 12.2211 17.751 12.096 17.7937 11.9743C17.7992 11.9588 17.8046 11.9434 17.81 11.928C17.858 11.786 17.898 11.652 17.928 11.51C18 11.176 18.016 10.844 17.984 10.512C17.9764 10.4136 17.9688 10.317 17.9477 10.2255C17.9454 10.2152 17.944 10.2046 17.944 10.194C17.936 10.126 17.92 10.05 17.898 9.98001C17.826 9.65601 17.714 9.332 17.572 9.014C17.524 8.89599 17.468 8.77599 17.414 8.66598C17.286 8.42802 17.152 8.19001 17 7.96C16.9695 7.91136 16.9357 7.86209 16.902 7.81289C16.8762 7.77511 16.8503 7.73737 16.826 7.70002C16.7297 7.5514 16.6213 7.40815 16.5163 7.26916C16.4926 7.2379 16.4692 7.20686 16.446 7.17602C16.384 7.09458 16.3161 7.01314 16.2477 6.93116C16.2103 6.88629 16.1728 6.84127 16.136 6.79599C16.032 6.66998 15.93 6.54998 15.826 6.43201C15.454 6.01201 15.064 5.63198 14.716 5.30802C14.652 5.24399 14.582 5.18001 14.51 5.11798C14.24 4.87201 13.994 4.65801 13.788 4.49198C13.726 4.44425 13.6703 4.39722 13.6185 4.35345C13.5835 4.32387 13.5503 4.29579 13.518 4.26998C13.4545 4.22272 13.3996 4.18086 13.3537 4.14585C13.3258 4.12459 13.3012 4.10585 13.28 4.08998C13.264 4.078 13.246 4.06999 13.228 4.06398C13.0932 4.02615 13 3.90323 13 3.76322V2.11201C13 1.80401 12.876 1.52802 12.678 1.326C12.48 1.12398 12.204 1 11.9 1C11.292 1 10.8 1.498 10.8 2.11201V3.2656C10.8 3.32504 10.7432 3.36806 10.686 3.35198L10.376 3.26399L10.102 3.18821C10.1004 3.18775 10.0987 3.18716 10.097 3.18657C10.0934 3.18529 10.0898 3.18401 10.086 3.18401C10.082 3.18401 10.078 3.18348 10.0742 3.18244L7.93999 2.60399C7.84603 2.578 7.766 2.68 7.81402 2.766L8.15602 3.39801C8.17546 3.44665 8.2001 3.4953 8.22543 3.54529C8.24175 3.57751 8.25835 3.61028 8.27403 3.64398C8.33001 3.75602 8.38603 3.87399 8.44002 3.992C8.48799 4.09599 8.53601 4.198 8.59203 4.30999C8.61561 4.36275 8.63965 4.41614 8.66403 4.4703C8.75336 4.6687 8.8473 4.87732 8.94 5.10202L8.94079 5.10389C9.02051 5.29326 9.10024 5.48265 9.17001 5.68C9.36199 6.178 9.54402 6.70999 9.70201 7.25601C9.7413 7.37805 9.7727 7.49617 9.80452 7.61587C9.81806 7.66682 9.83168 7.71806 9.84601 7.77001L9.86799 7.866C9.93201 8.12001 9.98799 8.372 10.028 8.62601C10.06 8.8 10.09 8.96598 10.106 9.134L10.106 9.13407C10.13 9.32404 10.154 9.51401 10.162 9.70398C10.178 9.87801 10.186 10.06 10.186 10.234C10.186 10.678 10.146 11.106 10.052 11.51C10.0462 11.5316 10.0403 11.5534 10.0344 11.5755C10.008 11.6739 9.98068 11.776 9.94802 11.874C9.91838 11.9792 9.87997 12.0844 9.84008 12.1937C9.82613 12.2319 9.812 12.2706 9.798 12.31C9.7957 12.3162 9.7934 12.3224 9.7911 12.3286C9.76138 12.4087 9.73114 12.4902 9.694 12.57C9.49601 13.046 9.24999 13.52 8.99602 13.964C8.624 14.622 8.25002 15.2 7.988 15.572C7.97207 15.5959 7.95652 15.6186 7.94154 15.6405C7.92269 15.6681 7.90474 15.6944 7.88803 15.72C7.80601 15.836 7.89002 16 8.032 16H9.707C10.3106 16 10.8 16.4894 10.8 17.093C10.8 17.6966 10.3106 18.186 9.707 18.186H8.00003C7.24802 18.186 6.55203 17.76 6.21599 17.078C6.042 16.736 5.974 16.36 6.01402 15.992C6.02401 15.882 5.94199 15.778 5.82999 15.778H0.17403C0.0779956 15.778 0 15.856 0 15.952V16.068C0 19.676 2.914 22.6 6.51002 22.6H16.656C18.5579 22.6 19.6378 20.8669 20.6993 19.1634C20.9951 18.6886 21.2896 18.216 21.6 17.784C22.158 17.008 23.5 16.392 23.892 16.224C23.956 16.196 24 16.132 24 16.06ZM1.51195 13.202L1.42794 13.334C1.35397 13.448 1.43594 13.6 1.57593 13.6H6.78395C6.84196 13.6 6.89594 13.572 6.92796 13.524C6.99596 13.4201 7.05994 13.312 7.11795 13.202C7.56797 12.446 7.96794 11.628 8.11394 11.024C8.45594 9.55604 7.72595 7.19805 6.87994 5.30201C6.82396 5.17604 6.64993 5.16401 6.57596 5.28004L1.51195 13.202Z"
fill="white"
/>
</svg>
)
export const CollectionSelectedAssetIcon = (props: SVGProps) => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" {...props}>
<path
d="M17.5 9.16667V6.52778C17.5 5.68401 16.7538 5 15.8333 5H4.16667C3.24619 5 2.5 5.68401 2.5 6.52778V17.2222C2.5 18.066 3.24619 18.75 4.16667 18.75H11.25"
stroke="#F5F6FC"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.5 14.6667L14.4048 16.6667L19.1667 11.6667"
stroke="#F5F6FC"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6.66671 5L6.66671 4.16666C6.66671 3.28261 7.0179 2.43476 7.64302 1.80964C8.26814 1.18452 9.11599 0.833333 10 0.833333C10.8841 0.833333 11.7319 1.18452 12.3571 1.80964C12.9822 2.43476 13.3334 3.28261 13.3334 4.16667L13.3334 5"
stroke="#F5F6FC"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
export const Nft20Icon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.1797 2C18.6608 1.99964 17.2055 2.60684 16.1403 3.68523V2H15.3199C13.8003 1.99901 12.3441 2.60626 11.2784 3.68523V2H10.4603C7.32315 2.00245 4.78057 4.53488 4.77811 7.65966V16.3545H4.51732C4.29849 16.3542 4.08024 16.3773 3.86643 16.4234C3.47831 16.5022 3.10944 16.6561 2.78085 16.8763C2.54716 16.3376 2.0146 15.9887 1.42549 15.9881C1.32123 15.9869 1.21723 15.998 1.11563 16.0214C0.327205 16.2173 -0.15257 17.0126 0.0440797 17.7979C0.202958 18.4325 0.764814 18.8854 1.42105 18.9079H1.46342C1.39473 19.2972 1.40305 19.6962 1.48796 20.0824C1.80032 21.5009 3.05723 22.515 4.51518 22.5248C4.95221 22.5247 5.38438 22.4332 5.78359 22.2562C6.32672 22.7363 7.02799 23.001 7.75417 23H18.0085C19.6557 23.0049 20.9951 21.6788 21 20.0381V20.0314V2H20.1797ZM20.1797 20.0314C20.176 21.2182 19.211 22.1793 18.0195 22.183H7.75417C7.02124 22.1819 6.33891 21.8107 5.94184 21.1971C5.66604 21.4217 5.34054 21.5777 4.99223 21.6523C4.83388 21.6855 4.67254 21.7026 4.51073 21.7034C3.43635 21.7023 2.50819 20.9554 2.28156 19.9093C2.21779 19.6077 2.21779 19.2962 2.28156 18.9946C2.29731 18.9073 2.31892 18.8213 2.34617 18.737C2.35062 18.6819 2.34607 18.6264 2.33278 18.5727C2.27026 18.273 2.00466 18.0586 1.69742 18.0598C1.65102 18.06 1.60483 18.0645 1.55921 18.0731C1.51396 18.0838 1.4675 18.089 1.42099 18.0886C1.06149 18.0888 0.769941 17.7986 0.769836 17.4405C0.769732 17.1328 0.986836 16.8673 1.28947 16.8052C1.33483 16.7953 1.38129 16.7909 1.42769 16.792C1.73362 16.7923 1.99802 17.0049 2.06305 17.3027C2.12556 17.6023 2.39116 17.8167 2.6984 17.8155C2.74496 17.8159 2.79131 17.8106 2.83662 17.8C2.89113 17.7895 2.94381 17.7708 2.99267 17.7445C3.0589 17.6862 3.12811 17.6314 3.19999 17.5801C3.45231 17.401 3.73925 17.2762 4.04262 17.2138C4.19925 17.1794 4.35923 17.1623 4.51968 17.1627C4.89739 17.162 5.26913 17.2559 5.60086 17.4358V7.66633C5.59589 4.99306 7.76766 2.82204 10.4516 2.81709H10.4605V7.65503C10.4617 4.98388 12.6355 2.81834 15.3179 2.81709H15.3201V7.65504C15.3214 4.98389 17.4952 2.81834 20.1776 2.81709H20.1798L20.1797 20.0314ZM18.5612 15.3443V19.3409C18.5576 20.0184 18.0064 20.5663 17.3263 20.5687H16.9406V19.7628C16.9171 19.317 16.5353 18.9746 16.0878 18.998C15.6731 19.0197 15.3417 19.3497 15.3199 19.7628V20.571H10.4603V19.7628C10.4579 19.3181 10.0953 18.959 9.64886 18.959C9.2032 18.9578 8.84092 19.3167 8.83966 19.7606V19.7628V20.571H8.45405C7.7725 20.5697 7.22033 20.0197 7.21907 19.3409V15.3443C7.22033 14.6654 7.7725 14.1154 8.45405 14.1142H8.83966V14.9202C8.83966 15.3657 9.20157 15.7272 9.64886 15.7285C10.0965 15.7272 10.4591 15.3661 10.4603 14.9202V14.1142H15.3199V14.9202C15.2965 15.366 15.6402 15.7463 16.0878 15.7697C16.5353 15.7931 16.9171 15.4507 16.9406 15.0049C16.9421 14.9767 16.9421 14.9484 16.9406 14.9202V14.1142H17.3263C18.0073 14.1166 18.5588 14.6659 18.5612 15.3443ZM12.1764 12.4616C12.1764 12.4616 11.7194 11.9677 11.6458 11.7557C11.5454 11.4663 11.6201 10.5311 11.6201 10.5311C11.6188 10.276 11.5133 10.1986 11.2572 10.1998H10.3578C10.1029 10.1998 9.97363 10.3285 9.97363 10.5824V11.6915C9.97363 12.0851 10.8363 12.4616 12.1764 12.4616ZM15.4274 10.531C15.4286 10.276 15.5341 10.1986 15.7902 10.1997H16.6897C16.9445 10.1997 17.0738 10.3285 17.0738 10.5823V11.6915C17.0738 12.0851 16.2111 12.4615 14.871 12.4615C14.871 12.4615 15.328 11.9676 15.4016 11.7556C15.502 11.4663 15.4274 10.531 15.4274 10.531ZM8.21559 4.38903C8.63871 4.05164 9.10875 3.7772 9.61102 3.57419C9.03525 3.5955 8.47512 3.7662 7.98603 4.06931C7.48842 4.37684 7.07127 4.79765 6.76889 5.29714C6.46039 5.79429 6.27902 6.35923 6.24057 6.94241C6.22832 7.08576 6.22832 7.22983 6.24057 7.37318C6.24427 7.49333 6.25569 7.60731 6.26714 7.72155L6.27399 7.7906L6.2902 7.91729L6.2902 7.9173C6.31983 8.15043 6.34945 8.38349 6.39662 8.61659C6.44525 8.36635 6.47544 8.11619 6.50563 7.86601L6.51474 7.7906C6.51977 7.74621 6.5241 7.70159 6.52841 7.65719L6.52841 7.65718C6.53747 7.56388 6.54643 7.47154 6.56157 7.38427C6.56575 7.35291 6.56947 7.32154 6.57315 7.29039C6.57729 7.25541 6.5814 7.22073 6.5861 7.18669C6.59119 7.14987 6.59921 7.11302 6.60722 7.07617L6.60722 7.07616L6.60722 7.07615C6.61322 7.0486 6.61921 7.02105 6.62398 6.99353C6.72766 6.48387 6.92008 5.99615 7.19242 5.55252C7.46524 5.11003 7.8112 4.71673 8.21559 4.38903ZM14.4706 3.57419C13.9924 3.77173 13.5427 4.03153 13.1331 4.34683C12.7415 4.65248 12.402 5.0191 12.1278 5.43257C11.8553 5.8463 11.6486 6.29936 11.5148 6.77588C11.4959 6.8404 11.4827 6.90553 11.4695 6.97063L11.4695 6.97067C11.4576 7.02879 11.4458 7.08688 11.43 7.14448C11.4093 7.22036 11.3997 7.2988 11.39 7.37768C11.3842 7.42576 11.3783 7.47401 11.3698 7.52194C11.3386 7.78393 11.3052 8.04373 11.2517 8.30797C11.2001 8.06325 11.1695 7.82238 11.1387 7.57998L11.1387 7.57994L11.1313 7.52194C11.1313 7.44632 11.1256 7.36927 11.1198 7.29161L11.1198 7.29159L11.1198 7.29156C11.1154 7.23241 11.1109 7.17291 11.109 7.11342C11.1045 6.97576 11.1224 6.84033 11.1402 6.70491C11.2114 6.14982 11.4088 5.61817 11.7176 5.15067C12.0234 4.68604 12.4342 4.29899 12.9169 4.02053C13.3875 3.74072 13.9228 3.58701 14.4706 3.57419ZM17.995 4.34683C18.4047 4.03168 18.8545 3.77188 19.3326 3.57419C18.7861 3.59217 18.2534 3.74968 17.7855 4.03158C17.3018 4.30566 16.8895 4.68891 16.5817 5.15067C16.2722 5.61802 16.074 6.14961 16.0021 6.70491C15.9843 6.84033 15.9687 6.97576 15.9731 7.11342C15.975 7.17161 15.9789 7.22981 15.9827 7.28768C15.988 7.36667 15.9932 7.44504 15.9932 7.52194C15.9985 7.56064 16.0036 7.59929 16.0088 7.63791C16.0386 7.8608 16.0683 8.08276 16.1158 8.30797C16.1649 8.0657 16.1952 7.82716 16.2256 7.58724C16.2284 7.56549 16.2312 7.54372 16.2339 7.52194C16.2406 7.48004 16.246 7.4379 16.2514 7.39584C16.2624 7.31085 16.2733 7.22618 16.2941 7.14448C16.3071 7.09389 16.3173 7.04293 16.3276 6.9919C16.3422 6.91973 16.3567 6.84743 16.3789 6.77588C16.5127 6.29936 16.7194 5.8463 16.9919 5.43257C17.266 5.01968 17.6047 4.65316 17.995 4.34683Z"
fill="white"
/>
</svg>
)
export const NftXIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 0L2.80713 9.19286L10.5399 16.6017C9.50042 17.3018 8.96753 18.0136 8.48384 18.6597L8.48313 18.6607L8.48293 18.6609C7.9305 19.4002 7.45337 20.0387 6.10491 20.7007C5.15896 21.1652 4.48456 21.2978 3.84753 21.3305C4.34999 21.9968 5.19599 22.5819 6.27942 23.0318C6.53176 22.9339 6.79713 22.818 7.07742 22.6803C8.90264 21.7852 9.61871 20.8277 10.2506 19.9829L10.2514 19.9817C10.8039 19.2423 11.281 18.6039 12.6296 17.9417C13.9782 17.2795 14.7753 17.2918 15.698 17.306H15.698L15.6982 17.306C16.3164 17.3158 16.9831 17.3259 17.7963 17.1518C16.8046 16.619 15.5296 16.2159 14.0884 15.9998L21.1928 9.19286L12 0ZM7.37363 17.0155L7.37899 17.0083L7.37901 17.0083C7.57949 16.7399 7.76995 16.4849 7.99763 16.237C5.23472 16.9028 3.33203 18.2972 3.33203 19.9111C3.33203 19.9712 3.33529 20.0311 3.34043 20.0904C3.61146 19.9875 3.8974 19.8644 4.20117 19.7151C6.0262 18.8185 6.74209 17.8606 7.37363 17.0155ZM18.6096 18.0667C17.5547 18.0496 16.3591 18.0312 14.5339 18.928C12.7087 19.8247 11.9928 20.7825 11.3613 21.6278L11.3609 21.6284C10.8085 22.3673 10.3313 23.0057 8.98288 23.6678C8.92185 23.6978 8.86288 23.7261 8.80322 23.7535C9.74427 23.9195 10.6982 24.002 11.6537 24C11.8645 24 12.0731 23.9962 12.2796 23.9885C12.6172 23.6313 12.8796 23.2809 13.1282 22.9481C13.6805 22.2088 14.1578 21.5702 15.5064 20.9081C16.855 20.2461 17.6523 20.2584 18.5748 20.2726H18.575C18.9998 20.2793 19.4476 20.2862 19.95 20.2337C19.967 20.1272 19.9755 20.0195 19.9756 19.9116C19.9756 19.2494 19.6548 18.6242 19.0862 18.071C18.9683 18.0715 18.8504 18.0699 18.7306 18.0683H18.7304L18.6096 18.0667ZM19.5883 21.1462C18.8621 22.2725 17.1703 23.1878 14.9902 23.6572C15.5194 23.0755 16.2343 22.4722 17.4111 21.8937C18.2554 21.4786 18.9648 21.2601 19.5883 21.1462Z"
fill="white"
/>
</svg>
)
export const X2y2Icon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.528 4.70405C19.6893 2.90732 17.174 1.8 14.4 1.8C8.7667 1.8 4.2 6.3667 4.2 12C4.2 17.6333 8.7667 22.2 14.4 22.2C17.1739 22.2 19.6893 21.0927 21.528 19.296C19.3347 22.156 15.8827 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0C15.8827 0 19.3347 1.84402 21.528 4.70405ZM6.77759 17.8368C8.24862 19.2742 10.2609 20.16 12.48 20.16C16.9867 20.16 20.64 16.5067 20.64 12C20.64 7.49337 16.9867 3.84002 12.48 3.84002C10.2609 3.84002 8.24862 4.72587 6.77759 6.16324C8.53226 3.87524 11.2939 2.40002 14.4 2.40002C19.702 2.40002 24 6.69809 24 12C24 17.302 19.702 21.6 14.4 21.6C11.2939 21.6 8.53226 20.1248 6.77759 17.8368ZM12 19.2C15.9765 19.2 19.2001 15.9764 19.2001 12C19.2001 8.02353 15.9765 4.79999 12 4.79999C8.0236 4.79999 4.80005 8.02353 4.80005 12C4.80005 15.9764 8.0236 19.2 12 19.2ZM12 16.8C14.651 16.8 16.8 14.651 16.8 12C16.8 9.34902 14.651 7.19999 12 7.19999C9.34908 7.19999 7.20005 9.34902 7.20005 12C7.20005 14.651 9.34908 16.8 12 16.8Z"
fill="white"
/>
</svg>
)
export const SudoSwapIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24ZM4.37501 7.35706V7.33475V7.3125H5.05176H5.72851L5.92208 7.82812L6.11564 8.34375L6.51939 9.4375L6.92308 10.5312L7.18845 11.2647L7.45383 11.9982L7.10851 12.9522L6.76326 13.9062L6.24608 15.2969L5.72895 16.6875H5.05201H4.37501V16.6622V16.6369L5.12889 14.5841L5.88276 12.5312L5.97514 12.2656L6.06745 12L5.97501 11.7344L5.88258 11.4688L5.12883 9.41288L4.37501 7.35706ZM18.0691 7.84375L18.168 7.57812L18.267 7.3125H18.9527H19.6384L19.6096 7.39062L19.5808 7.46875L18.7554 9.73438L17.93 12L18.7554 14.2656L19.5808 16.5312L19.6096 16.6094L19.6384 16.6875H18.9527H18.267L18.168 16.4219L18.0691 16.1562L17.5731 14.8125L17.0773 13.4688L16.8114 12.7344L16.5455 12L16.8114 11.2656L17.0773 10.5312L17.5731 9.1875L18.0691 7.84375ZM10.779 9.492L10.9989 9.43669L11.2188 9.38131L12.2188 9.39819L13.2188 9.41506L13.5938 9.5595L13.9688 9.70387L14.2512 9.993L14.5336 10.2821L14.6106 10.5389L14.6875 10.7957V10.9291V11.0625H14.0999H13.5123L13.4463 10.8734L13.3804 10.6842L13.0496 10.5302L12.7188 10.3763L12.0303 10.3756L11.3418 10.375L11.124 10.4914L10.9063 10.6079L10.7871 10.8109L10.668 11.0138L10.672 11.2816L10.676 11.5492L10.7441 11.6765L10.8123 11.8038L11.0043 11.9406L11.1965 12.0774L12.2701 12.2621L13.3438 12.4468L13.7313 12.6314L14.1188 12.8159L14.3195 13.0166L14.5203 13.2174L14.6481 13.4993L14.7759 13.7812L14.776 14.3125L14.7761 14.8438L14.6353 15.1984L14.4946 15.553L14.2499 15.8052L14.0051 16.0573L13.5495 16.1999L13.0938 16.3424L12.0625 16.3431L11.0313 16.3438L10.6875 16.2258L10.3438 16.1079L10.09 15.9612L9.83614 15.8145L9.70239 15.6721L9.56864 15.5297L9.44551 15.2493L9.32239 14.9688L9.27926 14.7344L9.23608 14.5H9.88564H10.5351L10.627 14.7647L10.7188 15.0293L11 15.1866L11.2813 15.3438H12.0625H12.8438L13.0542 15.255L13.2646 15.1663L13.4448 14.9175L13.625 14.6687L13.6247 14.35L13.6244 14.0312L13.4841 13.865L13.3438 13.6987L12.875 13.5398L12.4063 13.3808L11.8241 13.2834L11.242 13.1861L10.8554 13.0576L10.4688 12.9291L10.2395 12.7795L10.0101 12.6299L9.85439 12.4243L9.69864 12.2188L9.59164 11.8483L9.4847 11.4777L9.51633 11.1295L9.54795 10.7812L9.62876 10.5118L9.70958 10.2423L9.95214 9.99238L10.1946 9.7425L10.4868 9.61725L10.779 9.492Z"
fill="white"
/>
</svg>
)
export const LooksRareIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.13513 3.08105L0 10.2211L12 22.2162L24 10.2211L16.8649 3.08105H7.13513ZM6.48649 7.62158C9.51816 4.5766 14.4818 4.57658 17.5135 7.62156L20.1081 10.2162L17.5135 12.8108C14.4818 15.8558 9.51816 15.8558 6.48649 12.8108L3.89189 10.2162L6.48649 7.62158ZM8.27026 10.2162C8.27026 12.2769 9.94096 13.946 12 13.946C14.059 13.946 15.7297 12.2769 15.7297 10.2162C15.7297 8.15553 14.059 6.48651 12 6.48651C9.94096 6.48651 8.27026 8.15553 8.27026 10.2162ZM12 11.8379C11.1048 11.8379 10.3784 11.1122 10.3784 10.2162C10.3784 9.32028 11.1048 8.59462 12 8.59462C12.8952 8.59462 13.6216 9.32028 13.6216 10.2162C13.6216 11.1122 12.8952 11.8379 12 11.8379Z"
fill="white"
/>
</svg>
)
export const LarvaLabsMarketplaceIcon = (props: SVGProps) => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 5H12V17H9V5ZM13 5H16V13H13V5ZM13 14V17H22V14H13ZM2 17V14H8V17H2Z"
fill="white"
/>
</svg>
)
...@@ -2,19 +2,12 @@ import { Trans } from '@lingui/macro' ...@@ -2,19 +2,12 @@ import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics' import { useTrace } from '@uniswap/analytics'
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events' import { NFTEventName } from '@uniswap/analytics-events'
import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks' import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box' import { NftCard, NftCardDisplayProps } from 'nft/components/card'
import * as Card from 'nft/components/collection/Card' import { VerifiedIcon } from 'nft/components/icons'
import { AssetMediaType } from 'nft/components/collection/Card'
import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks' import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
import { WalletAsset } from 'nft/types' import { WalletAsset } from 'nft/types'
import { useEffect, useMemo, useRef, useState } from 'react' import { useMemo } from 'react'
const TOOLTIP_TIMEOUT = 2000
interface ViewMyNftsAssetProps { interface ViewMyNftsAssetProps {
asset: WalletAsset asset: WalletAsset
...@@ -23,31 +16,6 @@ interface ViewMyNftsAssetProps { ...@@ -23,31 +16,6 @@ interface ViewMyNftsAssetProps {
hideDetails: boolean hideDetails: boolean
} }
const getNftDisplayComponent = (
assetMediaType: AssetMediaType,
mediaShouldBePlaying: boolean,
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
) => {
switch (assetMediaType) {
case AssetMediaType.Image:
return <Card.Image />
case AssetMediaType.Video:
return <Card.Video shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
case AssetMediaType.Audio:
return <Card.Audio shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
}
}
const getUnsupportedNftTextComponent = (asset: WalletAsset) => (
<Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}>
{asset.asset_contract.tokenType === NftStandard.Erc1155 ? (
<Trans>Selling ERC-1155s coming soon</Trans>
) : (
<Trans>Blocked from trading</Trans>
)}
</Box>
)
export const ViewMyNftsAsset = ({ export const ViewMyNftsAsset = ({
asset, asset,
mediaShouldBePlaying, mediaShouldBePlaying,
...@@ -67,8 +35,6 @@ export const ViewMyNftsAsset = ({ ...@@ -67,8 +35,6 @@ export const ViewMyNftsAsset = ({
) )
}, [asset, sellAssets]) }, [asset, sellAssets])
const [showTooltip, setShowTooltip] = useState(false)
const isSelectedRef = useRef(isSelected)
const trace = useTrace() const trace = useTrace()
const onCardClick = () => handleSelect(isSelected) const onCardClick = () => handleSelect(isSelected)
...@@ -93,65 +59,32 @@ export const ViewMyNftsAsset = ({ ...@@ -93,65 +59,32 @@ export const ViewMyNftsAsset = ({
toggleCart() toggleCart()
} }
useEffect(() => { const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
if (isSelected !== isSelectedRef.current) {
setShowTooltip(true)
isSelectedRef.current = isSelected
const tooltipTimer = setTimeout(() => {
setShowTooltip(false)
}, TOOLTIP_TIMEOUT)
return () => { const display: NftCardDisplayProps = useMemo(() => {
clearTimeout(tooltipTimer) return {
} primaryInfo: !!asset.asset_contract.name && asset.asset_contract.name,
primaryInfoIcon: asset.collectionIsVerified && <VerifiedIcon height="16px" width="16px" />,
secondaryInfo: asset.name || asset.tokenId ? asset.name ?? `#${asset.tokenId}` : null,
selectedInfo: <Trans>Remove from bag</Trans>,
notSelectedInfo: <Trans>List for sale</Trans>,
disabledInfo: <Trans>Unavailable for listing</Trans>,
} }
isSelectedRef.current = isSelected }, [asset.asset_contract.name, asset.collectionIsVerified, asset.name, asset.tokenId])
return undefined
}, [isSelected, isSelectedRef])
const assetMediaType = Card.useAssetMediaType(asset)
const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
return ( return (
<Card.Container <NftCard
asset={asset} asset={asset}
selected={isSelected} display={display}
addAssetToBag={() => handleSelect(false)} isSelected={isSelected}
removeAssetFromBag={() => handleSelect(true)} isDisabled={Boolean(isDisabled)}
selectAsset={() => handleSelect(false)}
unselectAsset={() => handleSelect(true)}
onClick={onCardClick} onClick={onCardClick}
isDisabled={isDisabled} mediaShouldBePlaying={mediaShouldBePlaying}
> setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
<Card.ImageContainer isDisabled={isDisabled}> testId="nft-profile-asset"
<Tooltip doNotLinkToDetails={hideDetails}
text={ />
<Box as="span" className={bodySmall} color="textPrimary">
{isSelected ? <Trans>Added to bag</Trans> : <Trans>Removed from bag</Trans>}
</Box>
}
show={showTooltip}
style={{ display: 'block' }}
offsetX={0}
offsetY={-68}
hideArrow={true}
placement="bottom"
>
<MouseoverTooltip
text={getUnsupportedNftTextComponent(asset)}
placement="bottom"
offsetX={0}
offsetY={-60}
hideArrow={true}
style={{ display: 'block' }}
disableHover={!isDisabled}
timeout={isMobile ? TOOLTIP_TIMEOUT : undefined}
>
{getNftDisplayComponent(assetMediaType, mediaShouldBePlaying, setCurrentTokenPlayingMedia)}
</MouseoverTooltip>
</Tooltip>
</Card.ImageContainer>
<Card.DetailsContainer>
<Card.ProfileNftDetails asset={asset} hideDetails={hideDetails} />
</Card.DetailsContainer>
</Card.Container>
) )
} }
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