feat: audio video nft collection cards (#4628)
* feat: video asset cards * feat: audio cards * square aspect ratio for videos * adding playsinline
This commit is contained in:
parent
80c1f0cdf9
commit
f1c65afa98
@ -1,4 +1,5 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { calc } from '@vanilla-extract/css-utils'
|
||||
import { breakpoints, sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const card = style([
|
||||
@ -130,3 +131,16 @@ export const erc1155MinusButton = style([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const playbackSwitch = style([
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
width: '40',
|
||||
height: '40',
|
||||
zIndex: '1',
|
||||
}),
|
||||
{
|
||||
marginLeft: calc.subtract('100%', '50px'),
|
||||
transform: 'translateY(-56px)',
|
||||
},
|
||||
])
|
||||
|
@ -1,8 +1,9 @@
|
||||
import clsx from 'clsx'
|
||||
import Column from 'components/Column'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import * as styles from 'nft/components/collection/Card.css'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { MinusIconLarge, PlusIconLarge } from 'nft/components/icons'
|
||||
import { MinusIconLarge, PauseButtonIcon, PlayButtonIcon, PlusIconLarge } from 'nft/components/icons'
|
||||
import { body, subheadSmall } from 'nft/css/common.css'
|
||||
import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
@ -19,8 +20,6 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import * as styles from './Card.css'
|
||||
|
||||
/* -------- ASSET CONTEXT -------- */
|
||||
export interface CardContextProps {
|
||||
asset: GenieAsset
|
||||
@ -94,33 +93,222 @@ const Image = () => {
|
||||
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
if (noContent) {
|
||||
return <NoContentContainer />
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as={'img'}
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: 'auto',
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
background: loaded
|
||||
? 'none'
|
||||
: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit={'contain'}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
setLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && styles.cardImageHover)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
interface MediaProps {
|
||||
shouldPlay: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}
|
||||
|
||||
const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
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 />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!noContent ? (
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as={'img'}
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: 'auto',
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
background: loaded
|
||||
? 'none'
|
||||
: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit={'contain'}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
setLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && styles.cardImageHover)}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as={'img'}
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
willChange: 'transform',
|
||||
background: imageLoaded
|
||||
? 'none'
|
||||
: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit={'contain'}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
visibility={shouldPlay ? 'hidden' : 'visible'}
|
||||
className={clsx(hovered && styles.cardImageHover)}
|
||||
/>
|
||||
</Box>
|
||||
{shouldPlay ? (
|
||||
<>
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PauseButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" left="0" top="0" display="flex">
|
||||
<Box
|
||||
as="video"
|
||||
ref={vidRef}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
}}
|
||||
onEnded={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
loop
|
||||
playsInline
|
||||
>
|
||||
<source src={asset.animationUrl} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<NoContentContainer />
|
||||
<Box className={styles.playbackSwitch}>
|
||||
{((!isMobile && hovered) || isMobile) && (
|
||||
<PlayButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(asset.tokenId)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Audio = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
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 />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as={'img'}
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: 'auto',
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
background: imageLoaded
|
||||
? 'none'
|
||||
: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit={'contain'}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && styles.cardImageHover)}
|
||||
/>
|
||||
</Box>
|
||||
{shouldPlay ? (
|
||||
<>
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PauseButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" left="0" top="0" display="flex">
|
||||
<Box
|
||||
as="audio"
|
||||
ref={audRef}
|
||||
width="full"
|
||||
height="full"
|
||||
onEnded={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
>
|
||||
<source src={asset.animationUrl} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box className={styles.playbackSwitch}>
|
||||
{((!isMobile && hovered) || isMobile) && (
|
||||
<PlayButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(asset.tokenId)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@ -342,6 +530,7 @@ const NoContentContainer = () => (
|
||||
)
|
||||
|
||||
export {
|
||||
Audio,
|
||||
Button,
|
||||
Container,
|
||||
DetailsContainer,
|
||||
@ -355,4 +544,5 @@ export {
|
||||
SecondaryInfo,
|
||||
SecondaryRow,
|
||||
TertiaryInfo,
|
||||
Video,
|
||||
}
|
||||
|
@ -1,27 +1,50 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import * as Card from 'nft/components/collection/Card'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import { formatWeiToDecimal } from 'nft/utils/currency'
|
||||
import { isAudio } from 'nft/utils/isAudio'
|
||||
import { isVideo } from 'nft/utils/isVideo'
|
||||
import { MouseEvent, useMemo } from 'react'
|
||||
|
||||
import { GenieAsset } from '../../types'
|
||||
import { formatWeiToDecimal } from '../../utils/currency'
|
||||
enum AssetMediaType {
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
}
|
||||
|
||||
export const CollectionAsset = ({ asset }: { asset: GenieAsset }) => {
|
||||
// ignore structure more will go inside
|
||||
const { notForSale } = useMemo(() => {
|
||||
if (asset) {
|
||||
return {
|
||||
notForSale: asset.notForSale || BigNumber.from(asset.currentEthPrice ? asset.currentEthPrice : 0).lt(0),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
notForSale: true,
|
||||
}
|
||||
interface CollectionAssetProps {
|
||||
asset: GenieAsset
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}
|
||||
|
||||
export const CollectionAsset = ({ asset, mediaShouldBePlaying, setCurrentTokenPlayingMedia }: CollectionAssetProps) => {
|
||||
const { notForSale, assetMediaType } = useMemo(() => {
|
||||
let notForSale = true
|
||||
let assetMediaType = AssetMediaType.Image
|
||||
|
||||
notForSale = asset.notForSale || BigNumber.from(asset.currentEthPrice ? asset.currentEthPrice : 0).lt(0)
|
||||
if (isAudio(asset.animationUrl)) {
|
||||
assetMediaType = AssetMediaType.Audio
|
||||
} else if (isVideo(asset.animationUrl)) {
|
||||
assetMediaType = AssetMediaType.Video
|
||||
}
|
||||
|
||||
return {
|
||||
notForSale,
|
||||
assetMediaType,
|
||||
}
|
||||
}, [asset])
|
||||
|
||||
return (
|
||||
<Card.Container asset={asset}>
|
||||
<Card.Image />
|
||||
{assetMediaType === AssetMediaType.Image ? (
|
||||
<Card.Image />
|
||||
) : assetMediaType === AssetMediaType.Video ? (
|
||||
<Card.Video shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
|
||||
) : (
|
||||
<Card.Audio shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
|
||||
)}
|
||||
<Card.DetailsContainer>
|
||||
<Card.InfoContainer>
|
||||
<Card.PrimaryRow>
|
||||
|
@ -6,7 +6,7 @@ import { Center } from 'nft/components/Flex'
|
||||
import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css'
|
||||
import { useCollectionFilters } from 'nft/hooks'
|
||||
import { AssetsFetcher } from 'nft/queries'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
|
||||
@ -48,6 +48,8 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
|
||||
}
|
||||
)
|
||||
|
||||
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
|
||||
|
||||
const collectionNfts = useMemo(() => {
|
||||
if (!collectionAssets || !AssetsFetchSuccess) return undefined
|
||||
|
||||
@ -70,7 +72,14 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
|
||||
{collectionNfts.length > 0 ? (
|
||||
<div className={styles.assetList}>
|
||||
{collectionNfts.map((asset) => {
|
||||
return asset ? <CollectionAsset asset={asset} key={asset.address + asset.tokenId} /> : null
|
||||
return asset ? (
|
||||
<CollectionAsset
|
||||
key={asset.address + asset.tokenId}
|
||||
asset={asset}
|
||||
mediaShouldBePlaying={asset.tokenId === currentTokenPlayingMedia}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
|
Loading…
Reference in New Issue
Block a user