Commit bab2f47a authored by aballerr's avatar aballerr Committed by GitHub

style: filters update (#4749)

* styles filter update
parent 03237255
......@@ -18,6 +18,7 @@ import {
useIsMobile,
} from 'nft/hooks'
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
import { usePriceRange } from 'nft/hooks/usePriceRange'
import { AssetsFetcher } from 'nft/queries'
import { DropDownOption, GenieCollection, UniformHeight, UniformHeights } from 'nft/types'
import { getRarityStatus } from 'nft/utils/asset'
......@@ -63,6 +64,13 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const setMarketCount = useCollectionFilters((state) => state.setMarketCount)
const setSortBy = useCollectionFilters((state) => state.setSortBy)
const buyNow = useCollectionFilters((state) => state.buyNow)
const setPriceRangeLow = usePriceRange((state) => state.setPriceRangeLow)
const priceRangeLow = usePriceRange((state) => state.priceRangeLow)
const priceRangeHigh = usePriceRange((state) => state.priceRangeHigh)
const setPriceRangeHigh = usePriceRange((state) => state.setPriceRangeHigh)
const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax)
const setIsCollectionNftsLoading = useIsCollectionLoading((state) => state.setIsCollectionNftsLoading)
const removeTrait = useCollectionFilters((state) => state.removeTrait)
const removeMarket = useCollectionFilters((state) => state.removeMarket)
......@@ -231,7 +239,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const minMaxPriceChipText: string | undefined = useMemo(() => {
if (debouncedMinPrice && debouncedMaxPrice) {
return `Price: ${debouncedMinPrice}-${debouncedMaxPrice} ETH`
return `Price: ${debouncedMinPrice} - ${debouncedMaxPrice} ETH`
} else if (debouncedMinPrice) {
return `Min. Price: ${debouncedMinPrice} ETH`
} else if (debouncedMaxPrice) {
......@@ -269,6 +277,21 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}
}, [collectionStats, location])
useEffect(() => {
if (collectionStats && collectionStats.floorPrice) {
const lowValue = collectionStats.floorPrice
const maxValue = 10 * collectionStats.floorPrice
if (priceRangeLow === '') {
setPriceRangeLow(lowValue?.toFixed(2))
}
if (priceRangeHigh === '') {
setPriceRangeHigh(maxValue.toFixed(2))
}
}
}, [collectionStats, priceRangeLow, priceRangeHigh, setPriceRangeHigh, setPriceRangeLow])
return (
<>
<AnimatedBox position="sticky" top="72" width="full" zIndex="3">
......@@ -320,6 +343,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
scrollToTop()
setMin('')
setMax('')
setPrevMinMax([0, 100])
}}
/>
)}
......@@ -327,6 +351,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
<ClearAllButton
onClick={() => {
reset()
setPrevMinMax([0, 100])
scrollToTop()
}}
>
......
......@@ -14,14 +14,15 @@ export const CollectionSearch = () => {
<Box
as="input"
borderColor={{ default: 'backgroundOutline', focus: 'genieBlue' }}
borderWidth="1px"
borderWidth="1.5px"
borderStyle="solid"
borderRadius="12"
padding="12"
backgroundColor="backgroundSurface"
width="332"
fontSize="16"
height="44"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
color={{ placeholder: 'textTertiary', default: 'textPrimary' }}
value={searchByNameText}
placeholder={iscollectionStatsLoading ? '' : 'Search by name'}
className={clsx(iscollectionStatsLoading && styles.filterButtonLoading)}
......
......@@ -5,7 +5,7 @@ export const container = style([
sprinkles({
overflow: 'auto',
height: 'viewHeight',
paddingTop: '24',
paddingTop: '4',
}),
{
width: '300px',
......@@ -26,57 +26,81 @@ export const container = style([
])
export const rowHover = style([
sprinkles({
borderRadius: '12',
}),
{
':hover': {
background: themeVars.colors.backgroundSurface,
background: themeVars.colors.backgroundInteractive,
borderRadius: 12,
},
},
])
export const rowHoverOpen = style([
export const row = style([
sprinkles({
borderTopLeftRadius: '12',
borderTopRightRadius: '12',
borderBottomLeftRadius: '0',
borderBottomRightRadius: '0',
display: 'flex',
paddingRight: '16',
cursor: 'pointer',
fontSize: '16',
lineHeight: '20',
justifyContent: 'space-between',
alignItems: 'center',
paddingLeft: '12',
paddingTop: '10',
paddingBottom: '10',
}),
{
':hover': {
background: themeVars.colors.backgroundOutline,
},
},
])
export const subRowHover = style({
':hover': {
background: themeVars.colors.backgroundOutline,
background: themeVars.colors.backgroundInteractive,
},
})
export const detailsOpen = sprinkles({
background: 'backgroundModule',
overflow: 'hidden',
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'backgroundOutline',
export const borderTop = sprinkles({
borderTopStyle: 'solid',
borderTopColor: 'backgroundOutline',
borderTopWidth: '1px',
})
export const summaryOpen = sprinkles({
borderStyle: 'solid',
borderWidth: '1px',
borderColor: 'backgroundOutline',
export const borderBottom = sprinkles({
borderBottomStyle: 'solid',
borderBottomColor: 'backgroundOutline',
borderBottomWidth: '1px',
})
export const detailsOpen = style([
borderTop,
sprinkles({
overflow: 'hidden',
marginTop: '8',
marginBottom: '8',
}),
])
export const filterDropDowns = style([
borderBottom,
sprinkles({
overflowY: 'scroll',
}),
{
maxHeight: '190px',
maxHeight: '302px',
'::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
},
])
export const chevronIcon = style({
marginLeft: -1,
})
export const chevronContainer = style([
sprinkles({
color: 'textSecondary',
display: 'inline-block',
height: '28',
width: '28',
transition: '250',
}),
{
marginRight: -1,
},
])
......@@ -3,77 +3,41 @@ import * as styles from 'nft/components/collection/Filters.css'
import { MarketplaceSelect } from 'nft/components/collection/MarketplaceSelect'
import { PriceRange } from 'nft/components/collection/PriceRange'
import { Column, Row } from 'nft/components/Flex'
import { Radio } from 'nft/components/layout/Radio'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { subhead } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks'
import { Trait } from 'nft/hooks/useCollectionFilters'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { groupBy } from 'nft/utils/groupBy'
import { FocusEventHandler, FormEvent, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useReducer } from 'react'
import { Input } from '../layout/Input'
import { TraitSelect } from './TraitSelect'
export const Filters = ({
traits,
traitsByAmount,
}: {
traits: Trait[]
traitsByAmount: {
traitCount: number
numWithTrait: number
}[]
}) => {
export const Filters = ({ traits }: { traits: Trait[] }) => {
const { buyNow, setBuyNow } = useCollectionFilters((state) => ({
buyNow: state.buyNow,
setBuyNow: state.setBuyNow,
}))
const traitsByGroup: Record<string, Trait[]> = useMemo(() => {
if (traits) {
let groupedTraits = groupBy(traits, 'trait_type')
groupedTraits['Number of traits'] = []
for (let i = 0; i < traitsByAmount.length; i++) {
groupedTraits['Number of traits'].push({
trait_type: 'Number of traits',
trait_value: traitsByAmount[i].traitCount,
trait_count: traitsByAmount[i].numWithTrait,
})
}
groupedTraits = Object.assign({ 'Number of traits': null }, groupedTraits)
return groupedTraits
} else return {}
}, [traits, traitsByAmount])
const traitsByGroup: Record<string, Trait[]> = useMemo(() => (traits ? groupBy(traits, 'trait_type') : {}), [traits])
const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const [search, setSearch] = useState('')
const handleBuyNowToggle = () => {
setBuyNow(!buyNow)
}
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = ''
}
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = 'Search traits'
}
return (
<Box className={styles.container}>
<Row width="full" justifyContent="space-between">
<Row as="span" fontSize="20" color="textPrimary">
Filters
</Row>
</Row>
<Column paddingTop="8">
<Row width="full" justifyContent="space-between"></Row>
<Column marginTop="8">
<Row
justifyContent="space-between"
className={styles.rowHover}
className={`${styles.row} ${styles.rowHover}`}
gap="2"
borderRadius="12"
paddingTop="12"
paddingRight="16"
paddingBottom="12"
paddingLeft="12"
cursor="pointer"
onClick={(e) => {
e.preventDefault()
handleBuyNowToggle()
......@@ -81,41 +45,30 @@ export const Filters = ({
onMouseEnter={toggleBuyNowHover}
onMouseLeave={toggleBuyNowHover}
>
<Box fontSize="14" fontWeight="medium" as="summary">
Buy now
</Box>
<Radio hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle} />
<Box className={subhead}>Buy now</Box>
<Checkbox hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle}>
<span />
</Checkbox>
</Row>
<MarketplaceSelect />
<Box marginTop="12" marginBottom="12">
<Box as="span" fontSize="20">
Price
</Box>
<PriceRange />
</Box>
<Box marginTop="12">
<Box as="span" fontSize="20">
Traits
</Box>
{Object.entries(traitsByGroup).length > 0 && (
<Box
as="span"
color="textSecondary"
paddingLeft="8"
marginTop="12"
marginBottom="12"
className={styles.borderTop}
></Box>
)}
<Column marginTop="12" marginBottom="60" gap={{ sm: '4' }}>
<Input
display={!traits?.length ? 'none' : undefined}
value={search}
onChange={(e: FormEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
width="full"
marginBottom="8"
placeholder="Search traits"
autoComplete="off"
onFocus={handleFocus}
onBlur={handleBlur}
style={{ border: '2px solid rgba(153, 161, 189, 0.24)', maxWidth: '300px' }}
/>
{Object.entries(traitsByGroup).map(([type, traits]) => (
<TraitSelect key={type} {...{ type, traits, search }} />
<Column>
{Object.entries(traitsByGroup).map(([type, traits], index) => (
// the index is offset by two because price range and marketplace appear prior to it
<TraitSelect key={type} {...{ type, traits }} index={index + TraitPosition.TRAIT_START_INDEX} />
))}
</Column>
</Box>
</Column>
</Box>
)
......
......@@ -5,6 +5,8 @@ import { Column, Row } from 'nft/components/Flex'
import { ChevronUpIcon } from 'nft/components/icons'
import { subheadSmall } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { FormEvent, useEffect, useReducer, useState } from 'react'
import { Checkbox } from '../layout/Checkbox'
......@@ -58,9 +60,11 @@ const MarketplaceItem = ({
fontWeight="normal"
className={`${subheadSmall} ${styles.subRowHover}`}
paddingLeft="12"
paddingRight="12"
paddingRight="16"
borderRadius="12"
cursor="pointer"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
maxHeight="44"
style={{ paddingBottom: '22px', paddingTop: '22px' }}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
onClick={handleCheckbox}
......@@ -91,45 +95,49 @@ export const MarketplaceSelect = () => {
}))
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
return (
<>
<Box className={styles.detailsOpen} opacity={isOpen ? '1' : '0'} />
<Box
as="details"
className={clsx(subheadSmall, !isOpen && styles.rowHover, isOpen && styles.detailsOpen)}
borderRadius="12"
className={clsx(subheadSmall, !isOpen && styles.rowHover)}
open={isOpen}
borderRadius={isOpen ? '0' : '12'}
>
<Box
as="summary"
className={clsx(isOpen && styles.summaryOpen, isOpen ? styles.rowHoverOpen : styles.rowHover)}
className={`${styles.row} ${styles.rowHover}`}
display="flex"
justifyContent="space-between"
cursor="pointer"
alignItems="center"
fontSize="14"
fontSize="16"
paddingTop="12"
paddingLeft="12"
paddingRight="12"
paddingBottom={isOpen ? '8' : '12'}
paddingBottom="12"
lineHeight="20"
borderRadius="12"
maxHeight="48"
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
setTraitsOpen(TraitPosition.MARKPLACE_INDEX, !isOpen)
}}
>
Marketplaces
<Box display="flex" alignItems="center">
<Box
color="textSecondary"
transition="250"
height="28"
width="28"
className={styles.chevronContainer}
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon />
<ChevronUpIcon className={styles.chevronIcon} />
</Box>
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingLeft="0">
<Column className={styles.filterDropDowns} paddingBottom="8" paddingLeft="0">
{Object.entries(marketPlaceItems).map(([value, title]) => (
<MarketplaceItem
key={value}
......@@ -141,5 +149,6 @@ export const MarketplaceSelect = () => {
))}
</Column>
</Box>
</>
)
}
import { style } from '@vanilla-extract/css'
import { body } from 'nft/css/common.css'
import { sprinkles, themeVars } from 'nft/css/sprinkles.css'
// https://www.npmjs.com/package/react-slider
// https://codesandbox.io/s/peaceful-pine-gqszx6?file=/src/styles.css:587-641
export const sliderLight = style([
sprinkles({
height: '8',
marginTop: '20',
borderRadius: '8',
width: 'full',
}),
])
export const sliderDark = style([
sliderLight,
sprinkles({
backgroundColor: 'accentAction',
}),
])
export const tracker = style([
sprinkles({
backgroundColor: 'accentAction',
height: '8',
}),
{
selectors: {
'&:nth-child(1)': {
backgroundColor: themeVars.colors.accentActionSoft,
borderRadius: 8,
opacity: 0.65,
},
'&:nth-child(3)': {
backgroundColor: themeVars.colors.accentActionSoft,
borderRadius: 8,
opacity: 0.65,
},
},
},
])
export const thumb = style([
sprinkles({
width: '12',
height: '20',
borderRadius: '4',
cursor: 'pointer',
boxShadow: 'shallow',
backgroundColor: 'grey50',
}),
{
top: -6,
},
])
export const priceInput = style([
body,
sprinkles({
backgroundColor: 'transparent',
padding: '12',
borderRadius: '12',
borderStyle: 'solid',
borderWidth: '1.5px',
}),
])
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { NumericInput } from 'nft/components/layout/Input'
import { body } from 'nft/css/common.css'
import { useIsMobile } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { usePriceRange } from 'nft/hooks/usePriceRange'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { scrollToTop } from 'nft/utils/scrollToTop'
import { useEffect, useState } from 'react'
import { FocusEventHandler, FormEvent } from 'react'
import { FormEvent, useEffect, useState } from 'react'
import { FocusEventHandler } from 'react'
import { useLocation } from 'react-router-dom'
import ReactSlider from 'react-slider'
import { useIsDarkMode } from 'state/user/hooks'
import * as styles from './PriceRange.css'
import { TraitsHeader } from './TraitsHeader'
export const PriceRange = () => {
const [placeholderText, setPlaceholderText] = useState('')
......@@ -13,14 +22,23 @@ export const PriceRange = () => {
const setMaxPrice = useCollectionFilters((state) => state.setMaxPrice)
const minPrice = useCollectionFilters((state) => state.minPrice)
const maxPrice = useCollectionFilters((state) => state.maxPrice)
const isMobile = useIsMobile()
const priceRangeLow = usePriceRange((state) => state.priceRangeLow)
const priceRangeHigh = usePriceRange((state) => state.priceRangeHigh)
const setPriceRangeLow = usePriceRange((statae) => statae.setPriceRangeLow)
const setPriceRangeHigh = usePriceRange((statae) => statae.setPriceRangeHigh)
const prevMinMax = usePriceRange((state) => state.prevMinMax)
const setPrevMinMax = usePriceRange((state) => state.setPrevMinMax)
const isDarktheme = useIsDarkMode()
const isMobile = useIsMobile()
const location = useLocation()
useEffect(() => {
setMinPrice('')
setMaxPrice('')
}, [location.pathname, setMinPrice, setMaxPrice])
setPriceRangeLow('')
setPriceRangeHigh('')
}, [location.pathname, setMinPrice, setMaxPrice, setPriceRangeLow, setPriceRangeHigh])
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
setPlaceholderText(e.currentTarget.placeholder)
......@@ -32,53 +50,127 @@ export const PriceRange = () => {
setPlaceholderText('')
}
const updateMinPriceRange = (v: FormEvent<HTMLInputElement>) => {
const [, prevMax] = prevMinMax
// if there is actually a number, update the slider place
if (v.currentTarget.value) {
// we are calculating the new slider position here
const diff = parseInt(v.currentTarget.value) - parseInt(priceRangeLow)
const newLow = Math.floor(100 * (diff / (parseInt(priceRangeHigh) - parseInt(priceRangeLow))))
// if the slider min value is larger than or equal to the max, we don't want it to move past the max
// so we put the sliders on top of each other
// if it is less than, we can move it
if (parseInt(v.currentTarget.value) >= parseInt(maxPrice)) {
setPrevMinMax([prevMax, prevMax])
} else {
setPrevMinMax([newLow, prevMax])
}
} else {
// if there is no number, reset the slider position
setPrevMinMax([0, prevMax])
}
// set min price for price range querying
setMinPrice(v.currentTarget.value)
scrollToTop()
}
const updateMaxPriceRange = (v: FormEvent<HTMLInputElement>) => {
const [prevMin] = prevMinMax
if (v.currentTarget.value) {
const range = parseInt(priceRangeHigh) - parseInt(v.currentTarget.value)
const newMax = Math.floor(100 - 100 * (range / (parseInt(priceRangeHigh) - parseInt(priceRangeLow))))
if (parseInt(v.currentTarget.value) <= parseInt(minPrice)) {
setPrevMinMax([prevMin, prevMin])
} else {
setPrevMinMax([prevMin, newMax])
}
} else {
setPrevMinMax([prevMin, 100])
}
setMaxPrice(v.currentTarget.value)
scrollToTop()
}
const handleSliderLogic = (minMax: Array<number>) => {
const [newMin, newMax] = minMax
// strip commas so parseFloat can parse properly
const priceRangeHighNumber = parseFloat(priceRangeHigh.replace(/,/g, ''))
const priceRangeLowNumber = parseFloat(priceRangeLow.replace(/,/g, ''))
const diff = priceRangeHighNumber - priceRangeLowNumber
// minprice
const minChange = newMin / 100
const newMinPrice = minChange * diff + priceRangeLowNumber
// max price
const maxChange = (100 - newMax) / 100
const newMaxPrice = priceRangeHighNumber - maxChange * diff
setMinPrice(newMinPrice.toFixed(2))
setMaxPrice(newMaxPrice.toFixed(2))
// set back to placeholder when they move back to end of range
if (newMin === 0) {
setMinPrice('')
}
if (newMax === 100) {
setMaxPrice('')
}
// update the previous minMax for future checks
setPrevMinMax(minMax)
}
return (
<TraitsHeader title="Price range" index={TraitPosition.PRICE_RANGE_INDEX}>
<Row gap="12" marginTop="12" color="textPrimary">
<Row position="relative" style={{ flex: 1 }}>
<Row position="relative">
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
}}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
backgroundColor="transparent"
placeholder="Min"
defaultValue={minPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMinPrice(v.currentTarget.value)
width: isMobile ? '100%' : '126px',
}}
className={styles.priceInput}
placeholder={priceRangeLow}
onChange={updateMinPriceRange}
onFocus={handleFocus}
value={minPrice}
onBlur={handleBlur}
/>
</Row>
<Row position="relative" style={{ flex: 1 }}>
<Box className={body}>to</Box>
<Row position="relative" flex="1">
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
width: isMobile ? '100%' : '126px',
}}
borderColor={{ default: 'backgroundOutline', focus: 'textSecondary' }}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
backgroundColor="transparent"
placeholder="Max"
defaultValue={maxPrice}
className={styles.priceInput}
placeholder={priceRangeHigh}
value={maxPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMaxPrice(v.currentTarget.value)
}}
onChange={updateMaxPriceRange}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Row>
</Row>
<Row marginBottom="20" paddingLeft="8" paddingRight="8">
<ReactSlider
disabled={!priceRangeLow || !priceRangeHigh}
defaultValue={[0, 100]}
value={prevMinMax}
className={isDarktheme ? styles.sliderDark : styles.sliderLight}
trackClassName={styles.tracker}
thumbClassName={styles.thumb}
onChange={handleSliderLogic}
/>
</Row>
</TraitsHeader>
)
}
......@@ -5,7 +5,7 @@ const TraitChipWrap = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 4px 8px 12px;
padding: 2px 4px 2px 12px;
font-weight: 600;
border-radius: 12px;
background-color: ${({ theme }) => theme.backgroundInteractive};
......@@ -24,7 +24,6 @@ export const TraitChip = ({ onClick, value }: { value: string; onClick: () => vo
return (
<TraitChipWrap>
<span>{value}</span>
<CrossIconWrap onClick={onClick}>
<CrossIcon cursor="pointer" />
</CrossIconWrap>
......
import clsx from 'clsx'
import useDebounce from 'hooks/useDebounce'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ChevronUpIcon } from 'nft/components/icons'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { subheadSmall } from 'nft/css/common.css'
import { Trait, useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { scrollToTop } from 'nft/utils/scrollToTop'
import { FormEvent, MouseEvent, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { FormEvent, MouseEvent, useEffect, useMemo, useState } from 'react'
import { Input } from '../layout/Input'
import * as styles from './Filters.css'
import { TraitsHeader } from './TraitsHeader'
const TraitItem = ({
trait,
......@@ -68,8 +68,13 @@ const TraitItem = ({
justifyContent="space-between"
cursor="pointer"
paddingLeft="12"
paddingRight="12"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
paddingRight="16"
borderRadius="12"
style={{
paddingBottom: '22px',
paddingTop: '22px',
}}
maxHeight="44"
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={handleCheckbox}
......@@ -79,6 +84,7 @@ const TraitItem = ({
whiteSpace="nowrap"
textOverflow="ellipsis"
overflow="hidden"
style={{ minHeight: 15 }}
maxWidth={!showFullTraitName ? '160' : 'full'}
onMouseOver={(e) => isEllipsisActive(e)}
onMouseLeave={() => toggleShowFullTraitName({ shouldShow: false, trait_type: '', trait_value: '' })}
......@@ -88,7 +94,7 @@ const TraitItem = ({
: trait.trait_value}
</Box>
<Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}>
<Box as="span" color="textSecondary" minWidth={'8'} paddingTop={'2'} paddingRight={'12'} position={'relative'}>
<Box as="span" color="textTertiary" minWidth="8" paddingTop="2" paddingRight="12" position="relative">
{!showFullTraitName && trait.trait_count}
</Box>
</Checkbox>
......@@ -96,83 +102,32 @@ const TraitItem = ({
)
}
export const TraitSelect = ({ traits, type, search }: { traits: Trait[]; type: string; search: string }) => {
const debouncedSearch = useDebounce(search, 300)
export const TraitSelect = ({ traits, type, index }: { traits: Trait[]; type: string; index: number }) => {
const addTrait = useCollectionFilters((state) => state.addTrait)
const removeTrait = useCollectionFilters((state) => state.removeTrait)
const selectedTraits = useCollectionFilters((state) => state.traits)
const [search, setSearch] = useState('')
const debouncedSearch = useDebounce(search, 300)
const [isOpen, setOpen] = useState(
traits.some(({ trait_type, trait_value }) => {
return selectedTraits.some((selectedTrait) => {
return selectedTrait.trait_type === trait_type && selectedTrait.trait_value === String(trait_value)
})
})
)
const { isTypeIncluded, searchedTraits } = useMemo(() => {
const isTypeIncluded = type.includes(debouncedSearch)
const searchedTraits = traits.filter(
(t) => isTypeIncluded || t.trait_value.toString().toLowerCase().includes(debouncedSearch.toLowerCase())
const searchedTraits = useMemo(
() => traits.filter((t) => t.trait_value.toString().toLowerCase().includes(debouncedSearch.toLowerCase())),
[debouncedSearch, traits]
)
return { searchedTraits, isTypeIncluded }
}, [debouncedSearch, traits, type])
useLayoutEffect(() => {
if (debouncedSearch && searchedTraits.length) {
setOpen(true)
return () => {
setOpen(false)
}
}
return
}, [searchedTraits, debouncedSearch, setOpen])
return searchedTraits.length || isTypeIncluded ? (
<Box
as="details"
className={clsx(subheadSmall, !isOpen && styles.rowHover, isOpen && styles.detailsOpen)}
borderRadius="12"
open={isOpen}
>
<Box
as="summary"
className={clsx(isOpen && styles.summaryOpen, isOpen ? styles.rowHoverOpen : styles.rowHover)}
display="flex"
paddingTop="8"
paddingRight="12"
paddingBottom="8"
paddingLeft="12"
justifyContent="space-between"
cursor="pointer"
alignItems="center"
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
>
{type}
<Box display="flex" alignItems="center">
<Box color="textSecondary" display="inline-block" marginRight="12">
{searchedTraits.length}
</Box>
<Box
color="textSecondary"
display="inline-block"
transition="250"
height="28"
width="28"
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon />
</Box>
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingLeft="0">
{searchedTraits.map((trait) => {
return traits.length ? (
<TraitsHeader index={index} numTraits={traits.length} title={type}>
<Input
value={search}
onChange={(e: FormEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
placeholder="Search"
marginTop="8"
marginBottom="8"
autoComplete="off"
position="static"
width="full"
/>
<Column className={styles.filterDropDowns} paddingLeft="0" paddingBottom="8">
{searchedTraits.map((trait, index) => {
const isTraitSelected = selectedTraits.find(
({ trait_type, trait_value }) =>
trait_type === trait.trait_type && String(trait_value) === String(trait.trait_value)
......@@ -187,6 +142,6 @@ export const TraitSelect = ({ traits, type, search }: { traits: Trait[]; type: s
)
})}
</Column>
</Box>
</TraitsHeader>
) : null
}
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
import { ChevronUpIcon } from 'nft/components/icons'
import { subheadSmall } from 'nft/css/common.css'
import { TraitPosition, useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { ReactNode, useEffect, useState } from 'react'
interface TraitsHeaderProps {
title: string
children: ReactNode
numTraits?: number
index?: number
}
export const TraitsHeader = (props: TraitsHeaderProps) => {
const { children, index, title } = props
const [isOpen, setOpen] = useState(false)
const traitsOpen = useTraitsOpen((state) => state.traitsOpen)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const prevTraitIsOpen = index !== undefined ? traitsOpen[index - 1] : false
const showBorderTop = index !== TraitPosition.TRAIT_START_INDEX
useEffect(() => {
if (index !== undefined) {
setTraitsOpen(index, isOpen)
}
}, [isOpen, index, setTraitsOpen])
return (
<>
{showBorderTop && (
<Box
className={clsx(subheadSmall, !isOpen && styles.rowHover, styles.detailsOpen)}
opacity={!prevTraitIsOpen && isOpen && index !== 0 ? '1' : '0'}
marginTop={prevTraitIsOpen ? '0' : '8'}
/>
)}
<Box as="details" className={clsx(subheadSmall, !isOpen && styles.rowHover)} open={isOpen}>
<Box
as="summary"
className={`${styles.row} ${styles.rowHover}`}
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
>
{title}
<Box display="flex" alignItems="center">
<Box color="textTertiary" display="inline-block" marginRight="12">
{props.numTraits}
</Box>
<Box
className={styles.chevronContainer}
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon className={styles.chevronIcon} />
</Box>
</Box>
</Box>
{children}
</Box>
</>
)
}
......@@ -6,6 +6,7 @@ import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/ic
import { buttonTextMedium } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useIsCollectionLoading } from 'nft/hooks'
import { useCollectionFilters } from 'nft/hooks'
import { DropDownOption } from 'nft/types'
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
......@@ -26,13 +27,18 @@ export const SortDropdown = ({
top?: number
left?: number
}) => {
const sortBy = useCollectionFilters((state) => state.sortBy)
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const [isReversed, toggleReversed] = useReducer((s) => !s, false)
const [selectedIndex, setSelectedIndex] = useState(0)
const [selectedIndex, setSelectedIndex] = useState(sortBy)
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
const [maxWidth, setMaxWidth] = useState(0)
useEffect(() => {
setSelectedIndex(sortBy)
}, [sortBy])
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, () => isOpen && toggleOpen())
......
......@@ -35,13 +35,13 @@ export const checkMark = sprinkles({
display: 'none',
height: '24',
width: '24',
color: 'blue400',
color: 'white',
})
export const checkMarkActive = style([
sprinkles({
display: 'inline-block',
color: 'blue400',
color: 'white',
position: 'absolute',
top: '0',
right: '1',
......
......@@ -24,8 +24,9 @@ export const Checkbox: React.FC<CheckboxProps> = ({ hovered, children, ...props
{children}
<Box
as="span"
borderColor={props.checked || hovered ? 'blue400' : 'grey400'}
borderColor={props.checked || hovered ? 'accentAction' : 'grey400'}
className={styles.checkbox}
background={props.checked ? 'accentAction' : undefined}
// This element is purely decorative so
// we hide it for screen readers
aria-hidden="true"
......
......@@ -28,6 +28,8 @@ export const NumericInput = forwardRef<HTMLInputElement, BoxProps>((props, ref)
as="input"
autoComplete="off"
type="text"
borderColor={{ default: 'backgroundOutline', focus: 'textSecondary' }}
color={{ placeholder: 'textSecondary', default: 'textPrimary' }}
onInput={(v: FormEvent<HTMLInputElement>) => {
if (v.currentTarget.value === '.') {
v.currentTarget.value = '0.'
......
......@@ -34,6 +34,7 @@ const themeContractValues = {
elevation: '',
tooltip: '',
deep: '',
shallow: '',
},
}
......@@ -77,6 +78,7 @@ const dimensions = {
'276': '276px',
'288': '288px',
'292': '292px',
'332': '332px',
'386': '386px',
half: '50%',
full: '100%',
......@@ -178,7 +180,7 @@ export const vars = createGlobalTheme(':root', {
grey300: '#99A1BD',
grey200: '#B7BED4',
grey100: '#DDE3F7',
grey50: '#EDEFF7',
grey50: '#F5F6FC',
accentTextLightTertiary: 'rgba(255, 255, 255, 0.12)',
outline: 'rgba(153, 161, 189, 0.24)',
lightGrayOverlay: '#99A1BD14',
......@@ -383,6 +385,7 @@ const unresponsiveProperties = defineProperties({
cursor: ['default', 'pointer', 'auto'],
borderStyle,
borderBottomStyle: borderStyle,
borderTopStyle: borderStyle,
borderRadius: vars.radii,
borderTopLeftRadius: vars.radii,
borderTopRightRadius: vars.radii,
......@@ -393,6 +396,7 @@ const unresponsiveProperties = defineProperties({
borderTop: vars.border,
borderWidth,
borderBottomWidth: borderWidth,
borderTopWidth: borderWidth,
fontFamily: vars.fonts,
overflow,
overflowX: overflow,
......
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface PriceRangeProps {
priceRangeLow: string
setPriceRangeLow: (priceRangeLow: string) => void
priceRangeHigh: string
setPriceRangeHigh: (priceRangeHigh: string) => void
prevMinMax: Array<number>
setPrevMinMax: (prevMinMax: Array<number>) => void
}
export const usePriceRange = create<PriceRangeProps>()(
devtools(
(set) => ({
priceRangeLow: '',
setPriceRangeLow: (priceRangeLow: string) => {
set(() => {
return { priceRangeLow }
})
},
priceRangeHigh: '',
setPriceRangeHigh: (priceRangeHigh: string) => {
set(() => {
return { priceRangeHigh }
})
},
prevMinMax: [0, 100],
setPrevMinMax: (prevMinMax: Array<number>) => {
set(() => {
return { prevMinMax }
})
},
}),
{ name: 'usePriceRange' }
)
)
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface traitOpen {
[key: number]: boolean
}
interface TraitsOpenState {
traitsOpen: traitOpen
setTraitsOpen: (index: number, isOpen: boolean) => void
}
export enum TraitPosition {
MARKPLACE_INDEX = 0,
PRICE_RANGE_INDEX = 1,
TRAIT_START_INDEX = 2,
}
export const useTraitsOpen = create<TraitsOpenState>()(
devtools(
(set) => ({
traitsOpen: {},
setTraitsOpen: (index, isOpen) => {
set(({ traitsOpen }) => ({ traitsOpen: { ...traitsOpen, [index]: isOpen } }))
},
}),
{ name: 'useTraitsOpen' }
)
)
......@@ -96,12 +96,7 @@ const Collection = () => {
</Column>
<Row alignItems="flex-start" position="relative" paddingX="48">
<Box position="sticky" top="72" width="0">
{isFiltersExpanded && (
<Filters
traitsByAmount={collectionStats?.numTraitsByAmount ?? []}
traits={collectionStats?.traits ?? []}
/>
)}
{isFiltersExpanded && <Filters traits={collectionStats?.traits ?? []} />}
</Box>
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
......
......@@ -32,5 +32,6 @@ export const darkTheme: Theme = {
elevation: '0px 4px 16px rgba(70, 115, 250, 0.4)',
tooltip: '0px 4px 16px rgba(255, 255, 255, 0.2)',
deep: '12px 16px 24px rgba(0, 0, 0, 0.24), 12px 8px 12px rgba(0, 0, 0, 0.24), 4px 4px 8px rgba(0, 0, 0, 0.32)',
shallow: '4px 4px 10px rgba(0, 0, 0, 0.24), 2px 2px 4px rgba(0, 0, 0, 0.12), 1px 2px 2px rgba(0, 0, 0, 0.12)',
},
}
......@@ -32,5 +32,6 @@ export const lightTheme: Theme = {
elevation: '0px 4px 16px rgba(70, 115, 250, 0.4)',
tooltip: '0px 4px 16px rgba(10, 10, 59, 0.2)',
deep: '8px 12px 20px rgba(51, 53, 72, 0.04), 4px 6px 12px rgba(51, 53, 72, 0.02), 4px 4px 8px rgba(51, 53, 72, 0.04)',
shallow: '4px 4px 10px rgba(0, 0, 0, 0.24), 2px 2px 4px rgba(0, 0, 0, 0.12), 1px 2px 2px rgba(0, 0, 0, 0.12)',
},
}
......@@ -3799,6 +3799,13 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-slider@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/react-slider/-/react-slider-1.3.1.tgz#a3816989eb4fc172e7df316930f242fec90690fc"
integrity sha512-4X2yK7RyCIy643YCFL+bc6XNmcnBtt8n88uuyihvcn5G7Lut23eNQU3q3KmwF7MWIfKfsW5NxCjw0SeDZRtgaA==
dependencies:
"@types/react" "*"
"@types/react-table@^7.7.12":
version "7.7.12"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.12.tgz#628011d3cb695b07c678704a61f2f1d5b8e567fd"
......@@ -14481,7 +14488,7 @@ prompts@2.4.0, prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
......@@ -15009,6 +15016,13 @@ react-scripts@^4.0.3:
optionalDependencies:
fsevents "^2.1.3"
react-slider@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-2.0.4.tgz#21c656ffabc3bb4481cf6b49e6d647baeda83572"
integrity sha512-sWwQD01n6v+MbeLCYthJGZPc0kzOyhQHyd0bSo0edg+IAxTVQmj3Oy4SBK65eX6gNwS9meUn6Z5sIBUVmwAd9g==
dependencies:
prop-types "^15.8.1"
react-spring@^9.5.5:
version "9.5.5"
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.5.5.tgz#314009a65efc04d0ef157d3d60590dbb9de65f3c"
......
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