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.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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|||||||
...eventProperties,
|
...eventProperties,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(bagStatus)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FooterContainer>
|
<FooterContainer>
|
||||||
<Footer>
|
<Footer>
|
||||||
|
@ -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,
|
||||||
|
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 { 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 {
|
|||||||
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 = ({
|
|||||||
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 = ({
|
|||||||
}
|
}
|
||||||
}, [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 = ({
|
|||||||
}
|
}
|
||||||
}, [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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 { 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 {
|
|||||||
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 = ({
|
|||||||
)
|
)
|
||||||
}, [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 = ({
|
|||||||
toggleCart()
|
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
|
const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
|
||||||
|
|
||||||
|
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>,
|
||||||
|
}
|
||||||
|
}, [asset.asset_contract.name, asset.collectionIsVerified, asset.name, asset.tokenId])
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user