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:
parent
8e955e9257
commit
e35f9e16a1
135
src/nft/components/explore/ActivityFeed.tsx
Normal file
135
src/nft/components/explore/ActivityFeed.tsx
Normal file
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user