feat: [ListV2] Several Small Polish Changes (#5998)

* NFT-1075 update floor and last price to use body primary

* NFT-1080 update image thumbnail size and adjusted row accordingly

* NFT-1081 force page refresh on success screen

* NFT-1089 update same price icons

* NFT-1082 show warning colors over focus

* remove unused state var

* replace margin with padding for sticky header

* NFT-1109 properly calc if listing date is over 6mo

* NFT-1076 change listing button text to light and mobile profile bar to backgroundSurface

* NFT-1079 persist row data when listing to multiple markets

* perf improvement

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-02-21 13:56:26 -08:00 committed by GitHub
parent 9ac28a4571
commit 6efe8f3260
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 65 deletions

@ -96,11 +96,6 @@ export enum Currency {
Usd = 'USD'
}
export enum DatasourceProvider {
Alternate = 'ALTERNATE',
Legacy = 'LEGACY'
}
export type Dimensions = {
__typename?: 'Dimensions';
height?: Maybe<Scalars['Float']>;
@ -145,6 +140,49 @@ export enum MarketSortableField {
Volume = 'VOLUME'
}
export type NftActivity = {
__typename?: 'NftActivity';
address: Scalars['String'];
asset?: Maybe<NftAsset>;
fromAddress: Scalars['String'];
id: Scalars['ID'];
marketplace?: Maybe<NftMarketplace>;
orderStatus?: Maybe<OrderStatus>;
price?: Maybe<Amount>;
quantity?: Maybe<Scalars['Int']>;
timestamp: Scalars['Int'];
toAddress?: Maybe<Scalars['String']>;
tokenId?: Maybe<Scalars['String']>;
transactionHash?: Maybe<Scalars['String']>;
type: NftActivityType;
url?: Maybe<Scalars['String']>;
};
export type NftActivityConnection = {
__typename?: 'NftActivityConnection';
edges: Array<NftActivityEdge>;
pageInfo: PageInfo;
};
export type NftActivityEdge = {
__typename?: 'NftActivityEdge';
cursor: Scalars['String'];
node: NftActivity;
};
export type NftActivityFilterInput = {
activityTypes?: InputMaybe<Array<NftActivityType>>;
address?: InputMaybe<Scalars['String']>;
tokenId?: InputMaybe<Scalars['String']>;
};
export enum NftActivityType {
CancelListing = 'CANCEL_LISTING',
Listing = 'LISTING',
Sale = 'SALE',
Transfer = 'TRANSFER'
}
export type NftApproval = {
__typename?: 'NftApproval';
approvedAddress: Scalars['String'];
@ -196,7 +234,6 @@ export type NftAssetListingsArgs = {
after?: InputMaybe<Scalars['String']>;
asc?: InputMaybe<Scalars['Boolean']>;
before?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
@ -311,7 +348,6 @@ export type NftCollection = {
export type NftCollectionMarketsArgs = {
currencies: Array<Currency>;
datasource?: InputMaybe<DatasourceProvider>;
};
export type NftCollectionConnection = {
@ -602,6 +638,7 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
export type Query = {
__typename?: 'Query';
nftActivity?: Maybe<NftActivityConnection>;
nftAssets?: Maybe<NftAssetConnection>;
nftBalances?: Maybe<NftBalanceConnection>;
nftCollections?: Maybe<NftCollectionConnection>;
@ -617,13 +654,20 @@ export type Query = {
};
export type QueryNftActivityArgs = {
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftActivityFilterInput>;
limit?: InputMaybe<Scalars['Int']>;
};
export type QueryNftAssetsArgs = {
address: Scalars['String'];
after?: InputMaybe<Scalars['String']>;
asc?: InputMaybe<Scalars['Boolean']>;
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
datasource?: InputMaybe<DatasourceProvider>;
filter?: InputMaybe<NftAssetsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
@ -636,7 +680,6 @@ export type QueryNftBalancesArgs = {
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
filter?: InputMaybe<NftBalancesFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
@ -646,12 +689,10 @@ export type QueryNftBalancesArgs = {
export type QueryNftCollectionsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
datasource?: InputMaybe<DatasourceProvider>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftCollectionsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
};
@ -670,7 +711,6 @@ export type QueryNftRouteArgs = {
export type QueryPortfoliosArgs = {
ownerAddresses: Array<Scalars['String']>;
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
};

@ -103,8 +103,9 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
return asset.expirationTime != null && isNaN(asset.expirationTime)
})
const overMaxExpiration = sellAssets.some((asset) => {
return asset.expirationTime != null && asset.expirationTime - Date.now() > ms`180 days`
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
})
const listingsMissingPrice: [WalletAsset, Listing][] = []
const listingsBelowFloor: [WalletAsset, Listing][] = []
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
@ -285,7 +286,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
type="button"
style={{
color: showResolveIssues ? theme.accentTextDarkPrimary : theme.white,
color: showResolveIssues ? theme.accentTextLightPrimary : theme.white,
opacity:
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
(disableListButton && !showResolveIssues)

@ -240,21 +240,36 @@ export const RarityVerifiedIcon = () => (
</svg>
)
export const EditPriceIcon = (props: SVGProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.4713 5.06496L13.2161 4.28041C13.5935 3.88317 13.6233 3.33696 13.2459 2.95958L12.9877 2.69144C12.65 2.35378 12.084 2.40344 11.7165 2.77089L10.9419 3.52565L12.4713 5.06496ZM3.10986 13.2347L5.14573 12.3806L11.7463 5.78L10.2169 4.26055L3.61635 10.8711L2.72255 12.8374C2.62324 13.0658 2.88145 13.324 3.10986 13.2347Z"
fill="#70757A"
/>
</svg>
)
export const AttachPriceIcon = (props: SVGProps) => (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.76353 9.88671L8.53195 9.10825C7.93931 9.05803 7.51242 8.86216 7.20605 8.5558C6.35728 7.70702 6.35728 6.50669 7.20103 5.66796L8.86844 3.99553C9.71722 3.15178 10.9125 3.14676 11.7613 3.99553C12.6151 4.84932 12.6051 6.04464 11.7663 6.88839L10.9125 7.73716C11.0732 8.10881 11.1285 8.56082 11.0381 8.95256L12.4745 7.5212C13.71 6.29073 13.715 4.53292 12.4694 3.28738C11.2189 2.03682 9.47112 2.04687 8.23563 3.28236L6.48786 5.03013C5.25237 6.26562 5.24735 8.01841 6.49289 9.26394C6.78418 9.56026 7.18597 9.78124 7.76353 9.88671ZM8.23061 5.64285L7.46219 6.42131C8.05483 6.47655 8.48172 6.6674 8.78809 6.97376C9.64188 7.82254 9.63686 9.02287 8.79311 9.86662L7.1257 11.534C6.27693 12.3828 5.08161 12.3828 4.23786 11.534C3.38407 10.6802 3.38909 9.48995 4.23284 8.6462L5.08161 7.7924C4.9209 7.42577 4.87068 6.97376 4.95605 6.577L3.51967 8.01339C2.28418 9.24385 2.27916 10.9966 3.52469 12.2422C4.77525 13.4927 6.52302 13.4827 7.75851 12.2522L9.50628 10.4994C10.7418 9.26394 10.7468 7.51115 9.50126 6.26562C9.20996 5.97432 8.8132 5.75334 8.23061 5.64285Z"
fill="#4673FA"
/>
export const BrokenLinkIcon = (props: SVGProps) => (
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g clipPath="url(#clip0_79_4612)">
<path
d="M14.4344 11.3181L16.9344 8.81813C17.6934 8.03229 18.1133 6.97978 18.1039 5.8873C18.0944 4.79481 17.6562 3.74976 16.8836 2.97722C16.1111 2.20469 15.066 1.76649 13.9735 1.75699C12.8811 1.7475 11.8286 2.16748 11.0427 2.92647L9.60938 4.35147"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.20088 8.75098L2.70088 11.251C1.94189 12.0368 1.52191 13.0893 1.53141 14.1818C1.5409 15.2743 1.9791 16.3194 2.75164 17.0919C3.52417 17.8644 4.56922 18.3026 5.66171 18.3121C6.7542 18.3216 7.80671 17.9016 8.59255 17.1426L10.0175 15.7176"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 3.24316L14.7368 16.6952"
stroke="#98A1C0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_79_4612">
<rect width="20" height="20" fill="white" transform="translate(0.128906 0.0341797)" />
</clipPath>
</defs>
</svg>
)

@ -136,13 +136,18 @@ export const MarketplaceRow = ({
toggleExpandMarketplaceRows,
rowHovered,
}: 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 [marketIconHovered, toggleMarketIconHovered] = useReducer((s) => !s, false)
const [marketRowHovered, toggleMarketRowHovered] = useReducer((s) => !s, false)
const [listPrice, setListPrice] = useState<number | undefined>(
() =>
asset.newListings?.find((listing) =>
expandMarketplaceRows ? listing.marketplace.name === selectedMarkets?.[0].name : !!listing.price
)?.price
)
const [globalOverride, setGlobalOverride] = useState(false)
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
const price = showGlobalPrice ? globalPrice : listPrice
@ -198,7 +203,7 @@ export const MarketplaceRow = ({
if (!listPrice) setListPrice(globalPrice)
price = listPrice ? listPrice : globalPrice
} else {
price = globalPrice
price = listPrice
}
if (selectedMarkets.length) for (const marketplace of selectedMarkets) setAssetListPrice(asset, price, marketplace)
else setAssetListPrice(asset, price)
@ -228,14 +233,14 @@ export const MarketplaceRow = ({
return (
<Row onMouseEnter={toggleMarketRowHovered} onMouseLeave={toggleMarketRowHovered}>
<FloorPriceInfo>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
<ThemedText.BodyPrimary color="textSecondary" lineHeight="24px">
{asset.floorPrice ? `${asset.floorPrice.toFixed(3)} ETH` : '-'}
</ThemedText.BodySmall>
</ThemedText.BodyPrimary>
</FloorPriceInfo>
<LastPriceInfo>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
<ThemedText.BodyPrimary color="textSecondary" lineHeight="24px">
{asset.lastPrice ? `${asset.lastPrice.toFixed(3)} ETH` : '-'}
</ThemedText.BodySmall>
</ThemedText.BodyPrimary>
</LastPriceInfo>
<Row flex="2">

@ -8,7 +8,7 @@ import { Overlay } from 'nft/components/modals/Overlay'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
@ -112,24 +112,28 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApproved])
const closeModalOnClick = useCallback(() => {
listingStatus === ListingStatus.APPROVED ? window.location.reload() : overlayClick()
}, [listingStatus, overlayClick])
// In the case that a user removes all listings via retry logic, close modal
useEffect(() => {
!listings.length && overlayClick()
}, [listings, overlayClick])
!listings.length && closeModalOnClick()
}, [listings, closeModalOnClick])
return (
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
{listingStatus === ListingStatus.APPROVED ? (
<SuccessScreen overlayClick={overlayClick} />
<SuccessScreen overlayClick={closeModalOnClick} />
) : (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>List NFTs</Trans>
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
<X size={24} cursor="pointer" onClick={closeModalOnClick} />
</TitleRow>
<ListModalSection
sectionType={Section.APPROVE}
@ -147,7 +151,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
)}
</ListModalWrapper>
</Trace>
<Overlay onClick={overlayClick} />
<Overlay onClick={closeModalOnClick} />
</Portal>
)
}

@ -12,6 +12,8 @@ import { opacify } from 'theme/utils'
import { MarketplaceRow } from './MarketplaceRow'
import { SetPriceMethod } from './NFTListingsGrid'
const IMAGE_THUMBNAIL_SIZE = 60
const NFTListRowWrapper = styled(Row)`
padding: 24px 0px;
align-items: center;
@ -23,8 +25,8 @@ const NFTListRowWrapper = styled(Row)`
`
const RemoveIconContainer = styled.div`
width: 44px;
height: 44px;
width: ${IMAGE_THUMBNAIL_SIZE}px;
height: ${IMAGE_THUMBNAIL_SIZE}px;
padding-left: 12px;
align-self: flex-start;
align-items: center;
@ -51,8 +53,8 @@ const NFTInfoWrapper = styled(Row)`
`
const NFTImage = styled.img`
width: 44px;
height: 44px;
width: ${IMAGE_THUMBNAIL_SIZE}px;
height: ${IMAGE_THUMBNAIL_SIZE}px;
border-radius: 8px;
margin-right: 8px;
`
@ -85,6 +87,7 @@ const MarketPlaceRowWrapper = styled(Column)`
gap: 24px;
flex: 1.5;
margin-right: 12px;
padding: 6px 0px;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 2;

@ -28,7 +28,7 @@ const TableHeader = styled.div`
line-height: 20px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
margin-left: 48px;
padding-left: 48px;
}
`

@ -1,14 +1,14 @@
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { AttachPriceIcon, EditPriceIcon } from 'nft/components/icons'
import { BrokenLinkIcon } from 'nft/components/icons'
import { NumericInput } from 'nft/components/layout/Input'
import { body } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks'
import { ListingWarning, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils/currency'
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { AlertTriangle, Link } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { colors } from 'theme/colors'
@ -19,7 +19,7 @@ const PriceTextInputWrapper = styled(Column)`
`
const InputWrapper = styled(Row)<{ borderColor: string }>`
height: 44px;
height: 48px;
color: ${({ theme }) => theme.textTertiary};
padding: 12px;
border: 2px solid;
@ -34,12 +34,17 @@ const CurrencyWrapper = styled.div<{ listPrice: number | undefined }>`
`
const GlobalPriceIcon = styled.div`
display: block;
display: flex;
cursor: pointer;
position: absolute;
top: -6px;
right: -4px;
bottom: 32px;
right: -10px;
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 50%;
height: 28px;
width: 28px;
align-items: center;
justify-content: center;
`
const WarningRow = styled(Row)`
@ -104,7 +109,6 @@ export const PriceTextInput = ({
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)
@ -128,7 +132,7 @@ export const PriceTextInput = ({
const warningColor =
showResolveIssues && !listPrice
? colors.red400
: warningType !== WarningType.NONE && !focused
: warningType !== WarningType.NONE
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
warningType === WarningType.ALREADY_LISTED
? colors.red400
@ -151,10 +155,6 @@ export const PriceTextInput = ({
placeholder="0"
backgroundColor="none"
width={{ sm: '54', md: '68' }}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false)
}}
ref={inputRef}
onChange={(v: FormEvent<HTMLInputElement>) => {
if (!listPrice && v.currentTarget.value.includes('.') && parseFloat(v.currentTarget.value) === 0) {
@ -167,7 +167,7 @@ export const PriceTextInput = ({
<CurrencyWrapper listPrice={listPrice}>&nbsp;ETH</CurrencyWrapper>
{(isGlobalPrice || globalOverride) && (
<GlobalPriceIcon onClick={() => setGlobalOverride(!globalOverride)}>
{globalOverride ? <AttachPriceIcon /> : <EditPriceIcon />}
{globalOverride ? <BrokenLinkIcon /> : <Link size={20} color={warningColor} />}
</GlobalPriceIcon>
)}
</InputWrapper>

@ -138,7 +138,7 @@ export const ProfilePage = () => {
borderRadius="12"
paddingX="16"
paddingY="12"
background="backgroundModule"
background="backgroundSurface"
borderStyle="solid"
borderColor="backgroundOutline"
borderWidth="1px"