Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 215-change-content-of-operation-card
  • 216-create-table-for-operations
  • 249-404-not-found-after-choosing-an-account-from-search-bar-with-enter
  • 366-allow-env-level-configuration-of-default-site-colors-for-both-light-and-dark-modes
  • 504-incorrect-weight_threshold-indications-for-hive-fund-steem-dao-accounts
  • adubel/fix-skiped-tests
  • adubel/test-e2e-fix-locators
  • adubel/test-improvements
  • bw_format_msg_tweaks
  • copy_of_add_charts_to_balance_history
  • delrifai/#356_calculate_hive_power_for_voters_and_votes_history
  • delrifai/#372_fix_pagination_issues
  • delrifai/#384_add_scroll_left_right_to_table
  • delrifai/#394_add_charts_to_balance_history
  • delrifai/#433_add_noResult_component
  • delrifai/#505_fix_no_filter_balance_history
  • delrifai/all_blocks_fixes_1
  • develop
  • jlachor/healthchecker
  • jlachor/local-healthchecker
  • jlachor/virtual-in-raw
  • kbotor/80-configure-ssl-for-staging-and-test-deployments
  • kbotor/cors-fix
  • lbudginas/#240_transaction_link_scroll
  • lbudginas/#276_pretty_json_operation_view
  • lbudginas/#287_witness_table_update
  • lbudginas/#304_convert_vests_to_hp_account_page
  • lbudginas/#374_calendar_dates_selection_bug
  • lbudginas/#385_single_post_page
  • lbudginas/#390_auto_suggest_bg_bug
  • lbudginas/#396_account_operations_table_bug
  • lbudginas/#439_account_page_request_optimisation
  • lbudginas/#443_post_page_links_bug
  • lbudginas/#447_post_dollar_value_not_visible
  • lbudginas/#523_tooltips_on_mobile
  • lbudginas/account_page_votes_history
  • lbudginas/aggregate_operations_block_page
  • lbudginas/clear_storage_on_invalid_api_address
  • lbudginas/refactor_url_param_build
  • lbudginas/replace-date-time-pickers-shadcn-ui
  • master
  • mcfarhat-283-add_live_profile_tracking
  • mcfarhat-283-live_profile_tracking_updates
  • mcfarhat-285-add_vesting_delegations
  • mcfarhat-295-witness_page_feed_age
  • mcfarhat-301-hide_disabled_witnesses
  • mcfarhat-302-fix_crash_user_profile
  • mcfarhat-316-fix_issue_with_delegated_values_wallet
  • mcfarhat-332-fix_update_date_picker
  • mcfarhat-fix_delegated_values_display
  • test-tag
51 results

Target

Select target project
  • hive/block_explorer_ui
1 result
Select Git revision
  • 215-change-content-of-operation-card
  • 216-create-table-for-operations
  • 249-404-not-found-after-choosing-an-account-from-search-bar-with-enter
  • 366-allow-env-level-configuration-of-default-site-colors-for-both-light-and-dark-modes
  • 504-incorrect-weight_threshold-indications-for-hive-fund-steem-dao-accounts
  • adubel/fix-skiped-tests
  • adubel/test-e2e-fix-locators
  • adubel/test-improvements
  • bw_format_msg_tweaks
  • copy_of_add_charts_to_balance_history
  • delrifai/#356_calculate_hive_power_for_voters_and_votes_history
  • delrifai/#372_fix_pagination_issues
  • delrifai/#384_add_scroll_left_right_to_table
  • delrifai/#394_add_charts_to_balance_history
  • delrifai/#433_add_noResult_component
  • delrifai/#505_fix_no_filter_balance_history
  • delrifai/all_blocks_fixes_1
  • develop
  • jlachor/healthchecker
  • jlachor/local-healthchecker
  • jlachor/virtual-in-raw
  • kbotor/80-configure-ssl-for-staging-and-test-deployments
  • kbotor/cors-fix
  • lbudginas/#240_transaction_link_scroll
  • lbudginas/#276_pretty_json_operation_view
  • lbudginas/#287_witness_table_update
  • lbudginas/#304_convert_vests_to_hp_account_page
  • lbudginas/#374_calendar_dates_selection_bug
  • lbudginas/#385_single_post_page
  • lbudginas/#390_auto_suggest_bg_bug
  • lbudginas/#396_account_operations_table_bug
  • lbudginas/#439_account_page_request_optimisation
  • lbudginas/#443_post_page_links_bug
  • lbudginas/#447_post_dollar_value_not_visible
  • lbudginas/#523_tooltips_on_mobile
  • lbudginas/account_page_votes_history
  • lbudginas/aggregate_operations_block_page
  • lbudginas/clear_storage_on_invalid_api_address
  • lbudginas/refactor_url_param_build
  • lbudginas/replace-date-time-pickers-shadcn-ui
  • master
  • mcfarhat-283-add_live_profile_tracking
  • mcfarhat-283-live_profile_tracking_updates
  • mcfarhat-285-add_vesting_delegations
  • mcfarhat-295-witness_page_feed_age
  • mcfarhat-301-hide_disabled_witnesses
  • mcfarhat-302-fix_crash_user_profile
  • mcfarhat-316-fix_issue_with_delegated_values_wallet
  • mcfarhat-332-fix_update_date_picker
  • mcfarhat-fix_delegated_values_display
  • test-tag
51 results
Show changes
Commits on Source (6)
......@@ -171,41 +171,8 @@ const AccountBalanceCard: React.FC<AccountBalanceCardProps> = ({
className="flex justify-between items-center p-2 hover:bg-rowHover cursor-pointer px-4"
>
<div className="text-lg">{header}</div>
<div className="flex">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/balanceHistory/@${userDetails.name}`}
data-testid="balance-history-link"
className="text-link text-sm mr-1 "
onClick={(e) => e.stopPropagation()}
>
<FontAwesomeIcon
icon={faHistory}
size="sm"
className="mr-1"
/>
<span>Balance History</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
sideOffset={5}
alignOffset={10}
className="border-0"
>
<div className="bg-theme text-text p-2 text-sm">
<p>Click Here for Balance History</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span> {isBalancesHidden ? <ArrowDown /> : <ArrowUp />}</span>
</div>
</div>
</CardHeader>
<CardContent
hidden={isBalancesHidden}
......
import React, { useState, useMemo } from "react";
import { ArrowDown, ArrowUp } from "lucide-react";
import { Card, CardContent, CardHeader } from "../ui/card";
import Explorer from "@/types/Explorer";
import Link from "next/link";
import {
Tooltip,
TooltipProvider,
TooltipTrigger,
TooltipContent,
} from "@radix-ui/react-tooltip";
import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory";
import BalanceHistoryChart from "../balanceHistory/BalanceHistoryChart";
import moment from "moment";
import { useRouter } from "next/router";
import { Loader2 } from "lucide-react";
// Define the type for balance operation data
type AccountBalanceHistoryCardProps = {
header: string;
userDetails: Explorer.FormattedAccountDetails;
};
const AccountBalanceHistoryCard: React.FC<AccountBalanceHistoryCardProps> = ({
header,
userDetails,
}) => {
const [isBalancesHidden, setIsBalancesHidden] = useState(false);
const defaultFromDate = useMemo(
() => moment().subtract(1, "month").toDate(),
[]
);
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
const {
accountBalanceHistory: hiveBalanceHistory,
isAccountBalanceHistoryLoading: hiveBalanceHistoryLoading,
isAccountBalanceHistoryError: hiveBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"HIVE",
undefined,
undefined,
"desc",
defaultFromDate
);
const {
accountBalanceHistory: vestsBalanceHistory,
isAccountBalanceHistoryLoading: vestsBalanceHistoryLoading,
isAccountBalanceHistoryError: vestsBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"VESTS",
undefined,
undefined,
"desc",
defaultFromDate
);
const {
accountBalanceHistory: hbdBalanceHistory,
isAccountBalanceHistoryLoading: hbdBalanceHistoryLoading,
isAccountBalanceHistoryError: hbdBalanceHistoryError,
} = useBalanceHistory(
accountNameFromRoute,
"HBD",
undefined,
undefined,
"desc",
defaultFromDate
);
const handleBalancesVisibility = () => {
setIsBalancesHidden(!isBalancesHidden);
};
const isLoading =
hiveBalanceHistoryLoading ||
vestsBalanceHistoryLoading ||
hbdBalanceHistoryLoading;
const hasData =
hiveBalanceHistory?.operations_result?.length > 0 ||
vestsBalanceHistory?.operations_result?.length > 0 ||
hbdBalanceHistory?.operations_result?.length > 0;
const hasError =
hiveBalanceHistoryError ||
vestsBalanceHistoryError ||
hbdBalanceHistoryError;
// Reverse the vestsBalanceHistory data with useMemo
const reversedVestsBalanceHistory = useMemo(() => {
return Array.isArray(vestsBalanceHistory?.operations_result)
? [...vestsBalanceHistory.operations_result].reverse()
: [];
}, [vestsBalanceHistory?.operations_result]);
// Reverse the hiveBalanceHistory data with useMemo
const reversedHiveBalanceHistory = useMemo(() => {
return Array.isArray(hiveBalanceHistory?.operations_result)
? [...hiveBalanceHistory.operations_result].reverse()
: [];
}, [hiveBalanceHistory?.operations_result]);
// Reverse the hbdBalanceHistory data with useMemo
const reversedHbdBalanceHistory = useMemo(() => {
return Array.isArray(hbdBalanceHistory?.operations_result)
? [...hbdBalanceHistory.operations_result].reverse()
: [];
}, [hbdBalanceHistory?.operations_result]);
return (
<Card data-testid="properties-dropdown" className="overflow-hidden pb-0">
<CardHeader className="p-0">
<div
onClick={handleBalancesVisibility}
className="flex justify-between items-center p-2 hover:bg-rowHover cursor-pointer px-4"
>
<div className="text-lg">{header}</div>
<div className="flex">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/balanceHistory/@${userDetails.name}`}
data-testid="balance-history-link"
className="text-link text-sm underline"
onClick={(e) => e.stopPropagation()}
>
<span>Details</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="left"
align="start"
sideOffset={5}
alignOffset={10}
className="border-0"
>
<div className="bg-theme text-text p-2 text-sm">
<p>Click Here for Balance History</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<span>{isBalancesHidden ? <ArrowDown /> : <ArrowUp />}</span>
</div>
</div>
</CardHeader>
<CardContent
hidden={isBalancesHidden}
data-testid="balance-history-content"
>
{isLoading && (
<div className="flex justify-center items-center">
<Loader2 className="animate-spin mt-1 h-16 w-10 ml-10 dark:text-white" />
</div>
)}
{!isLoading && hasError && (
<p className="text-sm text-center">
Error loading balance information.
</p>
)}
{!isLoading && !hasData && (
<p className="text-sm text-center">No balance information found.</p>
)}
{!isLoading && hasData && (
<BalanceHistoryChart
hiveBalanceHistoryData={reversedHiveBalanceHistory}
vestsBalanceHistoryData={reversedVestsBalanceHistory}
hbdBalanceHistoryData={reversedHbdBalanceHistory}
quickView={true}
className="h-[340px]"
/>
)}
</CardContent>
</Card>
);
};
export default AccountBalanceHistoryCard;
......@@ -15,7 +15,7 @@ import AccountVestingDelegationsCard from "./AccountVestingDelegationsCard";
import AccountRcDelegationsCard from "./AccountRcDelegationsCard";
import AccountBalanceCard from "./AccountBalanceCard";
import Explorer from "@/types/Explorer";
import AccountBalanceHistoryCard from "./AccountBalanceHistoryCard";
interface AccountDetailsSectionProps {
accountName: string;
refetchAccountOperations: QueryObserverResult<Hive.AccountOperationsResponse>["refetch"];
......@@ -66,6 +66,11 @@ const AccountDetailsSection: React.FC<AccountDetailsSectionProps> = ({
header="Wallet"
userDetails={accountDetails}
/>
<AccountBalanceHistoryCard
header="Balance History"
userDetails={accountDetails}
/>
<AccountDetailsCard
header="Properties"
userDetails={accountDetails}
......
import React, { useState, useEffect } from "react";
import {
Line,
LineChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
Brush,
} from "recharts";
import { formatNumber } from "@/lib/utils";
import { cn } from "@/lib/utils";
import moment from "moment";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons";
interface BalanceHistoryChartProps {
hiveBalanceHistoryData?: {
timestamp: string;
balance_change: number;
balance: number;
}[];
vestsBalanceHistoryData?: {
timestamp: string;
balance_change: number;
balance: number;
}[];
hbdBalanceHistoryData?: {
timestamp: string;
balance_change: number;
balance: number;
}[];
className?: string;
quickView?: boolean;
}
const BalanceHistoryChart: React.FC<BalanceHistoryChartProps> = ({
hiveBalanceHistoryData,
vestsBalanceHistoryData,
hbdBalanceHistoryData,
className = "",
quickView = false,
}) => {
const [selectedCoinType, setSelectedCoinType] = useState<string>("HIVE");
const [isMobile, setIsMobile] = useState<boolean>(window.innerWidth < 480);
const [hiddenDataKeys, setHiddenDataKeys] = useState<string[]>([]);
// State to store available coins
const [availableCoins, setAvailableCoins] = useState<string[]>([]);
useEffect(() => {
const newAvailableCoins: string[] = [];
if (hiveBalanceHistoryData && hiveBalanceHistoryData.length > 0)
newAvailableCoins.push("HIVE");
if (vestsBalanceHistoryData && vestsBalanceHistoryData.length > 0)
newAvailableCoins.push("VESTS");
if (hbdBalanceHistoryData && hbdBalanceHistoryData.length > 0)
newAvailableCoins.push("HBD");
setAvailableCoins(newAvailableCoins);
}, [hiveBalanceHistoryData, vestsBalanceHistoryData, hbdBalanceHistoryData]);
useEffect(() => {
if (availableCoins.length === 1) {
setSelectedCoinType(availableCoins[0]);
} else if (
availableCoins.length > 1 &&
!availableCoins.includes(selectedCoinType)
) {
setSelectedCoinType(availableCoins[0]);
}
}, [availableCoins, selectedCoinType]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 480);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const colorMap: Record<string, string> = {
HIVE: "#8884d8",
VESTS: "#82ca9d",
HBD: "#ff7300",
};
const dataMap: Record<
string,
{ timestamp: string; balance_change: number; balance: number }[]
> = {
HIVE: hiveBalanceHistoryData || [],
VESTS: vestsBalanceHistoryData || [],
HBD: hbdBalanceHistoryData || [],
};
const handleCoinTypeChange = (coinType: string) => {
setSelectedCoinType(coinType);
};
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: any[];
label?: string;
}) => {
if (quickView || !active || !payload || payload.length === 0) return null;
const actualBalance =
dataMap[selectedCoinType]?.find((item) => item.timestamp === label)
?.balance ?? 0;
const balanceChange = payload[0]?.payload.balance_change ?? 0;
const isPositiveChange = balanceChange > 0;
return (
<div className="bg-theme dark:bg-theme p-2 rounded border border-explorer-light-gray">
<p className="font-bold">{`Date: ${label}`}</p>
{payload.map((pld, index) => (
<div key={index} style={{ color: pld.stroke }}>
<div>
{isPositiveChange ? (
<FontAwesomeIcon
icon={faArrowUp}
size="sm"
className="bg-green-400 p-[1.2px]"
/>
) : (
<FontAwesomeIcon
icon={faArrowDown}
size="sm"
color="red"
className="bg-red-400 p-[1.2px]"
/>
)}
{` ${formatNumber(
balanceChange,
selectedCoinType === "VESTS",
false
)}`}
</div>
<div>{`Balance: ${formatNumber(
actualBalance,
selectedCoinType === "VESTS",
false
)}`}</div>
</div>
))}
</div>
);
};
const renderCoinButtons = () => {
return availableCoins.map((coinType) => (
<button
key={coinType}
onClick={() => handleCoinTypeChange(coinType)}
className={cn(
"px-2 py-1 text-sm rounded m-[1px]",
selectedCoinType === coinType
? "bg-blue-500 text-white"
: "bg-gray-200 text-black hover:bg-gray-300 dark:bg-gray-600 dark:text-white hover:dark:bg-gray-500"
)}
>
{coinType}
</button>
));
};
const getMinMax = (data: { balance: number }[]) => {
const balance = data.map((item) => item.balance);
const minValue = Math.min(...balance);
const maxValue = Math.max(...balance);
return [minValue, maxValue];
};
const [minValue, maxValue] = getMinMax(dataMap[selectedCoinType]);
return (
<div className={cn("w-full", className)}>
{availableCoins.length > 1 && (
<div className="flex justify-end mb-4">{renderCoinButtons()}</div>
)}
<ResponsiveContainer width="100%" height="100%" className='mb-5'>
<LineChart
data={dataMap[selectedCoinType] || []}
margin={{ top: 20, right: 30, left: 20, bottom: isMobile ? 100 : 60 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tickCount={quickView ? 5 : 14}
tickFormatter={(value) => moment(value).format("MMM D")}
style={{ fontSize: "10px" }}
angle={isMobile ? -90 : 0}
dx={isMobile ? -8 : 0}
dy={isMobile ? 20 : 10}
/>
<YAxis
domain={[minValue, maxValue]}
tickFormatter={(tick) => {
if (selectedCoinType === "VESTS") {
const valueInK = tick / 1000;
let formattedValue = formatNumber(valueInK, true, false);
formattedValue = formattedValue.split(".")[0];
return `${formattedValue} K`;
}
return formatNumber(tick, selectedCoinType === "VESTS", false);
}}
style={{ fontSize: "10px" }}
tickCount={6}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="balance"
stroke={colorMap[selectedCoinType]}
activeDot={{ r: 6 }}
name={selectedCoinType}
dot={false}
hide={hiddenDataKeys.includes("balance")}
/>
{!quickView && (
<Brush
dataKey="timestamp"
height={30}
stroke="var(--color-switch-off)"
fill="var(--color-background)"
travellerWidth={10}
tickFormatter={(value) => moment(value).format("MMM D")}
y={380}
className="text-xs"
/>
)}
<Legend
onClick={(event) => {
const { dataKey } = event;
const isHidden = hiddenDataKeys.includes(dataKey);
if (isHidden) {
setHiddenDataKeys(
hiddenDataKeys.filter((key) => key !== dataKey)
);
} else {
setHiddenDataKeys([...hiddenDataKeys, dataKey]);
}
}}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default BalanceHistoryChart;
import { useState } from "react";
import { useState, useRef, useEffect } from "react";
import Hive from "@/types/Hive";
import {
Table,
......@@ -33,15 +33,14 @@ import { config } from "@/Config";
import { faChevronUp, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import useOperationsFormatter from "@/hooks/common/useOperationsFormatter";
import { convertOperationResultsToTableOperations } from "@/lib/utils";
interface OperationsTableProps {
interface BalanceHistoryTableProps {
operations: Explorer.BalanceHistoryForTable[];
total_operations: number;
total_pages: number;
current_page: number;
}
const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
const BalanceHistoryTable: React.FC<BalanceHistoryTableProps> = ({
operations,
total_operations,
total_pages,
......@@ -55,8 +54,28 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
const [expandedRow, setExpandedRow] = useState<number | null>(null);
const operationsTypes = useOperationsTypes().operationsTypes || [];
// Create refs to store row and detail div elements
const rowRefs = useRef<Map<number, HTMLTableRowElement>>(new Map());
const detailRefs = useRef<Map<number, HTMLDivElement>>(new Map());
// Track screen size for mobile responsiveness
const [isMobile, setIsMobile] = useState(false);
// Set screen size check for mobile view
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => {
window.removeEventListener("resize", checkMobile);
};
}, []);
const formatRawCoin = (coinValue: number) =>
formatNumber(coinValue, false, false);
router.query.coinType === 'VESTS' ? formatNumber(coinValue, true, false) : formatNumber(coinValue, false, false);
const getOperationColor = (op_type_id: number) => {
const operation = operationsTypes.find(
......@@ -109,6 +128,7 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
return <p>Loading operation details...</p>;
};
const getOneLineDescription = (operation: any) => {
const value = operation.op.value;
// Check if 'value' is a string or a valid React element
......@@ -136,6 +156,19 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
return null;
};
const handleRowClick = (operationId: number) => {
setExpandedRow((prev) => (prev === operationId ? null : operationId));
// Scroll the corresponding row and details into view with additional margin for visibility
const rowElement = rowRefs.current.get(operationId);
const detailElement = detailRefs.current.get(operationId);
if (rowElement && detailElement) {
// Ensure smooth scrolling for both row and details
rowElement.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
return (
<>
<CustomPagination
......@@ -177,7 +210,10 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
return (
<React.Fragment key={index}>
<TableRow key={operation.operationId}>
<TableRow
ref={(el) => el && rowRefs.current.set(operation.operationId, el)}
className={isExpanded ? "bg-rowOdd" : ""}
>
<TableCell data-testid="operation-type">
<div className="flex justify-stretch p-1 rounded">
<span
......@@ -194,11 +230,9 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
<TooltipTrigger asChild>
<div>
<TimeAgo
datetime={
new Date(
formatAndDelocalizeTime(operation.timestamp)
)
}
datetime={new Date(
formatAndDelocalizeTime(operation.timestamp)
)}
/>
</div>
</TooltipTrigger>
......@@ -227,11 +261,7 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
</TableCell>
<TableCell>
<button
onClick={() =>
setExpandedRow(
isExpanded ? null : operation.operationId
)
}
onClick={() => handleRowClick(operation.operationId)}
className="text-link"
>
<FontAwesomeIcon
......@@ -243,7 +273,9 @@ const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
</TableCell>
</TableRow>
{isExpanded && (
<TableRow>
<TableRow
ref={(el) => el && detailRefs.current.set(operation.operationId, el)}
>
<TableCell colSpan={7} className="p-4">
<div className="border rounded-2xl p-4">
<h3 className="text-lg font-bold">
......
......@@ -6,6 +6,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import useSearchRanges from "@/hooks/common/useSearchRanges";
import useURLParams from "@/hooks/common/useURLParams";
import OperationTypesDialog from "@/components/OperationTypesDialog";
import useAccountOperationTypes from "@/hooks/api/accountPage/useAccountOperationTypes";
interface AccountSearchParams {
accountName?: string | undefined;
......@@ -19,7 +21,7 @@ interface AccountSearchParams {
rangeSelectKey: string | undefined;
page: number | undefined;
filters: boolean[];
coinType?: string;
coinType?: string;
}
const defaultSearchParams: AccountSearchParams = {
......@@ -42,6 +44,30 @@ const BalanceHistorySearch = () => {
const COIN_TYPES = ["HIVE", "VESTS", "HBD"];
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
const { accountOperationTypes } =
useAccountOperationTypes(accountNameFromRoute);
const [selectedOperationTypes, setSelectedOperationTypes] = useState<
number[]
>([]);
const [singleOperationTypeId, setSingleOperationTypeId] = useState<
number | undefined
>(undefined);
const [fieldContent, setFieldContent] = useState<string>("");
const [selectedKeys, setSelectedKeys] = useState<string[] | undefined>(
undefined
);
const [selectedIndex, setSelectedIndex] = useState<string>("");
const changeSelectedOperationTypes = (operationTypesIds: number[]) => {
if (operationTypesIds.length === 1) {
setSingleOperationTypeId(operationTypesIds[0]);
} else {
setSingleOperationTypeId(undefined);
}
setSelectedKeys(undefined);
setFieldContent("");
setSelectedOperationTypes(operationTypesIds);
};
const { paramsState, setParams } = useURLParams(
{
......@@ -65,7 +91,6 @@ const BalanceHistorySearch = () => {
const [initialSearch, setInitialSearch] = useState<boolean>(false);
const [filters, setFilters] = useState<boolean[]>([]);
const searchRanges = useSearchRanges();
......@@ -129,8 +154,11 @@ const BalanceHistorySearch = () => {
const handleCoinTypeChange = (newCoinType: string) => {
setCoinType(newCoinType);
setParams({ ...paramsState, coinType: newCoinType });
setParams({
...paramsState,
coinType: newCoinType,
page: undefined, // Reset the page when the coin type changes
});
};
const handleFilterClear = () => {
......@@ -144,48 +172,66 @@ const BalanceHistorySearch = () => {
setFilters([]);
};
useEffect(() => {
useEffect(() => {
if (paramsState.coinType) {
setCoinType(paramsState.coinType);
}
}
if (paramsState && !initialSearch) {
handleSearch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paramsState]);
return (
<>
<p className="m-2 mb-6 mt-6">
Find balance history of given account by coin and range.
</p>
<Card className="mb-4">
<CardHeader>
<CardTitle>Filters</CardTitle>
<CardTitle className="">Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center mb-3">
<select
value={coinType}
onChange={(e) => handleCoinTypeChange(e.target.value)}
className="w-[180px] border border-gray-300 p-2 rounded bg-theme dark:bg-theme"
>
{COIN_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
<select
value={coinType}
onChange={(e) => handleCoinTypeChange(e.target.value)}
className="w-[180px] border border-gray-300 p-2 rounded bg-theme dark:bg-theme"
>
{COIN_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<SearchRanges rangesProps={searchRanges} />
<div className="flex items-center justify-between m-2">
<Button onClick={() => handleSearch(true)} data-testid="apply-filters">
{/* Operations Types commented for now
<div className="flex items-center mb-10 mt-2">
<OperationTypesDialog
operationTypes={accountOperationTypes}
selectedOperations={selectedOperationTypes}
setSelectedOperations={/*changeSelectedOperationTypes}
buttonClassName="bg-gray-500"
triggerTitle={/*getOperationButtonTitle(
selectedOperationTypes,
accountOperationTypes
)}
/>
</div> */}
<div>
<Button
onClick={() => handleSearch(true)}
data-testid="apply-filters"
>
<span>Apply filters</span>
</Button>
<Button onClick={() => handleFilterClear()} data-testid="clear-filters">
<Button
onClick={() => handleFilterClear()}
data-testid="clear-filters"
className="ml-2"
>
<span>Clear filters</span>
</Button>
</div>
......
......@@ -20,7 +20,7 @@ const useBalanceHistory = (
return await fetchingService.geAccounttBalanceHistory(
accountName,
coinType,
page ? page : 1,
page,
pageSize,
direction,
fromDate ? fromDate : undefined,
......
......@@ -2,7 +2,8 @@ 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";
......@@ -17,13 +18,65 @@ 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";
// 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);
// Fetch account details
const { accountDetails, isAccountDetailsLoading, isAccountDetailsError ,notFound } = useAccountDetails(accountNameFromRoute, false);
const {
accountDetails,
isAccountDetailsLoading,
isAccountDetailsError,
notFound,
} = useAccountDetails(accountNameFromRoute, false);
interface BalanceHistorySearchParams {
accountName?: string;
......@@ -70,14 +123,11 @@ export default function BalanceHistory() {
page,
} = paramsState;
let effectiveFromBlock = paramsState.fromBlock || fromDateParam;
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
) {
if (rangeSelectKey === "lastBlocks" && typeof effectiveFromBlock === "number" && paramsState.lastBlocks) {
effectiveToBlock = effectiveFromBlock + paramsState.lastBlocks;
}
......@@ -95,13 +145,39 @@ export default function BalanceHistory() {
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
"asc",
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) : [];
}, [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.";
}
return (
<>
<Head>
<title>@{accountNameFromRoute} - Hive Explorer</title>
</Head>
{/* Loading state for account details */}
{isAccountDetailsLoading ? (
<div className="flex justify-center text-center items-center">
<Loader2 className="animate-spin mt-1 text-black h-12 w-12 ml-3" />
......@@ -114,7 +190,6 @@ export default function BalanceHistory() {
<Card data-testid="account-details">
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-4 bg-theme dark:bg-theme">
{/* Avatar and Name */}
<div className="flex items-center gap-4">
<Image
className="rounded-full border-2 border-explorer-orange"
......@@ -125,15 +200,8 @@ export default function BalanceHistory() {
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}`}
>
{" "}
<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>
......@@ -144,10 +212,8 @@ export default function BalanceHistory() {
</CardHeader>
</Card>
{/* Filter Options (Always visible) */}
<BalanceHistorySearch />
{/* Show Error Message if No Balance History and No Loading State */}
{!isAccountBalanceHistoryLoading && !accountBalanceHistory?.total_operations ? (
<div className="w-full my-4 text-center">
No operations were found.
......@@ -157,15 +223,39 @@ export default function BalanceHistory() {
<Loader2 className="animate-spin mt-1 h-12 w-12 ml-3" />
</div>
) : (
// Show the table when balance history exists
<BalanceHistoryTable
operations={convertBalanceHistoryResultsToTableOperations(
accountBalanceHistory
)}
total_operations={accountBalanceHistory.total_operations}
total_pages={accountBalanceHistory.total_pages}
current_page={paramsState.page}
/>
<>
<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>
)
......