Skip to content
Snippets Groups Projects
Commit 34f3eb93 authored by Dima Rifai's avatar Dima Rifai
Browse files

Issue 328 - New Files for Balance History

parent c17c04e5
No related branches found
No related tags found
1 merge request!495Delrifai/#328 create balance history page
Pipeline #111289 failed
import Hive from "@/types/Hive";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table";
import Link from "next/link";
import React from "react";
import { cn } from "@/lib/utils";
import Explorer from "@/types/Explorer";
import { getOperationTypeForDisplay } from "@/utils/UI";
import { categorizedOperationTypes } from "@/utils/CategorizedOperationTypes";
import { colorByOperationCategory } from "../OperationTypesDialog";
import { useUserSettingsContext } from "@/contexts/UserSettingsContext";
import TimeAgo from "timeago-react";
import { formatAndDelocalizeTime } from "@/utils/TimeUtils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { useRouter } from "next/router";
import useOperationsTypes from "@/hooks/api/common/useOperationsTypes";
import { formatNumber } from "@/lib/utils";
import CustomPagination from "../CustomPagination";
import { config } from "@/Config";
interface OperationsTableProps {
operations: Explorer.BalanceHistoryForTable[];
total_operations: number;
total_pages: number;
current_page: number;
}
const BalanceHistoryTable: React.FC<OperationsTableProps> = ({
operations,
total_operations,
total_pages,
current_page,
}) => {
const router = useRouter();
const operationsTypes = useOperationsTypes().operationsTypes || [];
const formatRawCoin = (coinValue: number) =>
formatNumber(coinValue, false, false);
const getOperationColor = (op_type_id: number) => {
const operation = operationsTypes.find(
(op) => op.op_type_id === op_type_id
);
if (!operation) return "";
const category = categorizedOperationTypes.find((cat) =>
cat.types.includes(operation.operation_name)
);
return category ? colorByOperationCategory[category.name] : "";
};
const getOperationTypeForDisplayById = (op_type_id: number) =>
getOperationTypeForDisplay(
operationsTypes.find((op) => op.op_type_id === op_type_id)
?.operation_name || ""
);
const updateUrl = (page: number) => {
router.push({
pathname: router.pathname,
query: { ...router.query, page: page.toString() },
});
};
return (
<>
<CustomPagination
currentPage={current_page? current_page: 1}
onPageChange={updateUrl}
pageSize={config.standardPaginationSize}
totalCount={total_operations}
className="text-black dark:text-white"
isMirrored={false}
/>
{total_operations === 0 ? (
<div className="flex justify-center w-full">
No results matching given criteria
</div>
) : (
<Table className={cn("rounded-[6px] overflow-hidden max-w-[100%] text-xs mt-3")}>
<TableHeader>
<TableRow>
<TableHead>Operation Type</TableHead>
<TableHead>Date</TableHead>
<TableHead>Block Number</TableHead>
<TableHead>Balance</TableHead>
<TableHead>Balance Change</TableHead>
<TableHead>New Balance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{operations.map((operation) => {
const operationBgColor = getOperationColor(operation.opTypeId);
const coinName = router.query.coinType?router.query.coinType: 'HIVE'; //defaults to HIVE
return (
<TableRow key={operation.operationId}>
<TableCell data-testid="operation-type">
<div className="flex justify-stretch p-1 rounded">
<span className={`rounded w-4 mr-2 ${operationBgColor}`}></span>
<span>{getOperationTypeForDisplayById(operation.opTypeId)}</span>
</div>
</TableCell>
<TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<TimeAgo
datetime={new Date(
formatAndDelocalizeTime(operation.timestamp)
)}
/>
</div>
</TooltipTrigger>
<TooltipContent className="bg-theme text-text">
{formatAndDelocalizeTime(operation.timestamp)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell data-testid="block-number">
<Link className="text-link" href={`/block/${operation.blockNumber}`}>
{operation.blockNumber?.toLocaleString()}
</Link>
</TableCell>
<TableCell data-testid="operation-prev-balance">
{formatRawCoin(operation.prev_balance)} {coinName}
</TableCell>
<TableCell data-testid="operation-balance-change">
{formatRawCoin(operation.balanceChange)} {coinName}
</TableCell>
<TableCell>
{formatRawCoin(operation.balance)} {coinName}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</>
);
};
export default BalanceHistoryTable;
\ No newline at end of file
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import SearchRanges from "@/components/searchRanges/SearchRanges";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import useSearchRanges from "@/hooks/common/useSearchRanges";
import useURLParams from "@/hooks/common/useURLParams";
interface AccountSearchParams {
accountName?: string | undefined;
fromBlock: number | undefined;
toBlock: number | undefined;
fromDate: Date | undefined;
toDate: Date | undefined;
lastBlocks: number | undefined;
lastTime: number | undefined;
timeUnit: string | undefined;
rangeSelectKey: string | undefined;
page: number | undefined;
filters: boolean[];
coinType?: string;
}
const defaultSearchParams: AccountSearchParams = {
accountName: undefined,
fromBlock: undefined,
toBlock: undefined,
fromDate: undefined,
toDate: undefined,
lastBlocks: undefined,
lastTime: undefined,
timeUnit: "days",
rangeSelectKey: "none",
page: undefined,
filters: [],
coinType: "HIVE", // Default to HIVE
};
const BalanceHistorySearch = () => {
const [coinType, setCoinType] = useState<string>("HIVE"); // State to store the selected coin name
const COIN_TYPES = ["HIVE", "VESTS", "HBD"];
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
const { paramsState, setParams } = useURLParams(
{
...defaultSearchParams,
},
["accountName"]
);
const {
filters: filtersParam,
fromBlock: fromBlockParam,
toBlock: toBlockParam,
fromDate: fromDateParam,
toDate: toDateParam,
lastBlocks: lastBlocksParam,
timeUnit: timeUnitParam,
lastTime: lastTimeParam,
rangeSelectKey,
page,
} = paramsState;
const [initialSearch, setInitialSearch] = useState<boolean>(false);
const [filters, setFilters] = useState<boolean[]>([]);
const searchRanges = useSearchRanges();
const handleSearch = async (resetPage?: boolean) => {
if (
!initialSearch &&
(!!fromDateParam ||
!!toDateParam ||
!!fromBlockParam ||
!!toBlockParam ||
!!lastBlocksParam ||
!!lastTimeParam ||
!!filtersParam?.length)
) {
fromDateParam && searchRanges.setStartDate(fromDateParam);
toDateParam && searchRanges.setEndDate(toDateParam);
fromBlockParam && searchRanges.setFromBlock(fromBlockParam);
toBlockParam && searchRanges.setToBlock(toBlockParam);
lastBlocksParam && searchRanges.setLastBlocksValue(lastBlocksParam);
timeUnitParam && searchRanges.setTimeUnitSelectKey(timeUnitParam);
rangeSelectKey && searchRanges.setRangeSelectKey(rangeSelectKey);
searchRanges.setLastTimeUnitValue(lastTimeParam);
setFilters(filtersParam);
setInitialSearch(true);
} else {
if (!initialSearch) {
setInitialSearch(true);
}
const {
payloadFromBlock,
payloadToBlock,
payloadStartDate,
payloadEndDate,
} = await searchRanges.getRangesValues();
setParams({
...paramsState,
filters: filters,
fromBlock: payloadFromBlock,
toBlock: payloadToBlock,
fromDate: payloadStartDate,
toDate: payloadEndDate,
lastBlocks:
searchRanges.rangeSelectKey === "lastBlocks"
? searchRanges.lastBlocksValue
: undefined,
lastTime:
searchRanges.rangeSelectKey === "lastTime"
? searchRanges.lastTimeUnitValue
: undefined,
timeUnit:
searchRanges.rangeSelectKey === "lastTime"
? searchRanges.timeUnitSelectKey
: undefined,
rangeSelectKey: searchRanges.rangeSelectKey,
page: resetPage ? undefined : page,
});
}
};
const handleCoinTypeChange = (newCoinType: string) => {
setCoinType(newCoinType);
setParams({ ...paramsState, coinType: newCoinType });
};
const handleFilterClear = () => {
const newPage = rangeSelectKey !== "none" ? undefined : page;
setParams({
...defaultSearchParams,
accountName: accountNameFromRoute,
page: newPage,
});
searchRanges.setRangeSelectKey("none");
setFilters([]);
};
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>
</CardHeader>
<CardContent>
<div className="flex items-center mb-3">
<div className="w-auto bg-theme dark:bg-theme border-0 border-b-2">
<select
value={coinType}
onChange={(e) => handleCoinTypeChange(e.target.value)}
className="w-[180px] border border-gray-300 p-2 rounded"
>
{COIN_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
</div>
<SearchRanges rangesProps={searchRanges} />
<div className="flex items-center justify-between m-2">
<Button onClick={() => handleSearch(true)} data-testid="apply-filters">
<span>Apply filters</span>
</Button>
<Button onClick={() => handleFilterClear()} data-testid="clear-filters">
<span>Clear filters</span>
</Button>
</div>
</CardContent>
</Card>
</>
);
};
export default BalanceHistorySearch;
import { UseQueryResult, useQuery } from "@tanstack/react-query";
import moment from "moment";
import fetchingService from "@/services/FetchingService";
const useBalanceHistory = (
accountName: string,
coinType:string,
page: number | undefined,
pageSize : number | undefined,
direction: "asc" | "desc",
fromDate?: Date | number | undefined,
toDate?: Date| number |undefined,
) => {
/*const isDatesCorrect =
!moment(fromDate).isSame(toDate) && !moment(fromDate).isAfter(toDate);*/
const isDatesCorrect =true;
const fetchBalanceHist = async () =>
await fetchingService.geAccounttBalanceHistory(
accountName,
coinType,
page?page:1,
pageSize,
direction,
fromDate?fromDate:undefined,
toDate?toDate:undefined,
);
const {
data: accountBalanceHistory,
isLoading: isAccountBalanceHistoryLoading,
isError: isAccountBalanceHistoryError,
}: any = useQuery({
queryKey: [
"get_balance_history",
accountName,
coinType,
page,
pageSize,
direction,
fromDate,
toDate,
],
queryFn: fetchBalanceHist,
enabled: !!accountName && isDatesCorrect,
refetchOnWindowFocus: false,
});
return {
accountBalanceHistory,
isAccountBalanceHistoryLoading,
isAccountBalanceHistoryError,
};
};
export default useBalanceHistory;
import { useRouter } from "next/router";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { config } from "@/Config";
import { Loader2 } from "lucide-react";
import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory";
import useURLParams from "@/hooks/common/useURLParams";
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";
export default function BalanceHistory() {
const router = useRouter();
const accountNameFromRoute = (router.query.accountName as string)?.slice(1);
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;
let effectiveFromBlock = paramsState.fromBlock || fromDateParam;
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
);
return (
<>
<Head>
<title>@{accountNameFromRoute} - Hive Explorer</title>
</Head>
<div className="w-[95%] overflow-auto">
<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"
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>
{/* 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-black text-center">
No operations were found.
</div>
) : isAccountBalanceHistoryLoading ? (
<div className="flex justify-center text-center items-center">
<Loader2 className="animate-spin mt-1 text-black 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}
/>
)}
</div>
</>
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment