feat: nft filter bar (#4617)

* initial filter window

* filters

* filter button

* adding all filters to filter check

* sorting exports

* reviewing old css

* change to const

* responding to comments

* removing isMobile

* fixing radio input

* refactoring radio

* refactoring radio

* reusing the same class

* removing unused props

* removing useless clsx

* removing scrollToTop
This commit is contained in:
Jack Short 2022-09-15 13:01:04 -04:00 committed by GitHub
parent ea0fe83d00
commit 80c1f0cdf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 349 additions and 5 deletions

@ -4,6 +4,7 @@ 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 { Center } from 'nft/components/Flex' import { Center } from 'nft/components/Flex'
import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css' import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks'
import { AssetsFetcher } from 'nft/queries' import { AssetsFetcher } from 'nft/queries'
import { useMemo } from 'react' import { useMemo } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
@ -14,6 +15,8 @@ interface CollectionNftsProps {
} }
export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => { export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
const buyNow = useCollectionFilters((state) => state.buyNow)
const { const {
data: collectionAssets, data: collectionAssets,
isSuccess: AssetsFetchSuccess, isSuccess: AssetsFetchSuccess,
@ -24,11 +27,13 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
'collectionNfts', 'collectionNfts',
{ {
contractAddress, contractAddress,
notForSale: !buyNow,
}, },
], ],
async ({ pageParam = 0 }) => { async ({ pageParam = 0 }) => {
return await AssetsFetcher({ return await AssetsFetcher({
contractAddress, contractAddress,
notForSale: !buyNow,
pageParam, pageParam,
}) })
}, },

@ -0,0 +1,23 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
export const filterButton = sprinkles({
backgroundColor: 'blue400',
color: 'explicitWhite',
})
export const filterButtonExpanded = style({
background: vars.color.lightGrayButton,
color: themeVars.colors.blackBlue,
})
export const filterBadge = style([
sprinkles({
position: 'absolute',
left: '18',
fontSize: '28',
}),
{
top: '-3px',
},
])

@ -0,0 +1,69 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/FilterButton.css'
import { Row } from 'nft/components/Flex'
import { FilterIcon } from 'nft/components/icons'
import { useCollectionFilters } from 'nft/hooks'
import { putCommas } from 'nft/utils/putCommas'
export const FilterButton = ({
onClick,
isMobile,
isFiltersExpanded,
results,
}: {
isMobile: boolean
isFiltersExpanded: boolean
results?: number
onClick: () => void
}) => {
const { minPrice, maxPrice, minRarity, maxRarity, traits, markets, buyNow } = useCollectionFilters((state) => ({
minPrice: state.minPrice,
maxPrice: state.maxPrice,
minRarity: state.minRarity,
maxRarity: state.maxRarity,
traits: state.traits,
markets: state.markets,
buyNow: state.buyNow,
}))
const showFilterBadge = minPrice || maxPrice || minRarity || maxRarity || traits.length || markets.length || buyNow
return (
<Box
className={clsx(styles.filterButton, !isFiltersExpanded && styles.filterButtonExpanded)}
borderRadius="12"
fontSize="16"
cursor="pointer"
position="relative"
onClick={onClick}
paddingTop="12"
paddingLeft="12"
paddingBottom="12"
paddingRight={isMobile ? '8' : '12'}
width={isMobile ? '44' : 'auto'}
height="44"
whiteSpace="nowrap"
>
{showFilterBadge && (
<Row className={styles.filterBadge} color={isFiltersExpanded ? 'grey700' : 'blue400'}>
</Row>
)}
<FilterIcon
style={{ marginBottom: '-4px', paddingRight: `${!isFiltersExpanded || showFilterBadge ? '6px' : '0px'}` }}
/>
{!isMobile && !isFiltersExpanded && 'Filter'}
{showFilterBadge && !isMobile ? (
<Box display="inline-block" position="relative">
{!isFiltersExpanded && (
<Box as="span" position="absolute" left="4" style={{ top: '5px', fontSize: '8px' }}>
</Box>
)}
<Box paddingLeft={!isFiltersExpanded ? '12' : '2'}>{results ? putCommas(results) : 0} results</Box>
</Box>
) : null}
</Box>
)
}

@ -0,0 +1,82 @@
import { style } from '@vanilla-extract/css'
import { breakpoints, sprinkles, themeVars } from 'nft/css/sprinkles.css'
export const container = style([
sprinkles({
overflow: 'auto',
height: 'viewHeight',
paddingTop: '24',
}),
{
width: '300px',
paddingBottom: '96px',
'@media': {
[`(max-width: ${breakpoints.sm - 1}px)`]: {
width: 'auto',
height: 'auto',
paddingBottom: '0px',
},
},
selectors: {
'&::-webkit-scrollbar': {
display: 'none',
},
},
},
])
export const rowHover = style([
sprinkles({
borderRadius: '12',
}),
{
':hover': {
background: themeVars.colors.lightGray,
},
},
])
export const rowHoverOpen = style([
sprinkles({
borderTopLeftRadius: '12',
borderTopRightRadius: '12',
borderBottomLeftRadius: '0',
borderBottomRightRadius: '0',
}),
{
':hover': {
background: themeVars.colors.medGray,
},
},
])
export const subRowHover = style({
':hover': {
background: themeVars.colors.medGray,
},
})
export const detailsOpen = sprinkles({
background: 'darkGray10',
overflow: 'hidden',
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'medGray',
})
export const summaryOpen = sprinkles({
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'medGray',
})
export const filterDropDowns = style([
sprinkles({
overflowY: 'scroll',
}),
{
maxHeight: '190px',
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])

@ -0,0 +1,51 @@
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
import { Column, Row } from 'nft/components/Flex'
import { Radio } from 'nft/components/layout/Radio'
import { useCollectionFilters } from 'nft/hooks'
import { useReducer } from 'react'
export const Filters = () => {
const { buyNow, setBuyNow } = useCollectionFilters((state) => ({
buyNow: state.buyNow,
setBuyNow: state.setBuyNow,
}))
const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const handleBuyNowToggle = () => {
setBuyNow(!buyNow)
}
return (
<Box className={styles.container}>
<Row width="full" justifyContent="space-between">
<Row as="span" fontSize="20" color="blackBlue">
Filters
</Row>
</Row>
<Column paddingTop="8">
<Row
justifyContent="space-between"
className={styles.rowHover}
gap="2"
paddingTop="12"
paddingRight="16"
paddingBottom="12"
paddingLeft="12"
cursor="pointer"
onClick={(e) => {
e.preventDefault()
handleBuyNowToggle()
}}
onMouseEnter={toggleBuyNowHover}
onMouseLeave={toggleBuyNowHover}
>
<Box fontSize="14" fontWeight="medium" as="summary">
Buy now
</Box>
<Radio hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle} />
</Row>
</Column>
</Box>
)
}

@ -0,0 +1,4 @@
export { CollectionNfts } from './CollectionNfts'
export { CollectionStats } from './CollectionStats'
export { FilterButton } from './FilterButton'
export { Filters } from './Filters'

@ -0,0 +1,53 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, vars } from 'nft/css/sprinkles.css'
export const radio = style([
sprinkles({
position: 'relative',
display: 'inline-block',
height: '24',
width: '24',
cursor: 'pointer',
background: 'transparent',
borderRadius: { default: 'round', before: 'round' },
borderStyle: 'solid',
borderWidth: '2px',
top: '0',
left: '0',
right: '0',
bottom: '0',
transition: {
default: '250',
before: '250',
},
}),
])
export const greyBorderRadio = style([
radio,
sprinkles({
borderColor: 'grey400',
}),
])
export const blueBorderRadio = style([
radio,
sprinkles({
borderColor: 'blue400',
}),
])
export const selectedRadio = style([
blueBorderRadio,
{
':before': {
position: 'absolute',
backgroundColor: vars.color.blue400,
content: '',
height: '14px',
width: '14px',
top: 3,
left: 3,
},
},
])

@ -0,0 +1,21 @@
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/layout/Radio.css'
import { MouseEvent } from 'react'
interface RadioProps {
hovered: boolean
checked: boolean
onClick: (e: MouseEvent) => void
}
export const Radio = ({ hovered, checked, onClick }: RadioProps) => {
return (
<Box
as="label"
className={checked ? styles.selectedRadio : hovered ? styles.blueBorderRadio : styles.greyBorderRadio}
onClick={onClick}
/>
)
}
Radio.displayName = 'Radio'

@ -1,22 +1,31 @@
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { CollectionNfts } from 'nft/components/collection/CollectionNfts' import { CollectionNfts, CollectionStats, FilterButton, Filters } from 'nft/components/collection'
import { CollectionStats } from 'nft/components/collection/CollectionStats'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { useIsMobile } from 'nft/hooks/useIsMobile' import { useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css' import * as styles from 'nft/pages/collection/index.css'
import { CollectionStatsFetcher } from 'nft/queries' import { CollectionStatsFetcher } from 'nft/queries'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useSpring } from 'react-spring/web'
const FILTER_WIDTH = 332
const Collection = () => { const Collection = () => {
const { contractAddress } = useParams() const { contractAddress } = useParams()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () => const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () =>
CollectionStatsFetcher(contractAddress as string) CollectionStatsFetcher(contractAddress as string)
) )
/// @reviewer these look the same now but will be diff later
const { gridX, gridWidthOffset } = useSpring({
gridX: isFiltersExpanded ? FILTER_WIDTH : 0,
gridWidthOffset: isFiltersExpanded ? FILTER_WIDTH : 0,
})
return ( return (
<Column width="full"> <Column width="full">
<Box width="full" height="160"> <Box width="full" height="160">
@ -34,8 +43,30 @@ const Collection = () => {
<CollectionStats stats={collectionStats} isMobile={isMobile} /> <CollectionStats stats={collectionStats} isMobile={isMobile} />
</Row> </Row>
)} )}
<Row alignItems="flex-start" position="relative" paddingLeft="32" paddingRight="32"> <Row alignItems="flex-start" position="relative" paddingX="48">
<AnimatedBox width="full"> <Box position="sticky" top="72" width="0">
{isFiltersExpanded && <Filters />}
</Box>
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
<AnimatedBox
style={{
transform: gridX.interpolate((x) => `translate(${x as number}px)`),
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x as number}px)`),
}}
>
<AnimatedBox position="sticky" top="72" width="full" zIndex="3">
<Box backgroundColor="white08" width="full" paddingBottom="8" style={{ backdropFilter: 'blur(24px)' }}>
<Row marginTop="12" gap="12">
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
/>
</Row>
</Box>
</AnimatedBox>
{contractAddress && <CollectionNfts contractAddress={contractAddress} />} {contractAddress && <CollectionNfts contractAddress={contractAddress} />}
</AnimatedBox> </AnimatedBox>
</Row> </Row>

5
src/nft/utils/index.ts Normal file

@ -0,0 +1,5 @@
export * from './buildActivityAsset'
export * from './buildSellObject'
export * from './calcPoolPrice'
export * from './currency'
export * from './listNfts'