feat: [ListV2] error and warning states (#5921)
* update error message for price inputs * add grid and button warning states * add list below floor warning modal * only warn for prices 20% below floor * highlight unentered price in red on button press * missing dependency * updated modal name and mobile height * add new file * fix column presence * rookie mistake * bulk zustand imports * below floor threshold * move issue check higher * cleanup mouseEvent * rename color var --------- Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
parent
5def0dd166
commit
9cac9f8299
@ -1,18 +1,25 @@
|
|||||||
|
import { Plural, t } from '@lingui/macro'
|
||||||
|
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||||
import ms from 'ms.macro'
|
import ms from 'ms.macro'
|
||||||
import { Box } from 'nft/components/Box'
|
import { Box } from 'nft/components/Box'
|
||||||
import { Row } from 'nft/components/Flex'
|
import { Row } from 'nft/components/Flex'
|
||||||
import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons'
|
import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons'
|
||||||
|
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
|
||||||
import { bodySmall } from 'nft/css/common.css'
|
import { bodySmall } from 'nft/css/common.css'
|
||||||
import { themeVars } from 'nft/css/sprinkles.css'
|
import { themeVars } from 'nft/css/sprinkles.css'
|
||||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||||
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
||||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTheme } from 'styled-components/macro'
|
||||||
|
import shallow from 'zustand/shallow'
|
||||||
|
|
||||||
import * as styles from './ListingModal.css'
|
import * as styles from './ListingModal.css'
|
||||||
import { getListings } from './utils'
|
import { getListings } from './utils'
|
||||||
|
|
||||||
|
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
|
||||||
|
|
||||||
interface ListingButtonProps {
|
interface ListingButtonProps {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
buttonText: string
|
buttonText: string
|
||||||
@ -20,18 +27,46 @@ interface ListingButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
|
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
|
||||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
const {
|
||||||
const addMarketplaceWarning = useSellAsset((state) => state.addMarketplaceWarning)
|
addMarketplaceWarning,
|
||||||
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
|
sellAssets,
|
||||||
const listingStatus = useNFTList((state) => state.listingStatus)
|
removeAllMarketplaceWarnings,
|
||||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
showResolveIssues,
|
||||||
const setListings = useNFTList((state) => state.setListings)
|
toggleShowResolveIssues,
|
||||||
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
|
} = useSellAsset(
|
||||||
|
({
|
||||||
|
addMarketplaceWarning,
|
||||||
|
sellAssets,
|
||||||
|
removeAllMarketplaceWarnings,
|
||||||
|
showResolveIssues,
|
||||||
|
toggleShowResolveIssues,
|
||||||
|
}) => ({
|
||||||
|
addMarketplaceWarning,
|
||||||
|
sellAssets,
|
||||||
|
removeAllMarketplaceWarnings,
|
||||||
|
showResolveIssues,
|
||||||
|
toggleShowResolveIssues,
|
||||||
|
}),
|
||||||
|
shallow
|
||||||
|
)
|
||||||
|
const { listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||||
|
({ listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
|
||||||
|
listingStatus,
|
||||||
|
setListingStatus,
|
||||||
|
setListings,
|
||||||
|
setCollectionsRequiringApproval,
|
||||||
|
}),
|
||||||
|
shallow
|
||||||
|
)
|
||||||
|
|
||||||
|
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||||
const [showWarning, setShowWarning] = useState(false)
|
const [showWarning, setShowWarning] = useState(false)
|
||||||
const [canContinue, setCanContinue] = useState(false)
|
const [canContinue, setCanContinue] = useState(false)
|
||||||
|
const [issues, setIssues] = useState(0)
|
||||||
|
const theme = useTheme()
|
||||||
const warningRef = useRef<HTMLDivElement>(null)
|
const warningRef = useRef<HTMLDivElement>(null)
|
||||||
useOnClickOutside(warningRef, () => {
|
useOnClickOutside(warningRef, () => {
|
||||||
setShowWarning(false)
|
!isNftListV2 && setShowWarning(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -71,13 +106,30 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
|||||||
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(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
|
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
|
||||||
else if (listing.price < (asset?.floorPrice ?? 0) && !listing.overrideFloorPrice)
|
else if (
|
||||||
|
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
|
||||||
|
!listing.overrideFloorPrice
|
||||||
|
)
|
||||||
listingsBelowFloor.push([asset, listing])
|
listingsBelowFloor.push([asset, listing])
|
||||||
else if (asset.floor_sell_order_price && 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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// set number of issues
|
||||||
|
if (isNftListV2) {
|
||||||
|
const foundIssues =
|
||||||
|
Number(missingExpiration) +
|
||||||
|
Number(overMaxExpiration) +
|
||||||
|
listingsMissingPrice.length +
|
||||||
|
listingsAboveSellOrderFloor.length
|
||||||
|
setIssues(foundIssues)
|
||||||
|
!foundIssues && showResolveIssues && toggleShowResolveIssues()
|
||||||
|
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
|
||||||
|
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
|
||||||
|
toggleShowResolveIssues()
|
||||||
|
}
|
||||||
|
|
||||||
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
|
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
|
||||||
setCanContinue(continueCheck)
|
setCanContinue(continueCheck)
|
||||||
return [
|
return [
|
||||||
@ -90,7 +142,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
|||||||
listingsAboveSellOrderFloor,
|
listingsAboveSellOrderFloor,
|
||||||
invalidPrices,
|
invalidPrices,
|
||||||
]
|
]
|
||||||
}, [sellAssets])
|
}, [isNftListV2, sellAssets, showResolveIssues, toggleShowResolveIssues])
|
||||||
|
|
||||||
const [disableListButton, warningMessage] = useMemo(() => {
|
const [disableListButton, warningMessage] = useMemo(() => {
|
||||||
const disableListButton =
|
const disableListButton =
|
||||||
@ -158,94 +210,108 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
const warningWrappedClick = () => {
|
const warningWrappedClick = () => {
|
||||||
if ((!disableListButton && canContinue) || showWarningOverride) onClick()
|
if ((!disableListButton && canContinue) || showWarningOverride) {
|
||||||
else addWarningMessages()
|
if (issues && isNftListV2 && !showResolveIssues) toggleShowResolveIssues()
|
||||||
|
else if (listingsBelowFloor.length) setShowWarning(true)
|
||||||
|
else onClick()
|
||||||
|
} else addWarningMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" width="full">
|
<>
|
||||||
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
|
<Box position="relative" width="full">
|
||||||
<Row
|
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
|
||||||
className={`${bodySmall} ${styles.warningTooltip}`}
|
<Row
|
||||||
transition="250"
|
className={`${bodySmall} ${styles.warningTooltip}`}
|
||||||
onClick={() => setShowWarning(false)}
|
transition="250"
|
||||||
color="textSecondary"
|
onClick={() => setShowWarning(false)}
|
||||||
zIndex="3"
|
color="textSecondary"
|
||||||
borderRadius="4"
|
zIndex="3"
|
||||||
backgroundColor="backgroundSurface"
|
borderRadius="4"
|
||||||
height={!disableListButton ? '64' : '36'}
|
backgroundColor="backgroundSurface"
|
||||||
maxWidth="276"
|
height={!disableListButton ? '64' : '36'}
|
||||||
position="absolute"
|
maxWidth="276"
|
||||||
left="24"
|
position="absolute"
|
||||||
bottom="52"
|
left="24"
|
||||||
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
|
bottom="52"
|
||||||
style={{ maxWidth: !disableListButton ? '225px' : '' }}
|
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
|
||||||
ref={warningRef}
|
style={{ maxWidth: !disableListButton ? '225px' : '' }}
|
||||||
>
|
ref={warningRef}
|
||||||
<HazardIcon />
|
>
|
||||||
<Box marginLeft="4" marginRight="8">
|
<HazardIcon />
|
||||||
{warningMessage}
|
<Box marginLeft="4" marginRight="8">
|
||||||
</Box>
|
{warningMessage}
|
||||||
{disableListButton ? (
|
|
||||||
<Box paddingTop="6">
|
|
||||||
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
{disableListButton ? (
|
||||||
<Row
|
<Box paddingTop="6">
|
||||||
marginLeft="72"
|
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
|
||||||
cursor="pointer"
|
</Box>
|
||||||
color="accentAction"
|
) : (
|
||||||
onClick={() => {
|
<Row
|
||||||
setShowWarning(false)
|
marginLeft="72"
|
||||||
setCanContinue(true)
|
cursor="pointer"
|
||||||
onClick()
|
color="accentAction"
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
setShowWarning(false)
|
||||||
Continue
|
setCanContinue(true)
|
||||||
<ArrowRightIcon height="20" width="20" />
|
onClick()
|
||||||
</Row>
|
}}
|
||||||
)}
|
>
|
||||||
</Row>
|
Continue
|
||||||
)}
|
<ArrowRightIcon height="20" width="20" />
|
||||||
<Box
|
</Row>
|
||||||
as="button"
|
)}
|
||||||
border="none"
|
|
||||||
backgroundColor="accentAction"
|
|
||||||
cursor={
|
|
||||||
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
|
||||||
disableListButton
|
|
||||||
? 'default'
|
|
||||||
: 'pointer'
|
|
||||||
}
|
|
||||||
color="explicitWhite"
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
opacity:
|
|
||||||
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
|
||||||
disableListButton
|
|
||||||
? 0.3
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
|
|
||||||
<Row gap="8">
|
|
||||||
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
|
|
||||||
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
|
|
||||||
</Row>
|
</Row>
|
||||||
) : listingStatus === ListingStatus.APPROVED ? (
|
|
||||||
'Complete!'
|
|
||||||
) : listingStatus === ListingStatus.PAUSED ? (
|
|
||||||
'Paused'
|
|
||||||
) : listingStatus === ListingStatus.FAILED ? (
|
|
||||||
'Try again'
|
|
||||||
) : listingStatus === ListingStatus.CONTINUE ? (
|
|
||||||
'Continue'
|
|
||||||
) : (
|
|
||||||
buttonText
|
|
||||||
)}
|
)}
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
border="none"
|
||||||
|
backgroundColor={showResolveIssues ? 'accentFailure' : 'accentAction'}
|
||||||
|
cursor={
|
||||||
|
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
||||||
|
disableListButton
|
||||||
|
? 'default'
|
||||||
|
: 'pointer'
|
||||||
|
}
|
||||||
|
className={styles.button}
|
||||||
|
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
||||||
|
type="button"
|
||||||
|
style={{
|
||||||
|
color: showResolveIssues ? theme.accentTextDarkPrimary : theme.white,
|
||||||
|
opacity:
|
||||||
|
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
||||||
|
(disableListButton && !showResolveIssues)
|
||||||
|
? 0.3
|
||||||
|
: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
|
||||||
|
<Row gap="8">
|
||||||
|
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
|
||||||
|
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
|
||||||
|
</Row>
|
||||||
|
) : listingStatus === ListingStatus.APPROVED ? (
|
||||||
|
'Complete!'
|
||||||
|
) : listingStatus === ListingStatus.PAUSED ? (
|
||||||
|
'Paused'
|
||||||
|
) : listingStatus === ListingStatus.FAILED ? (
|
||||||
|
'Try again'
|
||||||
|
) : listingStatus === ListingStatus.CONTINUE ? (
|
||||||
|
'Continue'
|
||||||
|
) : showResolveIssues ? (
|
||||||
|
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
|
||||||
|
) : (
|
||||||
|
buttonText
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
{showWarning && (
|
||||||
|
<BelowFloorWarningModal
|
||||||
|
listingsBelowFloor={listingsBelowFloor}
|
||||||
|
closeModal={() => setShowWarning(false)}
|
||||||
|
startListing={onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -314,6 +314,7 @@ export const ListPage = () => {
|
|||||||
<ListingButton
|
<ListingButton
|
||||||
onClick={handleV2Click}
|
onClick={handleV2Click}
|
||||||
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
|
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
|
||||||
|
showWarningOverride={true}
|
||||||
/>
|
/>
|
||||||
</ListingButtonWrapper>
|
</ListingButtonWrapper>
|
||||||
</ProceedsAndButtonWrapper>
|
</ProceedsAndButtonWrapper>
|
||||||
|
@ -22,7 +22,7 @@ const PastPriceInfo = styled(Column)`
|
|||||||
display: none;
|
display: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
@media screen and (min-width: ${BREAKPOINTS.xxl}px) {
|
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
124
src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx
Normal file
124
src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Plural, t, Trans } from '@lingui/macro'
|
||||||
|
import { ButtonPrimary } from 'components/Button'
|
||||||
|
import Column from 'components/Column'
|
||||||
|
import { Portal } from 'nft/components/common/Portal'
|
||||||
|
import { Overlay } from 'nft/components/modals/Overlay'
|
||||||
|
import { Listing, WalletAsset } from 'nft/types'
|
||||||
|
import React from 'react'
|
||||||
|
import { AlertTriangle, X } from 'react-feather'
|
||||||
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
|
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||||
|
import { Z_INDEX } from 'theme/zIndex'
|
||||||
|
|
||||||
|
const ModalWrapper = styled(Column)`
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 420px;
|
||||||
|
z-index: ${Z_INDEX.modal};
|
||||||
|
background: ${({ theme }) => theme.backgroundSurface};
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||||
|
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const CloseIconWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`
|
||||||
|
const CloseIcon = styled(X)`
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const HazardIconWrap = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32px 120px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContinueButton = styled(ButtonPrimary)`
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-top: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const EditListings = styled.span`
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: ${({ theme }) => theme.accentAction};
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BelowFloorWarningModal = ({
|
||||||
|
listingsBelowFloor,
|
||||||
|
closeModal,
|
||||||
|
startListing,
|
||||||
|
}: {
|
||||||
|
listingsBelowFloor: [WalletAsset, Listing][]
|
||||||
|
closeModal: () => void
|
||||||
|
startListing: () => void
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const clickContinue = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
startListing()
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ModalWrapper>
|
||||||
|
<CloseIconWrapper>
|
||||||
|
<CloseIcon width={24} height={24} onClick={closeModal} />{' '}
|
||||||
|
</CloseIconWrapper>
|
||||||
|
<HazardIconWrap>
|
||||||
|
<AlertTriangle height={90} width={90} color={theme.accentCritical} />
|
||||||
|
</HazardIconWrap>
|
||||||
|
<ThemedText.HeadlineSmall lineHeight="28px" textAlign="center">
|
||||||
|
<Trans>Low listing price</Trans>
|
||||||
|
</ThemedText.HeadlineSmall>
|
||||||
|
<ThemedText.BodyPrimary textAlign="center">
|
||||||
|
<Plural
|
||||||
|
value={listingsBelowFloor.length !== 1 ? 2 : 1}
|
||||||
|
_1={t`One NFT is listed ${(
|
||||||
|
(1 - (listingsBelowFloor[0][1].price ?? 0) / (listingsBelowFloor[0][0].floorPrice ?? 0)) *
|
||||||
|
100
|
||||||
|
).toFixed(0)}% `}
|
||||||
|
other={t`${listingsBelowFloor.length} NFTs are listed significantly `}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Trans>below the collection’s floor price. Are you sure you want to continue?</Trans>
|
||||||
|
</ThemedText.BodyPrimary>
|
||||||
|
<ContinueButton onClick={clickContinue}>
|
||||||
|
<Trans>Continue</Trans>
|
||||||
|
</ContinueButton>
|
||||||
|
<EditListings onClick={closeModal}>
|
||||||
|
<Trans>Edit listings</Trans>
|
||||||
|
</EditListings>
|
||||||
|
</ModalWrapper>
|
||||||
|
<Overlay onClick={closeModal} />
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { useSellAsset } from 'nft/hooks'
|
|||||||
import { ListingWarning, WalletAsset } from 'nft/types'
|
import { ListingWarning, WalletAsset } from 'nft/types'
|
||||||
import { formatEth } from 'nft/utils/currency'
|
import { formatEth } from 'nft/utils/currency'
|
||||||
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
|
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
|
import { AlertTriangle } from 'react-feather'
|
||||||
import styled, { useTheme } from 'styled-components/macro'
|
import styled, { useTheme } from 'styled-components/macro'
|
||||||
import { BREAKPOINTS } from 'theme'
|
import { BREAKPOINTS } from 'theme'
|
||||||
import { colors } from 'theme/colors'
|
import { colors } from 'theme/colors'
|
||||||
@ -42,7 +43,11 @@ const GlobalPriceIcon = styled.div`
|
|||||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||||
`
|
`
|
||||||
|
|
||||||
const WarningMessage = styled(Row)<{ warningType: WarningType }>`
|
const WarningRow = styled(Row)`
|
||||||
|
gap: 4px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const WarningMessage = styled(Row)<{ $color: string }>`
|
||||||
top: 52px;
|
top: 52px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -50,17 +55,16 @@ const WarningMessage = styled(Row)<{ warningType: WarningType }>`
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? colors.red400 : theme.textSecondary)};
|
color: ${({ $color }) => $color};
|
||||||
|
|
||||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||||
right: unset;
|
right: unset;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const WarningAction = styled.div<{ warningType: WarningType }>`
|
const WarningAction = styled.div`
|
||||||
margin-left: 8px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? theme.accentAction : colors.red400)};
|
color: ${({ theme }) => theme.accentAction};
|
||||||
`
|
`
|
||||||
|
|
||||||
enum WarningType {
|
enum WarningType {
|
||||||
@ -73,10 +77,10 @@ const getWarningMessage = (warning: WarningType) => {
|
|||||||
let message = <></>
|
let message = <></>
|
||||||
switch (warning) {
|
switch (warning) {
|
||||||
case WarningType.BELOW_FLOOR:
|
case WarningType.BELOW_FLOOR:
|
||||||
message = <Trans>LISTING BELOW FLOOR </Trans>
|
message = <Trans>below floor price.</Trans>
|
||||||
break
|
break
|
||||||
case WarningType.ALREADY_LISTED:
|
case WarningType.ALREADY_LISTED:
|
||||||
message = <Trans>ALREADY LISTED FOR </Trans>
|
message = <Trans>Already listed at</Trans>
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
@ -107,6 +111,7 @@ export const PriceTextInput = ({
|
|||||||
const [warningType, setWarningType] = useState(WarningType.NONE)
|
const [warningType, setWarningType] = useState(WarningType.NONE)
|
||||||
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
|
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
|
||||||
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
|
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
|
||||||
|
const showResolveIssues = useSellAsset((state) => state.showResolveIssues)
|
||||||
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
|
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
@ -121,9 +126,16 @@ export const PriceTextInput = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [listPrice])
|
}, [listPrice])
|
||||||
|
|
||||||
const borderColor =
|
const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
|
||||||
warningType !== WarningType.NONE && !focused
|
|
||||||
|
const warningColor =
|
||||||
|
showResolveIssues && !listPrice
|
||||||
? colors.red400
|
? colors.red400
|
||||||
|
: warningType !== WarningType.NONE && !focused
|
||||||
|
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
|
||||||
|
warningType === WarningType.ALREADY_LISTED
|
||||||
|
? colors.red400
|
||||||
|
: theme.accentWarning
|
||||||
: isGlobalPrice
|
: isGlobalPrice
|
||||||
? theme.accentAction
|
? theme.accentAction
|
||||||
: listPrice != null
|
: listPrice != null
|
||||||
@ -132,7 +144,7 @@ export const PriceTextInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PriceTextInputWrapper>
|
<PriceTextInputWrapper>
|
||||||
<InputWrapper borderColor={borderColor}>
|
<InputWrapper borderColor={warningColor}>
|
||||||
<NumericInput
|
<NumericInput
|
||||||
as="input"
|
as="input"
|
||||||
pattern="[0-9]"
|
pattern="[0-9]"
|
||||||
@ -164,27 +176,27 @@ export const PriceTextInput = ({
|
|||||||
</GlobalPriceIcon>
|
</GlobalPriceIcon>
|
||||||
)}
|
)}
|
||||||
</InputWrapper>
|
</InputWrapper>
|
||||||
<WarningMessage warningType={warningType}>
|
<WarningMessage $color={warningColor}>
|
||||||
{warning
|
{warning
|
||||||
? warning.message
|
? warning.message
|
||||||
: warningType !== WarningType.NONE && (
|
: warningType !== WarningType.NONE && (
|
||||||
<>
|
<WarningRow>
|
||||||
{getWarningMessage(warningType)}
|
<AlertTriangle height={16} width={16} color={warningColor} />
|
||||||
|
<span>
|
||||||
{warningType === WarningType.BELOW_FLOOR
|
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
|
||||||
? formatEth(asset?.floorPrice ?? 0)
|
{getWarningMessage(warningType)}
|
||||||
: formatEth(asset?.floor_sell_order_price ?? 0)}
|
|
||||||
ETH
|
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
|
||||||
|
</span>
|
||||||
<WarningAction
|
<WarningAction
|
||||||
warningType={warningType}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
|
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
|
||||||
setWarningType(WarningType.NONE)
|
setWarningType(WarningType.NONE)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{warningType === WarningType.BELOW_FLOOR ? <Trans>DISMISS</Trans> : <Trans>REMOVE ITEM</Trans>}
|
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
|
||||||
</WarningAction>
|
</WarningAction>
|
||||||
</>
|
</WarningRow>
|
||||||
)}
|
)}
|
||||||
</WarningMessage>
|
</WarningMessage>
|
||||||
</PriceTextInputWrapper>
|
</PriceTextInputWrapper>
|
||||||
|
@ -5,6 +5,7 @@ import { ListingMarket, ListingWarning, WalletAsset } from '../types'
|
|||||||
|
|
||||||
interface SellAssetState {
|
interface SellAssetState {
|
||||||
sellAssets: WalletAsset[]
|
sellAssets: WalletAsset[]
|
||||||
|
showResolveIssues: boolean
|
||||||
selectSellAsset: (asset: WalletAsset) => void
|
selectSellAsset: (asset: WalletAsset) => void
|
||||||
removeSellAsset: (asset: WalletAsset) => void
|
removeSellAsset: (asset: WalletAsset) => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
@ -16,12 +17,14 @@ interface SellAssetState {
|
|||||||
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
|
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
|
||||||
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
|
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
|
||||||
removeAllMarketplaceWarnings: () => void
|
removeAllMarketplaceWarnings: () => void
|
||||||
|
toggleShowResolveIssues: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSellAsset = create<SellAssetState>()(
|
export const useSellAsset = create<SellAssetState>()(
|
||||||
devtools(
|
devtools(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
sellAssets: [],
|
sellAssets: [],
|
||||||
|
showResolveIssues: false,
|
||||||
selectSellAsset: (asset) =>
|
selectSellAsset: (asset) =>
|
||||||
set(({ sellAssets }) => {
|
set(({ sellAssets }) => {
|
||||||
if (sellAssets.length === 0) return { sellAssets: [asset] }
|
if (sellAssets.length === 0) return { sellAssets: [asset] }
|
||||||
@ -153,6 +156,11 @@ export const useSellAsset = create<SellAssetState>()(
|
|||||||
return { sellAssets: assetsCopy }
|
return { sellAssets: assetsCopy }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
toggleShowResolveIssues: () => {
|
||||||
|
set(({ showResolveIssues }) => {
|
||||||
|
return { showResolveIssues: !showResolveIssues }
|
||||||
|
})
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{ name: 'useSelectAsset' }
|
{ name: 'useSelectAsset' }
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user