diff --git a/src/components/ECharts/AreaChart/index.tsx b/src/components/ECharts/AreaChart/index.tsx index b0c713df7f..bfc97c92a1 100644 --- a/src/components/ECharts/AreaChart/index.tsx +++ b/src/components/ECharts/AreaChart/index.tsx @@ -3,7 +3,8 @@ import * as echarts from 'echarts/core' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' import { SelectWithCombobox } from '~/components/SelectWithCombobox' import { useDarkModeManager } from '~/contexts/LocalStorage' -import { download, slug, toNiceCsvDate } from '~/utils' +import { slug, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import type { IChartProps } from '../types' import { useDefaults } from '../useDefaults' import { stringToColour } from '../utils' @@ -34,6 +35,7 @@ export default function AreaChart({ ...props }: IChartProps) { const id = useId() + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const [legendOptions, setLegendOptions] = useState(customLegendOptions) @@ -337,26 +339,32 @@ export default function AreaChart({ { try { - let rows = [] + let headers: string[] + let rows: any[][] + if (!chartsStack || chartsStack.length === 0) { - rows = [['Timestamp', 'Date', 'Value']] + headers = ['Timestamp', 'Date', 'Value'] + rows = [] for (const [date, value] of chartData ?? []) { rows.push([date, toNiceCsvDate(date), value]) } } else { - rows = [['Timestamp', 'Date', ...chartsStack]] + headers = ['Timestamp', 'Date', ...chartsStack] + rows = [] for (const item of chartData ?? []) { const { date, ...rest } = item rows.push([date, toNiceCsvDate(date), ...chartsStack.map((stack) => rest[stack] ?? '')]) } } + const Mytitle = title ? slug(title) : 'data' const filename = `area-chart-${Mytitle}-${new Date().toISOString().split('T')[0]}.csv` - download(filename, rows.map((r) => r.join(',')).join('\n')) + downloadCSVFromHook(filename, [headers, ...rows]) } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isCSVLoading} smol /> )} diff --git a/src/components/ECharts/BarChart/index.tsx b/src/components/ECharts/BarChart/index.tsx index 3d5ae1abb0..8b9cdc4ece 100644 --- a/src/components/ECharts/BarChart/index.tsx +++ b/src/components/ECharts/BarChart/index.tsx @@ -3,7 +3,8 @@ import * as echarts from 'echarts/core' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' import { SelectWithCombobox } from '~/components/SelectWithCombobox' import { useDarkModeManager } from '~/contexts/LocalStorage' -import { download, slug, toNiceCsvDate } from '~/utils' +import { slug, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import type { IBarChartProps } from '../types' import { useDefaults } from '../useDefaults' import { stringToColour } from '../utils' @@ -28,6 +29,7 @@ export default function BarChart({ customComponents }: IBarChartProps) { const id = useId() + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const [legendOptions, setLegendOptions] = useState(customLegendOptions ? [...customLegendOptions] : []) @@ -236,26 +238,32 @@ export default function BarChart({ { try { - let rows = [] + let headers: string[] + let rows: any[][] + if (!stackKeys || stackKeys.length === 0) { - rows = [['Timestamp', 'Date', 'Value']] + headers = ['Timestamp', 'Date', 'Value'] + rows = [] for (const [date, value] of chartData ?? []) { rows.push([date, toNiceCsvDate(date), value]) } } else { - rows = [['Timestamp', 'Date', ...selectedStacks]] + headers = ['Timestamp', 'Date', ...selectedStacks] + rows = [] for (const item of chartData ?? []) { const { date, ...rest } = item rows.push([date, toNiceCsvDate(date), ...selectedStacks.map((stack) => rest[stack] ?? '')]) } } + const Mytitle = title ? slug(title) : 'data' const filename = `bar-chart-${Mytitle}-${new Date().toISOString().split('T')[0]}.csv` - download(filename, rows.map((r) => r.join(',')).join('\n')) + downloadCSVFromHook(filename, [headers, ...rows]) } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isCSVLoading} smol /> )} diff --git a/src/components/ECharts/utils.ts b/src/components/ECharts/utils.ts index 39969e8d6e..08d2b4c59e 100644 --- a/src/components/ECharts/utils.ts +++ b/src/components/ECharts/utils.ts @@ -106,32 +106,55 @@ export const formatLineChart = ({ } } -export function downloadChart(data: Record>, filename: string) { - let rows = [] - const charts = [] - const dateStore = {} - for (const chartName in data) { - charts.push(chartName) - for (const [date, value] of data[chartName]) { - if (!dateStore[date]) { - dateStore[date] = {} +export function downloadChart( + data: Record>, + filename: string, + options: { + onLoadingStart?: () => void; + onLoadingEnd?: () => void; + onError?: (error: Error) => void; + } = {} +) { + const { onLoadingStart, onLoadingEnd, onError } = options + + if (onLoadingStart) onLoadingStart() + + try { + const rows = [] + const charts = [] + const dateStore = {} + + for (const chartName in data) { + charts.push(chartName) + for (const [date, value] of data[chartName]) { + if (!dateStore[date]) { + dateStore[date] = {} + } + dateStore[date][chartName] = value } - dateStore[date][chartName] = value } - } - rows.push(['Timestamp', 'Date', ...charts]) - for (const date in dateStore) { - const values = [] - for (const chartName in data) { - values.push(dateStore[date]?.[chartName] ?? '') + + rows.push(['Timestamp', 'Date', ...charts]) + for (const date in dateStore) { + const values = [] + for (const chartName in data) { + values.push(dateStore[date]?.[chartName] ?? '') + } + rows.push([date, toNiceCsvDate(+date / 1000), ...values]) } - rows.push([date, toNiceCsvDate(+date / 1000), ...values]) + + download( + filename, + rows + .sort((a, b) => a[0] - b[0]) + .map((r) => r.join(',')) + .join('\n') + ) + + if (onLoadingEnd) onLoadingEnd() + } catch (error) { + if (onError) onError(error as Error) + else if (onLoadingEnd) onLoadingEnd() + throw error } - download( - filename, - rows - .sort((a, b) => a[0] - b[0]) - .map((r) => r.join(',')) - .join('\n') - ) } diff --git a/src/containers/BridgedTVL/BridgedTVLChainsList.tsx b/src/containers/BridgedTVL/BridgedTVLChainsList.tsx index 58ca28be60..3a0584c585 100644 --- a/src/containers/BridgedTVL/BridgedTVLChainsList.tsx +++ b/src/containers/BridgedTVL/BridgedTVLChainsList.tsx @@ -6,8 +6,11 @@ import { RowLinksWithDropdown } from '~/components/RowLinksWithDropdown' import { TableWithSearch } from '~/components/Table/TableWithSearch' import { TokenLogo } from '~/components/TokenLogo' import { chainIconUrl, download, formattedNum, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' export function BridgedTVLChainsList({ assets, chains, flows1d }) { + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() + const data = Object.keys(assets) .map((name) => { const chainAssets = assets?.[name] @@ -39,7 +42,7 @@ export function BridgedTVLChainsList({ assets, chains, flows1d }) { .concat(csvData.map((row) => headers.map((header) => row[header]).join(','))) .join('\n') - download('bridged-chains.csv', csv) + downloadCSV('bridged-chains.csv', csv) } return ( @@ -52,7 +55,7 @@ export function BridgedTVLChainsList({ assets, chains, flows1d }) { columnToSearch={['name']} customFilters={ <> - + } /> diff --git a/src/containers/Bridges/BridgesOverviewByChain.tsx b/src/containers/Bridges/BridgesOverviewByChain.tsx index 96e982f237..7c40a05455 100644 --- a/src/containers/Bridges/BridgesOverviewByChain.tsx +++ b/src/containers/Bridges/BridgesOverviewByChain.tsx @@ -11,6 +11,7 @@ import { TxsTableSwitch } from '~/containers/Bridges/TableSwitch' import { useBuildBridgeChartData } from '~/containers/Bridges/utils' import { BRIDGES_SHOWING_TXS, useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { download, formattedNum, getPrevVolumeFromChart, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' const BarChart = React.lazy(() => import('~/components/ECharts/BarChart')) as React.FC @@ -34,6 +35,8 @@ export function BridgesOverviewByChain({ const [chartType, setChartType] = React.useState(selectedChain === 'All' ? 'Volumes' : 'Bridge Volume') const [chartView, setChartView] = React.useState<'default' | 'netflow' | 'volume'>('netflow') const [activeTab, setActiveTab] = React.useState<'bridges' | 'messaging'>('bridges') + const { downloadCSV: downloadMainCSV, isLoading: isMainDownloadLoading } = useCSVDownload() + const { downloadCSV: downloadChartCSV, isLoading: isChartDownloadLoading } = useCSVDownload() useEffect(() => { setChartView('netflow') @@ -135,7 +138,7 @@ export function BridgesOverviewByChain({ }, 0) ]) }) - download(fileName, rows.map((r) => r.join(',')).join('\n')) + downloadMainCSV(fileName, rows.map((r) => r.join(',')).join('\n')) } const downloadChartCsv = () => { @@ -184,7 +187,7 @@ export function BridgesOverviewByChain({ if (rows.length === 0) { alert('Not supported for this chart type') } else { - download(fileName, rows.map((r) => r.join(',')).join('\n')) + downloadChartCSV(fileName, rows.map((r) => r.join(',')).join('\n')) } } @@ -234,7 +237,7 @@ export function BridgesOverviewByChain({ {formattedNum(monthTotalVolume, true)}

- +
{selectedChain === 'All' ? ( @@ -329,7 +332,7 @@ export function BridgesOverviewByChain({ )}
- +
diff --git a/src/containers/ChainOverview/Stats.tsx b/src/containers/ChainOverview/Stats.tsx index f7a57b6b89..baa815640d 100644 --- a/src/containers/ChainOverview/Stats.tsx +++ b/src/containers/ChainOverview/Stats.tsx @@ -5,7 +5,6 @@ import dayjs from 'dayjs' import toast from 'react-hot-toast' import { Bookmark } from '~/components/Bookmark' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart } from '~/components/ECharts/utils' import { EmbedChart } from '~/components/EmbedChart' import { Icon } from '~/components/Icon' import { TokenLogo } from '~/components/TokenLogo' @@ -14,7 +13,8 @@ import { chainCoingeckoIdsForGasNotMcap } from '~/constants/chainTokens' import { formatRaisedAmount } from '~/containers/ProtocolOverview/utils' import { useDarkModeManager, useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { useAuthContext } from '~/containers/Subscribtion/auth' -import { capitalizeFirstLetter, chainIconUrl, downloadCSV, formattedNum, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' +import { capitalizeFirstLetter, chainIconUrl, formattedNum, slug } from '~/utils' import { BAR_CHARTS, ChainChartLabels, chainCharts, chainOverviewChartColors } from './constants' import { IChainOverviewData } from './types' import { useFetchChainChartData } from './useFetchChainChartData' @@ -46,7 +46,8 @@ export const Stats = memo(function Stats(props: IStatsProps) { const [tvlSettings] = useLocalStorageSettingsManager('tvl') - const { isAuthenticated } = useAuthContext() + const { authorizedFetch, isAuthenticated } = useAuthContext() + const { downloadCSV, downloadChart, isLoading: isDownloadLoading } = useCSVDownload() const { toggledCharts, DENOMINATIONS, chainGeckoId, hasAtleasOneBarChart, groupBy, denomination } = useMemo(() => { const queryParams = JSON.parse(queryParamsString) @@ -650,7 +651,7 @@ export const Stats = memo(function Stats(props: IStatsProps) { .map((t) => `${t[0]}=true`) .join('&')}`.replaceAll(' ', '%20') - const response = await fetch(url) + const response = await authorizedFetch(url) if (!response || !response.ok) { toast.error('Failed to download CSV data') @@ -659,13 +660,13 @@ export const Stats = memo(function Stats(props: IStatsProps) { const csvData = await response.text() - downloadCSV(`${props.metadata.name}.csv`, csvData) } catch (error) { console.error('CSV download error:', error) toast.error('Failed to download CSV data') } }} + isLoading={isDownloadLoading} smol className="ml-auto" /> @@ -818,6 +819,7 @@ export const Stats = memo(function Stats(props: IStatsProps) { console.error('Error generating CSV:', error) } }} + isLoading={isDownloadLoading} smol /> diff --git a/src/containers/ChainOverview/Table.tsx b/src/containers/ChainOverview/Table.tsx index 26051149fd..8edbdf0f47 100644 --- a/src/containers/ChainOverview/Table.tsx +++ b/src/containers/ChainOverview/Table.tsx @@ -27,7 +27,8 @@ import { Tooltip } from '~/components/Tooltip' import { ICONS_CDN, removedCategoriesFromChainTvl } from '~/constants' import { subscribeToLocalStorage, useCustomColumns, useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { formatProtocolsList2 } from '~/hooks/data/defi' -import { chainIconUrl, download, formattedNum, formattedPercent, slug } from '~/utils' +import { chainIconUrl, formattedNum, formattedPercent, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { formatValue } from '../../utils' import { CustomColumnModal } from './CustomColumnModal' import { replaceAliases, sampleProtocol } from './customColumnsUtils' @@ -49,6 +50,7 @@ export const ChainProtocolsTable = ({ sampleRow?: any }) => { const { customColumns, setCustomColumns } = useCustomColumns() + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const router = useRouter() const [extraTvlsEnabled] = useLocalStorageSettingsManager('tvl') @@ -407,9 +409,9 @@ export const ChainProtocolsTable = ({ }) }) - const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n') const chainName = router.query.chain || 'all' - download(`defillama-${chainName}-protocols.csv`, csvContent) + const filename = `defillama-${chainName}-protocols.csv` + downloadCSVFromHook(filename, [headers, ...rows]) } return ( @@ -461,7 +463,7 @@ export const ChainProtocolsTable = ({ onDeleteCustomColumn={handleDeleteCustomColumn} /> - + diff --git a/src/containers/ChainsByCategory/index.tsx b/src/containers/ChainsByCategory/index.tsx index 1f17487126..0295e74dc1 100644 --- a/src/containers/ChainsByCategory/index.tsx +++ b/src/containers/ChainsByCategory/index.tsx @@ -7,8 +7,9 @@ import { RowLinksWithDropdown } from '~/components/RowLinksWithDropdown' import { useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { useGroupChainsByParent } from '~/hooks/data' import { formatDataWithExtraTvls, groupDataWithTvlsByDay } from '~/hooks/data/defi' +import { useCSVDownload } from '~/hooks/useCSVDownload' import Layout from '~/layout' -import { download, preparePieChartData, toNiceCsvDate } from '~/utils' +import { preparePieChartData, toNiceCsvDate } from '~/utils' import { getChainsByCategory } from './queries' import { ChainsByCategoryTable } from './Table' import { IChainsByCategoryData } from './types' @@ -32,6 +33,7 @@ export function ChainsByCategory({ const { query } = useRouter() const { minTvl, maxTvl } = query const [extraTvlsEnabled] = useLocalStorageSettingsManager('tvl') + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const { dataByChain, pieChartData, chainsWithExtraTvlsAndDominanceByDay, chainsUniqueFiltered } = React.useMemo(() => { @@ -69,7 +71,7 @@ export function ChainsByCategory({ } }, [chains, chainAssets, extraTvlsEnabled, stackedDataset, tvlTypes, minTvl, maxTvl, chainsUnique]) - const downloadCsv = async () => { + const handleCSVDownload = async () => { window.alert('Data download might take up to 1 minute, click OK to proceed') const rows = [['Timestamp', 'Date', ...chainsUniqueFiltered]] const { stackedDataset } = await getChainsByCategory({ category: 'All' }) @@ -84,7 +86,7 @@ export function ChainsByCategory({ .forEach((day) => { rows.push([day.date, toNiceCsvDate(day.date), ...chainsUniqueFiltered.map((chain) => day[chain] ?? '')]) }) - download('chains.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('chains.csv', rows.map((r) => r.join(',')).join('\n')) } const showByGroup = ['All', 'Non-EVM'].includes(category) ? true : false @@ -102,7 +104,7 @@ export function ChainsByCategory({
- + }> diff --git a/src/containers/DimensionAdapters/AdapterByChain.tsx b/src/containers/DimensionAdapters/AdapterByChain.tsx index 3a6491f10e..df820e403a 100644 --- a/src/containers/DimensionAdapters/AdapterByChain.tsx +++ b/src/containers/DimensionAdapters/AdapterByChain.tsx @@ -25,7 +25,8 @@ import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import useWindowSize from '~/hooks/useWindowSize' -import { chainIconUrl, download, formattedNum, slug } from '~/utils' +import { chainIconUrl, formattedNum, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { chainCharts } from '../ChainOverview/constants' import { protocolCharts } from '../ProtocolOverview/Chart/constants' import { AdapterByChainChart } from './ChainChart' @@ -92,6 +93,7 @@ const getProtocolsByCategory = (protocols: IAdapterByChainPageData['protocols'], export function AdapterByChain(props: IProps) { const router = useRouter() const [enabledSettings] = useLocalStorageSettingsManager('fees') + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const { selectedCategories, protocols, columnsOptions } = useMemo(() => { const selectedCategories = @@ -277,10 +279,9 @@ export function AdapterByChain(props: IProps) { ] }) - const csv = [header, ...csvdata].map((row) => row.join(',')).join('\n') - - download(`${props.type}-${props.chain}-protocols.csv`, csv) - }, [props, protocols]) + const filename = `${props.type}-${props.chain}-protocols.csv` + downloadCSVFromHook(filename, [header, ...csvdata]) + }, [props, protocols, downloadCSVFromHook]) const { category, chain, ...queries } = router.query @@ -482,7 +483,7 @@ export function AdapterByChain(props: IProps) { /> )} {SUPPORTED_OLD_VIEWS.includes(props.type) ? : null} - +
diff --git a/src/containers/DimensionAdapters/ChainsByAdapter.tsx b/src/containers/DimensionAdapters/ChainsByAdapter.tsx index ce0e783d82..22ce5abf61 100644 --- a/src/containers/DimensionAdapters/ChainsByAdapter.tsx +++ b/src/containers/DimensionAdapters/ChainsByAdapter.tsx @@ -20,7 +20,8 @@ import { alphanumericFalsyLast } from '~/components/Table/utils' import { TokenLogo } from '~/components/TokenLogo' import { useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import useWindowSize from '~/hooks/useWindowSize' -import { download, formattedNum, slug } from '~/utils' +import { formattedNum, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { ChainsByAdapterChart } from './ChainChart' import { IChainsByAdapterPageData } from './types' @@ -43,6 +44,7 @@ interface IProps extends IChainsByAdapterPageData { export function ChainsByAdapter(props: IProps) { const [enabledSettings] = useLocalStorageSettingsManager('fees') + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const [sorting, setSorting] = useState([{ desc: true, id: 'total24h' }]) const [columnFilters, setColumnFilters] = useState([]) @@ -123,10 +125,10 @@ export function ChainsByAdapter(props: IProps) { const csvdata = chains.map((protocol) => { return [protocol.name, protocol.total24h, protocol.total30d] }) - const csv = [header, ...csvdata].map((row) => row.join(',')).join('\n') - download(`${props.type}-chains-protocols.csv`, csv) - }, [props, chains]) + const filename = `${props.type}-chains-protocols.csv` + downloadCSVFromHook(filename, [header, ...csvdata]) + }, [props, chains, downloadCSVFromHook]) return ( <> @@ -164,7 +166,7 @@ export function ChainsByAdapter(props: IProps) { className="w-full rounded-md border border-(--form-control-border) bg-white py-1 pr-2 pl-7 text-sm text-black dark:bg-black dark:text-white" />
- + diff --git a/src/containers/DimensionAdapters/ProtocolChart.tsx b/src/containers/DimensionAdapters/ProtocolChart.tsx index 8d2809c4be..d65296d94a 100644 --- a/src/containers/DimensionAdapters/ProtocolChart.tsx +++ b/src/containers/DimensionAdapters/ProtocolChart.tsx @@ -7,6 +7,7 @@ import { SelectWithCombobox } from '~/components/SelectWithCombobox' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' import { download, firstDayOfMonth, getNDistinctColors, lastDayOfWeek, slug, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { ADAPTER_TYPES } from './constants' const INTERVALS_LIST = ['Daily', 'Weekly', 'Monthly', 'Cumulative'] as const @@ -77,6 +78,7 @@ const ChartByType = ({ }) => { const [chartInterval, changeChartInterval] = React.useState<(typeof INTERVALS_LIST)[number]>('Daily') const [selectedTypes, setSelectedTypes] = React.useState(allTypes) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const mainChartData = React.useMemo(() => { const chartData = {} @@ -252,11 +254,12 @@ const ChartByType = ({ const filename = `${csvTitle}-${chartInterval.toLowerCase()}-${ new Date().toISOString().split('T')[0] }.csv` - download(filename, rows.map((r) => r.join(',')).join('\n')) + downloadCSV(filename, rows, { addTimestamp: false }) } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isDownloadLoading} smol /> diff --git a/src/containers/Hacks/index.tsx b/src/containers/Hacks/index.tsx index 0fa569452e..93a08fe503 100644 --- a/src/containers/Hacks/index.tsx +++ b/src/containers/Hacks/index.tsx @@ -18,6 +18,7 @@ import { TagGroup } from '~/components/TagGroup' import { Tooltip } from '~/components/Tooltip' import Layout from '~/layout' import { capitalizeFirstLetter, download, formattedNum, toNiceDayMonthAndYear } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { IHacksPageData } from './queries' const PieChart = React.lazy(() => import('~/components/ECharts/PieChart')) as React.FC @@ -31,6 +32,7 @@ function HacksTable({ data }: { data: IHacksPageData['data'] }) { const [columnFilters, setColumnFilters] = React.useState([]) const [sorting, setSorting] = React.useState([{ desc: true, id: 'date' }]) const [projectName, setProjectName] = React.useState('') + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const instance = useReactTable({ data: data, columns: hacksColumns, @@ -119,11 +121,12 @@ function HacksTable({ data }: { data: IHacksPageData['data'] }) { link ]) } - download('hacks.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('hacks.csv', rows.map((r) => r.join(',')).join('\n')) } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isDownloadLoading} /> @@ -144,6 +147,7 @@ export const HacksContainer = ({ pieChartData }: IHacksPageData) => { const [chartType, setChartType] = React.useState('Monthly Sum') + const { downloadCSV: downloadChartCSV, isLoading: isChartDownloadLoading } = useCSVDownload() return ( @@ -178,12 +182,13 @@ export const HacksContainer = ({ for (const { name, value } of pieChartData) { rows.push([name, value]) } - download('total-hacked-by-technique.csv', rows.map((r) => r.join(',')).join('\n')) + downloadChartCSV('total-hacked-by-technique.csv', rows.map((r) => r.join(',')).join('\n')) } } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isChartDownloadLoading} smol /> diff --git a/src/containers/Liquidations/LiquidationsContent.tsx b/src/containers/Liquidations/LiquidationsContent.tsx index 3a6e2bc297..8a444f8f39 100644 --- a/src/containers/Liquidations/LiquidationsContent.tsx +++ b/src/containers/Liquidations/LiquidationsContent.tsx @@ -14,6 +14,7 @@ import { } from '~/containers/Liquidations/utils' import { LIQS_SETTINGS, useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { download, liquidationsIconUrl } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { LiquidableChanges24H } from './LiquidableChanges24H' import { StackBySwitch } from './StackBySwitch' import { TotalLiquidable } from './TotalLiquidable' @@ -26,6 +27,7 @@ const LiquidationsChart = React.lazy(() => export const LiquidationsContent = (props: { data: ChartData; prevData: ChartData; options: ISearchItem[] }) => { const { data, prevData } = props const [bobo, setBobo] = React.useState(false) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() return (
@@ -53,8 +55,9 @@ export const LiquidationsContent = (props: { data: ChartData; prevData: ChartDat { const csvString = await getLiquidationsCsvData(data.symbol) - download(`${data.symbol}-all-positions.csv`, csvString) + downloadCSV(`${data.symbol}-all-positions.csv`, csvString) }} + isLoading={isDownloadLoading} smol className="mt-auto mr-auto" /> diff --git a/src/containers/Oracles/index.tsx b/src/containers/Oracles/index.tsx index 3709e67b57..609f5e4e32 100644 --- a/src/containers/Oracles/index.tsx +++ b/src/containers/Oracles/index.tsx @@ -8,8 +8,9 @@ import { BasicLink } from '~/components/Link' import { RowLinksWithDropdown } from '~/components/RowLinksWithDropdown' import { TableWithSearch } from '~/components/Table/TableWithSearch' import { useCalcGroupExtraTvlsByDay } from '~/hooks/data' +import { useCSVDownload } from '~/hooks/useCSVDownload' import Layout from '~/layout' -import { download, formattedNum, preparePieChartData } from '~/utils' +import { formattedNum, preparePieChartData } from '~/utils' const PieChart = React.lazy(() => import('~/components/ECharts/PieChart')) as React.FC @@ -27,6 +28,7 @@ export const OraclesByChain = ({ chain }) => { const { chainsWithExtraTvlsByDay, chainsWithExtraTvlsAndDominanceByDay } = useCalcGroupExtraTvlsByDay(chartData) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const { tokenTvls, tokensList } = React.useMemo(() => { const tvls = Object.entries(chainsWithExtraTvlsByDay[chainsWithExtraTvlsByDay.length - 1]) .filter((item) => item[0] !== 'date') @@ -49,9 +51,9 @@ export const OraclesByChain = ({ return { tokenTvls, tokensList } }, [chainsWithExtraTvlsByDay, tokensProtocols, chainsByOracle]) - const downloadCsv = () => { + const handleCSVDownload = () => { const header = Object.keys(tokensList[0]).join(',') - download('oracles.csv', [header, ...tokensList.map((r) => Object.values(r).join(','))].join('\n')) + downloadCSV('oracles.csv', [header, ...tokensList.map((r) => Object.values(r).join(','))].join('\n')) } return ( @@ -60,7 +62,7 @@ export const OraclesByChain = ({
- + }> diff --git a/src/containers/ProDashboard/components/ProTable/TableHeader.tsx b/src/containers/ProDashboard/components/ProTable/TableHeader.tsx index 5fec4bdf3b..deb4c89972 100644 --- a/src/containers/ProDashboard/components/ProTable/TableHeader.tsx +++ b/src/containers/ProDashboard/components/ProTable/TableHeader.tsx @@ -13,6 +13,7 @@ interface TableHeaderProps { showColumnPanel: boolean setShowColumnPanel: (show: boolean) => void downloadCSV: () => void + isCSVLoading?: boolean colSpan?: 1 | 2 customViews?: CustomView[] onSaveView?: (name: string) => void @@ -31,6 +32,7 @@ export function TableHeader({ showColumnPanel, setShowColumnPanel, downloadCSV, + isCSVLoading, colSpan = 2, customViews = [], onSaveView, @@ -176,7 +178,7 @@ export function TableHeader({
)} - +
- +
- +
@@ -108,6 +109,7 @@ const pageName = ['Deals by Investor'] export const InvestorContainer = ({ raises, investors, rounds, sectors, chains, investorName }) => { const { pathname } = useRouter() + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const { filteredRaisesList, @@ -202,7 +204,14 @@ export const InvestorContainer = ({ raises, investors, rounds, sectors, chains,
- downloadCsv({ raises })} /> + { + const { headers, rows } = prepareCsvData({ raises }) + downloadCSVFromHook('raises.csv', [headers, ...rows]) + }} + isCSVLoading={isCSVLoading} + /> ) } diff --git a/src/containers/Raises/RaisesTable.tsx b/src/containers/Raises/RaisesTable.tsx index bb5019dd1c..2a69bfbfdc 100644 --- a/src/containers/Raises/RaisesTable.tsx +++ b/src/containers/Raises/RaisesTable.tsx @@ -16,7 +16,7 @@ import useWindowSize from '~/hooks/useWindowSize' const columnResizeMode = 'onChange' -export function RaisesTable({ raises, downloadCsv }) { +export function RaisesTable({ raises, downloadCsv, isCSVLoading }) { const [columnFilters, setColumnFilters] = React.useState([]) const [sorting, setSorting] = React.useState([{ desc: true, id: 'date' }]) const [columnOrder, setColumnOrder] = React.useState([]) @@ -98,7 +98,7 @@ export function RaisesTable({ raises, downloadCsv }) { > Download.json - +
diff --git a/src/containers/Raises/download.ts b/src/containers/Raises/download.ts index 2d3297a550..16e96649ca 100644 --- a/src/containers/Raises/download.ts +++ b/src/containers/Raises/download.ts @@ -1,46 +1,45 @@ -import { download, toNiceCsvDate } from '~/utils' +import { toNiceCsvDate } from '~/utils' // prepare csv data -export const downloadCsv = ({ raises }) => { - const rows = [ - [ - 'Name', - 'Timestamp', - 'Date', - 'Amount Raised', - 'Round', - 'Description', - 'Lead Investor', - 'Category', - 'Source', - 'Valuation', - 'Chains', - 'Other Investors' - ] +export const prepareCsvData = ({ raises }) => { + const headers = [ + 'Name', + 'Timestamp', + 'Date', + 'Amount Raised', + 'Round', + 'Description', + 'Lead Investor', + 'Category', + 'Source', + 'Valuation', + 'Chains', + 'Other Investors' ] const removeJumps = (text: string | number) => typeof text === 'string' ? '"' + text.replaceAll('\n', '').replaceAll('"', "'") + '"' : text - raises + + const rows = raises .sort((a, b) => b.date - a.date) - .forEach((item) => { - rows.push( - [ - item.name, - item.date, - toNiceCsvDate(item.date), - item.amount === null ? '' : item.amount * 1_000_000, - item.round ?? '', - item.sector ?? '', - item.leadInvestors?.join(' + ') ?? '', - item.category ?? '', - item.source ?? '', - item.valuation ?? '', - item.chains?.join(' + ') ?? '', - item.otherInvestors?.join(' + ') ?? '' - ].map(removeJumps) as string[] - ) + .map((item) => { + return [ + item.name, + item.date, + toNiceCsvDate(item.date), + item.amount === null ? '' : item.amount * 1_000_000, + item.round ?? '', + item.sector ?? '', + item.leadInvestors?.join(' + ') ?? '', + item.category ?? '', + item.source ?? '', + item.valuation ?? '', + item.chains?.join(' + ') ?? '', + item.otherInvestors?.join(' + ') ?? '' + ].map(removeJumps) as string[] }) - download(`raises.csv`, rows.map((r) => r.join(',')).join('\n')) + return { headers, rows } } + +export const downloadCsv = prepareCsvData diff --git a/src/containers/Raises/index.tsx b/src/containers/Raises/index.tsx index b13b9b3d96..85e5b9ff69 100644 --- a/src/containers/Raises/index.tsx +++ b/src/containers/Raises/index.tsx @@ -6,7 +6,8 @@ import type { ILineAndBarChartProps } from '~/components/ECharts/types' import { RaisesFilters } from '~/containers/Raises/Filters' import Layout from '~/layout' import { formattedNum } from '~/utils' -import { downloadCsv } from './download' +import { prepareCsvData } from './download' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { useRaisesData } from './hooks' import { RaisesTable } from './RaisesTable' @@ -16,6 +17,7 @@ const LineAndBarChart = React.lazy( const RaisesContainer = ({ raises, investors, rounds, sectors, chains, investorName }) => { const { pathname } = useRouter() + const { downloadCSV: downloadCSVFromHook, isLoading: isCSVLoading } = useCSVDownload() const { filteredRaisesList, @@ -71,7 +73,10 @@ const RaisesContainer = ({ raises, investors, rounds, sectors, chains, investorN Total Funding Amount ${formattedNum(totalAmountRaised)}

- downloadCsv({ raises })} smol className="mt-auto mr-auto" /> + { + const { headers, rows } = prepareCsvData({ raises }) + downloadCSVFromHook('raises.csv', [headers, ...rows]) + }} isLoading={isCSVLoading} smol className="mt-auto mr-auto" />
@@ -81,7 +86,10 @@ const RaisesContainer = ({ raises, investors, rounds, sectors, chains, investorN
- downloadCsv({ raises })} /> + { + const { headers, rows } = prepareCsvData({ raises }) + downloadCSVFromHook('raises.csv', [headers, ...rows]) + }} isCSVLoading={isCSVLoading} />
) } diff --git a/src/containers/RecentProtocols/Table.tsx b/src/containers/RecentProtocols/Table.tsx index fda5f1e6a6..ec08570f4f 100644 --- a/src/containers/RecentProtocols/Table.tsx +++ b/src/containers/RecentProtocols/Table.tsx @@ -21,7 +21,8 @@ import { columnSizes, protocolsColumns } from '~/components/Table/Defi/Protocols import { IProtocolRow } from '~/components/Table/Defi/Protocols/types' import { VirtualTable } from '~/components/Table/Table' import useWindowSize from '~/hooks/useWindowSize' -import { download, formattedNum, toNiceDaysAgo } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' +import { formattedNum, toNiceDaysAgo } from '~/utils' export function RecentlyListedProtocolsTable({ data, @@ -45,6 +46,7 @@ export function RecentlyListedProtocolsTable({ const [expanded, setExpanded] = React.useState({}) const windowSize = useWindowSize() const [columnFilters, setColumnFilters] = React.useState([]) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const router = useRouter() @@ -143,7 +145,7 @@ export function RecentlyListedProtocolsTable({ ) } - const downloadCSV = () => { + const handleCSVDownload = () => { const headers = ['Name', 'TVL', 'Change 1d', 'Change 7d', 'Change 1m', 'Listed At', 'Chains'] const csvData = data.map((row) => { return { @@ -156,7 +158,7 @@ export function RecentlyListedProtocolsTable({ 'Listed At': new Date(row.listedAt * 1000).toLocaleDateString() } }) - download( + downloadCSV( 'protocols.csv', [headers, ...csvData.map((row) => headers.map((header) => row[header]).join(','))].join('\n') ) @@ -203,7 +205,7 @@ export function RecentlyListedProtocolsTable({ {forkedList ? : null} - + diff --git a/src/containers/Stablecoins/ChainsWithStablecoins.tsx b/src/containers/Stablecoins/ChainsWithStablecoins.tsx index de8044e2e3..21fa4f04fa 100644 --- a/src/containers/Stablecoins/ChainsWithStablecoins.tsx +++ b/src/containers/Stablecoins/ChainsWithStablecoins.tsx @@ -6,7 +6,8 @@ import { Tooltip } from '~/components/Tooltip' import { ChartSelector } from '~/containers/Stablecoins/ChartSelector' import { getStablecoinDominance } from '~/containers/Stablecoins/utils' import { useCalcCirculating, useCalcGroupExtraPeggedByDay, useGroupChainsPegged } from '~/hooks/data/stablecoins' -import { download, formattedNum, preparePieChartData, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' +import { formattedNum, preparePieChartData, toNiceCsvDate } from '~/utils' import { PeggedChainsTable } from './Table' const AreaChart = React.lazy(() => import('~/components/ECharts/AreaChart')) as React.FC @@ -34,13 +35,14 @@ export function ChainsWithStablecoins({ }) { const [chartType, setChartType] = React.useState('Pie') const chartTypeList = ['Total Market Cap', 'Chain Market Caps', 'Pie', 'Dominance'] + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const filteredPeggedAssets = chainCirculatings const chainTotals = useCalcCirculating(filteredPeggedAssets) const { data: stackedData, dataWithExtraPeggedAndDominanceByDay } = useCalcGroupExtraPeggedByDay(stackedDataset) - const downloadCsv = () => { + const handleCSVDownload = () => { const rows = [['Timestamp', 'Date', ...chainList, 'Total']] stackedData .sort((a, b) => a.date - b.date) @@ -54,7 +56,7 @@ export function ChainsWithStablecoins({ }, 0) ]) }) - download('stablecoinsChainTotals.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('stablecoinsChainTotals.csv', rows.map((r) => r.join(',')).join('\n')) } const mcapToDisplay = formattedNum(totalMcapCurrent, true) @@ -134,7 +136,7 @@ export function ChainsWithStablecoins({ {dominance}%

- +
diff --git a/src/containers/Stablecoins/Filters/Dropdowns.tsx b/src/containers/Stablecoins/Filters/Dropdowns.tsx index a09ed7770d..0fbd52c44b 100644 --- a/src/containers/Stablecoins/Filters/Dropdowns.tsx +++ b/src/containers/Stablecoins/Filters/Dropdowns.tsx @@ -8,11 +8,13 @@ import { ResetAllStablecoinFilters } from './ResetAll' export function PeggedFiltersDropdowns({ pathname, nestedMenu, - downloadCsv + downloadCsv, + isDownloadLoading }: { pathname: string nestedMenu?: boolean downloadCsv: () => void + isDownloadLoading?: boolean }) { return ( <> @@ -21,7 +23,7 @@ export function PeggedFiltersDropdowns({ - + ) } diff --git a/src/containers/Stablecoins/Filters/index.tsx b/src/containers/Stablecoins/Filters/index.tsx index ccb5f09d28..a0bd246cf2 100644 --- a/src/containers/Stablecoins/Filters/index.tsx +++ b/src/containers/Stablecoins/Filters/index.tsx @@ -4,7 +4,7 @@ import { useIsClient } from '~/hooks' import { useMedia } from '~/hooks/useMedia' import { PeggedFiltersDropdowns } from './Dropdowns' -export function PeggedFilters(props: { pathname: string; downloadCsv: () => void }) { +export function PeggedFilters(props: { pathname: string; downloadCsv: () => void; isDownloadLoading?: boolean }) { const isSmall = useMedia(`(max-width: 639px)`) const isClient = useIsClient() return ( diff --git a/src/containers/Stablecoins/StablecoinOverview.tsx b/src/containers/Stablecoins/StablecoinOverview.tsx index e04317f3a3..02ab311151 100644 --- a/src/containers/Stablecoins/StablecoinOverview.tsx +++ b/src/containers/Stablecoins/StablecoinOverview.tsx @@ -14,10 +14,10 @@ import { oldBlue } from '~/constants/colors' import { buildStablecoinChartData } from '~/containers/Stablecoins/utils' import { UNRELEASED, useLocalStorageSettingsManager } from '~/contexts/LocalStorage' import { useCalcCirculating, useCalcGroupExtraPeggedByDay, useGroupBridgeData } from '~/hooks/data/stablecoins' +import { useCSVDownload } from '~/hooks/useCSVDownload' import Layout from '~/layout' import { capitalizeFirstLetter, - download, formattedNum, getBlockExplorer, peggedAssetIconUrl, @@ -67,6 +67,7 @@ export const PeggedAssetInfo = ({ mcap, bridgeInfo }) => { + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() let { name, onCoinGecko, @@ -118,7 +119,7 @@ export const PeggedAssetInfo = ({ const groupedChains = useGroupBridgeData(chainTotals, bridgeInfo) - const downloadCsv = () => { + const handleCSVDownload = () => { const rows = [['Timestamp', 'Date', ...chainsUnique, 'Total']] stackedData .sort((a, b) => a.date - b.date) @@ -132,7 +133,7 @@ export const PeggedAssetInfo = ({ }, 0) ]) }) - download('stablecoinsChains.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('stablecoinsChains.csv', rows.map((r) => r.join(',')).join('\n')) } return ( @@ -226,7 +227,7 @@ export const PeggedAssetInfo = ({ )} - +
diff --git a/src/containers/Stablecoins/StablecoinsByChain.tsx b/src/containers/Stablecoins/StablecoinsByChain.tsx index 6710fc6fb8..a3efd4b5bb 100644 --- a/src/containers/Stablecoins/StablecoinsByChain.tsx +++ b/src/containers/Stablecoins/StablecoinsByChain.tsx @@ -16,7 +16,8 @@ import { useCalcGroupExtraPeggedByDay, useFormatStablecoinQueryParams } from '~/hooks/data/stablecoins' -import { download, formattedNum, getPercentChange, preparePieChartData, slug, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' +import { formattedNum, getPercentChange, preparePieChartData, slug, toNiceCsvDate } from '~/utils' import { PeggedAssetsTable } from './Table' const AreaChart = React.lazy(() => import('~/components/ECharts/AreaChart')) as React.FC @@ -35,6 +36,7 @@ export function StablecoinsByChain({ doublecountedIds }) { const [chartType, setChartType] = React.useState('Total Market Cap') + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const chartTypeList = selectedChain !== 'All' @@ -135,7 +137,7 @@ export function StablecoinsByChain({ const { data: stackedData, dataWithExtraPeggedAndDominanceByDay } = useCalcGroupExtraPeggedByDay(stackedDataset) - const downloadCsv = () => { + const handleCSVDownload = () => { const filteredPeggedNames = peggedAssetNames.filter((name, i) => filteredIndexes.includes(i)) const rows = [['Timestamp', 'Date', ...filteredPeggedNames, 'Total']] stackedData @@ -150,7 +152,7 @@ export function StablecoinsByChain({ }, 0) ]) }) - download('stablecoins.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('stablecoins.csv', rows.map((r) => r.join(',')).join('\n')) } let title = `Stablecoins Market Cap` @@ -222,7 +224,7 @@ export function StablecoinsByChain({ <> - +
diff --git a/src/containers/Treasuries/index.tsx b/src/containers/Treasuries/index.tsx index 39bdd2e7d6..1b800a65d5 100644 --- a/src/containers/Treasuries/index.tsx +++ b/src/containers/Treasuries/index.tsx @@ -14,13 +14,15 @@ import { TableWithSearch } from '~/components/Table/TableWithSearch' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import Layout from '~/layout' -import { download, formattedNum, getDominancePercent, tokenIconUrl } from '~/utils' +import { formattedNum, getDominancePercent, tokenIconUrl } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' const pageName = ['Protocols', 'ranked by', 'Treasury'] export function Treasuries({ data, entity }) { const [columnFilters, setColumnFilters] = useState([]) const [sorting, setSorting] = useState([]) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const tableColumns = useMemo( () => (entity ? columns.filter((c: any) => !['ownTokens', 'coreTvl'].includes(c.accessorKey)) : columns), [entity] @@ -41,7 +43,7 @@ export function Treasuries({ data, entity }) { const [projectName, setProjectName] = useState('') - const downloadCSV = () => { + const handleCSVDownload = () => { const headers = [ 'Name', 'Category', @@ -75,7 +77,7 @@ export function Treasuries({ data, entity }) { const csv = [headers.join(',')] .concat(dataToDownload.map((row) => headers.map((header) => row[header]).join(','))) .join('\n') - download('treasuries.csv', csv) + downloadCSV('treasuries.csv', csv) } useEffect(() => { @@ -96,7 +98,7 @@ export function Treasuries({ data, entity }) { header={'Treasuries'} customFilters={ <> - + } /> diff --git a/src/containers/Yields/Filters/Dropdowns.tsx b/src/containers/Yields/Filters/Dropdowns.tsx index d2475698a5..99b8272789 100644 --- a/src/containers/Yields/Filters/Dropdowns.tsx +++ b/src/containers/Yields/Filters/Dropdowns.tsx @@ -52,7 +52,8 @@ export function YieldFilterDropdowns({ showTotalSupplied, showTotalBorrowed, showAvailable, - onCSVDownload + onCSVDownload, + isCSVDownloading }: IDropdownMenusProps) { const router = useRouter() @@ -316,6 +317,7 @@ export function YieldFilterDropdowns({ : 'flex cursor-pointer flex-nowrap items-center gap-2 rounded-md bg-(--btn-bg) px-3 py-2 text-xs text-(--text-primary) hover:bg-(--btn-hover-bg) focus-visible:bg-(--btn-hover-bg)' } onClick={onCSVDownload} + isLoading={isCSVDownloading} /> ) : null} diff --git a/src/containers/Yields/Filters/types.ts b/src/containers/Yields/Filters/types.ts index 1638527bd3..98e058cdfe 100644 --- a/src/containers/Yields/Filters/types.ts +++ b/src/containers/Yields/Filters/types.ts @@ -36,6 +36,7 @@ export interface IDropdownMenusProps { showTotalBorrowed?: boolean showAvailable?: boolean onCSVDownload?: () => void + isCSVDownloading?: boolean } export interface IYieldFiltersProps extends IDropdownMenusProps { @@ -48,4 +49,5 @@ export interface IYieldFiltersProps extends IDropdownMenusProps { noOfStrategies?: number showSearchOnMobile?: boolean onCSVDownload?: () => void + isCSVDownloading?: boolean } diff --git a/src/containers/Yields/index.tsx b/src/containers/Yields/index.tsx index 05b5840135..77ed4bb3a2 100644 --- a/src/containers/Yields/index.tsx +++ b/src/containers/Yields/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useRouter } from 'next/router' import { Announcement } from '~/components/Announcement' import { LocalLoader } from '~/components/LocalLoader' -import { download } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { YieldFiltersV2 } from './Filters' import { useFormatYieldQueryParams } from './hooks' import { YieldsPoolsTable } from './Tables/Pools' @@ -12,6 +12,7 @@ const YieldPage = ({ pools, projectList, chainList, categoryList, tokens, tokenS const { query, pathname, push } = useRouter() const { minTvl, maxTvl, minApy, maxApy } = query const [loading, setLoading] = React.useState(true) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const { selectedProjects, @@ -127,7 +128,7 @@ const YieldPage = ({ pools, projectList, chainList, categoryList, tokens, tokenS pathname, pairTokens ]) - const downloadCSV = React.useCallback(() => { + const handleCSVDownload = React.useCallback(() => { const headers = [ 'Pool', 'Project', @@ -191,8 +192,8 @@ const YieldPage = ({ pools, projectList, chainList, categoryList, tokens, tokenS } }) const csv = [headers].concat(csvData.map((row) => headers.map((header) => row[header]))).join('\n') - download('yields.csv', csv) - }, [poolsData]) + downloadCSV('yields.csv', csv) + }, [poolsData, downloadCSV]) return ( <> @@ -251,7 +252,8 @@ const YieldPage = ({ pools, projectList, chainList, categoryList, tokens, tokenS showTotalBorrowed={true} showAvailable={true} showLTV={true} - onCSVDownload={downloadCSV} + onCSVDownload={handleCSVDownload} + isCSVDownloading={isDownloadLoading} /> {loading ? ( diff --git a/src/hooks/useCSVDownload.ts b/src/hooks/useCSVDownload.ts new file mode 100644 index 0000000000..40333235fc --- /dev/null +++ b/src/hooks/useCSVDownload.ts @@ -0,0 +1,41 @@ +import { useState, useCallback } from 'react' +import { downloadCSV as downloadCSVUtil, downloadDatasetCSV as downloadDatasetCSVUtil } from '~/utils' +import { downloadChart as downloadChartUtil } from '~/components/ECharts/utils' + +export function useCSVDownload() { + const [isLoading, setIsLoading] = useState(false) + + const downloadCSV = useCallback((filename: string, csvData: any, options: any = {}) => { + return downloadCSVUtil(filename, csvData, { + ...options, + onLoadingStart: () => setIsLoading(true), + onLoadingEnd: () => setIsLoading(false), + onError: () => setIsLoading(false) + }) + }, []) + + const downloadDatasetCSV = useCallback((options: any) => { + return downloadDatasetCSVUtil({ + ...options, + onLoadingStart: () => setIsLoading(true), + onLoadingEnd: () => setIsLoading(false), + onError: () => setIsLoading(false) + }) + }, []) + + const downloadChart = useCallback((data: any, filename: string, options: any = {}) => { + return downloadChartUtil(data, filename, { + ...options, + onLoadingStart: () => setIsLoading(true), + onLoadingEnd: () => setIsLoading(false), + onError: () => setIsLoading(false) + }) + }, []) + + return { + isLoading, + downloadCSV, + downloadDatasetCSV, + downloadChart + } +} \ No newline at end of file diff --git a/src/pages/etfs.tsx b/src/pages/etfs.tsx index 29886fba91..d3357dfc98 100644 --- a/src/pages/etfs.tsx +++ b/src/pages/etfs.tsx @@ -8,6 +8,7 @@ import { TableWithSearch } from '~/components/Table/TableWithSearch' import { TagGroup } from '~/components/TagGroup' import Layout from '~/layout' import { download, firstDayOfMonth, formattedNum, lastDayOfWeek, toNiceCsvDate } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { withPerformanceLogging } from '~/utils/perf' const LineAndBarChart = React.lazy( @@ -79,6 +80,7 @@ const groupByList = ['Daily', 'Weekly', 'Monthly', 'Cumulative'] const PageView = ({ snapshot, flows, totalsByAsset, lastUpdated }: PageViewProps) => { const [groupBy, setGroupBy] = React.useState<(typeof groupByList)[number]>('Weekly') const [tickers, setTickers] = React.useState(['Bitcoin', 'Ethereum']) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const charts = React.useMemo(() => { const bitcoin = {} @@ -196,11 +198,12 @@ const PageView = ({ snapshot, flows, totalsByAsset, lastUpdated }: PageViewProps rows.push([date, toNiceCsvDate(date), flows[date]['Bitcoin'] ?? '', flows[date]['Ethereum'] ?? '']) } const filename = `etf-flows-${new Date().toISOString().split('T')[0]}.csv` - download(filename, rows.map((r) => r.join(',')).join('\n')) + downloadCSV(filename, rows.map((r) => r.join(',')).join('\n')) } catch (error) { console.error('Error generating CSV:', error) } }} + isLoading={isDownloadLoading} smol />
diff --git a/src/pages/forks.tsx b/src/pages/forks.tsx index 5aa264cb0d..d82ad53f2f 100644 --- a/src/pages/forks.tsx +++ b/src/pages/forks.tsx @@ -8,7 +8,8 @@ import { TableWithSearch } from '~/components/Table/TableWithSearch' import { getForkPageData } from '~/containers/Forks/queries' import { useCalcGroupExtraTvlsByDay, useCalcStakePool2Tvl } from '~/hooks/data' import Layout from '~/layout' -import { download, preparePieChartData } from '~/utils' +import { preparePieChartData } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { withPerformanceLogging } from '~/utils/perf' const PieChart = React.lazy(() => import('~/components/ECharts/PieChart')) as React.FC @@ -30,6 +31,7 @@ const pageName = ['Protocols', 'ranked by', 'TVL in Forks'] export default function Forks({ chartData, tokensProtocols, tokens, tokenLinks, parentTokens, forkColors }) { const forkedTokensData = useCalcStakePool2Tvl(parentTokens) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const { chainsWithExtraTvlsByDay, chainsWithExtraTvlsAndDominanceByDay } = useCalcGroupExtraTvlsByDay(chartData) @@ -59,7 +61,7 @@ export default function Forks({ chartData, tokensProtocols, tokens, tokenLinks, return { tokenTvls, tokensList } }, [chainsWithExtraTvlsByDay, tokensProtocols, forkedTokensData]) - const downloadCSV = () => { + const handleCSVDownload = () => { const headers = ['Name', 'Forked Protocols', 'TVL', 'Forked TVL / Original TVL %'] const csvData = tokensList.map((row) => { return { @@ -70,7 +72,7 @@ export default function Forks({ chartData, tokensProtocols, tokens, tokenLinks, } }) const csv = [headers].concat(csvData.map((row) => headers.map((header) => row[header]))).join('\n') - download('forks.csv', csv) + downloadCSV('forks.csv', csv) } return ( @@ -78,7 +80,7 @@ export default function Forks({ chartData, tokensProtocols, tokens, tokenLinks,
- + }> diff --git a/src/pages/hacks/total-value-lost.tsx b/src/pages/hacks/total-value-lost.tsx index afa6f111ea..2e41e2cb50 100644 --- a/src/pages/hacks/total-value-lost.tsx +++ b/src/pages/hacks/total-value-lost.tsx @@ -12,6 +12,7 @@ import { } from '~/containers/Hacks/queries' import Layout from '~/layout' import { download, formattedNum, tokenIconUrl } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { withPerformanceLogging } from '~/utils/perf' export const getStaticProps = withPerformanceLogging('protocols/total-value-lost-in-hacks', async () => { @@ -25,6 +26,7 @@ export const getStaticProps = withPerformanceLogging('protocols/total-value-lost const pageName = ['Protocols', 'ranked by', 'Total Value Lost in Hacks'] export default function TotalLostInHacks({ protocols }: IProtocolTotalValueLostInHacksByProtocol) { + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const [selectedColumns, setSelectedColumns] = React.useState>([ 'Name', 'Total Hacked', @@ -73,8 +75,9 @@ export default function TotalLostInHacks({ protocols }: IProtocolTotalValueLostI protocol.totalHacked - protocol.returnedFunds ]) } - download('total-value-lost-in-hacks.csv', rows.map((r) => r.join(',')).join('\n')) + downloadCSV('total-value-lost-in-hacks.csv', rows.map((r) => r.join(',')).join('\n')) }} + isLoading={isDownloadLoading} smol /> diff --git a/src/pages/protocol/bridge-aggregators/[...protocol].tsx b/src/pages/protocol/bridge-aggregators/[...protocol].tsx index 9decf3f46d..8d022043b2 100644 --- a/src/pages/protocol/bridge-aggregators/[...protocol].tsx +++ b/src/pages/protocol/bridge-aggregators/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' @@ -106,6 +107,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { return { 'Bridge Aggregator Volume': { @@ -172,6 +174,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol />
diff --git a/src/pages/protocol/dex-aggregators/[...protocol].tsx b/src/pages/protocol/dex-aggregators/[...protocol].tsx index 1c58dee927..b3eeef2868 100644 --- a/src/pages/protocol/dex-aggregators/[...protocol].tsx +++ b/src/pages/protocol/dex-aggregators/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' @@ -106,6 +107,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { return { 'DEX Aggregator Volume': { @@ -172,6 +174,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol />
diff --git a/src/pages/protocol/dexs/[...protocol].tsx b/src/pages/protocol/dexs/[...protocol].tsx index e610d3b01d..7679a912b0 100644 --- a/src/pages/protocol/dexs/[...protocol].tsx +++ b/src/pages/protocol/dexs/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' @@ -105,6 +106,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { return { 'DEX Volume': { @@ -171,6 +173,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol />
diff --git a/src/pages/protocol/fees/[...protocol].tsx b/src/pages/protocol/fees/[...protocol].tsx index 91a4215dda..32045552b5 100644 --- a/src/pages/protocol/fees/[...protocol].tsx +++ b/src/pages/protocol/fees/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { feesOptions } from '~/components/Filters/options' import { Select } from '~/components/Select' import { TokenLogo } from '~/components/TokenLogo' @@ -221,6 +222,7 @@ export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) const [charts, setCharts] = useState(props.defaultCharts) const [feesSettings] = useLocalStorageSettingsManager('fees') + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { const finalCharts = {} @@ -348,6 +350,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol /> diff --git a/src/pages/protocol/options/[...protocol].tsx b/src/pages/protocol/options/[...protocol].tsx index a9d34643b0..e220152031 100644 --- a/src/pages/protocol/options/[...protocol].tsx +++ b/src/pages/protocol/options/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { Select } from '~/components/Select' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' @@ -137,6 +138,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) const [charts, setCharts] = useState(props.defaultCharts) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { const finalCharts = {} @@ -239,6 +241,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol /> diff --git a/src/pages/protocol/perps-aggregators/[...protocol].tsx b/src/pages/protocol/perps-aggregators/[...protocol].tsx index 8923a192f1..f22fe868a8 100644 --- a/src/pages/protocol/perps-aggregators/[...protocol].tsx +++ b/src/pages/protocol/perps-aggregators/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' @@ -106,6 +107,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { return { 'Perp Aggregator Volume': { @@ -172,6 +174,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol /> diff --git a/src/pages/protocol/perps/[...protocol].tsx b/src/pages/protocol/perps/[...protocol].tsx index 6835bf8d72..775ed9afcb 100644 --- a/src/pages/protocol/perps/[...protocol].tsx +++ b/src/pages/protocol/perps/[...protocol].tsx @@ -1,7 +1,8 @@ import { lazy, Suspense, useMemo, useState } from 'react' import { maxAgeForNext } from '~/api' import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton' -import { downloadChart, formatBarChart } from '~/components/ECharts/utils' +import { formatBarChart } from '~/components/ECharts/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { TokenLogo } from '~/components/TokenLogo' import { Tooltip } from '~/components/Tooltip' import { oldBlue } from '~/constants/colors' @@ -106,6 +107,7 @@ const INTERVALS_LIST = ['daily', 'weekly', 'monthly', 'cumulative'] as const export default function Protocols(props) { const [groupBy, setGroupBy] = useState<(typeof INTERVALS_LIST)[number]>(props.defaultChartView) + const { downloadChart, isLoading } = useCSVDownload() const finalCharts = useMemo(() => { return { 'Perp Volume': { @@ -172,6 +174,7 @@ export default function Protocols(props) { console.error('Error generating CSV:', error) } }} + isLoading={isLoading} smol /> diff --git a/src/pages/token-usage.tsx b/src/pages/token-usage.tsx index 7b530ceb04..eac4a4336a 100644 --- a/src/pages/token-usage.tsx +++ b/src/pages/token-usage.tsx @@ -15,7 +15,8 @@ import { VirtualTable } from '~/components/Table/Table' import { TokenLogo } from '~/components/TokenLogo' import { PROTOCOLS_BY_TOKEN_API } from '~/constants' import Layout from '~/layout' -import { download, formattedNum, slug, tokenIconUrl } from '~/utils' +import { formattedNum, slug, tokenIconUrl } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { fetchJson } from '~/utils/async' import { withPerformanceLogging } from '~/utils/perf' @@ -43,6 +44,7 @@ const pageName = ['Token', 'usage in', 'Protocols'] export default function Tokens({ searchData }) { const router = useRouter() const { token, includecex } = router.query + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const tokenSymbol = token ? (typeof token === 'string' ? token : token[0]) : null const includeCentraliseExchanges = includecex @@ -65,7 +67,7 @@ export default function Tokens({ searchData }) { ) }, [protocols, includeCentraliseExchanges]) - const downloadCSV = () => { + const handleCSVDownload = () => { const data = filteredProtocols.map((p) => { return { Protocol: p.name, @@ -75,7 +77,7 @@ export default function Tokens({ searchData }) { }) const headers = ['Protocol', 'Category', 'Amount (USD)'] const csv = [headers.join(',')].concat(data.map((row) => headers.map((header) => row[header]).join(','))).join('\n') - download(`protocols-by-token-${tokenSymbol}.csv`, csv) + downloadCSV(`protocols-by-token-${tokenSymbol}.csv`, csv) } return ( @@ -112,7 +114,7 @@ export default function Tokens({ searchData }) { ) } /> - + diff --git a/src/pages/top-protocols.tsx b/src/pages/top-protocols.tsx index d0e1662585..c22cd592b3 100644 --- a/src/pages/top-protocols.tsx +++ b/src/pages/top-protocols.tsx @@ -13,7 +13,8 @@ import { BasicLink } from '~/components/Link' import { VirtualTable } from '~/components/Table/Table' import { TokenLogo } from '~/components/TokenLogo' import Layout from '~/layout' -import { chainIconUrl, download, slug } from '~/utils' +import { chainIconUrl, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { withPerformanceLogging } from '~/utils/perf' import { descriptions } from './categories' @@ -66,6 +67,7 @@ const pageName = ['Top Protocols'] export default function Chains({ data, uniqueCategories }) { const [sorting, setSorting] = React.useState([]) + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() const columnHelper = createColumnHelper() @@ -125,7 +127,7 @@ export default function Chains({ data, uniqueCategories }) { getSortedRowModel: getSortedRowModel() }) - const downloadCSV = React.useCallback(() => { + const handleCSVDownload = React.useCallback(() => { const headers = ['Chain', ...uniqueCategories] const csvData = data.map((row) => { return { @@ -137,14 +139,14 @@ export default function Chains({ data, uniqueCategories }) { const csv = [headers, ...csvData.map((row) => headers.map((header) => row[header]))] .map((row) => row.join(',')) .join('\n') - download('top-protocols.csv', csv) - }, [data, uniqueCategories]) + downloadCSV('top-protocols.csv', csv) + }, [data, uniqueCategories, downloadCSV]) return (

Protocols with highest TVL by chain on each category

- +
diff --git a/src/pages/yields/pool/[pool].tsx b/src/pages/yields/pool/[pool].tsx index d92b9a8030..d8b89e0027 100644 --- a/src/pages/yields/pool/[pool].tsx +++ b/src/pages/yields/pool/[pool].tsx @@ -17,7 +17,8 @@ import { useYieldPoolData } from '~/containers/Yields/queries/client' import Layout from '~/layout' -import { download, formattedNum, getColorFromNumber, slug } from '~/utils' +import { formattedNum, getColorFromNumber, slug } from '~/utils' +import { useCSVDownload } from '~/hooks/useCSVDownload' import { fetchApi } from '~/utils/async' const BarChart = lazy(() => import('~/components/ECharts/BarChart')) as React.FC @@ -87,6 +88,7 @@ const PageView = (props) => { const { data: chartBorrow, isLoading: fetchingChartDataBorrow } = useYieldChartLendBorrow(query.pool) const { data: config, isLoading: fetchingConfigData } = useYieldConfigData(poolData.project ?? '') + const { downloadCSV, isLoading: isDownloadLoading } = useCSVDownload() // prepare csv data const downloadCsv = () => { @@ -96,7 +98,7 @@ const PageView = (props) => { rows.push([item.apy, item.apyBase, item.apyReward, item.tvlUsd, item.timestamp]) }) - download(`${query.pool}.csv`, rows.map((r) => r.join(',')).join('\n')) + downloadCSV(`${query.pool}.csv`, rows.map((r) => r.join(',')).join('\n')) } const apy = poolData.apy?.toFixed(2) ?? 0 @@ -303,7 +305,7 @@ const PageView = (props) => {

- +
diff --git a/src/utils/index.js b/src/utils/index.js index f10eb16e35..df159dcae9 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -489,9 +489,18 @@ export function download(filename, text) { } export function downloadCSV(filename, csvData, options = {}) { + const { + mimeType = 'text/csv;charset=utf-8;', + addTimestamp = false, + onLoadingStart, + onLoadingEnd, + onError + } = options + + if (onLoadingStart) onLoadingStart() + try { - const { mimeType = 'text/csv;charset=utf-8;', addTimestamp = false } = options - + let csvContent if (Array.isArray(csvData)) { @@ -532,19 +541,26 @@ export function downloadCSV(filename, csvData, options = {}) { document.body.removeChild(link) window.URL.revokeObjectURL(downloadUrl) + + if (onLoadingEnd) onLoadingEnd() } catch (error) { - console.error('CSV download error:', error) + if (onError) onError(error) + else if (onLoadingEnd) onLoadingEnd() + download(filename, String(csvData)) } } export function downloadDatasetCSV({ data, - columns, + columns = null, columnHeaders = {}, - filename, - filenameSuffix, - addTimestamp = true + filename = null, + filenameSuffix = null, + addTimestamp = true, + onLoadingStart = null, + onLoadingEnd = null, + onError = null }) { try { if (!data || !Array.isArray(data) || data.length === 0) { @@ -572,10 +588,16 @@ export function downloadDatasetCSV({ } finalFilename += '.csv' - downloadCSV(finalFilename, csvData, { addTimestamp }) + downloadCSV(finalFilename, csvData, { + addTimestamp, + onLoadingStart, + onLoadingEnd, + onError + }) } catch (error) { - console.error('Dataset CSV download error:', error) + if (onError) onError(error) + else if (onLoadingEnd) onLoadingEnd() } }