Commit 629fe2c1 authored by Jack Short's avatar Jack Short Committed by GitHub

feat: [DetailsV2] trait bubbles (#6552)

parent d73763ce
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import Row from 'components/Row'
import { Unicon } from 'components/Unicon'
import useENSAvatar from 'hooks/useENSAvatar'
import useENSName from 'hooks/useENSName'
import { useIsMobile } from 'nft/hooks'
import { GenieAsset } from 'nft/types'
import { getLinkForTrait } from 'nft/utils'
import { ReactNode, useReducer } from 'react'
import { ChevronDown, DollarSign } from 'react-feather'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ClickableStyle, EllipsisStyle, ExternalLink, LinkStyle, ThemedText } from 'theme'
import { isAddress, shortenAddress } from 'utils'
import { ExplorerDataType } from 'utils/getExplorerLink'
import { getExplorerLink } from 'utils/getExplorerLink'
const StyledBubble = styled(Row)`
background-color: ${({ theme }) => theme.backgroundSurface};
padding: 10px 12px 10px 8px;
border-radius: 20px;
max-width: 144px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
max-width: 169px;
}
`
const StyledLabelMedium = styled.div`
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.textPrimary};
${EllipsisStyle}
`
const StyledIcon = styled(Row)`
width: 24px;
height: 24px;
flex-shrink: 0;
color: ${({ theme }) => theme.accentAction};
border-radius: 100%;
overflow: hidden;
justify-content: center;
align-items: center;
`
const StyledLink = styled(Link)`
${ClickableStyle}
${LinkStyle}
`
const ConditionalLinkWrapper = ({
isExternal,
href,
children,
}: {
isExternal?: boolean
href: string
children: ReactNode
}) => {
return isExternal ? (
<ExternalLink href={href}>{children}</ExternalLink>
) : (
<StyledLink to={href}>{children}</StyledLink>
)
}
const InfoBubble = ({
title,
info,
icon,
href,
isExternal,
}: {
title: ReactNode
info: string
icon: ReactNode
href: string
isExternal?: boolean
}) => {
return (
<Column gap="sm">
<ThemedText.Caption color="textSecondary">{title}</ThemedText.Caption>
<ConditionalLinkWrapper isExternal={isExternal} href={href}>
<StyledBubble gap="sm">
<StyledIcon>{icon}</StyledIcon>
<StyledLabelMedium>{info}</StyledLabelMedium>
</StyledBubble>
</ConditionalLinkWrapper>
</Column>
)
}
const InfoChipDropdown = styled.button`
padding: 10px;
background-color: ${({ theme }) => theme.backgroundSurface};
color: ${({ theme }) => theme.textSecondary};
border-radius: 100%;
border: none;
cursor: pointer;
`
const InfoChipDropdownContainer = styled(Column)`
height: 100%;
margin-top: auto;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const Break = styled(Column)`
flex-basis: 100%;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const InfoChipsContainer = styled(Row)`
gap: 4px;
width: 100%;
flex-wrap: wrap;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
gap: 12px;
flex-wrap: nowrap;
}
`
const StyledChevron = styled(ChevronDown)<{ isOpen: boolean }>`
transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
will-change: transform;
transition: transform ${({ theme }) => theme.transition.duration.medium};
`
export const InfoChips = ({ asset }: { asset: GenieAsset }) => {
const { chainId } = useWeb3React()
const isMobile = useIsMobile()
const [showExtraInfoChips, toggleShowExtraInfoChips] = useReducer((s) => !s, false)
const shouldShowExtraInfoChips = !isMobile || showExtraInfoChips
const topTrait = asset?.traits?.[0]
const traitCollectionAddress = topTrait && getLinkForTrait(topTrait, asset.address)
const isChecksummedAddress = isAddress(asset.ownerAddress)
const checksummedAddress = isChecksummedAddress ? isChecksummedAddress : undefined
const { ENSName } = useENSName(checksummedAddress)
const { avatar } = useENSAvatar(checksummedAddress)
const shortenedAddress = asset.ownerAddress ? shortenAddress(asset.ownerAddress) : ''
const addressToDisplay = ENSName ?? shortenedAddress
const avatarToDisplay = avatar ? (
<img src={avatar} width={24} height={24} />
) : (
<Unicon size={24} address={asset.ownerAddress ?? ''} />
)
return (
<Column gap="sm">
<InfoChipsContainer justify="center">
<InfoBubble
title={<Trans>Owner</Trans>}
info={addressToDisplay}
icon={avatarToDisplay}
href={getExplorerLink(chainId ?? 1, asset.ownerAddress ?? '', ExplorerDataType.ADDRESS)}
isExternal={true}
/>
{traitCollectionAddress && (
<>
<InfoBubble
title={<Trans>Trait Floor</Trans>}
info="5.3 ETH"
icon={<DollarSign size={20} />}
href={traitCollectionAddress}
/>
<InfoChipDropdownContainer>
<InfoChipDropdown onClick={toggleShowExtraInfoChips}>
<StyledChevron isOpen={showExtraInfoChips} size={20} display="block" />
</InfoChipDropdown>
</InfoChipDropdownContainer>
{shouldShowExtraInfoChips && (
<>
<Break />
<InfoBubble
title={<Trans>Top Trait</Trans>}
info={topTrait.trait_value}
icon=""
href={traitCollectionAddress}
/>
</>
)}
</>
)}
</InfoChipsContainer>
</Column>
)
}
......@@ -5,6 +5,7 @@ import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { InfoChips } from './InfoChips'
import { MediaRenderer } from './MediaRenderer'
const MAX_WIDTH = 560
......@@ -117,6 +118,7 @@ export const LandingPage = ({ asset, collection }: LandingPageProps) => {
</Row>
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
</InfoDetailsContainer>
<InfoChips asset={asset} />
</InfoContainer>
</LandingPageContainer>
)
......
import Column from 'components/Column'
import Row from 'components/Row'
import { Trait } from 'nft/types'
import { formatEth } from 'nft/utils'
import qs from 'qs'
import { formatEth, getLinkForTrait } from 'nft/utils'
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
......@@ -60,14 +59,9 @@ export const TraitRow = ({ trait, collectionAddress }: { trait: Trait; collectio
// rarity eventually should be number of items with this trait / total number of items, smaller rarity means more rare
const randomRarity = Math.random()
const rarityLevel = getRarityLevel(randomRarity)
const params = qs.stringify(
{ traits: [`("${trait.trait_type}","${trait.trait_value}")`] },
{
arrayFormat: 'comma',
}
)
return (
<TraitRowLink to={`/nfts/collection/${collectionAddress}?${params}`}>
<TraitRowLink to={getLinkForTrait(trait, collectionAddress)}>
<TraitRowContainer>
<TraitColumnValue>
<SubheaderTiny>{trait.trait_type}</SubheaderTiny>
......
......@@ -26,6 +26,81 @@ exports[`LandingPage renders it correctly 1`] = `
gap: 4px;
}
.c16 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c20 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 8px;
}
.c22 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c18 {
color: #7780A0;
}
.c19 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FB118E;
stroke: #FB118E;
font-weight: 500;
}
.c19:hover {
opacity: 0.6;
}
.c19:active {
opacity: 0.4;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
......@@ -40,6 +115,21 @@ exports[`LandingPage renders it correctly 1`] = `
justify-content: flex-start;
}
.c15 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 8px;
}
.c6 {
width: 100%;
-webkit-align-items: center;
......@@ -48,6 +138,50 @@ exports[`LandingPage renders it correctly 1`] = `
align-items: center;
}
.c21 {
background-color: #FFFFFF;
padding: 10px 12px 10px 8px;
border-radius: 20px;
max-width: 144px;
}
.c24 {
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: #0D111C;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.c23 {
width: 24px;
height: 24px;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
color: #FB118E;
border-radius: 100%;
overflow: hidden;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c17 {
gap: 4px;
width: 100%;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.c2 {
position: relative;
object-fit: contain;
......@@ -128,6 +262,21 @@ exports[`LandingPage renders it correctly 1`] = `
filter: drop-shadow(0px 12px 20px rgba(0,0,0,0.1));
}
@media screen and (min-width:640px) {
.c21 {
max-width: 169px;
}
}
@media screen and (min-width:640px) {
.c17 {
gap: 12px;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
}
}
@media screen and (min-width:1280px) {
.c4 {
-webkit-filter: blur(50px);
......@@ -259,6 +408,40 @@ exports[`LandingPage renders it correctly 1`] = `
Azuki #3318
</div>
</div>
<div
class="c15"
>
<div
class="c9 c16 c17"
>
<div
class="c15"
>
<div
class="c18 css-4u0e4f"
>
Owner
</div>
<a
class="c19"
href="https://etherscan.io/address/"
rel="noopener noreferrer"
target="_blank"
>
<div
class="c9 c20 c21"
>
<div
class="c9 c22 c23"
/>
<div
class="c24"
/>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
......
......@@ -13,7 +13,8 @@ import {
SquareSudoSwapMarketplaceIcon,
SquareZoraMarketplaceIcon,
} from 'nft/components/icons'
import { DetailsOrigin, GenieAsset, Listing, Markets, UpdatedGenieAsset, WalletAsset } from 'nft/types'
import { DetailsOrigin, GenieAsset, Listing, Markets, Trait, UpdatedGenieAsset, WalletAsset } from 'nft/types'
import qs from 'qs'
import { v4 as uuidv4 } from 'uuid'
export function getRarityStatus(
......@@ -125,3 +126,14 @@ export const generateTweetForList = (assets: WalletAsset[]): string => {
.join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}`
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
}
export function getLinkForTrait(trait: Trait, collectionAddress: string): string {
const params = qs.stringify(
{ traits: [`("${trait.trait_type}","${trait.trait_value}")`] },
{
arrayFormat: 'comma',
}
)
return `/nfts/collection/${collectionAddress}?${params}`
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment