Commit 13aa27c5 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #707 from blockscout/metadata-fixes

NFT page fixes
parents 6374c3c3 7f2e8c0b
import type { ResponsiveValue } from '@chakra-ui/react';
import { AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { Box, AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { Property } from 'csstype';
import React from 'react';
......@@ -30,6 +30,7 @@ const Fallback = ({ className, padding }: FallbackProps) => {
const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const [ isError, setIsError ] = React.useState(false);
const handleLoad = React.useCallback(() => {
setIsLoading(false);
......@@ -37,10 +38,37 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const handleLoadError = React.useCallback(() => {
setIsLoading(false);
setIsError(true);
}, []);
const _objectFit = objectFit || 'contain';
const content = (() => {
// as of ChakraUI v2.5.3
// fallback prop of Image component doesn't work well with loading prop lazy strategy
// so we have to render fallback and loader manually
if (isError || !url) {
return <Fallback className={ className } padding={ fallbackPadding }/>;
}
return (
<Box>
{ isLoading && <Skeleton position="absolute" left={ 0 } top={ 0 } w="100%" h="100%" zIndex="1"/> }
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url }
opacity={ isLoading ? 0 : 1 }
alt="Token instance image"
onError={ handleLoadError }
onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/>
</Box>
);
})();
return (
<AspectRatio
className={ className }
......@@ -54,17 +82,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
},
}}
>
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url || undefined }
alt="Token instance image"
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError }
onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/>
{ content }
</AspectRatio>
);
};
......
......@@ -93,14 +93,12 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
/>
</Flex>
<Grid
mt={ 8 }
mt={ 5 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
overflow="hidden"
>
{ divider }
<DetailsSponsoredItem/>
{ hasMetadata && (
<>
{ divider }
......@@ -148,6 +146,8 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
) }
</>
) }
{ divider }
<DetailsSponsoredItem/>
</Grid>
</>
);
......
......@@ -4,6 +4,7 @@ import React from 'react';
import MetadataItemArray from './MetadataItemArray';
import MetadataItemObject from './MetadataItemObject';
import MetadataItemPrimitive from './MetadataItemPrimitive';
import { sortFields } from './utils';
interface Props {
data: Record<string, unknown>;
......@@ -30,32 +31,32 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => {
case 'string':
case 'number':
case 'boolean': {
return <MetadataItemPrimitive name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
}
case 'object': {
if (value === null) {
return <MetadataItemPrimitive name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
}
if (Array.isArray(value) && value.length > 0) {
return <MetadataItemArray name={ name } value={ value } level={ level }/>;
return <MetadataItemArray key={ name } name={ name } value={ value } level={ level }/>;
}
if (Object.keys(value).length > 0) {
return <MetadataItemObject name={ name } value={ value as Record<string, unknown> } level={ level }/>;
return <MetadataItemObject key={ name } name={ name } value={ value as Record<string, unknown> } level={ level }/>;
}
}
// eslint-disable-next-line no-fallthrough
default: {
return <MetadataItemPrimitive name={ name } value={ String(value) } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ String(value) } isFlat={ isFlat } level={ level }/>;
}
}
}, [ level, isFlat ]);
return (
<Accordion allowMultiple fontSize="sm" ml={{ base: level === 0 ? 0 : 6, lg: `${ ml }px` }} defaultIndex={ level === 0 ? [ 0 ] : undefined }>
{ Object.entries(data).map(([ key, value ]) => renderItem(key, value)) }
{ Object.entries(data).sort(sortFields).map(([ key, value ]) => renderItem(key, value)) }
</Accordion>
);
};
......
......@@ -7,3 +7,24 @@ export function formatName(_name: string) {
return _upperFirst(name.trim());
}
const PINNED_FIELDS = [ 'name', 'description' ];
export function sortFields([ nameA ]: [string, unknown], [ nameB ]: [string, unknown]): number {
const pinnedIndexA = PINNED_FIELDS.indexOf(nameA.toLowerCase());
const pinnedIndexB = PINNED_FIELDS.indexOf(nameB.toLowerCase());
if (pinnedIndexA === -1 && pinnedIndexB === -1) {
return 0;
}
if (pinnedIndexB === -1) {
return -1;
}
if (pinnedIndexA === -1) {
return 1;
}
return pinnedIndexA > pinnedIndexB ? 1 : -1;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment