Files

125 lines
4.1 KiB
TypeScript

import React from 'react';
import { useInView } from 'react-intersection-observer';
import type { TokenInstance } from 'types/api/token';
import { Skeleton } from '@luxfi/ui/skeleton';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import NftFallback from './NftFallback';
import NftHtml from './NftHtml';
import NftImage from './NftImage';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
import NftVideo from './NftVideo';
import useNftMediaInfo from './useNftMediaInfo';
import type { MediaType, Size } from './utils';
import { mediaStyleProps } from './utils';
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'size'> {
data: TokenInstance;
size?: Size;
allowedTypes?: Array<MediaType>;
isLoading?: boolean;
withFullscreen?: boolean;
autoplayVideo?: boolean;
fallback?: React.ReactNode;
}
const NftMedia = ({ data, size = 'original', allowedTypes, isLoading, withFullscreen, autoplayVideo, fallback, ...rest }: Props) => {
const [ isMediaLoading, setIsMediaLoading ] = React.useState(true);
const [ isMediaLoadingError, setIsMediaLoadingError ] = React.useState(false);
const [ mediaInfoIndex, setMediaInfoIndex ] = React.useState(0);
const [ mediaInfoField, setMediaInfoField ] = React.useState<'animation_url' | 'image_url'>('animation_url');
const { ref, inView } = useInView({ triggerOnce: true });
const mediaInfoQuery = useNftMediaInfo({ data, size, allowedTypes, field: mediaInfoField, isEnabled: !isLoading && inView });
React.useEffect(() => {
if (!mediaInfoQuery.isPending && !mediaInfoQuery.data) {
if (mediaInfoField === 'animation_url') {
setMediaInfoField('image_url');
} else {
setIsMediaLoadingError(true);
setIsMediaLoading(false);
}
}
}, [ mediaInfoQuery.isPending, mediaInfoQuery.data, mediaInfoField ]);
const handleMediaLoaded = React.useCallback(() => {
setIsMediaLoading(false);
}, []);
const handleMediaLoadError = React.useCallback(() => {
if (mediaInfoQuery.data) {
if (mediaInfoIndex < mediaInfoQuery.data.length - 1) {
setMediaInfoIndex(mediaInfoIndex + 1);
return;
} else if (mediaInfoField === 'animation_url') {
setMediaInfoField('image_url');
setMediaInfoIndex(0);
return;
}
}
setIsMediaLoading(false);
setIsMediaLoadingError(true);
}, [ mediaInfoField, mediaInfoIndex, mediaInfoQuery.data ]);
const { open, onOpen, onOpenChange } = useDisclosure();
const content = (() => {
if (isLoading) {
return null;
}
if (isMediaLoadingError) {
const styleProps = withFullscreen ? {} : mediaStyleProps;
return fallback ?? <NftFallback { ...styleProps }/>;
}
const mediaInfo = mediaInfoQuery.data?.[mediaInfoIndex];
const props = {
onLoad: handleMediaLoaded,
onError: handleMediaLoadError,
...(withFullscreen ? { onClick: onOpen } : {}),
...(size !== 'sm' ? mediaStyleProps : {}),
};
switch (mediaInfo?.mediaType) {
case 'video': {
return <NftVideo { ...props } src={ mediaInfo.src } transport={ mediaInfo.transport } autoPlay={ autoplayVideo } instance={ data }/>;
}
case 'html':
return <NftHtml { ...props } src={ mediaInfo.src } transport={ mediaInfo.transport }/>;
case 'image': {
return <NftImage { ...props } src={ mediaInfo.src } srcSet={ mediaInfo.srcSet } transport={ mediaInfo.transport }/>;
}
default:
return null;
}
})();
return (
<>
<div className="relative overflow-hidden rounded-md isolate" style={{ aspectRatio: '1/1' }}
ref={ ref }
{ ...rest }
>
<>
<div className="[&>img]:object-contain [&>video]:object-contain">
{ content }
</div>
{ isMediaLoading && <Skeleton loading={ true } className="absolute left-0 top-0 w-full h-full z-[1]"/> }
</>
</div>
{ open && (
<NftMediaFullscreenModal open={ open } onOpenChange={ onOpenChange } data={ data } allowedTypes={ allowedTypes } field={ mediaInfoField }/>
) }
</>
);
};
export default React.memo(NftMedia);