Commit 03c76421 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into search-bar

parents 26e73db3 334ebdf9
<svg viewBox="0 0 201 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.027 8.408c-2.187 0-4.238.41-6.152 1.23a15.973 15.973 0 0 0-4.922 3.35c-1.413 1.413-2.53 3.076-3.35 4.99-.82 1.869-1.23 3.897-1.23 6.084a3.372 3.372 0 0 1-3.913 3.328l-2.409-.392a2.974 2.974 0 0 1-2.496-2.936c0-3.326.615-6.448 1.845-9.365 1.276-2.916 3.008-5.468 5.196-7.656 2.187-2.187 4.74-3.896 7.656-5.127C93.169.638 96.29 0 99.617 0h4.17c3.327 0 6.449.592 9.365 1.777 2.963 1.185 5.537 2.894 7.725 5.127 2.187 2.233 3.896 4.922 5.127 8.067 1.276 3.099 1.914 6.608 1.914 10.527 0 3.874-.592 7.77-1.777 11.69a67.002 67.002 0 0 1-4.58 11.552 87.122 87.122 0 0 1-6.495 11.006 147.24 147.24 0 0 1-7.451 10.049c-5.97 7.337-12.737 14.401-20.302 21.191h32.88a8.409 8.409 0 0 1 8.409 8.408H73.436v-2.253c0-2.988 1.345-5.81 3.612-7.756 7.075-6.073 13.482-12.33 19.22-18.77a162.094 162.094 0 0 0 8.408-10.322 100.413 100.413 0 0 0 7.314-11.211c2.142-3.874 3.851-7.793 5.127-11.758 1.322-4.01 1.983-7.952 1.983-11.826 0-2.871-.433-5.378-1.299-7.52-.866-2.141-2.028-3.919-3.487-5.332a13.909 13.909 0 0 0-5.058-3.144 16.03 16.03 0 0 0-5.879-1.094h-3.35ZM48.193 82.441v16.953a8.818 8.818 0 0 1-8.818-8.818v-8.135H3.3a3.3 3.3 0 0 1-2.872-4.926L42.701 2.857a2.937 2.937 0 0 1 5.492 1.447v69.73h8.409a8.408 8.408 0 0 1-8.409 8.407Zm-8.818-56.943L12.373 74.033h27.002V25.498Zm126.5-15.86c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.051 0 4.01.365 5.879 1.094a13.91 13.91 0 0 1 5.058 3.144c1.459 1.413 2.621 3.19 3.487 5.332.866 2.142 1.299 4.649 1.299 7.52 0 3.874-.661 7.816-1.983 11.826-1.276 3.965-2.985 7.884-5.127 11.758a100.413 100.413 0 0 1-7.314 11.211 162.124 162.124 0 0 1-8.408 10.322c-5.738 6.44-12.145 12.697-19.22 18.77-2.267 1.946-3.612 4.768-3.612 7.756v2.253h55.166a8.409 8.409 0 0 0-8.409-8.408h-32.881c7.566-6.79 14.333-13.854 20.303-21.191a147.24 147.24 0 0 0 7.451-10.049 87.122 87.122 0 0 0 6.495-11.006 67.002 67.002 0 0 0 4.58-11.553c1.185-3.919 1.777-7.815 1.777-11.689 0-3.92-.638-7.428-1.914-10.527-1.231-3.145-2.94-5.834-5.127-8.067-2.188-2.233-4.762-3.942-7.725-5.127C182.236.592 179.114 0 175.787 0h-4.17c-3.327 0-6.448.638-9.365 1.914-2.917 1.23-5.469 2.94-7.656 5.127-2.188 2.188-3.92 4.74-5.196 7.656-1.23 2.917-1.845 6.039-1.845 9.366a2.974 2.974 0 0 0 2.496 2.935l2.409.392a3.371 3.371 0 0 0 3.913-3.328c0-2.187.41-4.215 1.231-6.084.82-1.914 1.936-3.577 3.349-4.99a15.982 15.982 0 0 1 4.922-3.35Z" fill="currentColor"/>
</svg>
......@@ -129,6 +129,7 @@ function makePolicyMap() {
// ad
'servedbyadbutler.com',
'cdn.coinzilla.io',
],
'font-src': [
......
export default async function insertAdText() {
const ad = document.getElementsByClassName('coinzilla');
ad[0].textContent = 'coinzilla banner!';
}
import type { Page } from 'playwright-core';
export default async function insertAdPlaceholder(page: Page) {
await page.waitForSelector('#adBanner', { state: 'attached' });
await page.evaluate(() => {
const adContainer = document.getElementById('adBanner');
const adReplacer = document.createElement('div');
adReplacer.style.width = '200px';
adReplacer.style.height = '100px';
adReplacer.style.background = '#f00';
adContainer?.replaceChildren(adReplacer);
});
}
......@@ -48,6 +48,10 @@ const AddressDetails = ({ addressQuery }: Props) => {
},
});
if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
}
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import NextLink from 'next/link';
......@@ -52,16 +52,33 @@ const BlockDetails = () => {
router.push(url, undefined);
}, [ router ]);
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
if (isError) {
const is404 = error?.payload?.status === 404;
return is404 ? <span>This block has not been processed yet.</span> : <DataFetchAlert/>;
if (error?.payload?.status === 404) {
return <span>This block has not been processed yet.</span>;
}
if (error?.payload?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error });
}
return <DataFetchAlert/>;
}
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
/>
);
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
const validatorTitle = getNetworkValidatorTitle();
......@@ -255,7 +272,7 @@ const BlockDetails = () => {
{ /* ADDITIONAL INFO */ }
{ isExpanded && (
<>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem
title="Difficulty"
......
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import { Grid, GridItem, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const BlockDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
/>
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
......
......@@ -11,6 +11,7 @@ import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
......@@ -40,16 +41,15 @@ const AddressPageContent = () => {
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated addressQuery={ addressQuery }/> },
];
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
return (
<Page>
<Flex alignItems="center" columnGap={ 3 }>
<PageTitle text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }/>
{ tags.length > 0 && (
<Flex mb={ 6 } columnGap={ 2 }>
{ tags }
</Flex>
) }
</Flex>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionals={ tagsNode }
/>
<AddressDetails addressQuery={ addressQuery }/>
<RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>
</Page>
......
import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import isBrowser from 'lib/isBrowser';
import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
......@@ -52,16 +51,12 @@ const BlockPageContent = () => {
return (
<Page>
<Flex alignItems="center" columnGap={ 3 }>
{ hasGoBackLink && (
<Tooltip label="Back to blocks list">
<Link mb={ 6 } display="inline-flex" href={ referrer }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
</Tooltip>
) }
<PageTitle text={ `Block #${ router.query.id }` }/>
</Flex>
<TextAd mb={ 6 }/>
<PageTitle
text={ `Block #${ router.query.id }` }
backLinkUrl={ hasGoBackLink ? referrer : undefined }
backLinkLabel="Back to blocks list"
/>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
......@@ -42,7 +42,7 @@ const BlocksPageContent = () => {
return (
<Page>
<PageTitle text="Blocks"/>
<PageTitle text="Blocks" withTextAd/>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
......@@ -5,9 +5,9 @@ import * as blockMock from 'mocks/blocks/block';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import insertAdText from 'playwright/scripts/insertAdText';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import Home from './Home';
......@@ -42,7 +42,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
</TestApp>,
);
await page.evaluate(insertAdText);
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Link, Icon, Tag, Tooltip } from '@chakra-ui/react';
import { Flex, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser';
import networkExplorers from 'lib/networks/networkExplorers';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import ExternalLink from 'ui/shared/ExternalLink';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -52,36 +51,33 @@ const TransactionPageContent = () => {
return <ExternalLink key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
});
const additionals = (
<Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 }>
{ data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && (
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
>
{ explorersLinks }
</Flex>
) }
</Flex>
);
return (
<Page>
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<Flex alignItems="center" columnGap={ 3 }>
{ hasGoBackLink && (
<Tooltip label="Back to transactions list">
<Link display="inline-flex" href={ referrer } mb={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
</Tooltip>
) }
<PageTitle text="Transaction details"/>
</Flex>
{ data?.tx_tag && <Tag my={ 2 } ml={ 3 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && (
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
{ explorersLinks }
</Flex>
) }
</Flex>
<TextAd mb={ 6 }/>
<PageTitle
text="Transaction details"
additionals={ additionals }
backLinkUrl={ hasGoBackLink ? referrer : undefined }
backLinkLabel="Back to transactions list"
/>
<RoutedTabs tabs={ TABS }/>
<AdBanner mt={ 6 } justifyContent={{ base: 'center', lg: 'start' }}/>
</Page>
);
};
......
......@@ -37,7 +37,7 @@ const Transactions = () => {
return (
<Page>
<Box h="100%">
<PageTitle text="Transactions"/>
<PageTitle text="Transactions" withTextAd/>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
......@@ -16,6 +16,15 @@ test('status code 404', async({ mount }) => {
await expect(component).toHaveScreenshot();
});
test('status code 422', async({ mount }) => {
const component = await mount(
<TestApp>
<AppError statusCode={ 422 }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('status code 500', async({ mount }) => {
const component = await mount(
<TestApp>
......
......@@ -2,6 +2,7 @@ import { Box, Button, Heading, Icon, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import icon404 from 'icons/error-pages/404.svg';
import icon422 from 'icons/error-pages/422.svg';
import icon500 from 'icons/error-pages/500.svg';
import link from 'lib/link/link';
......@@ -16,6 +17,11 @@ const ERRORS: Record<string, {icon: React.FunctionComponent<React.SVGAttributes<
title: 'Page not found',
text: 'This page is no longer explorable! If you are lost, use the search bar to find what you are looking for.',
},
'422': {
icon: icon422,
title: 'Request cannot be processed',
text: 'Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info.',
},
'500': {
icon: icon500,
title: 'Oops! Something went wrong',
......
......@@ -2,17 +2,22 @@ import React from 'react';
interface Props {
children: React.ReactNode;
renderErrorScreen: () => React.ReactNode;
renderErrorScreen: (error?: Error) => React.ReactNode;
onError?: (error: Error) => void;
}
class ErrorBoundary extends React.PureComponent<Props> {
state = {
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.PureComponent<Props, State> {
state: State = {
hasError: false,
};
static getDerivedStateFromError() {
return { hasError: true };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error) {
......@@ -21,7 +26,7 @@ class ErrorBoundary extends React.PureComponent<Props> {
render() {
if (this.state.hasError) {
return this.props.renderErrorScreen();
return this.props.renderErrorScreen(this.state.error);
}
return this.props.children;
......
/* eslint-disable max-len */
import { Box, Heading, OrderedList, ListItem, Icon, useColorModeValue, Flex } from '@chakra-ui/react';
import React from 'react';
import txIcon from 'icons/transactions.svg';
const ErrorInvalidTxHash = () => {
const textColor = useColorModeValue('gray.500', 'gray.400');
const snippet = {
borderColor: useColorModeValue('blackAlpha.300', 'whiteAlpha.300'),
iconBg: useColorModeValue('blackAlpha.800', 'whiteAlpha.800'),
iconColor: useColorModeValue('white', 'black'),
};
return (
<Box mt="50px">
<Box p={ 4 } borderColor={ snippet.borderColor } borderRadius="md" w="230px" borderWidth="1px">
<Flex alignItems="center" pb={ 4 } borderBottomWidth="1px" borderColor={ snippet.borderColor }>
<Icon as={ txIcon } boxSize={ 8 } color={ snippet.iconColor } bgColor={ snippet.iconBg } p={ 1 } borderRadius="md"/>
<Box ml={ 2 }>
<Box w="125px" h="8px" borderRadius="full" bgColor={ snippet.iconBg }/>
<Box w="30px" h="8px" borderRadius="full" bgColor={ snippet.borderColor } mt={ 1.5 }/>
</Box>
</Flex>
<Flex justifyContent="space-between" alignItems="center" mt={ 3 }>
<Flex alignItems="center">
<Box boxSize={ 5 } borderRadius="full" bgColor={ snippet.borderColor }/>
<Box w="65px" h="8px" borderRadius="full" bgColor={ snippet.borderColor } ml={ 1.5 }/>
</Flex>
<Flex alignItems="center">
<Box boxSize={ 5 } borderRadius="full" bgColor={ snippet.borderColor }/>
<Box w="65px" h="8px" borderRadius="full" bgColor={ snippet.borderColor } ml={ 1.5 }/>
</Flex>
</Flex>
</Box>
<Heading size="2xl" fontFamily="body" mt={ 6 }>
Sorry, we are unable to locate this transaction hash
</Heading>
<OrderedList color={ textColor } mt={ 3 } spacing={ 3 }>
<ListItem>
If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page.
</ListItem>
<ListItem>
It could still be in the TX Pool of a different node, waiting to be broadcasted.
</ListItem>
<ListItem>
During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it.
</ListItem>
<ListItem>
If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information.
</ListItem>
</OrderedList>
</Box>
);
};
export default ErrorInvalidTxHash;
......@@ -9,6 +9,7 @@ import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
......@@ -49,10 +50,17 @@ const Page = ({
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
const renderErrorScreen = React.useCallback(() => {
return wrapChildren ?
<PageContent isHomePage={ isHomePage }><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<AppError statusCode={ 500 }/>;
const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.error?.status || 500;
const isInvalidTxHash = error?.message.includes('Invalid tx hash');
if (wrapChildren) {
const content = isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode } mt="50px"/>;
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
}
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ 500 }/>;
}, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? (
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import PageTitle from './PageTitle';
const textAdMock = {
ad: {
name: 'Hello utia!',
description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!',
thumbnail: 'https://utia.utia',
url: 'https://test.url',
cta_button: 'Click me!',
},
};
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock),
}));
await page.route(textAdMock.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
});
test('default view +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<PageTitle
text="Title"
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<PageTitle
text="Title"
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet"
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('long title with text ad, back link and addons +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<PageTitle
text="This title is long, really long"
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet, kak dela?"
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Heading, chakra } from '@chakra-ui/react';
import { Heading, Flex, Tooltip, Link, Icon, chakra } from '@chakra-ui/react';
import React from 'react';
const PageTitle = ({ text, className }: {text: string; className?: string}) => {
import eastArrowIcon from 'icons/arrows/east.svg';
import TextAd from 'ui/shared/ad/TextAd';
type Props = {
text: string;
additionals?: React.ReactNode;
withTextAd?: boolean;
className?: string;
backLinkLabel?: string;
backLinkUrl?: string;
}
const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const title = (
<Heading
as="h1"
size="lg"
flex="none"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
width={ backLinkUrl ? 'calc(100% - 36px)' : '100%' }
>
{ text }
</Heading>
);
return (
<Heading as="h1" size="lg" marginBottom={ 6 } className={ className }>{ text }</Heading>
<Flex
columnGap={ 3 }
rowGap={ 3 }
alignItems={{ base: 'start', lg: 'center' }}
flexDirection={{ base: 'column', lg: 'row' }}
mb={ 6 }
justifyContent="space-between"
className={ className }
>
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }>
<Flex
flexWrap="nowrap"
alignItems="center"
columnGap={ 3 }
overflow="hidden"
>
{ backLinkUrl && (
<Tooltip label={ backLinkLabel }>
<Link display="inline-flex" href={ backLinkUrl }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
</Tooltip>
) }
{ title }
</Flex>
{ additionals }
</Flex>
{ withTextAd && <TextAd flexShrink={ 100 }/> }
</Flex>
);
};
......
/* eslint-disable max-len */
import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script';
import React from 'react';
// eslint-disable-next-line @typescript-eslint/no-var-requires
// const adbutlerHTML = require('ui/shared/ad/adbutler.html');
// didn't find a way to raw-load html that works both for webpack (app build) and vite (playwright build)
const adbutlerHTML = `
<div id="ad-banner"></div>
<script type="text/javascript">if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}</script>
<script type="text/javascript">
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
const scriptText1 = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
const scriptText2 = `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;
if (isMobile) {
......@@ -23,12 +18,15 @@ const adbutlerHTML = `
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_523705_'+plc523705+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, 523705, [728,90], 'placement_523705_'+opt.place, opt); }, opt: { place: plc523705++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
}
</script>
`;
const AdbutlerBanner = ({ className }: { className?: string }) => {
return (
<Flex className={ className } dangerouslySetInnerHTML={{ __html: adbutlerHTML }}/>
<Flex className={ className } id="adBanner">
<div id="ad-banner"></div>
<Script id="ad-butler-1">{ scriptText1 }</Script>
<Script id="ad-butler-2">{ scriptText2 }</Script>
</Flex>
);
};
......
import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script';
import React from 'react';
import isBrowser from 'lib/isBrowser';
......@@ -18,18 +19,21 @@ type CPreferences = {
const CoinzillaBanner = ({ className }: { className?: string }) => {
const isInBrowser = isBrowser();
if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = {} as CPreferences;
cDisplayPreferences.zone = '26660bf627543e46851';
cDisplayPreferences.width = '728';
cDisplayPreferences.height = '90';
window.coinzilla_display.push(cDisplayPreferences);
}
React.useEffect(() => {
if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = {} as CPreferences;
cDisplayPreferences.zone = '26660bf627543e46851';
cDisplayPreferences.width = '728';
cDisplayPreferences.height = '90';
window.coinzilla_display.push(cDisplayPreferences);
}
}, [ isInBrowser ]);
return (
<Flex className={ className }>
<script async src="https://coinzillatag.com/lib/display.js"></script>
<Flex className={ className } id="adBanner">
<Script src="https://coinzillatag.com/lib/display.js"/>
<div className="coinzilla" data-zone="C-26660bf627543e46851"></div>
</Flex>
);
......
import { Box, Image, Link, Text, chakra } from '@chakra-ui/react';
import React, { useEffect } from 'react';
import { ndash } from 'lib/html-entities';
type AdData = {
ad: {
name: string;
description_short: string;
thumbnail: string;
url: string;
cta_button: string;
impressionUrl: string;
};
}
const CoinzillaTextAd = ({ className }: {className?: string}) => {
const [ adData, setAdData ] = React.useState<AdData | null>(null);
useEffect(() => {
fetch('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242')
.then(res => res.status === 200 ? res.json() : null)
.then((data) => {
setAdData(data);
if (data?.ad?.impressionUrl) {
fetch(data.ad.impressionUrl);
}
});
}, []);
if (!adData) {
return null;
}
const urlObject = new URL(adData.ad.url);
return (
<Box className={ className }>
<Text
as="span"
whiteSpace="pre-wrap"
fontWeight={ 700 }
mr={ 3 }
display={{ base: 'none', lg: 'inline' }}
>
ADs:
</Text>
{ urlObject.hostname === 'nifty.ink' ?
<Text as="span" mr={ 1 }>🎨</Text> :
<Image src={ adData.ad.thumbnail } width="20px" height="20px" mb="-4px" mr={ 1 } display="inline" alt=""/>
}
<Text as="span" whiteSpace="pre-wrap">{ `${ adData.ad.name } ${ ndash } ${ adData.ad.description_short } ` }</Text>
<Link href={ adData.ad.url }>{ adData.ad.cta_button }</Link>
</Box>
);
};
export default chakra(CoinzillaTextAd);
import { chakra } from '@chakra-ui/react';
import React from 'react';
import isSelfHosted from 'lib/isSelfHosted';
import CoinzillaTextAd from './CoinzillaTextAd';
const TextAd = ({ className }: {className?: string}) => {
if (!isSelfHosted()) {
return null;
}
return <CoinzillaTextAd className={ className }/>;
};
export default chakra(TextAd);
<div id="ad-banner"></div>
<script type="text/javascript">if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}</script>
<script type="text/javascript">
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;
if (isMobile) {
var plc539876 = window.plc539876 || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_539876_'+plc539876+'"></'+'div>';
document.getElementById("ad-banner").className = "ad-container mb-3";
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, 539876, [320,100], 'placement_539876_'+opt.place, opt); }, opt: { place: plc539876++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} else {
var plc523705 = window.plc523705 || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_523705_'+plc523705+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, 523705, [728,90], 'placement_523705_'+opt.place, opt); }, opt: { place: plc523705++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
}
</script>
\ No newline at end of file
......@@ -4,6 +4,7 @@ import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import TxDetails from './TxDetails';
......@@ -28,6 +29,7 @@ test('between addresses +@mobile +@dark-mode', async({ mount, page }) => {
);
await page.getByText('View details').click();
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -45,6 +47,8 @@ test('creating contact', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -61,6 +65,8 @@ test('with token transfer +@mobile', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -77,6 +83,8 @@ test('with decoded revert reason', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -93,6 +101,8 @@ test('with decoded raw reason', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
......@@ -110,6 +120,7 @@ test('pending', async({ mount, page }) => {
);
await page.getByText('View details').click();
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
import { Grid, GridItem, Text, Box, Icon, Link, Spinner, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react';
import {
Grid,
GridItem,
Text,
Box,
Icon,
Link,
Spinner,
Tag,
Flex,
Tooltip,
chakra,
useColorModeValue,
} from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -10,7 +23,9 @@ import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import AdBanner from 'ui/shared/ad/AdBanner';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -32,7 +47,11 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxDetails = () => {
const { data, isLoading, isError, socketStatus } = useFetchTxInfo();
const { data, isLoading, isError, socketStatus, error } = useFetchTxInfo();
const isMobile = useIsMobile();
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const [ isExpanded, setIsExpanded ] = React.useState(false);
......@@ -49,6 +68,14 @@ const TxDetails = () => {
}
if (isError) {
if (error?.payload?.status === 422) {
throw Error('Invalid tx hash', { cause: error as unknown as Error });
}
if (error?.payload?.status === 404) {
throw Error('Tx fetch failed', { cause: error as unknown as Error });
}
return <DataFetchAlert/>;
}
......@@ -140,7 +167,29 @@ const TxDetails = () => {
<Text variant="secondary">{ getConfirmationDuration(data.confirmation_duration) }</Text>
</DetailsInfoItem>
) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
{ isMobile ?
(
<GridItem
colSpan={{ base: undefined, lg: 2 }}
>
<AdBanner justifyContent="center"/>
</GridItem>
) :
(
<DetailsInfoItem
title="Sponsored"
hint="Sponsored banner advertisement"
>
<AdBanner/>
</DetailsInfoItem>
) }
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
/>
<DetailsInfoItem
title="From"
hint="Address (external or contract) sending the transaction."
......@@ -191,7 +240,13 @@ const TxDetails = () => {
</DetailsInfoItem>
{ data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
/>
<DetailsInfoItem
title="Value"
hint="Value sent in the native token (and USD) if applicable."
......
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import { Grid, GridItem, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const TxDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor={ borderColor }
/>
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
......
......@@ -6,6 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Transaction } from 'types/api/transaction';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import delay from 'lib/delay';
import useSocketChannel from 'lib/socket/useSocketChannel';
......@@ -16,7 +17,7 @@ interface Params {
updateDelay?: number;
}
type ReturnType = UseQueryResult<Transaction, unknown> & {
type ReturnType = UseQueryResult<Transaction, ResourceError<{ status: number }>> & {
socketStatus: 'close' | 'error' | undefined;
}
......@@ -25,7 +26,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const queryResult = useApiQuery('tx', {
const queryResult = useApiQuery<'tx', { status: number }>('tx', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id),
......
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