From 1b3ac93e812833b5d7775c5f9fd46f227eece66e Mon Sep 17 00:00:00 2001 From: isstuev Date: Fri, 6 Sep 2024 16:08:51 +0400 Subject: [PATCH] stats updates --- configs/envs/.env.eth_sepolia | 4 +- lib/metadata/generate.ts | 2 +- lib/metadata/getPageOgType.ts | 1 + lib/metadata/templates/description.ts | 9 +- lib/metadata/templates/title.ts | 2 + lib/metadata/types.ts | 2 + lib/mixpanel/getPageType.ts | 1 + lib/mixpanel/utils.ts | 3 + mocks/stats/line.ts | 37 +++ mocks/stats/lines.ts | 17 + nextjs/nextjs-routes.d.ts | 1 + nextjs/utils/fetchApi.ts | 4 +- package.json | 2 +- pages/stats/[id].tsx | 50 +++ public/static/logo.svg | 3 + stubs/stats.ts | 4 + ui/pages/Chart.tsx | 240 +++++++++++++++ ui/shared/CopyToClipboard.tsx | 3 +- ui/shared/Page/PageTitle.tsx | 4 +- ui/shared/chart/ChartIntervalSelect.tsx | 43 +++ ui/shared/chart/ChartMenu.tsx | 180 +++++++++++ ui/shared/chart/ChartResolutionSelect.tsx | 28 ++ ui/shared/chart/ChartWidget.tsx | 325 ++++++-------------- ui/shared/chart/ChartWidgetContent.tsx | 95 ++++++ ui/shared/chart/FullscreenChartModal.tsx | 12 +- ui/shared/chart/useChartQuery.tsx | 61 ++++ ui/shared/chart/useZoomReset.tsx | 19 ++ ui/shared/chart/utils/formatIntervalDate.ts | 3 + ui/stats/ChartWidgetContainer.tsx | 55 ++-- ui/stats/ChartsWidgetsList.tsx | 1 + ui/stats/StatsDropdownMenu.tsx | 4 +- ui/stats/StatsFilters.tsx | 23 +- ui/stats/constants/index.ts | 27 +- yarn.lock | 8 +- 34 files changed, 972 insertions(+), 301 deletions(-) create mode 100644 pages/stats/[id].tsx create mode 100644 public/static/logo.svg create mode 100644 ui/pages/Chart.tsx create mode 100644 ui/shared/chart/ChartIntervalSelect.tsx create mode 100644 ui/shared/chart/ChartMenu.tsx create mode 100644 ui/shared/chart/ChartResolutionSelect.tsx create mode 100644 ui/shared/chart/ChartWidgetContent.tsx create mode 100644 ui/shared/chart/useChartQuery.tsx create mode 100644 ui/shared/chart/useZoomReset.tsx create mode 100644 ui/shared/chart/utils/formatIntervalDate.ts diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index 1304b37c72..01698d12c4 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -14,7 +14,7 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" } NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_API_BASE_PATH=/ -NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com +NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com @@ -59,7 +59,7 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true -NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com +NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com \ No newline at end of file diff --git a/lib/metadata/generate.ts b/lib/metadata/generate.ts index 9282da7fe7..ee9c3f55cd 100644 --- a/lib/metadata/generate.ts +++ b/lib/metadata/generate.ts @@ -20,7 +20,7 @@ export default function generate(route: Rout }; const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params); - const description = compileValue(templates.description.make(route.pathname), params); + const description = compileValue(templates.description.make(route.pathname, Boolean(apiData)), params); const pageOgType = getPageOgType(route.pathname); diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 1f8498fe69..7babaa72b8 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -23,6 +23,7 @@ const OG_TYPE_DICT: Record = { '/apps': 'Root page', '/apps/[id]': 'Regular page', '/stats': 'Root page', + '/stats/[id]': 'Regular page', '/api-docs': 'Regular page', '/graphiql': 'Regular page', '/search-results': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index e4d0ccd715..49351be252 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record = { '/apps': DEFAULT_TEMPLATE, '/apps/[id]': DEFAULT_TEMPLATE, '/stats': DEFAULT_TEMPLATE, + '/stats/[id]': DEFAULT_TEMPLATE, '/api-docs': DEFAULT_TEMPLATE, '/graphiql': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE, @@ -71,8 +72,10 @@ const TEMPLATE_MAP: Record = { '/auth/unverified-email': DEFAULT_TEMPLATE, }; -export function make(pathname: Route['pathname']) { - const template = TEMPLATE_MAP[pathname]; +const TEMPLATE_MAP_ENHANCED: Partial> = { + '/stats/[id]': '%description%', +}; - return template ?? ''; +export function make(pathname: Route['pathname'], isEnriched = false) { + return (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname] ?? ''; } diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index e0c9d9e44c..c8ef390d71 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -23,6 +23,7 @@ const TEMPLATE_MAP: Record = { '/apps': '%network_name% DApps - Explore top apps', '/apps/[id]': '%network_name% marketplace app', '/stats': '%network_name% stats - %network_name% network insights', + '/stats/[id]': '%network_name% stats - %id% chart', '/api-docs': '%network_name% API docs - %network_name% developer tools', '/graphiql': 'GraphQL for %network_name% - %network_name% data query', '/search-results': '%network_name% search result for %q%', @@ -72,6 +73,7 @@ const TEMPLATE_MAP_ENHANCED: Partial> = { '/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%', '/apps/[id]': '%network_name% - %app_name%', '/address/[hash]': '%network_name% address details for %domain_name%', + '/stats/[id]': '%title% chart on %network_name%', }; export function make(pathname: Route['pathname'], isEnriched = false) { diff --git a/lib/metadata/types.ts b/lib/metadata/types.ts index fda74301ba..ddb29c852a 100644 --- a/lib/metadata/types.ts +++ b/lib/metadata/types.ts @@ -1,3 +1,4 @@ +import type { LineChart } from '@blockscout/stats-types'; import type { TokenInfo } from 'types/api/token'; import type { Route } from 'nextjs-routes'; @@ -9,6 +10,7 @@ export type ApiData = Pathname extends '/token/[hash]' ? TokenInfo : Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } : Pathname extends '/apps/[id]' ? { app_name: string } : + Pathname extends '/stats/[id]' ? LineChart['info'] : never ) | null; diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 3fc7896f81..af56fb6693 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -21,6 +21,7 @@ export const PAGE_TYPE_DICT: Record = { '/apps': 'DApps', '/apps/[id]': 'DApp', '/stats': 'Stats', + '/stats/[id]': 'Stats chart', '/api-docs': 'REST API', '/graphiql': 'GraphQL', '/search-results': 'Search results', diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index 4d59ec007e..f29756f1b3 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -116,6 +116,9 @@ Type extends EventTypes.PAGE_WIDGET ? ( 'Type': 'Address tag'; 'Info': string; 'URL': string; + } | { + 'Type': 'Share chart'; + 'Info': string; } ) : Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? { diff --git a/mocks/stats/line.ts b/mocks/stats/line.ts index 799cff4c45..ce9a4637f7 100644 --- a/mocks/stats/line.ts +++ b/mocks/stats/line.ts @@ -4,158 +4,195 @@ export const averageGasPrice: stats.LineChart = { chart: [ { date: '2023-12-22', + date_to: '2023-12-22', value: '37.7804422597599', is_approximate: false, }, { date: '2023-12-23', + date_to: '2023-12-23', value: '25.84889883009387', is_approximate: false, }, { date: '2023-12-24', + date_to: '2023-12-24', value: '25.818463227198574', is_approximate: false, }, { date: '2023-12-25', + date_to: '2023-12-25', value: '26.045513050051298', is_approximate: false, }, { date: '2023-12-26', + date_to: '2023-12-26', value: '21.42600692652399', is_approximate: false, }, { date: '2023-12-27', + date_to: '2023-12-27', value: '31.066730409846656', is_approximate: false, }, { date: '2023-12-28', + date_to: '2023-12-28', value: '33.63955781902089', is_approximate: false, }, { date: '2023-12-29', + date_to: '2023-12-29', value: '28.064736756058384', is_approximate: false, }, { date: '2023-12-30', + date_to: '2023-12-30', value: '23.074500869678175', is_approximate: false, }, { date: '2023-12-31', + date_to: '2023-12-31', value: '17.651005734615133', is_approximate: false, }, { date: '2024-01-01', + date_to: '2023-01-01', value: '14.906085174476441', is_approximate: false, }, { date: '2024-01-02', + date_to: '2023-01-02', value: '22.28459059038656', is_approximate: false, }, { date: '2024-01-03', + date_to: '2023-01-03', value: '39.8311646806592', is_approximate: false, }, { date: '2024-01-04', + date_to: '2023-01-04', value: '26.09989322256083', is_approximate: false, }, { date: '2024-01-05', + date_to: '2023-01-05', value: '22.821996688111998', is_approximate: false, }, { date: '2024-01-06', + date_to: '2023-01-06', value: '20.32680041262083', is_approximate: false, }, { date: '2024-01-07', + date_to: '2023-01-07', value: '32.535045831809704', is_approximate: false, }, { date: '2024-01-08', + date_to: '2023-01-08', value: '27.443477102139482', is_approximate: false, }, { date: '2024-01-09', + date_to: '2023-01-09', value: '20.7911332558055', is_approximate: false, }, { date: '2024-01-10', + date_to: '2023-01-10', value: '42.10740192523919', is_approximate: false, }, { date: '2024-01-11', + date_to: '2023-01-11', value: '35.75215680343582', is_approximate: false, }, { date: '2024-01-12', + date_to: '2023-01-12', value: '27.430414798093253', is_approximate: false, }, { date: '2024-01-13', + date_to: '2023-01-13', value: '20.170934096589875', is_approximate: false, }, { date: '2024-01-14', + date_to: '2023-01-14', value: '38.79660984371034', is_approximate: false, }, { date: '2024-01-15', + date_to: '2023-01-15', value: '26.140740484554204', is_approximate: false, }, { date: '2024-01-16', + date_to: '2023-01-16', value: '36.708543184194156', is_approximate: false, }, { date: '2024-01-17', + date_to: '2023-01-17', value: '40.325438794298876', is_approximate: false, }, { date: '2024-01-18', + date_to: '2023-01-18', value: '37.55145309930694', is_approximate: false, }, { date: '2024-01-19', + date_to: '2023-01-19', value: '33.271450114434664', is_approximate: false, }, { date: '2024-01-20', + date_to: '2023-01-20', value: '19.303304377685638', is_approximate: false, }, { date: '2024-01-21', + date_to: '2023-01-21', value: '14.375908594704976', is_approximate: false, }, ], + info: { + title: 'Chart title', + description: 'Chert description', + id: 'chart', + resolutions: [ 'DAY', 'MONTH' ], + }, }; diff --git a/mocks/stats/lines.ts b/mocks/stats/lines.ts index 9f8870249f..9dfccb0fdf 100644 --- a/mocks/stats/lines.ts +++ b/mocks/stats/lines.ts @@ -11,18 +11,21 @@ export const base: stats.LineCharts = { title: 'Accounts growth', description: 'Cumulative accounts number per period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'activeAccounts', title: 'Active accounts', description: 'Active accounts number per period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'newAccounts', title: 'New accounts', description: 'New accounts number per day', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, @@ -35,30 +38,35 @@ export const base: stats.LineCharts = { title: 'Average transaction fee', description: 'The average amount in ETH spent per transaction', units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'newTxns', title: 'New transactions', description: 'New transactions number', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'txnsFee', title: 'Transactions fees', description: 'Amount of tokens paid as fees', units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'txnsGrowth', title: 'Transactions growth', description: 'Cumulative transactions number', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'txnsSuccessRate', title: 'Transactions success rate', description: 'Successful transactions rate per day', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, @@ -71,18 +79,21 @@ export const base: stats.LineCharts = { title: 'Average block rewards', description: 'Average amount of distributed reward in tokens per day', units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'averageBlockSize', title: 'Average block size', description: 'Average size of blocks in bytes', units: 'Bytes', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'newBlocks', title: 'New blocks', description: 'New blocks number', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, @@ -95,6 +106,7 @@ export const base: stats.LineCharts = { title: 'New ETH transfers', description: 'New token transfers number for the period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, @@ -107,18 +119,21 @@ export const base: stats.LineCharts = { title: 'Average gas limit', description: 'Average gas limit per block for the period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'averageGasPrice', title: 'Average gas price', description: 'Average gas price for the period (Gwei)', units: 'Gwei', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'gasUsedGrowth', title: 'Gas used growth', description: 'Cumulative gas used for the period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, @@ -131,12 +146,14 @@ export const base: stats.LineCharts = { title: 'New verified contracts', description: 'New verified contracts number for the period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'verifiedContractsGrowth', title: 'Verified contracts growth', description: 'Cumulative number verified contracts for the period', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }, diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index 2d947f73ec..47205b94e2 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -55,6 +55,7 @@ declare module "nextjs-routes" { | StaticRoute<"/public-tags/submit"> | StaticRoute<"/search-results"> | StaticRoute<"/sprite"> + | DynamicRoute<"/stats/[id]", { "id": string }> | StaticRoute<"/stats"> | DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> diff --git a/nextjs/utils/fetchApi.ts b/nextjs/utils/fetchApi.ts index 63eff42384..5597cf8ede 100644 --- a/nextjs/utils/fetchApi.ts +++ b/nextjs/utils/fetchApi.ts @@ -12,6 +12,7 @@ type Params = ( { resource: R; pathParams?: ResourcePathParams; + queryParams?: Record; } | { url: string; route: string; @@ -22,12 +23,11 @@ type Params = ( export default async function fetchApi>(params: Params): Promise { const controller = new AbortController(); - const timeout = setTimeout(() => { controller.abort(); }, params.timeout || SECOND); - const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams); + const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams, params.queryParams); const route = 'route' in params ? params.route : RESOURCES[params.resource]['path']; const end = metrics?.apiRequestDuration.startTimer(); diff --git a/package.json b/package.json index 4a65de7ba0..1f80243919 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@blockscout/bens-types": "1.4.1", - "@blockscout/stats-types": "1.6.0", + "@blockscout/stats-types": "^1.6.2-alpha", "@blockscout/visualizer-types": "0.2.0", "@chakra-ui/react": "2.7.1", "@chakra-ui/theme-tools": "^2.0.18", diff --git a/pages/stats/[id].tsx b/pages/stats/[id].tsx new file mode 100644 index 0000000000..674ea34f31 --- /dev/null +++ b/pages/stats/[id].tsx @@ -0,0 +1,50 @@ +import type { GetServerSideProps, NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import type { Route } from 'nextjs-routes'; +import * as gSSP from 'nextjs/getServerSideProps'; +import type { Props } from 'nextjs/getServerSideProps'; +import PageNextJs from 'nextjs/PageNextJs'; +import detectBotRequest from 'nextjs/utils/detectBotRequest'; +import fetchApi from 'nextjs/utils/fetchApi'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +const Chart = dynamic(() => import('ui/pages/Chart'), { ssr: false }); + +const pathname: Route['pathname'] = '/stats/[id]'; + +const Page: NextPage> = (props: Props) => { + return ( + + + + ); +}; + +export default Page; + +export const getServerSideProps: GetServerSideProps> = async(ctx) => { + const baseResponse = await gSSP.base(ctx); + + if ('props' in baseResponse) { + if ( + config.meta.seo.enhancedDataEnabled || + (config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview') + ) { + const chartData = await fetchApi({ + resource: 'stats_line', + pathParams: { id: getQueryParamString(ctx.query.id) }, + queryParams: { from: dayjs().format('YYYY-MM-DD'), to: dayjs().format('YYYY-MM-DD') }, + timeout: 1000, + }); + + (await baseResponse.props).apiData = chartData?.info ?? null; + } + } + + return baseResponse; +}; diff --git a/public/static/logo.svg b/public/static/logo.svg new file mode 100644 index 0000000000..4545aeb9c1 --- /dev/null +++ b/public/static/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/stubs/stats.ts b/stubs/stats.ts index e1e70724fb..3e7be4b565 100644 --- a/stubs/stats.ts +++ b/stubs/stats.ts @@ -51,24 +51,28 @@ export const STATS_CHARTS_SECTION: stats.LineChartSection = { title: 'Average transaction fee', description: 'The average amount in ETH spent per transaction', units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'chart_1', title: 'Transactions fees', description: 'Amount of tokens paid as fees', units: 'ETH', + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'chart_2', title: 'New transactions', description: 'New transactions number', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, { id: 'chart_3', title: 'Transactions growth', description: 'Cumulative transactions number', units: undefined, + resolutions: [ 'DAY', 'MONTH' ], }, ], }; diff --git a/ui/pages/Chart.tsx b/ui/pages/Chart.tsx new file mode 100644 index 0000000000..6bce9667c2 --- /dev/null +++ b/ui/pages/Chart.tsx @@ -0,0 +1,240 @@ +import { Button, Flex, IconButton, Text } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import { Resolution } from '@blockscout/stats-types'; +import type { StatsIntervalIds } from 'types/client/stats'; + +import config from 'configs/app'; +import { useAppContext } from 'lib/contexts/app'; +import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import isBrowser from 'lib/isBrowser'; +import * as metadata from 'lib/metadata'; +import * as mixpanel from 'lib/mixpanel/index'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import isCustomAppError from 'ui/shared/AppError/isCustomAppError'; +import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect'; +import ChartMenu from 'ui/shared/chart/ChartMenu'; +import ChartResolutionSelect from 'ui/shared/chart/ChartResolutionSelect'; +import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent'; +import useChartQuery from 'ui/shared/chart/useChartQuery'; +import useZoomReset from 'ui/shared/chart/useZoomReset'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; +import IconSvg from 'ui/shared/IconSvg'; +import PageTitle from 'ui/shared/Page/PageTitle'; + +const DEFAULT_RESOLUTION = Resolution.DAY; + +const getIntervalByResolution = (resolution: Resolution): StatsIntervalIds => { + switch (resolution) { + case 'DAY': + return 'oneMonth'; + case 'WEEK': + return 'oneMonth'; + case 'MONTH': + return 'oneYear'; + case 'YEAR': + return 'all'; + default: + return 'oneMonth'; + } +}; + +const Chart = () => { + const router = useRouter(); + const id = getQueryParamString(router.query.id); + const [ intervalState, setIntervalState ] = React.useState(); + const [ resolution, setResolution ] = React.useState(DEFAULT_RESOLUTION); + const { isZoomResetInitial, handleZoom, handleZoomReset } = useZoomReset(); + + const interval = intervalState || getIntervalByResolution(resolution); + + const ref = React.useRef(null); + + const isMobile = useIsMobile(); + const isInBrowser = isBrowser(); + + const appProps = useAppContext(); + const backLink = React.useMemo(() => { + const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/stats'); + + if (!hasGoBackLink) { + return; + } + + return { + label: 'Back to charts list', + url: appProps.referrer, + }; + }, [ appProps.referrer ]); + + const handleReset = React.useCallback(() => { + handleZoomReset(); + setResolution(DEFAULT_RESOLUTION); + }, [ handleZoomReset ]); + + const { items, info, lineQuery } = useChartQuery(id, resolution, interval); + + React.useEffect(() => { + if (info && !config.meta.seo.enhancedDataEnabled) { + metadata.update({ pathname: '/stats/[id]', query: { id } }, info); + } + }, [ info, id ]); + + const onShare = React.useCallback(async() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Share chart', Info: id }); + try { + await window.navigator.share({ + title: info?.title, + text: info?.description, + url: window.location.href, + }); + } catch (error) {} + }, [ info, id ]); + + if (lineQuery.isError) { + if (isCustomAppError(lineQuery.error)) { + throwOnResourceLoadError({ resource: 'stats_line', error: lineQuery.error, isError: true }); + } + } + + const hasItems = (items && items.length > 2) || lineQuery.isPending; + + const isInfoLoading = !info && lineQuery.isPlaceholderData; + + const shareButton = isMobile ? ( + } + onClick={ onShare } + /> + ) : ( + + ); + + const shareAndMenu = ( + + { /* TS thinks window.navigator.share can't be undefined, but it can */ } + { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } + { (isInBrowser && ((window.navigator.share as any) ? + shareButton : + ( + + ) + )) } + { (hasItems || lineQuery.isPlaceholderData) && ( + + ) } + + ); + + return ( + <> + + + + Period + + { lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 2 && ( + <> + { isMobile ? 'Res.' : 'Resolution' } + + + ) } + { (!isZoomResetInitial || resolution !== 'DAY') && ( + isMobile ? ( + } + onClick={ handleReset } + /> + ) : ( + + ) + ) } + + { !isMobile && shareAndMenu } + + + + + + ); +}; + +export default Chart; diff --git a/ui/shared/CopyToClipboard.tsx b/ui/shared/CopyToClipboard.tsx index c17a16092e..1a0daa9d7c 100644 --- a/ui/shared/CopyToClipboard.tsx +++ b/ui/shared/CopyToClipboard.tsx @@ -22,6 +22,7 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type, // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 const { isOpen, onOpen, onClose } = useDisclosure(); const iconColor = useColorModeValue('gray.400', 'gray.500'); + const colorProps = colorScheme ? {} : { color: iconColor }; const iconName = icon || (type === 'link' ? 'link' : 'copy'); useEffect(() => { @@ -44,10 +45,10 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type, return ( } boxSize={ size } - color={ iconColor } variant={ variant } colorScheme={ colorScheme } display="inline-block" diff --git a/ui/shared/Page/PageTitle.tsx b/ui/shared/Page/PageTitle.tsx index e406b098eb..844f301498 100644 --- a/ui/shared/Page/PageTitle.tsx +++ b/ui/shared/Page/PageTitle.tsx @@ -154,9 +154,9 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa { withTextAd && } { secondRow && ( - + { secondRow } - + ) } ); diff --git a/ui/shared/chart/ChartIntervalSelect.tsx b/ui/shared/chart/ChartIntervalSelect.tsx new file mode 100644 index 0000000000..041cc06fc6 --- /dev/null +++ b/ui/shared/chart/ChartIntervalSelect.tsx @@ -0,0 +1,43 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { StatsInterval, StatsIntervalIds } from 'types/client/stats'; + +import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect'; +import { STATS_INTERVALS } from 'ui/stats/constants'; +import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu'; + +const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({ + id: id, + title: STATS_INTERVALS[id as StatsIntervalIds].title, +})) as Array; + +const intervalListShort = Object.keys(STATS_INTERVALS).map((id: string) => ({ + id: id, + title: STATS_INTERVALS[id as StatsIntervalIds].shortTitle, +})) as Array; + +type Props = { + interval: StatsIntervalIds; + onIntervalChange: (newInterval: StatsIntervalIds) => void; + isLoading?: boolean; +} + +const ChartIntervalSelect = ({ interval, onIntervalChange, isLoading }: Props) => { + return ( + <> + + items={ intervalListShort } onChange={ onIntervalChange } value={ interval }/> + + + + + + ); +}; + +export default React.memo(ChartIntervalSelect); diff --git a/ui/shared/chart/ChartMenu.tsx b/ui/shared/chart/ChartMenu.tsx new file mode 100644 index 0000000000..cfd7fd7852 --- /dev/null +++ b/ui/shared/chart/ChartMenu.tsx @@ -0,0 +1,180 @@ +import { + IconButton, + MenuButton, + MenuItem, + MenuList, + Skeleton, + useClipboard, + useColorModeValue, + VisuallyHidden, +} from '@chakra-ui/react'; +import domToImage from 'dom-to-image'; +import React from 'react'; + +import type { TimeChartItem } from './types'; + +import type { Route } from 'nextjs-routes'; +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import dayjs from 'lib/date/dayjs'; +import isBrowser from 'lib/isBrowser'; +import saveAsCSV from 'lib/saveAsCSV'; +import Menu from 'ui/shared/chakra/Menu'; +import IconSvg from 'ui/shared/IconSvg'; + +import FullscreenChartModal from './FullscreenChartModal'; + +export type Props = { + items?: Array; + title: string; + description?: string; + units?: string; + isLoading: boolean; + chartRef: React.RefObject; + href?: Route; +} + +const DOWNLOAD_IMAGE_SCALE = 5; + +const ChartMenu = ({ items, title, description, units, isLoading, chartRef, href }: Props) => { + const pngBackgroundColor = useColorModeValue('white', 'black'); + const [ isFullscreen, setIsFullscreen ] = React.useState(false); + + const chartUrl = href ? config.app.baseUrl + route(href) : ''; + const { onCopy } = useClipboard(chartUrl); + + const isInBrowser = isBrowser(); + + const showChartFullscreen = React.useCallback(() => { + setIsFullscreen(true); + }, []); + + const clearFullscreenChart = React.useCallback(() => { + setIsFullscreen(false); + }, []); + + const handleFileSaveClick = React.useCallback(() => { + // wait for context menu to close + setTimeout(() => { + if (chartRef.current) { + domToImage.toPng(chartRef.current, + { + quality: 100, + bgcolor: pngBackgroundColor, + width: chartRef.current.offsetWidth * DOWNLOAD_IMAGE_SCALE, + height: chartRef.current.offsetHeight * DOWNLOAD_IMAGE_SCALE, + filter: (node) => node.nodeName !== 'BUTTON', + style: { + borderColor: 'transparent', + transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`, + 'transform-origin': 'top left', + }, + }) + .then((dataUrl) => { + const link = document.createElement('a'); + link.download = `${ title } (Blockscout chart).png`; + link.href = dataUrl; + link.click(); + link.remove(); + }); + } + }, 100); + }, [ pngBackgroundColor, title, chartRef ]); + + const handleSVGSavingClick = React.useCallback(() => { + if (items) { + const headerRows = [ + 'Date', 'Value', + ]; + const dataRows = items.map((item) => [ + dayjs(item.date).format('YYYY-MM-DD'), String(item.value), + ]); + + saveAsCSV(headerRows, dataRows, `${ title } (Blockscout stats)`); + } + }, [ items, title ]); + + // TS thinks window.navigator.share can't be undefined, but it can + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hasShare = isInBrowser && (window.navigator.share as any); + + const handleShare = React.useCallback(async() => { + try { + await window.navigator.share({ + title: title, + text: description, + url: chartUrl, + }); + } catch (error) {} + }, [ title, description, chartUrl ]); + + return ( + <> + + + } + colorScheme="gray" + variant="ghost" + as={ IconButton } + > + + Open chart options menu + + + + + { href && ( + + + { hasShare ? 'Share' : 'Copy link' } + + ) } + + + View fullscreen + + + + Save as PNG + + + + Save as CSV + + + + { items && ( + + ) } + + ); +}; + +export default ChartMenu; diff --git a/ui/shared/chart/ChartResolutionSelect.tsx b/ui/shared/chart/ChartResolutionSelect.tsx new file mode 100644 index 0000000000..999ecf0136 --- /dev/null +++ b/ui/shared/chart/ChartResolutionSelect.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { Resolution } from '@blockscout/stats-types'; + +import { STATS_RESOLUTIONS } from 'ui/stats/constants'; +import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu'; + +type Props = { + resolution: Resolution; + resolutions: Array; + onResolutionChange: (resolution: Resolution) => void; + isLoading?: boolean; +} + +const ChartResolutionSelect = ({ resolution, resolutions, onResolutionChange, isLoading }: Props) => { + return ( + + resolutions.includes(r.id)) } + selectedId={ resolution } + onSelect={ onResolutionChange } + /> + + ); +}; + +export default React.memo(ChartResolutionSelect); diff --git a/ui/shared/chart/ChartWidget.tsx b/ui/shared/chart/ChartWidget.tsx index e32a1e29ae..28f1829812 100644 --- a/ui/shared/chart/ChartWidget.tsx +++ b/ui/shared/chart/ChartWidget.tsx @@ -1,31 +1,23 @@ import { - Box, - Center, chakra, Flex, - IconButton, Link, - MenuButton, - MenuItem, - MenuList, + IconButton, Skeleton, - Text, Tooltip, useColorModeValue, - VisuallyHidden, } from '@chakra-ui/react'; -import domToImage from 'dom-to-image'; -import React, { useRef, useCallback, useState } from 'react'; +import NextLink from 'next/link'; +import React, { useRef } from 'react'; import type { TimeChartItem } from './types'; -import dayjs from 'lib/date/dayjs'; -import { apos } from 'lib/html-entities'; -import saveAsCSV from 'lib/saveAsCSV'; -import Menu from 'ui/shared/chakra/Menu'; +import type { Route } from 'nextjs-routes'; + import IconSvg from 'ui/shared/IconSvg'; -import ChartWidgetGraph from './ChartWidgetGraph'; -import FullscreenChartModal from './FullscreenChartModal'; +import ChartMenu from './ChartMenu'; +import ChartWidgetContent from './ChartWidgetContent'; +import useZoomReset from './useZoomReset'; export type Props = { items?: Array; @@ -37,236 +29,113 @@ export type Props = { isError: boolean; emptyText?: string; noAnimation?: boolean; + href?: Route; } -const DOWNLOAD_IMAGE_SCALE = 5; - -const ChartWidget = ({ items, title, description, isLoading, className, isError, units, emptyText, noAnimation }: Props) => { +const ChartWidget = ({ + items, + title, + description, + isLoading, + className, + isError, + units, + emptyText, + noAnimation, + href, +}: Props) => { const ref = useRef(null); - const [ isFullscreen, setIsFullscreen ] = useState(false); - const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); + const { isZoomResetInitial, handleZoom, handleZoomReset } = useZoomReset(); - const pngBackgroundColor = useColorModeValue('white', 'black'); const borderColor = useColorModeValue('gray.200', 'gray.600'); - const handleZoom = useCallback(() => { - setIsZoomResetInitial(false); - }, []); - - const handleZoomResetClick = useCallback(() => { - setIsZoomResetInitial(true); - }, []); - - const showChartFullscreen = useCallback(() => { - setIsFullscreen(true); - }, []); - - const clearFullscreenChart = useCallback(() => { - setIsFullscreen(false); - }, []); - - const handleFileSaveClick = useCallback(() => { - // wait for context menu to close - setTimeout(() => { - if (ref.current) { - domToImage.toPng(ref.current, - { - quality: 100, - bgcolor: pngBackgroundColor, - width: ref.current.offsetWidth * DOWNLOAD_IMAGE_SCALE, - height: ref.current.offsetHeight * DOWNLOAD_IMAGE_SCALE, - filter: (node) => node.nodeName !== 'BUTTON', - style: { - borderColor: 'transparent', - transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`, - 'transform-origin': 'top left', - }, - }) - .then((dataUrl) => { - const link = document.createElement('a'); - link.download = `${ title } (Blockscout chart).png`; - link.href = dataUrl; - link.click(); - link.remove(); - }); - } - }, 100); - }, [ pngBackgroundColor, title ]); - - const handleSVGSavingClick = useCallback(() => { - if (items) { - const headerRows = [ - 'Date', 'Value', - ]; - const dataRows = items.map((item) => [ - dayjs(item.date).format('YYYY-MM-DD'), String(item.value), - ]); - - saveAsCSV(headerRows, dataRows, `${ title } (Blockscout stats)`); - } - }, [ items, title ]); - const hasItems = items && items.length > 2; - const content = (() => { - if (isError) { - return ( - - - { `The data didn${ apos }t load. Please, ` } - try to reload the page. - - - ); - } - - if (isLoading) { - return ; - } - - if (!hasItems) { - return ( -
- { emptyText || 'No data' } -
- ); - } - - return ( - - - - ); - })(); + const content = ( + + ); return ( - <> - - - + + + + + { href ? ( + + { title } + + ) : title + } + + + { description && ( - { title } + { description } - - { description && ( - - { description } - - ) } - - - - - - - { hasItems && ( - - - } - colorScheme="gray" - variant="ghost" - as={ IconButton } - > - - Open chart options menu - - - - - - - View fullscreen - - - - - Save as PNG - - - - - Save as CSV - - - - ) } - + ) } - { content } + + + + + { hasItems && ( + + ) } + - { hasItems && ( - - ) } - + { content } + ); }; diff --git a/ui/shared/chart/ChartWidgetContent.tsx b/ui/shared/chart/ChartWidgetContent.tsx new file mode 100644 index 0000000000..ee372f2102 --- /dev/null +++ b/ui/shared/chart/ChartWidgetContent.tsx @@ -0,0 +1,95 @@ +import { Box, Center, Flex, Link, Skeleton, Text, Image } from '@chakra-ui/react'; +import React from 'react'; + +import type { TimeChartItem } from './types'; + +import { apos } from 'lib/html-entities'; + +import ChartWidgetGraph from './ChartWidgetGraph'; + +export type Props = { + items?: Array; + title: string; + units?: string; + isLoading?: boolean; + isError?: boolean; + emptyText?: string; + handleZoom: () => void; + isZoomResetInitial: boolean; + isEnlarged?: boolean; + noAnimation?: boolean; +} + +const ChartWidgetContent = ({ + items, + title, + isLoading, + isError, + units, + emptyText, + handleZoom, + isZoomResetInitial, + isEnlarged, + noAnimation, +}: Props) => { + const hasItems = items && items.length > 2; + + if (isError) { + return ( + + + { `The data didn${ apos }t load. Please, ` } + try to reload the page. + + + ); + } + + if (isLoading) { + return ; + } + + if (!hasItems) { + return ( +
+ { emptyText || 'No data' } +
+ ); + } + + return ( + + + blockscout logo + + ); +}; + +export default React.memo(ChartWidgetContent); diff --git a/ui/shared/chart/FullscreenChartModal.tsx b/ui/shared/chart/FullscreenChartModal.tsx index 6835474851..da9d7d01e0 100644 --- a/ui/shared/chart/FullscreenChartModal.tsx +++ b/ui/shared/chart/FullscreenChartModal.tsx @@ -5,7 +5,7 @@ import type { TimeChartItem } from './types'; import IconSvg from 'ui/shared/IconSvg'; -import ChartWidgetGraph from './ChartWidgetGraph'; +import ChartWidgetContent from './ChartWidgetContent'; type Props = { isOpen: boolean; @@ -30,7 +30,7 @@ const FullscreenChartModal = ({ setIsZoomResetInitial(false); }, []); - const handleZoomResetClick = useCallback(() => { + const handleZoomReset = useCallback(() => { setIsZoomResetInitial(true); }, []); @@ -79,7 +79,7 @@ const FullscreenChartModal = ({ gridRow="1/3" size="sm" variant="outline" - onClick={ handleZoomResetClick } + onClick={ handleZoomReset } > Reset zoom @@ -91,13 +91,13 @@ const FullscreenChartModal = ({ - diff --git a/ui/shared/chart/useChartQuery.tsx b/ui/shared/chart/useChartQuery.tsx new file mode 100644 index 0000000000..6f9819de30 --- /dev/null +++ b/ui/shared/chart/useChartQuery.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import type { LineChart, Resolution } from '@blockscout/stats-types'; +import type { StatsIntervalIds } from 'types/client/stats'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { useAppContext } from 'lib/contexts/app'; +import { STATS_INTERVALS } from 'ui/stats/constants'; + +import formatDate from './utils/formatIntervalDate'; + +export default function useChartQuery(id: string, resolution: Resolution, interval: StatsIntervalIds, enabled = true) { + const { apiData } = useAppContext<'/stats/[id]'>(); + + const selectedInterval = STATS_INTERVALS[interval]; + + const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; + const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined; + + const [ info, setInfo ] = React.useState(apiData || undefined); + + const lineQuery = useApiQuery('stats_line', { + pathParams: { id }, + queryParams: { + from: startDate, + to: endDate, + resolution, + }, + queryOptions: { + enabled: enabled, + refetchOnMount: false, + placeholderData: { + info: { + title: 'Chart title placeholder', + description: 'Chart placeholder description chart placeholder description', + resolutions: [ 'DAY', 'WEEK', 'MONTH', 'YEAR' ], + id: 'placeholder', + units: undefined, + }, + chart: [], + }, + }, + }); + + React.useEffect(() => { + if (!info && lineQuery.data?.info && !lineQuery.isPlaceholderData) { + // save info to keep title and description when change query params + setInfo(lineQuery.data?.info); + } + }, [ info, lineQuery.data?.info, lineQuery.isPlaceholderData ]); + + const items = React.useMemo(() => lineQuery.data?.chart?.map((item) => { + return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate }; + }), [ lineQuery ]); + + return { + items, + info, + lineQuery, + }; +} diff --git a/ui/shared/chart/useZoomReset.tsx b/ui/shared/chart/useZoomReset.tsx new file mode 100644 index 0000000000..02db2f7a07 --- /dev/null +++ b/ui/shared/chart/useZoomReset.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export default function useZoomReset() { + const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); + + const handleZoom = React.useCallback(() => { + setIsZoomResetInitial(false); + }, []); + + const handleZoomReset = React.useCallback(() => { + setIsZoomResetInitial(true); + }, []); + + return { + isZoomResetInitial, + handleZoom, + handleZoomReset, + }; +} diff --git a/ui/shared/chart/utils/formatIntervalDate.ts b/ui/shared/chart/utils/formatIntervalDate.ts new file mode 100644 index 0000000000..701fa9a829 --- /dev/null +++ b/ui/shared/chart/utils/formatIntervalDate.ts @@ -0,0 +1,3 @@ +export default function formatDate(date: Date) { + return date.toISOString().substring(0, 10); +} diff --git a/ui/stats/ChartWidgetContainer.tsx b/ui/stats/ChartWidgetContainer.tsx index 5f41a5c2af..26b4fc8154 100644 --- a/ui/stats/ChartWidgetContainer.tsx +++ b/ui/stats/ChartWidgetContainer.tsx @@ -1,12 +1,14 @@ import { chakra } from '@chakra-ui/react'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; +import { Resolution } from '@blockscout/stats-types'; import type { StatsIntervalIds } from 'types/client/stats'; -import useApiQuery from 'lib/api/useApiQuery'; +import type { Route } from 'nextjs-routes'; + +import useChartQuery from 'ui/shared/chart/useChartQuery'; import ChartWidget from '../shared/chart/ChartWidget'; -import { STATS_INTERVALS } from './constants'; type Props = { id: string; @@ -17,50 +19,39 @@ type Props = { onLoadingError: () => void; isPlaceholderData: boolean; className?: string; + href?: Route; } -function formatDate(date: Date) { - return date.toISOString().substring(0, 10); -} - -const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData, className }: Props) => { - const selectedInterval = STATS_INTERVALS[interval]; - - const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; - const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined; - - const { data, isPending, isError } = useApiQuery('stats_line', { - pathParams: { id }, - queryParams: { - from: startDate, - to: endDate, - }, - queryOptions: { - enabled: !isPlaceholderData, - refetchOnMount: false, - }, - }); - - const items = useMemo(() => data?.chart?.map((item) => { - return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate }; - }), [ data ]); +const ChartWidgetContainer = ({ + id, + title, + description, + interval, + onLoadingError, + units, + isPlaceholderData, + className, + href, +}: Props) => { + const { items, lineQuery } = useChartQuery(id, Resolution.DAY, interval, !isPlaceholderData); useEffect(() => { - if (isError) { + if (lineQuery.isError) { onLoadingError(); } - }, [ isError, onLoadingError ]); + }, [ lineQuery.isError, onLoadingError ]); return ( ); }; diff --git a/ui/stats/ChartsWidgetsList.tsx b/ui/stats/ChartsWidgetsList.tsx index 3d40f550ba..f1001774d1 100644 --- a/ui/stats/ChartsWidgetsList.tsx +++ b/ui/stats/ChartsWidgetsList.tsx @@ -95,6 +95,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in units={ chart.units || undefined } isPlaceholderData={ isPlaceholderData } onLoadingError={ handleChartLoadingError } + href={{ pathname: '/stats/[id]', query: { id: chart.id } }} /> )) } diff --git a/ui/stats/StatsDropdownMenu.tsx b/ui/stats/StatsDropdownMenu.tsx index 203e3c79f0..89c93c2ef2 100644 --- a/ui/stats/StatsDropdownMenu.tsx +++ b/ui/stats/StatsDropdownMenu.tsx @@ -5,7 +5,7 @@ import Menu from 'ui/shared/chakra/Menu'; import IconSvg from 'ui/shared/IconSvg'; type Props = { - items: Array<{id: T; title: string}>; + items: ReadonlyArray<{id: T; title: string}>; selectedId: T; onSelect: (id: T) => void; } @@ -23,7 +23,7 @@ export function StatsDropdownMenu({ items, selectedId, onSelec > ({ - id: id, - title: STATS_INTERVALS[id as StatsIntervalIds].title, -})) as Array; - type Props = { sections?: Array; currentSection: string; @@ -37,7 +32,7 @@ const StatsFilters = ({ }: Props) => { const sectionsList = [ { id: 'all', - title: 'All', + title: 'All stats', }, ... (sections || []) ]; return ( @@ -49,12 +44,13 @@ const StatsFilters = ({ lg: `"section interval input"`, }} gridTemplateColumns={{ base: 'repeat(2, minmax(0, 1fr))', lg: 'auto auto 1fr' }} + alignItems="center" > - { isLoading ? : ( + { isLoading ? : ( - { isLoading ? : ( - - ) } + diff --git a/ui/stats/constants/index.ts b/ui/stats/constants/index.ts index bf77117c4c..23d52006c6 100644 --- a/ui/stats/constants/index.ts +++ b/ui/stats/constants/index.ts @@ -1,23 +1,48 @@ +import { Resolution } from '@blockscout/stats-types'; import type { StatsIntervalIds } from 'types/client/stats'; -export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; start?: Date } } = { +export const STATS_RESOLUTIONS: Array<{id: Resolution; title: string }> = [ + { + id: Resolution.DAY, + title: 'Day', + }, + { + id: Resolution.WEEK, + title: 'Week', + }, + { + id: Resolution.MONTH, + title: 'Month', + }, + { + id: Resolution.YEAR, + title: 'Year', + }, +]; + +export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; shortTitle: string; start?: Date } } = { all: { title: 'All time', + shortTitle: 'All time', }, oneMonth: { title: '1 month', + shortTitle: '1M', start: getStartDateInPast(1), }, threeMonths: { title: '3 months', + shortTitle: '3M', start: getStartDateInPast(3), }, sixMonths: { title: '6 months', + shortTitle: '6M', start: getStartDateInPast(6), }, oneYear: { title: '1 year', + shortTitle: '1Y', start: getStartDateInPast(12), }, }; diff --git a/yarn.lock b/yarn.lock index 50748757ea..019fa91942 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1327,10 +1327,10 @@ resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66" integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ== -"@blockscout/stats-types@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-1.6.0.tgz#cdb27ab3d3cb1eef7b8b069c39d4e09afda1aec9" - integrity sha512-MzItYOsLa3zgoFzRgFAgg7gynSXG0w/GqHzg5BGHcBPbPSp/g7A6mMtyIchI6TnZxxnCwziHHvzmJFXz11emUg== +"@blockscout/stats-types@^1.6.2-alpha": + version "1.6.2-alpha" + resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-1.6.2-alpha.tgz#e0ec8d12921255943a3b7fc860e1b97e73171a69" + integrity sha512-3rFDgCt0sP2pbPcZ6s3m/zdZxH6hs8PlEchDyqYvKIqVBiBmRwFnXWY22W/Y71r5DJkCjWYbLzxij0WXQxwlnA== "@blockscout/visualizer-types@0.2.0": version "0.2.0"