diff --git a/webapp/src/components/AssetCard/AssetCard.spec.tsx b/webapp/src/components/AssetCard/AssetCard.spec.tsx index 66eba391f8..e779118aad 100644 --- a/webapp/src/components/AssetCard/AssetCard.spec.tsx +++ b/webapp/src/components/AssetCard/AssetCard.spec.tsx @@ -93,6 +93,7 @@ describe('AssetCard', () => { expect(screen.getByTestId('asset-card-content')).toBeInTheDocument() }) }) + describe('when its not interesected', () => { it('should not render the Asset Card content', () => { renderAssetCard({ @@ -107,20 +108,25 @@ describe('AssetCard', () => { asset = { ...asset, itemId: 'itemId' } as Asset }) - describe('when the asset is an item', () => { - beforeEach(() => { - asset = { ...asset, itemId: 'itemId' } as Asset + it('should render the favorites counter', () => { + renderAssetCard({ + asset }) + mockAllIsIntersecting(true) + expect(screen.getByTestId(FAVORITES_COUNTER_TEST_ID)).toBeInTheDocument() + }) + }) - it('should render the favorites counter', () => { - renderAssetCard({ - asset - }) - mockAllIsIntersecting(true) - expect( - screen.getByTestId(FAVORITES_COUNTER_TEST_ID) - ).toBeInTheDocument() + describe('when the asset is an nft', () => { + beforeEach(() => { + asset = { ...asset, tokenId: 'tokenId' } as Asset + }) + + it('should not render the favorites counter', () => { + const { queryByTestId } = renderAssetCard({ + asset }) + expect(queryByTestId(FAVORITES_COUNTER_TEST_ID)).toBeNull() }) }) }) diff --git a/webapp/src/components/AssetCard/EmoteTags/EmoteTags.tsx b/webapp/src/components/AssetCard/EmoteTags/EmoteTags.tsx index 77986d31fc..a967ee14bb 100644 --- a/webapp/src/components/AssetCard/EmoteTags/EmoteTags.tsx +++ b/webapp/src/components/AssetCard/EmoteTags/EmoteTags.tsx @@ -1,5 +1,8 @@ -import { NFTCategory } from '@dcl/schemas' import classNames from 'classnames' +import { NFTCategory } from '@dcl/schemas' +import { Popup } from 'decentraland-ui' +import { T } from 'decentraland-dapps/dist/modules/translation/utils' +import { isNFT } from '../../../modules/asset/utils' import { AssetType } from '../../../modules/asset/types' import RarityBadge from '../../RarityBadge' import { Props } from './EmoteTags.types' @@ -7,17 +10,35 @@ import styles from './EmoteTags.module.css' const EmoteTags = (props: Props) => { const { asset } = props - const { rarity } = asset.data.emote! + const { rarity, loop } = asset.data.emote || {} return (
- + {rarity ? ( + + ) : null} + {isNFT(asset) && loop !== undefined ? ( + } + trigger={ +
+ +
+ } + /> + ) : null}
) } diff --git a/webapp/src/components/AssetImage/AssetImage.container.ts b/webapp/src/components/AssetImage/AssetImage.container.ts index eb992c1753..b94eadd417 100644 --- a/webapp/src/components/AssetImage/AssetImage.container.ts +++ b/webapp/src/components/AssetImage/AssetImage.container.ts @@ -39,6 +39,7 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { avatar = profile.avatars[0] } return { + wallet, avatar, wearableController: getWearablePreviewController(state), isTryingOn: getIsTryingOn(state), diff --git a/webapp/src/components/AssetImage/AssetImage.css b/webapp/src/components/AssetImage/AssetImage.css index e48216d359..36e4d811a1 100644 --- a/webapp/src/components/AssetImage/AssetImage.css +++ b/webapp/src/components/AssetImage/AssetImage.css @@ -334,7 +334,7 @@ width: 100%; bottom: -110px; padding-top: 20px; - height: 84px; + height: 94px; } .AssetImage .rarity-background, .AssetImage .WearablePreview { diff --git a/webapp/src/components/AssetImage/AssetImage.tsx b/webapp/src/components/AssetImage/AssetImage.tsx index 8be2a4b0a8..91113307ea 100644 --- a/webapp/src/components/AssetImage/AssetImage.tsx +++ b/webapp/src/components/AssetImage/AssetImage.tsx @@ -83,7 +83,8 @@ const AssetImage = (props: Props) => { onSetWearablePreviewController, children, hasBadges, - item + item, + wallet } = props const { parcel, estate, wearable, emote, ens } = asset.data @@ -264,22 +265,26 @@ const AssetImage = (props: Props) => { const isTryingOnEnabled = isTryingOn && hasRepresentation - const urn = !isNFT(asset) && asset.network === Network.ETHEREUM ? getEthereumItemUrn(asset) : '' + const ethereumUrn = + !isNFT(asset) && asset.network === Network.ETHEREUM + ? getEthereumItemUrn(asset) + : '' const wearablePreviewProps = !isNFT(asset) && asset.network === Network.ETHEREUM ? { - urns: [urn], + urns: [ethereumUrn], background: Rarity.getColor(asset.rarity), type: isTryingOn ? PreviewType.AVATAR : PreviewType.WEARABLE } : { contractAddress: asset.contractAddress, - itemId, tokenId } + const isOwnerOfNFT = isNFT(asset) && wallet?.address === asset.owner + wearablePreview = ( <> { {...wearablePreviewProps} dev={config.is(Env.DEVELOPMENT)} /> - {isAvailableForMint ? ( + {isAvailableForMint && !isOwnerOfNFT ? ( { ) + const isOwnerOfNFT = isNFT(asset) && wallet?.address === asset.owner + if (isDraggable) { wearablePreview = ( <> @@ -466,6 +473,16 @@ const AssetImage = (props: Props) => { onError={handleError} dev={config.is(Env.DEVELOPMENT)} /> + {isAvailableForMint && !isOwnerOfNFT ? ( + + ) : null} {isLoadingWearablePreview ? (
@@ -46,7 +48,12 @@ export enum ControlOptionAction { export type MapStateProps = Pick< Props, - 'avatar' | 'wearableController' | 'isTryingOn' | 'isPlayingEmote' | 'item' + | 'avatar' + | 'wearableController' + | 'isTryingOn' + | 'isPlayingEmote' + | 'item' + | 'wallet' > export type MapDispatchProps = Pick< Props, diff --git a/webapp/src/components/AssetList/AssetList.tsx b/webapp/src/components/AssetList/AssetList.tsx index 13d3b7713f..43c29f7666 100644 --- a/webapp/src/components/AssetList/AssetList.tsx +++ b/webapp/src/components/AssetList/AssetList.tsx @@ -93,21 +93,12 @@ const AssetList = (props: Props) => { ) } - // const currentSection = - // assetType === AssetType.ITEM - // ? t('browse_page.primary_market_title').toLocaleLowerCase() - // : t('browse_page.secondary_market_title').toLocaleLowerCase() - // const alternativeSection = - // assetType === AssetType.ITEM - // ? t('browse_page.secondary_market_title').toLocaleLowerCase() - // : t('browse_page.primary_market_title').toLocaleLowerCase() return (
{t(`${emptyStateTranslationString}.title`, { search - // currentSection })} @@ -115,8 +106,6 @@ const AssetList = (props: Props) => { id={`${emptyStateTranslationString}.action`} values={{ search, - // currentSection, - // section: alternativeSection, 'if-filters': (chunks: string) => hasFiltersEnabled ? chunks : '', clearFilters: (chunks: string) => ( diff --git a/webapp/src/components/AssetPage/BidsTable/BidsTable.module.css b/webapp/src/components/AssetPage/BidsTable/BidsTable.module.css index a9edce0533..3492661fc4 100644 --- a/webapp/src/components/AssetPage/BidsTable/BidsTable.module.css +++ b/webapp/src/components/AssetPage/BidsTable/BidsTable.module.css @@ -62,6 +62,7 @@ display: flex; align-items: center; font-size: 14px; + flex-wrap: wrap; } .manaField :global(.ui.header:last-child) { @@ -79,4 +80,9 @@ .linkedProfileRow { margin-left: unset; } + .viewListingContainer { + flex-direction: column; + gap: 6px; + padding: 6px; + } } diff --git a/webapp/src/components/AssetPage/BidsTable/BidsTable.tsx b/webapp/src/components/AssetPage/BidsTable/BidsTable.tsx index 960c1d4df1..ac74b0c6a5 100644 --- a/webapp/src/components/AssetPage/BidsTable/BidsTable.tsx +++ b/webapp/src/components/AssetPage/BidsTable/BidsTable.tsx @@ -15,7 +15,7 @@ import { ConfirmInputValueModal } from '../../ConfirmInputValueModal' import { formatDataToTable } from './utils' import { Props } from './BidsTable.types' -export const ROWS_PER_PAGE = 6 +export const ROWS_PER_PAGE = 5 const INITIAL_PAGE = 1 const BidsTable = (props: Props) => { @@ -60,6 +60,7 @@ const BidsTable = (props: Props) => { // We're doing this outside of redux to avoid having to store all orders when we only care about the first ROWS_PER_PAGE useEffect(() => { + let cancel = false if (nft) { setIsLoading(true) bidAPI @@ -72,6 +73,7 @@ const BidsTable = (props: Props) => { ((page - 1) * ROWS_PER_PAGE).toString() ) .then(response => { + if (cancel) return setTotal(response.total) setBids( formatDataToTable( @@ -83,10 +85,13 @@ const BidsTable = (props: Props) => { ) setTotalPages(Math.ceil(response.total / ROWS_PER_PAGE) | 0) }) - .finally(() => setIsLoading(false)) + .finally(() => !cancel && setIsLoading(false)) .catch(error => { console.error(error) }) + return () => { + cancel = true + } } }, [nft, setIsLoading, setBids, page, sortBy, address, isMobileOrTablet]) diff --git a/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.module.css b/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.module.css index 30b5bbda1f..1f0d92af51 100644 --- a/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.module.css +++ b/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.module.css @@ -182,4 +182,9 @@ .BuyNFTBox .informationContainer { gap: 10px; } + + .BuyNFTBox .centerItems { + flex-direction: column; + margin-top: 12px; + } } diff --git a/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.tsx b/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.tsx index bae0e81005..c3df3a788d 100644 --- a/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.tsx +++ b/webapp/src/components/AssetPage/BuyNFTBox/BuyNFTBox.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { ListingStatus, Order, OrderFilters, OrderSortBy } from '@dcl/schemas' @@ -29,6 +29,7 @@ const BuyNFTBox = ({ nft, address }: Props) => { const isOwner = nft && nft?.owner === address useEffect(() => { + let cancel = false if (!isOwner && nft) { bidAPI .fetchByNFT( @@ -41,15 +42,19 @@ const BuyNFTBox = ({ nft, address }: Props) => { address ) .then(response => { - if (response.total === 0) setCanBid(true) + if (response.total === 0 && !cancel) setCanBid(true) }) .catch(error => { console.error(error) }) } + return () => { + cancel = true + } }, [nft, address, isOwner]) useEffect(() => { + let cancel = false if (nft) { setIsLoading(true) @@ -64,161 +69,201 @@ const BuyNFTBox = ({ nft, address }: Props) => { orderAPI .fetchOrders(params, OrderSortBy.CHEAPEST) .then(response => { - if (response.data.length > 0) { + if (!cancel && response.data.length > 0) { setListing({ order: response.data[0], total: response.total }) } }) - .finally(() => setIsLoading(false)) + .finally(() => !cancel && setIsLoading(false)) .catch(error => { console.error(error) }) } + return () => { + cancel = true + } }, [nft]) - return ( -
- {isLoading ? ( -
- -
- ) : nft && listing ? ( -
-
-
- - {t('best_buying_option.minting.price').toUpperCase()} - -
-
- - {formatWeiToAssetCard(listing.order.price)} - -
- {+listing.order.price > 0 && ( -
- {'('} - - {')'} -
- )} + const renderLoading = useCallback( + () => ( +
+ +
+ ), + [] + ) + + const renderHasListing = useCallback(() => { + if (!nft || !listing) return null + return ( +
+
+
+ + {t('best_buying_option.minting.price').toUpperCase()} + +
+
+ + {formatWeiToAssetCard(listing.order.price)} +
+ {+listing.order.price > 0 && ( +
+ {'('} + + {')'} +
+ )}
+
-
- - {t('best_buying_option.buy_listing.issue_number').toUpperCase()} - -
- #{listing.order.issuedId} -
+
+ + {t('best_buying_option.buy_listing.issue_number').toUpperCase()} + +
+ #{listing.order.issuedId}
- {isOwner ? ( - listing ? ( - <> - - - - ) : ( - - ) - ) : ( - - )} - {canBid && ( +
+ {isOwner ? ( + <> - )} - - clock + + + ) : ( + + )} + {canBid && !isOwner && ( +
- ) : ( - nft && ( -
-
-
- - {t('best_buying_option.minting.price').toUpperCase()} - -
- {t('best_buying_option.buy_listing.no_offer')} + {t('best_buying_option.buy_listing.make_offer')} + + )} + + clock +   + {t('best_buying_option.buy_listing.expires')}  + {formatDistanceToNow(listing.order.expiresAt, { + addSuffix: true + })} + . + +
+ ) + }, [canBid, isOwner, listing, nft]) + + const renderOwnerAndNoListingOptions = useCallback(() => { + if (!nft) return null + return ( +
+ + {!listing ? ( + + ) : null} +
+ ) + }, [listing, nft]) + + return ( +
+ {isLoading + ? renderLoading() + : !!listing + ? renderHasListing() + : isOwner + ? renderOwnerAndNoListingOptions() + : nft && ( +
+
+
+ + {t('best_buying_option.minting.price').toUpperCase()} + +
+ {t('asset_card.not_for_sale')} +
-
-
- - {t('best_buying_option.buy_listing.make_offer').toUpperCase()} - -
- #{nft.issuedId} +
+ + {t( + 'best_buying_option.buy_listing.issue_number' + ).toUpperCase()} + +
+ #{nft.issuedId} +
+ {!isOwner && canBid && ( + + )}
- -
- ) - )} + )}
) } diff --git a/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.module.css b/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.module.css index 54fd9de775..906f274887 100644 --- a/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.module.css +++ b/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.module.css @@ -13,7 +13,7 @@ .EmoteDetail .assetImageContainer :global(.AssetImage) { border-radius: 12px; - overflow: hidden; + overflow: visible; } .EmoteDetail .badges { @@ -41,3 +41,23 @@ .issued { color: var(--secondary-text); } + +@media (max-width: 768px) { + .EmoteDetail .wearableInformationContainer { + flex-direction: column; + } + .EmoteDetail .assetImageContainer { + height: 100%; + } + .EmoteDetail .actionsContainer { + width: 100%; + } + .EmoteDetail .badges { + margin-bottom: 18px; + } + .EmoteDetail .emoteOwnerAndCollectionContainer { + margin-top: 18px; + display: flex; + flex-direction: column; + } +} diff --git a/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.tsx b/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.tsx index b901c65bbc..cd85066a09 100644 --- a/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.tsx +++ b/webapp/src/components/AssetPage/EmoteDetail/EmoteDetail.tsx @@ -92,7 +92,7 @@ const EmoteDetail = ({ nft }: Props) => {
-
+
diff --git a/webapp/src/components/AssetPage/ListingsTableContainer/ListingsTableContainer.module.css b/webapp/src/components/AssetPage/ListingsTableContainer/ListingsTableContainer.module.css index 328ee83b54..9929bb26da 100644 --- a/webapp/src/components/AssetPage/ListingsTableContainer/ListingsTableContainer.module.css +++ b/webapp/src/components/AssetPage/ListingsTableContainer/ListingsTableContainer.module.css @@ -17,18 +17,6 @@ padding-left: 20px; } -.listingsTableContainer :global(.filtertabsContainer) { - height: 69px; -} - -.listingsTableContainer :global(.ui.basic.table tbody tr) { - height: 65px; -} - -.listingsTableContainer :global(.dcl.mana.inline) { - line-height: 20px; -} - .sortByDropdown { border-radius: 12px; margin-right: 20px; @@ -50,6 +38,19 @@ align-items: center; } +@media (min-width: 768px) { + .listingsTableContainer :global(.filtertabsContainer) { + height: 69px; + } + + .listingsTableContainer :global(.ui.basic.table tbody tr) { + height: 65px; + } + .listingsTableContainer :global(.dcl.mana.inline) { + line-height: 20px; + } +} + @media (max-width: 768px) { :global(.ui.button.basic) { text-align: left; diff --git a/webapp/src/components/AssetPage/SaleRentActionBox/SaleRentActionBox.tsx b/webapp/src/components/AssetPage/SaleRentActionBox/SaleRentActionBox.tsx index 09c51402ab..05400d2223 100644 --- a/webapp/src/components/AssetPage/SaleRentActionBox/SaleRentActionBox.tsx +++ b/webapp/src/components/AssetPage/SaleRentActionBox/SaleRentActionBox.tsx @@ -141,7 +141,6 @@ const SaleRentActionBox = ({
{t('global.price')}