From 78b6ef60acd4ca56deab66f6ac5af3257d748840 Mon Sep 17 00:00:00 2001 From: Mike Grabowski Date: Wed, 16 Nov 2022 16:58:18 +0100 Subject: [PATCH] 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 --- src/nft/components/explore/Banner.tsx | 112 ++++++++++++---- src/nft/components/explore/Carousel.tsx | 126 ++++++++++-------- .../explore/TrendingCollections.tsx | 4 + src/nft/components/icons.tsx | 2 + src/nft/pages/explore/index.tsx | 1 - 5 files changed, 159 insertions(+), 86 deletions(-) diff --git a/src/nft/components/explore/Banner.tsx b/src/nft/components/explore/Banner.tsx index 93141625ca..f0a6880857 100644 --- a/src/nft/components/explore/Banner.tsx +++ b/src/nft/components/explore/Banner.tsx @@ -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 ( - - Better prices. {!isMobile &&
} - More listings. -
- {collections ? ( - - {collections.map((collection) => ( - } key={collection.address}> - navigate(`/nfts/collection/${collection.address}`)} - /> - - ))} - - ) : ( - - {[...Array(TRENDING_COLLECTION_SIZE)].map((index) => ( - - ))} - - )} + {activeCollection ? ( + activeCollection.bannerImageUrl ? ( + + ) : ( + + ) + ) : null} + + + Better prices. {!isMobile &&
} + More listings. +
+ {collections ? ( + + {collections.map((collection) => ( + } key={collection.address}> + navigate(`/nfts/collection/${collection.address}`)} + /> + + ))} + + ) : ( + + + + )} +
) } diff --git a/src/nft/components/explore/Carousel.tsx b/src/nft/components/explore/Carousel.tsx index 70be5466f9..829f6cc2c4 100644 --- a/src/nft/components/explore/Carousel.tsx +++ b/src/nft/components/explore/Carousel.tsx @@ -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(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 ( - toggleSlide(-1)}> + toggleSlide(-1)}> - + {springs.map(({ x }, i) => ( - { }} > {children[i]} - + ))} - toggleSlide(1)}> - - + toggleSlide(1)}> + + ) } + +export const LoadingCarousel = ({ children }: { children: ReactNode }) => ( + undefined}> + {[children]} + +) diff --git a/src/nft/components/explore/TrendingCollections.tsx b/src/nft/components/explore/TrendingCollections.tsx index f1a701422f..0321994541 100644 --- a/src/nft/components/explore/TrendingCollections.tsx +++ b/src/nft/components/explore/TrendingCollections.tsx @@ -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` diff --git a/src/nft/components/icons.tsx b/src/nft/components/icons.tsx index 813ceacfb3..c207dd8dce 100644 --- a/src/nft/components/icons.tsx +++ b/src/nft/components/icons.tsx @@ -174,6 +174,8 @@ export const EllipsisIcon = (props: SVGProps) => ( ) +export const ChevronRightIcon = (props: SVGProps) => + export const LightningBoltIcon = (props: SVGProps) => ( `${theme.breakpoint.md}px`}) { gap: 16px;