mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Merge branch 'pw-setup-pt2' into pw-tests
This commit is contained in:
@@ -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_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_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_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` |
|
||||
@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| Property | Type | Description | Example value
|
||||
| --- | --- | --- | --- |
|
||||
| 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'` |
|
||||
| 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'` |
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-1
@@ -19,5 +19,6 @@
|
||||
"app_index": "/apps/:id",
|
||||
"search_results": "/search-results",
|
||||
"other": "/search-results",
|
||||
"auth": "/auth/auth0"
|
||||
"auth": "/auth/auth0",
|
||||
"stats": "/stats"
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@ export const ROUTES = {
|
||||
pattern: PATHS.app_index,
|
||||
},
|
||||
|
||||
stats: {
|
||||
pattern: PATHS.stats,
|
||||
},
|
||||
|
||||
// SEARCH
|
||||
search_results: {
|
||||
pattern: PATHS.search_results,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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 }
|
||||
|
||||
export type AppItemPreview = {
|
||||
id: string;
|
||||
external: boolean;
|
||||
title: string;
|
||||
logo: string;
|
||||
shortDescription: string;
|
||||
categories: Array<MarketplaceCategoriesIds>;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type AppItemOverview = AppItemPreview & {
|
||||
author: string;
|
||||
url: string;
|
||||
description: string;
|
||||
site?: string;
|
||||
twitter?: string;
|
||||
|
||||
@@ -16,4 +16,5 @@ export enum QueryKeys {
|
||||
chartsMarket = 'charts-market',
|
||||
indexBlocks='indexBlocks',
|
||||
indexTxs='indexTxs',
|
||||
jsonRpcUrl='json-rpc-url'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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 }
|
||||
+12
-9
@@ -1,5 +1,4 @@
|
||||
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import NextLink from 'next/link';
|
||||
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
@@ -8,9 +7,9 @@ import type { AppItemPreview } from 'types/client/apps';
|
||||
import northEastIcon from 'icons/arrows/north-east.svg';
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
import link from 'lib/link/link';
|
||||
import notEmpty from 'lib/notEmpty';
|
||||
|
||||
import AppCardLink from './AppCardLink';
|
||||
import { APP_CATEGORIES } from './constants';
|
||||
|
||||
interface Props extends AppItemPreview {
|
||||
@@ -19,7 +18,10 @@ interface Props extends AppItemPreview {
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
const AppCard = ({ id,
|
||||
const AppCard = ({
|
||||
id,
|
||||
url,
|
||||
external,
|
||||
title,
|
||||
logo,
|
||||
shortDescription,
|
||||
@@ -85,11 +87,12 @@ const AppCard = ({ id,
|
||||
size={{ base: 'xs', sm: 'sm' }}
|
||||
fontWeight="semibold"
|
||||
>
|
||||
<NextLink href={ link('app_index', { id: id }) } passHref>
|
||||
<LinkOverlay>
|
||||
{ title }
|
||||
</LinkOverlay>
|
||||
</NextLink>
|
||||
<AppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
/>
|
||||
</Heading>
|
||||
|
||||
<Text
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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,
|
||||
<AppCard
|
||||
onInfoClick={ onAppClick }
|
||||
id={ app.id }
|
||||
external={ app.external }
|
||||
url={ app.url }
|
||||
title={ app.title }
|
||||
logo={ app.logo }
|
||||
shortDescription={ app.shortDescription }
|
||||
|
||||
+10
-13
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
Box, Button, Flex, 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,
|
||||
} from '@chakra-ui/react';
|
||||
import NextLink from 'next/link';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
|
||||
@@ -15,9 +14,9 @@ import twIcon from 'icons/social/tweet.svg';
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
import { nbsp } from 'lib/html-entities';
|
||||
import link from 'lib/link/link';
|
||||
import notEmpty from 'lib/notEmpty';
|
||||
|
||||
import AppModalLink from './AppModalLink';
|
||||
import { APP_CATEGORIES } from './constants';
|
||||
|
||||
type Props = {
|
||||
@@ -35,6 +34,8 @@ const AppModal = ({
|
||||
}: Props) => {
|
||||
const {
|
||||
title,
|
||||
url,
|
||||
external,
|
||||
author,
|
||||
description,
|
||||
site,
|
||||
@@ -119,16 +120,12 @@ const AppModal = ({
|
||||
marginTop={{ base: 6, sm: 0 }}
|
||||
>
|
||||
<Box display="flex">
|
||||
<NextLink href={ link('app_index', { id: id }) } passHref>
|
||||
<Button
|
||||
as="a"
|
||||
size="sm"
|
||||
marginRight={ 2 }
|
||||
width={{ base: '100%', sm: 'auto' }}
|
||||
>
|
||||
Launch app
|
||||
</Button>
|
||||
</NextLink>
|
||||
<AppModalLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useToken } from '@chakra-ui/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 ChartLine from 'ui/shared/chart/ChartLine';
|
||||
@@ -11,7 +11,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
|
||||
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
|
||||
|
||||
interface Props {
|
||||
data: ChainIndicatorChartData;
|
||||
data: TimeChartData;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Flex, Spinner } from '@chakra-ui/react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
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 ChainIndicatorChart from './ChainIndicatorChart';
|
||||
|
||||
type Props = UseQueryResult<ChainIndicatorChartData>;
|
||||
type Props = UseQueryResult<TimeChartData>;
|
||||
|
||||
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
|
||||
|
||||
const content = (() => {
|
||||
if (isLoading) {
|
||||
return <Spinner size="md" m="auto"/>;
|
||||
return <ContentLoader mt="auto"/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface 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 handleClick = React.useCallback(() => {
|
||||
|
||||
@@ -40,8 +40,8 @@ const ChainIndicators = () => {
|
||||
() => fetch('/node-api/stats'),
|
||||
);
|
||||
|
||||
const bgColor = useColorModeValue('white', 'gray.900');
|
||||
const listBgColor = useColorModeValue('gray.50', 'black');
|
||||
const bgColor = useColorModeValue('white', 'black');
|
||||
const listBgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
|
||||
if (indicators.length === 0) {
|
||||
return null;
|
||||
@@ -91,7 +91,16 @@ const ChainIndicators = () => {
|
||||
<ChainIndicatorChartContainer { ...queryResult }/>
|
||||
</Flex>
|
||||
{ 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) => (
|
||||
<ChainIndicatorItem
|
||||
key={ indicator.id }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { 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;
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface TChainIndicator<Q extends ChartsQueryKeys> {
|
||||
api: {
|
||||
queryName: Q;
|
||||
path: string;
|
||||
dataFn: (response: ChartsResponse<Q>) => ChainIndicatorChartData;
|
||||
dataFn: (response: ChartsResponse<Q>) => TimeChartData;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,5 +24,3 @@ export type ChartsResponse<Q extends ChartsQueryKeys> =
|
||||
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
|
||||
Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
|
||||
never;
|
||||
|
||||
export type ChainIndicatorChartData = Array<TimeChartDataItem>;
|
||||
|
||||
@@ -2,13 +2,14 @@ import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
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';
|
||||
|
||||
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();
|
||||
|
||||
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
|
||||
@@ -23,6 +24,6 @@ export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: T
|
||||
return {
|
||||
...queryResult,
|
||||
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
|
||||
} as UseQueryResult<ChainIndicatorChartData>;
|
||||
} as UseQueryResult<TimeChartData>;
|
||||
}, [ indicator, queryResult ]);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
|
||||
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
|
||||
.sort(sortByDateDesc),
|
||||
name: 'Tx/day',
|
||||
valueFormatter: (x) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
|
||||
valueFormatter: (x: number) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
|
||||
} ]),
|
||||
},
|
||||
};
|
||||
@@ -44,7 +44,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
|
||||
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
|
||||
.sort(sortByDateDesc),
|
||||
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> = {
|
||||
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
|
||||
.sort(sortByDateDesc),
|
||||
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';
|
||||
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
|
||||
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
|
||||
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
|
||||
import SkeletonTable from 'ui/shared/SkeletonTable';
|
||||
|
||||
import DataFetchAlert from '../shared/DataFetchAlert';
|
||||
|
||||
const DATA_LIMIT = 3;
|
||||
|
||||
const ApiKeysPage: React.FC = () => {
|
||||
|
||||
@@ -13,13 +13,12 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
|
||||
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
|
||||
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
|
||||
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
|
||||
import SkeletonTable from 'ui/shared/SkeletonTable';
|
||||
|
||||
import DataFetchAlert from '../shared/DataFetchAlert';
|
||||
|
||||
const CustomAbiPage: React.FC = () => {
|
||||
const customAbiModalProps = useDisclosure();
|
||||
const deleteModalProps = useDisclosure();
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
|
||||
import type { AppItemOverview } from 'types/client/apps';
|
||||
import { QueryKeys } from 'types/client/queries';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
@@ -27,7 +28,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
|
||||
}, []);
|
||||
|
||||
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
|
||||
[ 'json-rpc-url' ],
|
||||
[ QueryKeys.jsonRpcUrl ],
|
||||
async() => await fetch(`/node-api/config/json-rpc-url`),
|
||||
{ refetchOnMount: false },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import { Box, Text, chakra } from '@chakra-ui/react';
|
||||
import { keyframes } from '@chakra-ui/system';
|
||||
import React from 'react';
|
||||
|
||||
@@ -7,9 +7,13 @@ const runnerAnimation = keyframes`
|
||||
100% { left: '100%'; transform: translateX(-99%); }
|
||||
`;
|
||||
|
||||
const ContentLoader = () => {
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ContentLoader = ({ className }: Props) => {
|
||||
return (
|
||||
<Box display="inline-block">
|
||||
<Box display="inline-block" className={ className }>
|
||||
<Box
|
||||
width="100%"
|
||||
height="6px"
|
||||
@@ -31,4 +35,4 @@ const ContentLoader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentLoader;
|
||||
export default chakra(ContentLoader);
|
||||
|
||||
+16
-13
@@ -15,6 +15,7 @@ type Props = {
|
||||
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
|
||||
const [ filterQuery, setFilterQuery ] = useState('');
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
|
||||
|
||||
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
@@ -37,7 +38,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
|
||||
<InputLeftElement
|
||||
pointerEvents="none"
|
||||
>
|
||||
<Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
|
||||
<Icon as={ searchIcon } color={ iconColor }/>
|
||||
</InputLeftElement>
|
||||
|
||||
<Input
|
||||
@@ -50,18 +51,20 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
|
||||
textOverflow="ellipsis"
|
||||
/>
|
||||
|
||||
<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={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> }
|
||||
size="sm"
|
||||
onClick={ handleFilterQueryClear }
|
||||
/>
|
||||
</InputRightElement>
|
||||
{ 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
@@ -0,0 +1,120 @@
|
||||
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);
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
@@ -0,0 +1,49 @@
|
||||
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;
|
||||
@@ -0,0 +1,38 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
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 } ];
|
||||
@@ -0,0 +1,17 @@
|
||||
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',
|
||||
};
|
||||
Reference in New Issue
Block a user