feat: Listing marketplace dropdown (#5314)

* working dropdown button

* memo text

* working dropdown modal

* click outside

* respond to comments

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2022-11-18 13:22:08 -08:00 committed by GitHub
parent b95621758c
commit 5dbd0ae782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 187 additions and 85 deletions

@ -98,6 +98,7 @@ const ActionsSubContainer = styled.div`
export const SortDropdownContainer = styled.div<{ isFiltersExpanded: boolean }>`
width: max-content;
height: 44px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
${({ isFiltersExpanded }) => isFiltersExpanded && `display: none;`}
}

@ -56,7 +56,6 @@ export const SortDropdown = ({
borderRadius="12"
borderBottomLeftRadius={isOpen ? '0' : undefined}
borderBottomRightRadius={isOpen ? '0' : undefined}
height="44"
style={{ width }}
>
<Box

@ -1,4 +1,3 @@
import clsx from 'clsx'
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
import { getListingState } from 'nft/components/bag/profile/utils'
import { Box } from 'nft/components/Box'
@ -13,7 +12,7 @@ import {
VerifiedIcon,
} from 'nft/components/icons'
import { NumericInput } from 'nft/components/layout/Input'
import { badge, body, bodySmall, buttonTextMedium, caption, headlineSmall, subheadSmall } from 'nft/css/common.css'
import { badge, body, bodySmall, headlineSmall, subheadSmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
import {
@ -31,82 +30,9 @@ import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react
import styled from 'styled-components/macro'
import * as styles from './ListPage.css'
import { SelectMarketplacesDropdown } from './SelectMarketplacesDropdown'
import { SetDurationModal } from './SetDurationModal'
const MarkplaceWrap = styled.div`
align-self: flex-start;
padding-right: 40px;
max-width: 1200px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) {
padding-bottom: 20px;
}
`
const SelectMarketplacesModal = ({
setSelectedMarkets,
selectedMarkets,
}: {
setSelectedMarkets: Dispatch<ListingMarket[]>
selectedMarkets: ListingMarket[]
}) => {
return (
<MarkplaceWrap>
<Row className={headlineSmall}>Select marketplaces</Row>
<Row className={caption} color="textSecondary" marginTop="4">
Increase the visibility of your listings by selecting multiple marketplaces.
</Row>
<Row marginTop="14" gap="8" flexWrap="wrap">
{ListingMarkets.map((market) => {
return GlobalMarketplaceButton({ market, setSelectedMarkets, selectedMarkets })
})}
</Row>
</MarkplaceWrap>
)
}
interface GlobalMarketplaceButtonProps {
market: ListingMarket
setSelectedMarkets: Dispatch<ListingMarket[]>
selectedMarkets: ListingMarket[]
}
const GlobalMarketplaceButton = ({ market, setSelectedMarkets, selectedMarkets }: GlobalMarketplaceButtonProps) => {
const isSelected = selectedMarkets.includes(market)
const toggleSelected = () => {
isSelected
? setSelectedMarkets(selectedMarkets.filter((selected: ListingMarket) => selected !== market))
: setSelectedMarkets([...selectedMarkets, market])
}
return (
<Row
gap="6"
borderRadius="12"
backgroundColor="backgroundOutline"
height="44"
className={clsx(isSelected && styles.buttonSelected)}
onClick={toggleSelected}
width="max"
cursor="pointer"
>
<Box
as="img"
alt={market.name}
width={isSelected ? '24' : '20'}
height={isSelected ? '24' : '20'}
borderRadius="4"
objectFit="cover"
marginLeft={isSelected ? '8' : '12'}
src={isSelected ? '/nft/svgs/checkmark.svg' : market.icon}
/>
<Box className={buttonTextMedium}>{market.name}</Box>
<Box color="textSecondary" className={caption} marginRight="12">
{market.fee}% fee
</Box>
</Row>
)
}
enum SetPriceMethod {
SAME_PRICE,
FLOOR_PRICE,
@ -657,7 +583,7 @@ const MarketWrap = styled.section`
export const ListPage = () => {
const { setProfilePageState: setSellPageState } = useProfilePageState()
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[2]]) // default marketplace: x2y2
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
const toggleBag = useBag((s) => s.toggleBag)
const listings = useNFTList((state) => state.listings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
@ -701,7 +627,7 @@ export const ListPage = () => {
</Column>
<MarketWrap>
<Row flexWrap={{ sm: 'wrap', lg: 'nowrap' }}>
<SelectMarketplacesModal setSelectedMarkets={setSelectedMarkets} selectedMarkets={selectedMarkets} />
<SelectMarketplacesDropdown setSelectedMarkets={setSelectedMarkets} selectedMarkets={selectedMarkets} />
<SetDurationModal />
</Row>
<NFTListingsGrid selectedMarkets={selectedMarkets} />

@ -0,0 +1,171 @@
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Column, Row } from 'nft/components/Flex'
import { ChevronUpIcon } from 'nft/components/icons'
import { Checkbox } from 'nft/components/layout/Checkbox'
import { buttonTextMedium, caption } from 'nft/css/common.css'
import { ListingMarket } from 'nft/types'
import { ListingMarkets } from 'nft/utils/listNfts'
import { Dispatch, FormEvent, useMemo, useReducer, useRef } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
const MarketplaceRowWrapper = styled(Row)`
gap: 6px;
height: 44px;
width: 100%;
cursor: pointer;
justify-content: space-between;
padding: 0px 16px;
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
}
border-radius: 12px;
`
const MarketplaceDropdownIcon = styled.img`
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: cover;
`
const FeeText = styled.div`
color: ${({ theme }) => theme.textSecondary};
`
interface MarketplaceRowProps {
market: ListingMarket
setSelectedMarkets: Dispatch<ListingMarket[]>
selectedMarkets: ListingMarket[]
}
const MarketplaceRow = ({ market, setSelectedMarkets, selectedMarkets }: MarketplaceRowProps) => {
const isSelected = selectedMarkets.includes(market)
const [hovered, toggleHovered] = useReducer((s) => !s, false)
const toggleSelected = () => {
if (selectedMarkets.length === 1 && isSelected) return
isSelected
? setSelectedMarkets(selectedMarkets.filter((selected: ListingMarket) => selected !== market))
: setSelectedMarkets([...selectedMarkets, market])
}
const handleCheckbox = (e: FormEvent) => {
e.preventDefault()
e.stopPropagation()
}
return (
<MarketplaceRowWrapper onMouseEnter={toggleHovered} onMouseLeave={toggleHovered} onClick={toggleSelected}>
<Row gap="12" onClick={toggleSelected}>
<MarketplaceDropdownIcon alt={market.name} src={market.icon} />
<Column>
<ThemedText.BodyPrimary>{market.name}</ThemedText.BodyPrimary>
<FeeText className={caption}>{market.fee}% fee</FeeText>
</Column>
</Row>
<Checkbox hovered={hovered} checked={isSelected} onClick={handleCheckbox}>
<span />
</Checkbox>
</MarketplaceRowWrapper>
)
}
const HeaderButtonWrap = styled(Row)`
padding: 12px;
border-radius: 12px;
width: 220px;
justify-content: space-between;
background: ${({ theme }) => theme.backgroundModule};
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
}
`
const HeaderButtonContentWrapper = styled.div`
display: flex;
`
const MarketIcon = styled.img<{ index: number; totalSelected: number }>`
height: 20px;
width: 20px;
margin-right: 8px;
border: 1px solid;
border-color: ${({ theme }) => theme.backgroundInteractive};
border-radius: 4px;
z-index: ${({ index, totalSelected }) => totalSelected - index};
margin-left: ${({ index }) => `${index === 0 ? 0 : -18}px`};
`
const Chevron = styled(ChevronUpIcon)<{ isOpen: boolean }>`
height: 20px;
width: 20px;
transition: ${({
theme: {
transition: { duration },
},
}) => `${duration.fast} transform`};
transform: ${({ isOpen }) => `rotate(${isOpen ? 0 : 180}deg)`};
`
const ModalWrapper = styled.div`
display: flex;
flex-direction: column;
position: relative;
`
const DropdownWrapper = styled(Column)<{ isOpen: boolean }>`
padding: 16px 0px;
background-color: ${({ theme }) => theme.backgroundModule};
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
position: absolute;
top: 52px;
width: 220px;
border-radius: 12px;
gap: 12px;
z-index: ${Z_INDEX.modalBackdrop};
`
export const SelectMarketplacesDropdown = ({
setSelectedMarkets,
selectedMarkets,
}: {
setSelectedMarkets: Dispatch<ListingMarket[]>
selectedMarkets: ListingMarket[]
}) => {
const [isOpen, toggleIsOpen] = useReducer((s) => !s, false)
const dropdownDisplayText = useMemo(
() => (selectedMarkets.length === 1 ? selectedMarkets[0].name : 'Multiple'),
[selectedMarkets]
)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, () => isOpen && toggleIsOpen())
return (
<ModalWrapper ref={ref}>
<HeaderButtonWrap className={buttonTextMedium} onClick={toggleIsOpen}>
<HeaderButtonContentWrapper>
{selectedMarkets.map((market, index) => {
return (
<MarketIcon
key={index}
alt={market.name}
src={market.icon}
totalSelected={selectedMarkets.length}
index={index}
/>
)
})}
{dropdownDisplayText}
</HeaderButtonContentWrapper>
<Chevron isOpen={isOpen} />
</HeaderButtonWrap>
<DropdownWrapper isOpen={isOpen}>
{ListingMarkets.map((market) => {
return MarketplaceRow({ market, setSelectedMarkets, selectedMarkets })
})}
</DropdownWrapper>
</ModalWrapper>
)
}

@ -28,8 +28,13 @@ const DropdownWrapper = styled(ThemedText.BodyPrimary)`
cursor: pointer;
display: flex;
justify-content: flex-end;
padding-top: 24px;
height: min-content;
width: 80px;
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
}
border-radius: 12px;
padding: 8px;
`
const ErrorMessage = styled(Row)`

@ -26,6 +26,11 @@ import { ListingMarket, ListingStatus, WalletAsset } from '../types'
import { createSellOrder, encodeOrder, OfferItem, OrderPayload, signOrderData } from './x2y2'
export const ListingMarkets: ListingMarket[] = [
{
name: 'X2Y2',
fee: 0.5,
icon: '/nft/svgs/marketplaces/x2y2.svg',
},
{
name: 'LooksRare',
fee: 1.5,
@ -36,11 +41,6 @@ export const ListingMarkets: ListingMarket[] = [
fee: 2.5,
icon: '/nft/svgs/marketplaces/opensea.svg',
},
{
name: 'X2Y2',
fee: 0.5,
icon: '/nft/svgs/marketplaces/x2y2.svg',
},
]
const createConsiderationItem = (basisPoints: string, recipient: string): ConsiderationInputItem => {