diff --git a/src/app/components/VideoPreview/index.tsx b/src/app/components/VideoPreview/index.tsx new file mode 100644 index 0000000000..e0b562826d --- /dev/null +++ b/src/app/components/VideoPreview/index.tsx @@ -0,0 +1,120 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' +import Backdrop from '@mui/material/Backdrop' +import Modal from '@mui/material/Modal' +import Fade from '@mui/material/Fade' +import Typography from '@mui/material/Typography' +import CancelIcon from '@mui/icons-material/Cancel' +import PlayCircleFilledWhiteIcon from '@mui/icons-material/PlayCircleFilledWhite' +import IconButton from '@mui/material/IconButton' +import { styled } from '@mui/material/styles' +import { COLORS } from '../../../styles/theme/colors' + +const StyledThumbnailContainer = styled(Box)({ + position: 'relative', + maxWidth: '100%', + maxHeight: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}) + +const StyledThumbnail = styled('video', { + shouldForwardProp: prop => prop !== 'maxThumbnailSize', +})<{ maxThumbnailSize: string }>(({ maxThumbnailSize }) => ({ + maxWidth: maxThumbnailSize, + maxHeight: maxThumbnailSize, +})) + +const PlayButton = styled(IconButton)({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + backgroundColor: 'transparent', + color: COLORS.white, + fontSize: 'inherit', + '& .MuiSvgIcon-root': { + fontSize: '48px', + }, +}) + +const StyledVideo = styled('video')({ + maxWidth: '80dvw', + maxHeight: '80dvh', +}) + +type VideoPreviewProps = { + handlePreviewClose: () => void + handlePreviewOpen: () => void + onError: () => void + previewOpen: boolean + src: string + title: string | undefined + maxThumbnailSize: string +} + +export const VideoPreview: FC = ({ + handlePreviewClose, + handlePreviewOpen, + previewOpen, + onError, + src, + title, + maxThumbnailSize, +}) => { + const { t } = useTranslation() + const label = title || t('nft.videoPreview') + + return ( + <> + + + + + + + + + + + {title && ( + + {title} + + )} + + + + + + + + + + ) +} diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceImageCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceImageCard.tsx index e3ff13407c..5a0477ab76 100644 --- a/src/app/pages/NFTInstanceDashboardPage/InstanceImageCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/InstanceImageCard.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' import { Button } from '@mui/base/Button' @@ -15,7 +15,9 @@ import { processNftImageUrl } from '../../utils/nft-images' import { hasValidProtocol } from '../../utils/url' import { COLORS } from '../../../styles/theme/colors' import { ImagePreview } from '../../components/ImagePreview' +import { VideoPreview } from 'app/components/VideoPreview' import { NoPreview } from '../../components/NoPreview' +import { checkContentType } from 'app/utils/ipfs' const imageSize = '350px' @@ -81,6 +83,21 @@ export const InstanceImageCard: FC = ({ isFetched, isLoa const handlePreviewOpen = () => setPreviewOpen(true) const handlePreviewClose = () => setPreviewOpen(false) const [imageLoadError, setImageLoadError] = useState(false) + const [contentType, setContentType] = useState('') + + useEffect(() => { + async function fetchContentType() { + try { + const url = processNftImageUrl(nft?.image) + const type = await checkContentType(url) + setContentType(type) + } catch (error) { + console.error('Error fetching content type:', error) + } + } + + fetchContentType() + }, [nft?.image]) return ( = ({ isFetched, isLoa alignItems: 'center', }} > - setImageLoadError(true)} - title={nft.name} - maxThumbnailSize={imageSize} - /> + {contentType === 'image' && ( + setImageLoadError(true)} + title={nft.name} + maxThumbnailSize={imageSize} + /> + )} + {contentType === 'video' && ( + setImageLoadError(true)} + title={nft.name} + maxThumbnailSize={imageSize} + /> + )} prop !== 'isMobile', +})<{ isMobile: boolean }>(({ isMobile }) => ({ + minWidth: isMobile ? minMobileSize : minSize, + minHeight: isMobile ? minMobileSize : minSize, + width: '100%', + height: '100%', + maxHeight: minSize, + objectFit: 'cover', + transition: 'opacity 250ms ease-in-out', + '&:hover, &:focus-visible': { + opacity: 0.15, + }, +})) + const StyledBox = styled(Box)(() => ({ position: 'absolute', display: 'flex', @@ -59,17 +75,41 @@ export const ImageListItemImage: FC = ({ instance, to } const { t } = useTranslation() const { isMobile } = useScreenSize() const [imageLoadError, setImageLoadError] = useState(false) + const [contentType, setContentType] = useState('') + + useEffect(() => { + async function fetchContentType() { + try { + const url = processNftImageUrl(instance?.image) + const type = await checkContentType(url) + setContentType(type) + } catch (error) { + console.error('Error fetching content type:', error) + } + } + + fetchContentType() + }, [instance?.image]) return ( {hasValidProtocol(instance.image) && !imageLoadError ? ( - setImageLoadError(true)} - src={processNftImageUrl(instance.image)} - alt={getNftInstanceLabel(instance)} - loading="lazy" - isMobile={isMobile} - /> + (contentType === 'image' && ( + setImageLoadError(true)} + src={processNftImageUrl(instance.image)} + alt={getNftInstanceLabel(instance)} + loading="lazy" + isMobile={isMobile} + /> + )) || + (contentType === 'video' && ( + setImageLoadError(true)} + src={processNftImageUrl(instance.image)} + isMobile={isMobile} + /> + )) ) : ( )} diff --git a/src/app/utils/ipfs.ts b/src/app/utils/ipfs.ts index 1b489523e1..9d59792a4d 100644 --- a/src/app/utils/ipfs.ts +++ b/src/app/utils/ipfs.ts @@ -1,3 +1,4 @@ +import axios from 'axios' import { ipfs } from './externalLinks' export const ipfsUrlPrefix = 'ipfs://' @@ -12,3 +13,24 @@ export const accessIpfsUrl = (url: string): string => { } return `${ipfs.proxyPrefix}${url.substring(ipfsUrlPrefix.length)}` } + +/** + * Return content-type of the asset uploaded on the provided link + */ +export const checkContentType = async (url: string) => { + try { + const response = await axios.head(url); + const contentType = response.headers['content-type']; + + if (contentType && contentType.startsWith('image')) { + return 'image'; + } else if (contentType && contentType.startsWith('video')) { + return 'video'; + } else { + return 'unknown'; + } + } catch (error) { + console.error("Error fetching NFT resource:", error); + return 'error'; + } +} \ No newline at end of file diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 03be0ad937..4ef0a5c989 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -216,6 +216,7 @@ "instanceTokenId": "Token ID", "instanceTitleSuffix": "(NFT Instance)", "imagePreview": "Image preview", + "videoPreview": "Video preview", "metadata": "Metadata", "noMetadata": "There is no metadata on record for this NFT.", "noPreview": "No preview available",