Commit d3c4ca6e authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: add Sell listing page (#4664)

* add listing modal

* add new files

* Add listing page

* remove useeffect

* re-add useeffect and includes array

* position relative

* add listing datatype

* use pluralize

* readable const

* clsx

* parseFloat 0 default

* don't use any

* cant use months for ms

* remove unused input style

* border sprinkles

* clsx

* duration enum

* remove unused index

* pluralize

* clsx

* pluralize

* type refactoring

* move format to utils

* remove uneeded check

* border sprinkles

* change input based on ref

* remove console.log

* correct warning check

* better clsx
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 28538214
import { forwardRef } from 'react' import { isNumber } from 'nft/utils/numbers'
import { FormEvent } from 'react' import { FormEvent, forwardRef } from 'react'
import { Atoms } from '../../css/atoms'
import { isNumber } from '../../utils/numbers'
import { Box, BoxProps } from '../Box' 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) => ( export const Input = forwardRef<HTMLInputElement, BoxProps>((props, ref) => (
<Box <Box
ref={ref} ref={ref}
......
import { style } from '@vanilla-extract/css'
import { sprinkles, themeVars } from 'nft/css/sprinkles.css'
export const buttonSelected = style([
sprinkles({
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'genieBlue',
}),
])
export const nftDivider = style([
sprinkles({
height: '0',
width: 'full',
borderRadius: '20',
borderWidth: '0.5px',
borderStyle: 'solid',
borderColor: 'medGray',
}),
])
export const priceChevron = style([
sprinkles({
height: '20',
width: '20',
transition: '250',
}),
{
marginBottom: '-6px',
},
])
export const durationChevron = style([
sprinkles({
height: '16',
width: '16',
transition: '250',
}),
{
marginBottom: '-4px',
},
])
export const chevronDown = style({
transform: 'rotate(180deg)',
})
export const dropdown = style({
boxShadow: `0px 4px 16px ${themeVars.colors.blackBlue20}`,
marginLeft: '-12px',
})
export const removeAsset = style({
top: '31px',
left: '15px',
})
export const removeMarketplace = style({
top: '11px',
right: '14px',
})
import clsx from 'clsx'
import ms from 'ms.macro'
import { Box } from 'nft/components/Box'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Column, Row } from 'nft/components/Flex'
import {
AttachPriceIcon,
BackArrowIcon,
EditPriceIcon,
FloorPriceIcon,
PrevListingIcon,
RowsCollpsedIcon,
RowsExpandedIcon,
SamePriceIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { NumericInput } from 'nft/components/layout/Input'
import {
badge,
body,
bodySmall,
buttonTextMedium,
caption,
headlineSmall,
subhead,
subheadSmall,
} from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useNFTList, useSellAsset, useSellPageState } from 'nft/hooks'
import { DropDownOption, ListingMarket, ListingStatus, ListingWarning, SellPageStateType, WalletAsset } from 'nft/types'
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { ListingMarkets } from 'nft/utils/listNfts'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react'
import { ListingButton } from '../modal/ListingButton'
import { getListingState } from '../modal/utils'
import * as styles from './ListPage.css'
const SelectMarketplacesModal = ({
setSelectedMarkets,
selectedMarkets,
}: {
setSelectedMarkets: Dispatch<ListingMarket[]>
selectedMarkets: ListingMarket[]
}) => {
return (
<Column alignSelf="flex-start" paddingRight="40" paddingBottom={{ sm: '20', lg: '0' }}>
<Row className={headlineSmall}>Select marketplaces</Row>
<Row className={caption} color="darkGray" 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>
</Column>
)
}
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="medGray"
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="darkGray" className={caption} marginRight="12">
{market.fee}% fee
</Box>
</Row>
)
}
enum Duration {
hour = 'hour',
day = 'day',
week = 'week',
month = 'month',
}
const SetDurationModal = () => {
const [duration, setDuration] = useState(Duration.day)
const [displayDuration, setDisplayDuration] = useState(Duration.day)
const [amount, setAmount] = useState(7)
const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration)
const setCustomExpiration = (event: React.ChangeEvent<HTMLInputElement>) => {
setAmount(parseFloat(event.target.value))
setDuration(displayDuration)
}
const selectDuration = (duration: Duration) => {
setDuration(duration)
setDisplayDuration(duration)
}
const durationOptions: DropDownOption[] = useMemo(
() => [
{
displayText: 'Hours',
onClick: () => selectDuration(Duration.hour),
},
{
displayText: 'Days',
onClick: () => selectDuration(Duration.day),
},
{
displayText: 'Weeks',
onClick: () => selectDuration(Duration.week),
},
{
displayText: 'Months',
onClick: () => selectDuration(Duration.month),
},
],
[]
)
useEffect(() => {
setGlobalExpiration(convertDurationToExpiration(amount, duration))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [duration, amount])
return (
<Column marginLeft={{ sm: '0', lg: 'auto' }} marginRight="auto" alignSelf="flex-start">
<Row className={headlineSmall}>Set duration</Row>
<Row className={caption} color="darkGray" marginTop="4">
Select the amount of time your listings are available for purchase. Max 6 months.
</Row>
<Row marginTop="14" gap="6" flexWrap="wrap">
<GlobalDurationButton
amount={1}
duration={Duration.hour}
globalAmount={amount}
globalDuration={duration}
setGlobalAmount={setAmount}
setGlobalDuration={setDuration}
/>
<GlobalDurationButton
amount={7}
duration={Duration.day}
globalAmount={amount}
globalDuration={duration}
setGlobalAmount={setAmount}
setGlobalDuration={setDuration}
/>
<GlobalDurationButton
amount={6}
duration={Duration.month}
globalAmount={amount}
globalDuration={duration}
setGlobalAmount={setAmount}
setGlobalDuration={setDuration}
/>
<Row
color="medGray"
paddingRight="8"
paddingLeft="12"
paddingTop="12"
paddingBottom="12"
borderRadius="8"
borderWidth="1px"
borderStyle="solid"
borderColor="medGray"
position="relative"
height="44"
>
<Box
as="input"
borderStyle="none"
className={bodySmall}
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
placeholder="Set"
width="32"
marginRight="4"
backgroundColor="none"
onChange={setCustomExpiration}
flexShrink="0"
/>
<Box
cursor="pointer"
display="flex"
justifyContent="flex-end"
className={buttonTextMedium}
color="blackBlue"
marginTop="24"
style={{ width: '80px' }}
>
<SortDropdown
dropDownOptions={durationOptions}
mini
miniPrompt={displayDuration + (displayDuration === duration ? pluralize(amount) : 's')}
left={38}
/>
</Box>
</Row>
</Row>
</Column>
)
}
const convertDurationToExpiration = (amount: number, duration: Duration) => {
const durationFactor =
duration === Duration.hour ? 1 : duration === Duration.day ? 24 : duration === Duration.week ? 24 * 7 : 24 * 30
return Math.round(Date.now() + ms`1 hour` * durationFactor * amount)
}
interface GlobalDurationButtonProps {
amount: number
duration: Duration
globalAmount: number
globalDuration: Duration
setGlobalAmount: Dispatch<number>
setGlobalDuration: Dispatch<Duration>
}
const GlobalDurationButton = ({
amount,
duration,
globalAmount,
globalDuration,
setGlobalAmount,
setGlobalDuration,
}: GlobalDurationButtonProps) => {
const [isSelected, setIsSelected] = useState(false)
const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration)
const toggleSelected = () => {
if (isSelected) {
setGlobalExpiration(0)
}
setIsSelected(!isSelected)
}
useEffect(() => {
if (globalAmount === amount && globalDuration === duration) {
setIsSelected(true)
} else {
setIsSelected(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalAmount, globalDuration])
useEffect(() => {
if (isSelected) {
setGlobalAmount(amount)
setGlobalDuration(duration)
setGlobalExpiration(convertDurationToExpiration(amount, duration))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSelected])
return (
<Row
borderRadius="12"
backgroundColor="medGray"
height="44"
className={clsx(isSelected && styles.buttonSelected)}
onClick={toggleSelected}
width="max"
padding="14"
cursor="pointer"
>
<Box className={buttonTextMedium}>
{amount} {duration}
{pluralize(amount)}
</Box>
</Row>
)
}
enum SetPriceMethod {
SAME_PRICE,
FLOOR_PRICE,
PREV_LISTING,
}
const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
const sellAssets = useSellAsset((state) => state.sellAssets)
const [globalPriceMethod, setGlobalPriceMethod] = useState<SetPriceMethod>()
const [globalPrice, setGlobalPrice] = useState<number>()
const priceDropdownOptions: DropDownOption[] = useMemo(
() => [
{
displayText: 'Same price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.SAME_PRICE),
icon: <SamePriceIcon />,
},
{
displayText: 'Floor price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE),
icon: <FloorPriceIcon />,
},
{
displayText: 'Prev. listing',
onClick: () => setGlobalPriceMethod(SetPriceMethod.PREV_LISTING),
icon: <PrevListingIcon />,
},
],
[]
)
return (
<Column>
<Row className={headlineSmall}>Create your listings</Row>
<Row marginTop="20">
<Column
marginLeft={selectedMarkets.length > 1 ? '36' : '0'}
transition="500"
className={badge}
color="darkGray"
flex="2"
>
YOUR NFTS
</Column>
<Row flex={{ sm: '1', md: '3' }}>
<Column className={subheadSmall} style={{ flex: '1.5' }}>
<SortDropdown dropDownOptions={priceDropdownOptions} mini miniPrompt="Set price by" />
</Column>
<Column className={badge} color="darkGray" flex="1" display={{ sm: 'none', md: 'flex' }} textAlign="right">
MARKETPLACE FEE
</Column>
<Column className={badge} color="darkGray" flex="1" display={{ sm: 'none', md: 'flex' }} textAlign="right">
ROYALTIES
</Column>
<Column
className={badge}
color="darkGray"
style={{ flex: '1.5' }}
display={{ sm: 'none', md: 'flex' }}
textAlign="right"
>
YOU RECEIVE
</Column>
</Row>
</Row>
{sellAssets.map((asset) => {
return (
<>
<NFTListRow
asset={asset}
globalPriceMethod={globalPriceMethod}
globalPrice={globalPrice}
setGlobalPrice={setGlobalPrice}
selectedMarkets={selectedMarkets}
/>
{sellAssets.indexOf(asset) < sellAssets.length - 1 && <hr className={styles.nftDivider} />}
</>
)
})}
</Column>
)
}
enum WarningType {
BELOW_FLOOR = 'LISTING BELOW FLOOR ',
ALREADY_LISTED = 'ALREADY LISTED FOR ',
NONE = '',
}
interface PriceTextInputProps {
listPrice?: number
setListPrice: Dispatch<number | undefined>
isGlobalPrice: boolean
setGlobalOverride: Dispatch<boolean>
globalOverride: boolean
warning?: ListingWarning
asset: WalletAsset
}
const PriceTextInput = ({
listPrice,
setListPrice,
isGlobalPrice,
setGlobalOverride,
globalOverride,
warning,
asset,
}: PriceTextInputProps) => {
const [focused, setFocused] = useState(false)
const [warningType, setWarningType] = useState(WarningType.NONE)
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
useEffect(() => {
inputRef.current.value = listPrice !== undefined ? `${listPrice}` : ''
setWarningType(WarningType.NONE)
if (!warning && listPrice) {
if (listPrice < asset.floorPrice) setWarningType(WarningType.BELOW_FLOOR)
else if (asset.floor_sell_order_price && listPrice >= asset.floor_sell_order_price)
setWarningType(WarningType.ALREADY_LISTED)
} else if (warning && listPrice && listPrice >= 0) removeMarketplaceWarning(asset, warning)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice])
return (
<Column gap="12" position="relative">
<Row
color="medGray"
height="44"
width="min"
padding="4"
borderRadius="8"
borderWidth="1px"
borderStyle="solid"
borderColor={
warningType !== WarningType.NONE && !focused
? 'orange'
: isGlobalPrice
? 'genieBlue'
: listPrice != null
? 'darkGray'
: 'grey700'
}
>
<NumericInput
as="input"
pattern="[0-9]"
borderStyle="none"
className={body}
color={{ placeholder: 'darkGray', default: 'blackBlue' }}
placeholder="Set"
marginRight="0"
marginLeft="14"
backgroundColor="none"
style={{ width: '68px' }}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false)
}}
ref={inputRef}
onChange={(v: FormEvent<HTMLInputElement>) => {
const val = parseFloat(v.currentTarget.value)
setListPrice(isNaN(val) ? undefined : val)
}}
/>
<Box color={listPrice && listPrice >= 0 ? 'blackBlue' : 'darkGray'} marginRight="16">
&nbsp;ETH
</Box>
<Box
cursor="pointer"
display={isGlobalPrice || globalOverride ? 'block' : 'none'}
position="absolute"
style={{ marginTop: '-36px', marginLeft: '124px' }}
backgroundColor="white"
onClick={() => setGlobalOverride(!globalOverride)}
>
{globalOverride ? <AttachPriceIcon /> : <EditPriceIcon />}
</Box>
</Row>
<Row
top="52"
width="max"
className={badge}
color={warningType === WarningType.BELOW_FLOOR && !focused ? 'orange' : 'darkGray'}
position="absolute"
>
{focused ? (
<>
<Row display={asset.lastPrice ? 'flex' : 'none'} marginRight="8">
LAST: {formatEth(asset.lastPrice)} ETH
</Row>
<Row display={asset.floorPrice ? 'flex' : 'none'}>FLOOR: {formatEth(asset.floorPrice)} ETH</Row>
</>
) : (
<>
{warning
? warning.message
: warningType !== WarningType.NONE && (
<>
{warningType}
{warningType === WarningType.BELOW_FLOOR
? formatEth(asset.floorPrice)
: formatEth(asset.floor_sell_order_price)}
ETH
<Box
color={warningType === WarningType.BELOW_FLOOR ? 'genieBlue' : 'orange'}
marginLeft="8"
cursor="pointer"
onClick={() => {
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
setWarningType(WarningType.NONE)
}}
>
{warningType === WarningType.BELOW_FLOOR ? 'DISMISS' : 'REMOVE ITEM'}
</Box>
</>
)}
</>
)}
</Row>
</Column>
)
}
const EthPriceDisplay = ({ ethPrice = 0 }: { ethPrice?: number }) => {
const [ethConversion, setEthConversion] = useState(3000)
useEffect(() => {
fetchPrice().then((price) => {
setEthConversion(price ?? 0)
})
}, [])
return (
<Column width="full">
<Row width="full" justifyContent="flex-end">
<Box className={subhead} color={ethPrice !== 0 ? 'blackBlue' : 'darkGray'} marginLeft="auto">
{formatEth(ethPrice)} ETH
</Box>
{ethPrice !== 0 && (
<Box className={body} color="darkGray" marginLeft="12" marginRight="0">
{formatUsdPrice(ethPrice * ethConversion)}
</Box>
)}
</Row>
</Column>
)
}
function maxMarketFee(markets: ListingMarket[]): number {
let max = -1
markets.forEach((market) => {
if (market.fee > max) {
max = market.fee
}
})
return max
}
interface MarketplaceRowProps {
globalPriceMethod?: SetPriceMethod
globalPrice?: number
setGlobalPrice: Dispatch<number | undefined>
selectedMarkets: ListingMarket[]
removeMarket?: () => void
asset: WalletAsset
showMarketplaceLogo: boolean
}
const MarketplaceRow = ({
globalPriceMethod,
globalPrice,
setGlobalPrice,
selectedMarkets,
removeMarket = undefined,
asset,
showMarketplaceLogo,
}: MarketplaceRowProps) => {
const [listPrice, setListPrice] = useState<number>()
const [globalOverride, setGlobalOverride] = useState(false)
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
const setAssetListPrice = useSellAsset((state) => state.setAssetListPrice)
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
const marketplaceFee = selectedMarkets.length > 0 ? maxMarketFee(selectedMarkets) : 0
const price = showGlobalPrice ? globalPrice : listPrice
const feeInEth = price && (price * (asset.creatorPercentage * 100 + marketplaceFee)) / 100
const userReceives = price && feeInEth && price - feeInEth
const profit = userReceives && asset.lastPrice && userReceives - asset.lastPrice
const profitPercent = profit && asset.lastPrice && Math.round(profit && (profit / asset.lastPrice) * 100)
useEffect(() => {
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
setListPrice(asset.floorPrice)
setGlobalPrice(asset.floorPrice)
} else if (globalPriceMethod === SetPriceMethod.PREV_LISTING) {
setListPrice(asset.lastPrice)
setGlobalPrice(asset.lastPrice)
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
listPrice && !globalPrice ? setGlobalPrice(listPrice) : setListPrice(globalPrice)
setGlobalOverride(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalPriceMethod])
useEffect(() => {
if (selectedMarkets.length)
for (const marketplace of selectedMarkets) setAssetListPrice(asset, listPrice, marketplace)
else setAssetListPrice(asset, listPrice)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice])
useEffect(() => {
let price: number | undefined = undefined
if (globalOverride) {
if (!listPrice) setListPrice(globalPrice)
price = listPrice ? listPrice : globalPrice
} else {
price = globalPrice
}
if (selectedMarkets.length) for (const marketplace of selectedMarkets) setAssetListPrice(asset, price, marketplace)
else setAssetListPrice(asset, price)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalOverride])
useEffect(() => {
if (globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride) {
if (selectedMarkets.length)
for (const marketplace of selectedMarkets) setAssetListPrice(asset, globalPrice, marketplace)
else setAssetListPrice(asset, globalPrice)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalPrice])
let warning: ListingWarning | undefined = undefined
if (asset.listingWarnings && asset.listingWarnings?.length > 0) {
if (showMarketplaceLogo) {
for (const listingWarning of asset.listingWarnings) {
if (listingWarning.marketplace.name === selectedMarkets[0].name) warning = listingWarning
}
} else {
warning = asset.listingWarnings[0]
}
}
return (
<Row transition="500" marginLeft={selectedMarkets.length > 1 ? '20' : '0'}>
{showMarketplaceLogo && (
<Column
position="relative"
cursor="pointer"
onMouseEnter={handleHover}
onMouseLeave={handleHover}
style={{ marginLeft: '-28px' }}
onClick={(e) => {
e.stopPropagation()
removeAssetMarketplace(asset, selectedMarkets[0])
removeMarket && removeMarket()
}}
>
<Box className={styles.removeMarketplace} visibility={hovered ? 'visible' : 'hidden'} position="absolute">
<Box as="img" width="32" src={'/nft/svgs/minusCircle.svg'} alt="Remove item" />
</Box>
<Box
as="img"
alt={selectedMarkets[0].name}
width="28"
height="28"
borderRadius="4"
objectFit="cover"
src={selectedMarkets[0].icon}
marginRight="16"
/>
</Column>
)}
<Column style={{ flex: '1.5' }}>
{globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride ? (
<PriceTextInput
listPrice={globalPrice}
setListPrice={setGlobalPrice}
isGlobalPrice={true}
setGlobalOverride={setGlobalOverride}
globalOverride={globalOverride}
warning={warning}
asset={asset}
/>
) : (
<PriceTextInput
listPrice={listPrice}
setListPrice={setListPrice}
isGlobalPrice={false}
setGlobalOverride={setGlobalOverride}
globalOverride={globalOverride}
warning={warning}
asset={asset}
/>
)}
</Column>
<Row flex="1" display={{ sm: 'none', md: 'flex' }}>
<Box className={body} color="darkGray" width="full" textAlign="right">
{marketplaceFee > 0 ? marketplaceFee + (selectedMarkets.length > 1 ? '% MAX' : '%') : '--%'}
</Box>
</Row>
<Row flex="1" display={{ sm: 'none', md: 'flex' }}>
<Box className={body} color="darkGray" width="full" textAlign="right">
{(asset.creatorPercentage * 100).toFixed(1)}%
</Box>
</Row>
<Row style={{ flex: '1.5' }} display={{ sm: 'none', md: 'flex' }}>
<Column width="full">
<EthPriceDisplay ethPrice={userReceives} />
{(showGlobalPrice ? globalPrice && globalPrice !== 0 : listPrice !== 0) && (
<Row marginTop="4" width="full" fontSize="12" color="darkGray">
{profit ? <Box marginLeft="auto">Profit: {formatEth(profit)} ETH</Box> : null}
{profitPercent ? (
<Box marginLeft="8" marginRight="0">
({profitPercent > 0 && '+'}
{profitPercent > 1000 ? Math.round(profitPercent / 1000) + 'K' : profitPercent}%)
</Box>
) : null}
</Row>
)}
</Column>
</Row>
</Row>
)
}
export interface NFTListRowProps {
asset: WalletAsset
globalPriceMethod?: SetPriceMethod
setGlobalPrice: Dispatch<number | undefined>
globalPrice?: number
selectedMarkets: ListingMarket[]
}
const NFTListRow = ({ asset, globalPriceMethod, globalPrice, setGlobalPrice, selectedMarkets }: NFTListRowProps) => {
const [expandMarketplaceRows, setExpandMarketplaceRows] = useState(false)
const removeAsset = useSellAsset((state) => state.removeSellAsset)
const [localMarkets, setLocalMarkets] = useState([])
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
useEffect(() => {
setLocalMarkets(JSON.parse(JSON.stringify(selectedMarkets)))
selectedMarkets.length < 2 && setExpandMarketplaceRows(false)
}, [selectedMarkets])
return (
<Row marginTop="24" marginBottom="24">
<Row flexWrap="nowrap" flex="2" marginTop="0" marginBottom="auto" minWidth="0">
<Box
transition="500"
style={{
maxWidth: localMarkets.length > 1 ? '28px' : '0',
opacity: localMarkets.length > 1 ? '1' : '0',
}}
cursor="pointer"
onClick={() => setExpandMarketplaceRows(!expandMarketplaceRows)}
>
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
</Box>
<Box
position="relative"
cursor="pointer"
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={() => {
removeAsset(asset)
}}
>
<Box className={styles.removeAsset} visibility={hovered ? 'visible' : 'hidden'} position="absolute">
<Box as="img" width="32" src={'/nft/svgs/minusCircle.svg'} alt="Remove item" />
</Box>
<Box
as="img"
alt={asset.name}
width="48"
height="48"
borderRadius="8"
marginLeft={localMarkets.length > 1 ? '8' : '0'}
marginRight="8"
transition="500"
src={asset.image_url || '/nft/svgs/image-placeholder.svg'}
/>
</Box>
<Column gap="4" minWidth="0">
<Box paddingRight="8" overflow="hidden" textOverflow="ellipsis" className={subheadSmall}>
{asset.name ? asset.name : `#${asset.tokenId}`}
</Box>
<Box paddingRight="8" overflow="hidden" textOverflow="ellipsis" className={bodySmall}>
{asset.collection?.name}
{asset.collectionIsVerified && <VerifiedIcon style={{ marginBottom: '-5px' }} />}
</Box>
</Column>
</Row>
<Column flex={{ sm: '1', md: '3' }} gap="24">
{expandMarketplaceRows ? (
localMarkets.map((market, index) => {
return (
<MarketplaceRow
globalPriceMethod={globalPriceMethod}
globalPrice={globalPrice}
setGlobalPrice={setGlobalPrice}
selectedMarkets={[market]}
removeMarket={() => localMarkets.splice(index, 1)}
asset={asset}
showMarketplaceLogo={true}
key={index}
/>
)
})
) : (
<MarketplaceRow
globalPriceMethod={globalPriceMethod}
globalPrice={globalPrice}
setGlobalPrice={setGlobalPrice}
selectedMarkets={localMarkets}
asset={asset}
showMarketplaceLogo={false}
/>
)}
</Column>
</Row>
)
}
export const ListPage = () => { export const ListPage = () => {
return <div>List your NFTs</div> const { setSellPageState } = useSellPageState()
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[2]]) // default marketplace: x2y2
const toggleBag = useBag((s) => s.toggleBag)
const listings = useNFTList((state) => state.listings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListingStatus = useNFTList((state) => state.setListingStatus)
useEffect(() => {
const state = getListingState(collectionsRequiringApproval, listings)
if (state.allListingsApproved) setListingStatus(ListingStatus.APPROVED)
else if (state.anyPaused && !state.anyActiveFailures && !state.anyActiveSigning && !state.anyActiveRejections) {
setListingStatus(ListingStatus.CONTINUE)
} else if (state.anyPaused) setListingStatus(ListingStatus.PAUSED)
else if (state.anyActiveSigning) setListingStatus(ListingStatus.SIGNING)
else if (state.allListingsPending || (state.allCollectionsPending && state.allListingsDefined))
setListingStatus(ListingStatus.PENDING)
else if (state.anyActiveFailures && listingStatus !== ListingStatus.PAUSED) setListingStatus(ListingStatus.FAILED)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listings, collectionsRequiringApproval])
useEffect(() => {
setGlobalMarketplaces(selectedMarkets)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMarkets])
return (
<Column display="flex" flexWrap="nowrap">
<Column marginLeft="14" display={{ sm: 'none', lg: 'flex' }}>
<Box
aria-label="Back"
as="button"
border="none"
onClick={() => setSellPageState(SellPageStateType.SELECTING)}
type="button"
backgroundColor="transparent"
cursor="pointer"
width="min"
>
<BackArrowIcon fill={themeVars.colors.darkGray} />
</Box>
</Column>
<Column as="section" gap="48" paddingLeft="18" paddingRight="48" width="full">
<Row flexWrap={{ sm: 'wrap', lg: 'nowrap' }}>
<SelectMarketplacesModal setSelectedMarkets={setSelectedMarkets} selectedMarkets={selectedMarkets} />
<SetDurationModal />
</Row>
<NFTListingsGrid selectedMarkets={selectedMarkets} />
</Column>
<Box display={{ sm: 'flex', md: 'none' }} marginTop="14" marginX="16" marginBottom="32">
<ListingButton onClick={toggleBag} buttonText={'Continue listing'} />
</Box>
</Column>
)
} }
...@@ -71,11 +71,10 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -71,11 +71,10 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
if (asset.newListings) { if (asset.newListings) {
for (const listing of asset.newListings) { for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.push([asset, listing]) if (!listing.price) listingsMissingPrice.push([asset, listing])
else if (isNaN(parseFloat(listing.price)) || parseFloat(listing.price) < 0) else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
invalidPrices.push([asset, listing]) else if (listing.price < asset.floorPrice && !listing.overrideFloorPrice)
else if (parseFloat(listing.price) < asset.floorPrice && !listing.overrideFloorPrice)
listingsBelowFloor.push([asset, listing]) listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && parseFloat(listing.price) > asset.floor_sell_order_price) else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing]) listingsAboveSellOrderFloor.push([asset, listing])
} }
} }
......
...@@ -49,9 +49,7 @@ export const ListingSection = ({ ...@@ -49,9 +49,7 @@ export const ListingSection = ({
function getListingRowPrice(row: AssetRow): number | undefined { function getListingRowPrice(row: AssetRow): number | undefined {
const listingRow = row as ListingRow const listingRow = row as ListingRow
const newListings = listingRow.asset.newListings const newListings = listingRow.asset.newListings
return parseFloat( return newListings?.find((listing) => listing.marketplace.name === listingRow.marketplace.name)?.price ?? 0
newListings?.find((listing) => listing.marketplace.name === listingRow.marketplace.name)?.price ?? '0'
)
} }
const allApproved = !notAllApproved && rows.length > 0 && !isSuccessScreen const allApproved = !notAllApproved && rows.length > 0 && !isSuccessScreen
......
...@@ -129,13 +129,11 @@ export async function signListingRow( ...@@ -129,13 +129,11 @@ export async function signListingRow(
export const getTotalEthValue = (sellAssets: WalletAsset[]) => { export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
const total = sellAssets.reduce((total, asset: WalletAsset) => { const total = sellAssets.reduce((total, asset: WalletAsset) => {
if (asset.newListings?.length) { if (asset.newListings?.length) {
const maxListing = asset.newListings.reduce((a, b) => const maxListing = asset.newListings.reduce((a, b) => ((a.price ?? 0) > (b.price ?? 0) ? a : b))
parseFloat(a.price ?? '0') > parseFloat(b.price ?? '0') ? a : b
)
return ( return (
total + total +
parseFloat(maxListing.price ?? '0') - (maxListing.price ?? 0) -
parseFloat(maxListing.price ?? '0') * (maxListing.marketplace.fee / 100 + asset.creatorPercentage) (maxListing.price ?? 0) * (maxListing.marketplace.fee / 100 + asset.creatorPercentage)
) )
} }
return total return total
......
...@@ -10,7 +10,7 @@ interface SellAssetState { ...@@ -10,7 +10,7 @@ interface SellAssetState {
removeSellAsset: (asset: WalletAsset) => void removeSellAsset: (asset: WalletAsset) => void
reset: () => void reset: () => void
setGlobalExpiration: (expirationTime: number) => void setGlobalExpiration: (expirationTime: number) => void
setAssetListPrice: (asset: WalletAsset, price: string, marketplace?: ListingMarket) => void setAssetListPrice: (asset: WalletAsset, price?: number, marketplace?: ListingMarket) => void
setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
......
...@@ -29,7 +29,7 @@ export interface SellOrder { ...@@ -29,7 +29,7 @@ export interface SellOrder {
} }
export interface Listing { export interface Listing {
price?: string price?: number
marketplace: ListingMarket marketplace: ListingMarket
overrideFloorPrice?: boolean overrideFloorPrice?: boolean
} }
...@@ -65,7 +65,7 @@ export interface WalletAsset { ...@@ -65,7 +65,7 @@ export interface WalletAsset {
floor_sell_order_price: number floor_sell_order_price: number
// Used for creating new listings // Used for creating new listings
expirationTime?: number expirationTime?: number
marketAgnosticPrice?: string marketAgnosticPrice?: number
newListings?: Listing[] newListings?: Listing[]
marketplaces?: ListingMarket[] marketplaces?: ListingMarket[]
listingWarnings?: ListingWarning[] listingWarnings?: ListingWarning[]
......
...@@ -10,6 +10,16 @@ export const formatUsdPrice = (price: number) => { ...@@ -10,6 +10,16 @@ export const formatUsdPrice = (price: number) => {
} }
} }
export const formatEth = (price: number) => {
if (price > 1000000) {
return `${Math.round(price / 1000000)}M`
} else if (price > 1000) {
return `${Math.round(price / 1000)}K`
} else {
return `${Math.round(price * 100 + Number.EPSILON) / 100}`
}
}
export const formatUSDPriceWithCommas = (price: number) => { export const formatUSDPriceWithCommas = (price: number) => {
return `$${Math.round(price) return `$${Math.round(price)
.toString() .toString()
......
...@@ -15,12 +15,9 @@ import { ...@@ -15,12 +15,9 @@ import {
} from 'nft/queries/openSea' } from 'nft/queries/openSea'
import ERC721 from '../../abis/erc721.json' import ERC721 from '../../abis/erc721.json'
import { PostOpenSeaSellOrder } from '../queries' import { createLooksRareOrder, newX2Y2Order, PostOpenSeaSellOrder } from '../queries'
import { createLooksRareOrder } from '../queries'
import { newX2Y2Order } from '../queries'
import { INVERSE_BASIS_POINTS, OPENSEA_DEFAULT_FEE, OPENSEA_FEE_ADDRESS } from '../queries/openSea' import { INVERSE_BASIS_POINTS, OPENSEA_DEFAULT_FEE, OPENSEA_FEE_ADDRESS } from '../queries/openSea'
import { ListingMarket, WalletAsset } from '../types' import { ListingMarket, ListingStatus, WalletAsset } from '../types'
import { ListingStatus } 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[] = [
...@@ -128,7 +125,7 @@ export async function signListing( ...@@ -128,7 +125,7 @@ export async function signListing(
switch (marketplace.name) { switch (marketplace.name) {
case 'OpenSea': case 'OpenSea':
try { try {
const listingInWei = parseEther(listingPrice) const listingInWei = parseEther(`${listingPrice}`)
const { sellerFee, openseaFee, creatorFee } = getConsiderationItems(asset, listingInWei, signerAddress) const { sellerFee, openseaFee, creatorFee } = getConsiderationItems(asset, listingInWei, signerAddress)
const considerationItems = [sellerFee, openseaFee, creatorFee].filter( const considerationItems = [sellerFee, openseaFee, creatorFee].filter(
(item): item is ConsiderationInputItem => item !== undefined (item): item is ConsiderationInputItem => item !== undefined
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment