feat: explore carousel (#5042)
* feat: explore carousel * removing peeking cards * mobile carousel and pr changes * collectionStats fetching * total listings calc
This commit is contained in:
parent
734a15e350
commit
d4fb0913a4
@ -1,29 +1,72 @@
|
||||
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, headlineLarge } from 'nft/css/common.css'
|
||||
import { breakpoints, vars } 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 { QueryClient, useQuery } from 'react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { fetchTrendingCollections } from 'nft/queries'
|
||||
import { TimePeriod } from 'nft/types'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import ActivityFeed from './ActivityFeed'
|
||||
import * as styles from './Explore.css'
|
||||
import { Carousel } from './Carousel'
|
||||
import { CarouselCard, LoadingCarouselCard } from './CarouselCard'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const BannerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
margin-top: 24px;
|
||||
gap: 36px;
|
||||
max-width: 1200px;
|
||||
justify-content: space-between;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 14px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
max-width: 500px;
|
||||
font-weight: 500;
|
||||
font-size: 60px;
|
||||
line-height: 73px;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
padding-top: 40px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 100%), #fc72ff;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||
font-size: 48px;
|
||||
line-height: 67px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
font-size: 36px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 0px;
|
||||
}
|
||||
`
|
||||
const DEFAULT_TRENDING_COLLECTION_QUERY_AMOUNT = 5
|
||||
|
||||
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 navigate = useNavigate()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const { data: collections } = useQuery(
|
||||
['trendingCollections'],
|
||||
() => {
|
||||
@ -36,152 +79,31 @@ const Banner = () => {
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
/* Rotate through Top 5 Collections on 15 second interval */
|
||||
let stale = false
|
||||
if (hovered || stale) return
|
||||
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 () => {
|
||||
stale = true
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [current, collections, hovered])
|
||||
|
||||
return (
|
||||
<Box onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} cursor="pointer" width="full">
|
||||
{collections && collections[current] ? (
|
||||
<Link to={`/nfts/collection/${collections[current].address}`} style={{ textDecoration: 'none' }}>
|
||||
<Box style={{ height: '386px' }}>
|
||||
<div
|
||||
className={styles.bannerWrap}
|
||||
style={{ backgroundImage: `url(${collections[current].bannerImageUrl})` }}
|
||||
>
|
||||
<Box className={styles.bannerOverlay} width="full" />
|
||||
<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>
|
||||
</Link>
|
||||
<BannerContainer>
|
||||
<HeaderContainer>
|
||||
Best price. {!isMobile && <br />}
|
||||
Every listing.
|
||||
</HeaderContainer>
|
||||
{collections ? (
|
||||
<Carousel>
|
||||
{collections.map((collection) => (
|
||||
<CarouselCard
|
||||
key={collection.address}
|
||||
collection={collection}
|
||||
onClick={() => navigate(`/nfts/collection/${collection.address}`)}
|
||||
/>
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<>
|
||||
{/* TODO: Improve Loading State */}
|
||||
<p>Loading</p>
|
||||
</>
|
||||
<Carousel>
|
||||
{[...Array(DEFAULT_TRENDING_COLLECTION_QUERY_AMOUNT)].map((index) => (
|
||||
<LoadingCarouselCard key={'carouselCard' + index} />
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
</Box>
|
||||
</BannerContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Banner
|
||||
|
||||
/* Collection Details: displays collection stats within Banner */
|
||||
const CollectionDetails = ({
|
||||
collection,
|
||||
rank,
|
||||
hovered,
|
||||
}: {
|
||||
collection: TrendingCollection
|
||||
rank: number
|
||||
hovered: boolean
|
||||
}) => (
|
||||
<Column className={styles.collectionDetails} paddingTop="40">
|
||||
<div className={styles.volumeRank}>#{rank} volume in 24hr</div>
|
||||
<Row>
|
||||
<Box as="span" marginTop="16" className={clsx(headlineLarge, styles.collectionName)}>
|
||||
{collection.name}
|
||||
</Box>
|
||||
{collection.isVerified && (
|
||||
<Box as="span" marginTop="24">
|
||||
<VerifiedIcon height="32" width="32" />
|
||||
</Box>
|
||||
)}
|
||||
</Row>
|
||||
<Row className={bodySmall} marginTop="12" color="explicitWhite">
|
||||
<Box>
|
||||
<Box as="span" color="textSecondary" marginRight="4">
|
||||
Floor:
|
||||
</Box>
|
||||
{collection.floor ? formatEthPrice(collection.floor.toString()) : '--'} ETH
|
||||
</Box>
|
||||
<Box>
|
||||
{collection.floorChange ? (
|
||||
<Box as="span" color={collection.floorChange > 0 ? 'green200' : 'accentFailure'} marginLeft="4">
|
||||
{collection.floorChange > 0 && '+'}
|
||||
{formatChange(collection.floorChange)}%
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box marginLeft="24" color="explicitWhite">
|
||||
<Box as="span" color="textSecondary" marginRight="4">
|
||||
Volume:
|
||||
</Box>
|
||||
{collection.volume ? putCommas(+toSignificant(collection.volume.toString())) : '--'} ETH
|
||||
</Box>
|
||||
<Box>
|
||||
{collection.volumeChange ? (
|
||||
<Box as="span" color={collection.volumeChange > 0 ? 'green200' : 'accentFailure'} 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>
|
||||
)
|
||||
|
||||
/* Carousel Progress indicators */
|
||||
const CarouselProgress = ({
|
||||
length,
|
||||
currentIndex,
|
||||
setCurrent,
|
||||
}: {
|
||||
length: number
|
||||
currentIndex: number
|
||||
setCurrent: React.Dispatch<React.SetStateAction<number>>
|
||||
}) => (
|
||||
<Center marginTop="16">
|
||||
{Array(length)
|
||||
.fill(null)
|
||||
.map((value, carouselIndex) => (
|
||||
<Box
|
||||
cursor="pointer"
|
||||
paddingTop="16"
|
||||
paddingBottom="16"
|
||||
position="relative"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrent(carouselIndex)
|
||||
}}
|
||||
key={carouselIndex}
|
||||
>
|
||||
<Box
|
||||
as="span"
|
||||
className={styles.carouselIndicator}
|
||||
display="inline-block"
|
||||
backgroundColor={currentIndex === carouselIndex ? 'explicitWhite' : 'accentTextLightTertiary'}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Center>
|
||||
)
|
||||
|
160
src/nft/components/explore/Carousel.tsx
Normal file
160
src/nft/components/explore/Carousel.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { ChevronLeftIcon } from 'nft/components/icons'
|
||||
import { calculateCardIndex, calculateFirstCardIndex, calculateRank } from 'nft/utils'
|
||||
import { ReactNode, useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import { a, useSprings } from 'react-spring'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const CarouselContainer = styled.div`
|
||||
display: flex;
|
||||
max-width: 592px;
|
||||
width: 100%;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
height: 320px;
|
||||
}
|
||||
`
|
||||
|
||||
const CarouselCardContainer = styled.div`
|
||||
max-width: 512px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const CarouselCard = styled(a.div)`
|
||||
position: absolute;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
display: flex;
|
||||
top: 3px;
|
||||
height: 280px;
|
||||
will-change: transform;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
height: 296px;
|
||||
}
|
||||
`
|
||||
|
||||
const IconContainer = styled.div<{ right?: boolean }>`
|
||||
display: flex;
|
||||
height: 280px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
${({ right }) => (right ? 'transform: rotate(180deg)' : undefined)};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
height: 296px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
interface CarouselProps {
|
||||
children: ReactNode[]
|
||||
}
|
||||
|
||||
const FIRST_CARD_OFFSET = 0
|
||||
const MAX_CARD_WIDTH = 512
|
||||
|
||||
export const Carousel = ({ children }: CarouselProps) => {
|
||||
const { width } = useWindowSize()
|
||||
const carouselCardContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [cardWidth, setCardWidth] = useState(MAX_CARD_WIDTH)
|
||||
const [resetTimer, toggleResetTimer] = useReducer((state) => !state, false)
|
||||
|
||||
useEffect(() => {
|
||||
if (carouselCardContainerRef.current) {
|
||||
setCardWidth(Math.min(carouselCardContainerRef.current.offsetWidth, MAX_CARD_WIDTH))
|
||||
}
|
||||
}, [width])
|
||||
|
||||
const idx = useCallback((x: number, l = children.length) => calculateCardIndex(x, l), [children])
|
||||
const getPos = useCallback(
|
||||
(i: number, firstVis: number, firstVisIdx: number) => calculateFirstCardIndex(i, firstVis, firstVisIdx, idx),
|
||||
[idx]
|
||||
)
|
||||
const [springs, set] = useSprings(children.length, (i) => ({
|
||||
x: (i < children.length - 1 ? i : -1) * cardWidth + FIRST_CARD_OFFSET,
|
||||
}))
|
||||
const prev = useRef([0, 1])
|
||||
|
||||
const runSprings = useCallback(
|
||||
(y: number, vy: number) => {
|
||||
const firstVis = idx(Math.floor(y / cardWidth) % children.length)
|
||||
const firstVisIdx = vy < 0 ? children.length - 2 : 1
|
||||
set((i) => {
|
||||
const position = getPos(i, firstVis, firstVisIdx)
|
||||
const prevPosition = getPos(i, prev.current[0], prev.current[1])
|
||||
const rank = calculateRank(firstVis, firstVisIdx, position, children.length, y)
|
||||
return {
|
||||
x: (-y % (cardWidth * children.length)) + cardWidth * rank + FIRST_CARD_OFFSET,
|
||||
immediate: vy < 0 ? prevPosition > position : prevPosition < position,
|
||||
config: { tension: 250, friction: 30 },
|
||||
}
|
||||
})
|
||||
prev.current = [firstVis, firstVisIdx]
|
||||
},
|
||||
[idx, getPos, set, cardWidth, children.length]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
runSprings(index.current, 0)
|
||||
}, [runSprings])
|
||||
|
||||
const index = useRef(0)
|
||||
|
||||
const toggleSlide = useCallback(
|
||||
(next: -1 | 1) => {
|
||||
const offset = cardWidth * next
|
||||
index.current += offset
|
||||
|
||||
runSprings(index.current, next)
|
||||
toggleResetTimer()
|
||||
},
|
||||
[runSprings, cardWidth]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
toggleSlide(1)
|
||||
}, 7_000)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [toggleSlide, resetTimer])
|
||||
|
||||
return (
|
||||
<CarouselContainer>
|
||||
<IconContainer onClick={() => toggleSlide(-1)}>
|
||||
<ChevronLeftIcon width="16px" height="16px" />
|
||||
</IconContainer>
|
||||
<CarouselCardContainer ref={carouselCardContainerRef}>
|
||||
{springs.map(({ x }, i) => (
|
||||
<CarouselCard
|
||||
key={i}
|
||||
style={{
|
||||
width: cardWidth,
|
||||
x,
|
||||
}}
|
||||
>
|
||||
{children[i]}
|
||||
</CarouselCard>
|
||||
))}
|
||||
</CarouselCardContainer>
|
||||
<IconContainer right onClick={() => toggleSlide(1)}>
|
||||
<ChevronLeftIcon width="16px" height="16px" />
|
||||
</IconContainer>
|
||||
</CarouselContainer>
|
||||
)
|
||||
}
|
281
src/nft/components/explore/CarouselCard.tsx
Normal file
281
src/nft/components/explore/CarouselCard.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import { loadingAnimation } from 'components/Loader/styled'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { CollectionStatsFetcher } from 'nft/queries'
|
||||
import { Markets, TrendingCollection } from 'nft/types'
|
||||
import { formatWeiToDecimal } from 'nft/utils'
|
||||
import { useQuery } from 'react-query'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const CarouselCardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
:hover {
|
||||
outline: 3px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
}
|
||||
`
|
||||
|
||||
const CardHeaderContainer = styled.div<{ src: string }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 108px;
|
||||
padding-top: 32px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
background-image: ${({ src }) => `url(${src})`};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`
|
||||
|
||||
const LoadingCardHeaderContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 108px;
|
||||
padding-top: 32px;
|
||||
padding-bottom: 16px;
|
||||
padding-left: 28px;
|
||||
padding-right: 28px;
|
||||
animation: ${loadingAnimation} 1.5s infinite;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
${({ theme }) => theme.backgroundInteractive} 25%,
|
||||
${({ theme }) => theme.backgroundOutline} 50%,
|
||||
${({ theme }) => theme.backgroundInteractive} 75%
|
||||
);
|
||||
will-change: background-position;
|
||||
background-size: 400%;
|
||||
`
|
||||
|
||||
const CardHeaderRow = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const CardNameRow = styled.div`
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
const IconContainer = styled.div`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const CollectionNameContainer = styled.div`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const LoadingCollectionNameContainer = styled(LoadingBubble)`
|
||||
width: 50%;
|
||||
`
|
||||
|
||||
const HeaderOverlay = styled.div`
|
||||
position: absolute;
|
||||
height: 108px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%, rgba(0, 0, 0, 0.08));
|
||||
z-index: 0;
|
||||
`
|
||||
|
||||
const CollectionImage = styled.img`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
border: 2px solid ${({ theme }) => theme.accentTextLightPrimary};
|
||||
border-radius: 100px;
|
||||
`
|
||||
|
||||
const LoadingCollectionImage = styled.div`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 100px;
|
||||
animation: ${loadingAnimation} 1.5s infinite;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
${({ theme }) => theme.backgroundInteractive} 25%,
|
||||
${({ theme }) => theme.backgroundOutline} 50%,
|
||||
${({ theme }) => theme.backgroundInteractive} 75%
|
||||
);
|
||||
will-change: background-position;
|
||||
background-size: 400%;
|
||||
`
|
||||
|
||||
const CardBottomContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
row-gap: 16px;
|
||||
column-gap: 20px;
|
||||
padding-right: 28px;
|
||||
padding-left: 28px;
|
||||
padding-bottom: 20px;
|
||||
justify-content: space-between;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||
row-gap: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
color: ${({ theme }) => theme.userThemeColor};
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||
row-gap: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingTableElement = styled(LoadingBubble)`
|
||||
width: 50px;
|
||||
`
|
||||
|
||||
const TableElement = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
interface MarketplaceRowProps {
|
||||
marketplace: string
|
||||
floor?: string
|
||||
listings?: string
|
||||
}
|
||||
|
||||
export const MarketplaceRow = ({ marketplace, floor, listings }: MarketplaceRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<TableElement>{marketplace}</TableElement>
|
||||
<TableElement>{floor ?? '-'}</TableElement>
|
||||
<TableElement>{listings ?? '-'}</TableElement>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface CarouselCardProps {
|
||||
collection: TrendingCollection
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const MARKETS_TO_CHECK = [Markets.Opensea, Markets.X2Y2, Markets.LooksRare] as const
|
||||
const MARKETS_ENUM_TO_NAME = {
|
||||
[Markets.Opensea]: 'OpenSea',
|
||||
[Markets.X2Y2]: 'X2Y2',
|
||||
[Markets.LooksRare]: 'LooksRare',
|
||||
}
|
||||
|
||||
export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
|
||||
const { data: collectionStats, isLoading } = useQuery(
|
||||
['trendingCollectionStats', collection.address],
|
||||
() => CollectionStatsFetcher(collection.address),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<CarouselCardContainer onClick={onClick}>
|
||||
<CardHeaderContainer src={collection.bannerImageUrl}>
|
||||
<CardHeaderRow>
|
||||
<CollectionImage src={collection.imageUrl} />
|
||||
<CardNameRow>
|
||||
<CollectionNameContainer>
|
||||
<ThemedText.MediumHeader color={theme.accentTextLightPrimary} fontWeight="500" lineHeight="28px">
|
||||
{collection.name}
|
||||
</ThemedText.MediumHeader>
|
||||
</CollectionNameContainer>
|
||||
{collection.isVerified && (
|
||||
<IconContainer>
|
||||
<VerifiedIcon width="24px" height="24px" />
|
||||
</IconContainer>
|
||||
)}
|
||||
</CardNameRow>
|
||||
</CardHeaderRow>
|
||||
<HeaderOverlay />
|
||||
</CardHeaderContainer>
|
||||
<CardBottomContainer>
|
||||
{isLoading || !collectionStats ? (
|
||||
<LoadingTable />
|
||||
) : (
|
||||
<>
|
||||
<HeaderRow>Uniswap</HeaderRow>
|
||||
<HeaderRow>{formatWeiToDecimal(collection.floor.toString())} ETH Floor</HeaderRow>
|
||||
<HeaderRow>{collectionStats.marketplaceCount?.reduce((acc, cur) => acc + cur.count, 0)} Listings</HeaderRow>
|
||||
{MARKETS_TO_CHECK.map((market) => {
|
||||
const marketplace = collectionStats.marketplaceCount?.find(
|
||||
(marketplace) => marketplace.marketplace === market
|
||||
)
|
||||
return (
|
||||
<MarketplaceRow
|
||||
key={'trendingCollection' + collection.address}
|
||||
marketplace={MARKETS_ENUM_TO_NAME[market]}
|
||||
listings={marketplace?.count.toString()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</CardBottomContainer>
|
||||
</CarouselCardContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const DEFAULT_TABLE_ELEMENTS = 12
|
||||
|
||||
export const LoadingTable = () => {
|
||||
return (
|
||||
<>
|
||||
{[...Array(DEFAULT_TABLE_ELEMENTS)].map((index) => (
|
||||
<LoadingTableElement key={index} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingCarouselCard = () => {
|
||||
return (
|
||||
<CarouselCardContainer>
|
||||
<LoadingCardHeaderContainer>
|
||||
<CardHeaderRow>
|
||||
<LoadingCollectionImage />
|
||||
<LoadingCollectionNameContainer />
|
||||
</CardHeaderRow>
|
||||
<HeaderOverlay />
|
||||
</LoadingCardHeaderContainer>
|
||||
<CardBottomContainer>
|
||||
<LoadingTable />
|
||||
</CardBottomContainer>
|
||||
</CarouselCardContainer>
|
||||
)
|
||||
}
|
@ -2,13 +2,24 @@ import { PageName } from 'analytics/constants'
|
||||
import { Trace } from 'analytics/Trace'
|
||||
import Banner from 'nft/components/explore/Banner'
|
||||
import TrendingCollections from 'nft/components/explore/TrendingCollections'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const ExploreContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const NftExplore = () => {
|
||||
return (
|
||||
<>
|
||||
<Trace page={PageName.NFT_EXPLORE_PAGE} shouldLogImpression>
|
||||
<Banner />
|
||||
<TrendingCollections />
|
||||
<ExploreContainer>
|
||||
<Banner />
|
||||
<TrendingCollections />
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
</>
|
||||
)
|
||||
|
16
src/nft/utils/carousel.ts
Normal file
16
src/nft/utils/carousel.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const calculateCardIndex = (x: number, l: number) => {
|
||||
return (x < 0 ? x + l : x) % l
|
||||
}
|
||||
|
||||
export const calculateFirstCardIndex = (
|
||||
i: number,
|
||||
firstVis: number,
|
||||
firstVisIdx: number,
|
||||
idx: (x: number, l?: number) => number
|
||||
) => {
|
||||
return idx(i - firstVis + firstVisIdx)
|
||||
}
|
||||
|
||||
export const calculateRank = (firstVis: number, firstVisIdx: number, position: number, l: number, y: number) => {
|
||||
return firstVis - (y < 0 ? l : 0) + position - firstVisIdx + (y < 0 && firstVis === 0 ? l : 0)
|
||||
}
|
@ -2,6 +2,7 @@ export * from './asset'
|
||||
export * from './buildActivityAsset'
|
||||
export * from './buildSellObject'
|
||||
export * from './calcPoolPrice'
|
||||
export * from './carousel'
|
||||
export * from './currency'
|
||||
export * from './fetchPrice'
|
||||
export * from './isAudio'
|
||||
|
Loading…
Reference in New Issue
Block a user