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 }>`
|
||||
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} />
|
||||
|
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;
|
||||
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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user