Commit 59af04fd authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into pw-setup-pt2

parents a7345b51 a1116e5c
...@@ -66,7 +66,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -66,7 +66,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` | | NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` | | NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
...@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
| Property | Type | Description | Example value | Property | Type | Description | Example value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` | | id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` |
| external | `boolean` | If true means that the application opens in a new window, but not in an iframe. | `true` |
| title | `string` | Displayed title of the app. | `'The App'` | | title | `string` | Displayed title of the app. | `'The App'` |
| logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` | | logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` | | shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
......
...@@ -60,9 +60,9 @@ const config = Object.freeze({ ...@@ -60,9 +60,9 @@ const config = Object.freeze({
name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_NAME), name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_NAME),
symbol: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL), symbol: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL),
decimals: Number(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS)) || DEFAULT_CURRENCY_DECIMALS, decimals: Number(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS)) || DEFAULT_CURRENCY_DECIMALS,
address: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS),
}, },
assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME), assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME),
nativeTokenAddress: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS),
explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [], explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [],
verificationType: process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE || 'mining', verificationType: process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE || 'mining',
}, },
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m6 5.293 2.475-2.475.707.707L6.707 6l2.475 2.475-.707.707L6 6.707 3.525 9.182l-.707-.707L5.293 6 2.818 3.525l.707-.707L6 5.293Z" fill="currentColor"/>
</svg>
...@@ -19,5 +19,6 @@ ...@@ -19,5 +19,6 @@
"app_index": "/apps/:id", "app_index": "/apps/:id",
"search_results": "/search-results", "search_results": "/search-results",
"other": "/search-results", "other": "/search-results",
"auth": "/auth/auth0" "auth": "/auth/auth0",
"stats": "/stats"
} }
...@@ -85,6 +85,10 @@ export const ROUTES = { ...@@ -85,6 +85,10 @@ export const ROUTES = {
pattern: PATHS.app_index, pattern: PATHS.app_index,
}, },
stats: {
pattern: PATHS.stats,
},
// SEARCH // SEARCH
search_results: { search_results: {
pattern: PATHS.search_results, pattern: PATHS.search_results,
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Stats from '../ui/pages/Stats';
const StatsPage: NextPage = () => {
return (
<>
<Head><title>Ethereum Stats</title></Head>
<Stats/>
</>
);
};
export default StatsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
...@@ -19,15 +19,16 @@ export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string } ...@@ -19,15 +19,16 @@ export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = { export type AppItemPreview = {
id: string; id: string;
external: boolean;
title: string; title: string;
logo: string; logo: string;
shortDescription: string; shortDescription: string;
categories: Array<MarketplaceCategoriesIds>; categories: Array<MarketplaceCategoriesIds>;
url: string;
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
author: string; author: string;
url: string;
description: string; description: string;
site?: string; site?: string;
twitter?: string; twitter?: string;
......
...@@ -16,4 +16,5 @@ export enum QueryKeys { ...@@ -16,4 +16,5 @@ export enum QueryKeys {
chartsMarket = 'charts-market', chartsMarket = 'charts-market',
indexBlocks='indexBlocks', indexBlocks='indexBlocks',
indexTxs='indexTxs', indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url'
} }
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'transactions',
'gas',
}
export type StatsSectionIds = keyof typeof StatsSectionId;
export type StatsSection = { id: StatsSectionIds; value: string }
export enum StatsIntervalId {
'all',
'oneMonth',
'threeMonths',
'sixMonths',
'oneYear',
}
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export type StatsInterval = { id: StatsIntervalIds; value: string }
...@@ -120,7 +120,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -120,7 +120,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
{ data && ( { data && (
<Box marginBottom={ 5 }> <Box marginBottom={ 5 }>
<Controller <Controller
...@@ -144,14 +144,14 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -144,14 +144,14 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
disabled={ !isValid } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Generate API key' } { data ? 'Save' : 'Generate API key' }
</Button> </Button>
</Box> </Box>
</> </form>
); );
}; };
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -8,9 +7,9 @@ import type { AppItemPreview } from 'types/client/apps'; ...@@ -8,9 +7,9 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg'; import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import AppCardLink from './AppCardLink';
import { APP_CATEGORIES } from './constants'; import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview { interface Props extends AppItemPreview {
...@@ -19,7 +18,10 @@ interface Props extends AppItemPreview { ...@@ -19,7 +18,10 @@ interface Props extends AppItemPreview {
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
} }
const AppCard = ({ id, const AppCard = ({
id,
url,
external,
title, title,
logo, logo,
shortDescription, shortDescription,
...@@ -85,11 +87,12 @@ const AppCard = ({ id, ...@@ -85,11 +87,12 @@ const AppCard = ({ id,
size={{ base: 'xs', sm: 'sm' }} size={{ base: 'xs', sm: 'sm' }}
fontWeight="semibold" fontWeight="semibold"
> >
<NextLink href={ link('app_index', { id: id }) } passHref> <AppCardLink
<LinkOverlay> id={ id }
{ title } url={ url }
</LinkOverlay> external={ external }
</NextLink> title={ title }
/>
</Heading> </Heading>
<Text <Text
......
import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppLink = ({ url, external, id, title }: Props) => {
return external ? (
<LinkOverlay href={ url } isExternal={ true }>
{ title }
</LinkOverlay>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
);
};
export default AppLink;
...@@ -41,6 +41,8 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, ...@@ -41,6 +41,8 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps,
<AppCard <AppCard
onInfoClick={ onAppClick } onInfoClick={ onAppClick }
id={ app.id } id={ app.id }
external={ app.external }
url={ app.url }
title={ app.title } title={ app.title }
logo={ app.logo } logo={ app.logo }
shortDescription={ app.shortDescription } shortDescription={ app.shortDescription }
......
import { import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody, Box, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import NextLink from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
...@@ -15,9 +14,9 @@ import twIcon from 'icons/social/tweet.svg'; ...@@ -15,9 +14,9 @@ import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import AppModalLink from './AppModalLink';
import { APP_CATEGORIES } from './constants'; import { APP_CATEGORIES } from './constants';
type Props = { type Props = {
...@@ -35,6 +34,8 @@ const AppModal = ({ ...@@ -35,6 +34,8 @@ const AppModal = ({
}: Props) => { }: Props) => {
const { const {
title, title,
url,
external,
author, author,
description, description,
site, site,
...@@ -79,7 +80,9 @@ const AppModal = ({ ...@@ -79,7 +80,9 @@ const AppModal = ({
gridTemplateColumns={{ base: 'auto 1fr' }} gridTemplateColumns={{ base: 'auto 1fr' }}
paddingRight={{ sm: 12 }} paddingRight={{ sm: 12 }}
> >
<Box <Flex
alignItems="center"
justifyContent="center"
w={{ base: '72px', sm: '144px' }} w={{ base: '72px', sm: '144px' }}
h={{ base: '72px', sm: '144px' }} h={{ base: '72px', sm: '144px' }}
marginRight={{ base: 6, sm: 8 }} marginRight={{ base: 6, sm: 8 }}
...@@ -89,7 +92,7 @@ const AppModal = ({ ...@@ -89,7 +92,7 @@ const AppModal = ({
src={ logo } src={ logo }
alt={ `${ title } app icon` } alt={ `${ title } app icon` }
/> />
</Box> </Flex>
<Heading <Heading
as="h2" as="h2"
...@@ -117,16 +120,12 @@ const AppModal = ({ ...@@ -117,16 +120,12 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }} marginTop={{ base: 6, sm: 0 }}
> >
<Box display="flex"> <Box display="flex">
<NextLink href={ link('app_index', { id: id }) } passHref> <AppModalLink
<Button id={ id }
as="a" url={ url }
size="sm" external={ external }
marginRight={ 2 } title={ title }
width={{ base: '100%', sm: 'auto' }} />
>
Launch app
</Button>
</NextLink>
<IconButton <IconButton
aria-label="Mark as favorite" aria-label="Mark as favorite"
......
import { Button } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppModalLink = ({ url, external, id }: Props) => {
const buttonProps = {
size: 'sm',
marginRight: 2,
width: { base: '100%', sm: 'auto' },
...(external ? {
target: '_blank',
rel: 'noopener noreferrer',
} : {}),
};
return external ? (
<Button
as="a"
href={ url }
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
{ ...buttonProps }
>Launch app</Button>
</NextLink>
);
};
export default AppModalLink;
...@@ -104,7 +104,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -104,7 +104,7 @@ const BlocksContent = ({ type }: Props) => {
return ( return (
<> <>
{ data ? { !isLoading ?
totalText : totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/> <Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
} }
......
...@@ -139,7 +139,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -139,7 +139,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box> <Box>
<Controller <Controller
name="contract_address_hash" name="contract_address_hash"
...@@ -170,14 +170,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -170,14 +170,14 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
disabled={ !isValid } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Create custom ABI' } { data ? 'Save' : 'Create custom ABI' }
</Button> </Button>
</Box> </Box>
</> </form>
); );
}; };
......
...@@ -100,7 +100,6 @@ const LatestBlocksItem = ({ tx }: Props) => { ...@@ -100,7 +100,6 @@ const LatestBlocksItem = ({ tx }: Props) => {
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
target="_self"
/> />
</Address> </Address>
</Flex> </Flex>
......
...@@ -29,7 +29,7 @@ const LatestTxsNotice = ({ className }: Props) => { ...@@ -29,7 +29,7 @@ const LatestTxsNotice = ({ className }: Props) => {
<> <>
<Spinner size="sm" mr={ 3 }/> <Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text> <Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link href={ txsUrl }>Show in list</Link> <Link href={ txsUrl }>View all</Link>
</> </>
); );
} }
......
import { useToken } from '@chakra-ui/react'; import { useToken } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ChainIndicatorChartData } from './types'; import type { TimeChartData } from 'ui/shared/chart/types';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine'; import ChartLine from 'ui/shared/chart/ChartLine';
...@@ -11,7 +11,7 @@ import useChartSize from 'ui/shared/chart/useChartSize'; ...@@ -11,7 +11,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props { interface Props {
data: ChainIndicatorChartData; data: TimeChartData;
caption?: string; caption?: string;
} }
......
import { Flex, Spinner } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ChainIndicatorChartData } from './types'; import type { TimeChartData } from 'ui/shared/chart/types';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ChainIndicatorChart from './ChainIndicatorChart'; import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<ChainIndicatorChartData>; type Props = UseQueryResult<TimeChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => { const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
return <Spinner size="md" m="auto"/>; return <ContentLoader mt="auto"/>;
} }
if (isError) { if (isError) {
......
...@@ -18,7 +18,7 @@ interface Props { ...@@ -18,7 +18,7 @@ interface Props {
} }
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => { const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'gray.900'); const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
......
...@@ -40,8 +40,8 @@ const ChainIndicators = () => { ...@@ -40,8 +40,8 @@ const ChainIndicators = () => {
() => fetch('/node-api/stats'), () => fetch('/node-api/stats'),
); );
const bgColor = useColorModeValue('white', 'gray.900'); const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'black'); const listBgColor = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) { if (indicators.length === 0) {
return null; return null;
...@@ -91,7 +91,16 @@ const ChainIndicators = () => { ...@@ -91,7 +91,16 @@ const ChainIndicators = () => {
<ChainIndicatorChartContainer { ...queryResult }/> <ChainIndicatorChartContainer { ...queryResult }/>
</Flex> </Flex>
{ indicators.length > 1 && ( { indicators.length > 1 && (
<Flex flexShrink={ 0 } flexDir="column" as="ul" p={ 3 } borderRadius="lg" bgColor={ listBgColor } rowGap={ 3 } order={{ base: 1, lg: 2 }}> <Flex
flexShrink={ 0 }
flexDir="column"
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
{ indicators.map((indicator) => ( { indicators.map((indicator) => (
<ChainIndicatorItem <ChainIndicatorItem
key={ indicator.id } key={ indicator.id }
......
import type { ChartTransactionResponse, ChartMarketResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { Stats } from 'types/api/stats'; import type { Stats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import type { TimeChartDataItem } from 'ui/shared/chart/types'; import type { TimeChartData } from 'ui/shared/chart/types';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket; export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
...@@ -16,7 +16,7 @@ export interface TChainIndicator<Q extends ChartsQueryKeys> { ...@@ -16,7 +16,7 @@ export interface TChainIndicator<Q extends ChartsQueryKeys> {
api: { api: {
queryName: Q; queryName: Q;
path: string; path: string;
dataFn: (response: ChartsResponse<Q>) => ChainIndicatorChartData; dataFn: (response: ChartsResponse<Q>) => TimeChartData;
}; };
} }
...@@ -24,5 +24,3 @@ export type ChartsResponse<Q extends ChartsQueryKeys> = ...@@ -24,5 +24,3 @@ export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse : Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
Q extends QueryKeys.chartsMarket ? ChartMarketResponse : Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never; never;
export type ChainIndicatorChartData = Array<TimeChartDataItem>;
...@@ -2,13 +2,14 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -2,13 +2,14 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys, ChainIndicatorChartData } from './types'; import type { TChainIndicator, ChartsResponse, ChartsQueryKeys } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
type NotUndefined<T> = T extends undefined ? never : T; type NotUndefined<T> = T extends undefined ? never : T;
export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<ChainIndicatorChartData> { export default function useFetchChartData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<TimeChartData> {
const fetch = useFetch(); const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>; type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
...@@ -23,6 +24,6 @@ export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: T ...@@ -23,6 +24,6 @@ export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: T
return { return {
...queryResult, ...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data, data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
} as UseQueryResult<ChainIndicatorChartData>; } as UseQueryResult<TimeChartData>;
}, [ indicator, queryResult ]); }, [ indicator, queryResult ]);
} }
...@@ -25,7 +25,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = { ...@@ -25,7 +25,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
.map((item) => ({ date: new Date(item.date), value: item.tx_count })) .map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc), .sort(sortByDateDesc),
name: 'Tx/day', name: 'Tx/day',
valueFormatter: (x) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }), valueFormatter: (x: number) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
} ]), } ]),
}, },
}; };
...@@ -34,7 +34,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -34,7 +34,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
id: 'coin_price', id: 'coin_price',
title: `${ appConfig.network.currency.symbol } price`, title: `${ appConfig.network.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenLogo hash={ appConfig.network.nativeTokenAddress || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>, icon: <TokenLogo hash={ appConfig.network.currency.address || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`, hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: { api: {
queryName: QueryKeys.chartsMarket, queryName: QueryKeys.chartsMarket,
...@@ -44,7 +44,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -44,7 +44,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) })) .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
.sort(sortByDateDesc), .sort(sortByDateDesc),
name: `${ appConfig.network.currency.symbol } price`, name: `${ appConfig.network.currency.symbol } price`,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]), } ]),
}, },
}; };
...@@ -64,7 +64,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -64,7 +64,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) })) .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc), .sort(sortByDateDesc),
name: 'Market cap', name: 'Market cap',
valueFormatter: (x) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }), valueFormatter: (x: number) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
} ]), } ]),
}, },
}; };
......
...@@ -14,13 +14,12 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem'; ...@@ -14,13 +14,12 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const DATA_LIMIT = 3; const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => { const ApiKeysPage: React.FC = () => {
......
...@@ -13,13 +13,12 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; ...@@ -13,13 +13,12 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable'; import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal'; import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const CustomAbiPage: React.FC = () => { const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
......
...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url'; import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -27,7 +28,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -27,7 +28,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
}, []); }, []);
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>( const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ], [ QueryKeys.jsonRpcUrl ],
async() => await fetch(`/node-api/config/json-rpc-url`), async() => await fetch(`/node-api/config/json-rpc-url`),
{ refetchOnMount: false }, { refetchOnMount: false },
); );
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import StatsFilters from '../stats/StatsFilters';
import WidgetsList from '../stats/WidgetsList';
const Stats = () => {
return (
<Page>
<PageTitle text="Ethereum Stats"/>
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters/>
</Box>
<WidgetsList/>
</Page>
);
};
export default Stats;
...@@ -93,7 +93,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -93,7 +93,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }> <Box marginBottom={ 5 }>
<Controller <Controller
name="address" name="address"
...@@ -119,14 +119,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -119,14 +119,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
disabled={ !isValid } disabled={ !isValid }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
</Button> </Button>
</Box> </Box>
</> </form>
); );
}; };
......
...@@ -92,7 +92,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -92,7 +92,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
return ( return (
<> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }> <Box marginBottom={ 5 }>
<Controller <Controller
name="transaction" name="transaction"
...@@ -118,14 +118,14 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => ...@@ -118,14 +118,14 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
disabled={ !isValid } disabled={ !isValid }
isLoading={ pending } isLoading={ pending }
> >
{ data ? 'Save changes' : 'Add tag' } { data ? 'Save changes' : 'Add tag' }
</Button> </Button>
</Box> </Box>
</> </form>
); );
}; };
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
GridItem, GridItem,
Text, Text,
HStack, HStack,
chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -151,7 +152,12 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -151,7 +152,12 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}, [ changeToDataScreen ]); }, [ changeToDataScreen ]);
return ( return (
<Box width={{ base: 'auto', lg: `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` }} maxWidth="844px"> <chakra.form
noValidate
width={{ base: 'auto', lg: `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` }}
maxWidth="844px"
onSubmit={ handleSubmit(onSubmit) }
>
{ isAlertVisible && <Box mb={ 4 }><FormSubmitAlert/></Box> } { isAlertVisible && <Box mb={ 4 }><FormSubmitAlert/></Box> }
<Text size="sm" variant="secondary" paddingBottom={ 5 }>Company info</Text> <Text size="sm" variant="secondary" paddingBottom={ 5 }>Company info</Text>
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 5 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 5 }>
...@@ -230,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -230,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
disabled={ !isValid } disabled={ !isValid }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
...@@ -245,7 +251,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -245,7 +251,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
Cancel Cancel
</Button> </Button>
</HStack> </HStack>
</Box> </chakra.form>
); );
}; };
......
import { Box, Text } from '@chakra-ui/react'; import { Box, Text, chakra } from '@chakra-ui/react';
import { keyframes } from '@chakra-ui/system'; import { keyframes } from '@chakra-ui/system';
import React from 'react'; import React from 'react';
...@@ -7,9 +7,13 @@ const runnerAnimation = keyframes` ...@@ -7,9 +7,13 @@ const runnerAnimation = keyframes`
100% { left: '100%'; transform: translateX(-99%); } 100% { left: '100%'; transform: translateX(-99%); }
`; `;
const ContentLoader = () => { interface Props {
className?: string;
}
const ContentLoader = ({ className }: Props) => {
return ( return (
<Box display="inline-block"> <Box display="inline-block" className={ className }>
<Box <Box
width="100%" width="100%"
height="6px" height="6px"
...@@ -31,4 +35,4 @@ const ContentLoader = () => { ...@@ -31,4 +35,4 @@ const ContentLoader = () => {
); );
}; };
export default ContentLoader; export default chakra(ContentLoader);
import { Input, InputGroup, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react'; import { chakra, Icon, IconButton, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import crossIcon from 'icons/cross.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
type Props = { type Props = {
...@@ -13,6 +14,8 @@ type Props = { ...@@ -13,6 +14,8 @@ type Props = {
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target; const { value } = event.target;
...@@ -21,6 +24,12 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) = ...@@ -21,6 +24,12 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
onChange(value); onChange(value);
}, [ onChange ]); }, [ onChange ]);
const handleFilterQueryClear = useCallback(() => {
setFilterQuery('');
onChange('');
inputRef?.current?.focus();
}, [ onChange ]);
return ( return (
<InputGroup <InputGroup
size={ size } size={ size }
...@@ -29,10 +38,11 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) = ...@@ -29,10 +38,11 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
<InputLeftElement <InputLeftElement
pointerEvents="none" pointerEvents="none"
> >
<Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> <Icon as={ searchIcon } color={ iconColor }/>
</InputLeftElement> </InputLeftElement>
<Input <Input
ref={ inputRef }
size={ size } size={ size }
value={ filterQuery } value={ filterQuery }
onChange={ handleFilterQueryChange } onChange={ handleFilterQueryChange }
...@@ -40,6 +50,21 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) = ...@@ -40,6 +50,21 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
borderWidth="2px" borderWidth="2px"
textOverflow="ellipsis" textOverflow="ellipsis"
/> />
{ filterQuery ? (
<InputRightElement>
<IconButton
colorScheme="gray"
aria-label="Clear the filter input"
title="Clear the filter input"
w={ 6 }
h={ 6 }
icon={ <Icon as={ crossIcon } w={ 4 } h={ 4 } color={ iconColor }/> }
size="sm"
onClick={ handleFilterQueryClear }
/>
</InputRightElement>
) : null }
</InputGroup> </InputGroup>
); );
}; };
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
interface Props<TData> { interface Props<TData> {
...@@ -38,8 +39,10 @@ export default function FormModal<TData>({ ...@@ -38,8 +39,10 @@ export default function FormModal<TData>({
onClose(); onClose();
}, [ onClose, setAlertVisible ]); }, [ onClose, setAlertVisible ]);
const isMobile = useIsMobile();
return ( return (
<Modal isOpen={ isOpen } onClose={ onModalClose } size={{ base: 'full', lg: 'md' }}> <Modal isOpen={ isOpen } onClose={ onModalClose } size={ isMobile ? 'full' : 'md' }>
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
......
...@@ -17,7 +17,7 @@ interface Props { ...@@ -17,7 +17,7 @@ interface Props {
target?: HTMLAttributeAnchorTarget; target?: HTMLAttributeAnchorTarget;
} }
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target }: Props) => { const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self' }: Props) => {
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx', { id: id || hash }); url = link('tx', { id: id || hash });
...@@ -53,7 +53,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -53,7 +53,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
<Link <Link
className={ className } className={ className }
href={ url } href={ url }
target={ target || '_blank' } target={ target }
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
> >
......
...@@ -86,11 +86,19 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, anchorEl, ...props ...@@ -86,11 +86,19 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, anchorEl, ...props
.selectAll('.ChartTooltip__point') .selectAll('.ChartTooltip__point')
.attr('transform', (cur, i) => { .attr('transform', (cur, i) => {
const index = bisectDate(data[i].items, xDate, 1); const index = bisectDate(data[i].items, xDate, 1);
const d0 = data[i].items[index - 1]; const d0 = data[i].items[index - 1] as TimeChartItem | undefined;
const d1 = data[i].items[index]; const d1 = data[i].items[index] as TimeChartItem | undefined;
const d = xDate.getTime() - d0?.date.getTime() > d1?.date.getTime() - xDate.getTime() ? d1 : d0; const d = (() => {
if (!d0) {
if (d.date === undefined && d.value === undefined) { return d1;
}
if (!d1) {
return d0;
}
return xDate.getTime() - d0.date.getTime() > d1.date.getTime() - xDate.getTime() ? d1 : d0;
})();
if (d?.date === undefined && d?.value === undefined) {
// move point out of container // move point out of container
return 'translate(-100,-100)'; return 'translate(-100,-100)';
} }
......
...@@ -51,7 +51,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -51,7 +51,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}> <Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => { { SOCIAL_LINKS.map(sl => {
return ( return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }> <Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/> <Icon as={ sl.icon } boxSize={ 5 }/>
</Link> </Link>
); );
......
import { Box, Button, Grid, Heading, Text, useColorModeValue } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import ChartWidgetGraph from './ChartWidgetGraph';
import { demoData } from './constants/demo-data';
type Props = {
apiMethodURL: string;
title: string;
description: string;
}
const ChartWidget = ({ title, description }: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const handleZoom = useCallback(() => {
setIsZoomResetInitial(false);
}, []);
const handleZoomResetClick = useCallback(() => {
setIsZoomResetInitial(true);
}, []);
return (
<Box
padding={{ base: 3, md: 4 }}
borderRadius="md"
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<Grid>
<Heading
mb={ 1 }
size={{ base: 'xs', md: 'sm' }}
>
{ title }
</Heading>
<Text
mb={ 1 }
gridColumn={ 1 }
as="p"
variant="secondary"
fontSize="xs"
>
{ description }
</Text>
{ !isZoomResetInitial && (
<Button
gridColumn={ 2 }
justifySelf="end"
alignSelf="center"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
>
Reset zoom
</Button>
) }
</Grid>
<ChartWidgetGraph
items={ demoData }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
/>
</Box>
);
};
export default ChartWidget;
import { useToken } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props {
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
}
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const overlayRef = React.useRef<SVGRectElement>(null);
const color = useToken('colors', 'blue.500');
const displayedData = useMemo(() => items.slice(range[0], range[1]).map((d) =>
({ ...d, date: new Date(d.date) })), [ items, range ]);
const chartData = [ { items: items, name: 'chart', color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: 'chart', color } ],
width: innerWidth,
height: innerHeight,
});
const handleRangeSelect = React.useCallback((nextRange: [ number, number ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]);
onZoom();
}, [ onZoom, range ]);
useEffect(() => {
if (isZoomResetInitial) {
setRange([ 0, Infinity ]);
}
}, [ isZoomResetInitial ]);
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<ChartGridLine
type="horizontal"
scale={ yScale }
ticks={ 3 }
size={ innerWidth }
disableAnimation
/>
<ChartArea
data={ displayedData }
color={ color }
xScale={ xScale }
yScale={ yScale }
/>
<ChartLine
data={ displayedData }
xScale={ xScale }
yScale={ yScale }
stroke={ color }
animation="left"
strokeWidth={ 3 }
/>
<ChartAxis
type="left"
scale={ yScale }
ticks={ 5 }
tickFormat={ yTickFormat }
disableAnimation
/>
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartAxis
type="bottom"
scale={ xScale }
transform={ `translate(0, ${ innerHeight })` }
ticks={ 5 }
anchorEl={ overlayRef.current }
disableAnimation
/>
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
xScale={ xScale }
yScale={ yScale }
data={ chartData }
/>
<ChartSelectionX
anchorEl={ overlayRef.current }
height={ innerHeight }
scale={ xScale }
data={ chartData }
onSelect={ handleRangeSelect }
/>
</ChartOverlay>
</g>
</svg>
);
};
export default React.memo(ChartWidgetGraph);
import { Box, Button, Icon, Menu, MenuButton, MenuItemOption, MenuList, MenuOptionGroup } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
type Props<T extends string> = {
items: Array<{id: T; value: string}>;
selectedId: T;
onSelect: (id: T) => void;
}
export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelect }: Props<T>) {
const selectedCategory = items.find(category => category.id === selectedId);
const handleSelection = useCallback((id: string | Array<string>) => {
const selectedId = Array.isArray(id) ? id[0] : id;
onSelect(selectedId as T);
}, [ onSelect ]);
return (
<Menu
>
<MenuButton
as={ Button }
size="md"
variant="outline"
colorScheme="gray"
w="100%"
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory?.value }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
<MenuOptionGroup
value={ selectedId }
type="radio"
onChange={ handleSelection }
>
{ items.map((item) => (
<MenuItemOption
key={ item.id }
value={ item.id }
>
{ item.value }
</MenuItemOption>
)) }
</MenuOptionGroup>
</MenuList>
</Menu>
);
}
export default StatsDropdownMenu;
import { Grid, GridItem } from '@chakra-ui/react';
import debounce from 'lodash/debounce';
import React, { useCallback, useState } from 'react';
import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import FilterInput from 'ui/shared/FilterInput';
import { STATS_INTERVALS, STATS_SECTIONS } from './constants';
import StatsDropdownMenu from './StatsDropdownMenu';
const sectionsList = Object.keys(STATS_SECTIONS).map((id: string) => ({
id: id,
value: STATS_SECTIONS[id as StatsSectionIds],
})) as Array<StatsSection>;
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
value: STATS_INTERVALS[id as StatsIntervalIds],
})) as Array<StatsInterval>;
const StatsFilters = () => {
const [ selectedSectionId, setSelectedSectionId ] = useState<StatsSectionIds>('all');
const [ selectedIntervalId, setSelectedIntervalId ] = useState<StatsIntervalIds>('all');
const [ , setFilterQuery ] = useState('');
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
return (
<Grid
gap={ 2 }
templateAreas={{
base: `"input input"
"section interval"`,
lg: `"input section interval"`,
}}
gridTemplateColumns={{ lg: '1fr auto auto' }}
>
<GridItem
w="100%"
area="input"
>
<FilterInput
onChange={ debounceFilterCharts }
placeholder="Find chart, metric..."/>
</GridItem>
<GridItem
w={{ base: '100%', lg: 'auto' }}
area="section"
>
<StatsDropdownMenu
items={ sectionsList }
selectedId={ selectedSectionId }
onSelect={ setSelectedSectionId }
/>
</GridItem>
<GridItem
w={{ base: '100%', lg: 'auto' }}
area="interval"
>
<StatsDropdownMenu
items={ intervalList }
selectedId={ selectedIntervalId }
onSelect={ setSelectedIntervalId }
/>
</GridItem>
</Grid>
);
};
export default StatsFilters;
import { Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react';
import React from 'react';
import ChartWidget from './ChartWidget';
import { statisticsChartsScheme } from './constants/charts-scheme';
const WidgetsList = () => {
return (
<List>
{
statisticsChartsScheme.map((section) => (
<ListItem
key={ section.id }
mb={ 8 }
_last={{
marginBottom: 0,
}}
>
<Heading
size="md"
mb={ 4 }
>
{ section.title }
</Heading>
<Grid
templateColumns={{
sm: 'repeat(2, 1fr)',
}}
gap={ 4 }
>
{ section.charts.map((chart) => (
<GridItem key={ chart.id }>
<ChartWidget
apiMethodURL={ chart.apiMethodURL }
title={ chart.title }
description={ chart.description }
/>
</GridItem>
)) }
</Grid>
</ListItem>
))
}
</List>
);
};
export default WidgetsList;
export const statisticsChartsScheme = [
{
id: 'blocks',
title: 'Blocks',
charts: [
{
id: 'new-blocks',
title: 'New blocks',
description: 'New blocks number per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
{
id: 'average-block-size',
title: 'Average block size',
description: 'Average size of blocks in bytes per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'transaction-fees',
title: 'Transaction fees',
description: 'Amount of tokens paid as fees per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
{
id: 'native-coin-holders-growth',
title: 'Native coin holders growth',
description: 'Total token holders number per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
],
},
];
import type { TimeChartItem } from '../../shared/chart/types';
export const demoData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
date: new Date('2022-10-18T00:00:00.000Z'),
value: 370100,
}, { date: new Date('2022-10-19T00:00:00.000Z'), value: 283234 }, { date: new Date('2022-10-20T00:00:00.000Z'), value: 420910 }, {
date: new Date('2022-10-21T00:00:00.000Z'),
value: 411988,
}, { date: new Date('2022-10-22T00:00:00.000Z'), value: 356269 }, { date: new Date('2022-10-23T00:00:00.000Z'), value: 389747 }, {
date: new Date('2022-10-24T00:00:00.000Z'),
value: 387130,
}, { date: new Date('2022-10-25T00:00:00.000Z'), value: 428785 }, { date: new Date('2022-10-26T00:00:00.000Z'), value: 63809 }, {
date: new Date('2022-10-27T00:00:00.000Z'),
value: 50518,
}, { date: new Date('2022-10-28T00:00:00.000Z'), value: 39087 }, { date: new Date('2022-10-29T00:00:00.000Z'), value: 36789 }, {
date: new Date('2022-10-30T00:00:00.000Z'),
value: 48569,
}, { date: new Date('2022-10-31T00:00:00.000Z'), value: 62519 }, { date: new Date('2022-11-01T00:00:00.000Z'), value: 152059 }, {
date: new Date('2022-11-02T00:00:00.000Z'),
value: 63743,
}, { date: new Date('2022-11-03T00:00:00.000Z'), value: 83667 }, { date: new Date('2022-11-04T00:00:00.000Z'), value: 91725 }, {
date: new Date('2022-11-05T00:00:00.000Z'),
value: 82897,
}, { date: new Date('2022-11-06T00:00:00.000Z'), value: 62477 }, { date: new Date('2022-11-07T00:00:00.000Z'), value: 58131 }, {
date: new Date('2022-11-08T00:00:00.000Z'),
value: 74197,
}, { date: new Date('2022-11-09T00:00:00.000Z'), value: 43691 }, { date: new Date('2022-11-10T00:00:00.000Z'), value: 92887 }, {
date: new Date('2022-11-11T00:00:00.000Z'),
value: 79493,
}, { date: new Date('2022-11-12T00:00:00.000Z'), value: 86764 }, { date: new Date('2022-11-13T00:00:00.000Z'), value: 22338 }, {
date: new Date('2022-11-14T00:00:00.000Z'),
value: 62266,
}, { date: new Date('2022-11-15T00:00:00.000Z'), value: 84084 }, { date: new Date('2022-11-16T00:00:00.000Z'), value: 75898 } ];
import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats';
export const STATS_SECTIONS: { [key in StatsSectionIds]: string } = {
all: 'All stats',
accounts: 'Accounts',
blocks: 'Blocks',
transactions: 'Transactions',
gas: 'Gas',
};
export const STATS_INTERVALS: { [key in StatsIntervalIds]: string } = {
all: 'All time',
oneMonth: '1 month',
threeMonths: '3 months',
sixMonths: '6 months',
oneYear: '1 year',
};
...@@ -17,7 +17,6 @@ import useTxsSort from './useTxsSort'; ...@@ -17,7 +17,6 @@ import useTxsSort from './useTxsSort';
type Props = { type Props = {
queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs; queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs;
showDescription?: boolean;
stateFilter?: TTxsFilters['filter']; stateFilter?: TTxsFilters['filter'];
apiPath: string; apiPath: string;
showBlockInfo?: boolean; showBlockInfo?: boolean;
...@@ -25,7 +24,6 @@ type Props = { ...@@ -25,7 +24,6 @@ type Props = {
const TxsContent = ({ const TxsContent = ({
queryName, queryName,
showDescription,
stateFilter, stateFilter,
apiPath, apiPath,
showBlockInfo = true, showBlockInfo = true,
...@@ -81,7 +79,6 @@ const TxsContent = ({ ...@@ -81,7 +79,6 @@ const TxsContent = ({
return ( return (
<> <>
{ showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> }
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/> <TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/>
{ content } { content }
</> </>
......
...@@ -59,7 +59,6 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo ...@@ -59,7 +59,6 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
target="_self"
/> />
</Address> </Address>
</Flex> </Flex>
......
...@@ -43,7 +43,7 @@ const TxsNewItemNotice = ({ children, className }: Props) => { ...@@ -43,7 +43,7 @@ const TxsNewItemNotice = ({ children, className }: Props) => {
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }> <Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
<Spinner size="sm" mr={ 3 }/> <Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text> <Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link onClick={ handleClick }>Show in list</Link> <Link onClick={ handleClick }>View in list</Link>
</Alert> </Alert>
); );
})(); })();
......
...@@ -13,7 +13,6 @@ const TxsTab = ({ tab }: Props) => { ...@@ -13,7 +13,6 @@ const TxsTab = ({ tab }: Props) => {
return ( return (
<TxsContent <TxsContent
queryName={ QueryKeys.txsValidate } queryName={ QueryKeys.txsValidate }
showDescription
stateFilter="validated" stateFilter="validated"
apiPath="/node-api/transactions" apiPath="/node-api/transactions"
/> />
......
...@@ -87,7 +87,6 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -87,7 +87,6 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
target="_self"
/> />
</Address> </Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
......
...@@ -151,7 +151,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -151,7 +151,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
), []); ), []);
return ( return (
<> <form noValidate onSubmit={ handleSubmit(onSubmit) }>
<Box marginBottom={ 5 }> <Box marginBottom={ 5 }>
<Controller <Controller
name="address" name="address"
...@@ -189,14 +189,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -189,14 +189,14 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
onClick={ handleSubmit(onSubmit) } type="submit"
isLoading={ pending } isLoading={ pending }
disabled={ !isValid } disabled={ !isValid }
> >
{ data ? 'Save changes' : 'Add address' } { data ? 'Save changes' : 'Add address' }
</Button> </Button>
</Box> </Box>
</> </form>
); );
}; };
......
...@@ -20,9 +20,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -20,9 +20,9 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" 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 }/>
<Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }> <Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.nativeTokenAddress && ( { appConfig.network.currency.address && (
<TokenLogo <TokenLogo
hash={ appConfig.network.nativeTokenAddress } hash={ appConfig.network.currency.address }
name={ appConfig.network.name } name={ appConfig.network.name }
boxSize={ 4 } boxSize={ 4 }
borderRadius="sm" borderRadius="sm"
......
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