Files
explore/ui/shared/chart/ChartMenu.tsx
T
2025-06-11 20:03:31 +02:00

179 lines
5.2 KiB
TypeScript

import { useCopyToClipboard } from '@uidotdev/usehooks';
import domToImage from 'dom-to-image';
import React from 'react';
import type { TimeChartItem } from './types';
import type { Resolution } from '@blockscout/stats-types';
import dayjs from 'lib/date/dayjs';
import saveAsCSV from 'lib/saveAsCSV';
import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { IconButton } from 'toolkit/chakra/icon-button';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from 'toolkit/chakra/menu';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import { isBrowser } from 'toolkit/utils/isBrowser';
import IconSvg from 'ui/shared/IconSvg';
import FullscreenChartModal from './FullscreenChartModal';
export type Props = {
items?: Array<TimeChartItem>;
title: string;
description?: string;
units?: string;
isLoading: boolean;
chartRef: React.RefObject<HTMLDivElement | null>;
chartUrl?: string;
resolution?: Resolution;
zoomRange?: [ Date, Date ];
handleZoom: (range: [ Date, Date ]) => void;
handleZoomReset: () => void;
};
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartMenu = ({
items,
title,
description,
units,
isLoading,
chartRef,
chartUrl,
resolution,
zoomRange,
handleZoom,
handleZoomReset,
}: Props) => {
const pngBackgroundColor = useColorModeValue('white', 'black');
const fullscreenDialog = useDisclosure();
const [ , copyToClipboard ] = useCopyToClipboard();
const isInBrowser = isBrowser();
const showChartFullscreen = React.useCallback(() => {
fullscreenDialog.onOpenChange({ open: true });
}, [ fullscreenDialog ]);
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 handleCopy = React.useCallback(() => {
copyToClipboard(chartUrl ?? '');
}, [ chartUrl, copyToClipboard ]);
const handleShare = React.useCallback(async() => {
try {
await window.navigator.share({
title: title,
text: description,
url: chartUrl,
});
} catch (error) {}
}, [ title, description, chartUrl ]);
return (
<>
<MenuRoot>
<MenuTrigger asChild>
<IconButton variant="icon_secondary" size="md" aria-label="Open chart options menu" loadingSkeleton={ isLoading }>
<IconSvg name="dots"/>
</IconButton>
</MenuTrigger>
<MenuContent>
{ chartUrl && (
<MenuItem
value={ hasShare ? 'share' : 'copy' }
onClick={ hasShare ? handleShare : handleCopy }
closeOnSelect={ hasShare ? false : true }
>
<IconSvg name={ hasShare ? 'share' : 'copy' } boxSize={ 5 }/>
{ hasShare ? 'Share' : 'Copy link' }
</MenuItem>
) }
<MenuItem
value="fullscreen"
onClick={ showChartFullscreen }
>
<IconSvg name="scope" boxSize={ 5 }/>
View fullscreen
</MenuItem>
<MenuItem
value="save-png"
onClick={ handleFileSaveClick }
>
<IconSvg name="files/image" boxSize={ 5 }/>
Save as PNG
</MenuItem>
<MenuItem
value="save-csv"
onClick={ handleSVGSavingClick }
>
<IconSvg name="files/csv" boxSize={ 5 }/>
Save as CSV
</MenuItem>
</MenuContent>
</MenuRoot>
{ items && (
<FullscreenChartModal
open={ fullscreenDialog.open }
onOpenChange={ fullscreenDialog.onOpenChange }
items={ items }
title={ title }
description={ description }
units={ units }
resolution={ resolution }
zoomRange={ zoomRange }
handleZoom={ handleZoom }
handleZoomReset={ handleZoomReset }
/>
) }
</>
);
};
export default ChartMenu;