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:
Jack Short 2022-09-15 15:12:23 -04:00 committed by GitHub
parent 80c1f0cdf9
commit f1c65afa98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 279 additions and 43 deletions

@ -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>
) : (