Commit d5685156 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: mobile filters menu (#5163)

* header and scroll

* allow sweep buy now off

* generic filter row header

* begin building sort dropdown

* add file

* working checkmark

* remove icons

* updating scroll to work with mobile

* prevent scorlling behind menu

* hover styles

* remove console.log

* respond to comments

* revert null

* styled component header

* filter item styled component

* padding for items, slider, and inputs

* fixed scroll on mobile
Co-authored-by: default avatarAlex Ball <alex.ball@uniswap.org>
parent 5325b5f8
...@@ -33,13 +33,19 @@ const IconWrapper = styled.button` ...@@ -33,13 +33,19 @@ const IconWrapper = styled.button`
display: flex; display: flex;
padding: 2px; padding: 2px;
opacity: 1; opacity: 1;
transition: 125ms ease opacity; &:hover {
:hover { opacity: ${({ theme }) => theme.opacity.hover};
opacity: 0.6;
} }
:active {
opacity: 0.4; &:active {
opacity: ${({ theme }) => theme.opacity.click};
} }
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
` `
interface BagHeaderProps { interface BagHeaderProps {
numberOfAssets: number numberOfAssets: number
......
...@@ -18,10 +18,9 @@ import { CollectionAsset } from 'nft/components/collection/CollectionAsset' ...@@ -18,10 +18,9 @@ import { CollectionAsset } from 'nft/components/collection/CollectionAsset'
import * as styles from 'nft/components/collection/CollectionNfts.css' import * as styles from 'nft/components/collection/CollectionNfts.css'
import { SortDropdown } from 'nft/components/common/SortDropdown' import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Center, Column, Row } from 'nft/components/Flex' import { Center, Column, Row } from 'nft/components/Flex'
import { NonRarityIcon, RarityIcon, SweepIcon } from 'nft/components/icons' import { SweepIcon } from 'nft/components/icons'
import { bodySmall, buttonTextMedium, headlineMedium } from 'nft/css/common.css' import { bodySmall, buttonTextMedium, headlineMedium } from 'nft/css/common.css'
import { loadingAsset } from 'nft/css/loading.css' import { loadingAsset } from 'nft/css/loading.css'
import { vars } from 'nft/css/sprinkles.css'
import { import {
CollectionFilters, CollectionFilters,
initialCollectionFilterState, initialCollectionFilterState,
...@@ -57,8 +56,6 @@ interface CollectionNftsProps { ...@@ -57,8 +56,6 @@ interface CollectionNftsProps {
} }
const rarityStatusCache = new Map<string, boolean>() const rarityStatusCache = new Map<string, boolean>()
const nonRarityIcon = <NonRarityIcon width="20" height="20" viewBox="2 2 22 22" color={vars.color.blue400} />
const rarityIcon = <RarityIcon width="20" height="20" viewBox="2 2 24 24" color={vars.color.blue400} />
const ActionsContainer = styled.div` const ActionsContainer = styled.div`
display: flex; display: flex;
...@@ -199,6 +196,39 @@ export const CollectionNftsAndMenuLoading = () => ( ...@@ -199,6 +196,39 @@ export const CollectionNftsAndMenuLoading = () => (
</Column> </Column>
) )
export const getSortDropdownOptions = (setSortBy: (sortBy: SortBy) => void, hasRarity: boolean): DropDownOption[] => {
const options = [
{
displayText: 'Price: Low to High',
onClick: () => setSortBy(SortBy.LowToHigh),
reverseIndex: 2,
sortBy: SortBy.LowToHigh,
},
{
displayText: 'Price: High to Low',
onClick: () => setSortBy(SortBy.HighToLow),
reverseIndex: 1,
sortBy: SortBy.HighToLow,
},
]
return hasRarity
? options.concat([
{
displayText: 'Rarity: Rare to Common',
onClick: () => setSortBy(SortBy.RareToCommon),
reverseIndex: 4,
sortBy: SortBy.RareToCommon,
},
{
displayText: 'Rarity: Common to Rare',
onClick: () => setSortBy(SortBy.CommonToRare),
reverseIndex: 3,
sortBy: SortBy.CommonToRare,
},
])
: options
}
export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => { export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const traits = useCollectionFilters((state) => state.traits) const traits = useCollectionFilters((state) => state.traits)
...@@ -223,6 +253,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -223,6 +253,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const reset = useCollectionFilters((state) => state.reset) const reset = useCollectionFilters((state) => state.reset)
const setMin = useCollectionFilters((state) => state.setMinPrice) const setMin = useCollectionFilters((state) => state.setMinPrice)
const setMax = useCollectionFilters((state) => state.setMaxPrice) const setMax = useCollectionFilters((state) => state.setMaxPrice)
const setHasRarity = useCollectionFilters((state) => state.setHasRarity)
const toggleBag = useBag((state) => state.toggleBag) const toggleBag = useBag((state) => state.toggleBag)
const bagExpanded = useBag((state) => state.bagExpanded) const bagExpanded = useBag((state) => state.bagExpanded)
...@@ -272,51 +303,14 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -272,51 +303,14 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
setIsCollectionNftsLoading(isLoadingNext) setIsCollectionNftsLoading(isLoadingNext)
}, [isLoadingNext, setIsCollectionNftsLoading]) }, [isLoadingNext, setIsCollectionNftsLoading])
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts) const hasRarity = useMemo(() => {
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts) ?? false
setHasRarity(hasRarity)
return hasRarity
}, [collectionStats.address, collectionNfts, setHasRarity])
const sortDropDownOptions: DropDownOption[] = useMemo( const sortDropDownOptions: DropDownOption[] = useMemo(
() => () => getSortDropdownOptions(setSortBy, hasRarity),
hasRarity
? [
{
displayText: 'Low to High',
onClick: () => setSortBy(SortBy.LowToHigh),
icon: nonRarityIcon,
reverseIndex: 2,
},
{
displayText: 'High to Low',
onClick: () => setSortBy(SortBy.HighToLow),
icon: nonRarityIcon,
reverseIndex: 1,
},
{
displayText: 'Rare to Common',
onClick: () => setSortBy(SortBy.RareToCommon),
icon: rarityIcon,
reverseIndex: 4,
},
{
displayText: 'Common to Rare',
onClick: () => setSortBy(SortBy.CommonToRare),
icon: rarityIcon,
reverseIndex: 3,
},
]
: [
{
displayText: 'Low to High',
onClick: () => setSortBy(SortBy.LowToHigh),
icon: nonRarityIcon,
reverseIndex: 2,
},
{
displayText: 'High to Low',
onClick: () => setSortBy(SortBy.HighToLow),
icon: nonRarityIcon,
reverseIndex: 1,
},
],
[hasRarity, setSortBy] [hasRarity, setSortBy]
) )
...@@ -436,10 +430,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -436,10 +430,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
{!hasErc1155s ? ( {!hasErc1155s ? (
<SweepButton <SweepButton
toggled={sweepIsOpen} toggled={sweepIsOpen}
disabled={!buyNow} disabled={hasErc1155s}
className={buttonTextMedium} className={buttonTextMedium}
onClick={() => { onClick={() => {
if (!buyNow || hasErc1155s) return if (hasErc1155s) return
if (!sweepIsOpen) { if (!sweepIsOpen) {
scrollToTop() scrollToTop()
if (!bagExpanded && !isMobile) toggleBag() if (!bagExpanded && !isMobile) toggleBag()
......
...@@ -402,6 +402,7 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i ...@@ -402,6 +402,7 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i
{(stats.description || isCollectionStatsLoading) && isMobile && ( {(stats.description || isCollectionStatsLoading) && isMobile && (
<CollectionDescription description={stats.description ?? ''} /> <CollectionDescription description={stats.description ?? ''} />
)} )}
<div id="nft-anchor-mobile" />
<StatsRow isMobile display={{ sm: 'flex', md: 'none' }} stats={stats} marginTop="20" marginBottom="12" /> <StatsRow isMobile display={{ sm: 'flex', md: 'none' }} stats={stats} marginTop="20" marginBottom="12" />
</Box> </Box>
) )
......
...@@ -8,8 +8,12 @@ import { subhead } from 'nft/css/common.css' ...@@ -8,8 +8,12 @@ import { subhead } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks' import { useCollectionFilters } from 'nft/hooks'
import { Trait } from 'nft/hooks/useCollectionFilters' import { Trait } from 'nft/hooks/useCollectionFilters'
import { TraitPosition } from 'nft/hooks/useTraitsOpen' import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { useReducer } from 'react' import { DropDownOption } from 'nft/types'
import { useMemo, useReducer } from 'react'
import { isMobile } from 'utils/userAgent'
import { FilterSortDropdown } from '../common/SortDropdown'
import { getSortDropdownOptions } from './CollectionNfts'
import { TraitSelect } from './TraitSelect' import { TraitSelect } from './TraitSelect'
export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait[]> }) => { export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait[]> }) => {
...@@ -17,12 +21,19 @@ export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait ...@@ -17,12 +21,19 @@ export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait
buyNow: state.buyNow, buyNow: state.buyNow,
setBuyNow: state.setBuyNow, setBuyNow: state.setBuyNow,
})) }))
const setSortBy = useCollectionFilters((state) => state.setSortBy)
const hasRarity = useCollectionFilters((state) => state.hasRarity)
const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false) const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const handleBuyNowToggle = () => { const handleBuyNowToggle = () => {
setBuyNow(!buyNow) setBuyNow(!buyNow)
} }
const sortDropDownOptions: DropDownOption[] = useMemo(
() => getSortDropdownOptions(setSortBy, hasRarity ?? false),
[hasRarity, setSortBy]
)
return ( return (
<Box className={styles.container}> <Box className={styles.container}>
<Row width="full" justifyContent="space-between"></Row> <Row width="full" justifyContent="space-between"></Row>
...@@ -47,6 +58,7 @@ export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait ...@@ -47,6 +58,7 @@ export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait
<span /> <span />
</Checkbox> </Checkbox>
</Row> </Row>
{isMobile && <FilterSortDropdown sortDropDownOptions={sortDropDownOptions} />}
<MarketplaceSelect /> <MarketplaceSelect />
<PriceRange /> <PriceRange />
{Object.entries(traitsByGroup).length > 0 && ( {Object.entries(traitsByGroup).length > 0 && (
......
...@@ -9,9 +9,21 @@ import { subheadSmall } from 'nft/css/common.css' ...@@ -9,9 +9,21 @@ import { subheadSmall } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters' import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen' import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { FormEvent, useEffect, useMemo, useReducer, useState } from 'react' import { FormEvent, useEffect, useMemo, useReducer, useState } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Checkbox } from '../layout/Checkbox' import { Checkbox } from '../layout/Checkbox'
const FilterItemWrapper = styled(Row)`
justify-content: space-between;
padding: 10px 16px 10px 12px;
cursor: pointer;
border-radius: 12px;
&:hover {
background: ${({ theme }) => theme.backgroundInteractive};
}
`
export const MARKETPLACE_ITEMS = { export const MARKETPLACE_ITEMS = {
looksrare: 'LooksRare', looksrare: 'LooksRare',
nft20: 'NFT20', nft20: 'NFT20',
...@@ -22,6 +34,23 @@ export const MARKETPLACE_ITEMS = { ...@@ -22,6 +34,23 @@ export const MARKETPLACE_ITEMS = {
sudoswap: 'SudoSwap', sudoswap: 'SudoSwap',
} }
export const FilterItem = ({
title,
element,
onClick,
}: {
title: string
element: JSX.Element
onClick: React.MouseEventHandler<HTMLElement>
}) => {
return (
<FilterItemWrapper onClick={onClick}>
<ThemedText.BodyPrimary>{title}</ThemedText.BodyPrimary>
<ThemedText.SubHeaderSmall>{element}</ThemedText.SubHeaderSmall>
</FilterItemWrapper>
)
}
const MarketplaceItem = ({ const MarketplaceItem = ({
title, title,
value, value,
...@@ -54,67 +83,32 @@ const MarketplaceItem = ({ ...@@ -54,67 +83,32 @@ const MarketplaceItem = ({
sendAnalyticsEvent(EventName.NFT_FILTER_SELECTED, { filter_type: FilterTypes.MARKETPLACE }) sendAnalyticsEvent(EventName.NFT_FILTER_SELECTED, { filter_type: FilterTypes.MARKETPLACE })
} }
return ( const checkbox = (
<Row <Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}>
key={value} <Box as="span" color="textSecondary" marginLeft="4" paddingRight="12">
justifyContent="space-between" {count}
maxWidth="full"
overflowX={'hidden'}
overflowY={'hidden'}
fontWeight="normal"
className={`${subheadSmall} ${styles.subRowHover}`}
paddingLeft="12"
paddingRight="16"
borderRadius="12"
cursor="pointer"
maxHeight="44"
style={{ paddingBottom: '22px', paddingTop: '22px' }}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
onClick={handleCheckbox}
>
<Box as="span" fontSize="14" fontWeight="normal">
{title}{' '}
</Box> </Box>
<Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}> </Checkbox>
<Box as="span" color="textSecondary" marginLeft="4" paddingRight={'12'}>
{count}
</Box>
</Checkbox>
</Row>
) )
}
export const MarketplaceSelect = () => { return (
const { <div key={value} onMouseEnter={toggleHover} onMouseLeave={toggleHover}>
addMarket, <FilterItem title={title} element={checkbox} onClick={handleCheckbox} />
removeMarket, </div>
markets: selectedMarkets,
marketCount,
} = useCollectionFilters(({ markets, marketCount, removeMarket, addMarket }) => ({
markets,
marketCount,
removeMarket,
addMarket,
}))
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const MarketplaceItems = useMemo(
() =>
Object.entries(MARKETPLACE_ITEMS).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
)),
[addMarket, marketCount, removeMarket, selectedMarkets]
) )
}
export const FilterDropdown = ({
title,
items,
onClick,
isOpen,
}: {
title: string
items: JSX.Element[]
onClick: React.MouseEventHandler<HTMLElement>
isOpen: boolean
}) => {
return ( return (
<> <>
<Box className={styles.detailsOpen} opacity={isOpen ? '1' : '0'} /> <Box className={styles.detailsOpen} opacity={isOpen ? '1' : '0'} />
...@@ -137,13 +131,9 @@ export const MarketplaceSelect = () => { ...@@ -137,13 +131,9 @@ export const MarketplaceSelect = () => {
lineHeight="20" lineHeight="20"
borderRadius="12" borderRadius="12"
maxHeight="48" maxHeight="48"
onClick={(e) => { onClick={onClick}
e.preventDefault()
setOpen(!isOpen)
setTraitsOpen(TraitPosition.MARKPLACE_INDEX, !isOpen)
}}
> >
Marketplaces {title}
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
className={styles.chevronContainer} className={styles.chevronContainer}
...@@ -156,9 +146,48 @@ export const MarketplaceSelect = () => { ...@@ -156,9 +146,48 @@ export const MarketplaceSelect = () => {
</Box> </Box>
</Box> </Box>
<Column className={styles.filterDropDowns} paddingBottom="8" paddingLeft="0"> <Column className={styles.filterDropDowns} paddingBottom="8" paddingLeft="0">
{MarketplaceItems} {items}
</Column> </Column>
</Box> </Box>
</> </>
) )
} }
export const MarketplaceSelect = () => {
const {
addMarket,
removeMarket,
markets: selectedMarkets,
marketCount,
} = useCollectionFilters(({ markets, marketCount, removeMarket, addMarket }) => ({
markets,
marketCount,
removeMarket,
addMarket,
}))
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const MarketplaceItems = useMemo(
() =>
Object.entries(MARKETPLACE_ITEMS).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
)),
[addMarket, marketCount, removeMarket, selectedMarkets]
)
const onClick: React.MouseEventHandler<HTMLElement> = (e) => {
e.preventDefault()
setOpen(!isOpen)
setTraitsOpen(TraitPosition.MARKPLACE_INDEX, !isOpen)
}
return <FilterDropdown title={'Marketplaces'} items={MarketplaceItems} onClick={onClick} isOpen={isOpen} />
}
...@@ -6,14 +6,12 @@ import { Box } from 'nft/components/Box' ...@@ -6,14 +6,12 @@ import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex' import { Row } from 'nft/components/Flex'
import { NumericInput } from 'nft/components/layout/Input' import { NumericInput } from 'nft/components/layout/Input'
import { body } from 'nft/css/common.css' import { body } from 'nft/css/common.css'
import { useIsMobile } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters' import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { usePriceRange } from 'nft/hooks/usePriceRange' import { usePriceRange } from 'nft/hooks/usePriceRange'
import { TraitPosition } from 'nft/hooks/useTraitsOpen' import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { scrollToTop } from 'nft/utils/scrollToTop' import { scrollToTop } from 'nft/utils/scrollToTop'
import { default as Slider } from 'rc-slider' import { default as Slider } from 'rc-slider'
import { FormEvent, useEffect, useState } from 'react' import { FocusEventHandler, FormEvent, useEffect, useState } from 'react'
import { FocusEventHandler } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
...@@ -38,7 +36,6 @@ export const PriceRange = () => { ...@@ -38,7 +36,6 @@ export const PriceRange = () => {
const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax) const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax)
const theme = useTheme() const theme = useTheme()
const isMobile = useIsMobile()
const location = useLocation() const location = useLocation()
useEffect(() => { useEffect(() => {
...@@ -142,11 +139,11 @@ export const PriceRange = () => { ...@@ -142,11 +139,11 @@ export const PriceRange = () => {
return ( return (
<TraitsHeader title="Price range" index={TraitPosition.PRICE_RANGE_INDEX}> <TraitsHeader title="Price range" index={TraitPosition.PRICE_RANGE_INDEX}>
<Row gap="12" marginTop="12" color="textPrimary"> <Row marginTop="12" color="textPrimary" justifyContent="space-between">
<Row position="relative"> <Row position="relative">
<NumericInput <NumericInput
style={{ style={{
width: isMobile ? '100%' : '126px', width: '126px',
}} }}
className={styles.priceInput} className={styles.priceInput}
placeholder={priceRangeLow} placeholder={priceRangeLow}
...@@ -157,10 +154,10 @@ export const PriceRange = () => { ...@@ -157,10 +154,10 @@ export const PriceRange = () => {
/> />
</Row> </Row>
<Box className={body}>to</Box> <Box className={body}>to</Box>
<Row position="relative" flex="1"> <Row position="relative">
<NumericInput <NumericInput
style={{ style={{
width: isMobile ? '100%' : '126px', width: '126px',
}} }}
className={styles.priceInput} className={styles.priceInput}
placeholder={priceRangeHigh} placeholder={priceRangeHigh}
......
...@@ -3,8 +3,14 @@ import { Box } from 'nft/components/Box' ...@@ -3,8 +3,14 @@ import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css' import * as styles from 'nft/components/collection/Filters.css'
import { ChevronUpIcon } from 'nft/components/icons' import { ChevronUpIcon } from 'nft/components/icons'
import { subheadSmall } from 'nft/css/common.css' import { subheadSmall } from 'nft/css/common.css'
import { useIsMobile } from 'nft/hooks'
import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen' import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { ReactNode, useEffect, useState } from 'react' import { ReactNode, useEffect, useState } from 'react'
import styled from 'styled-components/macro'
const ChildreMobileWrapper = styled.div<{ isMobile: boolean }>`
padding: ${({ isMobile }) => (isMobile ? '0px 16px 0px 12px' : '0px')};
`
interface TraitsHeaderProps { interface TraitsHeaderProps {
title: string title: string
...@@ -18,6 +24,7 @@ export const TraitsHeader = (props: TraitsHeaderProps) => { ...@@ -18,6 +24,7 @@ export const TraitsHeader = (props: TraitsHeaderProps) => {
const [isOpen, setOpen] = useState(false) const [isOpen, setOpen] = useState(false)
const traitsOpen = useTraitsOpen((state) => state.traitsOpen) const traitsOpen = useTraitsOpen((state) => state.traitsOpen)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen) const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const isMobile = useIsMobile()
const prevTraitIsOpen = index !== undefined ? traitsOpen[index - 1] : false const prevTraitIsOpen = index !== undefined ? traitsOpen[index - 1] : false
const showBorderTop = index !== TraitPosition.TRAIT_START_INDEX const showBorderTop = index !== TraitPosition.TRAIT_START_INDEX
...@@ -63,7 +70,7 @@ export const TraitsHeader = (props: TraitsHeaderProps) => { ...@@ -63,7 +70,7 @@ export const TraitsHeader = (props: TraitsHeaderProps) => {
</Box> </Box>
</Box> </Box>
</Box> </Box>
{children} <ChildreMobileWrapper isMobile={isMobile}>{children}</ChildreMobileWrapper>
</Box> </Box>
</> </>
) )
......
import { Box } from 'nft/components/Box'
import { FilterDropdown, FilterItem } from 'nft/components/collection/MarketplaceSelect'
import { useCollectionFilters } from 'nft/hooks'
import { DropDownOption } from 'nft/types'
import { useState } from 'react'
export const FilterSortDropdown = ({ sortDropDownOptions }: { sortDropDownOptions: DropDownOption[] }) => {
const [isOpen, setOpen] = useState(false)
const onClick: React.MouseEventHandler<HTMLElement> = (e) => {
e.preventDefault()
setOpen(!isOpen)
}
const sortItems = sortDropDownOptions.map((option) => (
<SortByItem dropDownOption={option} parentOnClick={onClick} key={option.displayText} />
))
return <FilterDropdown title={'Sort by'} items={sortItems} onClick={onClick} isOpen={isOpen} />
}
const SortByItem = ({
dropDownOption,
parentOnClick,
}: {
dropDownOption: DropDownOption
parentOnClick: React.MouseEventHandler<HTMLElement>
}) => {
const sortBy = useCollectionFilters((state) => state.sortBy)
const checkMark =
dropDownOption.sortBy !== undefined && sortBy === dropDownOption.sortBy ? (
<Box
as="img"
alt={dropDownOption.displayText}
width="20"
height="20"
objectFit="cover"
src="/nft/svgs/checkmark.svg"
/>
) : (
<></>
)
const onClick: React.MouseEventHandler<HTMLElement> = (e) => {
e.preventDefault()
parentOnClick(e)
dropDownOption.onClick()
}
return <FilterItem title={dropDownOption.displayText} element={checkMark} onClick={onClick} />
}
...@@ -5,8 +5,7 @@ import { Row } from 'nft/components/Flex' ...@@ -5,8 +5,7 @@ import { Row } from 'nft/components/Flex'
import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/icons' import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/icons'
import { buttonTextMedium } from 'nft/css/common.css' import { buttonTextMedium } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useIsCollectionLoading } from 'nft/hooks' import { useCollectionFilters, useIsCollectionLoading } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks'
import { DropDownOption } from 'nft/types' import { DropDownOption } from 'nft/types'
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react' import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
...@@ -217,11 +216,6 @@ const DropDownItem = ({ ...@@ -217,11 +216,6 @@ const DropDownItem = ({
onClick={onClick} onClick={onClick}
cursor="pointer" cursor="pointer"
> >
{option.icon && (
<Box width="20" height="20">
{option.icon}
</Box>
)}
<Box marginLeft="8" className={buttonTextMedium}> <Box marginLeft="8" className={buttonTextMedium}>
{option.displayText} {option.displayText}
</Box> </Box>
......
export * from './FilterSortDropdown'
export * from './SortDropdown' export * from './SortDropdown'
...@@ -35,6 +35,7 @@ export type Trait = { ...@@ -35,6 +35,7 @@ export type Trait = {
interface State { interface State {
traits: Trait[] traits: Trait[]
hasRarity: boolean
markets: string[] markets: string[]
minPrice: string minPrice: string
maxPrice: string maxPrice: string
...@@ -48,6 +49,7 @@ interface State { ...@@ -48,6 +49,7 @@ interface State {
} }
type Actions = { type Actions = {
setHasRarity: (hasRarity: boolean) => void
setMarketCount: (_: Record<string, number>) => void setMarketCount: (_: Record<string, number>) => void
addMarket: (market: string) => void addMarket: (market: string) => void
removeMarket: (market: string) => void removeMarket: (market: string) => void
...@@ -72,6 +74,7 @@ export const initialCollectionFilterState: State = { ...@@ -72,6 +74,7 @@ export const initialCollectionFilterState: State = {
minRarity: '', minRarity: '',
maxRarity: '', maxRarity: '',
traits: [], traits: [],
hasRarity: false,
markets: [], markets: [],
marketCount: {}, marketCount: {},
buyNow: false, buyNow: false,
...@@ -84,6 +87,7 @@ export const useCollectionFilters = create<CollectionFilters>()( ...@@ -84,6 +87,7 @@ export const useCollectionFilters = create<CollectionFilters>()(
devtools( devtools(
(set) => ({ (set) => ({
...initialCollectionFilterState, ...initialCollectionFilterState,
setHasRarity: (hasRarity) => set({ hasRarity }),
setSortBy: (sortBy) => set({ sortBy }), setSortBy: (sortBy) => set({ sortBy }),
setSearch: (search) => set({ search }), setSearch: (search) => set({ search }),
setBuyNow: (buyNow) => set({ buyNow }), setBuyNow: (buyNow) => set({ buyNow }),
......
...@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' ...@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'
const isClient = typeof window !== 'undefined' const isClient = typeof window !== 'undefined'
function getIsMobile() { export function getIsMobile() {
return isClient ? window.innerWidth < breakpoints.sm : false return isClient ? window.innerWidth < breakpoints.sm : false
} }
......
...@@ -9,6 +9,7 @@ import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters } ...@@ -9,6 +9,7 @@ import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters }
import { CollectionNftsAndMenuLoading } from 'nft/components/collection/CollectionNfts' import { CollectionNftsAndMenuLoading } from 'nft/components/collection/CollectionNfts'
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton' import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { BagCloseIcon } from 'nft/components/icons'
import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks' import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css' import * as styles from 'nft/pages/collection/index.css'
import { GenieCollection } from 'nft/types' import { GenieCollection } from 'nft/types'
...@@ -16,6 +17,7 @@ import { Suspense, useEffect } from 'react' ...@@ -16,6 +17,7 @@ import { Suspense, useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useSpring } from 'react-spring' import { useSpring } from 'react-spring'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const FILTER_WIDTH = 332 const FILTER_WIDTH = 332
const BAG_WIDTH = 324 const BAG_WIDTH = 324
...@@ -26,12 +28,41 @@ const CollectionDescriptionSection = styled(Column)` ...@@ -26,12 +28,41 @@ const CollectionDescriptionSection = styled(Column)`
${styles.ScreenBreakpointsPaddings} ${styles.ScreenBreakpointsPaddings}
` `
const MobileFilterHeader = styled(Row)`
padding: 20px 16px;
justify-content: space-between;
`
const CollectionDisplaySection = styled(Row)` const CollectionDisplaySection = styled(Row)`
${styles.ScreenBreakpointsPaddings} ${styles.ScreenBreakpointsPaddings}
align-items: flex-start; align-items: flex-start;
position: relative; position: relative;
` `
const IconWrapper = styled.button`
background-color: transparent;
border-radius: 8px;
border: none;
color: ${({ theme }) => theme.textPrimary};
cursor: pointer;
display: flex;
padding: 2px 0px;
opacity: 1;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
&:active {
opacity: ${({ theme }) => theme.opacity.click};
}
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `opacity ${duration.medium} ${timing.ease}`};
`
const Collection = () => { const Collection = () => {
const { contractAddress } = useParams() const { contractAddress } = useParams()
const isMobile = useIsMobile() const isMobile = useIsMobile()
...@@ -46,14 +77,15 @@ const Collection = () => { ...@@ -46,14 +77,15 @@ const Collection = () => {
const collectionStats = useCollectionQuery(contractAddress as string) const collectionStats = useCollectionQuery(contractAddress as string)
const { gridX, gridWidthOffset } = useSpring({ const { gridX, gridWidthOffset } = useSpring({
gridX: isFiltersExpanded ? FILTER_WIDTH : 0, gridX: isFiltersExpanded && !isMobile ? FILTER_WIDTH : 0,
gridWidthOffset: isFiltersExpanded gridWidthOffset:
? isBagExpanded isFiltersExpanded && !isMobile
? BAG_WIDTH + FILTER_WIDTH ? isBagExpanded
: FILTER_WIDTH ? BAG_WIDTH + FILTER_WIDTH
: isBagExpanded : FILTER_WIDTH
? BAG_WIDTH : isBagExpanded
: 0, ? BAG_WIDTH
: 0,
}) })
useEffect(() => { useEffect(() => {
...@@ -111,12 +143,34 @@ const Collection = () => { ...@@ -111,12 +143,34 @@ const Collection = () => {
/> />
</CollectionDescriptionSection> </CollectionDescriptionSection>
<CollectionDisplaySection> <CollectionDisplaySection>
<Box position="sticky" top="72" width="0"> <Box
{isFiltersExpanded && <Filters traitsByGroup={collectionStats?.traits ?? {}} />} position={isMobile ? 'fixed' : 'sticky'}
top="0"
left="0"
width={isMobile ? 'full' : '0'}
height={isMobile && isFiltersExpanded ? 'full' : undefined}
background={isMobile ? 'backgroundBackdrop' : undefined}
zIndex={isMobile ? 'modalBackdrop' : undefined}
overflowY={isMobile ? 'scroll' : undefined}
>
{isFiltersExpanded && (
<>
{isMobile && (
<MobileFilterHeader>
<ThemedText.HeadlineSmall>Filter</ThemedText.HeadlineSmall>
<IconWrapper onClick={() => setFiltersExpanded(false)}>
<BagCloseIcon />
</IconWrapper>
</MobileFilterHeader>
)}
<Filters traitsByGroup={collectionStats?.traits ?? {}} />
</>
)}
</Box> </Box>
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */} {/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
<AnimatedBox <AnimatedBox
position={isMobile && isFiltersExpanded ? 'fixed' : 'static'}
style={{ style={{
transform: gridX.to((x) => `translate(${x as number}px)`), transform: gridX.to((x) => `translate(${x as number}px)`),
width: gridWidthOffset.to((x) => `calc(100% - ${x as number}px)`), width: gridWidthOffset.to((x) => `calc(100% - ${x as number}px)`),
......
import { SortBy } from 'nft/hooks'
import { SellOrder } from '../sell' import { SellOrder } from '../sell'
export interface OpenSeaCollection { export interface OpenSeaCollection {
...@@ -160,10 +162,10 @@ export enum ToolTipType { ...@@ -160,10 +162,10 @@ export enum ToolTipType {
// index starts at 1 for boolean reasons // index starts at 1 for boolean reasons
export interface DropDownOption { export interface DropDownOption {
displayText: string displayText: string
icon?: JSX.Element
onClick: () => void onClick: () => void
reverseIndex?: number reverseIndex?: number
reverseOnClick?: () => void reverseOnClick?: () => void
sortBy?: SortBy
} }
export enum DetailsOrigin { export enum DetailsOrigin {
......
import { getIsMobile } from 'nft/hooks'
export const scrollToTop = () => { export const scrollToTop = () => {
window.document.getElementById('nft-anchor')?.scrollIntoView({ const isMobile = getIsMobile()
const anchorElement = isMobile ? 'nft-anchor-mobile' : 'nft-anchor'
window.document.getElementById(anchorElement)?.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
}) })
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment