feat: render blurred collection cover photo in the header (#5233)
* initial commit * feat: blurred header * chore: replace with helper * chore: cleanup * chore: different extension * chore: layout tweaks * chore: tweaks * chore: prevent weird text selection on double click * chore: wip for linear gradient/plain color light mode * feat: linear-gradient when image missing * chore: clean up post merge * feat: different opacity for dark/light mode * chore: fix paddings
This commit is contained in:
parent
779a699ff0
commit
78b6ef60ac
@ -2,23 +2,58 @@ import { useLoadCollectionQuery } from 'graphql/data/nft/Collection'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { fetchTrendingCollections } from 'nft/queries'
|
||||
import { TimePeriod } from 'nft/types'
|
||||
import { Suspense, useMemo } from 'react'
|
||||
import { calculateCardIndex } from 'nft/utils'
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
import { Carousel } from './Carousel'
|
||||
import { Carousel, LoadingCarousel } from './Carousel'
|
||||
import { CarouselCard, LoadingCarouselCard } from './CarouselCard'
|
||||
|
||||
const BannerContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 32px 16px 0 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const AbsoluteFill = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
opacity: ${({ theme }) => (theme.darkMode ? 0.4 : 0.2)};
|
||||
`
|
||||
|
||||
const BannerBackground = styled(AbsoluteFill)<{ backgroundImage: string }>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-image: ${(props) => `url(${props.backgroundImage})`};
|
||||
filter: blur(62px);
|
||||
`
|
||||
|
||||
const PlainBackground = styled(AbsoluteFill)`
|
||||
background: ${({ theme }) => `linear-gradient(${opacify(10, theme.userThemeColor)}, transparent)`};
|
||||
`
|
||||
|
||||
const BannerMainArea = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
margin-top: 24px;
|
||||
height: 100%;
|
||||
gap: 36px;
|
||||
max-width: 1200px;
|
||||
justify-content: space-between;
|
||||
z-index: 2;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
flex-direction: column;
|
||||
@ -37,8 +72,10 @@ const HeaderContainer = styled.div`
|
||||
line-height: 88px;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
padding-top: 40px;
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 32px;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||
@ -57,6 +94,7 @@ const HeaderContainer = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
`
|
||||
|
||||
@ -93,31 +131,49 @@ const Banner = () => {
|
||||
const collectionAddresses = useMemo(() => collections?.map(({ address }) => address), [collections])
|
||||
useLoadCollectionQuery(collectionAddresses)
|
||||
|
||||
const [activeCollectionIdx, setActiveCollectionIdx] = useState(0)
|
||||
const onToggleNextSlide = useCallback(
|
||||
(direction: number) => {
|
||||
if (!collections) return
|
||||
setActiveCollectionIdx((idx) => calculateCardIndex(idx + direction, collections.length))
|
||||
},
|
||||
[collections]
|
||||
)
|
||||
|
||||
const activeCollection = collections?.[activeCollectionIdx]
|
||||
|
||||
return (
|
||||
<BannerContainer>
|
||||
<HeaderContainer>
|
||||
Better prices. {!isMobile && <br />}
|
||||
More listings.
|
||||
</HeaderContainer>
|
||||
{collections ? (
|
||||
<Carousel>
|
||||
{collections.map((collection) => (
|
||||
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
|
||||
<CarouselCard
|
||||
key={collection.address}
|
||||
collection={collection}
|
||||
onClick={() => navigate(`/nfts/collection/${collection.address}`)}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<Carousel>
|
||||
{[...Array(TRENDING_COLLECTION_SIZE)].map((index) => (
|
||||
<LoadingCarouselCard key={'carouselCard' + index} />
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
{activeCollection ? (
|
||||
activeCollection.bannerImageUrl ? (
|
||||
<BannerBackground backgroundImage={activeCollection.bannerImageUrl} />
|
||||
) : (
|
||||
<PlainBackground />
|
||||
)
|
||||
) : null}
|
||||
<BannerMainArea>
|
||||
<HeaderContainer>
|
||||
Better prices. {!isMobile && <br />}
|
||||
More listings.
|
||||
</HeaderContainer>
|
||||
{collections ? (
|
||||
<Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}>
|
||||
{collections.map((collection) => (
|
||||
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
|
||||
<CarouselCard
|
||||
key={collection.address}
|
||||
collection={collection}
|
||||
onClick={() => navigate(`/nfts/collection/${collection.address}`)}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</Carousel>
|
||||
) : (
|
||||
<LoadingCarousel>
|
||||
<LoadingCarouselCard />
|
||||
</LoadingCarousel>
|
||||
)}
|
||||
</BannerMainArea>
|
||||
</BannerContainer>
|
||||
)
|
||||
}
|
||||
|
@ -1,60 +1,69 @@
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { ChevronLeftIcon } from 'nft/components/icons'
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'nft/components/icons'
|
||||
import { calculateCardIndex, calculateFirstCardIndex, calculateRank } from 'nft/utils'
|
||||
import { ReactNode, useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { a, useSprings } from 'react-spring'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
|
||||
const CarouselContainer = styled.div`
|
||||
display: flex;
|
||||
max-width: 592px;
|
||||
width: 100%;
|
||||
const MAX_CARD_WIDTH = 530
|
||||
|
||||
@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;
|
||||
const carouselHeightStyle = css`
|
||||
height: 315px;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
height: 296px;
|
||||
}
|
||||
`
|
||||
|
||||
const IconContainer = styled.div<{ right?: boolean }>`
|
||||
const carouselItemStyle = css`
|
||||
padding-top: 3px;
|
||||
padding-bottom: 32px;
|
||||
`
|
||||
|
||||
const CarouselContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const CarouselCardContainer = styled.div`
|
||||
${carouselHeightStyle}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: ${MAX_CARD_WIDTH}px;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const CarouselItemCard = styled(a.div)`
|
||||
${carouselHeightStyle}
|
||||
${carouselItemStyle}
|
||||
|
||||
display: flex;
|
||||
height: 280px;
|
||||
justify-content: center;
|
||||
padding: 3px 32px 32px 32px;
|
||||
|
||||
position: absolute;
|
||||
will-change: transform;
|
||||
`
|
||||
|
||||
const CarouselItemIcon = styled.div`
|
||||
${carouselHeightStyle}
|
||||
${carouselItemStyle}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
${({ right }) => (right ? 'transform: rotate(180deg)' : undefined)};
|
||||
user-select: none;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -62,16 +71,16 @@ const IconContainer = styled.div<{ right?: boolean }>`
|
||||
|
||||
interface CarouselProps {
|
||||
children: ReactNode[]
|
||||
activeIndex: number
|
||||
toggleNextSlide: (idx: number) => void
|
||||
}
|
||||
|
||||
const FIRST_CARD_OFFSET = 0
|
||||
const MAX_CARD_WIDTH = 512
|
||||
|
||||
export const Carousel = ({ children }: CarouselProps) => {
|
||||
export const Carousel = ({ children, activeIndex, toggleNextSlide }: 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) {
|
||||
@ -108,21 +117,18 @@ export const Carousel = ({ children }: CarouselProps) => {
|
||||
[idx, getPos, set, cardWidth, children.length]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
runSprings(index.current, 0)
|
||||
}, [runSprings])
|
||||
const direction = useRef(0)
|
||||
|
||||
const index = useRef(0)
|
||||
useEffect(() => {
|
||||
runSprings(activeIndex * cardWidth, direction.current)
|
||||
}, [activeIndex, cardWidth, runSprings])
|
||||
|
||||
const toggleSlide = useCallback(
|
||||
(next: -1 | 1) => {
|
||||
const offset = cardWidth * next
|
||||
index.current += offset
|
||||
|
||||
runSprings(index.current, next)
|
||||
toggleResetTimer()
|
||||
direction.current = next
|
||||
toggleNextSlide(next)
|
||||
},
|
||||
[runSprings, cardWidth]
|
||||
[toggleNextSlide]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -132,16 +138,16 @@ export const Carousel = ({ children }: CarouselProps) => {
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [toggleSlide, resetTimer])
|
||||
}, [toggleSlide, activeIndex])
|
||||
|
||||
return (
|
||||
<CarouselContainer>
|
||||
<IconContainer onClick={() => toggleSlide(-1)}>
|
||||
<CarouselItemIcon onClick={() => toggleSlide(-1)}>
|
||||
<ChevronLeftIcon width="16px" height="16px" />
|
||||
</IconContainer>
|
||||
</CarouselItemIcon>
|
||||
<CarouselCardContainer ref={carouselCardContainerRef}>
|
||||
{springs.map(({ x }, i) => (
|
||||
<CarouselCard
|
||||
<CarouselItemCard
|
||||
key={i}
|
||||
style={{
|
||||
width: cardWidth,
|
||||
@ -149,12 +155,18 @@ export const Carousel = ({ children }: CarouselProps) => {
|
||||
}}
|
||||
>
|
||||
{children[i]}
|
||||
</CarouselCard>
|
||||
</CarouselItemCard>
|
||||
))}
|
||||
</CarouselCardContainer>
|
||||
<IconContainer right onClick={() => toggleSlide(1)}>
|
||||
<ChevronLeftIcon width="16px" height="16px" />
|
||||
</IconContainer>
|
||||
<CarouselItemIcon onClick={() => toggleSlide(1)}>
|
||||
<ChevronRightIcon width="16px" height="16px" />
|
||||
</CarouselItemIcon>
|
||||
</CarouselContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingCarousel = ({ children }: { children: ReactNode }) => (
|
||||
<Carousel activeIndex={0} toggleNextSlide={() => undefined}>
|
||||
{[children]}
|
||||
</Carousel>
|
||||
)
|
||||
|
@ -22,6 +22,10 @@ const ExploreContainer = styled.div`
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 36px;
|
||||
`
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
|
@ -174,6 +174,8 @@ export const EllipsisIcon = (props: SVGProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ChevronRightIcon = (props: SVGProps) => <ChevronLeftIcon {...props} transform="rotate(180)" />
|
||||
|
||||
export const LightningBoltIcon = (props: SVGProps) => (
|
||||
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -9,7 +9,6 @@ const ExploreContainer = styled.div`
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
gap: 16px;
|
||||
|
Loading…
Reference in New Issue
Block a user