Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c689e1d4 | ||
|
|
70cd7272a1 | ||
|
|
2b5769ac86 | ||
|
|
b811afd134 | ||
|
|
6131e6bfab |
@@ -1,5 +1,6 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
@@ -229,6 +230,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.gqlRouting}
|
||||
label="GraphQL NFT Routing"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NftGraphqlVariant}
|
||||
value={useNftGraphqlFlag()}
|
||||
featureFlag={FeatureFlag.nftGraphql}
|
||||
label="Migrate NFT read endpoints to GQL"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { Trans } from '@lingui/macro'
|
||||
import { useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { HistoryDuration, SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import useTrendingTokens from 'graphql/data/TrendingTokens'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
@@ -56,7 +58,7 @@ const SearchBarDropdownSection = ({
|
||||
</Row>
|
||||
<Column gap="12">
|
||||
{suggestions.map((suggestion, index) =>
|
||||
isLoading ? (
|
||||
isLoading || !suggestion ? (
|
||||
<SkeletonRow key={index} />
|
||||
) : isCollection(suggestion) ? (
|
||||
<CollectionRow
|
||||
@@ -123,6 +125,7 @@ export const SearchBarDropdown = ({
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isNFTPage = useIsNftPage()
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const isTokenPage = pathname.includes('/tokens')
|
||||
const [resultsState, setResultsState] = useState<ReactNode>()
|
||||
|
||||
@@ -131,24 +134,25 @@ export const SearchBarDropdown = ({
|
||||
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
|
||||
)
|
||||
|
||||
const trendingCollections = useMemo(
|
||||
() =>
|
||||
trendingCollectionResults
|
||||
? trendingCollectionResults
|
||||
.map((collection) => ({
|
||||
...collection,
|
||||
collectionAddress: collection.address,
|
||||
floorPrice: formatEthPrice(collection.floor?.toString()),
|
||||
stats: {
|
||||
total_supply: collection.totalSupply,
|
||||
one_day_change: collection.floorChange,
|
||||
floor_price: formatEthPrice(collection.floor?.toString()),
|
||||
},
|
||||
}))
|
||||
.slice(0, isNFTPage ? 3 : 2)
|
||||
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
|
||||
[isNFTPage, trendingCollectionResults]
|
||||
)
|
||||
const { data: gqlData, loading } = useTrendingCollections(3, HistoryDuration.Day)
|
||||
|
||||
const trendingCollections = useMemo(() => {
|
||||
const gatedTrendingCollections = isNftGraphqlEnabled ? gqlData : trendingCollectionResults
|
||||
return gatedTrendingCollections && (!isNftGraphqlEnabled || !loading)
|
||||
? gatedTrendingCollections
|
||||
.map((collection) => ({
|
||||
...collection,
|
||||
collectionAddress: collection.address,
|
||||
floorPrice: isNftGraphqlEnabled ? collection.floor : formatEthPrice(collection.floor?.toString()),
|
||||
stats: {
|
||||
total_supply: collection.totalSupply,
|
||||
one_day_change: collection.floorChange,
|
||||
floor_price: isNftGraphqlEnabled ? collection.floor : formatEthPrice(collection.floor?.toString()),
|
||||
},
|
||||
}))
|
||||
.slice(0, isNFTPage ? 3 : 2)
|
||||
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)]
|
||||
}, [gqlData, isNFTPage, isNftGraphqlEnabled, loading, trendingCollectionResults])
|
||||
|
||||
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import { DeltaText, getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
@@ -28,12 +29,6 @@ const PriceChangeContainer = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const PriceChangeText = styled.span<{ isNegative: boolean }>`
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme, isNegative }) => (isNegative ? theme.accentFailure : theme.accentSuccess)};
|
||||
`
|
||||
|
||||
const ArrowCell = styled.span`
|
||||
padding-top: 5px;
|
||||
padding-right: 3px;
|
||||
@@ -191,18 +186,20 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
</Row>
|
||||
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
{token.market?.price?.value && (
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
|
||||
</Row>
|
||||
)}
|
||||
{token.market?.pricePercentChange?.value && (
|
||||
<PriceChangeContainer>
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
|
||||
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
|
||||
</PriceChangeText>
|
||||
</PriceChangeContainer>
|
||||
{!!token.market?.price?.value && (
|
||||
<>
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
|
||||
</Row>
|
||||
<PriceChangeContainer>
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
<ThemedText.BodySmall>
|
||||
<DeltaText delta={token.market?.pricePercentChange?.value}>
|
||||
{Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}%
|
||||
</DeltaText>
|
||||
</ThemedText.BodySmall>
|
||||
</PriceChangeContainer>
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
</Link>
|
||||
|
||||
@@ -8,4 +8,5 @@ export enum FeatureFlag {
|
||||
swapWidget = 'swap_widget_replacement_enabled',
|
||||
gqlRouting = 'gqlRouting',
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
}
|
||||
|
||||
11
src/featureFlags/flags/nftlGraphql.ts
Normal file
11
src/featureFlags/flags/nftlGraphql.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useNftGraphqlFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.nftGraphql)
|
||||
}
|
||||
|
||||
export function useNftGraphqlEnabled(): boolean {
|
||||
return useNftGraphqlFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as NftGraphqlVariant }
|
||||
90
src/graphql/data/__generated__/types-and-hooks.ts
generated
90
src/graphql/data/__generated__/types-and-hooks.ts
generated
@@ -675,9 +675,11 @@ export type QueryNftAssetsArgs = {
|
||||
asc?: InputMaybe<Scalars['Boolean']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
chain?: InputMaybe<Chain>;
|
||||
cursor?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<NftAssetsFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
orderBy?: InputMaybe<NftAssetSortableField>;
|
||||
};
|
||||
|
||||
@@ -1119,6 +1121,14 @@ export type NftRouteQueryVariables = Exact<{
|
||||
|
||||
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', id: string, calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } } };
|
||||
|
||||
export type TrendingCollectionsQueryVariables = Exact<{
|
||||
size?: InputMaybe<Scalars['Int']>;
|
||||
timePeriod?: InputMaybe<HistoryDuration>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TrendingCollectionsQuery = { __typename?: 'Query', topCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', name?: string, isVerified?: boolean, nftContracts?: Array<{ __typename?: 'NftContract', address: string, totalSupply?: number }>, image?: { __typename?: 'Image', url: string }, bannerImage?: { __typename?: 'Image', url: string }, markets?: Array<{ __typename?: 'NftCollectionMarket', owners?: number, floorPrice?: { __typename?: 'TimestampedAmount', value: number }, totalVolume?: { __typename?: 'TimestampedAmount', value: number }, volume?: { __typename?: 'TimestampedAmount', value: number }, volumePercentChange?: { __typename?: 'TimestampedAmount', value: number }, floorPricePercentChange?: { __typename?: 'TimestampedAmount', value: number }, sales?: { __typename?: 'TimestampedAmount', value: number }, listings?: { __typename?: 'TimestampedAmount', value: number } }> } }> } };
|
||||
|
||||
|
||||
export const RecentlySearchedAssetsDocument = gql`
|
||||
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
|
||||
@@ -2117,4 +2127,82 @@ export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<N
|
||||
}
|
||||
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
|
||||
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
|
||||
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
|
||||
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
|
||||
export const TrendingCollectionsDocument = gql`
|
||||
query TrendingCollections($size: Int, $timePeriod: HistoryDuration) {
|
||||
topCollections(limit: $size, duration: $timePeriod) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
nftContracts {
|
||||
address
|
||||
totalSupply
|
||||
}
|
||||
image {
|
||||
url
|
||||
}
|
||||
bannerImage {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
value
|
||||
}
|
||||
owners
|
||||
totalVolume {
|
||||
value
|
||||
}
|
||||
volume(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
volumePercentChange(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
floorPricePercentChange(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
sales {
|
||||
value
|
||||
}
|
||||
totalVolume {
|
||||
value
|
||||
}
|
||||
listings {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTrendingCollectionsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTrendingCollectionsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTrendingCollectionsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTrendingCollectionsQuery({
|
||||
* variables: {
|
||||
* size: // value for 'size'
|
||||
* timePeriod: // value for 'timePeriod'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTrendingCollectionsQuery(baseOptions?: Apollo.QueryHookOptions<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>(TrendingCollectionsDocument, options);
|
||||
}
|
||||
export function useTrendingCollectionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>(TrendingCollectionsDocument, options);
|
||||
}
|
||||
export type TrendingCollectionsQueryHookResult = ReturnType<typeof useTrendingCollectionsQuery>;
|
||||
export type TrendingCollectionsLazyQueryHookResult = ReturnType<typeof useTrendingCollectionsLazyQuery>;
|
||||
export type TrendingCollectionsQueryResult = Apollo.QueryResult<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>;
|
||||
95
src/graphql/data/nft/TrendingCollections.ts
Normal file
95
src/graphql/data/nft/TrendingCollections.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import gql from 'graphql-tag'
|
||||
import { TrendingCollection } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { HistoryDuration, useTrendingCollectionsQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
gql`
|
||||
query TrendingCollections($size: Int, $timePeriod: HistoryDuration) {
|
||||
topCollections(limit: $size, duration: $timePeriod) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
nftContracts {
|
||||
address
|
||||
totalSupply
|
||||
}
|
||||
image {
|
||||
url
|
||||
}
|
||||
bannerImage {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
value
|
||||
}
|
||||
owners
|
||||
totalVolume {
|
||||
value
|
||||
}
|
||||
volume(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
volumePercentChange(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
floorPricePercentChange(duration: $timePeriod) {
|
||||
value
|
||||
}
|
||||
sales {
|
||||
value
|
||||
}
|
||||
listings {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useTrendingCollections(size: number, timePeriod: HistoryDuration) {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const { data, loading, error } = useTrendingCollectionsQuery({
|
||||
variables: {
|
||||
size,
|
||||
timePeriod,
|
||||
},
|
||||
skip: !isNftGraphqlEnabled,
|
||||
})
|
||||
|
||||
const trendingCollections: TrendingCollection[] | undefined = useMemo(
|
||||
() =>
|
||||
data?.topCollections?.edges?.map((edge) => {
|
||||
const collection = edge?.node
|
||||
return {
|
||||
name: collection.name,
|
||||
address: collection.nftContracts?.[0].address,
|
||||
imageUrl: collection.image?.url,
|
||||
bannerImageUrl: collection.bannerImage?.url,
|
||||
isVerified: collection.isVerified,
|
||||
volume: collection.markets?.[0].volume?.value,
|
||||
volumeChange: collection.markets?.[0].volumePercentChange?.value,
|
||||
floor: collection.markets?.[0].floorPrice?.value,
|
||||
floorChange: collection.markets?.[0].floorPricePercentChange?.value,
|
||||
marketCap: collection.markets?.[0].totalVolume?.value,
|
||||
percentListed:
|
||||
(collection.markets?.[0].listings?.value ?? 0) / (collection.nftContracts?.[0].totalSupply ?? 1),
|
||||
owners: collection.markets?.[0].owners,
|
||||
sales: collection.markets?.[0].sales?.value,
|
||||
totalSupply: collection.nftContracts?.[0].totalSupply,
|
||||
}
|
||||
}),
|
||||
[data?.topCollections?.edges]
|
||||
)
|
||||
|
||||
return {
|
||||
data: trendingCollections,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { calc } from '@vanilla-extract/css-utils'
|
||||
import { sprinkles, themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const card = style([
|
||||
sprinkles({
|
||||
overflow: 'hidden',
|
||||
paddingBottom: '12',
|
||||
borderRadius: '16',
|
||||
}),
|
||||
{
|
||||
boxSizing: 'border-box',
|
||||
WebkitBoxSizing: 'border-box',
|
||||
boxShadow: vars.color.cardDropShadow,
|
||||
backgroundColor: themeVars.colors.backgroundSurface,
|
||||
':after': {
|
||||
content: '',
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
right: ' 0px',
|
||||
bottom: ' 0px',
|
||||
left: '0px',
|
||||
border: ' 1px solid',
|
||||
borderRadius: '16px',
|
||||
borderColor: '#5D678524',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const loadingBackground = style({
|
||||
background: `linear-gradient(270deg, ${themeVars.colors.backgroundOutline} 0%, ${themeVars.colors.backgroundSurface} 100%)`,
|
||||
})
|
||||
|
||||
export const cardImageHover = style({
|
||||
transform: 'scale(1.15)',
|
||||
})
|
||||
|
||||
export const selectedCard = style([
|
||||
card,
|
||||
sprinkles({
|
||||
background: 'backgroundSurface',
|
||||
}),
|
||||
{
|
||||
':after': {
|
||||
border: '2px solid',
|
||||
borderColor: vars.color.accentAction,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const button = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
width: 'full',
|
||||
position: 'relative',
|
||||
paddingY: '8',
|
||||
marginTop: { sm: '8', md: '10' },
|
||||
marginBottom: '12',
|
||||
borderRadius: '12',
|
||||
border: 'none',
|
||||
justifyContent: 'center',
|
||||
transition: '250',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{
|
||||
lineHeight: '16px',
|
||||
},
|
||||
])
|
||||
|
||||
export const marketplaceIcon = style([
|
||||
sprinkles({
|
||||
display: 'inline-block',
|
||||
width: '16',
|
||||
height: '16',
|
||||
borderRadius: '4',
|
||||
flexShrink: '0',
|
||||
marginLeft: '8',
|
||||
}),
|
||||
{
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
])
|
||||
|
||||
export const rarityInfo = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
borderRadius: '4',
|
||||
height: '16',
|
||||
color: 'textPrimary',
|
||||
background: 'backgroundInteractive',
|
||||
fontSize: '10',
|
||||
fontWeight: 'semibold',
|
||||
paddingX: '4',
|
||||
}),
|
||||
{
|
||||
lineHeight: '12px',
|
||||
letterSpacing: '0.04em',
|
||||
backdropFilter: 'blur(6px)',
|
||||
},
|
||||
])
|
||||
|
||||
export const playbackSwitch = style([
|
||||
sprinkles({
|
||||
position: 'absolute',
|
||||
width: '40',
|
||||
height: '40',
|
||||
zIndex: '1',
|
||||
}),
|
||||
{
|
||||
marginLeft: calc.subtract('100%', '50px'),
|
||||
transform: 'translateY(-56px)',
|
||||
},
|
||||
])
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import clsx from 'clsx'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import {
|
||||
MinusIconLarge,
|
||||
PauseButtonIcon,
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
RarityVerifiedIcon,
|
||||
VerifiedIcon,
|
||||
} from 'nft/components/icons'
|
||||
import { body, bodySmall, buttonTextMedium, subhead } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset, Rarity, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
|
||||
import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils'
|
||||
@@ -32,11 +30,11 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { AlertTriangle, Pause, Play } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import * as styles from './Card.css'
|
||||
import { colors } from 'theme/colors'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
/* -------- ASSET CONTEXT -------- */
|
||||
export interface CardContextProps {
|
||||
@@ -166,6 +164,31 @@ const StyledImageContainer = styled.div<{ isDisabled?: boolean }>`
|
||||
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
|
||||
`
|
||||
|
||||
const CardContainer = styled.div<{ selected: boolean }>`
|
||||
position: relative;
|
||||
border-radius: ${BORDER_RADIUS}px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
overflow: hidden;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 16px;
|
||||
box-shadow: rgba(0, 0, 0, 10%) 0px 4px 12px;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
|
||||
:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
border: ${({ selected }) => (selected ? '2px' : '1px')} solid;
|
||||
border-radius: 16px;
|
||||
border-color: ${({ theme, selected }) => (selected ? theme.accentAction : opacify(12, colors.gray500))};
|
||||
pointer-events: none;
|
||||
}
|
||||
`
|
||||
|
||||
/* -------- ASSET CARD -------- */
|
||||
interface CardProps {
|
||||
asset: GenieAsset | WalletAsset
|
||||
@@ -220,19 +243,16 @@ const Container = ({
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={providerValue}>
|
||||
<Box
|
||||
position="relative"
|
||||
<CardContainer
|
||||
selected={selected}
|
||||
ref={assetRef}
|
||||
borderRadius={BORDER_RADIUS}
|
||||
className={selected ? styles.selectedCard : styles.card}
|
||||
draggable={false}
|
||||
onMouseEnter={toggleHover}
|
||||
onMouseLeave={toggleHover}
|
||||
transition="250"
|
||||
onClick={isDisabled ? () => null : onClick ?? handleAssetInBag}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</CardContainer>
|
||||
</CardContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -277,6 +297,13 @@ function getHeightFromAspectRatio(uniformAspectRatio: UniformAspectRatio, render
|
||||
: renderedHeight
|
||||
}
|
||||
|
||||
function getMediaAspectRatio(
|
||||
uniformAspectRatio?: UniformAspectRatio,
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
): string {
|
||||
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
|
||||
}
|
||||
|
||||
interface ImageProps {
|
||||
uniformAspectRatio?: UniformAspectRatio
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
@@ -284,6 +311,29 @@ interface ImageProps {
|
||||
setRenderedHeight?: (renderedHeight: number | undefined) => void
|
||||
}
|
||||
|
||||
const StyledMediaContainer = styled(Row)`
|
||||
overflow: hidden;
|
||||
border-top-left-radius: ${BORDER_RADIUS}px;
|
||||
border-top-right-radius: ${BORDER_RADIUS}px;
|
||||
`
|
||||
|
||||
const StyledImage = styled.img<{
|
||||
hovered: boolean
|
||||
imageLoading: boolean
|
||||
$aspectRatio?: string
|
||||
$hidden?: boolean
|
||||
}>`
|
||||
width: 100%;
|
||||
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} transform`};
|
||||
will-change: transform;
|
||||
object-fit: contain;
|
||||
visibility: ${({ $hidden }) => ($hidden ? 'hidden' : 'visible')};
|
||||
transform: ${({ hovered }) => hovered && 'scale(1.15)'};
|
||||
background: ${({ theme, imageLoading }) =>
|
||||
imageLoading && `linear-gradient(270deg, ${theme.backgroundOutline} 0%, ${theme.backgroundSurface} 100%)`};
|
||||
`
|
||||
|
||||
const Image = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
@@ -300,40 +350,50 @@ const Image = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" overflow="hidden" borderTopLeftRadius={BORDER_RADIUS} borderTopRightRadius={BORDER_RADIUS}>
|
||||
<Box
|
||||
as="img"
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: `${uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'}`,
|
||||
transition: 'transform 0.25s ease 0s',
|
||||
}}
|
||||
<StyledMediaContainer>
|
||||
<StyledImage
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit="contain"
|
||||
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
|
||||
hovered={hovered && !isMobile}
|
||||
imageLoading={!loaded}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && !isMobile && styles.cardImageHover, !loaded && styles.loadingBackground)}
|
||||
/>
|
||||
</Box>
|
||||
</StyledMediaContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function getMediaAspectRatio(
|
||||
uniformAspectRatio: UniformAspectRatio,
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
): string {
|
||||
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
|
||||
}
|
||||
|
||||
interface MediaProps {
|
||||
shouldPlay: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}
|
||||
|
||||
const PlaybackButton = styled.div`
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
z-index: 1;
|
||||
margin-left: calc(100% - 50px);
|
||||
transform: translateY(-56px);
|
||||
`
|
||||
|
||||
const StyledVideo = styled.video<{
|
||||
$aspectRatio?: string
|
||||
}>`
|
||||
width: 100%;
|
||||
aspect-ratio: ${({ $aspectRatio }) => $aspectRatio};
|
||||
`
|
||||
|
||||
const StyledInnerMediaContainer = styled(Row)`
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
`
|
||||
|
||||
const Video = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
@@ -360,52 +420,38 @@ const Video = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as="img"
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
|
||||
transition: 'transform 0.25s ease 0s',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
<StyledMediaContainer>
|
||||
<StyledImage
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit="contain"
|
||||
alt={asset.name || asset.tokenId}
|
||||
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
|
||||
hovered={hovered && !isMobile}
|
||||
imageLoading={!imageLoaded}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
visibility={shouldPlay ? 'hidden' : 'visible'}
|
||||
className={clsx(hovered && !isMobile && styles.cardImageHover, !imageLoaded && styles.loadingBackground)}
|
||||
$hidden={shouldPlay}
|
||||
/>
|
||||
</Box>
|
||||
</StyledMediaContainer>
|
||||
{shouldPlay ? (
|
||||
<>
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PauseButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
<PlaybackButton>
|
||||
<Pause
|
||||
size="24px"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" left="0" top="0" display="flex">
|
||||
<Box
|
||||
as="video"
|
||||
</PlaybackButton>
|
||||
<StyledInnerMediaContainer>
|
||||
<StyledVideo
|
||||
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
|
||||
ref={vidRef}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: `${
|
||||
uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
|
||||
}`,
|
||||
}}
|
||||
onEnded={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
@@ -414,29 +460,32 @@ const Video = ({
|
||||
playsInline
|
||||
>
|
||||
<source src={asset.animationUrl} />
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledVideo>
|
||||
</StyledInnerMediaContainer>
|
||||
</>
|
||||
) : (
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PlaybackButton>
|
||||
{((!isMobile && hovered) || isMobile) && (
|
||||
<PlayButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
<Play
|
||||
size="24px"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(asset.tokenId)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</PlaybackButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledAudio = styled.audio`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Audio = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
@@ -463,29 +512,25 @@ const Audio = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as="img"
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
}}
|
||||
<StyledMediaContainer>
|
||||
<StyledImage
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit="contain"
|
||||
alt={asset.name || asset.tokenId}
|
||||
$aspectRatio={getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio)}
|
||||
hovered={hovered && !isMobile}
|
||||
imageLoading={!imageLoaded}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setImageLoaded(true)
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && !isMobile && styles.cardImageHover, !imageLoaded && styles.loadingBackground)}
|
||||
/>
|
||||
</Box>
|
||||
</StyledMediaContainer>
|
||||
{shouldPlay ? (
|
||||
<>
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PlaybackButton>
|
||||
<PauseButtonIcon
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -494,26 +539,22 @@ const Audio = ({
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
</Box>
|
||||
<Box position="absolute" left="0" top="0" display="flex">
|
||||
<Box
|
||||
as="audio"
|
||||
</PlaybackButton>
|
||||
<StyledInnerMediaContainer>
|
||||
<StyledAudio
|
||||
ref={audRef}
|
||||
width="full"
|
||||
height="full"
|
||||
onEnded={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentTokenPlayingMedia(undefined)
|
||||
}}
|
||||
>
|
||||
<source src={asset.animationUrl} />
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledAudio>
|
||||
</StyledInnerMediaContainer>
|
||||
</>
|
||||
) : (
|
||||
<Box className={styles.playbackSwitch}>
|
||||
<PlaybackButton>
|
||||
{((!isMobile && hovered) || isMobile) && (
|
||||
<PlayButtonIcon
|
||||
width="100%"
|
||||
@@ -523,10 +564,9 @@ const Audio = ({
|
||||
e.stopPropagation()
|
||||
setCurrentTokenPlayingMedia(asset.tokenId)
|
||||
}}
|
||||
className="playback-icon"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</PlaybackButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -537,36 +577,39 @@ interface CardDetailsContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const StyledDetailsContainer = styled(Column)`
|
||||
position: relative;
|
||||
padding: 12px 12px 0px;
|
||||
justify-content: space-between;
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium}`};
|
||||
`
|
||||
|
||||
const DetailsContainer = ({ children }: CardDetailsContainerProps) => {
|
||||
return (
|
||||
<Row
|
||||
position="relative"
|
||||
paddingX="12"
|
||||
paddingTop="12"
|
||||
justifyContent="space-between"
|
||||
flexDirection="column"
|
||||
transition="250"
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
return <StyledDetailsContainer>{children}</StyledDetailsContainer>
|
||||
}
|
||||
|
||||
const StyledInfoContainer = styled.div`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const InfoContainer = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box overflow="hidden" width="full">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
return <StyledInfoContainer>{children}</StyledInfoContainer>
|
||||
}
|
||||
|
||||
const TruncatedTextRow = styled(Row)`
|
||||
const TruncatedTextRow = styled(ThemedText.BodySmall)`
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const AssetNameRow = styled(TruncatedTextRow)`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 16px !important;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
interface ProfileNftDetailsProps {
|
||||
@@ -574,6 +617,18 @@ interface ProfileNftDetailsProps {
|
||||
hideDetails: boolean
|
||||
}
|
||||
|
||||
const PrimaryRowContainer = styled.div`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const FloorPriceRow = styled(TruncatedTextRow)`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
|
||||
const assetName = () => {
|
||||
if (!asset.name && !asset.tokenId) return
|
||||
@@ -583,89 +638,95 @@ const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
|
||||
const shouldShowUserListedPrice = !asset.notForSale && asset.asset_contract.tokenType !== NftStandard.Erc1155
|
||||
|
||||
return (
|
||||
<Box overflow="hidden" width="full" flexWrap="nowrap">
|
||||
<PrimaryRowContainer>
|
||||
<PrimaryRow>
|
||||
<PrimaryDetails>
|
||||
<TruncatedTextRow className={bodySmall} style={{ color: themeVars.colors.textSecondary }}>
|
||||
<TruncatedTextRow color="textSecondary">
|
||||
{!!asset.asset_contract.name && <span>{asset.asset_contract.name}</span>}
|
||||
</TruncatedTextRow>
|
||||
{asset.collectionIsVerified && <VerifiedIcon height="18px" width="18px" />}
|
||||
</PrimaryDetails>
|
||||
{!hideDetails && <DetailsLink />}
|
||||
</PrimaryRow>
|
||||
<Row justifyItems="flex-start">
|
||||
<TruncatedTextRow
|
||||
className={body}
|
||||
style={{
|
||||
color: themeVars.colors.textPrimary,
|
||||
}}
|
||||
>
|
||||
{assetName()}
|
||||
</TruncatedTextRow>
|
||||
<Row>
|
||||
<AssetNameRow>{assetName()}</AssetNameRow>
|
||||
{asset.susFlag && <Suspicious />}
|
||||
</Row>
|
||||
<TruncatedTextRow className={buttonTextMedium} style={{ color: themeVars.colors.textPrimary }}>
|
||||
<FloorPriceRow>
|
||||
{shouldShowUserListedPrice && asset.floor_sell_order_price
|
||||
? `${floorFormatter(asset.floor_sell_order_price)} ETH`
|
||||
: ' '}
|
||||
</TruncatedTextRow>
|
||||
</Box>
|
||||
</FloorPriceRow>
|
||||
</PrimaryRowContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const PrimaryRow = ({ children }: { children: ReactNode }) => (
|
||||
<Row gap="8" justifyContent="space-between">
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
const StyledPrimaryRow = styled(Row)`
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const PrimaryRow = ({ children }: { children: ReactNode }) => <StyledPrimaryRow>{children}</StyledPrimaryRow>
|
||||
|
||||
const StyledPrimaryDetails = styled(Row)`
|
||||
justify-items: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const PrimaryDetails = ({ children }: { children: ReactNode }) => (
|
||||
<Row justifyItems="center" overflow="hidden" whiteSpace="nowrap">
|
||||
{children}
|
||||
</Row>
|
||||
<StyledPrimaryDetails>{children}</StyledPrimaryDetails>
|
||||
)
|
||||
|
||||
const PrimaryInfoContainer = styled.div`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
`
|
||||
|
||||
const PrimaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis" className={body}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
return <PrimaryInfoContainer>{children}</PrimaryInfoContainer>
|
||||
}
|
||||
|
||||
const SecondaryRow = ({ children }: { children: ReactNode }) => (
|
||||
<Row height="20" justifyContent="space-between" marginTop="6">
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
const StyledSecondaryRow = styled(Row)`
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
margin-top: 6px;
|
||||
`
|
||||
|
||||
const SecondaryRow = ({ children }: { children: ReactNode }) => <StyledSecondaryRow>{children}</StyledSecondaryRow>
|
||||
|
||||
const StyledSecondaryDetails = styled(Row)`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const SecondaryDetails = ({ children }: { children: ReactNode }) => (
|
||||
<Row overflow="hidden" whiteSpace="nowrap">
|
||||
{children}
|
||||
</Row>
|
||||
<StyledSecondaryDetails>{children}</StyledSecondaryDetails>
|
||||
)
|
||||
|
||||
const SecondaryInfoContainer = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
const SecondaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box
|
||||
color="textPrimary"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
style={{ lineHeight: '20px' }}
|
||||
className={subhead}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
return <SecondaryInfoContainer>{children}</SecondaryInfoContainer>
|
||||
}
|
||||
|
||||
const TertiaryInfoContainer = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const TertiaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box marginTop="8" color="textSecondary">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
return <TertiaryInfoContainer>{children}</TertiaryInfoContainer>
|
||||
}
|
||||
|
||||
interface Erc1155ControlsInterface {
|
||||
@@ -700,15 +761,18 @@ const Erc1155Controls = ({ quantity }: Erc1155ControlsInterface) => {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledMarketplaceIcon = styled.img`
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const MarketplaceIcon = ({ marketplace }: { marketplace: string }) => {
|
||||
return (
|
||||
<Box
|
||||
as="img"
|
||||
alt={marketplace}
|
||||
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
|
||||
className={styles.marketplaceIcon}
|
||||
/>
|
||||
)
|
||||
return <StyledMarketplaceIcon alt={marketplace} src={`/nft/svgs/marketplaces/${marketplace}.svg`} />
|
||||
}
|
||||
|
||||
const DetailsLink = () => {
|
||||
@@ -721,7 +785,7 @@ const DetailsLink = () => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<Box data-testid="nft-details-link">Details</Box>
|
||||
<div data-testid="nft-details-link">Details</div>
|
||||
</DetailsLinkContainer>
|
||||
)
|
||||
}
|
||||
@@ -734,6 +798,28 @@ interface RankingProps {
|
||||
rarityLogo?: string
|
||||
}
|
||||
|
||||
const RarityLogoContainer = styled(Row)`
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
`
|
||||
|
||||
const RarityText = styled(ThemedText.BodySmall)`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const RarityInfo = styled(Row)`
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
background: ${({ theme }) => theme.backgroundInteractive};
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 0px 4px;
|
||||
line-height: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
backdrop-filter: blur(6px);
|
||||
`
|
||||
|
||||
const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps) => {
|
||||
const { asset } = useCardContext()
|
||||
|
||||
@@ -744,44 +830,49 @@ const Ranking = ({ rarity, provider, rarityVerified, rarityLogo }: RankingProps)
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Row>
|
||||
<Box display="flex" marginRight="4">
|
||||
<img src={rarityLogo} alt="cardLogo" width={16} />
|
||||
</Box>
|
||||
<Box width="full" className={bodySmall}>
|
||||
<RarityLogoContainer>
|
||||
<img src={rarityLogo} alt="cardLogo" width={16} height={16} />
|
||||
</RarityLogoContainer>
|
||||
<RarityText>
|
||||
{rarityVerified
|
||||
? `Verified by ${
|
||||
('collectionName' in asset && asset.collectionName) ||
|
||||
('asset_contract' in asset && asset.asset_contract?.name)
|
||||
}`
|
||||
: `Ranking by ${rarity.primaryProvider === 'Genie' ? fallbackProvider : rarity.primaryProvider}`}
|
||||
</Box>
|
||||
</RarityText>
|
||||
</Row>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Box className={styles.rarityInfo}>
|
||||
<Box paddingTop="2" paddingBottom="2" display="flex">
|
||||
{putCommas(provider.rank)}
|
||||
</Box>
|
||||
|
||||
<Box display="flex" height="16">
|
||||
{rarityVerified ? <RarityVerifiedIcon /> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
<RarityInfo>
|
||||
<Row padding="2px 0px">{putCommas(provider.rank)}</Row>
|
||||
<Row>{rarityVerified ? <RarityVerifiedIcon /> : null}</Row>
|
||||
</RarityInfo>
|
||||
</MouseoverTooltip>
|
||||
</RankingContainer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const SUSPICIOUS_TEXT = 'Blocked on OpenSea'
|
||||
|
||||
const SUSPICIOUS_TEXT = t`Blocked on OpenSea`
|
||||
|
||||
const SuspiciousIconContainer = styled(Row)`
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const PoolIconContainer = styled(SuspiciousIconContainer)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const Suspicious = () => {
|
||||
return (
|
||||
<MouseoverTooltip text={<Box className={bodySmall}>{SUSPICIOUS_TEXT}</Box>} placement="top">
|
||||
<Box display="flex" flexShrink="0" marginLeft="4">
|
||||
<MouseoverTooltip text={<ThemedText.BodySmall>{SUSPICIOUS_TEXT}</ThemedText.BodySmall>} placement="top">
|
||||
<SuspiciousIconContainer>
|
||||
<SuspiciousIcon />
|
||||
</Box>
|
||||
</SuspiciousIconContainer>
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
@@ -790,44 +881,46 @@ const Pool = () => {
|
||||
return (
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Box className={bodySmall}>
|
||||
<ThemedText.BodySmall>
|
||||
This NFT is part of a liquidity pool. Buying this will increase the price of the remaining pooled NFTs.
|
||||
</Box>
|
||||
</ThemedText.BodySmall>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Box display="flex" flexShrink="0" marginLeft="4" color="textSecondary">
|
||||
<PoolIconContainer>
|
||||
<PoolIcon width="20" height="20" />
|
||||
</Box>
|
||||
</PoolIconContainer>
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const NoContentContainerBackground = styled.div<{ height?: number }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: ${({ height }) => (height ? `${height}px` : 'auto')};
|
||||
padding-top: 100%;
|
||||
background: ${({ theme }) =>
|
||||
`linear-gradient(90deg, ${theme.backgroundSurface} 0%, ${theme.backgroundInteractive} 95.83%)`};
|
||||
`
|
||||
|
||||
const NoContentText = styled(ThemedText.BodyPrimary)`
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
color: ${colors.gray500};
|
||||
`
|
||||
|
||||
const NoContentContainer = ({ height }: { height?: number }) => (
|
||||
<>
|
||||
<Box
|
||||
position="relative"
|
||||
width="full"
|
||||
style={{
|
||||
height: height ? `${height}px` : 'auto',
|
||||
paddingTop: '100%',
|
||||
background: `linear-gradient(90deg, ${themeVars.colors.backgroundSurface} 0%, ${themeVars.colors.backgroundInteractive} 95.83%)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
textAlign="center"
|
||||
left="1/2"
|
||||
top="1/2"
|
||||
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
|
||||
color="gray500"
|
||||
className={body}
|
||||
>
|
||||
Content not
|
||||
<NoContentContainerBackground height={height}>
|
||||
<NoContentText>
|
||||
<Trans>Content not</Trans>
|
||||
<br />
|
||||
available yet
|
||||
</Box>
|
||||
</Box>
|
||||
<Trans>available yet</Trans>
|
||||
</NoContentText>
|
||||
</NoContentContainerBackground>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
|
||||
import { fetchTrendingCollections } from 'nft/queries'
|
||||
import { TimePeriod } from 'nft/types'
|
||||
import { calculateCardIndex } from 'nft/utils'
|
||||
@@ -114,6 +117,7 @@ const TRENDING_COLLECTION_SIZE = 5
|
||||
|
||||
const Banner = () => {
|
||||
const navigate = useNavigate()
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
|
||||
const { data } = useQuery(
|
||||
['trendingCollections'],
|
||||
@@ -130,12 +134,18 @@ const Banner = () => {
|
||||
refetchOnMount: false,
|
||||
}
|
||||
)
|
||||
|
||||
const collections = useMemo(
|
||||
() => data?.filter((collection) => !EXCLUDED_COLLECTIONS.includes(collection.address)).slice(0, 5),
|
||||
[data]
|
||||
const { data: gqlData } = useTrendingCollections(
|
||||
TRENDING_COLLECTION_SIZE + EXCLUDED_COLLECTIONS.length,
|
||||
HistoryDuration.Day
|
||||
)
|
||||
|
||||
const collections = useMemo(() => {
|
||||
const gatedData = isNftGraphqlEnabled ? gqlData : data
|
||||
return gatedData
|
||||
?.filter((collection) => collection.address && !EXCLUDED_COLLECTIONS.includes(collection.address))
|
||||
.slice(0, 5)
|
||||
}, [data, gqlData, isNftGraphqlEnabled])
|
||||
|
||||
const [activeCollectionIdx, setActiveCollectionIdx] = useState(0)
|
||||
const onToggleNextSlide = useCallback(
|
||||
(direction: number) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { formatNumberOrString, NumberType } from '@uniswap/conedison/format'
|
||||
import { loadingAnimation } from 'components/Loader/styled'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { useCollection } from 'graphql/data/nft/Collection'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { Markets, TrendingCollection } from 'nft/types'
|
||||
import { formatWeiToDecimal } from 'nft/utils'
|
||||
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme/components/text'
|
||||
|
||||
@@ -235,7 +236,8 @@ const MARKETS_ENUM_TO_NAME = {
|
||||
}
|
||||
|
||||
export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
|
||||
const { data: gqlCollection, loading } = useCollection(collection.address)
|
||||
const { data: gqlCollection, loading } = useCollection(collection.address ?? '')
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
|
||||
if (loading) return <LoadingCarouselCard />
|
||||
|
||||
@@ -252,9 +254,14 @@ export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
|
||||
</FirstColumnTextWrapper>
|
||||
</TableElement>
|
||||
<TableElement>
|
||||
<ThemedText.SubHeaderSmall color="userThemeColor">
|
||||
{formatWeiToDecimal(collection.floor.toString())} ETH Floor
|
||||
</ThemedText.SubHeaderSmall>
|
||||
{collection.floor && (
|
||||
<ThemedText.SubHeaderSmall color="userThemeColor">
|
||||
{isNftGraphqlEnabled
|
||||
? ethNumberStandardFormatter(collection.floor)
|
||||
: formatWeiToDecimal(collection.floor.toString())}{' '}
|
||||
ETH Floor
|
||||
</ThemedText.SubHeaderSmall>
|
||||
)}
|
||||
</TableElement>
|
||||
<TableElement>
|
||||
<ThemedText.SubHeaderSmall color="userThemeColor">
|
||||
@@ -304,7 +311,7 @@ const CollectionName = styled(ThemedText.MediumHeader)`
|
||||
|
||||
const CarouselCardHeader = ({ collection }: { collection: TrendingCollection }) => {
|
||||
return (
|
||||
<CardHeaderContainer src={collection.bannerImageUrl}>
|
||||
<CardHeaderContainer src={collection.bannerImageUrl ?? ''}>
|
||||
<CardHeaderColumn>
|
||||
<CollectionImage src={collection.imageUrl} />
|
||||
<CollectionNameContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { SquareArrowDownIcon, SquareArrowUpIcon, VerifiedIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { Denomination } from 'nft/types'
|
||||
@@ -114,9 +115,12 @@ export const EthCell = ({
|
||||
usdPrice?: number
|
||||
}) => {
|
||||
const denominatedValue = getDenominatedValue(denomination, true, value, usdPrice)
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const formattedValue = denominatedValue
|
||||
? denomination === Denomination.ETH
|
||||
? formatWeiToDecimal(denominatedValue.toString(), true) + ' ETH'
|
||||
? isNftGraphqlEnabled
|
||||
? ethNumberStandardFormatter(denominatedValue.toString(), false, true, false) + ' ETH'
|
||||
: formatWeiToDecimal(denominatedValue.toString(), true) + ' ETH'
|
||||
: ethNumberStandardFormatter(denominatedValue, true, false, true)
|
||||
: '-'
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ export enum ColumnHeaders {
|
||||
|
||||
const VOLUME_CHANGE_MAX_VALUE = 9999
|
||||
|
||||
const compareFloats = (a: number, b: number): 1 | -1 => {
|
||||
const compareFloats = (a?: number, b?: number): 1 | -1 => {
|
||||
if (!a) return -1
|
||||
if (!b) return 1
|
||||
return Math.round(a * 100000) >= Math.round(b * 100000) ? 1 : -1
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ const CollectionTable = ({ data, timePeriod }: { data: CollectionTableColumn[];
|
||||
const { change } = cell.row.original.volume
|
||||
return timePeriod === TimePeriod.AllTime ? (
|
||||
<TextCell value="-" />
|
||||
) : change >= VOLUME_CHANGE_MAX_VALUE ? (
|
||||
) : change && change >= VOLUME_CHANGE_MAX_VALUE ? (
|
||||
<ChangeCell change={change}>{`>${VOLUME_CHANGE_MAX_VALUE}`}%</ChangeCell>
|
||||
) : (
|
||||
<ChangeCell change={change} />
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
|
||||
import ms from 'ms.macro'
|
||||
import { CollectionTableColumn, Denomination, TimePeriod, VolumeType } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils'
|
||||
@@ -29,7 +32,7 @@ const StyledHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
weight: 500;
|
||||
font-weight: 500;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
font-size: 20px;
|
||||
@@ -69,9 +72,25 @@ const StyledSelectorText = styled(ThemedText.SubHeader)<{ active: boolean }>`
|
||||
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
|
||||
`
|
||||
|
||||
function convertTimePeriodToHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.OneDay:
|
||||
return HistoryDuration.Day
|
||||
case TimePeriod.SevenDays:
|
||||
return HistoryDuration.Week
|
||||
case TimePeriod.ThirtyDays:
|
||||
return HistoryDuration.Month
|
||||
case TimePeriod.AllTime:
|
||||
return HistoryDuration.Max
|
||||
default:
|
||||
return HistoryDuration.Day
|
||||
}
|
||||
}
|
||||
|
||||
const TrendingCollections = () => {
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(TimePeriod.OneDay)
|
||||
const [isEthToggled, setEthToggled] = useState(true)
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
|
||||
const { isSuccess, data } = useQuery(
|
||||
['trendingCollections', timePeriod],
|
||||
@@ -86,6 +105,8 @@ const TrendingCollections = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { data: gqlData, loading } = useTrendingCollections(100, convertTimePeriodToHistoryDuration(timePeriod))
|
||||
|
||||
const { data: usdPrice } = useQuery(['fetchPrice', {}], () => fetchPrice(), {
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
@@ -94,8 +115,10 @@ const TrendingCollections = () => {
|
||||
})
|
||||
|
||||
const trendingCollections = useMemo(() => {
|
||||
if (isSuccess && data) {
|
||||
return data.map((d) => ({
|
||||
const gatedData = isNftGraphqlEnabled ? gqlData : data
|
||||
const dataLoaded = isNftGraphqlEnabled ? !loading : isSuccess
|
||||
if (dataLoaded && gatedData) {
|
||||
return gatedData.map((d) => ({
|
||||
...d,
|
||||
collection: {
|
||||
name: d.name,
|
||||
@@ -114,7 +137,6 @@ const TrendingCollections = () => {
|
||||
},
|
||||
owners: {
|
||||
value: d.owners,
|
||||
change: d.ownersChange,
|
||||
},
|
||||
sales: d.sales,
|
||||
totalSupply: d.totalSupply,
|
||||
@@ -122,7 +144,7 @@ const TrendingCollections = () => {
|
||||
usdPrice,
|
||||
}))
|
||||
} else return [] as CollectionTableColumn[]
|
||||
}, [data, isSuccess, isEthToggled, usdPrice])
|
||||
}, [isNftGraphqlEnabled, gqlData, data, loading, isSuccess, isEthToggled, usdPrice])
|
||||
|
||||
return (
|
||||
<ExploreContainer>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { ListingButton } from 'nft/components/profile/list/ListingButton'
|
||||
import {
|
||||
approveCollectionRow,
|
||||
getTotalEthValue,
|
||||
useSubscribeListingState,
|
||||
verifyStatus,
|
||||
} from 'nft/components/profile/list/utils'
|
||||
import { useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE } from 'nft/pages/profile/shared'
|
||||
import { looksRareNonceFetcher } from 'nft/queries'
|
||||
import { ListingStatus, ProfilePageStateType } from 'nft/types'
|
||||
import { fetchPrice, formatEth, formatUsdPrice } from 'nft/utils'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { formatEth } from 'nft/utils'
|
||||
import { ListingMarkets } from 'nft/utils/listNfts'
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
@@ -185,26 +194,10 @@ export const ListPage = () => {
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const {
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
} = useNFTList(
|
||||
({
|
||||
const { listings, collectionsRequiringApproval, setLooksRareNonce, setCollectionStatusAndCallback } = useNFTList(
|
||||
({ listings, collectionsRequiringApproval, setLooksRareNonce, setCollectionStatusAndCallback }) => ({
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
}) => ({
|
||||
listings,
|
||||
collectionsRequiringApproval,
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setLooksRareNonce,
|
||||
setCollectionStatusAndCallback,
|
||||
}),
|
||||
@@ -212,31 +205,16 @@ export const ListPage = () => {
|
||||
)
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
|
||||
const usdcValue = useStablecoinValue(parsedAmount)
|
||||
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
|
||||
const [showListModal, toggleShowListModal] = useReducer((s) => !s, false)
|
||||
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
const signer = provider?.getSigner()
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice().then((price) => {
|
||||
setEthPriceInUSD(price ?? 0)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// TODO with removal of list v1 see if this logic can be removed
|
||||
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])
|
||||
// instantiate listings and collections to approve when users modify input data
|
||||
useSubscribeListingState()
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalMarketplaces(selectedMarkets)
|
||||
@@ -247,14 +225,13 @@ export const ListPage = () => {
|
||||
token_ids: sellAssets.map((asset) => asset.tokenId),
|
||||
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
usd_value: usdcAmount,
|
||||
...trace,
|
||||
}
|
||||
|
||||
const startListingFlow = async () => {
|
||||
if (!signer) return
|
||||
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
const signerAddress = await signer.getAddress()
|
||||
const nonce = await looksRareNonceFetcher(signerAddress)
|
||||
setLooksRareNonce(nonce ?? 0)
|
||||
@@ -317,9 +294,7 @@ export const ListPage = () => {
|
||||
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
|
||||
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
|
||||
</EthValueWrapper>
|
||||
{!!totalEthListingValue && !!ethPriceInUSD && (
|
||||
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
|
||||
)}
|
||||
{!!usdcValue && <UsdValue>{usdcAmount}</UsdValue>}
|
||||
</ProceedsWrapper>
|
||||
<ListingButton onClick={showModalAndStartListing} />
|
||||
</ProceedsAndButtonWrapper>
|
||||
|
||||
@@ -2,15 +2,13 @@ import { Plural, t, Trans } from '@lingui/macro'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import ms from 'ms.macro'
|
||||
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
|
||||
import { useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useIsMobile, useSellAsset } from 'nft/hooks'
|
||||
import { Listing, WalletAsset } from 'nft/types'
|
||||
import { useMemo, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { getListings } from '../../bag/profile/utils'
|
||||
|
||||
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
|
||||
|
||||
const StyledListingButton = styled(BaseButton)<{ showResolveIssues: boolean; missingPrices: boolean }>`
|
||||
@@ -44,25 +42,9 @@ export const ListingButton = ({ onClick }: { onClick: () => void }) => {
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const { setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||
({ setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
// instantiate listings and collections to approve when user's modify input data
|
||||
useEffect(() => {
|
||||
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
|
||||
setListings(newListings)
|
||||
setCollectionsRequiringApproval(newCollectionsToApprove)
|
||||
setListingStatus(ListingStatus.DEFINED)
|
||||
}, [sellAssets, setCollectionsRequiringApproval, setListingStatus, setListings])
|
||||
|
||||
// Find issues with item listing data
|
||||
const [listingsMissingPrice, listingsBelowFloor] = useMemo(() => {
|
||||
const missingExpiration = sellAssets.some((asset) => {
|
||||
|
||||
@@ -4,19 +4,18 @@ import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { RowsCollpsedIcon, RowsExpandedIcon } from 'nft/components/icons'
|
||||
import { getRoyalty, useHandleGlobalPriceToggle, useSyncPriceWithGlobalMethod } from 'nft/components/profile/list/utils'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { ListingMarket, ListingWarning, WalletAsset } from 'nft/types'
|
||||
import { LOOKS_RARE_CREATOR_BASIS_POINTS } from 'nft/utils'
|
||||
import { ListingMarket, WalletAsset } from 'nft/types'
|
||||
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { Dispatch, DispatchWithoutAction, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { Dispatch, DispatchWithoutAction, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
import { SetPriceMethod } from './NFTListingsGrid'
|
||||
import { PriceTextInput } from './PriceTextInput'
|
||||
import { RoyaltyTooltip } from './RoyaltyTooltip'
|
||||
import { RemoveIconWrap } from './shared'
|
||||
import { RemoveIconWrap, SetPriceMethod } from './shared'
|
||||
|
||||
const LastPriceInfo = styled(Column)`
|
||||
text-align: left;
|
||||
@@ -104,13 +103,6 @@ const ReturnColumn = styled(Column)`
|
||||
}
|
||||
`
|
||||
|
||||
const getRoyalty = (listingMarket: ListingMarket, asset: WalletAsset) => {
|
||||
// LooksRare is a unique case where royalties for creators are a flat 0.5% or 50 basis points
|
||||
const baseFee = listingMarket.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints ?? 0
|
||||
|
||||
return baseFee * 0.01
|
||||
}
|
||||
|
||||
interface MarketplaceRowProps {
|
||||
globalPriceMethod?: SetPriceMethod
|
||||
globalPrice?: number
|
||||
@@ -118,7 +110,6 @@ interface MarketplaceRowProps {
|
||||
selectedMarkets: ListingMarket[]
|
||||
removeMarket?: () => void
|
||||
asset: WalletAsset
|
||||
showMarketplaceLogo: boolean
|
||||
expandMarketplaceRows?: boolean
|
||||
rowHovered?: boolean
|
||||
toggleExpandMarketplaceRows: DispatchWithoutAction
|
||||
@@ -131,7 +122,6 @@ export const MarketplaceRow = ({
|
||||
selectedMarkets,
|
||||
removeMarket = undefined,
|
||||
asset,
|
||||
showMarketplaceLogo,
|
||||
expandMarketplaceRows,
|
||||
toggleExpandMarketplaceRows,
|
||||
rowHovered,
|
||||
@@ -147,9 +137,16 @@ export const MarketplaceRow = ({
|
||||
)?.price
|
||||
)
|
||||
const [globalOverride, setGlobalOverride] = useState(false)
|
||||
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
|
||||
|
||||
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride
|
||||
const price = showGlobalPrice ? globalPrice : listPrice
|
||||
const setPrice = useCallback(
|
||||
(price?: number) => {
|
||||
showGlobalPrice ? setGlobalPrice(price) : setListPrice(price)
|
||||
for (const marketplace of selectedMarkets) setAssetListPrice(asset, price, marketplace)
|
||||
},
|
||||
[asset, selectedMarkets, setAssetListPrice, setGlobalPrice, showGlobalPrice]
|
||||
)
|
||||
|
||||
const fees = useMemo(() => {
|
||||
if (selectedMarkets.length === 1) {
|
||||
@@ -168,68 +165,25 @@ export const MarketplaceRow = ({
|
||||
const feeInEth = price && (price * fees) / 100
|
||||
const userReceives = price && feeInEth && price - feeInEth
|
||||
|
||||
useMemo(() => {
|
||||
for (const market of selectedMarkets) {
|
||||
if (market && asset && asset.basisPoints) {
|
||||
market.royalty = (market.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints) * 0.01
|
||||
}
|
||||
}
|
||||
}, [asset, selectedMarkets])
|
||||
useHandleGlobalPriceToggle(globalOverride, setListPrice, setPrice, listPrice, globalPrice)
|
||||
useSyncPriceWithGlobalMethod(
|
||||
asset,
|
||||
setListPrice,
|
||||
setGlobalPrice,
|
||||
setGlobalOverride,
|
||||
listPrice,
|
||||
globalPrice,
|
||||
globalPriceMethod
|
||||
)
|
||||
|
||||
// When in Same Price Mode and not overriding, update local price when global price changes
|
||||
useEffect(() => {
|
||||
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
|
||||
setListPrice(asset?.floorPrice)
|
||||
setGlobalPrice(asset.floorPrice)
|
||||
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
|
||||
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 = listPrice
|
||||
}
|
||||
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)
|
||||
if (showGlobalPrice) {
|
||||
setPrice(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 onMouseEnter={toggleMarketRowHovered} onMouseLeave={toggleMarketRowHovered}>
|
||||
<FloorPriceInfo>
|
||||
@@ -263,27 +217,14 @@ export const MarketplaceRow = ({
|
||||
))}
|
||||
</MarketIconsWrapper>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
<PriceTextInput
|
||||
listPrice={price}
|
||||
setListPrice={setPrice}
|
||||
isGlobalPrice={showGlobalPrice}
|
||||
setGlobalOverride={setGlobalOverride}
|
||||
globalOverride={globalOverride}
|
||||
asset={asset}
|
||||
/>
|
||||
{rowHovered && ((expandMarketplaceRows && marketRowHovered) || selectedMarkets.length > 1) && (
|
||||
<ExpandMarketIconWrapper onClick={toggleExpandMarketplaceRows}>
|
||||
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { getTotalEthValue, signListingRow } from 'nft/components/profile/list/utils'
|
||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { ListingStatus } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils'
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
@@ -46,64 +49,60 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
const signer = provider?.getSigner()
|
||||
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const {
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
} = useNFTList(
|
||||
({
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}) => ({
|
||||
listingStatus,
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const { setListingStatusAndCallback, setLooksRareNonce, getLooksRareNonce, collectionsRequiringApproval, listings } =
|
||||
useNFTList(
|
||||
({
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}) => ({
|
||||
setListingStatusAndCallback,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const [openSection, toggleOpenSection] = useReducer(
|
||||
(s) => (s === Section.APPROVE ? Section.SIGN : Section.APPROVE),
|
||||
Section.APPROVE
|
||||
)
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice().then((price) => {
|
||||
setEthPriceInUSD(price || 0)
|
||||
})
|
||||
}, [])
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
|
||||
const usdcValue = useStablecoinValue(parsedAmount)
|
||||
const usdcAmount = formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)
|
||||
|
||||
const allCollectionsApproved = useMemo(
|
||||
() => collectionsRequiringApproval.every((collection) => collection.status === ListingStatus.APPROVED),
|
||||
[collectionsRequiringApproval]
|
||||
)
|
||||
|
||||
const allListingsApproved = useMemo(
|
||||
() => listings.every((listing) => listing.status === ListingStatus.APPROVED),
|
||||
[listings]
|
||||
)
|
||||
|
||||
const signListings = async () => {
|
||||
if (!signer || !provider) return
|
||||
// sign listings
|
||||
for (const listing of listings) {
|
||||
await signListingRow(listing, signer, provider, getLooksRareNonce, setLooksRareNonce, setListingStatusAndCallback)
|
||||
}
|
||||
|
||||
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
|
||||
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
usd_value: usdcAmount,
|
||||
...trace,
|
||||
})
|
||||
}
|
||||
|
||||
// Once all collections have been approved, go to next section and start signing listings
|
||||
useEffect(() => {
|
||||
if (allCollectionsApproved) {
|
||||
signListings()
|
||||
@@ -113,8 +112,8 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
}, [allCollectionsApproved])
|
||||
|
||||
const closeModalOnClick = useCallback(() => {
|
||||
listingStatus === ListingStatus.APPROVED ? window.location.reload() : overlayClick()
|
||||
}, [listingStatus, overlayClick])
|
||||
allListingsApproved ? window.location.reload() : overlayClick()
|
||||
}, [allListingsApproved, overlayClick])
|
||||
|
||||
// In the case that a user removes all listings via retry logic, close modal
|
||||
useEffect(() => {
|
||||
@@ -125,7 +124,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
|
||||
<Portal>
|
||||
<Trace modal={InterfaceModalName.NFT_LISTING}>
|
||||
<ListModalWrapper>
|
||||
{listingStatus === ListingStatus.APPROVED ? (
|
||||
{allListingsApproved ? (
|
||||
<SuccessScreen overlayClick={closeModalOnClick} />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -6,7 +6,7 @@ import Row from 'components/Row'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { getTotalEthValue } from 'nft/components/bag/profile/utils'
|
||||
import { getTotalEthValue } from 'nft/components/profile/list/utils'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { formatEth, generateTweetForList, pluralize } from 'nft/utils'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
import { MarketplaceRow } from './MarketplaceRow'
|
||||
import { SetPriceMethod } from './NFTListingsGrid'
|
||||
import { SetPriceMethod } from './shared'
|
||||
|
||||
const IMAGE_THUMBNAIL_SIZE = 60
|
||||
|
||||
@@ -123,10 +123,10 @@ export const NFTListRow = ({
|
||||
const [hovered, toggleHovered] = useReducer((s) => !s, false)
|
||||
const theme = useTheme()
|
||||
|
||||
// Keep localMarkets up to date with changes to globalMarkets
|
||||
useEffect(() => {
|
||||
setLocalMarkets(JSON.parse(JSON.stringify(selectedMarkets)))
|
||||
selectedMarkets.length < 2 && expandMarketplaceRows && toggleExpandMarketplaceRows()
|
||||
}, [expandMarketplaceRows, selectedMarkets])
|
||||
}, [selectedMarkets])
|
||||
|
||||
return (
|
||||
<NFTListRowWrapper
|
||||
@@ -161,17 +161,16 @@ export const NFTListRow = ({
|
||||
</TokenInfoWrapper>
|
||||
</NFTInfoWrapper>
|
||||
<MarketPlaceRowWrapper>
|
||||
{expandMarketplaceRows ? (
|
||||
localMarkets.map((market, index) => {
|
||||
{expandMarketplaceRows && localMarkets.length > 1 ? (
|
||||
localMarkets.map((market) => {
|
||||
return (
|
||||
<MarketplaceRow
|
||||
globalPriceMethod={globalPriceMethod}
|
||||
globalPrice={globalPrice}
|
||||
setGlobalPrice={setGlobalPrice}
|
||||
selectedMarkets={[market]}
|
||||
removeMarket={() => localMarkets.splice(index, 1)}
|
||||
removeMarket={() => setLocalMarkets(localMarkets.filter((oldMarket) => oldMarket.name !== market.name))}
|
||||
asset={asset}
|
||||
showMarketplaceLogo={true}
|
||||
key={asset.name + market.name}
|
||||
expandMarketplaceRows={expandMarketplaceRows}
|
||||
rowHovered={hovered}
|
||||
@@ -186,7 +185,6 @@ export const NFTListRow = ({
|
||||
setGlobalPrice={setGlobalPrice}
|
||||
selectedMarkets={localMarkets}
|
||||
asset={asset}
|
||||
showMarketplaceLogo={false}
|
||||
rowHovered={hovered}
|
||||
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
@@ -12,6 +11,7 @@ import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import { Dropdown } from './Dropdown'
|
||||
import { NFTListRow } from './NFTListRow'
|
||||
import { SetPriceMethod } from './shared'
|
||||
|
||||
const TableHeader = styled.div`
|
||||
display: flex;
|
||||
@@ -143,13 +143,6 @@ const RowDivider = styled.hr`
|
||||
border-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
`
|
||||
|
||||
export enum SetPriceMethod {
|
||||
SAME_PRICE,
|
||||
FLOOR_PRICE,
|
||||
LAST_PRICE,
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const [globalPriceMethod, setGlobalPriceMethod] = useState(SetPriceMethod.CUSTOM)
|
||||
|
||||
@@ -3,16 +3,19 @@ import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { BrokenLinkIcon } from 'nft/components/icons'
|
||||
import { NumericInput } from 'nft/components/layout/Input'
|
||||
import { useUpdateInputAndWarnings } from 'nft/components/profile/list/utils'
|
||||
import { body } from 'nft/css/common.css'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { ListingWarning, WalletAsset } from 'nft/types'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import { formatEth } from 'nft/utils/currency'
|
||||
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import { Dispatch, useRef, useState } from 'react'
|
||||
import { AlertTriangle, Link } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
|
||||
import { WarningType } from './shared'
|
||||
|
||||
const PriceTextInputWrapper = styled(Column)`
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
@@ -71,12 +74,6 @@ const WarningAction = styled.div`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
|
||||
enum WarningType {
|
||||
BELOW_FLOOR,
|
||||
ALREADY_LISTED,
|
||||
NONE,
|
||||
}
|
||||
|
||||
const getWarningMessage = (warning: WarningType) => {
|
||||
let message = <></>
|
||||
switch (warning) {
|
||||
@@ -96,7 +93,6 @@ interface PriceTextInputProps {
|
||||
isGlobalPrice: boolean
|
||||
setGlobalOverride: Dispatch<boolean>
|
||||
globalOverride: boolean
|
||||
warning?: ListingWarning
|
||||
asset: WalletAsset
|
||||
}
|
||||
|
||||
@@ -106,42 +102,35 @@ export const PriceTextInput = ({
|
||||
isGlobalPrice,
|
||||
setGlobalOverride,
|
||||
globalOverride,
|
||||
warning,
|
||||
asset,
|
||||
}: PriceTextInputProps) => {
|
||||
const [warningType, setWarningType] = useState(WarningType.NONE)
|
||||
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
|
||||
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
|
||||
const showResolveIssues = useSellAsset((state) => state.showResolveIssues)
|
||||
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.value = listPrice !== undefined ? `${listPrice}` : ''
|
||||
setWarningType(WarningType.NONE)
|
||||
if (!warning && listPrice) {
|
||||
if (listPrice < (asset?.floorPrice ?? 0)) 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])
|
||||
|
||||
const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
|
||||
|
||||
const warningColor =
|
||||
showResolveIssues && !listPrice
|
||||
(showResolveIssues && !listPrice) ||
|
||||
warningType === WarningType.ALREADY_LISTED ||
|
||||
(warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20)
|
||||
? colors.red400
|
||||
: warningType !== WarningType.NONE
|
||||
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
|
||||
warningType === WarningType.ALREADY_LISTED
|
||||
? colors.red400
|
||||
: theme.accentWarning
|
||||
: isGlobalPrice
|
||||
: warningType === WarningType.BELOW_FLOOR
|
||||
? theme.accentWarning
|
||||
: isGlobalPrice || !!listPrice
|
||||
? theme.accentAction
|
||||
: listPrice != null
|
||||
? theme.textSecondary
|
||||
: theme.accentAction
|
||||
: theme.textSecondary
|
||||
|
||||
const setPrice = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!listPrice && event.target.value.includes('.') && parseFloat(event.target.value) === 0) {
|
||||
return
|
||||
}
|
||||
const val = parseFloat(event.target.value)
|
||||
setListPrice(isNaN(val) ? undefined : val)
|
||||
}
|
||||
|
||||
useUpdateInputAndWarnings(setWarningType, inputRef, asset, listPrice)
|
||||
|
||||
return (
|
||||
<PriceTextInputWrapper>
|
||||
@@ -156,13 +145,7 @@ export const PriceTextInput = ({
|
||||
backgroundColor="none"
|
||||
width={{ sm: '54', md: '68' }}
|
||||
ref={inputRef}
|
||||
onChange={(v: FormEvent<HTMLInputElement>) => {
|
||||
if (!listPrice && v.currentTarget.value.includes('.') && parseFloat(v.currentTarget.value) === 0) {
|
||||
return
|
||||
}
|
||||
const val = parseFloat(v.currentTarget.value)
|
||||
setListPrice(isNaN(val) ? undefined : val)
|
||||
}}
|
||||
onChange={setPrice}
|
||||
/>
|
||||
<CurrencyWrapper listPrice={listPrice}> ETH</CurrencyWrapper>
|
||||
{(isGlobalPrice || globalOverride) && (
|
||||
@@ -172,27 +155,25 @@ export const PriceTextInput = ({
|
||||
)}
|
||||
</InputWrapper>
|
||||
<WarningMessage $color={warningColor}>
|
||||
{warning
|
||||
? warning.message
|
||||
: warningType !== WarningType.NONE && (
|
||||
<WarningRow>
|
||||
<AlertTriangle height={16} width={16} color={warningColor} />
|
||||
<span>
|
||||
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
|
||||
{getWarningMessage(warningType)}
|
||||
|
||||
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
|
||||
</span>
|
||||
<WarningAction
|
||||
onClick={() => {
|
||||
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
|
||||
setWarningType(WarningType.NONE)
|
||||
}}
|
||||
>
|
||||
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
|
||||
</WarningAction>
|
||||
</WarningRow>
|
||||
)}
|
||||
{warningType !== WarningType.NONE && (
|
||||
<WarningRow>
|
||||
<AlertTriangle height={16} width={16} color={warningColor} />
|
||||
<span>
|
||||
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
|
||||
{getWarningMessage(warningType)}
|
||||
|
||||
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
|
||||
</span>
|
||||
<WarningAction
|
||||
onClick={() => {
|
||||
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
|
||||
setWarningType(WarningType.NONE)
|
||||
}}
|
||||
>
|
||||
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
|
||||
</WarningAction>
|
||||
</WarningRow>
|
||||
)}
|
||||
</WarningMessage>
|
||||
</PriceTextInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { getRoyalty } from 'nft/components/profile/list/utils'
|
||||
import { ListingMarket, WalletAsset } from 'nft/types'
|
||||
import { formatEth } from 'nft/utils'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -50,7 +51,7 @@ export const RoyaltyTooltip = ({
|
||||
asset: WalletAsset
|
||||
fees?: number
|
||||
}) => {
|
||||
const maxRoyalty = Math.max(...selectedMarkets.map((market) => market.royalty ?? 0))
|
||||
const maxRoyalty = Math.max(...selectedMarkets.map((market) => getRoyalty(market, asset) ?? 0)).toFixed(2)
|
||||
return (
|
||||
<RoyaltyContainer>
|
||||
{selectedMarkets.map((market) => (
|
||||
|
||||
@@ -14,3 +14,16 @@ export const TitleRow = styled(Row)`
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
export enum SetPriceMethod {
|
||||
SAME_PRICE,
|
||||
FLOOR_PRICE,
|
||||
LAST_PRICE,
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
export enum WarningType {
|
||||
BELOW_FLOOR,
|
||||
ALREADY_LISTED,
|
||||
NONE,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
|
||||
import { SetPriceMethod, WarningType } from 'nft/components/profile/list/shared'
|
||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { LOOKSRARE_MARKETPLACE_CONTRACT, X2Y2_TRANSFER_CONTRACT } from 'nft/queries'
|
||||
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
|
||||
import { CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { approveCollection, LOOKS_RARE_CREATOR_BASIS_POINTS, signListing } from 'nft/utils/listNfts'
|
||||
import { Dispatch, useEffect } from 'react'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
export async function approveCollectionRow(
|
||||
collectionRow: CollectionRow,
|
||||
@@ -12,10 +16,9 @@ export async function approveCollectionRow(
|
||||
collection: CollectionRow,
|
||||
status: ListingStatus,
|
||||
callback?: () => Promise<void>
|
||||
) => void,
|
||||
pauseAllRows?: () => void
|
||||
) => void
|
||||
) {
|
||||
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
|
||||
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
|
||||
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
|
||||
const { marketplace, collectionAddress } = collectionRow
|
||||
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
|
||||
@@ -31,11 +34,6 @@ export async function approveCollectionRow(
|
||||
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
|
||||
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
|
||||
))
|
||||
if (
|
||||
(collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) &&
|
||||
pauseAllRows
|
||||
)
|
||||
pauseAllRows()
|
||||
}
|
||||
|
||||
export async function signListingRow(
|
||||
@@ -44,31 +42,18 @@ export async function signListingRow(
|
||||
provider: Web3Provider,
|
||||
getLooksRareNonce: () => number,
|
||||
setLooksRareNonce: (nonce: number) => void,
|
||||
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void,
|
||||
pauseAllRows?: () => void
|
||||
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
|
||||
) {
|
||||
const looksRareNonce = getLooksRareNonce()
|
||||
const callback = () => {
|
||||
return signListingRow(
|
||||
listing,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
setListingStatusAndCallback,
|
||||
pauseAllRows
|
||||
)
|
||||
return signListingRow(listing, signer, provider, getLooksRareNonce, setLooksRareNonce, setListingStatusAndCallback)
|
||||
}
|
||||
setListingStatusAndCallback(listing, ListingStatus.SIGNING, callback)
|
||||
const { asset, marketplace } = listing
|
||||
const res = await signListing(marketplace, asset, signer, provider, looksRareNonce, (newStatus: ListingStatus) =>
|
||||
setListingStatusAndCallback(listing, newStatus, callback)
|
||||
)
|
||||
if (listing.status === ListingStatus.REJECTED && pauseAllRows) {
|
||||
pauseAllRows()
|
||||
} else {
|
||||
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
|
||||
}
|
||||
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
|
||||
}
|
||||
|
||||
export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
|
||||
@@ -86,7 +71,7 @@ export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
|
||||
return total ? Math.round(total * 10000 + Number.EPSILON) / 10000 : 0
|
||||
}
|
||||
|
||||
export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
|
||||
const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
|
||||
const newCollectionsToApprove: CollectionRow[] = []
|
||||
|
||||
const newListings: ListingRow[] = []
|
||||
@@ -123,69 +108,89 @@ export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], Listin
|
||||
return [newCollectionsToApprove, newListings]
|
||||
}
|
||||
|
||||
type ListingState = {
|
||||
allListingsPending: boolean
|
||||
allListingsDefined: boolean
|
||||
allListingsApproved: boolean
|
||||
allCollectionsPending: boolean
|
||||
allCollectionsDefined: boolean
|
||||
anyActiveSigning: boolean
|
||||
anyActiveFailures: boolean
|
||||
anyActiveRejections: boolean
|
||||
anyPaused: boolean
|
||||
}
|
||||
|
||||
export const getListingState = (
|
||||
collectionsRequiringApproval: CollectionRow[],
|
||||
listings: ListingRow[]
|
||||
): ListingState => {
|
||||
let allListingsPending = true
|
||||
let allListingsDefined = true
|
||||
let allListingsApproved = true
|
||||
let allCollectionsPending = true
|
||||
let allCollectionsDefined = true
|
||||
let anyActiveSigning = false
|
||||
let anyActiveFailures = false
|
||||
let anyActiveRejections = false
|
||||
let anyPaused = false
|
||||
|
||||
if (collectionsRequiringApproval.length === 0) {
|
||||
allCollectionsDefined = allCollectionsPending = false
|
||||
}
|
||||
for (const collection of collectionsRequiringApproval) {
|
||||
if (collection.status !== ListingStatus.PENDING) allCollectionsPending = false
|
||||
if (collection.status !== ListingStatus.DEFINED) allCollectionsDefined = false
|
||||
if (collection.status === ListingStatus.SIGNING) anyActiveSigning = true
|
||||
else if (collection.status === ListingStatus.FAILED) anyActiveFailures = true
|
||||
else if (collection.status === ListingStatus.REJECTED) anyActiveRejections = true
|
||||
else if (collection.status === ListingStatus.PAUSED) anyPaused = true
|
||||
}
|
||||
|
||||
if (listings.length === 0) {
|
||||
allListingsApproved = allListingsDefined = allListingsPending = false
|
||||
}
|
||||
for (const listing of listings) {
|
||||
if (listing.status !== ListingStatus.PENDING) allListingsPending = false
|
||||
if (listing.status !== ListingStatus.DEFINED) allListingsDefined = false
|
||||
if (listing.status !== ListingStatus.APPROVED) allListingsApproved = false
|
||||
if (listing.status === ListingStatus.SIGNING) anyActiveSigning = true
|
||||
else if (listing.status === ListingStatus.FAILED) anyActiveFailures = true
|
||||
else if (listing.status === ListingStatus.REJECTED) anyActiveRejections = true
|
||||
else if (listing.status === ListingStatus.PAUSED) anyPaused = true
|
||||
}
|
||||
return {
|
||||
allListingsPending,
|
||||
allListingsDefined,
|
||||
allListingsApproved,
|
||||
allCollectionsPending,
|
||||
allCollectionsDefined,
|
||||
anyActiveSigning,
|
||||
anyActiveFailures,
|
||||
anyActiveRejections,
|
||||
anyPaused,
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyStatus = (status: ListingStatus) => {
|
||||
return status !== ListingStatus.PAUSED && status !== ListingStatus.APPROVED
|
||||
}
|
||||
|
||||
export function useSubscribeListingState() {
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const { setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||
({ setListings, setCollectionsRequiringApproval }) => ({
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
useEffect(() => {
|
||||
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
|
||||
setListings(newListings)
|
||||
setCollectionsRequiringApproval(newCollectionsToApprove)
|
||||
}, [sellAssets, setCollectionsRequiringApproval, setListings])
|
||||
}
|
||||
|
||||
export function useHandleGlobalPriceToggle(
|
||||
globalOverride: boolean,
|
||||
setListPrice: Dispatch<number | undefined>,
|
||||
setPrice: (price?: number) => void,
|
||||
listPrice?: number,
|
||||
globalPrice?: number
|
||||
) {
|
||||
useEffect(() => {
|
||||
let price: number | undefined
|
||||
if (globalOverride) {
|
||||
if (!listPrice) setListPrice(globalPrice)
|
||||
price = globalPrice
|
||||
} else {
|
||||
price = listPrice
|
||||
}
|
||||
setPrice(price)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [globalOverride])
|
||||
}
|
||||
|
||||
export function useSyncPriceWithGlobalMethod(
|
||||
asset: WalletAsset,
|
||||
setListPrice: Dispatch<number | undefined>,
|
||||
setGlobalPrice: Dispatch<number | undefined>,
|
||||
setGlobalOverride: Dispatch<boolean>,
|
||||
listPrice?: number,
|
||||
globalPrice?: number,
|
||||
globalPriceMethod?: SetPriceMethod
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
|
||||
setListPrice(asset?.floorPrice)
|
||||
setGlobalPrice(asset.floorPrice)
|
||||
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
|
||||
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])
|
||||
}
|
||||
|
||||
export function useUpdateInputAndWarnings(
|
||||
setWarningType: Dispatch<WarningType>,
|
||||
inputRef: React.MutableRefObject<HTMLInputElement>,
|
||||
asset: WalletAsset,
|
||||
listPrice?: number
|
||||
) {
|
||||
useEffect(() => {
|
||||
setWarningType(WarningType.NONE)
|
||||
const price = listPrice ?? 0
|
||||
inputRef.current.value = `${price}`
|
||||
if (price < (asset?.floorPrice ?? 0) && price > 0) setWarningType(WarningType.BELOW_FLOOR)
|
||||
else if (asset.floor_sell_order_price && price >= asset.floor_sell_order_price)
|
||||
setWarningType(WarningType.ALREADY_LISTED)
|
||||
}, [asset?.floorPrice, asset.floor_sell_order_price, inputRef, listPrice, setWarningType])
|
||||
}
|
||||
|
||||
export const getRoyalty = (listingMarket: ListingMarket, asset: WalletAsset) => {
|
||||
// LooksRare is a unique case where royalties for creators are a flat 0.5% or 50 basis points
|
||||
const baseFee = listingMarket.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints ?? 0
|
||||
|
||||
return baseFee * 0.01
|
||||
}
|
||||
@@ -4,12 +4,10 @@ import { devtools } from 'zustand/middleware'
|
||||
|
||||
interface NFTListState {
|
||||
looksRareNonce: number
|
||||
listingStatus: ListingStatus
|
||||
listings: ListingRow[]
|
||||
collectionsRequiringApproval: CollectionRow[]
|
||||
setLooksRareNonce: (nonce: number) => void
|
||||
getLooksRareNonce: () => number
|
||||
setListingStatus: (status: ListingStatus) => void
|
||||
setListings: (listings: ListingRow[]) => void
|
||||
setCollectionsRequiringApproval: (collections: CollectionRow[]) => void
|
||||
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
|
||||
@@ -23,7 +21,6 @@ interface NFTListState {
|
||||
export const useNFTList = create<NFTListState>()(
|
||||
devtools((set, get) => ({
|
||||
looksRareNonce: 0,
|
||||
listingStatus: ListingStatus.DEFINED,
|
||||
listings: [],
|
||||
collectionsRequiringApproval: [],
|
||||
setLooksRareNonce: (nonce) =>
|
||||
@@ -33,10 +30,6 @@ export const useNFTList = create<NFTListState>()(
|
||||
getLooksRareNonce: () => {
|
||||
return get().looksRareNonce
|
||||
},
|
||||
setListingStatus: (status) =>
|
||||
set(() => {
|
||||
return { listingStatus: status }
|
||||
}),
|
||||
setListings: (listings) =>
|
||||
set(() => {
|
||||
const updatedListings = listings.map((listing) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import create from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
import { ListingMarket, ListingWarning, WalletAsset } from '../types'
|
||||
import { ListingMarket, WalletAsset } from '../types'
|
||||
|
||||
interface SellAssetState {
|
||||
sellAssets: WalletAsset[]
|
||||
@@ -16,10 +16,6 @@ interface SellAssetState {
|
||||
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
|
||||
toggleShowResolveIssues: () => void
|
||||
setIssues: (issues: number) => void
|
||||
// TODO: After merging v2, see if this marketplace logic can be removed
|
||||
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
|
||||
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
|
||||
removeAllMarketplaceWarnings: () => void
|
||||
}
|
||||
|
||||
export const useSellAsset = create<SellAssetState>()(
|
||||
@@ -118,47 +114,6 @@ export const useSellAsset = create<SellAssetState>()(
|
||||
return { sellAssets: assetsCopy }
|
||||
})
|
||||
},
|
||||
addMarketplaceWarning: (asset, warning) => {
|
||||
set(({ sellAssets }) => {
|
||||
const assetsCopy = [...sellAssets]
|
||||
asset.listingWarnings?.push(warning)
|
||||
const index = sellAssets.findIndex(
|
||||
(n) => n.tokenId === asset.tokenId && n.asset_contract.address === asset.asset_contract.address
|
||||
)
|
||||
assetsCopy[index] = asset
|
||||
return { sellAssets: assetsCopy }
|
||||
})
|
||||
},
|
||||
removeMarketplaceWarning: (asset, warning, setGlobalOverride?) => {
|
||||
set(({ sellAssets }) => {
|
||||
const assetsCopy = [...sellAssets]
|
||||
if (asset.listingWarnings === undefined || asset.newListings === undefined) return { sellAssets: assetsCopy }
|
||||
const warningIndex =
|
||||
asset.listingWarnings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
|
||||
asset.listingWarnings?.splice(warningIndex, 1)
|
||||
if (warning?.message?.includes('LISTING BELOW FLOOR')) {
|
||||
if (setGlobalOverride) {
|
||||
asset.newListings?.forEach((listing) => (listing.overrideFloorPrice = true))
|
||||
} else {
|
||||
const listingIndex =
|
||||
asset.newListings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
|
||||
asset.newListings[listingIndex].overrideFloorPrice = true
|
||||
}
|
||||
}
|
||||
const index = sellAssets.findIndex(
|
||||
(n) => n.tokenId === asset.tokenId && n.asset_contract.address === asset.asset_contract.address
|
||||
)
|
||||
assetsCopy[index] = asset
|
||||
return { sellAssets: assetsCopy }
|
||||
})
|
||||
},
|
||||
removeAllMarketplaceWarnings: () => {
|
||||
set(({ sellAssets }) => {
|
||||
const assetsCopy = [...sellAssets]
|
||||
assetsCopy.map((asset) => (asset.listingWarnings = []))
|
||||
return { sellAssets: assetsCopy }
|
||||
})
|
||||
},
|
||||
toggleShowResolveIssues: () => {
|
||||
set(({ showResolveIssues }) => {
|
||||
return { showResolveIssues: !showResolveIssues }
|
||||
|
||||
@@ -7,8 +7,8 @@ import { XXXL_BAG_WIDTH } from 'nft/components/bag/Bag'
|
||||
import { ListPage } from 'nft/components/profile/list/ListPage'
|
||||
import { ProfilePage } from 'nft/components/profile/view/ProfilePage'
|
||||
import { ProfilePageLoadingSkeleton } from 'nft/components/profile/view/ProfilePageLoadingSkeleton'
|
||||
import { useBag, useNFTList, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { ListingStatus, ProfilePageStateType } from 'nft/types'
|
||||
import { useBag, useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { Suspense, useEffect, useRef } from 'react'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -62,15 +62,8 @@ const ConnectWalletButton = styled(ButtonPrimary)`
|
||||
const ProfileContent = () => {
|
||||
const sellPageState = useProfilePageState((state) => state.state)
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
|
||||
const resetSellAssets = useSellAsset((state) => state.reset)
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
const setListingStatus = useNFTList((state) => state.setListingStatus)
|
||||
|
||||
useEffect(() => {
|
||||
removeAllMarketplaceWarnings()
|
||||
setListingStatus(ListingStatus.DEFINED)
|
||||
}, [removeAllMarketplaceWarnings, sellPageState, setListingStatus])
|
||||
|
||||
const { account } = useWeb3React()
|
||||
const accountRef = useRef(account)
|
||||
|
||||
@@ -35,21 +35,20 @@ export interface TransactionsResponse {
|
||||
}
|
||||
|
||||
export interface TrendingCollection {
|
||||
name: string
|
||||
address: string
|
||||
imageUrl: string
|
||||
bannerImageUrl: string
|
||||
isVerified: boolean
|
||||
volume: number
|
||||
volumeChange: number
|
||||
floor: number
|
||||
floorChange: number
|
||||
marketCap: number
|
||||
percentListed: number
|
||||
owners: number
|
||||
ownersChange: number
|
||||
totalSupply: number
|
||||
sales: number
|
||||
name?: string
|
||||
address?: string
|
||||
imageUrl?: string
|
||||
bannerImageUrl?: string
|
||||
isVerified?: boolean
|
||||
volume?: number
|
||||
volumeChange?: number
|
||||
floor?: number
|
||||
floorChange?: number
|
||||
marketCap?: number
|
||||
percentListed?: number
|
||||
owners?: number
|
||||
totalSupply?: number
|
||||
sales?: number
|
||||
}
|
||||
|
||||
export enum Denomination {
|
||||
@@ -59,26 +58,25 @@ export enum Denomination {
|
||||
|
||||
export interface CollectionTableColumn {
|
||||
collection: {
|
||||
name: string
|
||||
address: string
|
||||
logo: string
|
||||
isVerified: boolean
|
||||
name?: string
|
||||
address?: string
|
||||
logo?: string
|
||||
isVerified?: boolean
|
||||
}
|
||||
volume: {
|
||||
value: number
|
||||
change: number
|
||||
type: VolumeType
|
||||
value?: number
|
||||
change?: number
|
||||
type?: VolumeType
|
||||
}
|
||||
floor: {
|
||||
value: number
|
||||
change: number
|
||||
value?: number
|
||||
change?: number
|
||||
}
|
||||
owners: {
|
||||
value: number
|
||||
change: number
|
||||
value?: number
|
||||
}
|
||||
sales: number
|
||||
totalSupply: number
|
||||
sales?: number
|
||||
totalSupply?: number
|
||||
denomination: Denomination
|
||||
usdPrice?: number
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ export interface ListingMarket {
|
||||
name: string
|
||||
fee: number
|
||||
icon: string
|
||||
royalty?: number
|
||||
}
|
||||
export interface ListingWarning {
|
||||
marketplace: ListingMarket
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface SellOrder {
|
||||
@@ -72,7 +67,6 @@ export interface WalletAsset {
|
||||
marketAgnosticPrice?: number
|
||||
newListings?: Listing[]
|
||||
marketplaces?: ListingMarket[]
|
||||
listingWarnings?: ListingWarning[]
|
||||
}
|
||||
|
||||
export interface WalletCollection {
|
||||
|
||||
Reference in New Issue
Block a user