diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 7fe3a66457..894e99797d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0-development", "dependencies": { "@dcl/crypto": "^3.0.0", - "@dcl/schemas": "^6.15.0", + "@dcl/schemas": "^6.17.0", "@dcl/ui-env": "^1.2.0", "@ethersproject/providers": "^5.6.2", "classnames": "^2.3.1", @@ -2027,9 +2027,9 @@ } }, "node_modules/@dcl/schemas": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-6.15.0.tgz", - "integrity": "sha512-JepCFaNcaeTrONVh/TlCsajhogKhSfLjL1pAJfKIbRU/3tcZpfyWP2vka+s53pYVQbufQ1+MJzKIuG4+urC/dA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-6.17.0.tgz", + "integrity": "sha512-03mUwh8GmaU3wueVM64JF86acS4NvELKY2aZFBATWq/wUZ/KpaMedY7XFiB1eUmusWBVHmVRnsbbOZ1cw3CCFw==", "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -29041,9 +29041,9 @@ } }, "@dcl/schemas": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-6.15.0.tgz", - "integrity": "sha512-JepCFaNcaeTrONVh/TlCsajhogKhSfLjL1pAJfKIbRU/3tcZpfyWP2vka+s53pYVQbufQ1+MJzKIuG4+urC/dA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-6.17.0.tgz", + "integrity": "sha512-03mUwh8GmaU3wueVM64JF86acS4NvELKY2aZFBATWq/wUZ/KpaMedY7XFiB1eUmusWBVHmVRnsbbOZ1cw3CCFw==", "requires": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", diff --git a/webapp/package.json b/webapp/package.json index c457598921..cc98ace25f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -3,7 +3,7 @@ "version": "0.0.0-development", "dependencies": { "@dcl/crypto": "^3.0.0", - "@dcl/schemas": "^6.15.0", + "@dcl/schemas": "^6.17.0", "@dcl/ui-env": "^1.2.0", "@ethersproject/providers": "^5.6.2", "classnames": "^2.3.1", diff --git a/webapp/src/components/AccountSidebar/CurrentAccountSidebar/CurrentAccountSidebar.tsx b/webapp/src/components/AccountSidebar/CurrentAccountSidebar/CurrentAccountSidebar.tsx index d1ad8b6c94..abbbb6b4b8 100644 --- a/webapp/src/components/AccountSidebar/CurrentAccountSidebar/CurrentAccountSidebar.tsx +++ b/webapp/src/components/AccountSidebar/CurrentAccountSidebar/CurrentAccountSidebar.tsx @@ -77,6 +77,7 @@ const CurrentAccountSidebar = ({ section, onBrowse }: Props) => ( ( [AssetFilter.BodyShape]: true, [AssetFilter.Network]: true, [AssetFilter.OnSale]: false, - [AssetFilter.More]: false, + [AssetFilter.More]: false }} /> diff --git a/webapp/src/components/AssetCard/AssetCard.container.ts b/webapp/src/components/AssetCard/AssetCard.container.ts index 65e5d26655..85fd5dc0d6 100644 --- a/webapp/src/components/AssetCard/AssetCard.container.ts +++ b/webapp/src/components/AssetCard/AssetCard.container.ts @@ -9,7 +9,7 @@ import { getAssetPrice, isNFT } from '../../modules/asset/utils' import { locations } from '../../modules/routing/locations' import { getOpenRentalId } from '../../modules/rental/utils' import { getRentalById } from '../../modules/rental/selectors' -import { getPageName } from '../../modules/routing/selectors' +import { getPageName, getSortBy } from '../../modules/routing/selectors' import { PageName } from '../../modules/routing/types' import { MapStateProps, OwnProps, MapDispatchProps } from './AssetCard.types' import AssetCard from './AssetCard' @@ -39,7 +39,8 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { ? isClaimingBackLandTransactionPending(state, asset) : false, rental: rentalOfNFT, - showRentalChip: rentalOfNFT !== null && pageName === PageName.ACCOUNT + showRentalChip: rentalOfNFT !== null && pageName === PageName.ACCOUNT, + sortBy: getSortBy(state) } } diff --git a/webapp/src/components/AssetCard/AssetCard.css b/webapp/src/components/AssetCard/AssetCard.css index ede07561b3..8ac950a550 100644 --- a/webapp/src/components/AssetCard/AssetCard.css +++ b/webapp/src/components/AssetCard/AssetCard.css @@ -1,5 +1,48 @@ .AssetCard { - overflow: hidden; + height: 346px; + overflow: visible; +} + +.ui.cards a.card.AssetCard.catalog:hover, +.ui.link.card.AssetCard.catalog:hover, +.ui.link.cards .card.AssetCard.catalog:hover, +a.ui.card.AssetCard.catalog:hover, +.ui.cards > .ui.card.AssetCard.catalog.link:hover, +.ui.card.AssetCard.catalog.link:hover { + border: none !important; +} + +.ui.card.AssetCard > .content, +.ui.cards > .card.AssetCard > .content { + height: 185px; + transition: height 0.3s !important; +} + +.ui.card.AssetCard > .content > .header:not(.ui), +.ui.cards > .card.AssetCard > .content > .header:not(.ui) { + flex: unset; +} + +.ui.card.AssetCard:hover > .content.catalog, +.ui.cards > .card.AssetCard:hover > .content.catalog { + position: absolute; + margin-top: 161px; + width: 100%; + height: 222px; + background-color: var(--card); + box-shadow: 0px 4px 34px 0px rgba(255, 255, 255, 0.37) !important; + border-radius: 0px 0px 10px 10px !important; +} + +.ui.card.AssetCard:hover .AssetImage.catalog, +.ui.cards > .card.AssetCard:hover .AssetImage.catalog { + box-shadow: 0px 4px 34px 0px rgba(255, 255, 255, 0.37) !important; + border-radius: 10px !important; +} + +.ui.card.AssetCard .AssetImage, +.ui.card.AssetCard .AssetImage .rarity-background { + border-radius: 10px 10px 0px 0px !important; } .AssetCard .header, @@ -9,22 +52,54 @@ letter-spacing: -0.2px; } +.AssetCard .extraInformation { + visibility: hidden; + height: 0px; +} + +.AssetCard:hover .extraInformation { + visibility: visible; +} + +.AssetCard .ui.header.small { + font-size: 14.5px; + font-weight: 400; +} + +.AssetCard .CatalogItemInformation { + color: white; + display: flex; + flex-direction: column; + font-size: 14px; + transition: inherit; + flex: 1; +} + +.AssetCard:hover .CatalogItemInformation { + margin-top: 13px; +} + .AssetCard .header .title { flex: 1 2 auto; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; margin-right: 20px; + margin-bottom: 0px; + display: flex; + flex-direction: column; } -.ui.card.AssetCard > .content { - flex: none; +.AssetCard .creator { + font-size: 14px; + font-weight: 400; + color: var(--secondary-text); } .ui.card.AssetCard > .content > .header:not(.ui), .ui.cards > .card.AssetCard > .content > .header:not(.ui) { display: flex; - margin-bottom: 6px; + margin-bottom: 0px; } .AssetCard .dcl.mana.inline { @@ -46,10 +121,6 @@ a.ui.card.link:hover .meta { margin-top: 12px; } -.AssetCard .AssetImage { - overflow: hidden; -} - .AssetCard .AssetImage .ens-subdomain { background-size: 557px; background-position-y: -133px; @@ -79,7 +150,7 @@ a.ui.card.link:hover .meta { align-self: flex-end; background-color: #ecebed; border: 1px solid #a09ba8; - color: #43404A; + color: #43404a; font-size: 11px; border-radius: 50px !important; display: flex; @@ -92,6 +163,30 @@ a.ui.card.link:hover .meta { z-index: 2; } +.AssetCard .AssetImage { + height: 223px; +} + +.AssetCard .AssetImage.catalog { + height: 161px; +} + +.AssetCard .PriceInMana .ui.header.large { + font-size: 30px; + font-weight: 600; + margin: 0px; +} + +.AssetCard .mintIcon { + height: 14px; +} + +.AssetCard .dcl.atlas { + overflow: hidden; + border-top-right-radius: 10px; + border-top-left-radius: 10px; +} + @media (max-width: 1199px) { .AssetCard .LandBubble { align-self: center; diff --git a/webapp/src/components/AssetCard/AssetCard.tsx b/webapp/src/components/AssetCard/AssetCard.tsx index 1c0332bd7d..8613d82913 100644 --- a/webapp/src/components/AssetCard/AssetCard.tsx +++ b/webapp/src/components/AssetCard/AssetCard.tsx @@ -4,7 +4,12 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Link } from 'react-router-dom' import { Card, Icon } from 'decentraland-ui' import { formatWeiMANA } from '../../lib/mana' -import { getAssetName, getAssetUrl, isNFT } from '../../modules/asset/utils' +import { + getAssetName, + getAssetUrl, + isNFT, + isCatalogItem +} from '../../modules/asset/utils' import { Asset } from '../../modules/asset/types' import { NFT } from '../../modules/nft/types' import { isLand } from '../../modules/nft/utils' @@ -15,7 +20,10 @@ import { isRentalListingExecuted, isRentalListingOpen } from '../../modules/rental/utils' +import { SortBy } from '../../modules/routing/types' +import mintingIcon from '../../images/minting.png' import { Mana } from '../Mana' +import { LinkedProfile } from '../LinkedProfile' import { AssetImage } from '../AssetImage' import { FavoritesCounter } from '../FavoritesCounter' import { ParcelTags } from './ParcelTags' @@ -23,9 +31,13 @@ import { EstateTags } from './EstateTags' import { WearableTags } from './WearableTags' import { EmoteTags } from './EmoteTags' import { ENSTags } from './ENSTags' +import { fomrmatWeiToAssetCard } from './utils' import { Props } from './AssetCard.types' import './AssetCard.css' +const MINT = 'MINT' +const LISTING = 'LISTING' + const RentalPrice = ({ asset, rentalPricePerDay @@ -35,8 +47,8 @@ const RentalPrice = ({ }) => { return ( <> - - {formatWeiMANA(rentalPricePerDay)} + + {fomrmatWeiToAssetCard(rentalPricePerDay)} /{t('global.day')} @@ -90,7 +102,8 @@ const AssetCard = (props: Props) => { showRentalChip: showRentalBubble, onClick, isClaimingBackLandTransactionPending, - rental + rental, + sortBy } = props const title = getAssetName(asset) @@ -100,9 +113,118 @@ const AssetCard = (props: Props) => { [rental] ) + const catalogItemInformation = () => { + let information: { + action: string + actionIcon: string | null + price: string | null + extraInformation: React.ReactNode | null + } | null = null + if (isCatalogItem(asset)) { + if (asset.id === '0x801e3ba69794b5ba6b6c0b6e8a771f99ae5f4c4a-0') { + console.log( + 'asset.isOnSale && asset.available > 0', + asset.isOnSale && asset.available > 0 + ) + } + + const isAvailableForMint = asset.isOnSale && asset.available > 0 + + if (!isAvailableForMint && !asset.minListingPrice) { + information = { + action: t('asset_card.not_for_sale'), + actionIcon: null, + price: null, + extraInformation: `${t('asset_card.owners', { + count: asset.owners + })}` + } + } else { + const mostExpensive = + asset.maxListingPrice && asset.price < asset.maxListingPrice + ? LISTING + : isAvailableForMint + ? MINT + : null + const cheapest = + asset.minListingPrice && asset.price > asset.minListingPrice + ? LISTING + : isAvailableForMint + ? MINT + : null + + const displayExtraInfomationToMint = + (sortBy === SortBy.MOST_EXPENSIVE && mostExpensive === LISTING) || + (sortBy === SortBy.CHEAPEST && cheapest === LISTING) + + information = { + action: + sortBy === SortBy.CHEAPEST + ? t('asset_card.cheapest_option') + : sortBy === SortBy.MOST_EXPENSIVE + ? t('asset_card.most_expensive') + : isAvailableForMint + ? t('asset_card.available_for_mint') + : t('asset_card.cheapest_listing'), + actionIcon: isAvailableForMint ? mintingIcon : null, + price: + sortBy === SortBy.MOST_EXPENSIVE + ? mostExpensive === MINT + ? asset.price + : asset.maxListingPrice + : asset.minPrice, + extraInformation: + asset.maxListingPrice && asset.minListingPrice && asset.listings ? ( + + {displayExtraInfomationToMint + ? t('asset_card.also_minting') + : t('asset_card.listings', { count: asset.listings })} + :  + + {fomrmatWeiToAssetCard( + displayExtraInfomationToMint + ? asset.price + : asset.minListingPrice + )} + +   + {asset.listings > 1 && + !displayExtraInfomationToMint && + asset.minListingPrice !== asset.maxListingPrice && + `- ${fomrmatWeiToAssetCard(asset.maxListingPrice)}`} + + ) : null + } + } + } + return information ? ( +
+ + {information.action}   + {information.actionIcon && ( + mint + )} + + + {information.price && ( +
+ + {fomrmatWeiToAssetCard(information.price)} + +
+ )} + {information.extraInformation && ( + + {information.extraInformation} + + )} +
+ ) : null + } + return ( { }`} > { rental={rental} /> ) : null} - + -
{title}
- {price ? ( - - {formatWeiMANA(price)} +
+ {title} + {isCatalogItem(asset) && ( + + )} +
+ {!catalogItemInformation && price ? ( + + {fomrmatWeiToAssetCard(price)} ) : rentalPricePerDay ? ( ) : null}
- - {t(`networks.${asset.network.toLowerCase()}`)} - + {!isCatalogItem(asset) && ( + + {t(`networks.${asset.network.toLowerCase()}`)} + + )} + {rentalPricePerDay && price ? (
{
) : null}
+ {catalogItemInformation()} {parcel ? : null} {estate ? : null} diff --git a/webapp/src/components/AssetCard/AssetCard.types.ts b/webapp/src/components/AssetCard/AssetCard.types.ts index 66c16ee0fb..649b7c1a5c 100644 --- a/webapp/src/components/AssetCard/AssetCard.types.ts +++ b/webapp/src/components/AssetCard/AssetCard.types.ts @@ -11,6 +11,7 @@ export type Props = { isClaimingBackLandTransactionPending: boolean showRentalChip: boolean rental: RentalListing | null + sortBy: string | undefined } export type MapStateProps = Pick< @@ -20,6 +21,7 @@ export type MapStateProps = Pick< | 'showRentalChip' | 'rental' | 'isClaimingBackLandTransactionPending' + | 'sortBy' > export type MapDispatchProps = {} export type OwnProps = Pick diff --git a/webapp/src/components/AssetCard/WearableTags/WearableTags.types.ts b/webapp/src/components/AssetCard/WearableTags/WearableTags.types.ts index 3c5a21c8a5..57392a891b 100644 --- a/webapp/src/components/AssetCard/WearableTags/WearableTags.types.ts +++ b/webapp/src/components/AssetCard/WearableTags/WearableTags.types.ts @@ -1,6 +1,5 @@ -import { Item } from '@dcl/schemas' -import { NFT } from '../../../modules/nft/types' +import { Asset } from '../../../modules/asset/types' export type Props = { - asset: NFT | Item + asset: Asset } diff --git a/webapp/src/components/AssetCard/utils.tsx b/webapp/src/components/AssetCard/utils.tsx new file mode 100644 index 0000000000..8291404be6 --- /dev/null +++ b/webapp/src/components/AssetCard/utils.tsx @@ -0,0 +1,34 @@ +import { ethers } from 'ethers' +import { MAXIMUM_FRACTION_DIGITS } from 'decentraland-dapps/dist/lib/mana' +import { getMinimumValueForFractionDigits } from '../../lib/mana' + +const ONE_MILLION = 1000000 +const ONE_BILLION = 1000000000 +const ONE_TRILLION = 1000000000000 + +export function fomrmatWeiToAssetCard(wei: string): string { + const maximumFractionDigits = MAXIMUM_FRACTION_DIGITS + const value = Number(ethers.utils.formatEther(wei)) + + if (value === 0) { + return '0' + } + + const fixedValue = value.toLocaleString(undefined, { + maximumFractionDigits + }) + + if (fixedValue === '0') { + return getMinimumValueForFractionDigits(maximumFractionDigits).toString() + } + + if (value > ONE_TRILLION) { + return `${(+value / ONE_TRILLION).toLocaleString()}T` + } else if (value > ONE_BILLION) { + return `${(+value / ONE_BILLION).toLocaleString()}B` + } else if (value > ONE_MILLION) { + return `${(+value / ONE_MILLION).toLocaleString()}M` + } + + return fixedValue +} diff --git a/webapp/src/components/AssetFilters/AssetFilters.container.ts b/webapp/src/components/AssetFilters/AssetFilters.container.ts index 9ca5405749..4ab22edf5c 100644 --- a/webapp/src/components/AssetFilters/AssetFilters.container.ts +++ b/webapp/src/components/AssetFilters/AssetFilters.container.ts @@ -21,6 +21,7 @@ import { getRentalDays, getRarities, getSection, + getStatus, getWearableGenders } from '../../modules/routing/selectors' import { @@ -31,6 +32,7 @@ import { getIsRentalPeriodFilterEnabled } from '../../modules/features/selectors' import { LANDFilters } from '../Vendor/decentraland/types' +import { AssetStatusFilter } from '../../utils/filters' import { browse } from '../../modules/routing/actions' import { Section } from '../../modules/vendor/routing/types' import { getView } from '../../modules/ui/browse/selectors' @@ -69,6 +71,10 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { ? values.maxEstateSize || '' : getMaxEstateSize(state), rarities: 'rarities' in values ? values.rarities || [] : getRarities(state), + status: + 'status' in values + ? values.status + : (getStatus(state) as AssetStatusFilter), network: 'network' in values ? values.network : getNetwork(state), bodyShapes: 'wearableGenders' in values @@ -85,10 +91,20 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { landStatus, view: getView(state), section, - rentalDays: 'rentalDays' in values ? values.rentalDays : getRentalDays(state), - minDistanceToPlaza: 'minDistanceToPlaza' in values ? values.minDistanceToPlaza : getMinDistanceToPlaza(state), - maxDistanceToPlaza: 'maxDistanceToPlaza' in values ? values.maxDistanceToPlaza : getMaxDistanceToPlaza(state), - adjacentToRoad: 'adjacentToRoad' in values ? values.adjacentToRoad : getAdjacentToRoad(state), + rentalDays: + 'rentalDays' in values ? values.rentalDays : getRentalDays(state), + minDistanceToPlaza: + 'minDistanceToPlaza' in values + ? values.minDistanceToPlaza + : getMinDistanceToPlaza(state), + maxDistanceToPlaza: + 'maxDistanceToPlaza' in values + ? values.maxDistanceToPlaza + : getMaxDistanceToPlaza(state), + adjacentToRoad: + 'adjacentToRoad' in values + ? values.adjacentToRoad + : getAdjacentToRoad(state), isCreatorFiltersEnabled: getIsCreatorsFilterEnabled(state), isPriceFilterEnabled: getIsPriceFilterEnabled(state), isEstateSizeFilterEnabled: getIsEstateSizeFilterEnabled(state), diff --git a/webapp/src/components/AssetFilters/AssetFilters.tsx b/webapp/src/components/AssetFilters/AssetFilters.tsx index f065fe48cc..6a28c4e748 100644 --- a/webapp/src/components/AssetFilters/AssetFilters.tsx +++ b/webapp/src/components/AssetFilters/AssetFilters.tsx @@ -7,10 +7,9 @@ import { WearableGender } from '@dcl/schemas' import { getSectionFromCategory } from '../../modules/routing/search' -import { AssetType } from '../../modules/asset/types' import { isLandSection } from '../../modules/ui/utils' import { View } from '../../modules/ui/types' -import { Sections, SortBy } from '../../modules/routing/types' +import { Sections, SortBy, BrowseOptions } from '../../modules/routing/types' import { LANDFilters } from '../Vendor/decentraland/types' import { Menu } from '../Menu' import PriceFilter from './PriceFilter' @@ -31,6 +30,7 @@ import { filtersBySection, trackBarChartComponentChange } from './utils' +import { StatusFilter } from './StatusFilter' import './AssetFilters.css' export const AssetFilters = ({ @@ -41,13 +41,13 @@ export const AssetFilters = ({ collection, creators, rarities, + status, network, category, bodyShapes, isOnlySmart, isOnSale, emotePlayMode, - assetType, section, landStatus, defaultCollapsed, @@ -64,9 +64,21 @@ export const AssetFilters = ({ isCreatorFiltersEnabled, isRentalPeriodFilterEnabled }: Props): JSX.Element | null => { - const isPrimarySell = assetType === AssetType.ITEM const isInLandSection = isLandSection(section) + const handleBrowseParamChange = useCallback( + (options: BrowseOptions) => onBrowse(options), + [onBrowse] + ) + + const handlePriceChange = useCallback( + (value: [string, string]) => { + const [minPrice, maxPrice] = value + onBrowse({ minPrice, maxPrice }) + }, + [onBrowse] + ) + const handleRangeFilterChange = useCallback( ( filterNames: [string, string], @@ -263,6 +275,13 @@ export const AssetFilters = ({ defaultCollapsed={!!defaultCollapsed?.[AssetFilter.Network]} /> ) : null} + {shouldRenderFilter(AssetFilter.Status) ? ( + + ) : null} {isPriceFilterEnabled && shouldRenderFilter(AssetFilter.Price) && isOnSale && @@ -305,7 +324,7 @@ export const AssetFilters = ({ defaultCollapsed={!!defaultCollapsed?.[AssetFilter.PlayMode]} /> )} - {shouldRenderFilter(AssetFilter.Network) && !isPrimarySell && ( + {shouldRenderFilter(AssetFilter.Network) && ( void + defaultCollapsed?: boolean +} + +export const StatusFilter = ({ + status, + onChange, + defaultCollapsed = false +}: StatusFilterFilterProps) => { + const isMobileOrTablet = useTabletAndBelowMediaQuery() + const statusOptions = useMemo( + () => + Object.keys(AssetStatusFilter).map(opt => ({ + value: opt.toLocaleLowerCase(), + text: t(`nft_filters.status.${opt.toLocaleLowerCase()}`) + })), + [] + ) + + const handleChange = useCallback( + (_evt, { value }) => onChange({ status: value }), + [onChange] + ) + + const header = useMemo( + () => + isMobileOrTablet ? ( +
+ + {t('nft_filters.status.title')} + + + {status + ? t(`nft_filters.status.${status}`) + : t('nft_filters.status.on_sale')} + +
+ ) : ( + t('nft_filters.status.title') + ), + [isMobileOrTablet, status] + ) + + return ( + +
+ {statusOptions.map(option => ( + + {option.text} + {option.value !== AssetStatusFilter.NOT_FOR_SALE ? ( + + ) : null} + + } + value={option.value} + name="status" + checked={option.value === status} + /> + ))} +
+
+ ) +} diff --git a/webapp/src/components/AssetFilters/StatusFilter/index.ts b/webapp/src/components/AssetFilters/StatusFilter/index.ts new file mode 100644 index 0000000000..b9e8657de0 --- /dev/null +++ b/webapp/src/components/AssetFilters/StatusFilter/index.ts @@ -0,0 +1 @@ +export * from './StatusFilter' diff --git a/webapp/src/components/AssetFilters/utils.ts b/webapp/src/components/AssetFilters/utils.ts index b6ce96beef..f1cd5b862f 100644 --- a/webapp/src/components/AssetFilters/utils.ts +++ b/webapp/src/components/AssetFilters/utils.ts @@ -5,6 +5,7 @@ import * as events from '../../utils/events' export const enum AssetFilter { Rarity, + Status, Price, Collection, Creators, @@ -17,6 +18,7 @@ export const enum AssetFilter { const WearablesFilters = [ AssetFilter.Rarity, + AssetFilter.Status, AssetFilter.Price, AssetFilter.Network, AssetFilter.BodyShape, diff --git a/webapp/src/components/AssetImage/AssetImage.types.ts b/webapp/src/components/AssetImage/AssetImage.types.ts index 8dd24f21f9..1473b5ffa2 100644 --- a/webapp/src/components/AssetImage/AssetImage.types.ts +++ b/webapp/src/components/AssetImage/AssetImage.types.ts @@ -1,17 +1,16 @@ import React from 'react' import { Dispatch } from 'redux' import { Avatar, IPreviewController } from '@dcl/schemas' -import { Item } from '@dcl/schemas' -import { NFT } from '../../modules/nft/types' import { setIsTryingOn, SetIsTryingOnAction, setWearablePreviewController, SetWearablePreviewControllerAction } from '../../modules/ui/preview/actions' +import { Asset } from '../../modules/asset/types' export type Props = { - asset: NFT | Item + asset: Asset className?: string isDraggable?: boolean withNavigation?: boolean diff --git a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.module.css b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.module.css index 337dfafc61..701a3ab06d 100644 --- a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.module.css +++ b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.module.css @@ -170,6 +170,14 @@ color: black !important; } +.BestBuyingOption .primaryButton { + background-color: var(--primary) !important; +} + +.BestBuyingOption .outlinedButton { + border: 1px solid var(--secondary-text) !important; +} + .BestBuyingOption .expiresAt { display: flex; align-items: center; diff --git a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx index d169a9432d..60e591f4f5 100644 --- a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx +++ b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx @@ -4,6 +4,7 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Bid, BidSortBy, + Item, ListingStatus, Network, Order, @@ -24,6 +25,7 @@ import noListings from '../../../images/noListings.png' import { ManaToFiat } from '../../ManaToFiat' import { LinkedProfile } from '../../LinkedProfile' import { BuyNFTButtons } from '../SaleActionBox/BuyNFTButtons' +import { ItemSaleActions } from '../SaleActionBox/ItemSaleActions' import { BuyOptions, Props } from './BestBuyingOption.types' import styles from './BestBuyingOption.module.css' @@ -62,7 +64,7 @@ const BestBuyingOption = ({ asset, tableRef }: Props) => { } const sortBy = OrderSortBy.CHEAPEST - if (asset.network === Network.MATIC && asset.itemId) { + if (asset.network === Network.MATIC) { params.itemId = asset.itemId } else if (asset.network === Network.ETHEREUM) { params.nftName = asset.name @@ -99,6 +101,13 @@ const BestBuyingOption = ({ asset, tableRef }: Props) => { } }, [asset]) + const customClasses = { + primaryButton: styles.primaryButton, + secondaryButton: styles.buyWithCardClassName, + outlinedButton: styles.outlinedButton, + buyWithCardClassName: styles.buyWithCardClassName + } + return (
{isLoading ? ( @@ -156,11 +165,13 @@ const BestBuyingOption = ({ asset, tableRef }: Props) => { {formatWeiMANA(asset.price)}
-
- {'('} - - {')'} -
+ {+asset.price > 0 && ( +
+ {'('} + + {')'} +
+ )}
@@ -173,9 +184,9 @@ const BestBuyingOption = ({ asset, tableRef }: Props) => {
- ) : buyOption === BuyOptions.BUY_LISTING && asset && listing ? ( diff --git a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.container.ts b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.container.ts index 3bb87fb3e4..48f1e51349 100644 --- a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.container.ts +++ b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.container.ts @@ -2,10 +2,14 @@ import { connect } from 'react-redux' import { RootState } from '../../../../modules/reducer' import { getWallet } from '../../../../modules/wallet/selectors' import { MapStateProps } from './ItemSaleActions.types' -import SaleRentActionBox from './ItemSaleActions' +import ItemSaleActions from './ItemSaleActions' -const mapState = (state: RootState): MapStateProps => ({ - wallet: getWallet(state) -}) +const mapState = (state: RootState): MapStateProps => { + const wallet = getWallet(state) -export default connect(mapState)(SaleRentActionBox) + return { + wallet + } +} + +export default connect(mapState)(ItemSaleActions) diff --git a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.tsx b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.tsx index 8c50255ea9..11d1c11265 100644 --- a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.tsx +++ b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.tsx @@ -1,13 +1,13 @@ import { memo } from 'react' import { Button } from 'decentraland-ui' import { t } from 'decentraland-dapps/dist/modules/translation/utils' - import { getBuilderCollectionDetailUrl } from '../../../../modules/collection/utils' +import { BuyNFTButtons } from '../BuyNFTButtons' + import styles from './ItemSaleActions.module.css' import { Props } from './ItemSaleActions.types' -import { BuyNFTButtons } from '../BuyNFTButtons' -const NFTSaleActions = ({ item, wallet }: Props) => { +const ItemSaleActions = ({ item, wallet, customClassnames }: Props) => { const isOwner = wallet?.address === item.creator const canBuy = !isOwner && item.isOnSale && item.available > 0 const builderCollectionUrl = getBuilderCollectionDetailUrl( @@ -18,21 +18,41 @@ const NFTSaleActions = ({ item, wallet }: Props) => { <> {isOwner ? (
- - -
) : ( - canBuy && + canBuy && ( + + ) )} ) } -export default memo(NFTSaleActions) +export default memo(ItemSaleActions) diff --git a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.types.ts b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.types.ts index b36ba01c88..01b0abdd7c 100644 --- a/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.types.ts +++ b/webapp/src/components/AssetPage/SaleActionBox/ItemSaleActions/ItemSaleActions.types.ts @@ -3,8 +3,9 @@ import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' export type Props = { item: Item - wallet: Wallet | null + wallet?: Wallet | null + customClassnames?: { [key: string]: string } | undefined } -export type OwnProps = Pick +export type OwnProps = Pick export type MapStateProps = Pick diff --git a/webapp/src/components/AssetPage/SaleActionBox/SaleActionBox.tsx b/webapp/src/components/AssetPage/SaleActionBox/SaleActionBox.tsx index a49160ffaf..7c003822b4 100644 --- a/webapp/src/components/AssetPage/SaleActionBox/SaleActionBox.tsx +++ b/webapp/src/components/AssetPage/SaleActionBox/SaleActionBox.tsx @@ -3,7 +3,7 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils' import Price from '../../Price' import styles from './SaleActionBox.module.css' import { Props } from './SaleActionBox.types' -import { isNFT } from '../../../modules/asset/utils' +import { isCatalogItem, isNFT } from '../../../modules/asset/utils' import { ItemSaleActions } from './ItemSaleActions' import { NFTSaleActions } from './NFTSaleActions' @@ -14,9 +14,9 @@ const SaleActionBox = ({ asset }: Props) => {
{isNFT(asset) ? ( - ) : ( + ) : !isCatalogItem(asset) ? ( - )} + ) : null}
) : null diff --git a/webapp/src/components/AssetTopbar/AssetTopbar.container.ts b/webapp/src/components/AssetTopbar/AssetTopbar.container.ts index b166f3d2bf..23ba998581 100644 --- a/webapp/src/components/AssetTopbar/AssetTopbar.container.ts +++ b/webapp/src/components/AssetTopbar/AssetTopbar.container.ts @@ -12,6 +12,7 @@ import { getSearch, getSection, getSortBy, + getSortByOptions, hasFiltersEnabled } from '../../modules/routing/selectors' import { BrowseOptions } from '../../modules/routing/types' @@ -37,6 +38,7 @@ const mapState = (state: RootState): MapStateProps => { onlyOnRent: getOnlyOnRent(state), onlyOnSale: getOnlyOnSale(state), sortBy: getSortBy(state), + sortByOptions: getSortByOptions(state), assetType: getAssetType(state), section: getSection(state), hasFiltersEnabled: hasFiltersEnabled(state), diff --git a/webapp/src/components/AssetTopbar/AssetTopbar.tsx b/webapp/src/components/AssetTopbar/AssetTopbar.tsx index 70ae76729b..c3481220c5 100644 --- a/webapp/src/components/AssetTopbar/AssetTopbar.tsx +++ b/webapp/src/components/AssetTopbar/AssetTopbar.tsx @@ -7,9 +7,7 @@ import { Icon, useTabletAndBelowMediaQuery } from 'decentraland-ui' -import { NFTCategory } from '@dcl/schemas' import { t } from 'decentraland-dapps/dist/modules/translation/utils' -import { AssetType } from '../../modules/asset/types' import { useInput } from '../../lib/input' import { getCountText, getOrderByOptions } from './utils' import { SortBy } from '../../modules/routing/types' @@ -24,7 +22,6 @@ import { persistIsMapProperty } from '../../modules/ui/utils' import { Chip } from '../Chip' -import { AssetTypeFilter } from './AssetTypeFilter' import { Props } from './AssetTopbar.types' import { SelectedFilters } from './SelectedFilters' import styles from './AssetTopbar.module.css' @@ -32,7 +29,6 @@ import styles from './AssetTopbar.module.css' export const AssetTopbar = ({ search, view, - assetType, count, isLoading, isMap, @@ -43,7 +39,8 @@ export const AssetTopbar = ({ hasFiltersEnabled, onBrowse, onClearFilters, - onOpenFiltersModal + onOpenFiltersModal, + sortByOptions }: Props): JSX.Element => { const isMobile = useTabletAndBelowMediaQuery() const category = section ? getCategoryFromSection(section) : undefined @@ -62,13 +59,6 @@ export const AssetTopbar = ({ const [searchValue, setSearchValue] = useInput(search, handleSearch, 500) - const handleAssetTypeChange = useCallback( - (value: AssetType) => { - onBrowse({ assetType: value }) - }, - [onBrowse] - ) - const handleOrderByDropdownChange = useCallback( (_, props: DropdownProps) => { const sortBy: SortBy = props.value as SortBy @@ -117,6 +107,10 @@ export const AssetTopbar = ({ } }, [onBrowse, sortBy, orderByDropdownOptions]) + const sortByValue = sortByOptions.find(option => option.value === sortBy) + ? sortBy + : sortByOptions[0].value + return (
)}
- {view && - !isLandSection(section) && - !isAccountView(view) && - !isListsSection(section) && - (category === NFTCategory.WEARABLE || - category === NFTCategory.EMOTE) && ( - - )} {!isMap && (
{!isLoading ? ( @@ -185,8 +167,8 @@ export const AssetTopbar = ({
{isMobile ? ( diff --git a/webapp/src/components/AssetTopbar/AssetTopbar.types.ts b/webapp/src/components/AssetTopbar/AssetTopbar.types.ts index 0b5ecc5bb9..4e905fd01b 100644 --- a/webapp/src/components/AssetTopbar/AssetTopbar.types.ts +++ b/webapp/src/components/AssetTopbar/AssetTopbar.types.ts @@ -1,6 +1,6 @@ import { openModal } from 'decentraland-dapps/dist/modules/modal/actions' import { AssetType } from '../../modules/asset/types' -import { BrowseOptions } from '../../modules/routing/types' +import { BrowseOptions, SortByOption } from '../../modules/routing/types' import { Section } from '../../modules/vendor/routing/types' import { View } from '../../modules/ui/types' import { clearFilters } from '../../modules/routing/actions' @@ -11,6 +11,7 @@ export type Props = { isMap: boolean view: View | undefined sortBy: string | undefined + sortByOptions: SortByOption[] assetType: AssetType onlyOnSale: boolean | undefined onlyOnRent: boolean | undefined @@ -32,6 +33,7 @@ export type MapStateProps = Pick< | 'onlyOnRent' | 'onlyOnSale' | 'sortBy' + | 'sortByOptions' | 'section' | 'hasFiltersEnabled' | 'isLoading' diff --git a/webapp/src/components/FavoritesCounter/FavoritesCounter.types.ts b/webapp/src/components/FavoritesCounter/FavoritesCounter.types.ts index 67317bbcfa..781222ca1c 100644 --- a/webapp/src/components/FavoritesCounter/FavoritesCounter.types.ts +++ b/webapp/src/components/FavoritesCounter/FavoritesCounter.types.ts @@ -1,5 +1,5 @@ import { Dispatch } from 'redux' -import { Item } from '@dcl/schemas' +import { CatalogItem, Item } from '@dcl/schemas' import { openModal, OpenModalAction @@ -13,7 +13,7 @@ import { export type Props = { className?: string - item: Item + item: Item | CatalogItem isCollapsed?: boolean isPickedByUser: boolean count: number diff --git a/webapp/src/components/HomePage/Slideshow/Slideshow.css b/webapp/src/components/HomePage/Slideshow/Slideshow.css index b9321a0f7a..4ab30f6265 100644 --- a/webapp/src/components/HomePage/Slideshow/Slideshow.css +++ b/webapp/src/components/HomePage/Slideshow/Slideshow.css @@ -39,7 +39,7 @@ flex: 0 0 auto; width: 257px; margin-right: 12px; - height: 300px; + height: 346px; margin-top: 12px; } diff --git a/webapp/src/components/Navigation/Navigation.tsx b/webapp/src/components/Navigation/Navigation.tsx index 208a574acd..69583bf0e6 100644 --- a/webapp/src/components/Navigation/Navigation.tsx +++ b/webapp/src/components/Navigation/Navigation.tsx @@ -4,6 +4,7 @@ import classNames from 'classnames' import { Tabs, Mobile, Button, useMobileMediaQuery } from 'decentraland-ui' import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils' import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { AssetStatusFilter } from '../../utils/filters' import * as decentraland from '../../modules/vendor/decentraland' import { locations } from '../../modules/routing/locations' import { VendorName } from '../../modules/vendor' @@ -65,8 +66,8 @@ const Navigation = (props: Props) => { section: decentraland.Section.WEARABLES, vendor: VendorName.DECENTRALAND, page: 1, - sortBy: SortBy.RECENTLY_LISTED, - onlyOnSale: true + sortBy: SortBy.NEWEST, + status: AssetStatusFilter.ON_SALE, })} > diff --git a/webapp/src/modules/asset/types.ts b/webapp/src/modules/asset/types.ts index 3dedb7c6ba..33a9c0f233 100644 --- a/webapp/src/modules/asset/types.ts +++ b/webapp/src/modules/asset/types.ts @@ -1,13 +1,16 @@ -import { Item } from '@dcl/schemas' +import { Item, CatalogItem } from '@dcl/schemas' import { NFT } from '../nft/types' export enum AssetType { ITEM = 'item', - NFT = 'nft' + NFT = 'nft', + CATALOG_ITEM = 'catalog_item' } export type Asset = T extends AssetType.NFT ? NFT : T extends AssetType.ITEM ? Item + : T extends AssetType.CATALOG_ITEM + ? CatalogItem : NFT | Item diff --git a/webapp/src/modules/asset/utils.ts b/webapp/src/modules/asset/utils.ts index 4992a743b9..08fa4ecee3 100644 --- a/webapp/src/modules/asset/utils.ts +++ b/webapp/src/modules/asset/utils.ts @@ -1,5 +1,5 @@ import { call, put, race, take } from 'redux-saga/effects' -import { NFTCategory, Order, RentalListing } from '@dcl/schemas' +import { NFTCategory, Order, RentalListing, CatalogItem } from '@dcl/schemas' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' import { SET_PURCHASE } from 'decentraland-dapps/dist/modules/gateway/actions' @@ -109,6 +109,10 @@ export function isNFT(asset: Asset): asset is NFT { return 'tokenId' in asset } +export function isCatalogItem(asset: Asset): asset is CatalogItem { + return 'minPrice' in asset +} + export function isWearableOrEmote(asset: Asset): boolean { const categories: Array = [ NFTCategory.WEARABLE, diff --git a/webapp/src/modules/catalog/actions.spec.ts b/webapp/src/modules/catalog/actions.spec.ts new file mode 100644 index 0000000000..f3a961563d --- /dev/null +++ b/webapp/src/modules/catalog/actions.spec.ts @@ -0,0 +1,50 @@ +import { CatalogFilters, CatalogItem } from '@dcl/schemas' +import { + fetchCatalogFailure, + fetchCatalogRequest, + fetchCatalogSuccess, + FETCH_CATALOG_FAILURE, + FETCH_CATALOG_REQUEST, + FETCH_CATALOG_SUCCESS +} from './actions' + +const catalogFilters: CatalogFilters = {} + +const anErrorMessage = 'An error' + +describe('when creating the action to signal the start of the catalog request', () => { + it('should return an object representing the action', () => { + expect(fetchCatalogRequest({ filters: catalogFilters })).toEqual({ + type: FETCH_CATALOG_REQUEST, + meta: undefined, + payload: { filters: catalogFilters } + }) + }) +}) + +describe('when creating the action to signal a success in the items request', () => { + const catalogItems = [{} as CatalogItem] + const total = 1 + + it('should return an object representing the action', () => { + expect( + fetchCatalogSuccess(catalogItems, total, { filters: catalogFilters }) + ).toEqual({ + type: FETCH_CATALOG_SUCCESS, + meta: undefined, + payload: { catalogItems, total, options: { filters: catalogFilters } } + }) + }) +}) + +describe('when creating the action to signal a failure items request', () => { + it('should return an object representing the action', () => { + expect( + fetchCatalogFailure(anErrorMessage, { filters: catalogFilters }) + ).toEqual({ + type: FETCH_CATALOG_FAILURE, + meta: undefined, + payload: { error: anErrorMessage, options: { filters: catalogFilters } } + }) + }) +}) diff --git a/webapp/src/modules/catalog/actions.ts b/webapp/src/modules/catalog/actions.ts new file mode 100644 index 0000000000..da50bebd74 --- /dev/null +++ b/webapp/src/modules/catalog/actions.ts @@ -0,0 +1,27 @@ +import { action } from 'typesafe-actions' +import { CatalogItem } from '@dcl/schemas' +import { CatalogBrowseOptions } from '../ui/browse/types' + +// Fetch Catalog + +export const FETCH_CATALOG_REQUEST = '[Request] Fetch Catalog' +export const FETCH_CATALOG_SUCCESS = '[Success] Fetch Catalog' +export const FETCH_CATALOG_FAILURE = '[Failure] Fetch Catalog' + +export const fetchCatalogRequest = (options: CatalogBrowseOptions) => + action(FETCH_CATALOG_REQUEST, options) + +export const fetchCatalogSuccess = ( + catalogItems: CatalogItem[], + total: number, + options: CatalogBrowseOptions +) => action(FETCH_CATALOG_SUCCESS, { catalogItems, total, options }) + +export const fetchCatalogFailure = ( + error: string, + options: CatalogBrowseOptions +) => action(FETCH_CATALOG_FAILURE, { error, options }) + +export type FetchCatalogRequestAction = ReturnType +export type FetchCatalogSuccessAction = ReturnType +export type FetchCatalogFailureAction = ReturnType diff --git a/webapp/src/modules/catalog/reducer.spec.ts b/webapp/src/modules/catalog/reducer.spec.ts new file mode 100644 index 0000000000..833095495f --- /dev/null +++ b/webapp/src/modules/catalog/reducer.spec.ts @@ -0,0 +1,89 @@ +import { CatalogFilters, CatalogItem } from '@dcl/schemas' +import { loadingReducer } from 'decentraland-dapps/dist/modules/loading/reducer' +import { + fetchCatalogRequest, + fetchCatalogFailure, + fetchCatalogSuccess +} from './actions' +import { INITIAL_STATE, catalogReducer } from './reducer' + +const catalogFilters: CatalogFilters = {} + +const catalogItem = { + id: 'anId', + contractAddress: 'aContractAddress', + price: '5000000000000000000', + listings: 4, + minListingPrice: '1500000000000000000000', + maxListingPrice: '5000000000000000000000' +} as CatalogItem + +const anotherCatalogItem = { + id: 'anotherId', + contractAddress: 'aContractAddress', + price: '5000000000000000000', + listings: 4, + minListingPrice: '1500000000000000000000', + maxListingPrice: '5000000000000000000000' +} as CatalogItem + +const anErrorMessage = 'An error' + +describe(`when reducing the "${fetchCatalogRequest}" action`, () => { + it('should return a state with the loading set', () => { + const initialState = { + ...INITIAL_STATE, + loading: [] + } + + expect( + catalogReducer(initialState, fetchCatalogRequest(catalogFilters)) + ).toEqual({ + ...INITIAL_STATE, + loading: loadingReducer( + initialState.loading, + fetchCatalogRequest(catalogFilters) + ) + }) + }) +}) + +describe(`when reducing the "${fetchCatalogFailure}" action`, () => { + it('should return a state with the error set and the loading state cleared', () => { + const initialState = { + ...INITIAL_STATE, + error: null, + loading: loadingReducer([], fetchCatalogRequest(catalogFilters)) + } + + expect( + catalogReducer( + initialState, + fetchCatalogFailure(anErrorMessage, catalogFilters) + ) + ).toEqual({ + ...INITIAL_STATE, + error: anErrorMessage, + loading: [] + }) + }) +}) + +describe('when reducing the successful action of fetching catalog items', () => { + const requestAction = fetchCatalogRequest(catalogFilters) + const successAction = fetchCatalogSuccess([catalogItem], 1, catalogFilters) + + const initialState = { + ...INITIAL_STATE, + data: { anotherId: anotherCatalogItem }, + loading: loadingReducer([], requestAction) + } + + it('should return a state with the the loaded items and the loading state cleared', () => { + expect(catalogReducer(initialState, successAction)).toEqual({ + ...INITIAL_STATE, + loading: [], + data: { ...initialState.data, [catalogItem.id]: catalogItem } + }) + }) +}) diff --git a/webapp/src/modules/catalog/reducer.ts b/webapp/src/modules/catalog/reducer.ts new file mode 100644 index 0000000000..d39cfd02a7 --- /dev/null +++ b/webapp/src/modules/catalog/reducer.ts @@ -0,0 +1,69 @@ +import { CatalogItem } from '@dcl/schemas' +import { + loadingReducer, + LoadingState +} from 'decentraland-dapps/dist/modules/loading/reducer' +import { + FETCH_CATALOG_FAILURE, + FETCH_CATALOG_REQUEST, + FETCH_CATALOG_SUCCESS, + FetchCatalogFailureAction, + FetchCatalogRequestAction, + FetchCatalogSuccessAction +} from './actions' + +export type CatalogState = { + data: Record + loading: LoadingState + error: string | null +} + +export const INITIAL_STATE: CatalogState = { + data: {}, + loading: [], + error: null +} + +type CatalogReducerAction = + | FetchCatalogFailureAction + | FetchCatalogRequestAction + | FetchCatalogSuccessAction + +export function catalogReducer( + state = INITIAL_STATE, + action: CatalogReducerAction +): CatalogState { + switch (action.type) { + case FETCH_CATALOG_REQUEST: { + return { + ...state, + loading: loadingReducer(state.loading, action) + } + } + case FETCH_CATALOG_SUCCESS: { + const { catalogItems } = action.payload + return { + ...state, + loading: loadingReducer(state.loading, action), + data: { + ...state.data, + ...catalogItems.reduce((obj, catalogItem) => { + obj[catalogItem.id] = catalogItem + return obj + }, {} as Record) + }, + error: null + } + } + case FETCH_CATALOG_FAILURE: { + const { error } = action.payload + return { + ...state, + loading: loadingReducer(state.loading, action), + error + } + } + default: + return state + } +} diff --git a/webapp/src/modules/catalog/sagas.spec.ts b/webapp/src/modules/catalog/sagas.spec.ts new file mode 100644 index 0000000000..6a80e250ad --- /dev/null +++ b/webapp/src/modules/catalog/sagas.spec.ts @@ -0,0 +1,56 @@ +import { expectSaga } from 'redux-saga-test-plan' +import { call } from 'redux-saga/effects' +import { CatalogFilters, CatalogItem } from '@dcl/schemas' +import { + fetchCatalogRequest, + fetchCatalogSuccess, + fetchCatalogFailure +} from './actions' +import { catalogSaga } from './sagas' +import { catalogAPI } from '../vendor/decentraland/catalog/api' + +const catalogFilters: CatalogFilters = {} + +const catalogItem = { + id: 'anId', + contractAddress: 'aContractAddress', + price: '5000000000000000000', + listings: 4, + minListingPrice: '1500000000000000000000', + maxListingPrice: '5000000000000000000000' +} as CatalogItem + +const anError = 'An error occured' + +describe('when handling the fetch catalog items request action', () => { + describe('when the request fails', () => { + it('should dispatching a failing action with the error and the options', () => { + return expectSaga(catalogSaga) + .provide([ + [ + call([catalogAPI, 'fetch'], catalogFilters), + Promise.reject(new Error(anError)) + ] + ]) + .put(fetchCatalogFailure(anError, { filters: catalogFilters })) + .dispatch(fetchCatalogRequest({ filters: catalogFilters })) + .run() + }) + }) + + describe('when the request is successful', () => { + const fetchResult = { data: [catalogItem], total: 1 } + + it('should dispatch a successful action with the fetched catalog items', () => { + return expectSaga(catalogSaga) + .provide([[call([catalogAPI, 'fetch'], catalogFilters), fetchResult]]) + .put( + fetchCatalogSuccess(fetchResult.data, fetchResult.total, { + filters: catalogFilters + }) + ) + .dispatch(fetchCatalogRequest({ filters: catalogFilters })) + .run() + }) + }) +}) diff --git a/webapp/src/modules/catalog/sagas.ts b/webapp/src/modules/catalog/sagas.ts new file mode 100644 index 0000000000..8e7e93255a --- /dev/null +++ b/webapp/src/modules/catalog/sagas.ts @@ -0,0 +1,34 @@ +import { put, takeEvery } from '@redux-saga/core/effects' +import { call } from 'redux-saga/effects' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { isErrorWithMessage } from '../../lib/error' +import { catalogAPI } from '../vendor/decentraland/catalog/api' +import { + FETCH_CATALOG_REQUEST, + FetchCatalogRequestAction, + fetchCatalogFailure, + fetchCatalogSuccess +} from './actions' +import { CatalogItem } from '@dcl/schemas' + +export function* catalogSaga() { + yield takeEvery(FETCH_CATALOG_REQUEST, handleFetchCatalogRequest) +} + +function* handleFetchCatalogRequest(action: FetchCatalogRequestAction) { + const { filters } = action.payload + try { + const { data, total }: { data: CatalogItem[]; total: number } = yield call( + [catalogAPI, 'fetch'], + filters + ) + yield put(fetchCatalogSuccess(data, total, action.payload)) + } catch (error) { + yield put( + fetchCatalogFailure( + isErrorWithMessage(error) ? error.message : t('global.unknown_error'), + action.payload + ) + ) + } +} diff --git a/webapp/src/modules/catalog/selectors.spec.ts b/webapp/src/modules/catalog/selectors.spec.ts new file mode 100644 index 0000000000..d13c79664b --- /dev/null +++ b/webapp/src/modules/catalog/selectors.spec.ts @@ -0,0 +1,43 @@ +import { CatalogItem } from '@dcl/schemas' +import { RootState } from '../reducer' +import { INITIAL_STATE } from './reducer' +import { getData, getError, getLoading, getState } from './selectors' + +let state: RootState + +beforeEach(() => { + state = { + catalogItem: { + ...INITIAL_STATE, + data: { + anItemId: {} as CatalogItem + }, + error: 'anError', + loading: [] + } + } as any +}) + +describe("when getting the catalogItem's state", () => { + it('should return the state', () => { + expect(getState(state)).toEqual(state.catalogItem) + }) +}) + +describe('when getting the data of the state', () => { + it('should return the data', () => { + expect(getData(state)).toEqual(state.catalogItem.data) + }) +}) + +describe('when getting the error of the state', () => { + it('should return the error message', () => { + expect(getError(state)).toEqual(state.catalogItem.error) + }) +}) + +describe('when getting the loading state of the state', () => { + it('should return the loading state', () => { + expect(getLoading(state)).toEqual(state.catalogItem.loading) + }) +}) diff --git a/webapp/src/modules/catalog/selectors.ts b/webapp/src/modules/catalog/selectors.ts new file mode 100644 index 0000000000..ea222f5a93 --- /dev/null +++ b/webapp/src/modules/catalog/selectors.ts @@ -0,0 +1,14 @@ +import { CatalogItem } from '@dcl/schemas' +import { createSelector } from 'reselect' +import { RootState } from '../reducer' + +export const getState = (state: RootState) => state.catalogItem +export const getData = (state: RootState) => getState(state).data +export const getError = (state: RootState) => getState(state).error +export const getLoading = (state: RootState) => getState(state).loading + +export const getCatalogItems = createSelector< + RootState, + ReturnType, + CatalogItem[] +>(getData, itemsById => Object.values(itemsById)) diff --git a/webapp/src/modules/catalog/types.ts b/webapp/src/modules/catalog/types.ts new file mode 100644 index 0000000000..98336bd9fc --- /dev/null +++ b/webapp/src/modules/catalog/types.ts @@ -0,0 +1,15 @@ +import { + CatalogFilters, + CatalogSortBy, + CatalogSortDirection +} from '@dcl/schemas' + +export type CatalogQueryFilters = Omit< + CatalogFilters, + 'sortBy' | 'sortDirection' | 'limit' | 'offset' +> & { + sortBy?: CatalogSortBy + sortDirection?: CatalogSortDirection + limit?: number + offset?: number +} diff --git a/webapp/src/modules/nft/utils.ts b/webapp/src/modules/nft/utils.ts index 4ced9700a1..fd3a704ea4 100644 --- a/webapp/src/modules/nft/utils.ts +++ b/webapp/src/modules/nft/utils.ts @@ -1,4 +1,5 @@ -import { BodyShape, Item, NFTCategory } from '@dcl/schemas' +import { BodyShape, NFTCategory } from '@dcl/schemas' +import { Asset } from '../asset/types' import { NFT } from './types' export function getNFTId(contractAddress: string, tokenId: string) { @@ -35,13 +36,13 @@ export function isUnisex(bodyShapes: BodyShape[]) { return bodyShapes.length === 2 } -export function isLand(nft: NFT | Item) { +export function isLand(nft: Asset) { return ( nft.category === NFTCategory.PARCEL || nft.category === NFTCategory.ESTATE ) } -export function isParcel(nft: NFT | Item) { +export function isParcel(nft: Asset) { return nft.category === NFTCategory.PARCEL } diff --git a/webapp/src/modules/reducer.ts b/webapp/src/modules/reducer.ts index d66e1a4164..a7df90d72d 100644 --- a/webapp/src/modules/reducer.ts +++ b/webapp/src/modules/reducer.ts @@ -30,6 +30,7 @@ import { rentalReducer as rental } from './rental/reducer' import { eventReducer as event } from './event/reducer' import { contractReducer as contract } from './contract/reducer' import { favoritesReducer as favorites } from './favorites/reducer' +import { catalogReducer as catalogItem } from './catalog/reducer' export const createRootReducer = (history: History) => combineReducers({ @@ -61,7 +62,8 @@ export const createRootReducer = (history: History) => modal, contract, gateway, - favorites + favorites, + catalogItem }) export type RootState = ReturnType> diff --git a/webapp/src/modules/routing/sagas.spec.ts b/webapp/src/modules/routing/sagas.spec.ts index a0fc4675a6..3eb92862b2 100644 --- a/webapp/src/modules/routing/sagas.spec.ts +++ b/webapp/src/modules/routing/sagas.spec.ts @@ -2,8 +2,8 @@ import { EmotePlayMode, GenderFilterOption, ItemSortBy, - Network, NFTCategory, + Network, Rarity } from '@dcl/schemas' import { @@ -167,7 +167,8 @@ describe('when handling the fetchAssetsFromRoute request action', () => { return expectSaga(routingSaga) .provide([ [select(getCurrentBrowseOptions), browseOptions], - [select(getPage), 1] + [select(getPage), 1], + [select(getSection), Section.WEARABLES] ]) .put(fetchTrendingItemsRequest()) .dispatch(fetchAssetsFromRouteAction(browseOptions)) @@ -185,31 +186,6 @@ describe('when handling the fetchAssetsFromRoute request action', () => { page: 1 } - const filters: ItemBrowseOptions = { - view: browseOptions.view, - page: browseOptions.page, - filters: { - first: 24, - skip: 0, - sortBy: ItemSortBy.RECENTLY_REVIEWED, - creator: [address], - category: NFTCategory.EMOTE, - isWearableHead: false, - isWearableAccessory: false, - isOnSale: undefined, - wearableCategory: undefined, - emoteCategory: undefined, - isWearableSmart: undefined, - search: undefined, - rarities: undefined, - contracts: undefined, - wearableGenders: undefined, - emotePlayMode: undefined, - minPrice: undefined, - maxPrice: undefined - } - } - return expectSaga(routingSaga) .provide([ [call(getNewBrowseOptions, browseOptions), browseOptions], @@ -400,6 +376,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -427,6 +404,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -454,6 +432,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -481,6 +460,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -519,6 +499,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -546,6 +527,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -579,6 +561,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -605,6 +588,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -631,6 +615,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -658,6 +643,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -696,6 +682,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -723,6 +710,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -750,6 +738,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -777,6 +766,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -815,6 +805,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -842,6 +833,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -875,6 +867,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -901,6 +894,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -927,6 +921,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -954,6 +949,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), {}], [ call(fetchAssetsFromRoute, expectedBrowseOptions), @@ -989,6 +985,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), eventContracts], [call(fetchAssetsFromRoute, expectedBrowseOptions), Promise.resolve()] ]) @@ -1012,6 +1009,7 @@ describe('when handling the browse action', () => { .provide([ [select(getCurrentBrowseOptions), browseOptions], [select(getLocation), { pathname }], + [select(getSection), Section.WEARABLES], [select(getEventData), eventContracts], [call(fetchAssetsFromRoute, browseOptions), Promise.resolve()] ]) diff --git a/webapp/src/modules/routing/sagas.ts b/webapp/src/modules/routing/sagas.ts index 959eba1b0a..79f041e34c 100644 --- a/webapp/src/modules/routing/sagas.ts +++ b/webapp/src/modules/routing/sagas.ts @@ -17,6 +17,7 @@ import { LocationChangeAction } from 'connected-react-router' import { + CatalogFilters, NFTCategory, RentalStatus, Sale, @@ -36,7 +37,8 @@ import { getNetwork, getOnlySmart, getCurrentBrowseOptions, - getCurrentLocationAddress + getCurrentLocationAddress, + getSection } from '../routing/selectors' import { fetchNFTRequest, @@ -56,10 +58,11 @@ import { getCategoryFromSection, getDefaultOptionsByView, getSearchWearableCategory, - getItemSortBy, - getAssetOrderBy, getCollectionSortBy, - getSearchEmoteCategory + getSearchEmoteCategory, + getCatalogSortBy, + getItemSortBy, + getAssetOrderBy } from './search' import { getRarities, @@ -107,6 +110,8 @@ import { import { getData } from '../event/selectors' import { getPage } from '../ui/browse/selectors' import { fetchFavoritedItemsRequest } from '../favorites/actions' +import { AssetStatusFilter } from '../../utils/filters' +import { fetchCatalogRequest } from '../catalog/actions' import { buildBrowseURL } from './utils' export function* routingSaga() { @@ -209,7 +214,9 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { tenant, minPrice, maxPrice, - creators + creators, + network, + status } = options const address = @@ -279,32 +286,47 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { ) break default: - if (isItems) { - // TODO: clean up - const isWearableHead = - section === Sections[VendorName.DECENTRALAND].WEARABLES_HEAD - const isWearableAccessory = - section === Sections[VendorName.DECENTRALAND].WEARABLES_ACCESSORIES - - const wearableCategory = !isWearableAccessory - ? getSearchWearableCategory(section) + const isWearableHead = + section === Sections[VendorName.DECENTRALAND].WEARABLES_HEAD + const isWearableAccessory = + section === Sections[VendorName.DECENTRALAND].WEARABLES_ACCESSORIES + + const wearableCategory = !isWearableAccessory + ? getSearchWearableCategory(section) + : undefined + + const emoteCategory = + category === NFTCategory.EMOTE + ? getSearchEmoteCategory(section) : undefined - const emoteCategory = - category === NFTCategory.EMOTE - ? getSearchEmoteCategory(section) - : undefined - - const { rarities, wearableGenders, emotePlayMode } = options - + const { rarities, wearableGenders, emotePlayMode } = options + + if ( + view === View.MARKET && + (section.toString().includes(Section.EMOTES) || + section.toString().includes(Section.WEARABLES)) + ) { + const statusParameters: Partial = { + ...(status === AssetStatusFilter.ON_SALE ? { isOnSale: true } : {}), + ...(status === AssetStatusFilter.NOT_FOR_SALE + ? { isOnSale: false } + : {}), + ...(status === AssetStatusFilter.ONLY_LISTING + ? { onlyListing: true } + : {}), + ...(status === AssetStatusFilter.ONLY_MINTING + ? { onlyMinting: true } + : {}) + } yield put( - fetchItemsRequest({ + fetchCatalogRequest({ view, page, filters: { first, skip, - sortBy: getItemSortBy(sortBy), + sortBy: getCatalogSortBy(sortBy), isOnSale: onlyOnSale, creator: address ? [address] : creators, wearableCategory, @@ -315,36 +337,66 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { search, category, rarities: rarities, - contracts, wearableGenders, emotePlayMode, minPrice, - maxPrice + maxPrice, + network, + ...statusParameters } }) ) } else { - const [orderBy, orderDirection] = getAssetOrderBy(sortBy) - - yield put( - fetchNFTsRequest({ - vendor, - view, - page, - params: { - first, - skip, - orderBy, - orderDirection, - onlyOnSale, - onlyOnRent, - address, - category, - search - }, - filters: getFilters(vendor, options) // TODO: move to routing - }) - ) + if (isItems) { + yield put( + fetchItemsRequest({ + view, + page, + filters: { + first, + skip, + sortBy: getItemSortBy(sortBy), + isOnSale: onlyOnSale, + creator: address ? [address] : creators, + wearableCategory, + emoteCategory, + isWearableHead, + isWearableAccessory, + isWearableSmart: onlySmart, + search, + category, + rarities: rarities, + contracts, + wearableGenders, + emotePlayMode, + minPrice, + maxPrice + } + }) + ) + } else { + const [orderBy, orderDirection] = getAssetOrderBy(sortBy) + + yield put( + fetchNFTsRequest({ + vendor, + view, + page, + params: { + first, + skip, + orderBy, + orderDirection, + onlyOnSale, + onlyOnRent, + address, + category, + search + }, + filters: getFilters(vendor, options) // TODO: move to routing + }) + ) + } } } } @@ -355,6 +407,7 @@ export function* getNewBrowseOptions( let previous: BrowseOptions = yield select(getCurrentBrowseOptions) current = yield deriveCurrentOptions(previous, current) const view = deriveView(previous, current) + const section = yield select(getSection) const vendor = deriveVendor(previous, current) if (shouldResetOptions(previous, current)) { @@ -369,8 +422,7 @@ export function* getNewBrowseOptions( } } - const defaults = getDefaultOptionsByView(view, current.section as Section) - + const defaults = getDefaultOptionsByView(view, section) return { ...defaults, ...previous, diff --git a/webapp/src/modules/routing/search.ts b/webapp/src/modules/routing/search.ts index ab1c1ee501..0da1d809b2 100644 --- a/webapp/src/modules/routing/search.ts +++ b/webapp/src/modules/routing/search.ts @@ -1,4 +1,5 @@ import { + CatalogSortBy, CollectionSortBy, EmoteCategory, EmotePlayMode, @@ -12,6 +13,7 @@ import { BrowseOptions, SortBy, SortDirection } from './types' import { Section } from '../vendor/decentraland' import { NFTSortBy } from '../nft/types' import { isAccountView, isLandSection } from '../ui/utils' +import { AssetStatusFilter } from '../../utils/filters' const SEARCH_ARRAY_PARAM_SEPARATOR = '_' @@ -21,7 +23,7 @@ export function getDefaultOptionsByView( ): BrowseOptions { if (section === Section.LISTS) return {} - return { + let defaultOptions: Partial = { onlyOnSale: !view || !isAccountView(view), sortBy: view && isAccountView(view) @@ -30,6 +32,21 @@ export function getDefaultOptionsByView( ? SortBy.NEWEST : SortBy.RECENTLY_LISTED } + if (section && view === View.MARKET) { + const currentCategoryBySection = getCategoryFromSection(section) + if ( + currentCategoryBySection && + [NFTCategory.EMOTE, NFTCategory.WEARABLE].includes( + currentCategoryBySection + ) + ) { + defaultOptions = { + ...defaultOptions, + status: AssetStatusFilter.ON_SALE + } + } + } + return defaultOptions } export function getSearchParams(options?: BrowseOptions) { @@ -74,6 +91,9 @@ export function getSearchParams(options?: BrowseOptions) { options.rarities.join(SEARCH_ARRAY_PARAM_SEPARATOR) ) } + if (options.status) { + params.set('status', options.status.toString()) + } if (options.wearableGenders && options.wearableGenders.length > 0) { params.set( 'genders', @@ -302,6 +322,23 @@ export function getItemSortBy(sortBy: SortBy): ItemSortBy { } } +export function getCatalogSortBy(sortBy: SortBy): CatalogSortBy { + switch (sortBy) { + case SortBy.CHEAPEST: + return CatalogSortBy.CHEAPEST + case SortBy.MOST_EXPENSIVE: + return CatalogSortBy.MOST_EXPENSIVE + case SortBy.NEWEST: + return CatalogSortBy.NEWEST + case SortBy.RECENTLY_LISTED: + return CatalogSortBy.RECENTLY_LISTED + case SortBy.RECENTLY_SOLD: + return CatalogSortBy.RECENTLY_SOLD + default: + return CatalogSortBy.CHEAPEST + } +} + export function getCollectionSortBy(sortBy: SortBy): CollectionSortBy { switch (sortBy) { case SortBy.NAME: diff --git a/webapp/src/modules/routing/selectors.spec.ts b/webapp/src/modules/routing/selectors.spec.ts index 5f9959b7a9..0d0430ab05 100644 --- a/webapp/src/modules/routing/selectors.spec.ts +++ b/webapp/src/modules/routing/selectors.spec.ts @@ -4,6 +4,7 @@ import { Network, Rarity } from '@dcl/schemas' +import { AssetStatusFilter } from '../../utils/filters' import { AssetType } from '../asset/types' import { VendorName } from '../vendor' import { Section } from '../vendor/routing/types' @@ -11,6 +12,7 @@ import { View } from '../ui/types' import { PageName, Sections, SortBy } from './types' import { locations } from './locations' import { + getAllSortByOptions, getAssetType, getCreators, getIsMap, @@ -24,6 +26,8 @@ import { getSection, getSortBy, getViewAsGuest, + getSortByOptions, + getStatus, hasFiltersEnabled } from './selectors' @@ -203,10 +207,10 @@ describe('when getting the section', () => { }) describe("when there's no assetType URL param and the vendor is DECENTRALAND and the location is in browse", () => { - it('should return ITEM as the assetType', () => { + it('should return CATALOG_ITEM as the assetType', () => { expect( getAssetType.resultFunc('', locations.browse(), VendorName.DECENTRALAND) - ).toBe(AssetType.ITEM) + ).toBe(AssetType.CATALOG_ITEM) }) }) @@ -223,14 +227,14 @@ describe("when there's assetType URL param, the assetType is not NFT or ITEM and }) describe("when there's assetType URL param, the assetType is not NFT or ITEM and the vendor is DECENTRALAND and the location is in browse", () => { - it('should return ITEM as the assetType', () => { + it('should return CATALOG_ITEM as the assetType', () => { expect( getAssetType.resultFunc( 'assetType=something', locations.browse(), VendorName.DECENTRALAND ) - ).toBe(AssetType.ITEM) + ).toBe(AssetType.CATALOG_ITEM) }) }) @@ -581,3 +585,99 @@ describe('when getting if the page name', () => { }) }) }) + +describe('when there a status defined', () => { + let url: string + let status: string + beforeEach(() => { + status = 'only_minting' + url = `status=${status}` + }) + it('should return an empty array', () => { + expect(getStatus.resultFunc(url)).toEqual(status) + }) +}) + +describe('when getting the Sort By options', () => { + const baseSortByOptions = [ + getAllSortByOptions()[SortBy.NEWEST], + getAllSortByOptions()[SortBy.RECENTLY_SOLD], + getAllSortByOptions()[SortBy.CHEAPEST], + getAllSortByOptions()[SortBy.MOST_EXPENSIVE] + ] + let status: AssetStatusFilter + describe('and the status is defined', () => { + describe('and the status is ON_SALE', () => { + beforeEach(() => { + status = AssetStatusFilter.ON_SALE + }) + it('should return the base sort options array', () => { + expect(getSortByOptions.resultFunc(true, true, status)).toEqual( + baseSortByOptions + ) + }) + }) + describe('and the status is ONLY_MINTING', () => { + beforeEach(() => { + status = AssetStatusFilter.ONLY_MINTING + }) + it('should return the base sort options array', () => { + expect(getSortByOptions.resultFunc(true, true, status)).toEqual( + baseSortByOptions + ) + }) + }) + describe('and the status is ONLY_LISTING', () => { + beforeEach(() => { + status = AssetStatusFilter.ONLY_LISTING + }) + it('should return the base sort options array plus tghe RECENTLY_LISTED option', () => { + expect(getSortByOptions.resultFunc(true, true, status)).toEqual([ + getAllSortByOptions()[SortBy.RECENTLY_LISTED], + ...baseSortByOptions + ]) + }) + }) + describe('and the status is NOT_FOR_SALE', () => { + beforeEach(() => { + status = AssetStatusFilter.NOT_FOR_SALE + }) + it('should return an array with just the newest option', () => { + expect(getSortByOptions.resultFunc(true, true, status)).toEqual([ + getAllSortByOptions()[SortBy.NEWEST] + ]) + }) + }) + }) + describe('and the status is not defined', () => { + let status: string + beforeEach(() => { + status = '' + }) + describe('and the "onlyOnRent" is true', () => { + describe('and the "onlyOnSale" is false', () => { + it('should return an array with the valid on rent sort options', () => { + expect(getSortByOptions.resultFunc(true, false, status)).toEqual([ + getAllSortByOptions()[SortBy.RENTAL_LISTING_DATE], + getAllSortByOptions()[SortBy.NAME], + getAllSortByOptions()[SortBy.NEWEST], + getAllSortByOptions()[SortBy.MAX_RENTAL_PRICE] + ]) + }) + }) + }) + describe('and the "onlyOnSale" is true', () => { + describe('and the "onlyOnRent" is false', () => { + it('should return an array with just the valid on sale sort options', () => { + expect(getSortByOptions.resultFunc(false, true, status)).toEqual([ + getAllSortByOptions()[SortBy.RECENTLY_LISTED], + getAllSortByOptions()[SortBy.RECENTLY_SOLD], + getAllSortByOptions()[SortBy.CHEAPEST], + getAllSortByOptions()[SortBy.NEWEST], + getAllSortByOptions()[SortBy.NAME] + ]) + }) + }) + }) + }) +}) diff --git a/webapp/src/modules/routing/selectors.ts b/webapp/src/modules/routing/selectors.ts index 120d9b7248..e008faa1e9 100644 --- a/webapp/src/modules/routing/selectors.ts +++ b/webapp/src/modules/routing/selectors.ts @@ -10,6 +10,8 @@ import { Network, Rarity } from '@dcl/schemas' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { AssetStatusFilter } from '../../utils/filters' import { getView } from '../ui/browse/selectors' import { View } from '../ui/types' import { VendorName } from '../vendor/types' @@ -26,7 +28,7 @@ import { getURLParam, getURLParamArray_nonStandard } from './search' -import { BrowseOptions, PageName, SortBy } from './types' +import { BrowseOptions, PageName, SortBy, SortByOption } from './types' import { locations } from './locations' export const getState = (state: RootState) => state.routing @@ -92,14 +94,12 @@ export const getSortBy = createSelector< View | undefined, Section, SortBy | undefined ->( - getRouterSearch, - getView, - getSection, - (search, view, section) => +>(getRouterSearch, getView, getSection, (search, view, section) => { + return ( getURLParam(search, 'sortBy') || getDefaultOptionsByView(view, section).sortBy -) + ) +}) export const getOnlyOnSale = createSelector< RootState, @@ -137,6 +137,111 @@ export const getOnlyOnRent = createSelector< } }) +export const getAllSortByOptions = () => ({ + [SortBy.NEWEST]: { value: SortBy.NEWEST, text: t('filters.newest') }, + [SortBy.NAME]: { value: SortBy.NAME, text: t('filters.name') }, + [SortBy.RECENTLY_SOLD]: { + value: SortBy.RECENTLY_SOLD, + text: t('filters.recently_sold') + }, + [SortBy.CHEAPEST]: { + value: SortBy.CHEAPEST, + text: t('filters.cheapest') + }, + [SortBy.MOST_EXPENSIVE]: { + value: SortBy.MOST_EXPENSIVE, + text: t('filters.most_expensive') + }, + [SortBy.MAX_RENTAL_PRICE]: { + value: SortBy.MAX_RENTAL_PRICE, + text: t('filters.cheapest') + }, + [SortBy.RECENTLY_LISTED]: { + value: SortBy.RECENTLY_LISTED, + text: t('filters.recently_listed') + }, + [SortBy.RENTAL_LISTING_DATE]: { + value: SortBy.RENTAL_LISTING_DATE, + text: t('filters.recently_listed_for_rent') + } +}) + +export const getStatus = createSelector( + getRouterSearch, + search => getURLParam(search, 'status') || '' +) + +export const getSortByOptions = createSelector< + RootState, + boolean | undefined, + boolean | undefined, + string, + SortByOption[] +>(getOnlyOnRent, getOnlyOnSale, getStatus, (onlyOnRent, onlyOnSale, status) => { + const SORT_BY_MAP = getAllSortByOptions() + let orderByDropdownOptions: SortByOption[] = [] + if (status) { + const baseFilters = [ + SORT_BY_MAP[SortBy.NEWEST], + SORT_BY_MAP[SortBy.RECENTLY_SOLD], + SORT_BY_MAP[SortBy.CHEAPEST], + SORT_BY_MAP[SortBy.MOST_EXPENSIVE] + ] + switch (status) { + case AssetStatusFilter.ON_SALE: + case AssetStatusFilter.ONLY_MINTING: + orderByDropdownOptions = baseFilters + break + case AssetStatusFilter.ONLY_LISTING: + orderByDropdownOptions = [ + SORT_BY_MAP[SortBy.RECENTLY_LISTED], + ...baseFilters + ] + break + case AssetStatusFilter.NOT_FOR_SALE: + orderByDropdownOptions = [SORT_BY_MAP[SortBy.NEWEST]] + break + } + return orderByDropdownOptions + } + if (onlyOnRent) { + orderByDropdownOptions = [ + { + value: SortBy.RENTAL_LISTING_DATE, + text: t('filters.recently_listed_for_rent') + }, + { value: SortBy.NAME, text: t('filters.name') }, + { value: SortBy.NEWEST, text: t('filters.newest') }, + { value: SortBy.MAX_RENTAL_PRICE, text: t('filters.cheapest') } + ] + } else { + orderByDropdownOptions = [ + { value: SortBy.NEWEST, text: t('filters.newest') }, + { value: SortBy.NAME, text: t('filters.name') } + ] + } + + if (onlyOnSale) { + orderByDropdownOptions = [ + { + value: SortBy.RECENTLY_LISTED, + text: t('filters.recently_listed') + }, + { + value: SortBy.RECENTLY_SOLD, + text: t('filters.recently_sold') + }, + { + value: SortBy.CHEAPEST, + text: t('filters.cheapest') + }, + ...orderByDropdownOptions + ] + } + + return orderByDropdownOptions +}) + export const getIsSoldOut = createSelector< RootState, string, @@ -231,8 +336,9 @@ export const getAssetType = createSelector< if (!assetTypeParam || !(assetTypeParam.toUpperCase() in AssetType)) { if (vendor === VendorName.DECENTRALAND && pathname === locations.browse()) { - return AssetType.ITEM + return AssetType.CATALOG_ITEM } + return AssetType.NFT } return assetTypeParam as AssetType diff --git a/webapp/src/modules/routing/types.ts b/webapp/src/modules/routing/types.ts index 41eb98fa89..d1c1bd1169 100644 --- a/webapp/src/modules/routing/types.ts +++ b/webapp/src/modules/routing/types.ts @@ -7,6 +7,7 @@ import { WearableGender, GenderFilterOption } from '@dcl/schemas' +import { AssetStatusFilter } from '../../utils/filters' import { AssetType } from '../asset/types' import { VendorName } from '../vendor/types' import { View } from '../ui/types' @@ -18,6 +19,7 @@ export enum SortBy { NEWEST = 'newest', RECENTLY_LISTED = 'recently_listed', CHEAPEST = 'cheapest', + MOST_EXPENSIVE = 'most_expensive', RECENTLY_REVIEWED = 'recently_reviewed', RECENTLY_SOLD = 'recently_sold', SIZE = 'size', @@ -29,6 +31,11 @@ export enum SortBy { CHEAPEST_RENT = 'cheapest_rent' } +export type SortByOption = { + value: SortBy + text: string +} + export enum SortDirection { ASC = 'asc', DESC = 'desc' @@ -47,6 +54,7 @@ export type BrowseOptions = { isMap?: boolean isFullscreen?: boolean rarities?: Rarity[] + status?: AssetStatusFilter wearableGenders?: (WearableGender | GenderFilterOption)[] search?: string contracts?: string[] diff --git a/webapp/src/modules/routing/utils.ts b/webapp/src/modules/routing/utils.ts index 25dce5888a..b51c56a62c 100644 --- a/webapp/src/modules/routing/utils.ts +++ b/webapp/src/modules/routing/utils.ts @@ -20,6 +20,7 @@ export const rentalFilters = [ export const sellFilters = [ SortBy.NAME, SortBy.CHEAPEST, + SortBy.MOST_EXPENSIVE, SortBy.NEWEST, SortBy.RECENTLY_REVIEWED, SortBy.RECENTLY_SOLD, diff --git a/webapp/src/modules/sagas.ts b/webapp/src/modules/sagas.ts index b77eab520b..3ca4b59c7e 100644 --- a/webapp/src/modules/sagas.ts +++ b/webapp/src/modules/sagas.ts @@ -25,6 +25,7 @@ import { translationSaga } from './translation/sagas' import { uiSaga } from './ui/sagas' import { walletSaga } from './wallet/sagas' import { itemSaga } from './item/sagas' +import { catalogSaga } from './catalog/sagas' import { collectionSaga } from './collection/sagas' import { saleSaga } from './sale/sagas' import { accountSaga } from './account/sagas' @@ -100,6 +101,7 @@ export function* rootSaga(getIdentity: () => AuthIdentity | undefined) { gatewaySaga(), locationSaga(), transakSaga(), - favoritesSaga(getIdentity) + favoritesSaga(getIdentity), + catalogSaga() ]) } diff --git a/webapp/src/modules/translation/locales/en.json b/webapp/src/modules/translation/locales/en.json index fcc634d53c..33609b91d3 100644 --- a/webapp/src/modules/translation/locales/en.json +++ b/webapp/src/modules/translation/locales/en.json @@ -168,6 +168,7 @@ "cheapest": "Cheapest", "cheapest_sale": "Cheapest (Sale)", "cheapest_rent": "Cheapest (Rent)", + "most_expensive": "Most expensive", "no_results": "No results were found.", "type_to_search": "Type to search for collections.", "clear": "Clear all filters", @@ -375,6 +376,16 @@ "available_for_female": "Available for female", "available_for_male": "Available for male" }, + "status": { + "title": "Status", + "on_sale": "On Sale", + "on_sale_tooltip": "Includes items available for minting and/or with available listings.", + "only_minting": "Only available for minting", + "only_minting_tooltip": "Only includes items that are available for minting (buying directly from the creators).", + "only_listing": "Only listings", + "only_listing_tooltip": "Only includes items that are being resold.", + "not_for_sale": "Not for sale" + }, "rarities": { "title": "Rarity", "all_items": "All rarities", @@ -1191,7 +1202,15 @@ "rented_until": "Rented until {endDate, date, medium}", "claiming_back": "Claiming back {asset_type, select, parcel {parcel} estate {estate} other {LAND}}", "rental_ended": "Rental period over" - } + }, + "listings": "{count} {count, plural, one {Listing} other {Listings}}", + "available_for_mint": "Available for mint", + "cheapest_listing": "Cheapest Listing", + "not_for_sale": "Not for sale", + "owners": "{count} {count, plural, one {Owner} other {Owners}}", + "cheapest_option": "Cheapest Option", + "most_expensive": "Most Expensive", + "also_minting": "Also Available for minting" }, "rentals_promotional_modal": { "title": "Announcing LAND rentals", diff --git a/webapp/src/modules/translation/locales/es.json b/webapp/src/modules/translation/locales/es.json index 3b5d62ee3e..3591e2d318 100644 --- a/webapp/src/modules/translation/locales/es.json +++ b/webapp/src/modules/translation/locales/es.json @@ -162,8 +162,7 @@ "recently_listed_for_rent": "Listados recientemente para rentar", "recently_sold": "Vendidos recientemente", "cheapest": "Más baratos", - "cheapest_sale": "Más baratos (Venta)", - "cheapest_rent": "Más baratos (Renta)", + "most_expensive": "Más caros", "no_results": "No se encontraron resultados.", "type_to_search": "Escriba para buscar collecciones.", "clear": "Borrar filtros", @@ -371,6 +370,16 @@ "male": "Solo masculino", "unisex": "Solo unisex" }, + "status": { + "title": "Estado", + "on_sale": "En venta", + "on_sale_tooltip": "Incluye items disponible para mintear o publicaciones disponibles", + "only_minting": "Solo disponible para mintear", + "only_minting_tooltip": "Solo incluye items disponibles para mintear (comprando directamente al creador).", + "only_listing": "Solo publicaciones", + "only_listing_tooltip": "Solo incluye items que estan siendo re-vendidos", + "not_for_sale": "No a la venta" + }, "rarities": { "title": "Rareza", "all_items": "Todas las rarezas", @@ -1185,7 +1194,15 @@ "rented_until": "Rentada hasta {endDate, date, medium}", "claiming_back": "Reclamando {asset_type, select, parcel {la parcel} estate {el estate} other {LAND}}", "rental_ended": "Período de renta terminado" - } + }, + "listings": "{count} {count, plural, one {Collecionable} other {Collecionables}}", + "available_for_mint": "Disponible para mintear", + "cheapest_listing": "Coleccionable más barato", + "not_for_sale": "No disponible para la venta", + "owners": "{count} {count, plural, one {Dueño} other {Dueños}}", + "cheapest_option": "Más baratos", + "most_expensive": "Más Caro", + "also_minting": "También disponibles para mintear" }, "rentals_promotional_modal": { "title": "¡La renta de tierras está disponible!", diff --git a/webapp/src/modules/translation/locales/zh.json b/webapp/src/modules/translation/locales/zh.json index 6d6af0ad68..5edfc322ae 100644 --- a/webapp/src/modules/translation/locales/zh.json +++ b/webapp/src/modules/translation/locales/zh.json @@ -164,6 +164,7 @@ "cheapest": "最低价", "cheapest_sale": "最低价 (文塔)", "cheapest_rent": "最低价 (莲太)", + "most_expensive": "最贵的", "no_results": "未找到结果。", "type_to_search": "键入以搜索集合。", "clear": "清除过滤器", @@ -372,6 +373,16 @@ "male": "只有男性", "unisex": "仅男女皆宜" }, + "status": { + "title": "地位", + "on_sale": "特价中", + "on_sale_tooltip": "包括可用于铸造和/或具有可用列表的项目。", + "only_minting": "仅可用于铸币", + "only_minting_tooltip": "仅包括可铸造的物品(直接从创作者处购买)。", + "only_listing": "仅列表", + "only_listing_tooltip": "仅包括正在转售的项目。", + "not_for_sale": "不作为产品销售" + }, "rarities": { "title": "稀有度", "all_items": "所有稀有", @@ -1188,7 +1199,15 @@ "rented_until": "租到 {endDate, date, medium}", "claiming_back": "土地正在收回", "rental_ended": "租赁已结束" - } + }, + "listings": "{count} {count, plural, one {收藏品} other {收藏品}}", + "available_for_mint": "可用於鑄幣", + "cheapest_listing": "最便宜的收藏品", + "not_for_sale": "不可出售", + "owners": "{count} {count, plural, one {所有者} other {所有者}}", + "cheapest_option": "最便宜", + "most_expensive": "最貴", + "also_minting": "也可用於鑄造" }, "rentals_promotional_modal": { "title": "Announcing official LAND rentals", diff --git a/webapp/src/modules/ui/browse/reducer.ts b/webapp/src/modules/ui/browse/reducer.ts index 562729ee37..f509ecffe3 100644 --- a/webapp/src/modules/ui/browse/reducer.ts +++ b/webapp/src/modules/ui/browse/reducer.ts @@ -10,6 +10,11 @@ import { UndoUnpickingItemAsFavoriteSuccessAction, UnpickItemAsFavoriteSuccessAction } from '../../favorites/actions' +import { + FetchCatalogRequestAction, + FetchCatalogSuccessAction, + FETCH_CATALOG_SUCCESS +} from '../../catalog/actions' import { FetchItemsRequestAction, FetchItemsSuccessAction, @@ -35,6 +40,7 @@ export type BrowseUIState = { nftIds: string[] listIds: string[] itemIds: string[] + catalogIds: string[] lastTimestamp: number count?: number } @@ -45,6 +51,7 @@ export const INITIAL_STATE: BrowseUIState = { nftIds: [], listIds: [], itemIds: [], + catalogIds: [], count: undefined, lastTimestamp: 0 } diff --git a/webapp/src/modules/ui/browse/selectors.ts b/webapp/src/modules/ui/browse/selectors.ts index d646bc6d59..f9b7df4800 100644 --- a/webapp/src/modules/ui/browse/selectors.ts +++ b/webapp/src/modules/ui/browse/selectors.ts @@ -4,7 +4,8 @@ import { NFTCategory, Order, RentalListing, - RentalStatus + RentalStatus, + CatalogItem } from '@dcl/schemas' import { Transaction, @@ -17,6 +18,8 @@ import { getData as getRentalData } from '../../rental/selectors' import { getFavoritedItems as getFavoritedItemsFromState } from '../../favorites/selectors' import { FavoritesData } from '../../favorites/types' import { CLAIM_ASSET_TRANSACTION_SUBMITTED } from '../../rental/actions' +import { getData as getCatalogData } from '../../catalog/selectors' +import { CatalogState } from '../../catalog/reducer' import { NFTState } from '../../nft/reducer' import { RootState } from '../../reducer' import { BrowseUIState } from './reducer' @@ -55,7 +58,16 @@ const getItems = createSelector< browse.itemIds.map(id => itemsById[id]) ) -const getOnSaleItems = createSelector< +export const getCatalogItems = createSelector< + RootState, + BrowseUIState, + CatalogState['data'], + CatalogItem[] +>(getState, getCatalogData, (browse, catalogsById) => + browse.catalogIds.map(id => catalogsById[id]) +) + +export const getOnSaleItems = createSelector< RootState, ReturnType, ReturnType, @@ -71,7 +83,9 @@ export const getBrowseAssets = ( section: Section, assetType: AssetType ): Asset[] => { - if (assetType === AssetType.ITEM) { + if (assetType === AssetType.CATALOG_ITEM) { + return getCatalogItems(state) + } else if (assetType === AssetType.ITEM) { return section === Sections.decentraland.LISTS ? getItemsPickedByUser(state) : getItems(state) diff --git a/webapp/src/modules/ui/browse/types.ts b/webapp/src/modules/ui/browse/types.ts index aa0e724d64..faccadaab2 100644 --- a/webapp/src/modules/ui/browse/types.ts +++ b/webapp/src/modules/ui/browse/types.ts @@ -1,8 +1,15 @@ -import { Item, Order, RentalListing } from '@dcl/schemas' +import { CatalogFilters, Item, Order, RentalListing } from '@dcl/schemas' import { NFT } from '../../nft/types' import { VendorName } from '../../vendor' +import { View } from '../types' export type OnSaleNFT = [NFT, Order] export type OnRentNFT = [NFT, RentalListing] export type OnSaleElement = Item | OnSaleNFT + +export type CatalogBrowseOptions = { + view?: View + page?: number + filters?: CatalogFilters +} diff --git a/webapp/src/modules/vendor/decentraland/catalog/api.ts b/webapp/src/modules/vendor/decentraland/catalog/api.ts new file mode 100644 index 0000000000..abe6213ef6 --- /dev/null +++ b/webapp/src/modules/vendor/decentraland/catalog/api.ts @@ -0,0 +1,140 @@ +import { BaseAPI } from 'decentraland-dapps/dist/lib/api' +import { NFT_SERVER_URL } from '../nft' +import { retryParams } from '../utils' +import { CatalogFilters, CatalogItem } from '@dcl/schemas' + +class CatalogApi extends BaseAPI { + fetch = async (filters: CatalogFilters = {}): Promise => { + const queryParams = this.buildItemsQueryString(filters) + return this.request('get', `/catalog?${queryParams}`) + } + + private buildItemsQueryString(filters: CatalogFilters): string { + const queryParams = new URLSearchParams() + + if (filters.first) { + queryParams.append('first', filters.first.toString()) + } + + if (filters.skip) { + queryParams.append('skip', filters.skip.toString()) + } + + if (filters.category) { + queryParams.append('category', filters.category) + } + + if (filters.creator) { + let creators = Array.isArray(filters.creator) + ? filters.creator + : [filters.creator] + creators.forEach(creator => queryParams.append('creator', creator)) + } + + if (filters.isSoldOut) { + queryParams.append('isSoldOut', 'true') + } + + if (filters.isOnSale) { + queryParams.append('isOnSale', 'true') + } + + if (filters.search) { + queryParams.set('search', filters.search) + } + + if (filters.isWearableHead) { + queryParams.append('isWearableHead', 'true') + } + + if (filters.isWearableSmart) { + queryParams.append('isWearableSmart', 'true') + } + + if (filters.isWearableAccessory) { + queryParams.append('isWearableAccessory', 'true') + } + + if (filters.wearableCategory) { + queryParams.append('wearableCategory', filters.wearableCategory) + } + + if (filters.rarities) { + for (const rarity of filters.rarities) { + queryParams.append('rarity', rarity) + } + } + + if (filters.wearableGenders) { + for (const wearableGender of filters.wearableGenders) { + queryParams.append('wearableGender', wearableGender) + } + } + + if (filters.emoteCategory) { + queryParams.append('emoteCategory', filters.emoteCategory) + } + + if (filters.emotePlayMode) { + for (const emotePlayMode of filters.emotePlayMode) { + queryParams.append('emotePlayMode', emotePlayMode) + } + } + + // if (filters.emoteGenders) { + // filters.emoteGenders.forEach(emoteGender => + // queryParams.append('emoteGender', emoteGender) + // ) + // } + + if (filters.contractAddresses) { + filters.contractAddresses.forEach(contract => + queryParams.append('contractAddress', contract) + ) + } + + if (filters.itemId) { + queryParams.append('itemId', filters.itemId) + } + + if (filters.network) { + queryParams.append('network', filters.network) + } + + if (filters.minPrice) { + queryParams.append('minPrice', filters.minPrice) + } + + if (filters.maxPrice) { + queryParams.append('maxPrice', filters.maxPrice) + } + + if (filters.onlyMinting) { + queryParams.append('onlyMinting', 'true') + } + + if (filters.onlyListing) { + queryParams.append('onlyListing', 'true') + } + + if (filters.sortBy) { + queryParams.append('sortBy', filters.sortBy) + } + + if (filters.sortDirection) { + queryParams.append('sortDirection', filters.sortDirection) + } + + if (filters.limit) { + queryParams.append('limit', filters.limit.toString()) + } + + if (filters.offset) { + queryParams.append('offset', filters.offset.toString()) + } + + return queryParams.toString() + } +} + +export const catalogAPI = new CatalogApi(NFT_SERVER_URL, retryParams) diff --git a/webapp/src/utils/filters.tsx b/webapp/src/utils/filters.tsx index 2af7c02ad9..3847ffee85 100644 --- a/webapp/src/utils/filters.tsx +++ b/webapp/src/utils/filters.tsx @@ -9,6 +9,13 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { Mana } from '../components/Mana' import { LANDFilters } from '../components/Vendor/decentraland/types' +export enum AssetStatusFilter { + ON_SALE = 'on_sale', + ONLY_MINTING = 'only_minting', + ONLY_LISTING = 'only_listing', + NOT_FOR_SALE = 'not_for_sale' +} + export const AVAILABLE_FOR_MALE = 'AVAILABLE_FOR_MALE' export const AVAILABLE_FOR_FEMALE = 'AVAILABLE_FOR_FEMALE'