Skip to content

Commit

Permalink
feat: Add favorites counter to cards and detail (decentraland#1495)
Browse files Browse the repository at this point in the history
* feat: Create Favorites Counter component

* fix: Avoid changes in the size of the bubbles on hover

* refactor: Use css modules

* feat: Add favorites counter to the item detail page and to the asset cards

* refactor: Apply PR feedback

* test: Add skipped tests (waiting for render with providers) to the components that changed

* test: Add tests for the title component and the new favorites counter inside

* test: Ass tests for the favorite counter in the item detail
  • Loading branch information
kevinszuchet authored Mar 27, 2023
1 parent 8c6d79e commit 31fc96f
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 14 deletions.
4 changes: 3 additions & 1 deletion webapp/src/components/AssetCard/AssetCard.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getOpenRentalId } from '../../modules/rental/utils'
import { getRentalById } from '../../modules/rental/selectors'
import { MapStateProps, OwnProps, MapDispatchProps } from './AssetCard.types'
import AssetCard from './AssetCard'
import { getIsFavoritesEnabled } from '../../modules/features/selectors'

const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
const { order, asset } = ownProps
Expand Down Expand Up @@ -41,7 +42,8 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
showRentalChip:
rentalOfNFT !== null &&
view === View.CURRENT_ACCOUNT &&
getLocation(state).pathname !== locations.root()
getLocation(state).pathname !== locations.root(),
isFavoritesEnabled: getIsFavoritesEnabled(state)
}
}

Expand Down
8 changes: 8 additions & 0 deletions webapp/src/components/AssetCard/AssetCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,11 @@ a.ui.card.link:hover .meta {
text-align: left;
}
}

.AssetCard .FavoritesCounterBubble {
display: flex;
align-self: flex-start;
position: fixed;
top: 8px;
left: 8px;
}
100 changes: 100 additions & 0 deletions webapp/src/components/AssetCard/AssetCard.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { BodyShape, ChainId, Network, NFTCategory, Rarity } from '@dcl/schemas'
import { Asset } from '../../modules/asset/types'
import { renderWithProviders } from '../../utils/test'
import AssetCard from './AssetCard'
import { Props as AssetCardProps } from './AssetCard.types'

const FAVORITES_COUNTER_TEST_ID = 'favorites-counter'

function renderAssetCard(props: Partial<AssetCardProps> = {}) {
return renderWithProviders(
<AssetCard
asset={{} as Asset}
price={null}
isClaimingBackLandTransactionPending={false}
showRentalChip={false}
rental={null}
isFavoritesEnabled={false}
{...props}
/>
)
}

describe('AssetCard', () => {
let asset: Asset

beforeEach(() => {
asset = {
id: 'assetId',
name: 'assetName',
thumbnail: 'assetThumbnail',
url: 'assetUrl',
category: NFTCategory.WEARABLE,
contractAddress: '0xContractAddress',
itemId: '',
rarity: Rarity.UNIQUE,
price: '5000000000000000',
available: 0,
isOnSale: false,
creator: '0xCreator',
beneficiary: null,
createdAt: 0,
updatedAt: 0,
reviewedAt: 0,
soldAt: 0,
data: {
wearable: {
rarity: Rarity.UNIQUE,
bodyShapes: [BodyShape.MALE]
} as Asset['data']['wearable']
},
network: Network.MATIC,
chainId: ChainId.MATIC_MUMBAI,
firstListedAt: null
}
})

it('should render the Asset Card', () => {
renderAssetCard({ asset })
})

describe('when the favorites feature flag is not enabled', () => {
it('should not render the favorites counter', () => {
const { queryByTestId } = renderAssetCard({
asset,
isFavoritesEnabled: false
})
expect(queryByTestId(FAVORITES_COUNTER_TEST_ID)).toBeNull()
})
})

describe('when the favorites feature flag is enabled', () => {
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,
isFavoritesEnabled: true
})
expect(queryByTestId(FAVORITES_COUNTER_TEST_ID)).toBeNull()
})
})

describe('when the asset is an item', () => {
beforeEach(() => {
asset = { ...asset, itemId: 'itemId' } as Asset
})

it('should render the favorites counter', () => {
const { getByTestId } = renderAssetCard({
asset,
isFavoritesEnabled: true
})
expect(getByTestId(FAVORITES_COUNTER_TEST_ID)).toBeInTheDocument()
})
})
})
})
9 changes: 7 additions & 2 deletions webapp/src/components/AssetCard/AssetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 } from '../../modules/asset/utils'
import { getAssetName, getAssetUrl, isNFT } from '../../modules/asset/utils'
import { Asset } from '../../modules/asset/types'
import { NFT } from '../../modules/nft/types'
import { isLand } from '../../modules/nft/utils'
Expand All @@ -17,6 +17,7 @@ import {
} from '../../modules/rental/utils'
import { Mana } from '../Mana'
import { AssetImage } from '../AssetImage'
import { FavoritesCounter } from '../FavoritesCounter'
import { ParcelTags } from './ParcelTags'
import { EstateTags } from './EstateTags'
import { WearableTags } from './WearableTags'
Expand Down Expand Up @@ -89,7 +90,8 @@ const AssetCard = (props: Props) => {
showRentalChip: showRentalBubble,
onClick,
isClaimingBackLandTransactionPending,
rental
rental,
isFavoritesEnabled
} = props

const title = getAssetName(asset)
Expand All @@ -112,6 +114,9 @@ const AssetCard = (props: Props) => {
showOrderListedTag={showListedTag}
showMonospace
/>
{isFavoritesEnabled && !isNFT(asset) ? (
<FavoritesCounter className="FavoritesCounterBubble" item={asset} />
) : null}
{showRentalBubble ? (
<RentalChip
asset={asset}
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/components/AssetCard/AssetCard.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Props = {
isClaimingBackLandTransactionPending: boolean
showRentalChip: boolean
rental: RentalListing | null
isFavoritesEnabled: boolean
}

export type MapStateProps = Pick<
Expand All @@ -20,6 +21,7 @@ export type MapStateProps = Pick<
| 'showRentalChip'
| 'rental'
| 'isClaimingBackLandTransactionPending'
| 'isFavoritesEnabled'
>
export type MapDispatchProps = {}
export type OwnProps = Pick<Props, 'asset' | 'order' | 'isManager'>
11 changes: 11 additions & 0 deletions webapp/src/components/AssetPage/Title/Title.container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import { getIsFavoritesEnabled } from '../../../modules/features/selectors'
import { RootState } from '../../../modules/reducer'
import { MapStateProps } from './Title.types'
import Title from './Title'

const mapState = (state: RootState): MapStateProps => ({
isFavoritesEnabled: getIsFavoritesEnabled(state)
})

export default connect(mapState)(Title)
13 changes: 13 additions & 0 deletions webapp/src/components/AssetPage/Title/Title.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,17 @@
line-height: 42px;
letter-spacing: 0.4000000059604645px;
text-align: left;
/* TODO: this may be moved after updating the detail page on the unified markets change */
display: flex;
align-items: center;
}

/* TODO: the following lines may be moved after updating the detail page on the unified markets change */
.text {
flex: 1 0;
}

.favorites {
display: flex;
align-self: flex-end;
}
58 changes: 58 additions & 0 deletions webapp/src/components/AssetPage/Title/Title.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Asset } from '../../../modules/asset/types'
import { getAssetName } from '../../../modules/asset/utils'
import { renderWithProviders } from '../../../utils/test'
import Title from './Title'

const FAVORITES_COUNTER_TEST_ID = 'favorites-counter'

describe('Title', () => {
let asset: Asset

beforeEach(() => {
asset = { name: 'Asset Name' } as Asset
})

it('should render the Asset Name', () => {
const { getByText } = renderWithProviders(
<Title asset={asset} isFavoritesEnabled />
)
expect(getByText(getAssetName(asset))).toBeInTheDocument()
})

describe('when the favorites feature flag is not enabled', () => {
it('should not render the favorites counter', () => {
const { queryByTestId } = renderWithProviders(
<Title asset={asset} isFavoritesEnabled={false} />
)
expect(queryByTestId(FAVORITES_COUNTER_TEST_ID)).toBeNull()
})
})

describe('when the favorites feature flag is enabled', () => {
describe('when the asset is an nft', () => {
beforeEach(() => {
asset = { ...asset, tokenId: 'tokenId' } as Asset
})

it('should not render the favorites counter', () => {
const { queryByTestId } = renderWithProviders(
<Title asset={asset} isFavoritesEnabled />
)
expect(queryByTestId(FAVORITES_COUNTER_TEST_ID)).toBeNull()
})
})

describe('when the asset is an item', () => {
beforeEach(() => {
asset = { ...asset, itemId: 'itemId' } as Asset
})

it('should render the favorites counter', () => {
const { getByTestId } = renderWithProviders(
<Title asset={asset} isFavoritesEnabled />
)
expect(getByTestId(FAVORITES_COUNTER_TEST_ID)).toBeInTheDocument()
})
})
})
})
19 changes: 16 additions & 3 deletions webapp/src/components/AssetPage/Title/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import React from 'react'
import { getAssetName } from '../../../modules/asset/utils'
import { getAssetName, isNFT } from '../../../modules/asset/utils'
import { FavoritesCounter } from '../../FavoritesCounter'
import { Props } from './Title.types'
import styles from './Title.module.css'

const Title = ({ asset }: Props) => {
return <div className={styles.title}>{getAssetName(asset)}</div>
const Title = ({ asset, isFavoritesEnabled }: Props) => {
return (
<div className={styles.title}>
<span className={styles.text}>{getAssetName(asset)}</span>
{/* TODO: this may be moved after the new detail page for unified markets */}
{isFavoritesEnabled && !isNFT(asset) ? (
<FavoritesCounter
isCollapsed
className={styles.favorites}
item={asset}
/>
) : null}
</div>
)
}

export default React.memo(Title)
3 changes: 3 additions & 0 deletions webapp/src/components/AssetPage/Title/Title.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ import { Asset } from '../../../modules/asset/types'

export type Props = {
asset: Asset
isFavoritesEnabled: boolean
}

export type MapStateProps = Pick<Props, 'isFavoritesEnabled'>
2 changes: 1 addition & 1 deletion webapp/src/components/AssetPage/Title/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import Title from './Title'
import Title from './Title.container'

export default Title
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import FavoritesCounter from './FavoritesCounter'
// TOOD: use the values from the store
const mapState = (_state: RootState): MapStateProps => ({
isPickedByUser: Math.floor(Math.random() * 2) > 0,
count: Math.floor(Math.random() * 50)
count: Math.floor(Math.random() * 5000)
})

const mapDispatch = (_dispatch: MapDispatch): MapDispatchProps => ({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const FavoritesCounter = (props: Props) => {
: t('favorites_counter.pick_label')
}
role="button"
data-testid="favorites-counter"
>
<div className={styles.bubble}>
<Icon
Expand Down
14 changes: 8 additions & 6 deletions webapp/src/utils/test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { render, RenderResult, waitFor } from '@testing-library/react'
import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider'
import { ConnectedRouter } from 'connected-react-router'
import { createMemoryHistory } from 'history'
import { Provider } from 'react-redux'
import { Store } from 'redux'
import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider'
import { RootState } from '../modules/reducer'
import { initTestStore } from '../modules/store'
import * as locales from '../modules/translation/locales'
Expand All @@ -17,12 +19,13 @@ export function renderWithProviders(
storage: { loading: false },
translation: { data: locales, locale: 'en' }
})
const history = createMemoryHistory()

function AppProviders({ children }: { children: JSX.Element }) {
return (
<Provider store={initializedStore}>
<TranslationProvider locales={Object.keys(locales)}>
{children}
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</TranslationProvider>
</Provider>
)
Expand All @@ -31,12 +34,11 @@ export function renderWithProviders(
return render(component, { wrapper: AppProviders })
}


export async function waitForComponentToFinishLoading(screen: RenderResult) {
// TODO: Make loader accessible so we can get the info without using the container ui#310
await waitFor(() =>
expect(screen.container.getElementsByClassName('loader-container').length).toEqual(
0
)
expect(
screen.container.getElementsByClassName('loader-container').length
).toEqual(0)
)
}

0 comments on commit 31fc96f

Please sign in to comment.