From e35f9e16a1cde12d18fa665d85faa977040d060e Mon Sep 17 00:00:00 2001 From: Greg Bugyis Date: Wed, 21 Sep 2022 02:08:25 +0300 Subject: [PATCH] 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 --- src/nft/components/explore/ActivityFeed.tsx | 135 ++++++++++++++++++++ src/nft/components/explore/Banner.tsx | 116 +++++++++-------- src/nft/components/explore/Explore.css.ts | 41 +++++- src/nft/queries/genie/index.ts | 1 + src/nft/types/collection/collection.ts | 7 + 5 files changed, 247 insertions(+), 53 deletions(-) create mode 100644 src/nft/components/explore/ActivityFeed.tsx diff --git a/src/nft/components/explore/ActivityFeed.tsx b/src/nft/components/explore/ActivityFeed.tsx new file mode 100644 index 0000000000..d943606cb8 --- /dev/null +++ b/src/nft/components/explore/ActivityFeed.tsx @@ -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 ( + + {collectionActivity ? ( + + + { + e.preventDefault() + e.stopPropagation() + navigate(`/nfts/asset/${address}/${collectionActivity.events[current].tokenId}?origin=explore`) + }} + /> + + + {collectionActivity.events.map((activityEvent: ActivityEvent, index: number) => { + return ( + + ) + })} + + + ) : null} + + ) +} + +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 ( + { + e.preventDefault() + e.stopPropagation() + navigate(`/nft/asset/${event.collectionAddress}/${event.tokenId}?origin=explore`) + }} + > + + + {ActivityEventTypeDisplay[event.eventType]} + + for + + {formattedPrice} ETH + + + {event.eventTimestamp && isValidDate(event.eventTimestamp) && ( + + {getTimeDifference(event.eventTimestamp?.toString())} + {event.marketplace && } + + )} + + ) +} + +export default ActivityFeed + +const MarketplaceIcon = ({ marketplace }: { marketplace: Markets }) => { + return ( + + ) +} diff --git a/src/nft/components/explore/Banner.tsx b/src/nft/components/explore/Banner.tsx index b2e11a4a85..cce358f468 100644 --- a/src/nft/components/explore/Banner.tsx +++ b/src/nft/components/explore/Banner.tsx @@ -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})` }} > - + + + {windowWidth && windowWidth > breakpoints.lg && } + + @@ -84,58 +98,56 @@ const CollectionDetails = ({ rank: number hovered: boolean }) => ( - - -
#{rank} volume in 24hr
- - - {collection.name} + +
#{rank} volume in 24hr
+ + + {collection.name} + + {collection.isVerified && ( + + - {collection.isVerified && ( - - + )} + + + + + Floor: + + {collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH + + + {collection.floorChange ? ( + 0 ? 'green200' : 'error'} marginLeft="4"> + {collection.floorChange > 0 && '+'} + {formatChange(collection.floorChange)}% - )} - - - - - Floor: + ) : null} + + + + Volume: + + {collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH + + + {collection.volumeChange ? ( + 0 ? 'green200' : 'error'} marginLeft="4"> + {collection.volumeChange > 0 && '+'} + {formatChange(collection.volumeChange)}% - {collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH - - - {collection.floorChange ? ( - 0 ? 'green200' : 'error'} marginLeft="4"> - {collection.floorChange > 0 && '+'} - {formatChange(collection.floorChange)}% - - ) : null} - - - - Volume: - - {collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH - - - {collection.volumeChange ? ( - 0 ? 'green200' : 'error'} marginLeft="4"> - {collection.volumeChange > 0 && '+'} - {formatChange(collection.volumeChange)}% - - ) : null} - - - - Explore collection - -
-
+ ) : null} +
+ + + Explore collection + + ) /* Carousel Progress indicators */ diff --git a/src/nft/components/explore/Explore.css.ts b/src/nft/components/explore/Explore.css.ts index e0efd39f6a..ec1f99a9f8 100644 --- a/src/nft/components/explore/Explore.css.ts +++ b/src/nft/components/explore/Explore.css.ts @@ -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([ { diff --git a/src/nft/queries/genie/index.ts b/src/nft/queries/genie/index.ts index 303229a2a8..3d38aaf322 100644 --- a/src/nft/queries/genie/index.ts +++ b/src/nft/queries/genie/index.ts @@ -1,3 +1,4 @@ +export * from './ActivityFetcher' export * from './AssetsFetcher' export * from './CollectionPreviewFetcher' export * from './CollectionStatsFetcher' diff --git a/src/nft/types/collection/collection.ts b/src/nft/types/collection/collection.ts index 7890a7453d..2eda0dac50 100644 --- a/src/nft/types/collection/collection.ts +++ b/src/nft/types/collection/collection.ts @@ -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',