feat: Add sell page filters sidebar (#4630)

* working sell filters

* split filters into own file

* include new file

* fix eslint warnings

* update filter button param and fix rerender bug

* de morgon's law

* usecallback

* move max_padding

* extend htmlinputelement for checkbox

* styles cleanup

* simplify checkbox sprinkles

* add null check to collectionfilteritem

* remove x axis scrollbar on collections

* update fitlerbutton logic

* scrollbar width

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2022-09-16 10:55:26 -07:00 committed by GitHub
parent efaefe2e44
commit 49c5cbbf3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 864 additions and 39 deletions

@ -3,8 +3,9 @@ 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 { useCollectionFilters, useWalletCollections } from 'nft/hooks'
import { putCommas } from 'nft/utils/putCommas'
import { useLocation } from 'react-router-dom'
export const FilterButton = ({
onClick,
@ -26,8 +27,13 @@ export const FilterButton = ({
markets: state.markets,
buyNow: state.buyNow,
}))
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
const { pathname } = useLocation()
const isSellPage = pathname.startsWith('/nfts/sell')
const showFilterBadge = minPrice || maxPrice || minRarity || maxRarity || traits.length || markets.length || buyNow
const showFilterBadge = isSellPage
? collectionFilters.length > 0
: minPrice || maxPrice || minRarity || maxRarity || traits.length || markets.length || buyNow
return (
<Box
className={clsx(styles.filterButton, !isFiltersExpanded && styles.filterButtonExpanded)}

@ -12,19 +12,21 @@ const pathAnim = keyframes({
const pathAnimCommonProps = {
animationDirection: 'alternate',
animationTimingFunction: 'linear',
animation: `0.5s infinite ${pathAnim}`,
}
export const path = style({
selectors: {
'&:nth-child(1)': {
animation: `0.5s infinite ${pathAnim}`,
...pathAnimCommonProps,
},
'&:nth-child(2)': {
animation: `0.5s infinite ${pathAnim}`,
animationDelay: '0.1s',
...pathAnimCommonProps,
},
'&:nth-child(3)': {
animation: `0.5s infinite ${pathAnim}`,
animationDelay: '0.2s',
...pathAnimCommonProps,
},

@ -0,0 +1,9 @@
import { style } from '@vanilla-extract/css'
export const activeDropdown = style({
borderBottom: 'none',
})
export const activeDropDownItems = style({
borderTop: 'none',
})

@ -0,0 +1,240 @@
import clsx from 'clsx'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { ArrowsIcon, ChevronUpIcon, ReversedArrowsIcon } from 'nft/components/icons'
import { buttonTextMedium } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { DropDownOption } from 'nft/types'
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
import * as styles from './SortDropdown.css'
export const SortDropdown = ({
dropDownOptions,
inFilters,
mini,
miniPrompt,
top,
left,
}: {
dropDownOptions: DropDownOption[]
inFilters?: boolean
mini?: boolean
miniPrompt?: string
top?: number
left?: number
}) => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const [isReversed, toggleReversed] = useReducer((s) => !s, false)
const [selectedIndex, setSelectedIndex] = useState(0)
const [maxWidth, setMaxWidth] = useState(0)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, () => isOpen && toggleOpen())
useEffect(() => setMaxWidth(0), [dropDownOptions])
const reversable = useMemo(
() => dropDownOptions[selectedIndex].reverseOnClick || dropDownOptions[selectedIndex].reverseIndex,
[selectedIndex, dropDownOptions]
)
return (
<Box
ref={ref}
transition="250"
borderRadius="12"
borderBottomLeftRadius={isOpen ? '0' : undefined}
borderBottomRightRadius={isOpen ? '0' : undefined}
height="44"
style={{ width: inFilters ? 'full' : mini ? 'min' : maxWidth ? maxWidth : '300px' }}
>
<Box
as="button"
fontSize="14"
borderRadius="12"
borderStyle={isOpen && !mini ? 'solid' : 'none'}
background={mini ? 'none' : 'lightGray'}
borderColor="medGray"
borderWidth="1px"
borderBottomLeftRadius={isOpen ? '0' : undefined}
borderBottomRightRadius={isOpen ? '0' : undefined}
padding={inFilters ? '12' : mini ? '0' : '8'}
color="blackBlue"
whiteSpace="nowrap"
display="flex"
justifyContent="space-between"
alignItems="center"
width={inFilters ? 'full' : 'inherit'}
onClick={toggleOpen}
cursor="pointer"
className={clsx(isOpen && !mini && styles.activeDropdown)}
>
<Box display="flex" alignItems="center">
{!isOpen && reversable && (
<Row
onClick={(e) => {
e.stopPropagation()
if (dropDownOptions[selectedIndex].reverseOnClick) {
dropDownOptions[selectedIndex].reverseOnClick?.()
toggleReversed()
} else {
dropDownOptions[dropDownOptions[selectedIndex].reverseIndex ?? 1 - 1].onClick()
setSelectedIndex(dropDownOptions[selectedIndex].reverseIndex ?? 1 - 1)
}
}}
>
{dropDownOptions[selectedIndex].reverseOnClick && (isReversed ? <ArrowsIcon /> : <ReversedArrowsIcon />)}
{dropDownOptions[selectedIndex].reverseIndex &&
(selectedIndex > (dropDownOptions[selectedIndex].reverseIndex ?? 1) - 1 ? (
<ArrowsIcon />
) : (
<ReversedArrowsIcon />
))}
</Row>
)}
<Box
marginLeft={reversable ? '4' : '0'}
marginRight={mini ? '2' : '0'}
color="blackBlue"
className={buttonTextMedium}
>
{mini ? miniPrompt : isOpen ? 'Sort by' : dropDownOptions[selectedIndex].displayText}
</Box>
</Box>
<ChevronUpIcon
secondaryColor={mini ? themeVars.colors.blackBlue : undefined}
secondaryWidth={mini ? '20' : undefined}
secondaryHeight={mini ? '20' : undefined}
style={{
transform: isOpen ? '' : 'rotate(180deg)',
}}
/>
</Box>
<Box
position="absolute"
zIndex="2"
width={inFilters ? 'auto' : 'inherit'}
right={inFilters ? '16' : 'auto'}
paddingBottom="8"
fontSize="14"
background="lightGray"
borderStyle="solid"
borderColor="medGray"
borderWidth="1px"
borderRadius="8"
borderTopLeftRadius={mini ? undefined : '0'}
borderTopRightRadius={mini ? undefined : '0'}
overflowY="hidden"
transition="250"
display={isOpen || !maxWidth ? 'block' : 'none'}
visibility={maxWidth ? 'visible' : 'hidden'}
marginTop={mini ? '12' : '0'}
className={clsx(!mini && styles.activeDropDownItems)}
style={{
top: top ? `${top}px` : 'inherit',
left: inFilters ? '16px' : left ? `${left}px` : 'inherit',
}}
>
{!maxWidth
? [
dropDownOptions.reduce((acc, curr) => {
return curr.displayText.length >= acc.displayText.length ? curr : acc
}, dropDownOptions[0]),
].map((option, index) => {
return <LargestItem key={index} option={option} index={index} setMaxWidth={setMaxWidth} />
})
: isOpen &&
dropDownOptions.map((option, index) => {
return (
<DropDownItem
key={index}
option={option}
index={index}
mini={mini}
onClick={() => {
dropDownOptions[index].onClick()
setSelectedIndex(index)
toggleOpen()
isReversed && toggleReversed()
}}
/>
)
})}
</Box>
</Box>
)
}
const DropDownItem = ({
option,
index,
onClick,
mini,
}: {
option: DropDownOption
index: number
onClick?: () => void
mini?: boolean
}) => {
return (
<Box
as="button"
border="none"
key={index}
display="flex"
alignItems="center"
paddingTop="10"
paddingBottom="10"
paddingLeft="12"
paddingRight={mini ? '20' : '0'}
width="full"
background={{
default: 'lightGray',
hover: 'lightGrayButton',
}}
color="blackBlue"
onClick={onClick}
cursor="pointer"
>
{option.icon && (
<Box width="28" height="28">
{option.icon}
</Box>
)}
<Box marginLeft="8" className={buttonTextMedium}>
{option.displayText}
</Box>
</Box>
)
}
const MAX_PADDING = 52
const LargestItem = ({
option,
index,
setMaxWidth,
}: {
option: DropDownOption
index: number
setMaxWidth: (width: number) => void
}) => {
const maxWidthRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (maxWidthRef && maxWidthRef.current) {
setMaxWidth(Math.ceil(maxWidthRef.current.getBoundingClientRect().width) + MAX_PADDING)
}
})
return (
<Box key={index} position="absolute" ref={maxWidthRef}>
<DropDownItem option={option} index={index} />
</Box>
)
}

@ -0,0 +1 @@
export * from './SortDropdown'

@ -0,0 +1,49 @@
import { style } from '@vanilla-extract/css'
import { sprinkles } from 'nft/css/sprinkles.css'
export const input = style([
sprinkles({ position: 'absolute' }),
{
top: '-24px',
selectors: {
'&[type="checkbox"]': {
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
overflow: 'hidden',
position: 'absolute',
whiteSpace: 'nowrap',
width: '1px',
},
},
},
])
export const checkbox = style([
sprinkles({
display: 'inline-block',
marginRight: '1',
borderRadius: '4',
height: '24',
width: '24',
borderStyle: 'solid',
borderWidth: '2px',
}),
])
export const checkMark = sprinkles({
display: 'none',
height: '24',
width: '24',
color: 'blue400',
})
export const checkMarkActive = style([
sprinkles({
display: 'inline-block',
color: 'blue400',
position: 'absolute',
top: '0',
right: '1',
}),
])

@ -0,0 +1,37 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import { ApprovedCheckmarkIcon } from 'nft/components/icons'
import React from 'react'
import * as styles from './Checkbox.css'
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
hovered: boolean
children: React.ReactNode
}
export const Checkbox: React.FC<CheckboxProps> = ({ hovered, children, ...props }: CheckboxProps) => {
return (
<Box
as="label"
display="flex"
alignItems="center"
position="relative"
overflow="hidden"
cursor="pointer"
lineHeight="1"
>
{children}
<Box
as="span"
borderColor={props.checked || hovered ? 'blue400' : 'grey400'}
className={styles.checkbox}
// This element is purely decorative so
// we hide it for screen readers
aria-hidden="true"
/>
<input {...props} className={styles.input} type="checkbox" />
<ApprovedCheckmarkIcon className={clsx(styles.checkMark, props.checked && styles.checkMarkActive)} />
</Box>
)
}

@ -0,0 +1,239 @@
import { AnimatedBox, Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { XMarkIcon } from 'nft/components/icons'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { buttonTextSmall, headlineSmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useFiltersExpanded, useIsMobile, useWalletCollections } from 'nft/hooks'
import { WalletCollection } from 'nft/types'
import { Dispatch, FormEvent, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'
import { useSpring } from 'react-spring/web'
import * as styles from './SelectPage.css'
export const FilterSidebar = ({ SortDropdown }: { SortDropdown: () => JSX.Element }) => {
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
const setCollectionFilters = useWalletCollections((state) => state.setCollectionFilters)
const walletCollections = useWalletCollections((state) => state.walletCollections)
const listFilter = useWalletCollections((state) => state.listFilter)
const setListFilter = useWalletCollections((state) => state.setListFilter)
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
const isMobile = useIsMobile()
const { sidebarX } = useSpring({
sidebarX: isFiltersExpanded ? 0 : -360,
})
return (
// @ts-ignore
<AnimatedBox
position={{ sm: 'fixed', md: 'sticky' }}
top={{ sm: '40', md: 'unset' }}
left={{ sm: '0', md: 'unset' }}
width={{ sm: 'full', md: 'auto' }}
height={{ sm: 'full', md: 'auto' }}
zIndex={{ sm: '3', md: 'auto' }}
display={isFiltersExpanded ? 'flex' : 'none'}
style={{ transform: sidebarX.interpolate((x) => `translateX(${x}px)`) }}
>
<Box
paddingTop={{ sm: '24', md: '0' }}
paddingLeft={{ sm: '16', md: '0' }}
paddingRight="16"
width={{ sm: 'full', md: 'auto' }}
>
<Row width="full" justifyContent="space-between">
<Row as="span" className={headlineSmall} color="blackBlue">
Filters
</Row>
{isMobile && (
<Box
as="button"
border="none"
backgroundColor="transparent"
color="darkGray"
onClick={() => setFiltersExpanded(false)}
>
<XMarkIcon fill={themeVars.colors.blackBlue} />
</Box>
)}
</Row>
<Row marginTop="14" marginLeft="2" gap="6" flexWrap="wrap" width="276">
<ListStatusFilterButtons listFilter={listFilter} setListFilter={setListFilter} />
</Row>
{isMobile && (
<Box paddingTop="20">
<SortDropdown />
</Box>
)}
<CollectionSelect
collections={walletCollections}
collectionFilters={collectionFilters}
setCollectionFilters={setCollectionFilters}
/>
</Box>
</AnimatedBox>
)
}
const CollectionSelect = ({
collections,
collectionFilters,
setCollectionFilters,
}: {
collections: WalletCollection[]
collectionFilters: Array<string>
setCollectionFilters: (address: string) => void
}) => {
const [collectionSearchText, setCollectionSearchText] = useState('')
const [displayCollections, setDisplayCollections] = useState(collections)
useEffect(() => {
if (collectionSearchText) {
const filtered = collections.filter((collection) =>
collection.name?.toLowerCase().includes(collectionSearchText.toLowerCase())
)
setDisplayCollections(filtered)
} else {
setDisplayCollections(collections)
}
}, [collectionSearchText, collections])
return (
<>
<Box className={headlineSmall} marginTop="20" marginBottom="12">
Collections
</Box>
<Box paddingBottom="12" paddingTop="0" borderRadius="8">
<Column as="ul" paddingLeft="0" gap="10" style={{ maxHeight: '508px' }}>
<CollectionFilterSearch
collectionSearchText={collectionSearchText}
setCollectionSearchText={setCollectionSearchText}
/>
<Box
background="lightGray"
borderRadius="12"
paddingTop="8"
paddingBottom="8"
overflowY="scroll"
style={{ scrollbarWidth: 'none' }}
>
{displayCollections?.map((collection, index) => (
<CollectionItem
key={index}
collection={collection}
collectionFilters={collectionFilters}
setCollectionFilters={setCollectionFilters}
/>
))}
</Box>
</Column>
</Box>
</>
)
}
const CollectionFilterSearch = ({
collectionSearchText,
setCollectionSearchText,
}: {
collectionSearchText: string
setCollectionSearchText: Dispatch<SetStateAction<string>>
}) => {
return (
<Box
as="input"
borderColor={{ default: 'medGray', focus: 'genieBlue' }}
borderWidth="1px"
borderStyle="solid"
borderRadius="8"
padding="12"
marginLeft="0"
marginBottom="24"
backgroundColor="white"
fontSize="14"
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
placeholder="Search collections"
value={collectionSearchText}
onChange={(e: FormEvent<HTMLInputElement>) => setCollectionSearchText(e.currentTarget.value)}
/>
)
}
const CollectionItem = ({
collection,
collectionFilters,
setCollectionFilters,
}: {
collection: WalletCollection
collectionFilters: Array<string>
setCollectionFilters: (address: string) => void
}) => {
const [isCheckboxSelected, setCheckboxSelected] = useState(false)
const [hovered, toggleHovered] = useReducer((state) => {
return !state
}, false)
const isChecked = useCallback(
(address: string) => {
return collectionFilters.some((collection) => collection === address)
},
[collectionFilters]
)
const handleCheckbox = () => {
setCheckboxSelected(!isCheckboxSelected)
setCollectionFilters(collection.address)
}
return (
<Row
cursor="pointer"
paddingRight="14"
height="44"
as="li"
background={hovered ? 'medGray' : undefined}
onMouseEnter={toggleHovered}
onMouseLeave={toggleHovered}
onClick={handleCheckbox}
>
<Box as="img" borderRadius="round" marginLeft="16" width="20" height="20" src={collection.image} />
<Box as="span" marginLeft="6" marginRight="auto" className={styles.collectionName}>
{collection.name}{' '}
</Box>
<Checkbox checked={isChecked(collection.address)} hovered={hovered} onChange={handleCheckbox}>
<Box as="span" color="darkGray" marginRight="12" marginLeft="auto">
{collection.count}
</Box>
</Checkbox>
</Row>
)
}
const statusArray = ['All', 'Unlisted', 'Listed']
const ListStatusFilterButtons = ({
listFilter,
setListFilter,
}: {
listFilter: string
setListFilter: (value: string) => void
}) => {
return (
<>
{statusArray.map((value, index) => (
<Row
key={index}
borderRadius="12"
backgroundColor="medGray"
height="44"
className={value === listFilter ? styles.buttonSelected : null}
onClick={() => setListFilter(value)}
width="max"
padding="14"
cursor="pointer"
>
<Box className={buttonTextSmall}>{value}</Box>
</Row>
))}
</>
)
}

@ -1,20 +1,49 @@
import clsx from 'clsx'
import { AnimatedBox, Box } from 'nft/components/Box'
import { assetList } from 'nft/components/collection/CollectionNfts.css'
import { FilterButton } from 'nft/components/collection/FilterButton'
import { LoadingSparkle } from 'nft/components/common/Loading/LoadingSparkle'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Center, Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import {
BagFillIcon,
ClockIconFilled,
CrossIcon,
NonRarityIconFilled,
PaintPaletteIconFilled,
TagFillIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { FilterSidebar } from 'nft/components/sell/select/FilterSidebar'
import { subhead, subheadSmall } from 'nft/css/common.css'
import { useBag, useIsMobile, useSellAsset, useSellPageState, useWalletBalance, useWalletCollections } from 'nft/hooks'
import { vars } from 'nft/css/sprinkles.css'
import {
useBag,
useFiltersExpanded,
useIsMobile,
useSellAsset,
useSellPageState,
useWalletBalance,
useWalletCollections,
} from 'nft/hooks'
import { fetchMultipleCollectionStats, fetchWalletAssets, OSCollectionsFetcher } from 'nft/queries'
import { SellPageStateType, WalletAsset } from 'nft/types'
import { DropDownOption, SellPageStateType, WalletAsset, WalletCollection } from 'nft/types'
import { Dispatch, FormEvent, SetStateAction, useEffect, useMemo, useReducer, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useInfiniteQuery, useQuery } from 'react-query'
import { Link } from 'react-router-dom'
import { useSpring } from 'react-spring/web'
import * as styles from './SelectPage.css'
enum SortBy {
FloorPrice,
LastPrice,
DateAcquired,
DateCreated,
DateListed,
}
const formatEth = (price: number) => {
if (price > 1000000) {
return `${Math.round(price / 1000000)}M`
@ -32,6 +61,8 @@ function roundFloorPrice(price?: number, n?: number) {
export const SelectPage = () => {
const { address } = useWalletBalance()
const collectionFilters = useWalletCollections((state) => state.collectionFilters)
const setCollectionFilters = useWalletCollections((state) => state.setCollectionFilters)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const { data: ownerCollections } = useQuery(
['ownerCollections', address],
@ -79,11 +110,17 @@ export const SelectPage = () => {
const setWalletAssets = useWalletCollections((state) => state.setWalletAssets)
const displayAssets = useWalletCollections((state) => state.displayAssets)
const setDisplayAssets = useWalletCollections((state) => state.setDisplayAssets)
const walletCollections = useWalletCollections((state) => state.walletCollections)
const setWalletCollections = useWalletCollections((state) => state.setWalletCollections)
const listFilter = useWalletCollections((state) => state.listFilter)
const sellAssets = useSellAsset((state) => state.sellAssets)
const reset = useSellAsset((state) => state.reset)
const setSellPageState = useSellPageState((state) => state.setSellPageState)
const [sortBy, setSortBy] = useState(SortBy.DateAcquired)
const [orderByASC, setOrderBy] = useState(true)
const [searchText, setSearchText] = useState('')
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
const isMobile = useIsMobile()
useEffect(() => {
setWalletAssets(ownerAssets?.flat() ?? [])
@ -93,8 +130,6 @@ export const SelectPage = () => {
ownerCollections && setWalletCollections(ownerCollections)
}, [ownerCollections, setWalletCollections])
const listFilter = useWalletCollections((state) => state.listFilter)
useEffect(() => {
if (searchText) {
const filtered = walletAssets.filter((asset) => asset.name?.toLowerCase().includes(searchText.toLowerCase()))
@ -115,47 +150,169 @@ export const SelectPage = () => {
}
}, [collectionStats, ownerCollections, setWalletCollections])
useEffect(() => {
const sorted = displayAssets && [...displayAssets]
if (sortBy === SortBy.FloorPrice && orderByASC) sorted?.sort((a, b) => (b.floorPrice || 0) - (a.floorPrice || 0))
else if (sortBy === SortBy.FloorPrice && !orderByASC)
sorted?.sort((a, b) => (a.floorPrice || 0) - (b.floorPrice || 0))
else if (sortBy === SortBy.LastPrice && orderByASC) sorted?.sort((a, b) => b.lastPrice - a.lastPrice)
else if (sortBy === SortBy.LastPrice && !orderByASC) sorted?.sort((a, b) => a.lastPrice - b.lastPrice)
else if (sortBy === SortBy.DateCreated && orderByASC)
sorted?.sort(
(a, b) => new Date(a.asset_contract.created_date).getTime() - new Date(b.asset_contract.created_date).getTime()
)
else if (sortBy === SortBy.DateCreated && !orderByASC)
sorted?.sort(
(a, b) => new Date(b.asset_contract.created_date).getTime() - new Date(a.asset_contract.created_date).getTime()
)
else if (sortBy === SortBy.DateAcquired && orderByASC)
sorted?.sort((a, b) => new Date(a.date_acquired).getTime() - new Date(b.date_acquired).getTime())
else if (sortBy === SortBy.DateAcquired && !orderByASC)
sorted?.sort((a, b) => new Date(b.date_acquired).getTime() - new Date(a.date_acquired).getTime())
else if (sortBy === SortBy.DateListed && orderByASC) sorted?.sort((a, b) => +b.listing_date - +a.listing_date)
else if (sortBy === SortBy.DateListed && !orderByASC) sorted?.sort((a, b) => +a.listing_date - +b.listing_date)
setDisplayAssets(sorted, listFilter)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortBy, orderByASC, listFilter])
useEffect(() => {
if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) {
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.floorPrice
collection.floorPrice = floorPrice ? Math.round(floorPrice * 1000 + Number.EPSILON) / 1000 : 0 //round to at most 3 digits
}
setWalletCollections(ownerCollectionsCopy)
}
}, [collectionStats, ownerCollections, setWalletCollections])
const { gridX, gridWidthOffset } = useSpring({
gridX: isFiltersExpanded ? 300 : -16,
gridWidthOffset: isFiltersExpanded ? 300 /* right padding */ : 0,
})
const sortDropDownOptions: DropDownOption[] = useMemo(
() => [
{
displayText: 'Floor price',
onClick: () => {
setOrderBy(false)
setSortBy(SortBy.FloorPrice)
},
icon: <NonRarityIconFilled width="28" height="28" color={vars.color.blue400} />,
reverseOnClick: () => setOrderBy(!orderByASC),
},
{
displayText: 'Last price',
onClick: () => {
setOrderBy(false)
setSortBy(SortBy.LastPrice)
},
icon: <ClockIconFilled width="28" height="28" />,
reverseOnClick: () => setOrderBy(!orderByASC),
},
{
displayText: 'Date acquired',
onClick: () => {
setOrderBy(false)
setSortBy(SortBy.DateAcquired)
},
icon: <BagFillIcon width="28" height="28" color={vars.color.blue400} />,
reverseOnClick: () => setOrderBy(!orderByASC),
},
{
displayText: 'Date created',
onClick: () => {
setOrderBy(false)
setSortBy(SortBy.DateCreated)
},
icon: <PaintPaletteIconFilled width="28" height="28" color={vars.color.blue400} />,
reverseOnClick: () => setOrderBy(!orderByASC),
},
{
displayText: 'Date listed',
onClick: () => {
setOrderBy(false)
setSortBy(SortBy.DateListed)
},
icon: <TagFillIcon width="28" height="28" color={vars.color.blue400} />,
reverseOnClick: () => setOrderBy(!orderByASC),
},
],
[orderByASC]
)
const SortWalletAssetsDropdown = () => <SortDropdown dropDownOptions={sortDropDownOptions} />
return (
// Column style is temporary while we move over the filters bar that adjust width
<Column style={{ width: 'calc(100vw - 32px)' }}>
<Column width="full">
<Row
alignItems="flex-start"
position="relative"
paddingLeft={{ sm: '0', md: '52' }}
paddingLeft={{ sm: '16', md: '52' }}
paddingRight={{ sm: '0', md: '72' }}
paddingTop={{ sm: '16', md: '40' }}
>
<AnimatedBox paddingX="16" flexShrink="0" width="full">
<Row gap="8" flexWrap="nowrap">
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
<SelectAllButton />
</Row>
<InfiniteScroll
next={fetchNextPage}
hasMore={hasNextPage ?? false}
loader={
hasNextPage ? (
<Center>
<LoadingSparkle />
</Center>
) : null
}
dataLength={displayAssets.length}
style={{ overflow: 'unset' }}
<FilterSidebar SortDropdown={SortWalletAssetsDropdown} />
{(!isMobile || !isFiltersExpanded) && (
// @ts-ignore
<AnimatedBox
paddingLeft={isFiltersExpanded ? '24' : '16'}
flexShrink="0"
style={{
transform: gridX.interpolate(
(x) => `translate(${Number(x) - (!isMobile && isFiltersExpanded ? 300 : 0)}px)`
),
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x}px)`),
}}
>
<div className={assetList}>
{displayAssets && displayAssets.length
? displayAssets.map((asset, index) => <WalletAssetDisplay asset={asset} key={index} />)
: null}
</div>
</InfiniteScroll>
</AnimatedBox>
<Row gap="8" flexWrap="nowrap">
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
results={displayAssets.length}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
/>
{!isMobile && <SortDropdown dropDownOptions={sortDropDownOptions} />}
<CollectionSearch searchText={searchText} setSearchText={setSearchText} />
<SelectAllButton />
</Row>
<Row>
<CollectionFiltersRow
collections={walletCollections}
collectionFilters={collectionFilters}
setCollectionFilters={setCollectionFilters}
clearCollectionFilters={clearCollectionFilters}
/>
</Row>
<InfiniteScroll
next={fetchNextPage}
hasMore={hasNextPage ?? false}
loader={
hasNextPage ? (
<Center>
<LoadingSparkle />
</Center>
) : null
}
dataLength={displayAssets.length}
style={{ overflow: 'unset' }}
>
<div className={assetList}>
{displayAssets && displayAssets.length
? displayAssets.map((asset, index) => <WalletAssetDisplay asset={asset} key={index} />)
: null}
</div>
</InfiniteScroll>
</AnimatedBox>
)}
</Row>
{sellAssets.length > 0 && (
<Row
display={{ sm: 'flex', md: 'none' }}
position="fixed"
bottom="60"
bottom="24"
left="16"
height="56"
borderRadius="12"
@ -322,11 +479,13 @@ const SelectAllButton = () => {
const resetSellAssets = useSellAsset((state) => state.reset)
useEffect(() => {
if (!isAllSelected) resetSellAssets()
if (isAllSelected) {
displayAssets.forEach((asset) => selectSellAsset(asset))
} else {
resetSellAssets()
}
}, [displayAssets, isAllSelected, resetSellAssets, selectSellAsset])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAllSelected, resetSellAssets, selectSellAsset])
const toggleAllSelected = () => {
setIsAllSelected(!isAllSelected)
@ -356,6 +515,89 @@ const SelectAllButton = () => {
)
}
const CollectionFiltersRow = ({
collections,
collectionFilters,
setCollectionFilters,
clearCollectionFilters,
}: {
collections: WalletCollection[]
collectionFilters: Array<string>
setCollectionFilters: (address: string) => void
clearCollectionFilters: Dispatch<SetStateAction<void>>
}) => {
const getCollection = (collectionAddress: string) => {
return collections?.find((collection) => collection.address === collectionAddress)
}
return (
<Row paddingTop="18" gap="8" flexWrap="wrap">
{collectionFilters &&
collectionFilters.map((collectionAddress, index) => (
<CollectionFilterItem
collection={getCollection(collectionAddress)}
key={index}
setCollectionFilters={setCollectionFilters}
/>
))}
{collectionFilters?.length ? (
<Box
as="button"
paddingLeft="8"
paddingRight="8"
color="genieBlue"
background="none"
fontSize="16"
border="none"
cursor="pointer"
onClick={() => clearCollectionFilters()}
>
Clear all
</Box>
) : null}
</Row>
)
}
const CollectionFilterItem = ({
collection,
setCollectionFilters,
}: {
collection: WalletCollection | undefined
setCollectionFilters: (address: string) => void
}) => {
if (!collection) return null
return (
<Row
justifyContent="center"
paddingRight="4"
paddingTop="4"
paddingBottom="4"
paddingLeft="8"
borderRadius="12"
background="medGray"
fontSize="14"
>
<Box as="img" borderRadius="round" width="20" height="20" src={collection.image} />
<Box marginLeft="6" className={styles.collectionFilterBubbleText}>
{collection?.name}
</Box>
<Box
color="darkGray"
background="none"
height="28"
width="28"
padding="0"
as="button"
border="none"
cursor="pointer"
onClick={() => setCollectionFilters(collection.address)}
>
<CrossIcon />
</Box>
</Row>
)
}
const CollectionSearch = ({
searchText,
setSearchText,

@ -21,7 +21,7 @@ export const mobileSellWrapper = style([
top: { sm: '0', md: 'unset' },
zIndex: { sm: '3', md: 'auto' },
height: { sm: 'full', md: 'auto' },
width: { sm: 'full', md: 'auto' },
width: 'full',
overflowY: 'scroll',
}),
{