feat: NFT Explore Activity Feed (#4635)

* NFT Explore: Add Activity Feed to Banner section

* Renamed separate style file

* Fix positioning to not squish details section

* Add back activeRow state

* Hide Activity on smaller screens

* Fix for uneven widths between collections

* Addressing PR feedback

Co-authored-by: gbugyis <greg@bugyis.com>
This commit is contained in:
Greg Bugyis 2022-09-21 02:08:25 +03:00 committed by GitHub
parent 8e955e9257
commit e35f9e16a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 247 additions and 53 deletions

@ -0,0 +1,135 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ActivityFetcher } from 'nft/queries'
import { ActivityEvent, ActivityEventTypeDisplay, Markets } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { getTimeDifference, isValidDate } from 'nft/utils/date'
import { putCommas } from 'nft/utils/putCommas'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { useQuery } from 'react-query'
import { useNavigate } from 'react-router-dom'
import * as styles from './Explore.css'
const ActivityFeed = ({ address }: { address: string }) => {
const [current, setCurrent] = useState(0)
const [hovered, toggleHover] = useReducer((state) => !state, false)
const navigate = useNavigate()
const { data: collectionActivity } = useQuery(['collectionActivity', address], () => ActivityFetcher(address), {
staleTime: 5000,
})
useEffect(() => {
const interval = setInterval(() => {
if (collectionActivity && !hovered) setCurrent(current === collectionActivity.events.length - 1 ? 0 : current + 1)
}, 3000)
return () => clearInterval(interval)
}, [current, collectionActivity, hovered])
return (
<Column
borderRadius="20"
overflow="hidden"
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
marginTop="40"
style={{ background: 'rgba(13, 14, 14, 0.7)', height: '270px', width: '60%' }}
>
{collectionActivity ? (
<Box display="flex" flexDirection="row" flexWrap="nowrap" overflow="hidden">
<Column padding="20" style={{ maxWidth: '286px' }}>
<Box
as="img"
src={collectionActivity.events[current].tokenMetadata?.imageUrl}
borderRadius="12"
style={{ width: '230px', height: '230px' }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
navigate(`/nfts/asset/${address}/${collectionActivity.events[current].tokenId}?origin=explore`)
}}
/>
</Column>
<Column width="full" position="relative">
{collectionActivity.events.map((activityEvent: ActivityEvent, index: number) => {
return (
<ActivityRow
event={activityEvent}
index={index}
key={`${activityEvent.eventType}${activityEvent.tokenId}`}
current={current}
/>
)
})}
</Column>
</Box>
) : null}
</Column>
)
}
const ActivityRow = ({ event, index, current }: { event: ActivityEvent; index: number; current: number }) => {
const navigate = useNavigate()
const formattedPrice = useMemo(
() => (event.price ? putCommas(formatEthPrice(event.price)).toString() : null),
[event.price]
)
const scrollPosition = useMemo(() => {
const itemHeight = 56
if (current === index) return current === 0 ? 0 : itemHeight / 2
if (index > current)
return current === 0 ? (index - current) * itemHeight : (index - current) * itemHeight + itemHeight / 2
if (index < current)
return current === 0 ? -(current - index) * itemHeight : -((current - index) * itemHeight - itemHeight / 2)
else return 0
}, [index, current])
return (
<Row
className={clsx(styles.activityRow, index === current && styles.activeRow)}
paddingTop="8"
paddingBottom="8"
fontSize="14"
width="full"
paddingLeft="16"
style={{ transform: `translateY(${scrollPosition}px)` }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
navigate(`/nft/asset/${event.collectionAddress}/${event.tokenId}?origin=explore`)
}}
>
<Box as="img" src={event.tokenMetadata?.imageUrl} borderRadius="12" marginRight="8" width="40" height="40" />
<Box as="span" color="explicitWhite">
<Box as="span">{ActivityEventTypeDisplay[event.eventType]}</Box>
<Box as="span" color="grey300" paddingLeft="4" paddingRight="4">
for
</Box>
{formattedPrice} ETH
</Box>
{event.eventTimestamp && isValidDate(event.eventTimestamp) && (
<Box className={styles.timestamp}>
{getTimeDifference(event.eventTimestamp?.toString())}
{event.marketplace && <MarketplaceIcon marketplace={event.marketplace} />}
</Box>
)}
</Row>
)
}
export default ActivityFeed
const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => {
return (
<Box
as="img"
alt={marketplace}
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
className={styles.marketplaceIcon}
/>
)
}

@ -1,24 +1,30 @@
import clsx from 'clsx'
import { useWindowSize } from 'hooks/useWindowSize'
import { Box } from 'nft/components/Box'
import { Center, Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import { bodySmall, buttonMedium, header1 } from 'nft/css/common.css'
import { vars } from 'nft/css/sprinkles.css'
import { fetchTrendingCollections } from 'nft/queries'
import { breakpoints } from 'nft/css/sprinkles.css'
import { ActivityFetcher, fetchTrendingCollections } from 'nft/queries'
import { TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { formatChange, toSignificant } from 'nft/utils/toSignificant'
import { useEffect, useState } from 'react'
import { useQuery } from 'react-query'
import { QueryClient, useQuery } from 'react-query'
import { Link } from 'react-router-dom'
import ActivityFeed from './ActivityFeed'
import * as styles from './Explore.css'
const queryClient = new QueryClient()
const Banner = () => {
/* Sets initially displayed collection to random number between 0 and 4 */
const [current, setCurrent] = useState(Math.floor(Math.random() * 5))
const [hovered, setHover] = useState(false)
const { width: windowWidth } = useWindowSize()
const { data: collections } = useQuery(
['trendingCollections'],
() => {
@ -38,7 +44,11 @@ const Banner = () => {
const interval = setInterval(async () => {
if (collections) {
const nextCollectionIndex = (current + 1) % collections.length
const nextCollectionAddress = collections[nextCollectionIndex].address
setCurrent(nextCollectionIndex)
await queryClient.prefetchQuery(['collectionActivity', nextCollectionAddress], () =>
ActivityFetcher(nextCollectionAddress as string)
)
}
}, 15_000)
return () => {
@ -57,7 +67,11 @@ const Banner = () => {
style={{ backgroundImage: `url(${collections[current].bannerImageUrl})` }}
>
<Box className={styles.bannerOverlay} width="full" />
<CollectionDetails collection={collections[current]} hovered={hovered} rank={current + 1} />
<Box as="section" className={styles.section} display="flex" flexDirection="row" flexWrap="nowrap">
<CollectionDetails collection={collections[current]} hovered={hovered} rank={current + 1} />
{windowWidth && windowWidth > breakpoints.lg && <ActivityFeed address={collections[current].address} />}
</Box>
<CarouselProgress length={collections.length} currentIndex={current} setCurrent={setCurrent} />
</div>
</Box>
@ -84,58 +98,56 @@ const CollectionDetails = ({
rank: number
hovered: boolean
}) => (
<Box as="section" className={styles.section} paddingTop="40">
<Column className={styles.collectionDetails} paddingTop="24">
<div className={styles.volumeRank}>#{rank} volume in 24hr</div>
<Row>
<Box as="span" marginTop="16" className={clsx(header1, styles.collectionName)}>
{collection.name}
<Column className={styles.collectionDetails} paddingTop="40">
<div className={styles.volumeRank}>#{rank} volume in 24hr</div>
<Row>
<Box as="span" marginTop="16" className={clsx(header1, styles.collectionName)}>
{collection.name}
</Box>
{collection.isVerified && (
<Box as="span" marginTop="24">
<VerifiedIcon height="32" width="32" />
</Box>
{collection.isVerified && (
<Box as="span" marginTop="24">
<VerifiedIcon height="32" width="32" />
)}
</Row>
<Row className={bodySmall} marginTop="12" color="explicitWhite">
<Box>
<Box as="span" color="darkGray" marginRight="4">
Floor:
</Box>
{collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH
</Box>
<Box>
{collection.floorChange ? (
<Box as="span" color={collection.floorChange > 0 ? 'green200' : 'error'} marginLeft="4">
{collection.floorChange > 0 && '+'}
{formatChange(collection.floorChange)}%
</Box>
)}
</Row>
<Row className={bodySmall} marginTop="12" color="explicitWhite">
<Box>
<Box as="span" color="darkGray" marginRight="4">
Floor:
) : null}
</Box>
<Box marginLeft="24" color="explicitWhite">
<Box as="span" color="darkGray" marginRight="4">
Volume:
</Box>
{collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH
</Box>
<Box>
{collection.volumeChange ? (
<Box as="span" color={collection.volumeChange > 0 ? 'green200' : 'error'} marginLeft="4">
{collection.volumeChange > 0 && '+'}
{formatChange(collection.volumeChange)}%
</Box>
{collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH
</Box>
<Box>
{collection.floorChange ? (
<Box as="span" color={collection.floorChange > 0 ? 'green200' : 'error'} marginLeft="4">
{collection.floorChange > 0 && '+'}
{formatChange(collection.floorChange)}%
</Box>
) : null}
</Box>
<Box marginLeft="24" color="explicitWhite">
<Box as="span" color="darkGray" marginRight="4">
Volume:
</Box>
{collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH
</Box>
<Box>
{collection.volumeChange ? (
<Box as="span" color={collection.volumeChange > 0 ? 'green200' : 'error'} marginLeft="4">
{collection.volumeChange > 0 && '+'}
{formatChange(collection.volumeChange)}%
</Box>
) : null}
</Box>
</Row>
<Link
className={clsx(buttonMedium, styles.exploreCollection)}
to={`/nfts/collection/${collection.address}`}
style={{ textDecoration: 'none', backgroundColor: `${hovered ? vars.color.blue400 : vars.color.grey700}` }}
>
Explore collection
</Link>
</Column>
</Box>
) : null}
</Box>
</Row>
<Link
className={clsx(buttonMedium, styles.exploreCollection)}
to={`/nfts/collection/${collection.address}`}
style={{ textDecoration: 'none', backgroundColor: `${hovered ? vars.color.blue400 : vars.color.grey700}` }}
>
Explore collection
</Link>
</Column>
)
/* Carousel Progress indicators */

@ -62,7 +62,7 @@ export const collectionDetails = style([
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.md}px)`]: {
[`(min-width: ${breakpoints.lg}px)`]: {
width: '40%',
},
},
@ -106,6 +106,45 @@ export const carouselIndicator = sprinkles({
display: 'inline-block',
})
/* Activity Feed Styles */
export const activityRow = style([
sprinkles({
position: 'absolute',
alignItems: { sm: 'flex-start', lg: 'center' },
}),
{
transition: 'transform 0.4s ease',
},
])
export const activeRow = sprinkles({
backgroundColor: 'grey800',
})
export const timestamp = style([
sprinkles({
position: 'absolute',
fontSize: '12',
color: 'grey300',
right: { sm: 'unset', lg: '12' },
left: { sm: '64', lg: 'unset' },
top: { sm: '28', lg: 'unset' },
}),
])
export const marketplaceIcon = style([
sprinkles({
width: '16',
height: '16',
borderRadius: '4',
flexShrink: '0',
marginLeft: '8',
}),
{
verticalAlign: 'bottom',
},
])
/* Value Prop Styles */
export const valuePropWrap = style([
{

@ -1,3 +1,4 @@
export * from './ActivityFetcher'
export * from './AssetsFetcher'
export * from './CollectionPreviewFetcher'
export * from './CollectionStatsFetcher'

@ -48,6 +48,13 @@ export enum ActivityEventType {
Transfer = 'TRANSFER',
}
export enum ActivityEventTypeDisplay {
'LISTING' = 'Listed',
'SALE' = 'Sold',
'TRANSFER' = 'Transferred',
'CANCEL_LISTING' = 'Cancelled',
}
export enum OrderStatus {
VALID = 'VALID',
EXECUTED = 'EXECUTED',