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

Target

Select target project
  • hive/block_explorer_ui
1 result
Show changes
Commits on Source (36)
Showing
with 1315 additions and 1010 deletions
......@@ -21,6 +21,7 @@ interface CustomPaginationProps {
isMirrored?: boolean;
className?: string;
handleLatestPage?: () => void;
handleFirstPage?:()=>void;
}
const CustomPagination: React.FC<CustomPaginationProps> = ({
......@@ -29,9 +30,10 @@ const CustomPagination: React.FC<CustomPaginationProps> = ({
siblingCount = 1,
pageSize,
onPageChange,
isMirrored = true,
isMirrored = false,
className,
handleLatestPage,
handleFirstPage
}) => {
const paginationRange = usePagination({
currentPage,
......@@ -54,14 +56,33 @@ const CustomPagination: React.FC<CustomPaginationProps> = ({
};
const onFirstPage = () => {
onPageChange(1);
};
if (handleFirstPage) {
handleFirstPage(); // Use the custom handler if provided
} else {
onPageChange(1); // Default behavior: go to first page
}
}
const maxPage = Math.max(
Number(paginationRange[0]),
Number(paginationRange.at(-1))
);
const lastPage = Math.ceil(totalCount / pageSize);
// Don't render pagination if there's only one page
if (lastPage <= 1) {
return null;
}
const onLastPage = () => {
if (handleLatestPage) {
handleLatestPage(); // Use the custom handler if provided
} else {
onPageChange(lastPage); // Default behavior: go to the last page
}
}
return (
<Pagination className={className}>
<PaginationContent className="md:gap-x-4">
......@@ -69,7 +90,7 @@ const CustomPagination: React.FC<CustomPaginationProps> = ({
(isMirrored ? currentPage !== maxPage : currentPage !== 1) && (
<>
<PaginationItem
onClick={handleLatestPage}
onClick={isMirrored ? onLastPage : onFirstPage}
className="cursor-pointer"
>
<PaginationLatest />
......@@ -128,7 +149,7 @@ const CustomPagination: React.FC<CustomPaginationProps> = ({
<PaginationNext />
</PaginationItem>
<PaginationItem
onClick={onFirstPage}
onClick={isMirrored ? onFirstPage : onLastPage}
className="cursor-pointer"
>
<PaginationFirst />
......@@ -140,4 +161,4 @@ const CustomPagination: React.FC<CustomPaginationProps> = ({
);
};
export default CustomPagination;
export default CustomPagination;
\ No newline at end of file
import * as React from "react";
import React, { useState } from "react";
import { Calendar as CalendarIcon } from "lucide-react";
import { Matcher } from "react-day-picker";
import moment from "moment";
import { Button } from "./ui/button";
import { Calendar } from "./ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import TimePicker from "./TimePicker";
import { numberToTimeString } from "@/utils/StringUtils";
import CustomDateTimePicker from "./customDateTime/CustomDateTImePicker";
interface DateTimePickerProps {
date: Date;
......@@ -17,6 +17,17 @@ interface DateTimePickerProps {
firstDate?: Date;
}
const displayDate = (d: Date) => {
return `${d.toDateString()} ${numberToTimeString(
d.getUTCHours()
)}:${numberToTimeString(d.getUTCMinutes())}:${numberToTimeString(
d.getUTCSeconds()
)} UTC`;
};
const normalizeToMidnight = (d: Date) => {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
};
const DateTimePicker: React.FC<DateTimePickerProps> = ({
date,
setDate,
......@@ -25,33 +36,41 @@ const DateTimePicker: React.FC<DateTimePickerProps> = ({
lastDate,
firstDate,
}) => {
const handleSelect = (date: Date | undefined) => {
if (date) {
setDate(new Date(date));
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
const handleSelect = (d: Date | undefined) => {
if (d) {
setDate(moment(d).toDate());
}
};
const displayDate = (date: Date) => {
return `${date.toDateString()} ${numberToTimeString(
date.getUTCHours()
)}:${numberToTimeString(date.getUTCMinutes())}:${numberToTimeString(
date.getUTCSeconds()
)} UTC`;
};
const isValidDate = (d: Date) => {
const nd = normalizeToMidnight(d);
const disableFuture: Matcher | Matcher[] | undefined | any = (date: Date) => {
if (disableFutureDates) {
if (firstDate) {
return date <= firstDate || date < new Date("1900-01-01");
const nf = normalizeToMidnight(firstDate);
const now = normalizeToMidnight(new Date());
return nd >= nf && nd <= now;
}
if (lastDate) {
return date >= lastDate || date < new Date("1900-01-01");
const nl = normalizeToMidnight(lastDate);
return nd <= nl;
}
return date >= new Date() || date < new Date("1900-01-01");
return nd < new Date();
}
return true;
};
const handleCloseDateTimePicker = () => {
setIsCalendarOpen(false);
};
return (
<Popover>
<Popover
open={isCalendarOpen}
onOpenChange={() => setIsCalendarOpen(!isCalendarOpen)}
>
<PopoverTrigger
asChild
className="z-10 border-0 border-b-2"
......@@ -59,7 +78,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = ({
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal h-auto",
"w-full justify-start text-left font-normal h-auto cursor-pointer",
!date && "text-muted-foreground"
)}
data-testid="datepicker-trigger"
......@@ -72,19 +91,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = ({
side={side}
className="w-auto p-0 text-text bg-theme"
>
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
data-testid="datepicker-calender"
disabled={disableFuture}
<CustomDateTimePicker
value={date}
onChange={handleSelect}
open={isCalendarOpen}
onClose={handleCloseDateTimePicker}
isValidDate={isValidDate}
/>
<div className="flex justify-center items-center mb-4">
<TimePicker
date={date}
onSelect={handleSelect}
/>
</div>
</PopoverContent>
</Popover>
);
......
......@@ -123,8 +123,8 @@ const OperationsTable: React.FC<OperationsTableProps> = ({
>
<TableHeader>
<TableRow>
<TableHead className="sticky left-0 bg-theme "></TableHead>
<TableHead className="pl-2 sticky left-12 bg-theme ">Block</TableHead>
<TableHead className="sticky left-0"></TableHead>
<TableHead className="pl-2 sticky left-12">Block</TableHead>
<TableHead>Transaction</TableHead>
<TableHead>Time</TableHead>
<TableHead>Operation</TableHead>
......@@ -152,11 +152,11 @@ const OperationsTable: React.FC<OperationsTableProps> = ({
operationPerspective === "incoming",
})}
>
<TableCell className="sticky left-0 bg-theme xl:bg-inherit">
<TableCell className="sticky left-0 xl:bg-inherit">
<CopyJSON value={getUnformattedValue(operation)} />
</TableCell>
<TableCell
className="pl-2 sticky left-12 bg-theme xl:bg-inherit"
className="pl-2 sticky left-12 xl:bg-inherit"
data-testid="block-number-operation-table"
>
<Link
......
import React, { useState, useRef, useEffect } from "react";
import { Search, X, CornerDownLeft as Enter } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import Hive from "@/types/Hive";
import React, { useState, useRef } from "react";
import { cn } from "@/lib/utils";
import { capitalizeFirst, trimAccountName } from "@/utils/StringUtils";
import useMediaQuery from "@/hooks/common/useMediaQuery";
import useDebounce from "@/hooks/common/useDebounce";
import useOnClickOutside from "@/hooks/common/useOnClickOutside";
import useInputType from "@/hooks/api/common/useInputType";
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import AutocompleteInput from "./ui/AutoCompleteInput";
import { Search } from "lucide-react";
import { X } from "lucide-react";
interface SearchBarProps {
open: boolean;
onChange?: (open: boolean) => void;
className?: string;
}
const getResultTypeHeader = (result: Hive.InputTypeResponse) => {
switch (result.input_type) {
case "block_num":
return "block";
case "transaction_hash":
return "transaction";
case "block_hash":
return "block";
default:
return "account";
}
};
const renderSearchData = (
data: Hive.InputTypeResponse,
onClick: Function,
selected: number
) => {
if (data.input_type === "account_name_array") {
return (
<div className="flex flex-col">
{(data.input_value as string[]).map((account, index) => {
return (
<div
key={index}
className={cn(
"px-4 py-2 hover:bg-explorer-light-gray flex items-center justify-between",
{
"md:bg-explorer-light-gray bg-opacity-50": selected === index,
"border-t border-gray-700": !!index,
}
)}
>
<Link
onClick={() => onClick()}
href={`/@${account}`}
className="w-full"
>
User <span className="text-explorer-turquoise">{account}</span>
</Link>
{selected === index && <Enter className="hidden md:inline" />}
</div>
);
})}
</div>
);
} else if (data.input_type === "invalid_input") {
// TODO: handle empty data for block num and transaction
return (
<div className="px-4 py-2">Account not found: {data.input_value}</div>
);
} else {
const resultType = getResultTypeHeader(data);
const href =
resultType === "account"
? `/@${data.input_value}`
: `/${resultType}/${data.input_value}`;
return (
<div className="px-4 py-2 flex items-center justify-between">
<Link
onClick={() => onClick()}
className="w-full block"
href={href}
data-testid="navbar-search-content-link"
>
{capitalizeFirst(resultType)}{" "}
<span className="text-explorer-turquoise">{data.input_value}</span>
</Link>
<Enter className="hidden md:inline" />
</div>
);
}
};
const SearchBar: React.FC<SearchBarProps> = ({ open, onChange, className }) => {
const [searchTerm, setSearchTerm] = useState("");
const [inputFocus, setInputFocus] = useState(false);
const [selectedResult, setSelectedResult] = useState(0);
const [isAutocompleteVisible, setAutocompleteVisible] = useState(open);
const searchContainerRef = useRef(null);
const [searchInputType, setSearchInputType] = useState<string>("");
useOnClickOutside(searchContainerRef, () => setInputFocus(false));
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
const { inputTypeData } = useInputType(searchInputType);
const updateInput = async (value: string) => {
setSearchInputType(value);
};
const debouncedSearch = useDebounce(
(value: string) => updateInput(trimAccountName(value)),
1000
);
const handleInputChange = (value: string) => {
setSearchTerm(value);
debouncedSearch(value);
const handleToggle = () => {
setAutocompleteVisible(!isAutocompleteVisible);
if (onChange) onChange(!isAutocompleteVisible);
};
const resetSearchBar = () => {
setInputFocus(false);
setSearchTerm("");
updateInput("");
setSelectedResult(0);
};
useEffect(() => {
const keyDownEvent = (event: KeyboardEvent) => {
if (inputFocus && inputTypeData?.input_value?.length) {
if (event.code === "ArrowDown") {
setSelectedResult((selectedResult) =>
selectedResult < inputTypeData.input_value.length - 1
? selectedResult + 1
: selectedResult
);
}
if (event.code === "ArrowUp") {
setSelectedResult((selectedResult) =>
selectedResult > 0 ? selectedResult - 1 : selectedResult
);
}
if (event.code === "Enter") {
if (inputTypeData.input_type === "account_name_array") {
router.push(`/@${inputTypeData.input_value[selectedResult]}`);
} else {
if (inputTypeData.input_type === "account_name") {
router.push(`/@${inputTypeData.input_value}`);
}
}
resetSearchBar();
setInputFocus(false);
}
}
};
document.addEventListener("keydown", keyDownEvent);
return () => {
document.removeEventListener("keydown", keyDownEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputFocus, inputTypeData, selectedResult]);
return (
<>
<div
className={cn(
"w-0 hidden md:w-1/4 relative bg-theme dark:bg-theme",
{
"w-full inline": open,
},
className
)}
ref={searchContainerRef}
>
<div className="border-input border-b-2 flex items-center pr-2">
<Input
className="border-0"
type="text"
placeholder="Search user, block, transaction"
value={searchTerm}
onChange={(e) => handleInputChange(e.target.value)}
onFocus={() => setInputFocus(true)}
data-testid="search-bar-input"
/>
{isMobile ? (
<X
className="cursor-pointer"
onClick={() => {
resetSearchBar();
onChange && onChange(false);
}}
/>
) : !!searchTerm.length ? (
<X
className="cursor-pointer"
onClick={() => resetSearchBar()}
/>
) : (
<Search />
{isAutocompleteVisible && (
<AutocompleteInput
value={searchTerm}
onChange={setSearchTerm}
placeholder="Search user, block, transaction"
inputType={['block_num', 'transaction_hash', 'block_hash', 'account_name']}
className={cn(
"w-full md:w-1/4 bg-theme dark:bg-theme border-0 border-b-2 width rowHover",
className
)}
</div>
{inputFocus && !!inputTypeData?.input_value && (
<div className="absolute bg-theme dark:bg-theme w-full max-h-96 overflow-y-auto border border-input border-t-0">
{renderSearchData(inputTypeData, resetSearchBar, selectedResult)}
</div>
)}
</div>
{!open && (
linkResult={true}
addLabel={true}
/>
)}
{!isAutocompleteVisible && (
<Button
className="px-0 bg-inherit"
onClick={() => onChange && onChange(true)}
onClick={handleToggle}
className="md:hidden"
>
<Search />
</Button>
)}
{isAutocompleteVisible && (
<X
onClick={handleToggle}
className="md:hidden"
/>
)}
</>
);
};
......
......@@ -27,6 +27,12 @@ import { useHiveChainContext } from "@/contexts/HiveChainContext";
import { convertVestsToHP } from "@/utils/Calculations";
import fetchingService from "@/services/FetchingService";
interface Supply {
amount: string;
nai: string;
precision: number;
}
type VotersDialogProps = {
accountName: string;
isVotesHistoryOpen: boolean;
......@@ -57,8 +63,20 @@ const VotesHistoryDialog: React.FC<VotersDialogProps> = ({
);
const [toDate, setToDate] = useState<Date>(moment().toDate());
const [isHP, setIsHP] = useState<boolean>(true); // Toggle state
const [totalVestingShares, setTotalVestingShares] = useState<Supply>({
amount: "0",
nai: "",
precision: 0,
});
const [totalVestingFundHive, setTotalVestingFundHive] = useState<Supply>({
amount: "0",
nai: "",
precision: 0,
});
const { hiveChain } = useHiveChainContext();
const { witnessDetails } = useWitnessDetails(accountName, true) as any;
const { votesHistory, isVotesHistoryLoading } = useWitnessVotesHistory(
accountName,
isVotesHistoryOpen,
......@@ -67,38 +85,30 @@ const VotesHistoryDialog: React.FC<VotersDialogProps> = ({
liveDataEnabled
);
useEffect(() => {
setPage(1);
if (votesHistory && votesHistory?.length > PAGE_SIZE) {
setDisplayData(votesHistory.slice(0, PAGE_SIZE - 1));
} else {
setDisplayData(votesHistory);
}
}, [votesHistory]);
const handlePageChange = (page: number) => {
setPage(page);
setDisplayData(
votesHistory?.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1)
);
};
interface Supply {
amount: string;
nai: string;
precision: number;
}
const [totalVestingShares, setTotalVestingShares] = useState<Supply>({
amount: "0",
nai: "",
precision: 0,
});
const [totalVestingFundHive, setTotalVestingFundHive] = useState<Supply>({
amount: "0",
nai: "",
precision: 0,
});
const { hiveChain } = useHiveChainContext();
const fetchHivePower = (value: string, isHP: boolean): string => {
if (isHP) {
if (!hiveChain) return "";
return convertVestsToHP(
hiveChain,
value,
totalVestingFundHive,
totalVestingShares
);
}
return formatNumber(parseInt(value), true, false) + " Vests"; // Return raw vests if not toggled to HP
};
useEffect(() => {
if (moment(fromDate).isSame(toDate) || moment(fromDate).isAfter(toDate)) {
setFromDate(moment(fromDate).subtract(1, "hours").toDate());
}
}, [fromDate, toDate]);
useEffect(() => {
const fetchDynamicGlobalProperties = async () => {
......@@ -115,18 +125,14 @@ const VotesHistoryDialog: React.FC<VotersDialogProps> = ({
fetchDynamicGlobalProperties();
}, []);
const fetchHivePower = (value: string, isHP: boolean): string => {
if (isHP) {
if (!hiveChain) return "";
return convertVestsToHP(
hiveChain,
value,
totalVestingFundHive,
totalVestingShares
);
useEffect(() => {
setPage(1);
if (votesHistory && votesHistory?.length > PAGE_SIZE) {
setDisplayData(votesHistory.slice(0, PAGE_SIZE - 1));
} else {
setDisplayData(votesHistory);
}
return formatNumber(parseInt(value), true, false) + " Vests"; // Return raw vests if not toggled to HP
};
}, [votesHistory]);
return (
<Dialog
......
......@@ -7,7 +7,10 @@ import VestsTooltip from "../VestsTooltip";
import Explorer from "@/types/Explorer";
import { changeHBDToDollarsDisplay, grabNumericValue } from "@/utils/StringUtils";
import { cn, formatNumber } from "@/lib/utils";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHistory } from "@fortawesome/free-solid-svg-icons";
import { Tooltip,TooltipProvider,TooltipTrigger,TooltipContent } from "@radix-ui/react-tooltip";
type AccountBalanceCardProps = {
header: string;
userDetails: Explorer.FormattedAccountDetails;
......@@ -146,7 +149,40 @@ const AccountBalanceCard: React.FC<AccountBalanceCardProps> = ({
onClick={handleBalancesVisibility}
className="flex justify-between align-center p-2 hover:bg-rowHover cursor-pointer px-4"
>
<div className="text-lg">{header}</div>
<div className="text-lg">
{header} /
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/balanceHistory/@${userDetails.name}`}
data-testid="balance-history-link"
className="text-link mr-2 font-light text-sm text-pretty underline"
onClick={(e) => e.stopPropagation()}
>
<FontAwesomeIcon
icon={faHistory}
size="sm"
className="mr-1 ml-2"
/>
<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 ml-3 text-sm">
<p>Click Here for Balance History</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{isBalancesHidden ? <ArrowDown /> : <ArrowUp />}
</div>
</CardHeader>
......@@ -155,7 +191,10 @@ const AccountBalanceCard: React.FC<AccountBalanceCardProps> = ({
data-testid="card-content"
>
<Table>
<TableBody>{buildTableBody(keys)}{renderBalance()}</TableBody>
<TableBody>
{buildTableBody(keys)}
{renderBalance()}
</TableBody>
</Table>
</CardContent>
</Card>
......
......@@ -63,7 +63,7 @@ const AccountMainCard: React.FC<AccountMainCardProps> = ({
data-testid="user-avatar"
/>
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white" data-testid="account-name">
{accountDetails.name}
</h2>
{accountDetails.is_witness && (
......@@ -192,7 +192,7 @@ const AccountMainCard: React.FC<AccountMainCardProps> = ({
<div className="text-center flex flex-col justify-space-between w-full gap-2">
<span className="text">Creation Date:</span>
<span
className="text"
className="text"
data-testid="creation-date"
>
{formatAndDelocalizeTime(accountDetails.created)}
......@@ -201,7 +201,7 @@ const AccountMainCard: React.FC<AccountMainCardProps> = ({
<div className="text-center flex flex-col justify-space-between w-full gap-2">
<span className="text">Reputation:</span>
<span
className="text"
className="text"
data-testid="creation-date"
>
{accountDetails.reputation}
......
......@@ -49,6 +49,7 @@ const AccountTopBar: React.FC<AccountTopBarProps> = ({
totalCount={accountOperations.total_operations || 0}
pageSize={config.standardPaginationSize}
onPageChange={setPage}
isMirrored={true}
/>
</div>
<div className="my-1 flex gap-x-2">
......
......@@ -43,7 +43,7 @@ const MobileAccountNameCard: React.FC<MobileAccountNameCardProps> = ({
data-testid="user-avatar"
/>
<div>
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">
<h2 className="text-lg font-semibold text-gray-800 dark:text-white" data-testid="account-name">
{accountDetails.name}
</h2>
{accountDetails.is_witness && (
......
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 React, { useState, useRef, useEffect } from "react";
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addMonths,
addDays,
isSameMonth,
isSameDay,
setMonth,
setYear,
} from "date-fns";
import "./customDateTimePicker.css";
interface CustomDateTimePickerProps {
value: Date;
onChange: (date: Date) => void;
open: boolean;
onClose: () => void;
isValidDate: (day: Date) => boolean;
}
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const WEEK_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const CustomDateTimePicker: React.FC<CustomDateTimePickerProps> = ({
value,
onChange,
open,
onClose,
isValidDate,
}) => {
const [internalValue, setInternalValue] = useState(value || new Date());
const [currentMonth, setCurrentMonth] = useState(
value ? new Date(value) : new Date()
);
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false);
const [tempYear, setTempYear] = useState((value || new Date()).getFullYear());
const containerRef: React.MutableRefObject<null | any> = useRef(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
event.target instanceof Node &&
!containerRef.current.contains(event.target)
) {
setShowYearMonthPicker(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleDateClick = (day: Date) => {
if (isValidDate && !isValidDate(day)) return;
const hours = internalValue.getUTCHours();
const minutes = internalValue.getUTCMinutes();
const seconds = internalValue.getUTCSeconds();
const updatedDate = new Date(
Date.UTC(
day.getFullYear(),
day.getMonth(),
day.getDate(),
hours,
minutes,
seconds
)
);
setInternalValue(updatedDate);
};
const handleMonthChange = (direction: any) => {
setCurrentMonth(addMonths(currentMonth, direction));
};
const handleTimeChange = (type: any, newValue: any) => {
const updatedDate = new Date(internalValue.getTime());
if (type === "hours") {
updatedDate.setUTCHours(
parseInt(newValue, 10),
updatedDate.getUTCMinutes(),
updatedDate.getUTCSeconds()
);
} else if (type === "minutes") {
updatedDate.setUTCMinutes(
parseInt(newValue, 10),
updatedDate.getUTCSeconds()
);
} else if (type === "seconds") {
updatedDate.setUTCSeconds(parseInt(newValue, 10));
}
setInternalValue(updatedDate);
};
const handleConfirm = () => {
onChange(internalValue);
setShowYearMonthPicker(false);
onClose();
};
const renderDays = () => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(monthStart);
const startDate = startOfWeek(monthStart, { weekStartsOn: 0 });
const endDate = endOfWeek(monthEnd, { weekStartsOn: 0 });
const dayFormat = "d";
const rows = [];
let days = [];
let day = startDate;
while (day <= endDate) {
const weekStart = day;
for (let i = 0; i < 7; i++) {
const cloneDay = day;
const formattedDate = format(day, dayFormat);
const isSelectedDay = isSameDay(cloneDay, internalValue);
const inCurrentMonth = isSameMonth(cloneDay, monthStart);
const dayIsValid = !isValidDate || isValidDate(cloneDay);
const disabledClass = !inCurrentMonth ? "not-same-month" : "";
const selectedClass = isSelectedDay ? "selected-day" : "";
const invalidClass = dayIsValid ? "" : "disabled-day";
days.push(
<div
className={`calendar-day ${disabledClass} ${selectedClass} ${invalidClass}`}
key={cloneDay.toISOString()}
onClick={() => handleDateClick(cloneDay)}
>
<span>{formattedDate}</span>
</div>
);
day = addDays(day, 1);
}
rows.push(
<div
className="calendar-row"
key={weekStart.toISOString()}
>
{days}
</div>
);
days = [];
}
return <div className="calendar-body">{rows}</div>;
};
const handleYearChange = (e: any) => {
setTempYear(parseInt(e.target.value, 10));
};
const handleMonthSelect = (monthIndex: any) => {
const updatedMonth = setYear(
setMonth(new Date(currentMonth), monthIndex),
tempYear
);
setCurrentMonth(updatedMonth);
setShowYearMonthPicker(false);
};
const hours = internalValue.getUTCHours();
const minutes = internalValue.getUTCMinutes();
const seconds = internalValue.getUTCSeconds();
return (
<>
{open && (
<div className="datetime-popover">
{!showYearMonthPicker && (
<>
<div className="calendar-header">
<button
className="month-button"
onClick={() => handleMonthChange(-1)}
>
{"<"}
</button>
<span
className="month-label"
onClick={() => {
setTempYear(currentMonth.getFullYear());
setShowYearMonthPicker(true);
}}
>
{format(currentMonth, "MMMM yyyy")}
</span>
<button
className="month-button"
onClick={() => handleMonthChange(1)}
>
{">"}
</button>
</div>
<div className="calendar-day-names">
{WEEK_DAYS.map((day) => (
<div
className="day-name"
key={day}
>
{day}
</div>
))}
</div>
{renderDays()}
<div className="time-section">
<div className="time-inputs">
<div className="time-field">
<label>Hours</label>
<input
type="number"
min="0"
max="23"
value={hours}
onChange={(e) =>
handleTimeChange("hours", e.target.value)
}
/>
</div>
<div className="time-field">
<label>Minutes</label>
<input
type="number"
min="0"
max="59"
value={minutes}
onChange={(e) =>
handleTimeChange("minutes", e.target.value)
}
/>
</div>
<div className="time-field">
<label>Seconds</label>
<input
type="number"
min="0"
max="59"
value={seconds}
onChange={(e) =>
handleTimeChange("seconds", e.target.value)
}
/>
</div>
</div>
</div>
<div className="actions">
<button
onClick={() => {
onClose();
setShowYearMonthPicker(false);
}}
>
Cancel
</button>
<button onClick={handleConfirm}>OK</button>
</div>
</>
)}
{showYearMonthPicker && (
<div className="year-month-picker">
<div className="year-input-section">
<label>Select Year:</label>
<input
type="number"
value={tempYear}
onChange={handleYearChange}
style={{ width: "80px", textAlign: "center" }}
/>
</div>
<div className="month-grid">
{MONTHS.map((month, index) => (
<div
key={month}
className="month-cell"
onClick={() => handleMonthSelect(index)}
>
{month}
</div>
))}
</div>
<div className="actions">
<button onClick={() => setShowYearMonthPicker(false)}>
Back
</button>
</div>
</div>
)}
</div>
)}
</>
);
};
export default CustomDateTimePicker;
.datetime-popover {
position: relative;
z-index: 9999 !important;
background: var(--color-background);
color: var(--color-text);
min-width: 255px;
padding: 8px;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.month-button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 4px
}
.month-button:hover {
background: var(--color-button-hover);
}
.month-label {
font-weight: bold;
font-size: 14px;
cursor: pointer;
}
.calendar-day-names {
display: flex;
background: var(--color-row-even);
}
.calendar-body {
display: flex;
flex-direction: column;
background: var(--color-row-even);
}
.calendar-row {
display: flex;
}
.day-name,
.calendar-day {
flex: 1;
text-align: center;
margin: 2px 0;
font-size: 12px;
background: var(--color-row-odd)
}
.calendar-day {
cursor: pointer;
border-radius: 4px;
padding: 4px 0;
transition: background 0.2s;
}
.calendar-day:not(.not-same-month) {
color: #000;
}
.calendar-day.not-same-month {
color: #aaa;
}
.calendar-day:hover:not(.selected-day):not(.not-same-month) {
background: var(--color-row-hover)
}
.selected-day {
background: var(--color-turquoise);
}
.time-section {
margin-top: 8px;
padding: 8px 0;
border-top: 1px solid #ccc;
}
.time-inputs {
display: flex;
justify-content: space-around;
}
.time-field {
display: flex;
flex-direction: column;
align-items: center;
}
.time-field label {
font-size: 12px;
margin-bottom: 4px;
}
.time-field input {
width: 50px;
text-align: center;
padding: 4px;
border: solid var(--color-text) 1px;
border-radius: 4px;
background: var(--color-background);
color: var(--color-text)
}
.actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.actions button {
margin-left: 8px;
padding: 4px 8px;
cursor: pointer;
border: none;
border-radius: 4px;
background: var(--color-button);
}
.actions button:hover {
background: var(--color-button-hover)
}
.year-month-picker {
display: flex;
flex-direction: column;
padding: 8px;
max-width: 250px;
}
.year-input-section {
display: flex;
align-items: center;
margin-bottom: 8px;
justify-content: center;
}
.year-input-section label {
margin-right: 8px;
}
.year-input-section input {
background: var(--color-background);
color: var(--color-text);
border: solid var(--color-text) 1px;
border-radius: 4px;
padding: 4px;
}
.month-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 8px;
}
.month-cell {
width: 40%;
text-align: center;
padding: 8px;
margin: 4px;
background: var(--color-button);
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
color: var(--color-text)
}
.month-cell:hover {
background: var(--color-button-hover);
}
.disabled-day {
background: var(--color-row-even);
pointer-events: none;
cursor: default;
}
\ No newline at end of file
This diff is collapsed.
import { useState } from "react";
import { Loader2 } from "lucide-react";
import Hive from "@/types/Hive";
import Explorer from "@/types/Explorer";
import { getOperationButtonTitle } from "@/utils/UI";
import { trimAccountName } from "@/utils/StringUtils";
import { SearchRangesResult } from "@/hooks/common/useSearchRanges";
import { Input } from "@/components/ui/input";
import SearchRanges from "@/components/searchRanges/SearchRanges";
import OperationTypesDialog from "@/components/OperationTypesDialog";
import { Button } from "@/components/ui/button";
import AutocompleteInput from "@/components/ui/AutoCompleteInput";
interface AccountSearchProps {
startAccountOperationsSearch: (
accountSearchOperationsProps: Explorer.AccountSearchOperationsProps
) => Promise<void>;
operationsTypes?: Hive.OperationPattern[];
loading?: boolean;
searchRanges: SearchRangesResult;
}
import { startAccountOperationsSearch } from "./utils/accountSearchHelpers";
import { useSearchesContext } from "@/contexts/SearchesContext";
import useOperationsTypes from "@/hooks/api/common/useOperationsTypes";
import useAccountOperations from "@/hooks/api/accountPage/useAccountOperations";
const AccountSearch = () => {
const {
setLastSearchKey,
setAccountOperationsPage,
accountOperationsSearchProps,
setAccountOperationsSearchProps,
setPreviousAccountOperationsSearchProps,
searchRanges,
} = useSearchesContext();
const { isAccountOperationsLoading } = useAccountOperations(
accountOperationsSearchProps
);
const { operationsTypes } = useOperationsTypes();
const AccountSearch: React.FC<AccountSearchProps> = ({
startAccountOperationsSearch,
operationsTypes,
loading,
searchRanges,
}) => {
const [accountName, setAccountName] = useState<string>("");
const [selectedOperationTypes, setSelectedOperationTypes] = useState<
number[]
......@@ -53,7 +55,13 @@ const AccountSearch: React.FC<AccountSearchProps> = ({
? selectedOperationTypes
: undefined,
};
startAccountOperationsSearch(accountOperationsSearchProps);
startAccountOperationsSearch(
accountOperationsSearchProps,
(val: "account") => setLastSearchKey(val),
setAccountOperationsPage,
setAccountOperationsSearchProps,
setPreviousAccountOperationsSearchProps
);
}
};
......@@ -67,8 +75,7 @@ const AccountSearch: React.FC<AccountSearchProps> = ({
inputType="account_name"
className="w-1/2 bg-theme dark:bg-theme border-0 border-b-2"
required={true}
/>
/>
</div>
<SearchRanges
rangesProps={searchRanges}
......@@ -93,7 +100,9 @@ const AccountSearch: React.FC<AccountSearchProps> = ({
disabled={!accountName}
>
Search
{loading && <Loader2 className="ml-2 animate-spin h-4 w-4 ..." />}
{isAccountOperationsLoading && (
<Loader2 className="ml-2 animate-spin h-4 w-4 ..." />
)}
</Button>
{!accountName && (
<label className="ml-2 text-muted-foreground">Set account name</label>
......
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">
<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">
<span>Apply filters</span>
</Button>
<Button onClick={() => handleFilterClear()} data-testid="clear-filters">
<span>Clear filters</span>
</Button>
</div>
</CardContent>
</Card>
</>
);
};
export default BalanceHistorySearch;
import { useState } from "react";
import { Loader2, HelpCircle } from "lucide-react";
import { Loader2 } from "lucide-react";
import { config } from "@/Config";
import Hive from "@/types/Hive";
import Explorer from "@/types/Explorer";
import { getOperationButtonTitle } from "@/utils/UI";
import useSearchRanges from "@/hooks/common/useSearchRanges";
import useOperationKeys from "@/hooks/api/homePage/useOperationKeys";
import SearchRanges from "@/components/searchRanges/SearchRanges";
import OperationTypesDialog from "@/components/OperationTypesDialog";
......@@ -22,23 +20,26 @@ import {
SelectItem,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { trimAccountName } from "@/utils/StringUtils";
import AutocompleteInput from "@/components/ui/AutoCompleteInput";
interface BlockSearchProps {
startBlockSearch: (
blockSearchProps: Explorer.BlockSearchProps
) => Promise<void>;
operationsTypes?: Hive.OperationPattern[];
loading?: boolean;
}
import { useSearchesContext } from "@/contexts/SearchesContext";
import useBlockSearch from "@/hooks/api/homePage/useBlockSearch";
import useOperationsTypes from "@/hooks/api/common/useOperationsTypes";
import { startBlockSearch } from "./utils/blockSearchHelpers";
const BlockSearch = () => {
const {
blockSearchProps,
setBlockSearchProps,
setLastSearchKey,
searchRanges,
} = useSearchesContext();
const { operationsTypes } = useOperationsTypes();
const { blockSearchDataLoading } = useBlockSearch(blockSearchProps);
const BlockSearch: React.FC<BlockSearchProps> = ({
startBlockSearch,
operationsTypes,
loading,
}) => {
const [accountName, setAccountName] = useState<string>("");
const [selectedOperationTypes, setSelectedOperationTypes] = useState<
number[]
......@@ -52,7 +53,6 @@ const BlockSearch: React.FC<BlockSearchProps> = ({
);
const [selectedIndex, setSelectedIndex] = useState<string>("");
const searchRanges = useSearchRanges("lastBlocks");
const { operationKeysData } = useOperationKeys(singleOperationTypeId);
const { getRangesValues } = searchRanges;
......@@ -82,7 +82,7 @@ const BlockSearch: React.FC<BlockSearchProps> = ({
setKeysForProperty(Number(newValue));
};
const onButtonClick = async () => {
const handleStartBlockSearch = async () => {
const {
payloadFromBlock,
payloadToBlock,
......@@ -105,18 +105,21 @@ const BlockSearch: React.FC<BlockSearchProps> = ({
content: fieldContent !== "" ? fieldContent : undefined,
},
};
startBlockSearch(blockSearchProps);
startBlockSearch(blockSearchProps, setBlockSearchProps, (val: "block") =>
setLastSearchKey(val)
);
};
return (
<>
<div className="flex flex-col">
<AutocompleteInput
<AutocompleteInput
value={accountName}
onChange={setAccountName}
placeholder="Account name"
inputType="account_name"
className="w-1/2 bg-theme dark:bg-theme border-0 border-b-2"
className="w-1/2 bg-theme border-0 border-b-2"
/>
</div>
<SearchRanges
......@@ -233,10 +236,12 @@ const BlockSearch: React.FC<BlockSearchProps> = ({
<div className="flex items-center ">
<Button
data-testid="block-search-btn"
onClick={onButtonClick}
onClick={handleStartBlockSearch}
>
Search
{loading && <Loader2 className="ml-2 animate-spin h-4 w-4 ..." />}
{blockSearchDataLoading && (
<Loader2 className="ml-2 animate-spin h-4 w-4 ..." />
)}
</Button>
</div>
</>
......
import { useEffect, useState } from "react";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import Explorer from "@/types/Explorer";
import { trimAccountName } from "@/utils/StringUtils";
import { SearchRangesResult } from "@/hooks/common/useSearchRanges";
import { Input } from "@/components/ui/input";
import SearchRanges from "@/components/searchRanges/SearchRanges";
import { Button } from "@/components/ui/button";
import AutocompleteInput from "@/components/ui/AutoCompleteInput";
interface CommentsPermlinkSearchProps {
startCommentPermlinkSearch: (
accountSearchOperationsProps: Explorer.CommentPermlinSearchParams
) => Promise<void>;
data?: Explorer.PermlinkSearchProps;
loading?: boolean;
searchRanges: SearchRangesResult;
}
import { useSearchesContext } from "@/contexts/SearchesContext";
import usePermlinkSearch from "@/hooks/api/common/usePermlinkSearch";
import { startCommentPermlinkSearch } from "./utils/commentPermlinkSearchHelpers";
const CommentsPermlinkSearch = () => {
const {
permlinkSearchProps,
setPermlinkSearchProps,
setCommentPaginationPage,
setCommentType,
setLastSearchKey,
searchRanges,
} = useSearchesContext();
const { permlinkSearchDataLoading } = usePermlinkSearch(permlinkSearchProps);
const CommentsPermlinkSearch: React.FC<CommentsPermlinkSearchProps> = ({
startCommentPermlinkSearch,
loading,
data,
searchRanges,
}) => {
const [accountName, setAccountName] = useState<string>("");
const { getRangesValues } = searchRanges;
const setSearchValues = (data: Explorer.PermlinkSearchProps | any) => {
data.accountName && setAccountName(data.accountName);
searchRanges.setRangesValues(data);
};
const onButtonClick = async () => {
if (accountName !== "") {
const {
......@@ -51,26 +46,24 @@ const CommentsPermlinkSearch: React.FC<CommentsPermlinkSearchProps> = ({
? searchRanges.lastBlocksValue
: undefined,
lastTime: searchRanges.lastTimeUnitValue,
page: data?.page || 1,
rangeSelectKey: searchRanges.rangeSelectKey,
timeUnit: searchRanges.timeUnitSelectKey,
};
startCommentPermlinkSearch(commentPermlinksSearchProps);
startCommentPermlinkSearch(
commentPermlinksSearchProps,
setPermlinkSearchProps,
setCommentPaginationPage,
setCommentType,
(val: "comment-permlink") => setLastSearchKey(val)
);
}
};
useEffect(() => {
if (!!data) {
setSearchValues(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<p className="ml-2">Find comments permlinks by account name</p>
<div className="flex flex-col">
<AutocompleteInput
<AutocompleteInput
value={accountName}
onChange={setAccountName}
placeholder="Author"
......@@ -91,7 +84,9 @@ const CommentsPermlinkSearch: React.FC<CommentsPermlinkSearchProps> = ({
disabled={!accountName}
>
Search
{loading && <Loader2 className="ml-2 animate-spin h-4 w-4 ..." />}
{permlinkSearchDataLoading && (
<Loader2 className="ml-2 animate-spin h-4 w-4 ..." />
)}
</Button>
{!accountName && (
<label className="text-gray-300 dark:text-gray-500 ">
......
import { useEffect, useState } from "react";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { config } from "@/Config";
import Hive from "@/types/Hive";
import Explorer from "@/types/Explorer";
import { getOperationButtonTitle } from "@/utils/UI";
import { trimAccountName } from "@/utils/StringUtils";
import {
convertBooleanArrayToIds,
convertIdsToBooleanArray,
} from "@/lib/utils";
import { SearchRangesResult } from "@/hooks/common/useSearchRanges";
import { convertIdsToBooleanArray } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import SearchRanges from "@/components/searchRanges/SearchRanges";
import OperationTypesDialog from "@/components/OperationTypesDialog";
import { Button } from "@/components/ui/button";
import AutocompleteInput from "@/components/ui/AutoCompleteInput";
interface CommentsSearchProps {
startCommentsSearch: (
accountSearchOperationsProps: Explorer.CommentSearchParams
) => Promise<void>;
operationsTypes?: Hive.OperationPattern[];
data?: Explorer.CommentSearchParams;
loading?: boolean;
searchRanges: SearchRangesResult;
}
import { startCommentSearch } from "./utils/commentSearchHelpers";
import { useSearchesContext } from "@/contexts/SearchesContext";
import useOperationsTypes from "@/hooks/api/common/useOperationsTypes";
import useCommentSearch from "@/hooks/api/common/useCommentSearch";
const CommentsSearch = () => {
const {
setCommentSearchProps,
commentSearchProps,
setCommentPaginationPage,
commentPaginationPage,
setPreviousCommentSearchProps,
setLastSearchKey,
searchRanges,
} = useSearchesContext();
const { commentSearchDataLoading } = useCommentSearch(commentSearchProps);
const { operationsTypes } = useOperationsTypes();
const CommentsSearch: React.FC<CommentsSearchProps> = ({
startCommentsSearch,
operationsTypes,
loading,
data,
searchRanges,
}) => {
const [accountName, setAccountName] = useState<string>("");
const [permlink, setPermlink] = useState<string>("");
const [
......@@ -42,16 +39,6 @@ const CommentsSearch: React.FC<CommentsSearchProps> = ({
const { getRangesValues } = searchRanges;
const setSearchValues = (data: Explorer.CommentSearchParams) => {
data.accountName && setAccountName(Array.isArray(data.accountName) ? data.accountName[0] : data.accountName);
data.permlink && setPermlink(data.permlink);
data.filters &&
setSelectedCommentSearchOperationTypes(
convertBooleanArrayToIds(data.filters)
);
searchRanges.setRangesValues(data);
};
const onButtonClick = async () => {
if (accountName !== "") {
const {
......@@ -76,21 +63,20 @@ const CommentsSearch: React.FC<CommentsSearchProps> = ({
? searchRanges.lastBlocksValue
: undefined,
lastTime: searchRanges.lastTimeUnitValue,
page: data?.page || 1,
page: commentPaginationPage,
rangeSelectKey: searchRanges.rangeSelectKey,
timeUnit: searchRanges.timeUnitSelectKey,
};
startCommentsSearch(commentSearchProps);
startCommentSearch(
commentSearchProps,
setCommentSearchProps,
setCommentPaginationPage,
setPreviousCommentSearchProps,
(val: "comment") => setLastSearchKey(val)
);
}
};
useEffect(() => {
if (!!data) {
setSearchValues(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<p className="ml-2">
......@@ -98,7 +84,7 @@ const CommentsSearch: React.FC<CommentsSearchProps> = ({
permlink.
</p>
<div className="flex flex-col">
<AutocompleteInput
<AutocompleteInput
value={accountName}
onChange={setAccountName}
placeholder="Author"
......@@ -140,7 +126,9 @@ const CommentsSearch: React.FC<CommentsSearchProps> = ({
disabled={!accountName || !permlink}
>
Search
{loading && <Loader2 className="ml-2 animate-spin h-4 w-4 ..." />}
{commentSearchDataLoading && (
<Loader2 className="ml-2 animate-spin h-4 w-4 ..." />
)}
</Button>
{!accountName && (
<label className="text-gray-300 dark:text-gray-500 ">
......
import React, { useState } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import BlockSearch from "./BlockSearch";
import AccountSearch from "./AccountSearch";
import CommentsPermlinkSearch from "./CommentPermlinkSearch";
import CommentsSearch from "./CommentsSearch";
const ACCORDION_SECTIONS = [
{ name: "Block Search", value: "block" },
{ name: "Account Search", value: "account" },
{ name: "Permalink Search", value: "comment-permlink" },
{ name: "Comment Search", value: "comment" },
];
const getAccordionContentByName = (name: string) => {
switch (name) {
case "Block Search":
return <BlockSearch />;
break;
case "Account Search":
return <AccountSearch />;
break;
case "Permalink Search":
return <CommentsPermlinkSearch />;
break;
case "Comment Search":
return <CommentsSearch />;
}
};
const renderAccordionItem = () => {
return ACCORDION_SECTIONS.map(({ name, value }) => {
return (
<AccordionItem
value={value}
key={value}
>
<AccordionTrigger className="p-3 mb-2">{name}</AccordionTrigger>
<AccordionContent className="px-2 flex flex-col gap-y-4">
{getAccordionContentByName(name)}
</AccordionContent>
</AccordionItem>
);
});
};
const SearchesAccordionCard = () => {
const [accordionValue, setAccordionValue] = useState<string>("block");
return (
<Card
className="mt-4"
data-testid="block-search-section"
>
<CardHeader>
<CardTitle>Search</CardTitle>
</CardHeader>
<CardContent>
<Accordion
type="single"
className="w-full"
value={accordionValue}
onValueChange={setAccordionValue}
>
{renderAccordionItem()}
</Accordion>
</CardContent>
</Card>
);
};
export default SearchesAccordionCard;
import { useRef } from "react";
import BlockSearchResults from "./searchesResults/BlockSearchResults";
import CommentPermlinkSearchResults from "./searchesResults/CommentPermlinkSearchResults";
import CommentSearchResults from "./searchesResults/CommentSearchResults";
import AccountSearchResults from "./searchesResults/AccountSearchResults";
import { useSearchesContext } from "@/contexts/SearchesContext";
import useBlockSearch from "@/hooks/api/homePage/useBlockSearch";
import useAccountOperations from "@/hooks/api/accountPage/useAccountOperations";
import useCommentSearch from "@/hooks/api/common/useCommentSearch";
import usePermlinkSearch from "@/hooks/api/common/usePermlinkSearch";
const SearchesResponseSection = () => {
const searchesRef = useRef<HTMLDivElement | null>(null);
const {
blockSearchProps,
accountOperationsSearchProps,
permlinkSearchProps,
commentSearchProps,
lastSearchKey,
} = useSearchesContext();
const { blockSearchData } = useBlockSearch(blockSearchProps);
const { accountOperations } = useAccountOperations(
accountOperationsSearchProps
);
const { permlinkSearchData } = usePermlinkSearch(permlinkSearchProps);
const { commentSearchData } = useCommentSearch(commentSearchProps);
return (
<div
className="pt-4 scroll-mt-16"
ref={searchesRef}
>
{blockSearchData && lastSearchKey === "block" && <BlockSearchResults />}
{accountOperations && lastSearchKey === "account" && (
<AccountSearchResults />
)}
{permlinkSearchData && lastSearchKey === "comment-permlink" && (
<CommentPermlinkSearchResults />
)}
{commentSearchData && lastSearchKey === "comment" && (
<CommentSearchResults />
)}
</div>
);
};
export default SearchesResponseSection;