Commit e35f9e16 authored by Greg Bugyis's avatar Greg Bugyis Committed by GitHub

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: default avatargbugyis <greg@bugyis.com>
parent 8e955e92
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}
/>
)
}
import clsx from 'clsx' import clsx from 'clsx'
import { useWindowSize } from 'hooks/useWindowSize'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Center, Column, Row } from 'nft/components/Flex' import { Center, Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons' import { VerifiedIcon } from 'nft/components/icons'
import { bodySmall, buttonMedium, header1 } from 'nft/css/common.css' import { bodySmall, buttonMedium, header1 } from 'nft/css/common.css'
import { vars } from 'nft/css/sprinkles.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 { TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency' import { formatEthPrice } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas' import { putCommas } from 'nft/utils/putCommas'
import { formatChange, toSignificant } from 'nft/utils/toSignificant' import { formatChange, toSignificant } from 'nft/utils/toSignificant'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useQuery } from 'react-query' import { QueryClient, useQuery } from 'react-query'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import ActivityFeed from './ActivityFeed'
import * as styles from './Explore.css' import * as styles from './Explore.css'
const queryClient = new QueryClient()
const Banner = () => { const Banner = () => {
/* Sets initially displayed collection to random number between 0 and 4 */ /* Sets initially displayed collection to random number between 0 and 4 */
const [current, setCurrent] = useState(Math.floor(Math.random() * 5)) const [current, setCurrent] = useState(Math.floor(Math.random() * 5))
const [hovered, setHover] = useState(false) const [hovered, setHover] = useState(false)
const { width: windowWidth } = useWindowSize()
const { data: collections } = useQuery( const { data: collections } = useQuery(
['trendingCollections'], ['trendingCollections'],
() => { () => {
...@@ -38,7 +44,11 @@ const Banner = () => { ...@@ -38,7 +44,11 @@ const Banner = () => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
if (collections) { if (collections) {
const nextCollectionIndex = (current + 1) % collections.length const nextCollectionIndex = (current + 1) % collections.length
const nextCollectionAddress = collections[nextCollectionIndex].address
setCurrent(nextCollectionIndex) setCurrent(nextCollectionIndex)
await queryClient.prefetchQuery(['collectionActivity', nextCollectionAddress], () =>
ActivityFetcher(nextCollectionAddress as string)
)
} }
}, 15_000) }, 15_000)
return () => { return () => {
...@@ -57,7 +67,11 @@ const Banner = () => { ...@@ -57,7 +67,11 @@ const Banner = () => {
style={{ backgroundImage: `url(${collections[current].bannerImageUrl})` }} style={{ backgroundImage: `url(${collections[current].bannerImageUrl})` }}
> >
<Box className={styles.bannerOverlay} width="full" /> <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} /> <CarouselProgress length={collections.length} currentIndex={current} setCurrent={setCurrent} />
</div> </div>
</Box> </Box>
...@@ -84,58 +98,56 @@ const CollectionDetails = ({ ...@@ -84,58 +98,56 @@ const CollectionDetails = ({
rank: number rank: number
hovered: boolean hovered: boolean
}) => ( }) => (
<Box as="section" className={styles.section} paddingTop="40"> <Column className={styles.collectionDetails} paddingTop="40">
<Column className={styles.collectionDetails} paddingTop="24"> <div className={styles.volumeRank}>#{rank} volume in 24hr</div>
<div className={styles.volumeRank}>#{rank} volume in 24hr</div> <Row>
<Row> <Box as="span" marginTop="16" className={clsx(header1, styles.collectionName)}>
<Box as="span" marginTop="16" className={clsx(header1, styles.collectionName)}> {collection.name}
{collection.name} </Box>
</Box> {collection.isVerified && (
{collection.isVerified && ( <Box as="span" marginTop="24">
<Box as="span" marginTop="24"> <VerifiedIcon height="32" width="32" />
<VerifiedIcon height="32" width="32" />
</Box>
)}
</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>
<Box> )}
{collection.floorChange ? ( </Row>
<Box as="span" color={collection.floorChange > 0 ? 'green200' : 'error'} marginLeft="4"> <Row className={bodySmall} marginTop="12" color="explicitWhite">
{collection.floorChange > 0 && '+'} <Box>
{formatChange(collection.floorChange)}% <Box as="span" color="darkGray" marginRight="4">
</Box> Floor:
) : null}
</Box> </Box>
<Box marginLeft="24" color="explicitWhite"> {collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH
<Box as="span" color="darkGray" marginRight="4"> </Box>
Volume: <Box>
{collection.floorChange ? (
<Box as="span" color={collection.floorChange > 0 ? 'green200' : 'error'} marginLeft="4">
{collection.floorChange > 0 && '+'}
{formatChange(collection.floorChange)}%
</Box> </Box>
{collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH ) : null}
</Box>
<Box marginLeft="24" color="explicitWhite">
<Box as="span" color="darkGray" marginRight="4">
Volume:
</Box> </Box>
<Box> {collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH
{collection.volumeChange ? ( </Box>
<Box as="span" color={collection.volumeChange > 0 ? 'green200' : 'error'} marginLeft="4"> <Box>
{collection.volumeChange > 0 && '+'} {collection.volumeChange ? (
{formatChange(collection.volumeChange)}% <Box as="span" color={collection.volumeChange > 0 ? 'green200' : 'error'} marginLeft="4">
</Box> {collection.volumeChange > 0 && '+'}
) : null} {formatChange(collection.volumeChange)}%
</Box> </Box>
</Row> ) : null}
<Link </Box>
className={clsx(buttonMedium, styles.exploreCollection)} </Row>
to={`/nfts/collection/${collection.address}`} <Link
style={{ textDecoration: 'none', backgroundColor: `${hovered ? vars.color.blue400 : vars.color.grey700}` }} className={clsx(buttonMedium, styles.exploreCollection)}
> to={`/nfts/collection/${collection.address}`}
Explore collection style={{ textDecoration: 'none', backgroundColor: `${hovered ? vars.color.blue400 : vars.color.grey700}` }}
</Link> >
</Column> Explore collection
</Box> </Link>
</Column>
) )
/* Carousel Progress indicators */ /* Carousel Progress indicators */
......
...@@ -62,7 +62,7 @@ export const collectionDetails = style([ ...@@ -62,7 +62,7 @@ export const collectionDetails = style([
}), }),
{ {
'@media': { '@media': {
[`screen and (min-width: ${breakpoints.md}px)`]: { [`(min-width: ${breakpoints.lg}px)`]: {
width: '40%', width: '40%',
}, },
}, },
...@@ -106,6 +106,45 @@ export const carouselIndicator = sprinkles({ ...@@ -106,6 +106,45 @@ export const carouselIndicator = sprinkles({
display: 'inline-block', 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 */ /* Value Prop Styles */
export const valuePropWrap = style([ export const valuePropWrap = style([
{ {
......
export * from './ActivityFetcher'
export * from './AssetsFetcher' export * from './AssetsFetcher'
export * from './CollectionPreviewFetcher' export * from './CollectionPreviewFetcher'
export * from './CollectionStatsFetcher' export * from './CollectionStatsFetcher'
......
...@@ -48,6 +48,13 @@ export enum ActivityEventType { ...@@ -48,6 +48,13 @@ export enum ActivityEventType {
Transfer = 'TRANSFER', Transfer = 'TRANSFER',
} }
export enum ActivityEventTypeDisplay {
'LISTING' = 'Listed',
'SALE' = 'Sold',
'TRANSFER' = 'Transferred',
'CANCEL_LISTING' = 'Cancelled',
}
export enum OrderStatus { export enum OrderStatus {
VALID = 'VALID', VALID = 'VALID',
EXECUTED = 'EXECUTED', EXECUTED = 'EXECUTED',
......
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