Commit 9cac9f82 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

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: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 5def0dd1
...@@ -314,6 +314,7 @@ export const ListPage = () => { ...@@ -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)` ...@@ -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;
} }
` `
......
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 `}
/>
&nbsp;
<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' ...@@ -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` ...@@ -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 }>` ...@@ -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) => { ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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} />
&nbsp; <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)} &nbsp;
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' ...@@ -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 { ...@@ -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>()( ...@@ -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' }
) )
......
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