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:
parent
b95621758c
commit
5dbd0ae782
@ -98,6 +98,7 @@ const ActionsSubContainer = styled.div`
|
|||||||
|
|
||||||
export const SortDropdownContainer = styled.div<{ isFiltersExpanded: boolean }>`
|
export const SortDropdownContainer = styled.div<{ isFiltersExpanded: boolean }>`
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
height: 44px;
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||||
${({ isFiltersExpanded }) => isFiltersExpanded && `display: none;`}
|
${({ isFiltersExpanded }) => isFiltersExpanded && `display: none;`}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,6 @@ export const SortDropdown = ({
|
|||||||
borderRadius="12"
|
borderRadius="12"
|
||||||
borderBottomLeftRadius={isOpen ? '0' : undefined}
|
borderBottomLeftRadius={isOpen ? '0' : undefined}
|
||||||
borderBottomRightRadius={isOpen ? '0' : undefined}
|
borderBottomRightRadius={isOpen ? '0' : undefined}
|
||||||
height="44"
|
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import clsx from 'clsx'
|
|
||||||
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
|
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
|
||||||
import { getListingState } from 'nft/components/bag/profile/utils'
|
import { getListingState } from 'nft/components/bag/profile/utils'
|
||||||
import { Box } from 'nft/components/Box'
|
import { Box } from 'nft/components/Box'
|
||||||
@ -13,7 +12,7 @@ import {
|
|||||||
VerifiedIcon,
|
VerifiedIcon,
|
||||||
} from 'nft/components/icons'
|
} from 'nft/components/icons'
|
||||||
import { NumericInput } from 'nft/components/layout/Input'
|
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 { themeVars } from 'nft/css/sprinkles.css'
|
||||||
import { useBag, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
import { useBag, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||||
import {
|
import {
|
||||||
@ -31,82 +30,9 @@ import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react
|
|||||||
import styled from 'styled-components/macro'
|
import styled from 'styled-components/macro'
|
||||||
|
|
||||||
import * as styles from './ListPage.css'
|
import * as styles from './ListPage.css'
|
||||||
|
import { SelectMarketplacesDropdown } from './SelectMarketplacesDropdown'
|
||||||
import { SetDurationModal } from './SetDurationModal'
|
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 {
|
enum SetPriceMethod {
|
||||||
SAME_PRICE,
|
SAME_PRICE,
|
||||||
FLOOR_PRICE,
|
FLOOR_PRICE,
|
||||||
@ -657,7 +583,7 @@ const MarketWrap = styled.section`
|
|||||||
export const ListPage = () => {
|
export const ListPage = () => {
|
||||||
const { setProfilePageState: setSellPageState } = useProfilePageState()
|
const { setProfilePageState: setSellPageState } = useProfilePageState()
|
||||||
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
|
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 toggleBag = useBag((s) => s.toggleBag)
|
||||||
const listings = useNFTList((state) => state.listings)
|
const listings = useNFTList((state) => state.listings)
|
||||||
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
|
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
|
||||||
@ -701,7 +627,7 @@ export const ListPage = () => {
|
|||||||
</Column>
|
</Column>
|
||||||
<MarketWrap>
|
<MarketWrap>
|
||||||
<Row flexWrap={{ sm: 'wrap', lg: 'nowrap' }}>
|
<Row flexWrap={{ sm: 'wrap', lg: 'nowrap' }}>
|
||||||
<SelectMarketplacesModal setSelectedMarkets={setSelectedMarkets} selectedMarkets={selectedMarkets} />
|
<SelectMarketplacesDropdown setSelectedMarkets={setSelectedMarkets} selectedMarkets={selectedMarkets} />
|
||||||
<SetDurationModal />
|
<SetDurationModal />
|
||||||
</Row>
|
</Row>
|
||||||
<NFTListingsGrid selectedMarkets={selectedMarkets} />
|
<NFTListingsGrid selectedMarkets={selectedMarkets} />
|
||||||
|
171
src/nft/components/profile/list/SelectMarketplacesDropdown.tsx
Normal file
171
src/nft/components/profile/list/SelectMarketplacesDropdown.tsx
Normal file
@ -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;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-top: 24px;
|
height: min-content;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||||
|
}
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ErrorMessage = styled(Row)`
|
const ErrorMessage = styled(Row)`
|
||||||
|
@ -26,6 +26,11 @@ import { ListingMarket, ListingStatus, WalletAsset } from '../types'
|
|||||||
import { createSellOrder, encodeOrder, OfferItem, OrderPayload, signOrderData } from './x2y2'
|
import { createSellOrder, encodeOrder, OfferItem, OrderPayload, signOrderData } from './x2y2'
|
||||||
|
|
||||||
export const ListingMarkets: ListingMarket[] = [
|
export const ListingMarkets: ListingMarket[] = [
|
||||||
|
{
|
||||||
|
name: 'X2Y2',
|
||||||
|
fee: 0.5,
|
||||||
|
icon: '/nft/svgs/marketplaces/x2y2.svg',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'LooksRare',
|
name: 'LooksRare',
|
||||||
fee: 1.5,
|
fee: 1.5,
|
||||||
@ -36,11 +41,6 @@ export const ListingMarkets: ListingMarket[] = [
|
|||||||
fee: 2.5,
|
fee: 2.5,
|
||||||
icon: '/nft/svgs/marketplaces/opensea.svg',
|
icon: '/nft/svgs/marketplaces/opensea.svg',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'X2Y2',
|
|
||||||
fee: 0.5,
|
|
||||||
icon: '/nft/svgs/marketplaces/x2y2.svg',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const createConsiderationItem = (basisPoints: string, recipient: string): ConsiderationInputItem => {
|
const createConsiderationItem = (basisPoints: string, recipient: string): ConsiderationInputItem => {
|
||||||
|
Loading…
Reference in New Issue
Block a user