Commit 1d870916 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #1127 from blockscout/nft-and-qr-modals

Nft and qr modals
parents a4dfabcf 85b0c40c
...@@ -72,6 +72,14 @@ const baseStyle = definePartsStyle((props) => ({ ...@@ -72,6 +72,14 @@ const baseStyle = definePartsStyle((props) => ({
})); }));
const sizes = { const sizes = {
sm: definePartsStyle({
dialogContainer: {
height: '100%',
},
dialog: {
maxW: '536px',
},
}),
md: definePartsStyle({ md: definePartsStyle({
dialogContainer: { dialogContainer: {
height: '100%', height: '100%',
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as addressMock from 'mocks/address/address';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import AddressQrCode from './AddressQrCode'; import AddressQrCode from './AddressQrCode';
...@@ -8,7 +9,7 @@ import AddressQrCode from './AddressQrCode'; ...@@ -8,7 +9,7 @@ import AddressQrCode from './AddressQrCode';
test('default view +@mobile +@dark-mode', async({ mount, page }) => { test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<AddressQrCode hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29"/> <AddressQrCode address={ addressMock.withoutName }/>
</TestApp>, </TestApp>,
); );
await page.getByRole('button', { name: /qr code/i }).click(); await page.getByRole('button', { name: /qr code/i }).click();
......
...@@ -6,7 +6,9 @@ import { ...@@ -6,7 +6,9 @@ import {
ModalBody, ModalBody,
ModalContent, ModalContent,
ModalCloseButton, ModalCloseButton,
ModalHeader,
ModalOverlay, ModalOverlay,
LightMode,
Box, Box,
useDisclosure, useDisclosure,
Tooltip, Tooltip,
...@@ -18,10 +20,12 @@ import { useRouter } from 'next/router'; ...@@ -18,10 +20,12 @@ import { useRouter } from 'next/router';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import React from 'react'; import React from 'react';
import type { Address as AddressType } from 'types/api/address';
import qrCodeIcon from 'icons/qr_code.svg'; import qrCodeIcon from 'icons/qr_code.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getPageType from 'lib/mixpanel/getPageType'; import getPageType from 'lib/mixpanel/getPageType';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
const SVG_OPTIONS = { const SVG_OPTIONS = {
margin: 0, margin: 0,
...@@ -29,13 +33,13 @@ const SVG_OPTIONS = { ...@@ -29,13 +33,13 @@ const SVG_OPTIONS = {
interface Props { interface Props {
className?: string; className?: string;
hash: string; address: AddressType;
isLoading?: boolean; isLoading?: boolean;
} }
const AddressQrCode = ({ hash, className, isLoading }: Props) => { const AddressQrCode = ({ address, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const [ qr, setQr ] = React.useState(''); const [ qr, setQr ] = React.useState('');
...@@ -45,7 +49,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => { ...@@ -45,7 +49,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
React.useEffect(() => { React.useEffect(() => {
if (isOpen) { if (isOpen) {
QRCode.toString(hash, SVG_OPTIONS, (error: Error | null | undefined, svg: string) => { QRCode.toString(address.hash, SVG_OPTIONS, (error: Error | null | undefined, svg: string) => {
if (error) { if (error) {
setError('We were unable to generate QR code.'); setError('We were unable to generate QR code.');
Sentry.captureException(error, { tags: { source: 'qr_code' } }); Sentry.captureException(error, { tags: { source: 'qr_code' } });
...@@ -57,7 +61,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => { ...@@ -57,7 +61,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
mixpanel.logEvent(mixpanel.EventTypes.QR_CODE, { 'Page type': pageType }); mixpanel.logEvent(mixpanel.EventTypes.QR_CODE, { 'Page type': pageType });
}); });
} }
}, [ hash, isOpen, onClose, pageType ]); }, [ address.hash, isOpen, onClose, pageType ]);
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>; return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
...@@ -77,15 +81,38 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => { ...@@ -77,15 +81,38 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
icon={ <Icon as={ qrCodeIcon } boxSize={ 5 }/> } icon={ <Icon as={ qrCodeIcon } boxSize={ 5 }/> }
/> />
</Tooltip> </Tooltip>
<Modal isOpen={ isOpen } onClose={ onClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/> { error && (
<ModalContent bgColor={ error ? undefined : 'white' }> <Modal isOpen={ isOpen } onClose={ onClose } size={{ base: 'full', lg: 'sm' }}>
{ isMobile && <ModalCloseButton/> } <ModalOverlay/>
<ModalBody mb={ 0 }> <ModalContent>
{ error ? <Alert status="warning">{ error }</Alert> : <Box dangerouslySetInnerHTML={{ __html: qr }}/> } <ModalBody mb={ 0 }>
</ModalBody> <Alert status="warning">{ error }</Alert>
</ModalContent> </ModalBody>
</Modal> </ModalContent>
</Modal>
) }
{ !error && (
<LightMode>
<Modal isOpen={ isOpen } onClose={ onClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Address QR code</ModalHeader>
<ModalCloseButton/>
<ModalBody mb={ 0 }>
<AddressEntity
mb={ 3 }
fontWeight={ 500 }
color="text"
address={ address }
noLink
/>
<Box p={ 4 } dangerouslySetInnerHTML={{ __html: qr }}/>
</ModalBody>
</ModalContent>
</Modal>
</LightMode>
) }
</> </>
); );
}; };
......
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Link, Text, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
...@@ -12,27 +12,26 @@ import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; ...@@ -12,27 +12,26 @@ import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance & { isLoading: boolean }; type Props = AddressTokenBalance & { isLoading: boolean };
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => { const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => {
const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } }); const tokenInstanceLink = tokenId ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) : undefined;
return ( return (
<LinkBox <Box
w={{ base: '100%', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
border="1px solid" border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px" borderRadius="12px"
p="10px" p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm" fontSize="sm"
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
<LinkOverlay href={ isLoading ? undefined : tokenLink }> <Link href={ isLoading ? undefined : tokenInstanceLink }>
<NftMedia <NftMedia
mb="18px" mb="18px"
url={ tokenInstance?.animation_url || tokenInstance?.image_url || null } url={ tokenInstance?.animation_url || tokenInstance?.image_url || null }
isLoading={ isLoading } isLoading={ isLoading }
/> />
</LinkOverlay> </Link>
{ tokenId && ( { tokenId && (
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
...@@ -44,7 +43,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo ...@@ -44,7 +43,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
overflow="hidden" overflow="hidden"
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) } href={ tokenInstanceLink }
> >
{ tokenId } { tokenId }
</Link> </Link>
...@@ -58,7 +57,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo ...@@ -58,7 +57,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
noCopy noCopy
noSymbol noSymbol
/> />
</LinkBox> </Box>
); );
}; };
......
...@@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props ...@@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
{ !isLoading && !address.is_contract && config.features.account.isEnabled && ( { !isLoading && !address.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/> <AddressQrCode address={ address } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
{ config.features.account.isEnabled && <AddressActionsMenu isLoading={ isLoading }/> } { config.features.account.isEnabled && <AddressActionsMenu isLoading={ isLoading }/> }
</Flex> </Flex>
); );
......
import { Icon, useColorModeValue } from '@chakra-ui/react'; import { Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import nftIcon from 'icons/nft_shield.svg'; import nftIcon from 'icons/nft_shield.svg';
const NftFallback = () => { const NftFallback = ({ className }: {className?: string}) => {
return ( return (
<Icon <Icon
className={ className }
as={ nftIcon } as={ nftIcon }
p="50px" p="50px"
color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') } color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }
bgColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
/> />
); );
}; };
export default NftFallback; export default chakra(NftFallback);
import { chakra } from '@chakra-ui/react'; import { chakra, LinkOverlay } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { mediaStyleProps } from './utils';
interface Props { interface Props {
src: string; src: string;
onLoad: () => void; onLoad: () => void;
onError: () => void; onError: () => void;
onClick?: () => void;
} }
const NftHtml = ({ src, onLoad, onError }: Props) => { const NftHtml = ({ src, onLoad, onError, onClick }: Props) => {
return ( return (
<chakra.iframe <LinkOverlay
src={ src } onClick={ onClick }
sandbox="allow-scripts" { ...mediaStyleProps }
onLoad={ onLoad } >
onError={ onError } <chakra.iframe
/> src={ src }
h="100%"
w="100%"
sandbox="allow-scripts"
onLoad={ onLoad }
onError={ onError }
/>
</LinkOverlay>
); );
}; };
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
isOpen: boolean;
onClose: () => void;
}
const NftHtmlWithFullscreen = ({ src, isOpen, onClose }: Props) => {
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.iframe
w="90vw"
h="90vh"
src={ src }
sandbox="allow-scripts"
/>
</NftMediaFullscreenModal>
);
};
export default NftHtmlWithFullscreen;
import { chakra, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import NftHtml from './NftHtml';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftHtmlWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftHtml src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.iframe
w="90vw"
h="90vh"
src={ src }
sandbox="allow-scripts"
onLoad={ onLoad }
onError={ onError }
/>
</NftMediaFullscreenModal>
</>
);
};
export default NftHtmlWithFullscreen;
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { mediaStyleProps } from './utils';
interface Props { interface Props {
url: string; src: string;
onLoad: () => void; onLoad: () => void;
onError: () => void; onError: () => void;
onClick?: () => void;
} }
const NftImage = ({ url, onLoad, onError }: Props) => { const NftImage = ({ src, onLoad, onError, onClick }: Props) => {
return ( return (
<Image <Image
w="100%" w="100%"
h="100%" h="100%"
src={ url } src={ src }
alt="Token instance image" alt="Token instance image"
onError={ onError } onError={ onError }
onLoad={ onLoad } onLoad={ onLoad }
onClick={ onClick }
{ ...mediaStyleProps }
/> />
); );
}; };
......
import {
Image,
} from '@chakra-ui/react';
import React from 'react';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
isOpen: boolean;
onClose: () => void;
}
const NftImageWithFullscreen = ({ src, isOpen, onClose }: Props) => {
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<Image src={ src } alt="Token instance image" maxH="90vh" maxW="90vw"/>
</NftMediaFullscreenModal>
);
};
export default NftImageWithFullscreen;
import {
Image,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import NftImage from './NftImage';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftImageWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftImage src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<Image src={ src } alt="Token instance image" maxH="90vh" maxW="90vw"/>
</NftMediaFullscreenModal>
</>
);
};
export default NftImageWithFullscreen;
...@@ -5,20 +5,47 @@ import TestApp from 'playwright/TestApp'; ...@@ -5,20 +5,47 @@ import TestApp from 'playwright/TestApp';
import NftMedia from './NftMedia'; import NftMedia from './NftMedia';
test.use({ viewport: { width: 250, height: 250 } }); test.describe('no url', () => {
test.use({ viewport: { width: 250, height: 250 } });
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ null }/>
</TestApp>,
);
test('no url +@dark-mode', async({ mount }) => { await expect(component).toHaveScreenshot();
const component = await mount( });
<TestApp> });
<NftMedia url={ null }/>
</TestApp>, test.describe('image', () => {
); test.use({ viewport: { width: 250, height: 250 } });
const MEDIA_URL = 'https://localhost:3000/my-image.jpg';
await expect(component).toHaveScreenshot(); test.beforeEach(async({ page }) => {
await page.route(MEDIA_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
});
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
}); });
test('image +@dark-mode', async({ mount, page }) => { test('image preview hover', async({ mount, page }) => {
const MEDIA_URL = 'https://localhost:3000/my-image.jpg'; const MEDIA_URL = 'https://localhost:3000/my-image.jpg';
await page.route(MEDIA_URL, (route) => { await page.route(MEDIA_URL, (route) => {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
...@@ -28,33 +55,62 @@ test('image +@dark-mode', async({ mount, page }) => { ...@@ -28,33 +55,62 @@ test('image +@dark-mode', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<NftMedia url={ MEDIA_URL }/> <NftMedia url={ MEDIA_URL } w="250px"/>
</TestApp>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await component.getByAltText('Token instance image').hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 250 } });
}); });
test('page', async({ mount, page }) => { test('image fullscreen +@dark-mode +@mobile', async({ mount, page }) => {
const MEDIA_URL = 'https://localhost:3000/page.html'; const MEDIA_URL = 'https://localhost:3000/my-image.jpg';
const MEDIA_TYPE_API_URL = `/node-api/media-type?url=${ encodeURIComponent(MEDIA_URL) }`;
await page.route(MEDIA_URL, (route) => { await page.route(MEDIA_URL, (route) => {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
path: './playwright/mocks/page.html', path: './playwright/mocks/image_long.jpg',
}); });
}); });
await page.route(MEDIA_TYPE_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ type: 'html' }),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<NftMedia url={ MEDIA_URL }/> <NftMedia url={ MEDIA_URL } withFullscreen w="250px"/>
</TestApp>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await component.getByAltText('Token instance image').click();
await expect(page).toHaveScreenshot();
});
test.describe('page', () => {
test.use({ viewport: { width: 250, height: 250 } });
const MEDIA_URL = 'https://localhost:3000/page.html';
const MEDIA_TYPE_API_URL = `/node-api/media-type?url=${ encodeURIComponent(MEDIA_URL) }`;
test.beforeEach(async({ page }) => {
await page.route(MEDIA_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/page.html',
});
});
await page.route(MEDIA_TYPE_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ type: 'html' }),
}));
});
test('preview +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<NftMedia url={ MEDIA_URL }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
}); });
import { AspectRatio, chakra, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { AspectRatio, chakra, Skeleton, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import NftFallback from './NftFallback'; import NftFallback from './NftFallback';
import NftHtml from './NftHtml'; import NftHtml from './NftHtml';
import NftHtmlFullscreen from './NftHtmlFullscreen';
import NftImage from './NftImage'; import NftImage from './NftImage';
import NftImageFullscreen from './NftImageFullscreen';
import NftVideo from './NftVideo'; import NftVideo from './NftVideo';
import NftVideoFullscreen from './NftVideoFullscreen';
import useNftMediaType from './useNftMediaType'; import useNftMediaType from './useNftMediaType';
import { mediaStyleProps } from './utils';
interface Props { interface Props {
url: string | null; url: string | null;
className?: string; className?: string;
isLoading?: boolean; isLoading?: boolean;
withFullscreen?: boolean;
} }
const NftMedia = ({ url, className, isLoading }: Props) => { const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => {
const [ isMediaLoading, setIsMediaLoading ] = React.useState(Boolean(url)); const [ isMediaLoading, setIsMediaLoading ] = React.useState(Boolean(url));
const [ isLoadingError, setIsLoadingError ] = React.useState(false); const [ isLoadingError, setIsLoadingError ] = React.useState(false);
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const { ref, inView } = useInView({ triggerOnce: true }); const { ref, inView } = useInView({ triggerOnce: true });
const type = useNftMediaType(url, !isLoading && inView); const type = useNftMediaType(url, !isLoading && inView);
const handleMediaLoaded = React.useCallback(() => { const handleMediaLoaded = React.useCallback(() => {
...@@ -32,18 +36,51 @@ const NftMedia = ({ url, className, isLoading }: Props) => { ...@@ -32,18 +36,51 @@ const NftMedia = ({ url, className, isLoading }: Props) => {
setIsLoadingError(true); setIsLoadingError(true);
}, []); }, []);
const { isOpen, onOpen, onClose } = useDisclosure();
const content = (() => { const content = (() => {
if (!url || isLoadingError) { if (!url || isLoadingError) {
return <NftFallback/>; const styleProps = withFullscreen ? {} : mediaStyleProps;
return <NftFallback { ...styleProps }/>;
}
const props = {
src: url,
onLoad: handleMediaLoaded,
onError: handleMediaLoadError,
...(withFullscreen ? { onClick: onOpen } : {}),
};
switch (type) {
case 'video':
return <NftVideo { ...props }/>;
case 'html':
return <NftHtml { ...props }/>;
case 'image':
return <NftImage { ...props }/>;
default:
return null;
} }
})();
const modal = (() => {
if (!url || !withFullscreen) {
return null;
}
const props = {
src: url,
isOpen,
onClose,
};
switch (type) { switch (type) {
case 'video': case 'video':
return <NftVideo src={ url } onLoad={ handleMediaLoaded } onError={ handleMediaLoadError }/>; return <NftVideoFullscreen { ...props }/>;
case 'html': case 'html':
return <NftHtml src={ url } onLoad={ handleMediaLoaded } onError={ handleMediaLoadError }/>; return <NftHtmlFullscreen { ...props }/>;
case 'image': case 'image':
return <NftImage url={ url } onLoad={ handleMediaLoaded } onError={ handleMediaLoadError }/>; return <NftImageFullscreen { ...props }/>;
default: default:
return null; return null;
} }
...@@ -53,11 +90,11 @@ const NftMedia = ({ url, className, isLoading }: Props) => { ...@@ -53,11 +90,11 @@ const NftMedia = ({ url, className, isLoading }: Props) => {
<AspectRatio <AspectRatio
ref={ ref } ref={ ref }
className={ className } className={ className }
bgColor={ isLoading || isMediaLoading ? 'transparent' : bgColor }
ratio={ 1 / 1 } ratio={ 1 / 1 }
overflow="hidden" overflow="hidden"
borderRadius="md" borderRadius="md"
objectFit="contain" objectFit="contain"
isolation="isolate"
sx={{ sx={{
'&>img, &>video': { '&>img, &>video': {
objectFit: 'contain', objectFit: 'contain',
...@@ -66,6 +103,7 @@ const NftMedia = ({ url, className, isLoading }: Props) => { ...@@ -66,6 +103,7 @@ const NftMedia = ({ url, className, isLoading }: Props) => {
> >
<> <>
{ content } { content }
{ modal }
{ isMediaLoading && <Skeleton position="absolute" left={ 0 } top={ 0 } w="100%" h="100%" zIndex="1"/> } { isMediaLoading && <Skeleton position="absolute" left={ 0 } top={ 0 } w="100%" h="100%" zIndex="1"/> }
</> </>
</AspectRatio> </AspectRatio>
......
import {
Modal,
ModalContent,
ModalCloseButton,
ModalOverlay,
} from '@chakra-ui/react';
import React from 'react';
interface Props {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const NftMediaFullscreenModal = ({ isOpen, onClose, children }: Props) => {
return (
<Modal isOpen={ isOpen } onClose={ onClose } motionPreset="none">
<ModalOverlay/>
<ModalContent w="unset" maxW="100vw" p={ 0 } background="none" boxShadow="none">
<ModalCloseButton position="fixed" top={{ base: 2.5, lg: 8 }} right={{ base: 2.5, lg: 8 }} color="whiteAlpha.800"/>
{ children }
</ModalContent>
</Modal>
);
};
export default NftMediaFullscreenModal;
import { chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { mediaStyleProps, videoPlayProps } from './utils';
interface Props { interface Props {
src: string; src: string;
onLoad: () => void; onLoad: () => void;
onError: () => void; onError: () => void;
onClick?: () => void;
} }
const NftVideo = ({ src, onLoad, onError }: Props) => { const NftVideo = ({ src, onLoad, onError, onClick }: Props) => {
return ( return (
<chakra.video <chakra.video
{ ...videoPlayProps }
src={ src } src={ src }
autoPlay
disablePictureInPicture
loop
muted
playsInline
onCanPlayThrough={ onLoad } onCanPlayThrough={ onLoad }
onError={ onError } onError={ onError }
borderRadius="md" borderRadius="md"
onClick={ onClick }
{ ...mediaStyleProps }
/> />
); );
}; };
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
import { videoPlayProps } from './utils';
interface Props {
src: string;
isOpen: boolean;
onClose: () => void;
}
const NftVideoWithFullscreen = ({ src, isOpen, onClose }: Props) => {
return (
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.video
{ ...videoPlayProps }
src={ src }
maxH="90vh"
maxW="90vw"
/>
</NftMediaFullscreenModal>
);
};
export default NftVideoWithFullscreen;
import {
chakra,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import NftMediaFullscreenModal from './NftMediaFullscreenModal';
import NftVideo from './NftVideo';
import { videoPlayProps } from './utils';
interface Props {
src: string;
onLoad: () => void;
onError: () => void;
}
const NftVideoWithFullscreen = ({ src, onLoad, onError }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<NftVideo src={ src } onLoad={ onLoad } onError={ onError } onClick={ onOpen }/>
<NftMediaFullscreenModal isOpen={ isOpen } onClose={ onClose }>
<chakra.video
{ ...videoPlayProps }
src={ src }
onCanPlayThrough={ onLoad }
onError={ onError }
maxH="90vh"
maxW="90vw"
/>
</NftMediaFullscreenModal>
</>
);
};
export default NftVideoWithFullscreen;
...@@ -26,3 +26,24 @@ export function getPreliminaryMediaType(url: string): MediaType | undefined { ...@@ -26,3 +26,24 @@ export function getPreliminaryMediaType(url: string): MediaType | undefined {
return 'video'; return 'video';
} }
} }
export const mediaStyleProps = {
transitionProperty: 'transform',
transitionDuration: 'normal',
transitionTimingFunction: 'ease',
cursor: 'pointer',
_hover: {
base: {},
lg: {
transform: 'scale(1.2)',
},
},
};
export const videoPlayProps = {
autoPlay: true,
disablePictureInPicture: true,
loop: true,
muted: true,
playsInline: true,
};
import { Flex, Text, LinkBox, LinkOverlay, useColorModeValue, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Text, Link, useColorModeValue, Skeleton } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -24,29 +25,22 @@ const NFTItem = ({ item, isLoading }: Props) => { ...@@ -24,29 +25,22 @@ const NFTItem = ({ item, isLoading }: Props) => {
/> />
); );
const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: item.token.address, id: item.id } });
return ( return (
<LinkBox <Box
w={{ base: '100%', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
border="1px solid" border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px" borderRadius="12px"
p="10px" p="10px"
_hover={{ boxShadow: isLoading ? 'none' : 'md' }}
fontSize="sm" fontSize="sm"
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
{ isLoading ? mediaElement : ( <Link href={ isLoading ? undefined : url }>
<NextLink { mediaElement }
href={{ pathname: '/token/[hash]/instance/[id]', query: { hash: item.token.address, id: item.id } }} </Link>
passHref
legacyBehavior
>
<LinkOverlay>
{ mediaElement }
</LinkOverlay>
</NextLink>
) }
{ item.id && ( { item.id && (
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
...@@ -58,6 +52,7 @@ const NFTItem = ({ item, isLoading }: Props) => { ...@@ -58,6 +52,7 @@ const NFTItem = ({ item, isLoading }: Props) => {
whiteSpace="nowrap" whiteSpace="nowrap"
display="block" display="block"
isLoading={ isLoading } isLoading={ isLoading }
href={ url }
> >
{ item.id } { item.id }
</LinkInternal> </LinkInternal>
...@@ -77,7 +72,7 @@ const NFTItem = ({ item, isLoading }: Props) => { ...@@ -77,7 +72,7 @@ const NFTItem = ({ item, isLoading }: Props) => {
/> />
</Flex> </Flex>
) } ) }
</LinkBox> </Box>
); );
}; };
......
...@@ -88,6 +88,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => { ...@@ -88,6 +88,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
flexShrink={ 0 } flexShrink={ 0 }
alignSelf={{ base: 'center', lg: 'flex-start' }} alignSelf={{ base: 'center', lg: 'flex-start' }}
isLoading={ isLoading } isLoading={ isLoading }
withFullscreen
/> />
</Flex> </Flex>
<Grid <Grid
......
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