Skip to content

Commit

Permalink
Merge pull request #1127 from blockscout/nft-and-qr-modals
Browse files Browse the repository at this point in the history
Nft and qr modals
  • Loading branch information
isstuev authored Sep 24, 2023
2 parents a4dfabc + 85b0c40 commit 1d87091
Show file tree
Hide file tree
Showing 37 changed files with 455 additions and 92 deletions.
8 changes: 8 additions & 0 deletions theme/components/Modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ const baseStyle = definePartsStyle((props) => ({
}));

const sizes = {
sm: definePartsStyle({
dialogContainer: {
height: '100%',
},
dialog: {
maxW: '536px',
},
}),
md: definePartsStyle({
dialogContainer: {
height: '100%',
Expand Down
3 changes: 2 additions & 1 deletion ui/address/details/AddressQrCode.pw.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';

import * as addressMock from 'mocks/address/address';
import TestApp from 'playwright/TestApp';

import AddressQrCode from './AddressQrCode';

test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<AddressQrCode hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29"/>
<AddressQrCode address={ addressMock.withoutName }/>
</TestApp>,
);
await page.getByRole('button', { name: /qr code/i }).click();
Expand Down
57 changes: 42 additions & 15 deletions ui/address/details/AddressQrCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
ModalBody,
ModalContent,
ModalCloseButton,
ModalHeader,
ModalOverlay,
LightMode,
Box,
useDisclosure,
Tooltip,
Expand All @@ -18,24 +20,26 @@ import { useRouter } from 'next/router';
import QRCode from 'qrcode';
import React from 'react';

import type { Address as AddressType } from 'types/api/address';

import qrCodeIcon from 'icons/qr_code.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getPageType from 'lib/mixpanel/getPageType';
import * as mixpanel from 'lib/mixpanel/index';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';

const SVG_OPTIONS = {
margin: 0,
};

interface Props {
className?: string;
hash: string;
address: AddressType;
isLoading?: boolean;
}

const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const AddressQrCode = ({ address, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile();

const router = useRouter();

const [ qr, setQr ] = React.useState('');
Expand All @@ -45,7 +49,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {

React.useEffect(() => {
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) {
setError('We were unable to generate QR code.');
Sentry.captureException(error, { tags: { source: 'qr_code' } });
Expand All @@ -57,7 +61,7 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
mixpanel.logEvent(mixpanel.EventTypes.QR_CODE, { 'Page type': pageType });
});
}
}, [ hash, isOpen, onClose, pageType ]);
}, [ address.hash, isOpen, onClose, pageType ]);

if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
Expand All @@ -77,15 +81,38 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => {
icon={ <Icon as={ qrCodeIcon } boxSize={ 5 }/> }
/>
</Tooltip>
<Modal isOpen={ isOpen } onClose={ onClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/>
<ModalContent bgColor={ error ? undefined : 'white' }>
{ isMobile && <ModalCloseButton/> }
<ModalBody mb={ 0 }>
{ error ? <Alert status="warning">{ error }</Alert> : <Box dangerouslySetInnerHTML={{ __html: qr }}/> }
</ModalBody>
</ModalContent>
</Modal>

{ error && (
<Modal isOpen={ isOpen } onClose={ onClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/>
<ModalContent>
<ModalBody mb={ 0 }>
<Alert status="warning">{ error }</Alert>
</ModalBody>
</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>
) }
</>
);
};
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 7 additions & 8 deletions ui/address/tokens/NFTItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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 type { AddressTokenBalance } from 'types/api/address';
Expand All @@ -12,27 +12,26 @@ import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance & { isLoading: boolean };

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 (
<LinkBox
<Box
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ isLoading ? undefined : tokenLink }>
<Link href={ isLoading ? undefined : tokenInstanceLink }>
<NftMedia
mb="18px"
url={ tokenInstance?.animation_url || tokenInstance?.image_url || null }
isLoading={ isLoading }
/>
</LinkOverlay>
</Link>
{ tokenId && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
Expand All @@ -44,7 +43,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
whiteSpace="nowrap"
textOverflow="ellipsis"
overflow="hidden"
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
href={ tokenInstanceLink }
>
{ tokenId }
</Link>
Expand All @@ -58,7 +57,7 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
noCopy
noSymbol
/>
</LinkBox>
</Box>
);
};

Expand Down
2 changes: 1 addition & 1 deletion ui/shared/AddressHeadingInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
{ !isLoading && !address.is_contract && config.features.account.isEnabled && (
<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 }/> }
</Flex>
);
Expand Down
8 changes: 5 additions & 3 deletions ui/shared/nft/NftFallback.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Icon, useColorModeValue } from '@chakra-ui/react';
import { Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';

import nftIcon from 'icons/nft_shield.svg';

const NftFallback = () => {
const NftFallback = ({ className }: {className?: string}) => {
return (
<Icon
className={ className }
as={ nftIcon }
p="50px"
color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }
bgColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
/>
);
};

export default NftFallback;
export default chakra(NftFallback);
26 changes: 18 additions & 8 deletions ui/shared/nft/NftHtml.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { chakra } from '@chakra-ui/react';
import { chakra, LinkOverlay } from '@chakra-ui/react';
import React from 'react';

import { mediaStyleProps } from './utils';

interface Props {
src: string;
onLoad: () => void;
onError: () => void;
onClick?: () => void;
}

const NftHtml = ({ src, onLoad, onError }: Props) => {
const NftHtml = ({ src, onLoad, onError, onClick }: Props) => {
return (
<chakra.iframe
src={ src }
sandbox="allow-scripts"
onLoad={ onLoad }
onError={ onError }
/>
<LinkOverlay
onClick={ onClick }
{ ...mediaStyleProps }
>
<chakra.iframe
src={ src }
h="100%"
w="100%"
sandbox="allow-scripts"
onLoad={ onLoad }
onError={ onError }
/>
</LinkOverlay>
);
};

Expand Down
25 changes: 25 additions & 0 deletions ui/shared/nft/NftHtmlFullscreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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;
33 changes: 33 additions & 0 deletions ui/shared/nft/NftHtmlWithFullscreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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;
11 changes: 8 additions & 3 deletions ui/shared/nft/NftImage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Image } from '@chakra-ui/react';
import React from 'react';

import { mediaStyleProps } from './utils';

interface Props {
url: string;
src: string;
onLoad: () => void;
onError: () => void;
onClick?: () => void;
}

const NftImage = ({ url, onLoad, onError }: Props) => {
const NftImage = ({ src, onLoad, onError, onClick }: Props) => {
return (
<Image
w="100%"
h="100%"
src={ url }
src={ src }
alt="Token instance image"
onError={ onError }
onLoad={ onLoad }
onClick={ onClick }
{ ...mediaStyleProps }
/>
);
};
Expand Down
22 changes: 22 additions & 0 deletions ui/shared/nft/NftImageFullscreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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;
Loading

0 comments on commit 1d87091

Please sign in to comment.