mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Add fallback image/card for NFTs when image was not fetched correctly or does not exist (#15034)
* add fallback image/card for collectibles when image was not fetched correctly or does not exist * UI and storybook updates (#15071) * UI and storybook updates * Adding break so token id is displayed * subtle border fix * Updating content * Removing unused image * Adding proptype descriptions * Lint fix Co-authored-by: George Marshall <george.marshall@consensys.net>
This commit is contained in:
parent
afb3475d17
commit
3a31326199
3
app/_locales/en/messages.json
generated
3
app/_locales/en/messages.json
generated
@ -3865,6 +3865,9 @@
|
|||||||
"unknownCameraErrorTitle": {
|
"unknownCameraErrorTitle": {
|
||||||
"message": "Ooops! Something went wrong...."
|
"message": "Ooops! Something went wrong...."
|
||||||
},
|
},
|
||||||
|
"unknownCollection": {
|
||||||
|
"message": "Unnamed collection"
|
||||||
|
},
|
||||||
"unknownNetwork": {
|
"unknownNetwork": {
|
||||||
"message": "Unknown Private Network"
|
"message": "Unknown Private Network"
|
||||||
},
|
},
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
@import 'collectibles-items/index';
|
@import 'collectibles-items/index';
|
||||||
@import 'collectibles-tab/index';
|
@import 'collectibles-tab/index';
|
||||||
@import 'collectible-details/index';
|
@import 'collectible-details/index';
|
||||||
|
@import 'collectible-default-image/index';
|
||||||
@import 'collectible-options/index';
|
@import 'collectible-options/index';
|
||||||
@import 'collectibles-detection-notice/index';
|
@import 'collectibles-detection-notice/index';
|
||||||
@import 'connected-accounts-list/index';
|
@import 'connected-accounts-list/index';
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Typography from '../../ui/typography';
|
||||||
|
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
|
||||||
|
export default function CollectibleDefaultImage({
|
||||||
|
name,
|
||||||
|
tokenId,
|
||||||
|
handleImageClick,
|
||||||
|
}) {
|
||||||
|
const t = useI18nContext();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames('collectible-default', {
|
||||||
|
'collectible-default--clickable': handleImageClick,
|
||||||
|
})}
|
||||||
|
onClick={handleImageClick}
|
||||||
|
>
|
||||||
|
<Typography variant={TYPOGRAPHY.H6} className="collectible-default__text">
|
||||||
|
{name ?? t('unknownCollection')} <br /> #{tokenId}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectibleDefaultImage.propTypes = {
|
||||||
|
/**
|
||||||
|
* The name of the collectible collection if not supplied will default to "Unnamed collection"
|
||||||
|
*/
|
||||||
|
name: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* The token id of the collectible
|
||||||
|
*/
|
||||||
|
tokenId: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* The click handler for the collectible default image
|
||||||
|
*/
|
||||||
|
handleImageClick: PropTypes.func,
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CollectibleDefaultImage from '.';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/App/CollectibleDefaultImage',
|
||||||
|
id: __filename,
|
||||||
|
argTypes: {
|
||||||
|
name: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
tokenId: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
handleImageClick: {
|
||||||
|
action: 'handleImageClick',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
name: null,
|
||||||
|
tokenId: '12345',
|
||||||
|
handleImageClick: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => (
|
||||||
|
<div style={{ width: 200, height: 200 }}>
|
||||||
|
<CollectibleDefaultImage {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
||||||
|
|
||||||
|
export const handleImageClick = (args) => (
|
||||||
|
<div style={{ width: 200, height: 200 }}>
|
||||||
|
<CollectibleDefaultImage {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
handleImageClick.args = {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
handleImageClick: () => window.alert('CollectibleDefaultImage clicked!'),
|
||||||
|
};
|
1
ui/components/app/collectible-default-image/index.js
Normal file
1
ui/components/app/collectible-default-image/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './collectible-default-image';
|
22
ui/components/app/collectible-default-image/index.scss
Normal file
22
ui/components/app/collectible-default-image/index.scss
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.collectible-default {
|
||||||
|
background-color: var(--color-background-alternative);
|
||||||
|
padding-top: 100%; // retains 1:1 aspect ratio
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,7 @@ import { usePrevious } from '../../../hooks/usePrevious';
|
|||||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||||
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
||||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||||
|
import CollectibleDefaultImage from '../collectible-default-image';
|
||||||
|
|
||||||
export default function CollectibleDetails({ collectible }) {
|
export default function CollectibleDetails({ collectible }) {
|
||||||
const {
|
const {
|
||||||
@ -176,7 +177,11 @@ export default function CollectibleDetails({ collectible }) {
|
|||||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||||
className="collectible-details__card"
|
className="collectible-details__card"
|
||||||
>
|
>
|
||||||
<img className="collectible-details__image" src={image} />
|
{image ? (
|
||||||
|
<img className="collectible-details__image" src={image} />
|
||||||
|
) : (
|
||||||
|
<CollectibleDefaultImage name={name} tokenId={tokenId} />
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
<Box
|
<Box
|
||||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
@ -215,6 +220,7 @@ export default function CollectibleDetails({ collectible }) {
|
|||||||
<Typography
|
<Typography
|
||||||
color={COLORS.TEXT_ALTERNATIVE}
|
color={COLORS.TEXT_ALTERNATIVE}
|
||||||
variant={TYPOGRAPHY.H6}
|
variant={TYPOGRAPHY.H6}
|
||||||
|
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CollectibleDetails from './collectible-details';
|
import CollectibleDetails from './collectible-details';
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/App/CollectiblesDetail',
|
|
||||||
id: __filename,
|
|
||||||
argTypes: {
|
|
||||||
collectible: {
|
|
||||||
control: 'object',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const collectible = {
|
const collectible = {
|
||||||
name: 'Catnip Spicywright',
|
name: 'Catnip Spicywright',
|
||||||
tokenId: '1124157',
|
tokenId: '1124157',
|
||||||
@ -20,12 +10,32 @@ const collectible = {
|
|||||||
"Good day. My name is Catnip Spicywight, which got me teased a lot in high school. If I want to put low fat mayo all over my hamburgers, I shouldn't have to answer to anyone about it, am I right? One time I beat Arlene in an arm wrestle.",
|
"Good day. My name is Catnip Spicywight, which got me teased a lot in high school. If I want to put low fat mayo all over my hamburgers, I shouldn't have to answer to anyone about it, am I right? One time I beat Arlene in an arm wrestle.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultStory = () => {
|
export default {
|
||||||
return <CollectibleDetails collectible={collectible} />;
|
title: 'Components/App/CollectiblesDetail',
|
||||||
|
id: __filename,
|
||||||
|
argTypes: {
|
||||||
|
collectible: {
|
||||||
|
control: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
collectible,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => {
|
||||||
|
return <CollectibleDetails {...args} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
DefaultStory.storyName = 'Default';
|
DefaultStory.storyName = 'Default';
|
||||||
|
|
||||||
DefaultStory.args = {
|
export const NoImage = (args) => {
|
||||||
collectible,
|
return <CollectibleDetails {...args} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
NoImage.args = {
|
||||||
|
collectible: {
|
||||||
|
...collectible,
|
||||||
|
image: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -28,6 +28,8 @@ import { getAssetImageURL } from '../../../helpers/utils/util';
|
|||||||
import { updateCollectibleDropDownState } from '../../../store/actions';
|
import { updateCollectibleDropDownState } from '../../../store/actions';
|
||||||
import { usePrevious } from '../../../hooks/usePrevious';
|
import { usePrevious } from '../../../hooks/usePrevious';
|
||||||
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
|
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import CollectibleDefaultImage from '../collectible-default-image';
|
||||||
|
|
||||||
const width =
|
const width =
|
||||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||||
@ -46,6 +48,7 @@ export default function CollectiblesItems({
|
|||||||
const previousCollectionKeys = usePrevious(collectionsKeys);
|
const previousCollectionKeys = usePrevious(collectionsKeys);
|
||||||
const selectedAddress = useSelector(getSelectedAddress);
|
const selectedAddress = useSelector(getSelectedAddress);
|
||||||
const chainId = useSelector(getCurrentChainId);
|
const chainId = useSelector(getCurrentChainId);
|
||||||
|
const t = useI18nContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -101,7 +104,7 @@ export default function CollectiblesItems({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="collectibles-items__collection-image-alt">
|
<div className="collectibles-items__collection-image-alt">
|
||||||
{collectionName[0]}
|
{collectionName?.[0]?.toUpperCase() ?? null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -164,7 +167,9 @@ export default function CollectiblesItems({
|
|||||||
variant={TYPOGRAPHY.H5}
|
variant={TYPOGRAPHY.H5}
|
||||||
margin={[0, 0, 0, 2]}
|
margin={[0, 0, 0, 2]}
|
||||||
>
|
>
|
||||||
{`${collectionName} (${collectibles.length})`}
|
{`${collectionName ?? t('unknownCollection')} (${
|
||||||
|
collectibles.length
|
||||||
|
})`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
||||||
@ -180,29 +185,48 @@ export default function CollectiblesItems({
|
|||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
|
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
|
||||||
{collectibles.map((collectible, i) => {
|
{collectibles.map((collectible, i) => {
|
||||||
const { image, address, tokenId, backgroundColor } = collectible;
|
const {
|
||||||
|
image,
|
||||||
|
address,
|
||||||
|
tokenId,
|
||||||
|
backgroundColor,
|
||||||
|
name,
|
||||||
|
} = collectible;
|
||||||
const collectibleImage = getAssetImageURL(image, ipfsGateway);
|
const collectibleImage = getAssetImageURL(image, ipfsGateway);
|
||||||
|
const handleImageClick = () =>
|
||||||
|
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
width={width}
|
width={width}
|
||||||
key={`collectible-${i}`}
|
key={`collectible-${i}`}
|
||||||
className="collectibles-items__collection-item-wrapper"
|
className="collectibles-items__item-wrapper"
|
||||||
>
|
>
|
||||||
<Card padding={0} justifyContent={JUSTIFY_CONTENT.CENTER}>
|
<Card
|
||||||
<div
|
padding={0}
|
||||||
className="collectibles-items__collection-item"
|
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||||
style={{
|
className="collectibles-items__item-wrapper__card"
|
||||||
backgroundColor,
|
>
|
||||||
}}
|
{collectibleImage ? (
|
||||||
>
|
<div
|
||||||
<img
|
className="collectibles-items__item"
|
||||||
onClick={() =>
|
style={{
|
||||||
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
|
backgroundColor,
|
||||||
}
|
}}
|
||||||
className="collectibles-items__collection-item-image"
|
>
|
||||||
src={collectibleImage}
|
<img
|
||||||
|
onClick={handleImageClick}
|
||||||
|
className="collectibles-items__item-image"
|
||||||
|
src={collectibleImage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CollectibleDefaultImage
|
||||||
|
name={name}
|
||||||
|
tokenId={tokenId}
|
||||||
|
handleImageClick={handleImageClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -27,29 +27,33 @@
|
|||||||
color: var(--color-overlay-inverse);
|
color: var(--color-overlay-inverse);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-item-wrapper {
|
&__item-wrapper {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-item {
|
&__item {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
|
||||||
|
|
||||||
&-item-image {
|
&-image {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__icon-chevron {
|
&__icon-chevron {
|
||||||
color: var(--color-icon-default);
|
color: var(--color-icon-default);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user