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:
parent
efaefe2e44
commit
49c5cbbf3b
@ -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',
|
||||
})
|
240
src/nft/components/common/SortDropdown/SortDropdown.tsx
Normal file
240
src/nft/components/common/SortDropdown/SortDropdown.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
1
src/nft/components/common/SortDropdown/index.ts
Normal file
1
src/nft/components/common/SortDropdown/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './SortDropdown'
|
49
src/nft/components/layout/Checkbox.css.ts
Normal file
49
src/nft/components/layout/Checkbox.css.ts
Normal file
@ -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',
|
||||
}),
|
||||
])
|
37
src/nft/components/layout/Checkbox.tsx
Normal file
37
src/nft/components/layout/Checkbox.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
239
src/nft/components/sell/select/FilterSidebar.tsx
Normal file
239
src/nft/components/sell/select/FilterSidebar.tsx
Normal file
@ -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',
|
||||
}),
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user