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:
Mike Grabowski 2022-11-16 16:58:18 +01:00 committed by GitHub
parent 779a699ff0
commit 78b6ef60ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 86 deletions

@ -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,14 +131,33 @@ 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>
{activeCollection ? (
activeCollection.bannerImageUrl ? (
<BannerBackground backgroundImage={activeCollection.bannerImageUrl} />
) : (
<PlainBackground />
)
) : null}
<BannerMainArea>
<HeaderContainer>
Better prices. {!isMobile && <br />}
More listings.
</HeaderContainer>
{collections ? (
<Carousel>
<Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}>
{collections.map((collection) => (
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
<CarouselCard
@ -112,12 +169,11 @@ const Banner = () => {
))}
</Carousel>
) : (
<Carousel>
{[...Array(TRENDING_COLLECTION_SIZE)].map((index) => (
<LoadingCarouselCard key={'carouselCard' + index} />
))}
</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;