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 { useIsMobile } from 'nft/hooks'
|
||||||
import { fetchTrendingCollections } from 'nft/queries'
|
import { fetchTrendingCollections } from 'nft/queries'
|
||||||
import { TimePeriod } from 'nft/types'
|
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 { useQuery } from 'react-query'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components/macro'
|
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'
|
import { CarouselCard, LoadingCarouselCard } from './CarouselCard'
|
||||||
|
|
||||||
const BannerContainer = styled.div`
|
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;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 320px;
|
height: 100%;
|
||||||
margin-top: 24px;
|
|
||||||
gap: 36px;
|
gap: 36px;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -37,8 +72,10 @@ const HeaderContainer = styled.div`
|
|||||||
line-height: 88px;
|
line-height: 88px;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding-top: 40px;
|
align-self: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
color: ${({ theme }) => theme.textPrimary};
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||||
@ -57,6 +94,7 @@ const HeaderContainer = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -93,14 +131,33 @@ const Banner = () => {
|
|||||||
const collectionAddresses = useMemo(() => collections?.map(({ address }) => address), [collections])
|
const collectionAddresses = useMemo(() => collections?.map(({ address }) => address), [collections])
|
||||||
useLoadCollectionQuery(collectionAddresses)
|
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 (
|
return (
|
||||||
<BannerContainer>
|
<BannerContainer>
|
||||||
|
{activeCollection ? (
|
||||||
|
activeCollection.bannerImageUrl ? (
|
||||||
|
<BannerBackground backgroundImage={activeCollection.bannerImageUrl} />
|
||||||
|
) : (
|
||||||
|
<PlainBackground />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
<BannerMainArea>
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
Better prices. {!isMobile && <br />}
|
Better prices. {!isMobile && <br />}
|
||||||
More listings.
|
More listings.
|
||||||
</HeaderContainer>
|
</HeaderContainer>
|
||||||
{collections ? (
|
{collections ? (
|
||||||
<Carousel>
|
<Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}>
|
||||||
{collections.map((collection) => (
|
{collections.map((collection) => (
|
||||||
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
|
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
|
||||||
<CarouselCard
|
<CarouselCard
|
||||||
@ -112,12 +169,11 @@ const Banner = () => {
|
|||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
) : (
|
) : (
|
||||||
<Carousel>
|
<LoadingCarousel>
|
||||||
{[...Array(TRENDING_COLLECTION_SIZE)].map((index) => (
|
<LoadingCarouselCard />
|
||||||
<LoadingCarouselCard key={'carouselCard' + index} />
|
</LoadingCarousel>
|
||||||
))}
|
|
||||||
</Carousel>
|
|
||||||
)}
|
)}
|
||||||
|
</BannerMainArea>
|
||||||
</BannerContainer>
|
</BannerContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,69 @@
|
|||||||
import { useWindowSize } from 'hooks/useWindowSize'
|
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 { 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 { a, useSprings } from 'react-spring'
|
||||||
import styled from 'styled-components/macro'
|
import styled, { css } from 'styled-components/macro'
|
||||||
|
|
||||||
const CarouselContainer = styled.div`
|
const MAX_CARD_WIDTH = 530
|
||||||
display: flex;
|
|
||||||
max-width: 592px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
const carouselHeightStyle = css`
|
||||||
height: 320px;
|
height: 315px;
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
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`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||||
height: 296px;
|
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;
|
display: flex;
|
||||||
height: 280px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 3px 32px 32px 32px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
will-change: transform;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CarouselItemIcon = styled.div`
|
||||||
|
${carouselHeightStyle}
|
||||||
|
${carouselItemStyle}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 8px;
|
user-select: none;
|
||||||
${({ right }) => (right ? 'transform: rotate(180deg)' : undefined)};
|
|
||||||
color: ${({ theme }) => theme.textPrimary};
|
color: ${({ theme }) => theme.textPrimary};
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
opacity: ${({ theme }) => theme.opacity.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`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -62,16 +71,16 @@ const IconContainer = styled.div<{ right?: boolean }>`
|
|||||||
|
|
||||||
interface CarouselProps {
|
interface CarouselProps {
|
||||||
children: ReactNode[]
|
children: ReactNode[]
|
||||||
|
activeIndex: number
|
||||||
|
toggleNextSlide: (idx: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIRST_CARD_OFFSET = 0
|
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 { width } = useWindowSize()
|
||||||
const carouselCardContainerRef = useRef<HTMLDivElement>(null)
|
const carouselCardContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const [cardWidth, setCardWidth] = useState(MAX_CARD_WIDTH)
|
const [cardWidth, setCardWidth] = useState(MAX_CARD_WIDTH)
|
||||||
const [resetTimer, toggleResetTimer] = useReducer((state) => !state, false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (carouselCardContainerRef.current) {
|
if (carouselCardContainerRef.current) {
|
||||||
@ -108,21 +117,18 @@ export const Carousel = ({ children }: CarouselProps) => {
|
|||||||
[idx, getPos, set, cardWidth, children.length]
|
[idx, getPos, set, cardWidth, children.length]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const direction = useRef(0)
|
||||||
runSprings(index.current, 0)
|
|
||||||
}, [runSprings])
|
|
||||||
|
|
||||||
const index = useRef(0)
|
useEffect(() => {
|
||||||
|
runSprings(activeIndex * cardWidth, direction.current)
|
||||||
|
}, [activeIndex, cardWidth, runSprings])
|
||||||
|
|
||||||
const toggleSlide = useCallback(
|
const toggleSlide = useCallback(
|
||||||
(next: -1 | 1) => {
|
(next: -1 | 1) => {
|
||||||
const offset = cardWidth * next
|
direction.current = next
|
||||||
index.current += offset
|
toggleNextSlide(next)
|
||||||
|
|
||||||
runSprings(index.current, next)
|
|
||||||
toggleResetTimer()
|
|
||||||
},
|
},
|
||||||
[runSprings, cardWidth]
|
[toggleNextSlide]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -132,16 +138,16 @@ export const Carousel = ({ children }: CarouselProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [toggleSlide, resetTimer])
|
}, [toggleSlide, activeIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContainer>
|
<CarouselContainer>
|
||||||
<IconContainer onClick={() => toggleSlide(-1)}>
|
<CarouselItemIcon onClick={() => toggleSlide(-1)}>
|
||||||
<ChevronLeftIcon width="16px" height="16px" />
|
<ChevronLeftIcon width="16px" height="16px" />
|
||||||
</IconContainer>
|
</CarouselItemIcon>
|
||||||
<CarouselCardContainer ref={carouselCardContainerRef}>
|
<CarouselCardContainer ref={carouselCardContainerRef}>
|
||||||
{springs.map(({ x }, i) => (
|
{springs.map(({ x }, i) => (
|
||||||
<CarouselCard
|
<CarouselItemCard
|
||||||
key={i}
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
width: cardWidth,
|
width: cardWidth,
|
||||||
@ -149,12 +155,18 @@ export const Carousel = ({ children }: CarouselProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children[i]}
|
{children[i]}
|
||||||
</CarouselCard>
|
</CarouselItemCard>
|
||||||
))}
|
))}
|
||||||
</CarouselCardContainer>
|
</CarouselCardContainer>
|
||||||
<IconContainer right onClick={() => toggleSlide(1)}>
|
<CarouselItemIcon onClick={() => toggleSlide(1)}>
|
||||||
<ChevronLeftIcon width="16px" height="16px" />
|
<ChevronRightIcon width="16px" height="16px" />
|
||||||
</IconContainer>
|
</CarouselItemIcon>
|
||||||
</CarouselContainer>
|
</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;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-top: 36px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledHeader = styled.div`
|
const StyledHeader = styled.div`
|
||||||
|
@ -174,6 +174,8 @@ export const EllipsisIcon = (props: SVGProps) => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const ChevronRightIcon = (props: SVGProps) => <ChevronLeftIcon {...props} transform="rotate(180)" />
|
||||||
|
|
||||||
export const LightningBoltIcon = (props: SVGProps) => (
|
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">
|
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
@ -9,7 +9,6 @@ const ExploreContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
Loading…
Reference in New Issue
Block a user