Commit ea0fe83d authored by aballerr's avatar aballerr Committed by GitHub

chore: Merging details part 2 (#4594)

* Merging details part 2
Co-authored-by: default avatarAlex Ball <alexball@UNISWAP-MAC-038.local>
parent 994836fb
import { globalStyle, style } from '@vanilla-extract/css'
import { sprinkles } from '../../css/sprinkles.css'
export const hiddenText = style([
sprinkles({
overflow: 'hidden',
}),
{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
},
])
export const span = style({})
globalStyle(`${hiddenText} p`, {
display: 'none',
})
globalStyle(`${hiddenText} p:first-child`, {
display: 'block',
})
globalStyle(`${span} p:first-child`, {
textOverflow: 'ellipsis',
overflow: 'hidden',
margin: 0,
})
import clsx from 'clsx'
import { useState } from 'react'
import { Box, BoxProps } from '../Box'
import * as styles from './ExpandableText.css'
const RevealButton = (props: BoxProps) => (
<Box
as="button"
display="inline"
fontWeight="bold"
border="none"
fontSize="14"
color="darkGray"
padding="0"
background="transparent"
{...props}
/>
)
export const ExpandableText = ({ children, ...props }: BoxProps) => {
const [isExpanded, setExpanded] = useState(false)
return (
<Box
display="flex"
flexDirection={isExpanded ? 'column' : 'row'}
alignItems={isExpanded ? 'flex-start' : 'flex-end'}
justifyContent="flex-start"
fontSize="14"
color="darkGray"
marginTop="0"
marginBottom="20"
{...props}
>
<span className={clsx(styles.span, !isExpanded && styles.hiddenText)}>
{children}{' '}
{isExpanded ? (
<RevealButton marginTop={isExpanded ? '8' : 'unset'} onClick={() => setExpanded(!isExpanded)}>
Show less
</RevealButton>
) : (
<RevealButton onClick={() => setExpanded(!isExpanded)}>Show more</RevealButton>
)}
</span>
</Box>
)
}
...@@ -238,6 +238,16 @@ const borderWidth = ['0px', '1px', '1.5px', '2px', '4px'] ...@@ -238,6 +238,16 @@ const borderWidth = ['0px', '1px', '1.5px', '2px', '4px']
const borderStyle = ['none', 'solid'] as const const borderStyle = ['none', 'solid'] as const
// TODO: remove when code is done being ported over
// I'm leaving this here as a reference of the old breakpoints while we port over the new code
// tabletSm: 656,
// tablet: 708,
// tabletL: 784,
// tabletXl: 830,
// desktop: 948,
// desktopL: 1030,
// desktopXl: 1260,
export const breakpoints = { export const breakpoints = {
sm: 640, sm: 640,
md: 768, md: 768,
......
import { style } from '@vanilla-extract/css'
import { center, subhead } from '../../css/common.css'
import { sprinkles, vars } from '../../css/sprinkles.css'
export const image = style([
sprinkles({ borderRadius: '20', height: 'full', alignSelf: 'center' }),
{
width: 'calc(90vh - 165px)',
height: 'calc(90vh - 165px)',
maxHeight: '678px',
maxWidth: '678px',
boxShadow: `0px 20px 50px var(--shadow), 0px 10px 50px rgba(70, 115, 250, 0.2)`,
'@media': {
'(max-width: 1024px)': {
maxHeight: '64vh',
maxWidth: '64vh',
},
'(max-width: 640px)': {
maxHeight: '56vh',
maxWidth: '56vh',
},
},
},
])
export const container = style([
center,
{
minHeight: 'calc(100vh - 97px)',
},
])
export const marketplace = sprinkles({ borderRadius: '4' })
export const tab = style([
subhead,
sprinkles({ color: 'darkGray', border: 'none', padding: '0', background: 'transparent' }),
{
selectors: {
'&[data-active="true"]': {
textDecoration: 'underline',
textDecorationColor: vars.color.genieBlue,
textUnderlineOffset: '8px',
textDecorationThickness: '2px',
color: vars.color.blackBlue,
},
},
},
])
export const creator = style({
'@media': {
'(max-width: 640px)': {
display: 'none',
},
},
})
export const columns = style([
sprinkles({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
width: 'full',
paddingLeft: { sm: '16', lg: '24', xl: '52' },
paddingRight: { sm: '16', lg: '24', xl: '52' },
paddingBottom: { sm: '16', lg: '24', xl: '52' },
paddingTop: '16',
gap: { sm: '32', lg: '28', xl: '52' },
}),
{
boxSizing: 'border-box',
'@media': {
'(max-width: 1024px)': {
flexDirection: 'column',
alignItems: 'center',
},
},
},
])
export const column = style({
maxWidth: '50%',
width: '50%',
alignSelf: 'center',
'@media': {
'(max-width: 1024px)': {
maxWidth: 'calc(88%)',
width: 'calc(88%)',
},
},
})
export const columnRight = style({
maxHeight: 'calc(100vh - 165px)',
overflow: 'scroll',
'@media': {
'(max-width: 1024px)': {
maxHeight: '100%',
},
},
selectors: {
'&::-webkit-scrollbar': {
display: 'none',
},
},
scrollbarWidth: 'none',
})
export const audioControls = style({
position: 'absolute',
left: '0',
right: '0',
textAlign: 'center',
marginRight: 'auto',
marginLeft: 'auto',
bottom: 'calc(10%)',
})
import clsx from 'clsx'
import useENSName from 'hooks/useENSName'
import qs from 'query-string'
import { useEffect, useMemo, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useParams } from 'react-router-dom' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import { useSpring } from 'react-spring/web'
import { AnimatedBox, Box } from '../../components/Box'
import { CollectionProfile } from '../../components/details/CollectionProfile'
import { Details } from '../../components/details/Details' import { Details } from '../../components/details/Details'
import { Traits } from '../../components/details/Traits'
import { Center, Column, Row } from '../../components/Flex'
import { CloseDropDownIcon, CornerDownLeftIcon, ShareIcon } from '../../components/icons'
import { ExpandableText } from '../../components/layout/ExpandableText'
import { header2 } from '../../css/common.css'
import { themeVars } from '../../css/sprinkles.css'
import { useBag } from '../../hooks'
import { fetchSingleAsset } from '../../queries' import { fetchSingleAsset } from '../../queries'
import { CollectionInfoForAsset, GenieAsset } from '../../types' import { CollectionInfoForAsset, GenieAsset } from '../../types'
import { shortenAddress } from '../../utils/address'
import { isAudio } from '../../utils/isAudio'
import { isVideo } from '../../utils/isVideo'
import { rarityProviderLogo } from '../../utils/rarity'
import * as styles from './Asset.css'
const AudioPlayer = ({
imageUrl,
animationUrl,
name,
collectionName,
dominantColor,
}: GenieAsset & { dominantColor: [number, number, number] }) => {
return (
<Box position="relative" display="inline-block" alignSelf="center">
<Box as="audio" className={styles.audioControls} width="292" controls src={animationUrl} />
<img
className={styles.image}
src={imageUrl}
alt={name || collectionName}
style={{
['--shadow' as string]: `rgba(${dominantColor.join(', ')}, 0.5)`,
minWidth: '300px',
minHeight: '300px',
}}
/>
</Box>
)
}
const AssetView = ({
mediaType,
asset,
dominantColor,
}: {
mediaType: 'image' | 'video' | 'audio'
asset: GenieAsset
dominantColor: [number, number, number]
}) => {
const style = { ['--shadow' as string]: `rgba(${dominantColor.join(', ')}, 0.5)` }
switch (mediaType) {
case 'video':
return <video src={asset.animationUrl} className={styles.image} autoPlay controls muted loop style={style} />
case 'image':
return (
<img className={styles.image} src={asset.imageUrl} alt={asset.name || asset.collectionName} style={style} />
)
case 'audio':
return <AudioPlayer {...asset} dominantColor={dominantColor} />
}
}
enum MediaType {
Audio = 'audio',
Video = 'video',
Image = 'image',
}
const Asset = () => { const Asset = () => {
const { tokenId = '', contractAddress = '' } = useParams() const { tokenId = '', contractAddress = '' } = useParams()
const { data } = useQuery(['assetDetail', contractAddress, tokenId], () => const { data } = useQuery(['assetDetail', contractAddress, tokenId], () =>
fetchSingleAsset({ contractAddress, tokenId }) fetchSingleAsset({ contractAddress, tokenId })
) )
const { pathname, search } = useLocation()
const navigate = useNavigate()
const bagExpanded = useBag((state) => state.bagExpanded)
const [creatorAddress, setCreatorAddress] = useState('')
const [ownerAddress, setOwnerAddress] = useState('')
const [dominantColor] = useState<[number, number, number]>([0, 0, 0])
const creatorEnsName = useENSName(creatorAddress)
const ownerEnsName = useENSName(ownerAddress)
const parsed = qs.parse(search)
const asset = useMemo(() => (data ? data[0] : ({} as GenieAsset)), [data])
const collection = useMemo(() => (data ? data[1] : ({} as CollectionInfoForAsset)), [data])
const { gridWidthOffset } = useSpring({
gridWidthOffset: bagExpanded ? 324 : 0,
})
const [showTraits, setShowTraits] = useState(true)
let asset = {} as GenieAsset useEffect(() => {
let collection = {} as CollectionInfoForAsset if (asset.creator) setCreatorAddress(asset.creator.address)
if (asset.owner) setOwnerAddress(asset.owner)
}, [asset])
if (data) { const { rarityProvider } = useMemo(
asset = data[0] || {} () =>
collection = data[1] || {} asset.rarity
? {
rarityProvider: asset.rarity.providers.find(
({ provider: _provider }) => _provider === asset.rarity?.primaryProvider
),
rarityLogo: rarityProviderLogo[asset.rarity.primaryProvider] || '',
} }
: {},
[asset.rarity]
)
const assetMediaType = useMemo(() => {
if (isAudio(asset.animationUrl)) {
return MediaType.Audio
} else if (isVideo(asset.animationUrl)) {
return MediaType.Video
}
return MediaType.Image
}, [asset])
return ( return (
<div> <AnimatedBox
{' '} style={{
// @ts-ignore
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x}px)`),
}}
className={styles.container}
>
<div className={styles.columns}>
<Column className={styles.column}>
{assetMediaType === MediaType.Image ? (
<img
className={styles.image}
src={asset.imageUrl}
alt={asset.name || collection.collectionName}
style={{ ['--shadow' as string]: `rgba(${dominantColor.join(', ')}, 0.5)` }}
/>
) : (
<AssetView asset={asset} mediaType={assetMediaType} dominantColor={dominantColor} />
)}
</Column>
<Column className={clsx(styles.column, styles.columnRight)} width="full">
<Column>
<Row marginBottom="8" alignItems="center" justifyContent={rarityProvider ? 'space-between' : 'flex-end'}>
<Row gap="12">
<Center
as="button"
padding="0"
border="none"
background="transparent"
onClick={async () => {
await navigator.clipboard.writeText(window.location.hostname + pathname)
}}
>
<ShareIcon />
</Center>
<Center
as="button"
border="none"
width="32"
height="32"
padding="0"
background="transparent"
cursor="pointer"
onClick={() => {
if (!parsed.origin || parsed.origin === 'collection') {
navigate(`/nft/collection/${asset.address}`, undefined)
} else if (parsed.origin === 'sell') {
navigate('/nft/sell', undefined)
} else if (parsed.origin === 'explore') {
navigate(`/nft`, undefined)
} else if (parsed.origin === 'activity') {
navigate(`/nft/collection/${asset.address}/activity`, undefined)
}
}}
>
{parsed.origin ? (
<CornerDownLeftIcon width="28" height="28" />
) : (
<CloseDropDownIcon color={themeVars.colors.darkGray} />
)}
</Center>
</Row>
</Row>
<Row as="h1" marginTop="0" marginBottom="12" gap="2" className={header2}>
{asset.name || `${collection.collectionName} #${asset.tokenId}`}
</Row>
{collection.collectionDescription ? (
<ExpandableText>
<ReactMarkdown
allowedTypes={['link', 'paragraph', 'strong', 'code', 'emphasis', 'text']}
source={collection.collectionDescription}
/>
</ExpandableText>
) : null}
<Row
justifyContent={{
sm: 'space-between',
}}
gap={{
sm: 'unset',
}}
marginBottom="36"
>
{ownerAddress.length > 0 && (
<a
target="_blank"
rel="noreferrer"
href={`https://etherscan.io/address/${asset.owner}`}
style={{ textDecoration: 'none' }}
>
<CollectionProfile
label="Owner"
avatarUrl=""
name={ownerEnsName.ENSName ?? shortenAddress(ownerAddress, 0, 4)}
/>
</a>
)}
<Link to={`/collection/${asset.address}`} style={{ textDecoration: 'none' }}>
<CollectionProfile
label="Collection"
avatarUrl={collection.collectionImageUrl}
name={collection.collectionName}
isVerified={collection.isVerified}
/>
</Link>
{creatorAddress ? (
<a
target="_blank"
rel="noreferrer"
href={`https://etherscan.io/address/${creatorAddress}`}
style={{ textDecoration: 'none' }}
>
<CollectionProfile
label="Creator"
avatarUrl={asset.creator.profile_img_url}
name={creatorEnsName.ENSName ?? shortenAddress(creatorAddress, 0, 4)}
isVerified
className={styles.creator}
/>
</a>
) : null}
</Row>
<Row gap="32" marginBottom="20">
<button data-active={showTraits} onClick={() => setShowTraits(true)} className={styles.tab}>
Traits
</button>
<button data-active={!showTraits} onClick={() => setShowTraits(false)} className={styles.tab}>
Details
</button>
</Row>
{showTraits ? (
<Traits collectionAddress={asset.address} traits={asset.traits ?? []} />
) : (
<Details <Details
contractAddress={contractAddress} contractAddress={contractAddress}
tokenId={tokenId} tokenId={tokenId}
...@@ -31,7 +272,11 @@ const Asset = () => { ...@@ -31,7 +272,11 @@ const Asset = () => {
metadataUrl={asset.externalLink} metadataUrl={asset.externalLink}
totalSupply={collection.totalSupply} totalSupply={collection.totalSupply}
/> />
)}
</Column>
</Column>
</div> </div>
</AnimatedBox>
) )
} }
......
...@@ -99,7 +99,7 @@ export interface GenieAsset { ...@@ -99,7 +99,7 @@ export interface GenieAsset {
decimals?: number decimals?: number
collectionIsVerified?: boolean collectionIsVerified?: boolean
rarity?: Rarity rarity?: Rarity
owner: OpenSeaUser owner: string
creator: OpenSeaUser creator: OpenSeaUser
externalLink: string externalLink: string
traits?: { traits?: {
......
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