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
This commit is contained in:
parent
799edfb493
commit
b963d3b27b
@ -33,7 +33,7 @@ describe('Testing nfts', () => {
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().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-activity')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).should('exist')
|
||||
|
@ -16,12 +16,12 @@ try {
|
||||
}
|
||||
|
||||
// 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.
|
||||
const LAST_SIZE_ENTRY_KB = 1432
|
||||
|
||||
const SIZE_TOLERANCE_KB = 5
|
||||
const SIZE_TOLERANCE_KB = 10
|
||||
|
||||
const jsEntrypoints = entrypoints.filter((entrypoint) => entrypoint.endsWith('js'))
|
||||
assert(jsEntrypoints.length === 3)
|
||||
|
@ -495,8 +495,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
|
||||
...eventProperties,
|
||||
}
|
||||
|
||||
console.log(bagStatus)
|
||||
|
||||
return (
|
||||
<FooterContainer>
|
||||
<Footer>
|
||||
|
@ -4,7 +4,7 @@ import clsx from 'clsx'
|
||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
|
||||
import { TimedLoader } from 'nft/components/bag/TimedLoader'
|
||||
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 {
|
||||
ChevronDownBagIcon,
|
||||
|
297
src/nft/components/card/containers.tsx
Normal file
297
src/nft/components/card/containers.tsx
Normal file
@ -0,0 +1,297 @@
|
||||
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,
|
||||
}
|
157
src/nft/components/card/icons.tsx
Normal file
157
src/nft/components/card/icons.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
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>
|
||||
)
|
||||
}
|
123
src/nft/components/card/index.tsx
Normal file
123
src/nft/components/card/index.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
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>
|
||||
)
|
||||
}
|
256
src/nft/components/card/media.tsx
Normal file
256
src/nft/components/card/media.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
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>
|
||||
</>
|
||||
)
|
192
src/nft/components/card/utils.tsx
Normal file
192
src/nft/components/card/utils.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
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'
|
||||
}
|
@ -1,952 +0,0 @@
|
||||
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'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfacePageName, 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 { Box } from 'nft/components/Box'
|
||||
import { bodySmall } from 'nft/css/common.css'
|
||||
import { NftCard, NftCardDisplayProps } from 'nft/components/card'
|
||||
import { Ranking as RankingContainer, Suspicious as SuspiciousContainer } from 'nft/components/card/icons'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { GenieAsset, isPooledMarket, UniformAspectRatio } from 'nft/types'
|
||||
import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useAssetMediaType, useNotForSale } from './Card'
|
||||
import { AssetMediaType } from './Card'
|
||||
import * as Card from './Card'
|
||||
import { GenieAsset, UniformAspectRatio } from 'nft/types'
|
||||
import { formatWeiToDecimal } from 'nft/utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
interface CollectionAssetProps {
|
||||
asset: GenieAsset
|
||||
@ -29,25 +21,11 @@ interface CollectionAssetProps {
|
||||
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 = ({
|
||||
asset,
|
||||
isMobile,
|
||||
mediaShouldBePlaying,
|
||||
setCurrentTokenPlayingMedia,
|
||||
rarityVerified,
|
||||
uniformAspectRatio,
|
||||
setUniformAspectRatio,
|
||||
renderedHeight,
|
||||
@ -56,7 +34,6 @@ export const CollectionAsset = ({
|
||||
const bagManuallyClosed = useBag((state) => state.bagManuallyClosed)
|
||||
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
|
||||
const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag)
|
||||
const usedSweep = useBag((state) => state.usedSweep)
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
const bagExpanded = useBag((state) => state.bagExpanded)
|
||||
const setBagExpanded = useBag((state) => state.setBagExpanded)
|
||||
@ -73,21 +50,8 @@ export const CollectionAsset = ({
|
||||
}
|
||||
}, [asset, itemsInBag])
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
const isSelectedRef = useRef(isSelected)
|
||||
|
||||
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 notForSale = asset.notForSale || BigNumber.from(asset.priceInfo ? asset.priceInfo.ETHPrice : 0).lt(0)
|
||||
const provider = asset?.rarity?.providers ? asset.rarity.providers[0] : undefined
|
||||
const handleAddAssetToBag = useCallback(() => {
|
||||
if (BigNumber.from(asset.priceInfo?.ETHPrice ?? 0).gt(0)) {
|
||||
addAssetsToBag([asset])
|
||||
@ -103,122 +67,37 @@ export const CollectionAsset = ({
|
||||
}
|
||||
}, [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(() => {
|
||||
removeAssetsFromBag([asset])
|
||||
}, [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 (
|
||||
<Card.Container
|
||||
<NftCard
|
||||
asset={asset}
|
||||
selected={isSelected}
|
||||
addAssetToBag={handleAddAssetToBag}
|
||||
removeAssetFromBag={handleRemoveAssetFromBag}
|
||||
>
|
||||
<Card.ImageContainer isDisabled={asset.notForSale}>
|
||||
<StyledContainer data-testid="nft-collection-asset">
|
||||
<Tooltip
|
||||
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={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
|
||||
display={display}
|
||||
isSelected={isSelected}
|
||||
isDisabled={Boolean(asset.notForSale)}
|
||||
selectAsset={handleAddAssetToBag}
|
||||
unselectAsset={handleRemoveAssetFromBag}
|
||||
mediaShouldBePlaying={mediaShouldBePlaying}
|
||||
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}
|
||||
testId="nft-collection-asset"
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -2,19 +2,12 @@ import { Trans } from '@lingui/macro'
|
||||
import { useTrace } from '@uniswap/analytics'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
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 { Box } from 'nft/components/Box'
|
||||
import * as Card from 'nft/components/collection/Card'
|
||||
import { AssetMediaType } from 'nft/components/collection/Card'
|
||||
import { bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { NftCard, NftCardDisplayProps } from 'nft/components/card'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
const TOOLTIP_TIMEOUT = 2000
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface ViewMyNftsAssetProps {
|
||||
asset: WalletAsset
|
||||
@ -23,31 +16,6 @@ interface ViewMyNftsAssetProps {
|
||||
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 = ({
|
||||
asset,
|
||||
mediaShouldBePlaying,
|
||||
@ -67,8 +35,6 @@ export const ViewMyNftsAsset = ({
|
||||
)
|
||||
}, [asset, sellAssets])
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false)
|
||||
const isSelectedRef = useRef(isSelected)
|
||||
const trace = useTrace()
|
||||
const onCardClick = () => handleSelect(isSelected)
|
||||
|
||||
@ -93,65 +59,32 @@ export const ViewMyNftsAsset = ({
|
||||
toggleCart()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected !== isSelectedRef.current) {
|
||||
setShowTooltip(true)
|
||||
isSelectedRef.current = isSelected
|
||||
const tooltipTimer = setTimeout(() => {
|
||||
setShowTooltip(false)
|
||||
}, TOOLTIP_TIMEOUT)
|
||||
|
||||
return () => {
|
||||
clearTimeout(tooltipTimer)
|
||||
}
|
||||
}
|
||||
isSelectedRef.current = isSelected
|
||||
return undefined
|
||||
}, [isSelected, isSelectedRef])
|
||||
|
||||
const assetMediaType = Card.useAssetMediaType(asset)
|
||||
const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
|
||||
|
||||
return (
|
||||
<Card.Container
|
||||
asset={asset}
|
||||
selected={isSelected}
|
||||
addAssetToBag={() => handleSelect(false)}
|
||||
removeAssetFromBag={() => handleSelect(true)}
|
||||
onClick={onCardClick}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<Card.ImageContainer isDisabled={isDisabled}>
|
||||
<Tooltip
|
||||
text={
|
||||
<Box as="span" className={bodySmall} color="textPrimary">
|
||||
{isSelected ? <Trans>Added to bag</Trans> : <Trans>Removed from bag</Trans>}
|
||||
</Box>
|
||||
const display: NftCardDisplayProps = useMemo(() => {
|
||||
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>,
|
||||
}
|
||||
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>
|
||||
}, [asset.asset_contract.name, asset.collectionIsVerified, asset.name, asset.tokenId])
|
||||
|
||||
return (
|
||||
<NftCard
|
||||
asset={asset}
|
||||
display={display}
|
||||
isSelected={isSelected}
|
||||
isDisabled={Boolean(isDisabled)}
|
||||
selectAsset={() => handleSelect(false)}
|
||||
unselectAsset={() => handleSelect(true)}
|
||||
onClick={onCardClick}
|
||||
mediaShouldBePlaying={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
testId="nft-profile-asset"
|
||||
doNotLinkToDetails={hideDetails}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user