Commit 60d2851e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #331 from blockscout/account-fixes

account fixes
parents 0bd3c366 74d4fce9
...@@ -11,7 +11,7 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => { ...@@ -11,7 +11,7 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
return null; return null;
} }
}; };
const stripTrailingSlash = (str: string) => str.at(-1) === '/' ? str.slice(0, -1) : str; const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const env = process.env.VERCEL_ENV || process.env.NODE_ENV; const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development'; const isDev = env === 'development';
......
<svg viewBox="0 0 201 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M126.092 85.414c1.267-2.895 1.9-5.993 1.9-9.295V23.881c0-3.302-.633-6.4-1.9-9.295-1.221-2.895-2.917-5.427-5.088-7.598-2.171-2.171-4.704-3.867-7.598-5.088-2.895-1.267-5.993-1.9-9.295-1.9h-4.138C96.67 0 93.55.633 90.61 1.9c-2.895 1.22-5.428 2.917-7.599 5.088-2.17 2.17-3.89 4.703-5.156 7.598-1.221 2.895-1.832 5.993-1.832 9.295v52.238c0 3.302.611 6.4 1.832 9.295 1.267 2.894 2.985 5.427 5.156 7.598 2.171 2.171 4.704 3.89 7.599 5.156C93.55 99.39 96.67 100 99.973 100h4.138c3.302 0 6.4-.61 9.295-1.832 2.894-1.266 5.427-2.985 7.598-5.156 2.171-2.17 3.867-4.704 5.088-7.598Zm-8.073-67.571c.814 1.854 1.221 3.867 1.221 6.038v52.238c0 2.171-.407 4.207-1.221 6.106a16.717 16.717 0 0 1-3.324 4.953c-1.402 1.402-3.053 2.51-4.953 3.324-1.854.814-3.867 1.221-6.038 1.221h-3.324c-2.171 0-4.206-.407-6.106-1.221a16.718 16.718 0 0 1-4.952-3.324 16.718 16.718 0 0 1-3.325-4.953c-.814-1.9-1.22-3.935-1.22-6.106V23.881c0-2.171.406-4.184 1.22-6.038.814-1.9 1.922-3.55 3.325-4.953a16.718 16.718 0 0 1 4.952-3.324c1.9-.814 3.935-1.221 6.106-1.221h3.324c2.171 0 4.184.407 6.038 1.22 1.9.815 3.551 1.923 4.953 3.325 1.402 1.402 2.51 3.053 3.324 4.953Zm-70.19 63.975v16.825a8.752 8.752 0 0 1-8.752-8.752v-8.073H3.275a3.275 3.275 0 0 1-2.85-4.89L42.38 2.836a2.914 2.914 0 0 1 5.45 1.436v69.203h8.345a8.345 8.345 0 0 1-8.345 8.344Zm-8.752-56.513L12.28 73.474h26.797V25.305Zm152.711 56.513v16.825a8.752 8.752 0 0 1-8.752-8.752v-8.073h-35.802a3.275 3.275 0 0 1-2.85-4.89l41.954-74.093a2.914 2.914 0 0 1 5.45 1.436v69.203h8.345a8.345 8.345 0 0 1-8.345 8.344Zm-8.752-56.513-26.798 48.169h26.798V25.305Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 197 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M124.363 76.7c0 3.326-.638 6.448-1.914 9.364-1.23 2.917-2.939 5.47-5.127 7.657-2.187 2.187-4.739 3.919-7.656 5.195-2.917 1.23-6.038 1.846-9.365 1.846h-4.17c-3.327 0-6.472-.616-9.434-1.846-2.916-1.276-5.469-3.008-7.656-5.195-2.188-2.188-3.92-4.74-5.195-7.657C72.616 83.148 72 80.026 72 76.7V24.063c0-3.327.615-6.45 1.846-9.366 1.276-2.916 3.007-5.468 5.195-7.656 2.188-2.187 4.74-3.896 7.656-5.127C89.66.638 92.804 0 96.131 0h4.17c3.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.897 4.74 5.127 7.656 1.276 2.917 1.914 6.039 1.914 9.366v52.636Zm-8.818-52.638c0-2.187-.41-4.215-1.231-6.084-.82-1.914-1.936-3.577-3.349-4.99-1.413-1.412-3.076-2.529-4.99-3.35-1.869-.82-3.897-1.23-6.084-1.23h-3.35c-2.188 0-4.238.41-6.152 1.23a16.848 16.848 0 0 0-4.99 3.35c-1.413 1.413-2.53 3.076-3.35 4.99-.82 1.869-1.23 3.897-1.23 6.084V76.7c0 2.188.41 4.239 1.23 6.153a16.847 16.847 0 0 0 3.35 4.99 16.847 16.847 0 0 0 4.99 3.35c1.914.82 3.965 1.23 6.152 1.23h3.35c2.187 0 4.215-.41 6.084-1.23 1.914-.82 3.577-1.937 4.99-3.35a16.834 16.834 0 0 0 3.349-4.99c.821-1.915 1.231-3.965 1.231-6.153V24.063ZM52.363 76.7c0 3.327-.638 6.449-1.914 9.365-1.23 2.917-2.94 5.47-5.127 7.657-2.187 2.187-4.74 3.919-7.656 5.195-2.917 1.23-6.038 1.846-9.365 1.846h-4.17c-3.327 0-6.471-.616-9.434-1.846-2.916-1.276-5.468-3.008-7.656-5.195-2.187-2.188-3.92-4.74-5.195-7.657C.616 83.148 0 80.026 0 76.7c0-.83.673-1.504 1.504-1.504h5.81c.831 0 1.504.674 1.504 1.504 0 2.188.41 4.239 1.23 6.153a16.847 16.847 0 0 0 3.35 4.99 16.847 16.847 0 0 0 4.99 3.35c1.915.82 3.965 1.23 6.153 1.23h3.35c2.187 0 4.215-.41 6.084-1.23 1.914-.82 3.577-1.937 4.99-3.35a16.847 16.847 0 0 0 3.35-4.99c.82-1.915 1.23-3.965 1.23-6.153V57.695c0-2.187-.41-4.215-1.23-6.084-.82-1.914-1.937-3.577-3.35-4.99a15.212 15.212 0 0 0-4.99-3.418c-1.869-.82-3.897-1.23-6.084-1.23h-3.35c-2.188 0-4.238.41-6.152 1.23a16.228 16.228 0 0 0-4.99 3.418c-1.413 1.413-2.53 3.076-3.35 4.99a14.485 14.485 0 0 0-1.112 4.09c-.134 1.096-1.014 1.994-2.119 1.994h-4.61a2 2 0 0 1-1.99-2.198l4.85-48.724a6 6 0 0 1 5.97-5.406H47.51a8.408 8.408 0 0 1-8.408 8.408H13.877l-3.213 30.147c2.096-2.005 4.535-3.555 7.315-4.649 2.78-1.139 5.765-1.709 8.955-1.709H28.3c3.327 0 6.448.639 9.365 1.914 2.917 1.231 5.469 2.94 7.656 5.127 2.188 2.188 3.897 4.763 5.127 7.725 1.276 2.917 1.914 6.038 1.914 9.365V76.7Zm142.086 9.365c1.276-2.916 1.914-6.038 1.914-9.365V24.063c0-3.327-.638-6.45-1.914-9.366-1.23-2.916-2.939-5.468-5.127-7.656-2.187-2.187-4.739-3.896-7.656-5.127C178.749.638 175.628 0 172.301 0h-4.17c-3.327 0-6.471.638-9.434 1.914-2.916 1.23-5.468 2.94-7.656 5.127-2.187 2.188-3.919 4.74-5.195 7.656-1.231 2.917-1.846 6.039-1.846 9.366v52.636c0 3.327.615 6.449 1.846 9.365 1.276 2.917 3.008 5.47 5.195 7.657 2.188 2.187 4.74 3.919 7.656 5.195 2.963 1.23 6.107 1.846 9.434 1.846h4.17c3.327 0 6.448-.616 9.365-1.846 2.917-1.276 5.469-3.008 7.656-5.195 2.188-2.188 3.897-4.74 5.127-7.657Zm-8.135-68.085c.821 1.868 1.231 3.896 1.231 6.084V76.7c0 2.188-.41 4.239-1.231 6.153a16.834 16.834 0 0 1-3.349 4.99c-1.413 1.413-3.076 2.53-4.99 3.35-1.869.82-3.897 1.23-6.084 1.23h-3.35c-2.187 0-4.238-.41-6.152-1.23a16.851 16.851 0 0 1-4.991-3.35 16.853 16.853 0 0 1-3.349-4.99c-.82-1.915-1.231-3.965-1.231-6.153V24.063c0-2.188.411-4.216 1.231-6.084.82-1.915 1.937-3.578 3.349-4.99a16.852 16.852 0 0 1 4.991-3.35c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.187 0 4.215.41 6.084 1.23 1.914.82 3.577 1.937 4.99 3.35 1.413 1.412 2.529 3.075 3.349 4.99Z" fill="currentColor"/>
</svg>
...@@ -9,7 +9,7 @@ type Props = { ...@@ -9,7 +9,7 @@ type Props = {
const AppContext = createContext<PageProps>({ cookies: '', referrer: '' }); const AppContext = createContext<PageProps>({ cookies: '', referrer: '' });
export function AppWrapper({ children, pageProps }: Props) { export function AppContextProvider({ children, pageProps }: Props) {
return ( return (
<AppContext.Provider value={ pageProps }> <AppContext.Provider value={ pageProps }>
{ children } { children }
......
...@@ -7,6 +7,7 @@ export enum NAMES { ...@@ -7,6 +7,7 @@ export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode',
} }
export function get(name?: NAMES | undefined | null, serverCookie?: string) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
...@@ -99,14 +99,14 @@ function makePolicyMap() { ...@@ -99,14 +99,14 @@ function makePolicyMap() {
...MAIN_DOMAINS, ...MAIN_DOMAINS,
// github avatars // github assets (e.g trustwallet token icons)
'avatars.githubusercontent.com',
// other github assets (e.g trustwallet token icons)
'raw.githubusercontent.com', 'raw.githubusercontent.com',
// auth0 assets // auth0 assets and avatars
's.gravatar.com', 's.gravatar.com',
'i0.wp.com', 'i1.wp.com', 'i2.wp.com', 'i3.wp.com',
'lh3.googleusercontent.com', // google avatars
'avatars.githubusercontent.com', // github avatars
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...networkExternalAssets.map((url) => url.host),
...@@ -123,6 +123,10 @@ function makePolicyMap() { ...@@ -123,6 +123,10 @@ function makePolicyMap() {
'fonts.googleapis.com', 'fonts.googleapis.com',
], ],
'prefetch-src': [
...MAIN_DOMAINS,
],
'object-src': [ 'object-src': [
KEY_WORDS.NONE, KEY_WORDS.NONE,
], ],
......
...@@ -23,7 +23,7 @@ import appConfig from 'configs/app/config'; ...@@ -23,7 +23,7 @@ import appConfig from 'configs/app/config';
// }, // },
// ]).replaceAll('"', '\''); // ]).replaceAll('"', '\'');
const stripTrailingSlash = (str: string) => str.at(-1) === '/' ? str.slice(0, -1) : str; const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const addLeadingSlash = (str: string) => str.at(0) === '/' ? str : '/' + str; const addLeadingSlash = (str: string) => str.at(0) === '/' ? str : '/' + str;
const networkExplorers: Array<NetworkExplorer> = (() => { const networkExplorers: Array<NetworkExplorer> = (() => {
......
import * as Sentry from '@sentry/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AppWrapper } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra'; import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
import AppError from 'ui/shared/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry(); useConfigSentry();
...@@ -30,15 +33,36 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -30,15 +33,36 @@ function MyApp({ Component, pageProps }: AppProps) {
}, },
})); }));
const renderErrorScreen = React.useCallback(() => {
return (
<AppError
statusCode={ 500 }
height="100vh"
display="flex"
flexDirection="column"
alignItems="flex-start"
justifyContent="center"
width="fit-content"
margin="0 auto"
/>
);
}, []);
const handleError = React.useCallback((error: Error) => {
Sentry.captureException(error);
}, []);
return ( return (
<AppWrapper pageProps={ pageProps }> <Chakra theme={ theme } cookies={ pageProps.cookies }>
<QueryClientProvider client={ queryClient }> <ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<Chakra theme={ theme } cookies={ pageProps.cookies }> <AppContextProvider pageProps={ pageProps }>
<Component { ...pageProps }/> <QueryClientProvider client={ queryClient }>
</Chakra> <Component { ...pageProps }/>
<ReactQueryDevtools/> <ReactQueryDevtools/>
</QueryClientProvider> </QueryClientProvider>
</AppWrapper> </AppContextProvider>
</ErrorBoundary>
</Chakra>
); );
} }
......
...@@ -17,33 +17,54 @@ ...@@ -17,33 +17,54 @@
*/ */
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import type { NextPageContext } from 'next'; import type { GetServerSideProps } from 'next';
import NextErrorComponent from 'next/error'; import NextErrorComponent from 'next/error';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import sentryConfig from 'configs/sentry/nextjs'; import sentryConfig from 'configs/sentry/nextjs';
import * as cookies from 'lib/cookies';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props as ServerSidePropsCommon } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsCommon } from 'lib/next/getServerSideProps';
import AppError from 'ui/shared/AppError';
import Page from 'ui/shared/Page/Page';
type ContextOrProps = { type Props = ServerSidePropsCommon & {
req?: NextPageContext['req']; statusCode: number;
res?: NextPageContext['res']; }
err?: NextPageContext['err'] | string;
pathname?: string; const CustomErrorComponent = (props: Props) => {
statusCode?: number; if (props.statusCode === 404) {
}; const title = getNetworkTitle();
const CustomErrorComponent = (props: { statusCode: number }) => { return (
return <NextErrorComponent statusCode={ props.statusCode }/>; <>
<Head>
<title>{ title }</title>
</Head>
<Page>
<AppError statusCode={ 404 } mt="50px"/>
</Page>
</>
);
}
const colorModeCookie = cookies.getFromCookieString(props.cookies, cookies.NAMES.COLOR_MODE);
return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>;
}; };
CustomErrorComponent.getInitialProps = async(contextData: ContextOrProps) => { export default CustomErrorComponent;
export const getServerSideProps: GetServerSideProps = async(context) => {
Sentry.init(sentryConfig); Sentry.init(sentryConfig);
// In case this is running in a serverless function, await this in order to give Sentry // In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits // time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData); await Sentry.captureUnderscoreErrorException(context);
// This will contain the status code of the response const commonSSPResult = await getServerSidePropsCommon(context);
return NextErrorComponent.getInitialProps(contextData as NextPageContext); const commonSSProps = 'props' in commonSSPResult ? commonSSPResult.props : undefined;
};
export default CustomErrorComponent; return { props: { ...commonSSProps, statusCode: context.res.statusCode } };
};
...@@ -10,6 +10,10 @@ const baseStyle: SystemStyleInterpolation = (props) => { ...@@ -10,6 +10,10 @@ const baseStyle: SystemStyleInterpolation = (props) => {
}; };
const sizes = { const sizes = {
'2xl': defineStyle({
fontSize: '48px',
lineHeight: '60px',
}),
lg: defineStyle({ lg: defineStyle({
fontSize: '32px', fontSize: '32px',
lineHeight: '40px', lineHeight: '40px',
......
...@@ -58,9 +58,8 @@ export interface UserInfo { ...@@ -58,9 +58,8 @@ export interface UserInfo {
export interface WatchlistAddress { export interface WatchlistAddress {
address_hash: string; address_hash: string;
name: string; name: string;
address_balance: number; address_balance: string;
coin_name: string; exchange_rate: string;
exchange_rate: number;
notification_settings: NotificationSettings; notification_settings: NotificationSettings;
notification_methods: NotificationMethods; notification_methods: NotificationMethods;
id: string; id: string;
......
...@@ -269,12 +269,16 @@ const BlockDetails = () => { ...@@ -269,12 +269,16 @@ const BlockDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Difficulty" title="Difficulty"
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time.` } hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time.` }
whiteSpace="normal"
wordBreak="break-all"
> >
{ BigNumber(data.difficulty).toFormat() } { BigNumber(data.difficulty).toFormat() }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Total difficulty" title="Total difficulty"
hint="Total difficulty of the chain until this block." hint="Total difficulty of the chain until this block."
whiteSpace="normal"
wordBreak="break-all"
> >
{ BigNumber(data.total_difficulty).toFormat() } { BigNumber(data.total_difficulty).toFormat() }
</DetailsInfoItem> </DetailsInfoItem>
......
...@@ -56,7 +56,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -56,7 +56,7 @@ const ApiKeysPage: React.FC = () => {
const description = ( const description = (
<AccountPageDescription> <AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space } Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="#">“How to use a Blockscout API key”</Link>. <Link href="https://docs.blockscout.com/for-users/api#api-keys">“How to use a Blockscout API key”</Link>.
</AccountPageDescription> </AccountPageDescription>
); );
......
...@@ -31,7 +31,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,7 +31,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
<Td> <Td>
<AddressSnippet address={ item.address_hash }/> <AddressSnippet address={ item.address_hash }/>
</Td> </Td>
<Td> <Td whiteSpace="nowrap">
<TruncatedTextTooltip label={ item.name }> <TruncatedTextTooltip label={ item.name }>
<Tag> <Tag>
{ item.name } { item.name }
......
import { Box, Button, Heading, Icon, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import icon404 from 'icons/error-pages/404.svg';
import icon500 from 'icons/error-pages/500.svg';
import link from 'lib/link/link';
interface Props {
statusCode: number;
className?: string;
}
const ERRORS: Record<string, {icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>; text: string; title: string }> = {
'404': {
icon: icon404,
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.',
},
'500': {
icon: icon500,
title: 'Oops! Something went wrong',
text: 'An unexpected error has occurred. Try reloading the page, or come back soon and try again.',
},
};
const AppError = ({ statusCode, className }: Props) => {
const error = ERRORS[String(statusCode)] || ERRORS['500'];
return (
<Box className={ className }>
<Icon as={ error.icon } width="200px" height="auto"/>
<Heading mt={ 8 } size="2xl" fontFamily="body">{ error.title }</Heading>
<Text variant="secondary" mt={ 3 }> { error.text } </Text>
<Button
mt={ 8 }
size="lg"
variant="outline"
as="a"
href={ link('network_index') }
>
Back to home
</Button>
</Box>
);
};
export default chakra(AppError);
import React from 'react';
interface Props {
children: React.ReactNode;
renderErrorScreen: () => React.ReactNode;
onError?: (error: Error) => void;
}
class ErrorBoundary extends React.PureComponent<Props> {
state = {
hasError: false,
};
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
this.props.onError?.(error);
}
render() {
if (this.state.hasError) {
return this.props.renderErrorScreen();
}
return this.props.children;
}
}
export default ErrorBoundary;
...@@ -10,6 +10,8 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -10,6 +10,8 @@ import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection'; import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
...@@ -33,6 +35,12 @@ const Page = ({ ...@@ -33,6 +35,12 @@ const Page = ({
const directionContext = useScrollDirection(); const directionContext = useScrollDirection();
const renderErrorScreen = React.useCallback(() => {
return wrapChildren ?
<PageContent><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<AppError statusCode={ 500 }/>;
}, [ wrapChildren ]);
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent> <PageContent>{ children }</PageContent>
) : children; ) : children;
...@@ -44,7 +52,9 @@ const Page = ({ ...@@ -44,7 +52,9 @@ const Page = ({
<NavigationDesktop/> <NavigationDesktop/>
<Flex flexDir="column" width="100%"> <Flex flexDir="column" width="100%">
<Header hideOnScrollDown={ hideMobileHeaderOnScrollDown }/> <Header hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
{ renderedChildren } <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren }
</ErrorBoundary>
</Flex> </Flex>
</Flex> </Flex>
</ScrollDirectionContext.Provider> </ScrollDirectionContext.Provider>
......
...@@ -15,7 +15,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -15,7 +15,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
const calculateCut = React.useCallback(() => { const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width; const listWidth = listRef.current?.getBoundingClientRect().width;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width); const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths.at(-1); const menuWidth = tabWidths[tabWidths.length - 1];
if (!listWidth || !menuWidth) { if (!listWidth || !menuWidth) {
return tabs.length; return tabs.length;
......
import { useColorModeValue, chakra, SkeletonCircle, Image } from '@chakra-ui/react'; import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Identicon from 'react-identicons'; import Identicon from 'react-identicons';
...@@ -7,7 +7,28 @@ import type { UserInfo } from 'types/api/account'; ...@@ -7,7 +7,28 @@ import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
const ProfileIcon = chakra(Identicon); // for those who haven't got profile
// or if we cannot download the profile picture for some reasons
const FallbackImage = ({ size, id }: { size: number; id: string }) => {
const bgColor = useToken('colors', useColorModeValue('blackAlpha.100', 'white'));
return (
<Box
flexShrink={ 0 }
maxWidth={ `${ size }px` }
maxHeight={ `${ size }px` }
>
<Box boxSize={ size * 2 } transformOrigin="left top" transform="scale(0.5)" borderRadius="full" overflow="hidden">
<Identicon
bg={ bgColor }
string={ id }
// the displayed size is doubled for retina displays and then scaled down
size={ size * 2 }
/>
</Box>
</Box>
);
};
interface Props { interface Props {
size: number; size: number;
...@@ -18,41 +39,31 @@ interface Props { ...@@ -18,41 +39,31 @@ interface Props {
const UserAvatar = ({ size, data, isFetched }: Props) => { const UserAvatar = ({ size, data, isFetched }: Props) => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies));
const [ isImageLoadError, setImageLoadError ] = React.useState(false);
const sizeString = `${ size }px`; const sizeString = `${ size }px`;
const bgColor = useColorModeValue('blackAlpha.100', 'white');
const handleImageLoadError = React.useCallback(() => {
setImageLoadError(true);
}, []);
if (hasAuth && !isFetched) { if (hasAuth && !isFetched) {
return <SkeletonCircle h={ sizeString } w={ sizeString }/>; return <SkeletonCircle h={ sizeString } w={ sizeString }/>;
} }
if (data?.avatar) {
return (
<Image
flexShrink={ 0 }
src={ data.avatar }
alt={ `Profile picture of ${ data?.name || data?.nickname || '' }` }
w={ sizeString }
minW={ sizeString }
h={ sizeString }
minH={ sizeString }
borderRadius="full"
overflow="hidden"
/>
);
}
return ( return (
<ProfileIcon <Image
flexShrink={ 0 } flexShrink={ 0 }
maxWidth={ sizeString } src={ data?.avatar }
maxHeight={ sizeString } alt={ `Profile picture of ${ data?.name || data?.nickname || '' }` }
string={ data?.email || 'randomness' } w={ sizeString }
// the displayed size is doubled for retina displays minW={ sizeString }
size={ size * 2 } h={ sizeString }
bg={ bgColor } minH={ sizeString }
borderRadius="full" borderRadius="full"
overflow="hidden" overflow="hidden"
fallback={ isImageLoadError || !data?.avatar ? <FallbackImage size={ size } id={ data?.email || 'randomness' }/> : undefined }
onError={ handleImageLoadError }
/> />
); );
}; };
......
...@@ -93,7 +93,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -93,7 +93,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack> </VStack>
</Td> </Td>
<Td> <Td whiteSpace="nowrap">
{ tx.method ? ( { tx.method ? (
<TruncatedTextTooltip label={ tx.method }> <TruncatedTextTooltip label={ tx.method }>
<Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }> <Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }>
......
import { HStack, VStack, Text, Icon, useColorModeValue } from '@chakra-ui/react'; import { HStack, VStack, Text, Icon, useColorModeValue, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
...@@ -8,26 +8,36 @@ import TokensIcon from 'icons/tokens.svg'; ...@@ -8,26 +8,36 @@ import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg'; // import WalletIcon from 'icons/wallet.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50'); const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1); const infoItemsPaddingLeft = { base: 0, lg: 8 };
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
const infoItemsPaddingLeft = { base: 0, lg: 10 };
return ( return (
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700"> <VStack spacing={ 2 } align="stretch" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/> <AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }> <Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.nativeTokenAddress && { appConfig.network.nativeTokenAddress && (
<TokenLogo hash={ appConfig.network.nativeTokenAddress } name={ appConfig.network.name } boxSize={ 4 } mr="10px"/> } <TokenLogo
<Text color={ mainTextColor }>{ `${ appConfig.network.currency.symbol } balance:${ nbsp }` + nativeBalance }</Text> hash={ appConfig.network.nativeTokenAddress }
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text> name={ appConfig.network.name }
</HStack> boxSize={ 4 }
borderRadius="sm"
mr={ 2 }
/>
) }
<Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } balance: </Text>
<CurrencyValue
value={ item.address_balance }
exchangeRate={ item.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
accuracy={ 2 }
accuracyUsd={ 2 }
/>
</Flex>
{ item.tokens_count && ( { item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/> <Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
......
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