From 4f8c4820d2e897593fe304cf9dcac477f54aa0ba Mon Sep 17 00:00:00 2001 From: vthomas13 <10986371+vthomas13@users.noreply.github.com> Date: Fri, 2 Jun 2023 11:01:51 -0400 Subject: [PATCH] Multichain NFT network badges (#19029) * adding badges for nfts * fixing default nft styling issue * adding multichain flag, making borderRadius inline * Apply suggestions from code review Co-authored-by: George Marshall * fixing imports * removing nullcheck for guaranteed fields * moving badgewrapper UI into multichain component * using Box for button, removing inline style, border-radius for NFT default image * adding nft badges to NFT Details page * nits, snap update * fixing/refactoring nftdefaultimage display, adding clickable, removing handleimageclick, refactor NFTItem, required props * editing nft-default-image story, test, and snap * Updating to fix positioning, use Box props to reduce CSS and BEM naming conventions * moving minor styling to Box props, adding comment * display block typo --------- Co-authored-by: George Marshall --- .../nft-default-image.test.js.snap | 18 ++-- .../app/nft-default-image/index.scss | 15 ++-- .../nft-default-image/nft-default-image.js | 40 ++++++--- .../nft-default-image.stories.js | 17 +--- .../nft-default-image.test.js | 26 +++--- .../__snapshots__/nft-details.test.js.snap | 2 +- ui/components/app/nft-details/index.scss | 12 +++ ui/components/app/nft-details/nft-details.js | 49 ++++++---- ui/components/app/nfts-items/index.scss | 1 - ui/components/app/nfts-items/nfts-items.js | 75 ++++++++++------ .../multichain/multichain-components.scss | 1 + ui/components/multichain/nft-item/index.js | 1 + ui/components/multichain/nft-item/nft-item.js | 90 +++++++++++++++++++ .../multichain/nft-item/nft-item.scss | 27 ++++++ .../multichain/nft-item/nft-item.stories.js | 41 +++++++++ .../multichain/nft-item/nft-item.test.js | 70 +++++++++++++++ 16 files changed, 381 insertions(+), 104 deletions(-) create mode 100644 ui/components/multichain/nft-item/index.js create mode 100644 ui/components/multichain/nft-item/nft-item.js create mode 100644 ui/components/multichain/nft-item/nft-item.scss create mode 100644 ui/components/multichain/nft-item/nft-item.stories.js create mode 100644 ui/components/multichain/nft-item/nft-item.test.js diff --git a/ui/components/app/nft-default-image/__snapshots__/nft-default-image.test.js.snap b/ui/components/app/nft-default-image/__snapshots__/nft-default-image.test.js.snap index 3268aaa77..e70892df2 100644 --- a/ui/components/app/nft-default-image/__snapshots__/nft-default-image.test.js.snap +++ b/ui/components/app/nft-default-image/__snapshots__/nft-default-image.test.js.snap @@ -2,13 +2,13 @@ exports[`NFT Default Image should match snapshot with all provided props 1`] = `
- +
`; -exports[`NFT Default Image should match snapshot with missing image click handler 1`] = ` +exports[`NFT Default Image should match snapshot with missing clickable prop 1`] = `
NFT Name @@ -43,12 +43,12 @@ exports[`NFT Default Image should match snapshot with missing image click handle exports[`NFT Default Image should render with no props 1`] = `
[unknownCollection] diff --git a/ui/components/app/nft-default-image/index.scss b/ui/components/app/nft-default-image/index.scss index 2db0da289..e76dbec41 100644 --- a/ui/components/app/nft-default-image/index.scss +++ b/ui/components/app/nft-default-image/index.scss @@ -1,22 +1,17 @@ .nft-default { - background-color: var(--color-background-alternative); - padding-top: 100%; // retains 1:1 aspect ratio + padding-top: 100%; position: relative; - width: 100%; + + &--clickable { + cursor: pointer; + } &__text { overflow: hidden; - text-overflow: ellipsis; - text-align: center; position: absolute; - white-space: nowrap; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 32px); } - - &--clickable { - cursor: pointer; - } } diff --git a/ui/components/app/nft-default-image/nft-default-image.js b/ui/components/app/nft-default-image/nft-default-image.js index ef4cc90cf..ca24c384a 100644 --- a/ui/components/app/nft-default-image/nft-default-image.js +++ b/ui/components/app/nft-default-image/nft-default-image.js @@ -1,26 +1,46 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { TextVariant } from '../../../helpers/constants/design-system'; +import { + Display, + AlignItems, + BlockSize, + JustifyContent, + TextVariant, + BorderRadius, + TextAlign, + BackgroundColor, +} from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Text } from '../../component-library'; +import Box from '../../ui/box/box'; -export default function NftDefaultImage({ name, tokenId, handleImageClick }) { +export default function NftDefaultImage({ name, tokenId, clickable = false }) { const t = useI18nContext(); - const Tag = handleImageClick ? 'button' : 'div'; return ( - - + {name ?? t('unknownCollection')}
#{tokenId}
-
+ ); } @@ -34,7 +54,7 @@ NftDefaultImage.propTypes = { */ tokenId: PropTypes.string, /** - * The click handler for the NFT default image + * Controls the css class for the cursor hover */ - handleImageClick: PropTypes.func, + clickable: PropTypes.bool, }; diff --git a/ui/components/app/nft-default-image/nft-default-image.stories.js b/ui/components/app/nft-default-image/nft-default-image.stories.js index 0d1ba2f12..3f83fcf95 100644 --- a/ui/components/app/nft-default-image/nft-default-image.stories.js +++ b/ui/components/app/nft-default-image/nft-default-image.stories.js @@ -11,14 +11,14 @@ export default { tokenId: { control: 'text', }, - handleImageClick: { - action: 'handleImageClick', + clickable: { + control: 'boolean', }, }, args: { name: null, tokenId: '12345', - handleImageClick: null, + clickable: true, }, }; @@ -29,14 +29,3 @@ export const DefaultStory = (args) => ( ); DefaultStory.storyName = 'Default'; - -export const HandleImageClick = (args) => ( -
- -
-); - -HandleImageClick.args = { - // eslint-disable-next-line no-alert - handleImageClick: () => window.alert('NftDefaultImage clicked!'), -}; diff --git a/ui/components/app/nft-default-image/nft-default-image.test.js b/ui/components/app/nft-default-image/nft-default-image.test.js index a02e8cf76..3077f68de 100644 --- a/ui/components/app/nft-default-image/nft-default-image.test.js +++ b/ui/components/app/nft-default-image/nft-default-image.test.js @@ -1,5 +1,4 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import NftDefaultImage from '.'; @@ -14,7 +13,7 @@ describe('NFT Default Image', () => { const props = { name: 'NFT Name', tokenId: '123', - handleImageClick: jest.fn(), + clickable: true, }; const { container } = renderWithProvider(); @@ -22,7 +21,7 @@ describe('NFT Default Image', () => { expect(container).toMatchSnapshot(); }); - it('should match snapshot with missing image click handler', () => { + it('should match snapshot with missing clickable prop', () => { const props = { name: 'NFT Name', tokenId: '123', @@ -58,18 +57,17 @@ describe('NFT Default Image', () => { expect(nftElement).toBeInTheDocument(); }); - it('should handle image click', () => { - const props = { - handleImageClick: jest.fn(), - }; - - const { queryByTestId } = renderWithProvider( - , + it('does not render component with clickable class when clickable is false', () => { + const { container } = renderWithProvider( + , ); + expect(container.firstChild).not.toHaveClass('nft-default--clickable'); + }); - const nftImageElement = queryByTestId('nft-default-image'); - fireEvent.click(nftImageElement); - - expect(props.handleImageClick).toHaveBeenCalled(); + it('renders component with clickable class when clickable is true', () => { + const { container } = renderWithProvider( + , + ); + expect(container.firstChild).toHaveClass('nft-default--clickable'); }); }); diff --git a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap index 6dc2d4854..67777d09a 100644 --- a/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap +++ b/ui/components/app/nft-details/__snapshots__/nft-details.test.js.snap @@ -40,7 +40,7 @@ exports[`NFT Details should match minimal props and state snapshot 1`] = ` class="box nft-details box--flex-direction-row" >
@@ -178,22 +182,35 @@ export default function NftDetails({ nft }) { } /> -
- - {image ? ( - {nftImageAlt} + {process.env.MULTICHAIN ? ( + + - ) : ( - - )} - + + ) : ( + + {image ? ( + {nftImageAlt} + ) : ( + + )} + + )} -
+
{lastSale ? ( <> diff --git a/ui/components/app/nfts-items/index.scss b/ui/components/app/nfts-items/index.scss index 312ebe7d4..225cd2149 100644 --- a/ui/components/app/nfts-items/index.scss +++ b/ui/components/app/nfts-items/index.scss @@ -46,7 +46,6 @@ padding: 0; &-image { - border-radius: 4px; width: 100%; height: 100%; cursor: pointer; diff --git a/ui/components/app/nfts-items/nfts-items.js b/ui/components/app/nfts-items/nfts-items.js index 03d4d8f7d..75eee0a61 100644 --- a/ui/components/app/nfts-items/nfts-items.js +++ b/ui/components/app/nfts-items/nfts-items.js @@ -5,7 +5,6 @@ import { useHistory } from 'react-router-dom'; import { isEqual } from 'lodash'; import Box from '../../ui/box'; import Typography from '../../ui/typography/typography'; -import Card from '../../ui/card'; import { Color, TypographyVariant, @@ -22,6 +21,7 @@ import { getCurrentChainId, getIpfsGateway, getSelectedAddress, + getCurrentNetwork, } from '../../../selectors'; import { ASSET_ROUTE } from '../../../helpers/constants/routes'; import { getAssetImageURL } from '../../../helpers/utils/util'; @@ -32,6 +32,8 @@ import { getNftsDropdownState } from '../../../ducks/metamask/metamask'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { Icon, IconName } from '../../component-library'; import NftDefaultImage from '../nft-default-image'; +import Card from '../../ui/card/card'; +import { NftItem } from '../../multichain/nft-item'; const width = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP @@ -50,6 +52,7 @@ export default function NftsItems({ const previousCollectionKeys = usePrevious(collectionsKeys); const selectedAddress = useSelector(getSelectedAddress); const chainId = useSelector(getCurrentChainId); + const currentChain = useSelector(getCurrentNetwork); const t = useI18nContext(); useEffect(() => { @@ -173,7 +176,6 @@ export default function NftsItems({ const nftImageAlt = getNftImageAlt(nft); const handleImageClick = () => history.push(`${ASSET_ROUTE}/${address}/${tokenId}`); - return ( - - {nftImage ? ( - + ) : ( + - - ) : ( - - )} - + )} + + )} ); })} diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index c61690c53..98152d1be 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -15,3 +15,4 @@ @import 'network-list-item/'; @import 'network-list-menu/'; @import 'product-tour-popover/product-tour-popover'; +@import 'nft-item/nft-item'; diff --git a/ui/components/multichain/nft-item/index.js b/ui/components/multichain/nft-item/index.js new file mode 100644 index 000000000..91930d246 --- /dev/null +++ b/ui/components/multichain/nft-item/index.js @@ -0,0 +1 @@ +export { NftItem } from './nft-item'; diff --git a/ui/components/multichain/nft-item/nft-item.js b/ui/components/multichain/nft-item/nft-item.js new file mode 100644 index 000000000..f31f99fc9 --- /dev/null +++ b/ui/components/multichain/nft-item/nft-item.js @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import NftDefaultImage from '../../app/nft-default-image/nft-default-image'; +import { + AvatarNetwork, + BadgeWrapper, + BadgeWrapperAnchorElementShape, +} from '../../component-library'; +import { + BackgroundColor, + Display, + JustifyContent, + Size, +} from '../../../helpers/constants/design-system'; +import Box from '../../ui/box/box'; + +export const NftItem = ({ + alt, + name, + src, + networkName, + networkSrc, + tokenId, + onClick, + clickable = false, +}) => { + return ( + + + } + > + {src ? ( + + ) : ( + + )} + + + ); +}; + +NftItem.propTypes = { + src: PropTypes.string, + alt: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + networkName: PropTypes.string.isRequired, + networkSrc: PropTypes.string.isRequired, + tokenId: PropTypes.string.isRequired, + onClick: PropTypes.func, + clickable: PropTypes.bool, +}; diff --git a/ui/components/multichain/nft-item/nft-item.scss b/ui/components/multichain/nft-item/nft-item.scss new file mode 100644 index 000000000..0ca5fded8 --- /dev/null +++ b/ui/components/multichain/nft-item/nft-item.scss @@ -0,0 +1,27 @@ +.nft-item { + &__container { + width: 100%; + height: 100%; + padding: 0; + border-radius: 8px; + cursor: unset; + } + + &__badge-wrapper { + max-width: 100%; + position: relative; + align-self: center; + cursor: unset; + + &__clickable { + cursor: pointer; + } + } + + &__item-image { + border-radius: 8px; + padding: 0; + width: 100%; + height: 100%; + } +} diff --git a/ui/components/multichain/nft-item/nft-item.stories.js b/ui/components/multichain/nft-item/nft-item.stories.js new file mode 100644 index 000000000..564f5dd9c --- /dev/null +++ b/ui/components/multichain/nft-item/nft-item.stories.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { NftItem } from './nft-item'; + +export default { + title: 'Components/Multichain/NftItem', + argTypes: { + alt: { + control: 'text', + }, + name: { + control: 'text', + }, + src: { + control: 'text', + }, + networkName: { + control: 'text', + }, + networkSrc: { + control: 'text', + }, + tokenId: { + control: 'text', + }, + onClick: { + action: 'onClick', + }, + }, + args: { + alt: 'Join Archer and his 6,969 frens as they take a trip further down the rabbit hole in search of a world with vibrant art, great vibes, and psychedelic tales.', + name: 'Monkey Trip #2422', + src: 'https://i.seadn.io/gcs/files/878e670c38e0f02e58bf730c51c30d0c.jpg', + networkName: 'Ethereum Mainnet', + networkSrc: './images/eth_logo.png', + tokenId: '2422', + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/nft-item/nft-item.test.js b/ui/components/multichain/nft-item/nft-item.test.js new file mode 100644 index 000000000..bff4df87d --- /dev/null +++ b/ui/components/multichain/nft-item/nft-item.test.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { NftItem } from '.'; + +describe('NftItem component', () => { + const mockOnClick = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with an image source', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('nft-item')).toBeInTheDocument(); + expect(getByTestId('nft-network-badge')).toBeInTheDocument(); + expect(getByTestId('nft-image')).toBeInTheDocument(); + expect(getByTestId('nft-image')).toHaveAttribute('src', 'test-src'); + }); + + it('renders correctly with default image when no image source is provided', () => { + const { getByTestId, queryByTestId } = render( + , + ); + + expect(getByTestId('nft-item')).toBeInTheDocument(); + expect(getByTestId('nft-network-badge')).toBeInTheDocument(); + expect(queryByTestId('nft-image')).not.toBeInTheDocument(); + expect(getByTestId('nft-default-image')).toBeInTheDocument(); + }); + + it('calls onClick when the NFT image is clicked', () => { + const { getByTestId } = render( + , + ); + + fireEvent.click(getByTestId('nft-image')); + expect(mockOnClick).toHaveBeenCalled(); + }); +});