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": {
|
||||
"message": "Ooops! Something went wrong...."
|
||||
},
|
||||
"unknownCollection": {
|
||||
"message": "Unnamed collection"
|
||||
},
|
||||
"unknownNetwork": {
|
||||
"message": "Unknown Private Network"
|
||||
},
|
||||
|
@ -14,6 +14,7 @@
|
||||
@import 'collectibles-items/index';
|
||||
@import 'collectibles-tab/index';
|
||||
@import 'collectible-details/index';
|
||||
@import 'collectible-default-image/index';
|
||||
@import 'collectible-options/index';
|
||||
@import 'collectibles-detection-notice/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 { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||
import CollectibleDefaultImage from '../collectible-default-image';
|
||||
|
||||
export default function CollectibleDetails({ collectible }) {
|
||||
const {
|
||||
@ -176,7 +177,11 @@ export default function CollectibleDetails({ collectible }) {
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
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>
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
@ -215,6 +220,7 @@ export default function CollectibleDetails({ collectible }) {
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{description}
|
||||
|
@ -1,16 +1,6 @@
|
||||
import React from 'react';
|
||||
import CollectibleDetails from './collectible-details';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/CollectiblesDetail',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
collectible: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const collectible = {
|
||||
name: 'Catnip Spicywright',
|
||||
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.",
|
||||
};
|
||||
|
||||
export const DefaultStory = () => {
|
||||
return <CollectibleDetails collectible={collectible} />;
|
||||
export default {
|
||||
title: 'Components/App/CollectiblesDetail',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
collectible: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
collectible,
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => {
|
||||
return <CollectibleDetails {...args} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
collectible,
|
||||
export const NoImage = (args) => {
|
||||
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 { usePrevious } from '../../../hooks/usePrevious';
|
||||
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import CollectibleDefaultImage from '../collectible-default-image';
|
||||
|
||||
const width =
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
@ -46,6 +48,7 @@ export default function CollectiblesItems({
|
||||
const previousCollectionKeys = usePrevious(collectionsKeys);
|
||||
const selectedAddress = useSelector(getSelectedAddress);
|
||||
const chainId = useSelector(getCurrentChainId);
|
||||
const t = useI18nContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -101,7 +104,7 @@ export default function CollectiblesItems({
|
||||
}
|
||||
return (
|
||||
<div className="collectibles-items__collection-image-alt">
|
||||
{collectionName[0]}
|
||||
{collectionName?.[0]?.toUpperCase() ?? null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -164,7 +167,9 @@ export default function CollectiblesItems({
|
||||
variant={TYPOGRAPHY.H5}
|
||||
margin={[0, 0, 0, 2]}
|
||||
>
|
||||
{`${collectionName} (${collectibles.length})`}
|
||||
{`${collectionName ?? t('unknownCollection')} (${
|
||||
collectibles.length
|
||||
})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
||||
@ -180,29 +185,48 @@ export default function CollectiblesItems({
|
||||
{isExpanded ? (
|
||||
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
|
||||
{collectibles.map((collectible, i) => {
|
||||
const { image, address, tokenId, backgroundColor } = collectible;
|
||||
const {
|
||||
image,
|
||||
address,
|
||||
tokenId,
|
||||
backgroundColor,
|
||||
name,
|
||||
} = collectible;
|
||||
const collectibleImage = getAssetImageURL(image, ipfsGateway);
|
||||
const handleImageClick = () =>
|
||||
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`);
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={width}
|
||||
key={`collectible-${i}`}
|
||||
className="collectibles-items__collection-item-wrapper"
|
||||
className="collectibles-items__item-wrapper"
|
||||
>
|
||||
<Card padding={0} justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||
<div
|
||||
className="collectibles-items__collection-item"
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onClick={() =>
|
||||
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
|
||||
}
|
||||
className="collectibles-items__collection-item-image"
|
||||
src={collectibleImage}
|
||||
<Card
|
||||
padding={0}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
className="collectibles-items__item-wrapper__card"
|
||||
>
|
||||
{collectibleImage ? (
|
||||
<div
|
||||
className="collectibles-items__item"
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onClick={handleImageClick}
|
||||
className="collectibles-items__item-image"
|
||||
src={collectibleImage}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CollectibleDefaultImage
|
||||
name={name}
|
||||
tokenId={tokenId}
|
||||
handleImageClick={handleImageClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
|
@ -27,29 +27,33 @@
|
||||
color: var(--color-overlay-inverse);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-item-wrapper {
|
||||
align-self: center;
|
||||
&__item-wrapper {
|
||||
align-self: center;
|
||||
|
||||
&__card {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
}
|
||||
&__item {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
|
||||
&-item-image {
|
||||
&-image {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-chevron {
|
||||
color: var(--color-icon-default);
|
||||
}
|
||||
&__icon-chevron {
|
||||
color: var(--color-icon-default);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user