chore: merge marketplace and traits (#4645)

* porting of filters

Co-authored-by: Jack Short <john.short.tj@gmail.com>
This commit is contained in:
aballerr 2022-09-21 07:22:05 -04:00 committed by GitHub
parent e35f9e16a1
commit e180153c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 664 additions and 52 deletions

@ -1,10 +1,13 @@
import clsx from 'clsx' import clsx from 'clsx'
import { Box } from 'nft/components/Box' import useDebounce from 'hooks/useDebounce'
import { AnimatedBox, Box } from 'nft/components/Box'
import { FilterButton } from 'nft/components/collection'
import { CollectionAsset } from 'nft/components/collection/CollectionAsset' import { CollectionAsset } from 'nft/components/collection/CollectionAsset'
import * as styles from 'nft/components/collection/CollectionNfts.css' import * as styles from 'nft/components/collection/CollectionNfts.css'
import { Row } from 'nft/components/Flex'
import { Center } from 'nft/components/Flex' import { Center } from 'nft/components/Flex'
import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css' import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks' import { useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import { AssetsFetcher } from 'nft/queries' import { AssetsFetcher } from 'nft/queries'
import { UniformHeight, UniformHeights } from 'nft/types' import { UniformHeight, UniformHeights } from 'nft/types'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
@ -16,7 +19,17 @@ interface CollectionNftsProps {
} }
export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => { export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
const traits = useCollectionFilters((state) => state.traits)
const minPrice = useCollectionFilters((state) => state.minPrice)
const maxPrice = useCollectionFilters((state) => state.maxPrice)
const markets = useCollectionFilters((state) => state.markets)
const buyNow = useCollectionFilters((state) => state.buyNow) const buyNow = useCollectionFilters((state) => state.buyNow)
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded()
const isMobile = useIsMobile()
const debouncedMinPrice = useDebounce(minPrice, 500)
const debouncedMaxPrice = useDebounce(maxPrice, 500)
const { const {
data: collectionAssets, data: collectionAssets,
isSuccess: AssetsFetchSuccess, isSuccess: AssetsFetchSuccess,
@ -26,15 +39,29 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
[ [
'collectionNfts', 'collectionNfts',
{ {
traits,
contractAddress, contractAddress,
markets,
notForSale: !buyNow, notForSale: !buyNow,
price: {
low: debouncedMinPrice,
high: debouncedMaxPrice,
symbol: 'ETH',
},
}, },
], ],
async ({ pageParam = 0 }) => { async ({ pageParam = 0 }) => {
return await AssetsFetcher({ return await AssetsFetcher({
contractAddress, contractAddress,
markets,
notForSale: !buyNow, notForSale: !buyNow,
pageParam, pageParam,
traits,
price: {
low: debouncedMinPrice,
high: debouncedMaxPrice,
symbol: 'ETH',
},
}) })
}, },
{ {
@ -67,6 +94,20 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
} }
return ( return (
<>
<AnimatedBox position="sticky" top="72" width="full" zIndex="3">
<Box backgroundColor="white08" width="full" paddingBottom="8" style={{ backdropFilter: 'blur(24px)' }}>
<Row marginTop="12" gap="12">
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
collectionCount={collectionNfts?.[0]?.totalCount ?? 0}
/>
</Row>
</Box>
</AnimatedBox>
<InfiniteScroll <InfiniteScroll
next={fetchNextPage} next={fetchNextPage}
hasMore={hasNextPage ?? false} hasMore={hasNextPage ?? false}
@ -100,5 +141,6 @@ export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
</Center> </Center>
)} )}
</InfiniteScroll> </InfiniteScroll>
</>
) )
} }

@ -12,11 +12,13 @@ export const FilterButton = ({
isMobile, isMobile,
isFiltersExpanded, isFiltersExpanded,
results, results,
collectionCount = 0,
}: { }: {
isMobile: boolean isMobile: boolean
isFiltersExpanded: boolean isFiltersExpanded: boolean
results?: number results?: number
onClick: () => void onClick: () => void
collectionCount?: number
}) => { }) => {
const { minPrice, maxPrice, minRarity, maxRarity, traits, markets, buyNow } = useCollectionFilters((state) => ({ const { minPrice, maxPrice, minRarity, maxRarity, traits, markets, buyNow } = useCollectionFilters((state) => ({
minPrice: state.minPrice, minPrice: state.minPrice,
@ -67,7 +69,9 @@ export const FilterButton = ({
</Box> </Box>
)} )}
<Box paddingLeft={!isFiltersExpanded ? '12' : '2'}>{results ? putCommas(results) : 0} results</Box> <Box paddingLeft={!isFiltersExpanded ? '12' : '2'}>
{collectionCount > 0 ? putCommas(collectionCount) : 0} results
</Box>
</Box> </Box>
) : null} ) : null}
</Box> </Box>

@ -1,21 +1,62 @@
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css' import * as styles from 'nft/components/collection/Filters.css'
import { MarketplaceSelect } from 'nft/components/collection/MarketplaceSelect'
import { PriceRange } from 'nft/components/collection/PriceRange'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { Radio } from 'nft/components/layout/Radio' import { Radio } from 'nft/components/layout/Radio'
import { useCollectionFilters } from 'nft/hooks' import { useCollectionFilters } from 'nft/hooks'
import { FocusEventHandler, FormEvent, useMemo, useState } from 'react'
import { useReducer } from 'react' import { useReducer } from 'react'
export const Filters = () => { import { Trait } from '../../hooks/useCollectionFilters'
import { groupBy } from '../../utils/groupBy'
import { Input } from '../layout/Input'
import { TraitSelect } from './TraitSelect'
export const Filters = ({
traits,
traitsByAmount,
}: {
traits: Trait[]
traitsByAmount: {
traitCount: number
numWithTrait: number
}[]
}) => {
const { buyNow, setBuyNow } = useCollectionFilters((state) => ({ const { buyNow, setBuyNow } = useCollectionFilters((state) => ({
buyNow: state.buyNow, buyNow: state.buyNow,
setBuyNow: state.setBuyNow, 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 [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false) const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const [search, setSearch] = useState('')
const handleBuyNowToggle = () => { const handleBuyNowToggle = () => {
setBuyNow(!buyNow) setBuyNow(!buyNow)
} }
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = ''
}
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = 'Search traits'
}
return ( return (
<Box className={styles.container}> <Box className={styles.container}>
<Row width="full" justifyContent="space-between"> <Row width="full" justifyContent="space-between">
@ -45,6 +86,36 @@ export const Filters = () => {
</Box> </Box>
<Radio hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle} /> <Radio hovered={buyNowHovered} checked={buyNow} onClick={handleBuyNowToggle} />
</Row> </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>
<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>
</Box>
</Column> </Column>
</Box> </Box>
) )

@ -0,0 +1,145 @@
import clsx from 'clsx'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
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 { FormEvent, useEffect, useReducer, useState } from 'react'
import { Checkbox } from '../layout/Checkbox'
export const marketPlaceItems = {
looksrare: 'LooksRare',
nft20: 'NFT20',
nftx: 'NFTX',
opensea: 'OpenSea',
x2y2: 'X2Y2',
}
const MarketplaceItem = ({
title,
value,
addMarket,
removeMarket,
isMarketSelected,
count,
}: {
title: string
value: string
addMarket: (market: string) => void
removeMarket: (market: string) => void
isMarketSelected: boolean
count?: number
}) => {
const [isCheckboxSelected, setCheckboxSelected] = useState(false)
const [hovered, toggleHover] = useReducer((state) => !state, false)
useEffect(() => {
setCheckboxSelected(isMarketSelected)
}, [isMarketSelected])
const handleCheckbox = (e: FormEvent) => {
e.preventDefault()
if (!isCheckboxSelected) {
addMarket(value)
setCheckboxSelected(true)
} else {
removeMarket(value)
setCheckboxSelected(false)
}
}
return (
<Row
key={value}
justifyContent="space-between"
maxWidth="full"
overflowX={'hidden'}
overflowY={'hidden'}
fontWeight="normal"
className={`${subheadSmall} ${styles.subRowHover}`}
paddingLeft="12"
paddingRight="12"
cursor="pointer"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
onMouseEnter={toggleHover}
onMouseLeave={toggleHover}
onClick={handleCheckbox}
>
<Box as="span" fontSize="14" fontWeight="normal">
{title}{' '}
</Box>
<Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}>
<Box as="span" color="darkGray" marginLeft="4" paddingRight={'12'}>
{count}
</Box>
</Checkbox>
</Row>
)
}
export const MarketplaceSelect = () => {
const {
addMarket,
removeMarket,
markets: selectedMarkets,
marketCount,
} = useCollectionFilters(({ markets, marketCount, removeMarket, addMarket }) => ({
markets,
marketCount,
removeMarket,
addMarket,
}))
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
return (
<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"
justifyContent="space-between"
cursor="pointer"
alignItems="center"
fontSize="14"
paddingTop="12"
paddingLeft="12"
paddingRight="12"
paddingBottom={isOpen ? '8' : '12'}
onClick={(e) => {
e.preventDefault()
setOpen(!isOpen)
}}
>
Marketplaces
<Box
color="darkGray"
transition="250"
height="28"
width="28"
style={{
transform: `rotate(${isOpen ? 0 : 180}deg)`,
}}
>
<ChevronUpIcon />
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingLeft="0">
{Object.entries(marketPlaceItems).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
))}
</Column>
</Box>
)
}

@ -0,0 +1,86 @@
import { useIsMobile } from 'nft/hooks'
import { useEffect, useState } from 'react'
import { FocusEventHandler, FormEvent } from 'react'
import { useLocation } from 'react-router-dom'
import { useCollectionFilters } from '../../hooks/useCollectionFilters'
import { isNumber } from '../../utils/numbers'
import { scrollToTop } from '../../utils/scrollToTop'
import { Row } from '../Flex'
import { NumericInput } from '../layout/Input'
export const PriceRange = () => {
const [placeholderText, setPlaceholderText] = useState('')
const setMinPrice = useCollectionFilters((state) => state.setMinPrice)
const setMaxPrice = useCollectionFilters((state) => state.setMaxPrice)
const minPrice = useCollectionFilters((state) => state.minPrice)
const maxPrice = useCollectionFilters((state) => state.maxPrice)
const isMobile = useIsMobile()
const location = useLocation()
useEffect(() => {
setMinPrice('')
setMaxPrice('')
}, [location.pathname, setMinPrice, setMaxPrice])
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
setPlaceholderText(e.currentTarget.placeholder)
e.currentTarget.placeholder = ''
}
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
e.currentTarget.placeholder = placeholderText
setPlaceholderText('')
}
return (
<Row gap="12" marginTop="12" color="blackBlue">
<Row position="relative" style={{ flex: 1 }}>
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
}}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
backgroundColor="transparent"
placeholder="Min"
defaultValue={minPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMinPrice(isNumber(v.currentTarget.value) ? parseFloat(v.currentTarget.value) : '')
}}
onFocus={handleFocus}
value={minPrice}
onBlur={handleBlur}
/>
</Row>
<Row position="relative" style={{ flex: 1 }}>
<NumericInput
style={{
width: isMobile ? '100%' : '142px',
border: '2px solid rgba(153, 161, 189, 0.24)',
}}
borderColor={{ default: 'medGray', focus: 'darkGray' }}
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
backgroundColor="transparent"
placeholder="Max"
defaultValue={maxPrice}
value={maxPrice}
onChange={(v: FormEvent<HTMLInputElement>) => {
scrollToTop()
setMaxPrice(isNumber(v.currentTarget.value) ? parseFloat(v.currentTarget.value) : '')
}}
onFocus={handleFocus}
onBlur={handleBlur}
/>
</Row>
</Row>
)
}

@ -0,0 +1,12 @@
import { style } from '@vanilla-extract/css'
export const list = style({
overflowAnchor: 'none',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
selectors: {
'&::-webkit-scrollbar': {
display: 'none',
},
},
})

@ -0,0 +1,194 @@
import clsx from 'clsx'
import useDebounce from 'hooks/useDebounce'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { scrollToTop } from 'nft/utils/scrollToTop'
import { useMemo } from 'react'
import { FormEvent, MouseEvent } from 'react'
import { useEffect, useLayoutEffect, useState } from 'react'
import { subheadSmall } from '../../css/common.css'
import { Trait, useCollectionFilters } from '../../hooks/useCollectionFilters'
import { Box } from '../Box'
import { Column, Row } from '../Flex'
import { ChevronUpIcon } from '../icons'
import { Checkbox } from '../layout/Checkbox'
import * as styles from './Filters.css'
const TraitItem = ({
trait,
addTrait,
removeTrait,
isTraitSelected,
}: {
trait: Trait
addTrait: (trait: Trait) => void
removeTrait: (trait: Trait) => void
isTraitSelected: boolean
}) => {
const [isCheckboxSelected, setCheckboxSelected] = useState(false)
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
const toggleShowFullTraitName = useCollectionFilters((state) => state.toggleShowFullTraitName)
const { shouldShow, trait_value, trait_type } = useCollectionFilters((state) => state.showFullTraitName)
const isEllipsisActive = (e: MouseEvent<HTMLElement>) => {
if (e.currentTarget.offsetWidth < e.currentTarget.scrollWidth) {
toggleShowFullTraitName({
shouldShow: true,
trait_value: trait.trait_value,
trait_type: trait.trait_type,
})
}
}
useEffect(() => {
setCheckboxSelected(isTraitSelected)
}, [isTraitSelected])
const handleCheckbox = (e: FormEvent) => {
e.preventDefault()
scrollToTop()
if (!isCheckboxSelected) {
addTrait(trait)
setCheckboxSelected(true)
} else {
removeTrait(trait)
setCheckboxSelected(false)
}
}
const showFullTraitName = shouldShow && trait_type === trait.trait_type && trait_value === trait.trait_value
return (
<Row
key={trait.trait_value}
maxWidth="full"
overflowX={'hidden'}
overflowY={'hidden'}
fontWeight="normal"
className={`${subheadSmall} ${styles.subRowHover}`}
justifyContent="space-between"
cursor="pointer"
paddingLeft="12"
paddingRight="12"
style={{ paddingBottom: '21px', paddingTop: '21px', maxHeight: '44px' }}
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={handleCheckbox}
>
<Box
as="span"
whiteSpace="nowrap"
textOverflow="ellipsis"
overflow="hidden"
maxWidth={!showFullTraitName ? '160' : 'full'}
onMouseOver={(e) => isEllipsisActive(e)}
onMouseLeave={() => toggleShowFullTraitName({ shouldShow: false, trait_type: '', trait_value: '' })}
>
{trait.trait_type === 'Number of traits'
? `${trait.trait_value} trait${pluralize(Number(trait.trait_value))}`
: trait.trait_value}
</Box>
<Checkbox checked={isCheckboxSelected} hovered={hovered} onChange={handleCheckbox}>
<Box as="span" color="darkGray" minWidth={'8'} paddingTop={'2'} paddingRight={'12'} position={'relative'}>
{!showFullTraitName && trait.trait_count}
</Box>
</Checkbox>
</Row>
)
}
export const TraitSelect = ({ traits, type, search }: { traits: Trait[]; type: string; search: string }) => {
const debouncedSearch = useDebounce(search, 300)
const addTrait = useCollectionFilters((state) => state.addTrait)
const removeTrait = useCollectionFilters((state) => state.removeTrait)
const selectedTraits = useCollectionFilters((state) => state.traits)
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())
)
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="darkGray" display="inline-block" marginRight="12">
{searchedTraits.length}
</Box>
<Box
color="darkGray"
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) => {
const isTraitSelected = selectedTraits.find(
({ trait_type, trait_value }) =>
trait_type === trait.trait_type && String(trait_value) === String(trait.trait_value)
)
return (
<TraitItem
isTraitSelected={!!isTraitSelected}
key={trait.trait_value}
{...{ trait, addTrait, removeTrait }}
/>
)
})}
</Column>
</Box>
) : null
}

@ -0,0 +1,55 @@
import { forwardRef } from 'react'
import { FormEvent } from 'react'
import { Atoms } from '../../css/atoms'
import { isNumber } from '../../utils/numbers'
import { Box, BoxProps } from '../Box'
export const defaultInputStyle: Atoms = {
borderColor: { default: 'medGray', focus: 'darkGray' },
borderWidth: '1px',
borderStyle: 'solid',
borderRadius: '8',
padding: '12',
fontSize: '14',
color: { placeholder: 'darkGray', default: 'blackBlue' },
backgroundColor: 'transparent',
}
export const Input = forwardRef<HTMLInputElement, BoxProps>((props, ref) => (
<Box
ref={ref}
as="input"
borderColor={{ default: 'medGray', focus: 'darkGray' }}
borderWidth="1px"
borderStyle="solid"
borderRadius="12"
padding="12"
fontSize="14"
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
backgroundColor="transparent"
{...props}
/>
))
Input.displayName = 'Input'
export const NumericInput = forwardRef<HTMLInputElement, BoxProps>((props, ref) => {
return (
<Box
ref={ref}
as="input"
autoComplete="off"
type="text"
onInput={(v: FormEvent<HTMLInputElement>) => {
v.currentTarget.value =
!!v.currentTarget.value && isNumber(v.currentTarget.value) && parseFloat(v.currentTarget.value) >= 0
? v.currentTarget.value
: ''
}}
{...props}
/>
)
})
NumericInput.displayName = 'Input'

@ -1,9 +1,10 @@
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { CollectionNfts, CollectionStats, FilterButton, Filters } from 'nft/components/collection' import { CollectionNfts, CollectionStats, Filters } from 'nft/components/collection'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { useFiltersExpanded, useIsMobile } from 'nft/hooks' import { useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css' import * as styles from 'nft/pages/collection/index.css'
import { CollectionStatsFetcher } from 'nft/queries' import { CollectionStatsFetcher } from 'nft/queries'
import { useEffect } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useSpring } from 'react-spring/web' import { useSpring } from 'react-spring/web'
@ -14,7 +15,8 @@ const Collection = () => {
const { contractAddress } = useParams() const { contractAddress } = useParams()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [isFiltersExpanded, setFiltersExpanded] = useFiltersExpanded() const [isFiltersExpanded] = useFiltersExpanded()
const setMarketCount = useCollectionFilters((state) => state.setMarketCount)
const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () => const { data: collectionStats } = useQuery(['collectionStats', contractAddress], () =>
CollectionStatsFetcher(contractAddress as string) CollectionStatsFetcher(contractAddress as string)
@ -25,6 +27,14 @@ const Collection = () => {
gridWidthOffset: isFiltersExpanded ? FILTER_WIDTH : 0, gridWidthOffset: isFiltersExpanded ? FILTER_WIDTH : 0,
}) })
useEffect(() => {
const marketCount: Record<string, number> = {}
collectionStats?.marketplaceCount?.forEach(({ marketplace, count }) => {
marketCount[marketplace] = count
})
setMarketCount(marketCount)
}, [collectionStats?.marketplaceCount, setMarketCount])
return ( return (
<Column width="full"> <Column width="full">
<Box width="full" height="160"> <Box width="full" height="160">
@ -44,7 +54,9 @@ const Collection = () => {
)} )}
<Row alignItems="flex-start" position="relative" paddingX="48"> <Row alignItems="flex-start" position="relative" paddingX="48">
<Box position="sticky" top="72" width="0"> <Box position="sticky" top="72" width="0">
{isFiltersExpanded && <Filters />} {isFiltersExpanded && (
<Filters traitsByAmount={collectionStats?.numTraitsByAmount ?? []} traits={collectionStats?.traits ?? []} />
)}
</Box> </Box>
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */} {/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}
@ -54,18 +66,6 @@ const Collection = () => {
width: gridWidthOffset.interpolate((x) => `calc(100% - ${x as number}px)`), width: gridWidthOffset.interpolate((x) => `calc(100% - ${x as number}px)`),
}} }}
> >
<AnimatedBox position="sticky" top="72" width="full" zIndex="3">
<Box backgroundColor="white08" width="full" paddingBottom="8" style={{ backdropFilter: 'blur(24px)' }}>
<Row marginTop="12" gap="12">
<FilterButton
isMobile={isMobile}
isFiltersExpanded={isFiltersExpanded}
onClick={() => setFiltersExpanded(!isFiltersExpanded)}
/>
</Row>
</Box>
</AnimatedBox>
{contractAddress && <CollectionNfts contractAddress={contractAddress} />} {contractAddress && <CollectionNfts contractAddress={contractAddress} />}
</AnimatedBox> </AnimatedBox>
</Row> </Row>

@ -0,0 +1,3 @@
const DESKTOP_OFFSET = 420
export const scrollToTop = () => window.scrollTo({ top: DESKTOP_OFFSET })