import { useRouter } from "next/router"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import React, { useMemo } from "react"; import moment from "moment"; import { config } from "@/Config"; import { Loader2 } from "lucide-react"; import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory"; import useURLParams from "@/hooks/common/useURLParams"; import useAccountDetails from "@/hooks/api/accountPage/useAccountDetails"; import { convertBalanceHistoryResultsToTableOperations } from "@/lib/utils"; import { getHiveAvatarUrl } from "@/utils/HiveBlogUtils"; import BalanceHistoryTable from "@/components/balanceHistory/BalanceHistoryTable"; import BalanceHistorySearch from "@/components/home/searches/BalanceHistorySearch"; import { Card, CardHeader } from "@/components/ui/card"; import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart"; import ErrorPage from "../ErrorPage"; // Memoizing the BalanceHistoryChart component to avoid unnecessary re-renders const MemoizedBalanceHistoryChart = React.memo(BalanceHistoryChart); interface Operation { timestamp: number; // Timestamp in seconds balance: number; // Balance associated with the operation } const prepareData = (operations: Operation[]) => { if (!operations || operations.length === 0) return []; const dailyData = new Map<string, { balance: number; balance_change: number }>(); operations.forEach((operation: any) => { let date; if (typeof operation.timestamp === 'string') { date = new Date(operation.timestamp); } else if (typeof operation.timestamp === 'number') { date = new Date(operation.timestamp * 1000); } else { return; } if (!isNaN(date.getTime())) { const dateString = date.toISOString().split('T')[0]; let balance_change = parseInt(operation.balance_change, 10); let balance = parseInt(operation.balance, 10); if (dailyData.has(dateString)) { dailyData.get(dateString)!.balance_change += balance_change; dailyData.get(dateString)!.balance = balance; } else { dailyData.set(dateString, { balance, balance_change }); } } }); const preparedData = Array.from(dailyData.entries()).map(([date, data]) => ({ timestamp: date, balance: data.balance, balance_change: data.balance_change, })); return preparedData; }; export default function BalanceHistory() { const router = useRouter(); const accountNameFromRoute = (router.query.accountName as string)?.slice(1); const { accountDetails, isAccountDetailsLoading, isAccountDetailsError, notFound, } = useAccountDetails(accountNameFromRoute, false); interface BalanceHistorySearchParams { accountName?: string; coinType: string; fromBlock: Date | number | undefined; toBlock: Date | number | undefined; fromDate: undefined; toDate: undefined; lastBlocks: number | undefined; lastTime: number | undefined; timeUnit: string | undefined; rangeSelectKey: string | undefined; page: number; filters: boolean[]; } const defaultSearchParams: BalanceHistorySearchParams = { accountName: accountNameFromRoute, coinType: "HIVE", fromBlock: undefined, toBlock: undefined, fromDate: undefined, toDate: undefined, lastBlocks: undefined, lastTime: undefined, timeUnit: "days", rangeSelectKey: "none", page: 1, filters: [], }; const { paramsState } = useURLParams(defaultSearchParams, ["accountName"]); const { filters: filtersParam, fromBlock: fromBlockParam, toBlock: toBlockParam, fromDate: fromDateParam, toDate: toDateParam, lastBlocks: lastBlocksParam, timeUnit: timeUnitParam, lastTime: lastTimeParam, rangeSelectKey, page, } = paramsState; const defaultFromDate = React.useMemo(() => moment().subtract(1, "month").toDate(), []); let effectiveFromBlock = paramsState.fromBlock || fromDateParam || defaultFromDate; let effectiveToBlock = paramsState.toBlock || toDateParam; if (rangeSelectKey === "lastBlocks" && typeof effectiveFromBlock === "number" && paramsState.lastBlocks) { effectiveToBlock = effectiveFromBlock + paramsState.lastBlocks; } const { accountBalanceHistory, isAccountBalanceHistoryLoading, isAccountBalanceHistoryError, } = useBalanceHistory( accountNameFromRoute, paramsState.coinType, paramsState.page, config.standardPaginationSize, "desc", effectiveFromBlock, effectiveToBlock ); // Update chartData to return loading, error, and data const { accountBalanceHistory: chartData, isAccountBalanceHistoryLoading: isChartDataLoading, isAccountBalanceHistoryError: isChartDataError, } = useBalanceHistory( accountNameFromRoute, paramsState.coinType, undefined, 5000, // Default size for chart data "desc", effectiveFromBlock, effectiveToBlock ); // Use useMemo to memoize the prepared data so it only recalculates when chartData changes const preparedData = useMemo(() => { return chartData ? prepareData(chartData.operations_result?.slice().reverse()) : []; }, [chartData]); // This will only recompute when chartData changes let message = ""; if (effectiveFromBlock === defaultFromDate && !fromBlockParam && !toBlockParam) { message = "Showing Results for the last month."; } else { message = "Showing Results with applied filters."; } // get the accountName const routeAccountName = Array.isArray(router.query.accountName) ? router.query.accountName[0] // If it's an array, get the first element : router.query.accountName; // Otherwise, treat it as a string directly if(routeAccountName && !routeAccountName.startsWith("@") || !accountNameFromRoute) { return <ErrorPage />; } return ( <> <Head> <title>@{accountNameFromRoute} - Hive Explorer</title> </Head> {isAccountDetailsLoading ? ( <div className="flex justify-center text-center items-center"> <Loader2 className="animate-spin mt-1 text-black h-12 w-12 ml-3" /> </div> ) : notFound ? ( <div>Account not found</div> ) : ( accountNameFromRoute && ( <div className="w-[95%]"> <Card data-testid="account-details"> <CardHeader> <div className="flex flex-wrap items-center justify-between gap-4 bg-theme dark:bg-theme"> <div className="flex items-center gap-4"> <Image className="rounded-full border-2 border-explorer-orange" src={getHiveAvatarUrl(accountNameFromRoute)} alt="avatar" width={60} height={60} data-testid="user-avatar" /> <div> <h2 className="text-lg font-semibold text-gray-800 dark:text-white" data-testid="account-name"> <Link className="text-link" href={`/@${accountNameFromRoute}`}> {accountNameFromRoute} </Link> / <span className="text-text">Balance History</span> </h2> </div> </div> </div> </CardHeader> </Card> <BalanceHistorySearch /> {!isAccountBalanceHistoryLoading && !accountBalanceHistory?.total_operations ? ( <div className="w-full my-4 text-center"> No operations were found. </div> ) : isAccountBalanceHistoryLoading ? ( <div className="flex justify-center text-center items-center"> <Loader2 className="animate-spin mt-1 h-12 w-12 ml-3" /> </div> ) : ( <> <Card data-testid="account-details"> {message && ( <div className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg mb-4 text-center text-sm text-gray-500"> {message}<br /> Results are limited to 5000 records and grouped by day.<br /> </div> )} {isChartDataLoading ? ( <div className="flex justify-center text-center items-center"> <Loader2 className="animate-spin mt-1 h-16 w-10 ml-10 dark:text-white" /> </div> ) : !isChartDataError ? ( <MemoizedBalanceHistoryChart hiveBalanceHistoryData={(!paramsState.coinType || paramsState.coinType === "HIVE") ? preparedData : undefined} vestsBalanceHistoryData={paramsState.coinType === "VESTS" ? preparedData : undefined} hbdBalanceHistoryData={paramsState.coinType === "HBD" ? preparedData : undefined} quickView={false} className="h-[450px] mb-10 mr-0 pr-1 pb-6" /> ) : ( <div>Error loading chart data</div> )} </Card> <BalanceHistoryTable operations={convertBalanceHistoryResultsToTableOperations(accountBalanceHistory)} total_operations={accountBalanceHistory.total_operations} total_pages={accountBalanceHistory.total_pages} current_page={paramsState.page} /> </> )} </div> ) )} </> ); }