diff --git a/components/LastBlocksWidget.tsx b/components/LastBlocksWidget.tsx index 0c277888c32a018fb6ae2b2807ce1f3adedef2c8..ab1c561f824763834c32cc113e4ee2d482618065 100644 --- a/components/LastBlocksWidget.tsx +++ b/components/LastBlocksWidget.tsx @@ -36,6 +36,7 @@ type ChartBlockData = { virtual: number; }; + const CustomTooltip = ({ active, payload, @@ -48,28 +49,27 @@ const CustomTooltip = ({ if (active && payload && payload.length) { const totalOperations = payload.reduce((acc, pld) => acc + pld.value, 0); return ( - <div className="bg-theme dark:bg-theme p-2 rounded border border-explorer-light-gray"> - <p className="font-bold">{`Block ${label}`}</p> - <div className="my-2"> + <div className="data-box"> + <p className="font-bold text-xl">{`Block ${label}`}</p> + <div className="my-3 flex items-center"> <Image - className="rounded-full inline" + className="rounded-full" src={getHiveAvatarUrl(payload[0].payload.witness)} alt="avatar" - width={40} - height={40} + width={50} + height={50} /> - <p className="inline ml-4">{payload[0].payload.witness}</p> + <p className="ml-4 font-semibold">{payload[0].payload.witness}</p> </div> - <div>operations: {totalOperations}</div> - <div> + <div className="text-sm opacity-80">Operations: {totalOperations}</div> + <div className="mt-2 space-y-2"> {payload.map((pld, index) => ( - <div - key={index} - style={{ color: pld.fill }} - > - <div> - {pld.dataKey} {pld.value} - </div> + <div key={index} className="flex items-center"> + <div + className="w-4 h-4 rounded-full" + style={{ backgroundColor: pld.fill }} + /> + <span className="ml-2 text-sm">{`${pld.dataKey}: ${pld.value}`}</span> </div> ))} </div> @@ -80,6 +80,7 @@ const CustomTooltip = ({ return null; }; + const getOpsCount = (lastBlocks: Hive.LastBlocksTypeResponse[]) => { const opsCount: ChartBlockData[] = lastBlocks.map((block) => ({ name: block.block_num.toString(), @@ -216,7 +217,7 @@ const LastBlocksWidget: React.FC<LastBlocksWidgetProps> = ({ content={<CustomTooltip />} /> <Legend - wrapperStyle={{ position: "relative" }} + wrapperStyle={{ position: "relative" , marginLeft: "35px"}} align="center" /> <Bar diff --git a/components/home/CurrentBlockCard.tsx b/components/home/CurrentBlockCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2eaecb49d63c2ccb649a48fa2bd9bede85a76090 --- /dev/null +++ b/components/home/CurrentBlockCard.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import Hive from "@/types/Hive"; + + +import Image from "next/image"; +import Link from "next/link"; +import { getHiveAvatarUrl } from "@/utils/HiveBlogUtils"; +import { + faCube, + faBoxes, + faExchangeAlt, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface CurrentBlockCardProps { + blockDetails?: Hive.BlockDetails; + transactionCount?: number; + opcount?: number; + liveBlockNumber?: number | null; + timeDifferenceInSeconds ?:number | null; +} + +const CurrentBlockCard: React.FC<CurrentBlockCardProps> = ({ + blockDetails, + transactionCount, + opcount, + liveBlockNumber, + timeDifferenceInSeconds, +}) => { + + return ( + + <div className="data-box relative flex flex-col w-full min-h-[160px]"> + <div className="flex flex-col w-full"> + <div className="text-lg border-b">Current Block</div> + <div className="flex justify-between items-center mt-1 min-h-[35px] flex-wrap"> + {/* Block Number and Icon */} + <div className="flex items-center space-x-1"> + <FontAwesomeIcon icon={faCube} size="sm" /> + <Link + href={`/block/${blockDetails?.block_num}`} + data-testid="block-number-link" + > + <span className="text-link text-lg font-semibold"> + {liveBlockNumber + ? liveBlockNumber?.toLocaleString() + : blockDetails?.block_num + ? blockDetails?.block_num.toLocaleString() + : ""} + </span> + </Link> + </div> + + {/* Producer Info */} + <div className="flex flex-wrap items-center space-x-1 min-w-[140px] min-h-10 transition-opacity duration-500 ease-in-out opacity-100"> + <p className="text-sm">By:</p> + + {blockDetails?.producer_account && ( + <Link + className="flex items-center space-x-1 text-link" + href={`/@${blockDetails?.producer_account}`} + data-testid="current-witness-link" + > + <Image + className="rounded-full border-2 border-link" + src={getHiveAvatarUrl(blockDetails?.producer_account)} + alt="avatar" + width={30} + height={30} + /> + <p className="text-link text-sm font-semibold"> + {blockDetails?.producer_account} + </p> + </Link> + )} + </div> + </div> + {/* Time Difference */} + <div className="flex text-xs font-semibold text-explorer-red w-[65px] min-w-[65px] justify-end"> + {timeDifferenceInSeconds} secs ago + </div> + {/* Operations and Transactions Info */} + <div className="flex flex-col justify-end space-y-2 pt-4 min-h-[40px]"> + <div className="flex items-center justify-end"> + <div className="min-w-[120px]"> + <FontAwesomeIcon icon={faBoxes} size="xs" /> + <span className="ml-1">Operations: </span> + <span className="font-semibold text-sm"> + {opcount ? opcount : ""} + </span> + </div> + </div> + <div className="flex items-center justify-end"> + <div className="min-w-[120px]"> + <FontAwesomeIcon icon={faExchangeAlt} size="xs" /> + <span className="ml-1">Trxs: </span> + <span className="font-semibold text-sm"> + {transactionCount} + </span> + </div> + </div> + </div> + </div> + </div> + ); +}; + +export default CurrentBlockCard; diff --git a/components/home/HeadBlockCard.tsx b/components/home/HeadBlockCard.tsx index 3a1b79b2b224483add16f896bc5365d96bb247e7..1e4ccd68d70384876bc23369a4ba8890b05a7abc 100644 --- a/components/home/HeadBlockCard.tsx +++ b/components/home/HeadBlockCard.tsx @@ -1,12 +1,7 @@ -import { useState } from "react"; -import { Loader2 } from "lucide-react"; -import Image from "next/image"; -import Link from "next/link"; - +import { useState, useEffect, useMemo } from "react"; import { config } from "@/Config"; import Explorer from "@/types/Explorer"; import Hive from "@/types/Hive"; -import { getHiveAvatarUrl } from "@/utils/HiveBlogUtils"; import { getVestsToHiveRatio } from "@/utils/Calculations"; import { useUserSettingsContext } from "../../contexts/UserSettingsContext"; import useBlockchainSyncInfo from "@/hooks/common/useBlockchainSyncInfo"; @@ -19,6 +14,12 @@ import { import { getBlockDifference } from "./SyncInfo"; import { Toggle } from "../ui/toggle"; import { Card, CardContent, CardHeader } from "../ui/card"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faClock, +} from "@fortawesome/free-solid-svg-icons"; + +import CurrentBlockCard from "./CurrentBlockCard"; interface HeadBlockCardProps { headBlockCardData?: Explorer.HeadBlockCardData | any; @@ -33,6 +34,9 @@ const HeadBlockCard: React.FC<HeadBlockCardProps> = ({ blockDetails, opcount = 0, }) => { + const isBlockCardLoading = + !headBlockCardData || !headBlockCardData.headBlockDetails ? true : false; + const [hiddenPropertiesByCard, setHiddenPropertiesByCard] = useState<any>({ timeCard: true, supplyCard: true, @@ -40,8 +44,6 @@ const HeadBlockCard: React.FC<HeadBlockCardProps> = ({ }); const { settings, setSettings } = useUserSettingsContext(); - const vestsToHiveRatio = getVestsToHiveRatio(headBlockCardData); - const handleHideBlockchainDates = () => { setHiddenPropertiesByCard({ ...hiddenPropertiesByCard, @@ -76,109 +78,213 @@ const HeadBlockCard: React.FC<HeadBlockCardProps> = ({ const isLiveDataToggleDisabled = blockDifference > config.liveblockSecurityDifference || isLoading; + const blockchainTime = headBlockCardData?.headBlockDetails.blockchainTime; + const formattedBlockchainTime = blockchainTime + ?.replace(/\//g, "-") + .replace(" UTC", ""); + const blockchainDate = formattedBlockchainTime + ? new Date(formattedBlockchainTime).getTime() + : null; + + const [timeDifferenceInSeconds, setTimeDifferenceInSeconds] = useState< + number | null + >(null); + + useEffect(() => { + const calculateTimeDifference = () => { + if (!blockDetails?.created_at || !blockchainDate) { + setTimeDifferenceInSeconds(null); + return; + } + + const blockCreationDate = new Date(blockDetails.created_at).getTime(); + const timeDifference = Math.abs(blockchainDate - blockCreationDate); + + setTimeDifferenceInSeconds(Math.floor(timeDifference / 1000)); + }; + + calculateTimeDifference(); + + }, [blockDetails?.created_at, blockchainDate]); + + // refresh interval + const intervalTime = config.accountRefreshInterval; + + /*States to handle seamless update of blockNumber , blockChainTime, feedprice, and vests/hive ratio when liveData is on*/ + const [liveBlockchainTime, setLiveBlockchainTime] = useState<Date | null>( + null + ); + const [liveBlockNumber, setLiveBlockNumber] = useState<number | null>( + blockDetails?.block_num ?? null + ); + const [liveFeedPrice, setLiveFeedPrice] = useState<number | undefined>( + headBlockCardData?.headBlockDetails?.feedPrice + ); + const [liveVestsToHiveRatio, setLiveVestsToHiveRatio] = useState< + string | undefined + >(getVestsToHiveRatio(headBlockCardData)); + + // Update liveFeedPrice when feedPrice changes + useEffect(() => { + if (headBlockCardData?.headBlockDetails?.feedPrice) { + setLiveFeedPrice(headBlockCardData.headBlockDetails.feedPrice); + } + }, [headBlockCardData?.headBlockDetails?.feedPrice]); + + // Update liveVestsToHiveRatio + useEffect(() => { + const newVestsToHiveRatio = getVestsToHiveRatio(headBlockCardData); + if (newVestsToHiveRatio) { + setLiveVestsToHiveRatio(newVestsToHiveRatio); + } + }, [ + headBlockCardData + ]); + + /*Block Chain Time Update*/ + useEffect(() => { + if (!blockchainDate || !settings.liveData) return; + const initialTimeDifference = Date.now() - blockchainDate; + + setLiveBlockchainTime(new Date(blockchainDate + initialTimeDifference)); + + const intervalId = setInterval(() => { + setLiveBlockchainTime((prevTime) => { + if (!prevTime) return new Date(blockchainDate); + const currentTime = Date.now(); + const updatedTime = new Date( + blockchainDate + (currentTime - blockchainDate) + ); + return updatedTime; + }); + }, intervalTime); + + return () => clearInterval(intervalId); + }, [blockchainDate, settings.liveData , intervalTime]); + + /*Block Number Update*/ + useEffect(() => { + if (!blockDetails?.block_num || !settings.liveData) return; + + setLiveBlockNumber(blockDetails.block_num); + + const intervalId = setInterval(() => { + setLiveBlockNumber((prevBlockNum) => { + if (!prevBlockNum) return blockDetails.block_num; + return prevBlockNum; + }); + }, intervalTime); + + return () => clearInterval(intervalId); + }, [blockDetails?.block_num, settings.liveData,intervalTime]); + + /*Feed Price and Vest/Hive Ratio Update*/ + useEffect(() => { + if (!settings.liveData) return; + const intervalId = setInterval(() => { + // Update Feed Price only if it has changed + setLiveFeedPrice((prevFeedPrice) => { + const newFeedPrice = + headBlockCardData?.headBlockDetails?.feedPrice ?? 0; + if (prevFeedPrice !== newFeedPrice) { + return newFeedPrice; + } + return prevFeedPrice; + }); + + // Update Vests to Hive Ratio only if it has changed + setLiveVestsToHiveRatio((prevRatio) => { + const newRatio = getVestsToHiveRatio(headBlockCardData); + if (prevRatio !== newRatio) { + return newRatio; + } + return prevRatio; + }); + }, intervalTime); + + return () => clearInterval(intervalId); + }, [settings.liveData, headBlockCardData, intervalTime]); + return ( - <Card - className="col-span-4 md:col-span-1" - data-testid="head-block-card" - > - <CardHeader> - <Toggle - disabled={isLiveDataToggleDisabled} - checked={settings.liveData} - onClick={() => - setSettings({ - ...settings, - liveData: !settings.liveData, - }) - } - className="text-base" - leftLabel="Live data" - /> - <div className="text-explorer-turquoise text-2xl text-left"> - <Link - className="text-link" - href={`/block/${blockDetails?.block_num}`} - data-testid="block-number-link" - > - Block: {blockDetails?.block_num?.toLocaleString()} - </Link> + <Card className="col-span-4 md:col-span-1" data-testid="head-block-card"> + <CardHeader className="flex justify-between items-end py-2 border-b "> + {/* Blockchain Time and Live Data Toggle */} + <div className="flex flex-col items-end space-y-2"> + <div className="flex items-end space-x-2 text-[12px]"> + <FontAwesomeIcon icon={faClock} size="xl" /> + <span className="font-semibold">Blockchain Time:</span> + <span className="font-semibold text-right"> + {liveBlockchainTime + ? `${liveBlockchainTime + .toISOString() + .replace("T", " ") + .slice(0, 19)} UTC` + : blockchainTime ?? ""} + </span> + </div> + + <div className="mt-4"> + <Toggle + disabled={isLiveDataToggleDisabled} + checked={settings.liveData} + onClick={() => + setSettings({ + ...settings, + liveData: !settings.liveData, + }) + } + className="text-base" + leftLabel="Live data" + /> + </div> </div> </CardHeader> - <CardContent className="p-2"> - <div className="my-2">Operations per block : {opcount} </div> - {blockDetails?.producer_account && ( - <div className="flex"> - <p>Current witness : </p> - <Link - className="flex justify-between items-center min-h-[40px]" - href={`/@${blockDetails?.producer_account}`} - data-testid="current-witness-link" - > - <div className="flex"> - <p - className="text-link mx-2" - data-testid="current-witness-name" - > - {blockDetails?.producer_account} - </p> - <div className="min-w-[30px]"> - <Image - className="rounded-full border-2 border-link" - src={getHiveAvatarUrl(blockDetails?.producer_account)} - alt="avatar" - width={40} - height={40} - /> - </div> - </div> - </Link> + <CardContent className="p-4 space-y-4"> + {/* Other Information*/} + <div className="data-box"> + <div> + <span>Feed Price:</span> {liveFeedPrice} + </div> + <div> + <span>Vests To Hive Ratio:</span> {liveVestsToHiveRatio} VESTS </div> - )} - <div className="my-2"> - Feed Price : {headBlockCardData?.headBlockDetails.feedPrice ?? ""} - </div> - <div className="my-2"> - Vests To Hive Ratio : {vestsToHiveRatio} VESTS - </div> - <div> - Blockchain Time :{" "} - {!!headBlockCardData?.headBlockDetails.blockchainTime && - headBlockCardData?.headBlockDetails.blockchainTime} </div> + {/* Last Block Information */} + <CurrentBlockCard + blockDetails={blockDetails} + transactionCount={transactionCount} + opcount={opcount} + timeDifferenceInSeconds={timeDifferenceInSeconds} + liveBlockNumber={liveBlockNumber} + /> + <div> - <div className="text-center my-4 text-xl">Properties</div> - - {!headBlockCardData || !headBlockCardData.headBlockDetails ? ( - <div className="flex justify-center m-2"> - <Loader2 className="animate-spin mt-1 text-white h-12 w-12 ml-3 ..." /> - </div> - ) : ( - <> - <HeadBlockPropertyCard - parameters={fundAndSupplyParameters} - header="Fund and Supply" - isParamsHidden={hiddenPropertiesByCard.supplyCard} - handleHideParams={handleHideSupplyParams} - /> - <HeadBlockPropertyCard - parameters={hiveParameters} - header="Hive Parameters" - isParamsHidden={hiddenPropertiesByCard.hiveParamsCard} - handleHideParams={handleHideHiveParams} - /> - <HeadBlockPropertyCard - parameters={blockchainDates} - header="Blockchain Dates" - isParamsHidden={hiddenPropertiesByCard.timeCard} - handleHideParams={handleHideBlockchainDates} - /> - </> - )} + <HeadBlockPropertyCard + parameters={fundAndSupplyParameters} + header="Fund and Supply" + isParamsHidden={hiddenPropertiesByCard.supplyCard} + handleHideParams={handleHideSupplyParams} + isLoading={isBlockCardLoading} + /> + <HeadBlockPropertyCard + parameters={hiveParameters} + header="Hive Parameters" + isParamsHidden={hiddenPropertiesByCard.hiveParamsCard} + handleHideParams={handleHideHiveParams} + isLoading={isBlockCardLoading} + /> + <HeadBlockPropertyCard + parameters={blockchainDates} + header="Blockchain Dates" + isParamsHidden={hiddenPropertiesByCard.timeCard} + handleHideParams={handleHideBlockchainDates} + isLoading={isBlockCardLoading} + /> </div> </CardContent> </Card> ); }; -export default HeadBlockCard; \ No newline at end of file +export default HeadBlockCard; diff --git a/components/home/HeadBlockPropertyCard.tsx b/components/home/HeadBlockPropertyCard.tsx index b6906c807a37cd8330d55c23273e5619693a29b6..7aaed66b334bfa8e92bf96bd703a199d43f47243 100644 --- a/components/home/HeadBlockPropertyCard.tsx +++ b/components/home/HeadBlockPropertyCard.tsx @@ -1,15 +1,15 @@ -import { Fragment } from "react"; import { ArrowDown, ArrowUp } from "lucide-react"; - import { convertUTCDateToLocalDate } from "@/utils/TimeUtils"; import useDynamicGlobal from "@/hooks/api/homePage/useDynamicGlobal"; import { Table, TableBody, TableRow, TableCell } from "../ui/table"; import { - fundAndSupplyParameters, - hiveParameters, - blockchainDates, +fundAndSupplyParameters, +hiveParameters, +blockchainDates, } from "./headBlockParameters"; +import { Loader2 } from "lucide-react"; + const cardNameMap = new Map([ ["feedPrice", "Feed price"], ["blockchainTime", "Blockchain time"], @@ -53,6 +53,7 @@ interface HeadBlockPropertyCardProps { header: string; isParamsHidden: boolean; handleHideParams: () => void; + isLoading: boolean; } const buildTableBody = ( @@ -61,18 +62,16 @@ const buildTableBody = ( dynamicGlobalData: any ) => { return parameters.map((param: string, index: number) => ( - <Fragment key={index}> - <TableRow className={"border-b border-gray-700 hover:bg-inherit"}> - <TableCell>{cardNameMap.get(param)}</TableCell> - <TableCell> - {header === "Blockchain Dates" + <TableRow key={index} className="border-b border-gray-700 hover:bg-inherit"> + <TableCell>{cardNameMap.get(param)}</TableCell> + <TableCell> + {header === "Blockchain Dates" ? convertUTCDateToLocalDate( dynamicGlobalData?.headBlockDetails[param] ) - : dynamicGlobalData?.headBlockDetails[param]} - </TableCell> - </TableRow> - </Fragment> + : dynamicGlobalData?.headBlockDetails[param]} + </TableCell> + </TableRow> )); }; @@ -81,31 +80,32 @@ const HeadBlockPropertyCard: React.FC<HeadBlockPropertyCardProps> = ({ header, isParamsHidden, handleHideParams, + isLoading, }) => { const { dynamicGlobalData } = useDynamicGlobal() as any; return ( - <div - className="bg-theme py-1 rounded-[6px]" - data-testid="expandable-list" - > - <div - onClick={handleHideParams} - className="h-full flex justify-between align-center py-2 hover:bg-rowHover cursor-pointer px-2" - > + <div className="bg-theme py-1 rounded-[6px] data-box" data-testid="expandable-list" style={{ overflowX: "auto", width: "100%" }}> + <div onClick={handleHideParams} className="h-full w-full flex items-center justify-between py-1 cursor-pointer px-1"> <div className="text-lg">{header}</div> - {isParamsHidden ? <ArrowDown /> : <ArrowUp />} - </div> - <div - hidden={isParamsHidden} - data-testid="conntent-expandable-list" - > - <Table> - <TableBody> - {buildTableBody(parameters, header, dynamicGlobalData)} - </TableBody> - </Table> + <div>{isParamsHidden ? <ArrowDown /> : <ArrowUp />}</div> </div> + + {isLoading && !isParamsHidden ? ( + <div className="flex justify-center w-full"> + <Loader2 className="animate-spin mt-1 text-white h-8 w-8" /> + </div> + ) : ( + <div hidden={isParamsHidden} data-testid="content-expandable-list"> + <div style={{ overflowX: "auto" }}> + <Table className="min-w-full"> + <TableBody> + {dynamicGlobalData?.headBlockDetails && buildTableBody(parameters, header, dynamicGlobalData)} + </TableBody> + </Table> + </div> + </div> + )} </div> ); }; diff --git a/components/searchRanges/SearchRanges.tsx b/components/searchRanges/SearchRanges.tsx index d4c7660662b15ba1919bd996604f0413eb5c274b..8da6b19ec9ffe66a0059b35893e789fac7e49e0e 100644 --- a/components/searchRanges/SearchRanges.tsx +++ b/components/searchRanges/SearchRanges.tsx @@ -1,12 +1,11 @@ -import React, { useEffect , useState } from "react"; +import React, { useEffect, useState } from "react"; import moment from "moment"; import { SearchRangesResult } from "../../hooks/common/useSearchRanges"; import { Select, SelectContent, SelectTrigger, SelectItem } from "../ui/select"; import { Input } from "../ui/input"; import DateTimePicker from "../DateTimePicker"; -import ErrorMessage from "../ErrorMessage";// Import the ErrorMessage component - +import ErrorMessage from "../ErrorMessage"; // Import the ErrorMessage component interface SearchRangesProps { rangesProps: SearchRangesResult; safeTimeRangeDisplay?: boolean; @@ -37,23 +36,77 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ setLastTimeUnitValue, } = rangesProps; - // Validate and update numeric values + + const [rangeError, setRangeError] = useState<string | null>(null); + + const handleOnBlur = ( + e: React.FocusEvent<HTMLInputElement>, + fieldSetter: Function, + validateField: Function | null + ) => { + const value = e.target.value; + const numericValue = value ? Number(value) : undefined; + + // Fetch the latest block number dynamically + let validated = true; + if (validateField) { + validated = validateField(e, numericValue); + } + + validated ? fieldSetter(numericValue) : fieldSetter(null); + }; + + const validateToBlock = ( + e: React.FocusEvent<HTMLInputElement>, + value: number | undefined, + ) => { + if (value !== undefined && value <= 0) { + setRangeError("Block Number must be a positive number"); + e.target.value = ""; + return false; + } + if (value && fromBlock && !isNaN(value) && value < fromBlock) { + setRangeError("To block must be greater than From block"); + e.target.value = ""; + return false; + } + return true; + }; + + const validateFromBlock = ( + e: React.FocusEvent<HTMLInputElement>, + value: number | undefined, + ) => { + if (value !== undefined && value <= 0) { + setRangeError("Block Number must be a positive number"); + e.target.value = ""; + return false; + } + if (value && toBlock && !isNaN(value) && value > toBlock) { + setRangeError("From block must be less than To block"); + e.target.value = ""; + return false; + } + return true; + }; + const handleNumericInput = ( e: React.ChangeEvent<HTMLInputElement>, allowDecimal: boolean = false ) => { let cleanedValue = e.target.value; - + // Clean the value based on the logic cleanedValue = allowDecimal ? cleanedValue.replace(/[^0-9.]/g, "") // Allow numbers and decimal point : cleanedValue.replace(/[^0-9]/g, ""); // Only allow numbers - + if (allowDecimal && cleanedValue.split(".").length > 2) { - cleanedValue = cleanedValue.slice(0, cleanedValue.indexOf(".") + 1) + - cleanedValue.split(".").slice(1).join(""); // Remove extra decimals + cleanedValue = + cleanedValue.slice(0, cleanedValue.indexOf(".") + 1) + + cleanedValue.split(".").slice(1).join(""); // Remove extra decimals } - + if (cleanedValue.length > 15) { cleanedValue = cleanedValue.slice(0, 15); // Limit to 15 digits } @@ -68,10 +121,9 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ ) { setStartDate(moment(startDate).subtract(1, "hours").toDate()); } - //eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [startDate, endDate]); - const [blockRangeError, setBlockRangeError] = useState<string | null>(null); // Error state for block range validation return ( <div className="py-2 flex flex-col gap-y-2"> @@ -109,11 +161,7 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ type="text" // Use type="text" to allow custom validation defaultValue={lastBlocksValue || ""} onChange={(e) => handleNumericInput(e)} - onBlur={(e) => { - const value = e.target.value; - const numericValue = value ? Number(value) : undefined; - setLastBlocksValue(numericValue); - }} + onBlur={(e) => handleOnBlur(e,setLastBlocksValue,null)} placeholder={"Last"} /> </div> @@ -129,11 +177,7 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ className="bg-theme border-0 border-b-2 text-text" defaultValue={lastTimeUnitValue || ""} onChange={(e) => handleNumericInput(e,true)} - onBlur={(e) => { - const value = e.target.value; - const numericValue = value ? Number(value) : undefined; - setLastTimeUnitValue(numericValue); - }} + onBlur={(e) => handleOnBlur(e,setLastTimeUnitValue,null)} placeholder={"Last"} /> </div> @@ -174,11 +218,7 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ data-testid="from-block-input" defaultValue={fromBlock || ""} onChange={(e) => handleNumericInput(e)} - onBlur={(e) => { - const value = e.target.value; - const numericValue = value ? Number(value) : undefined; - setFromBlock(numericValue); - }} + onBlur={(e) => handleOnBlur(e,setFromBlock,validateFromBlock)} placeholder="From" /> </div> @@ -190,35 +230,15 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ defaultValue={toBlock || ""} onChange={(e) => handleNumericInput(e)} placeholder={"To"} - onBlur={(e: React.FocusEvent<HTMLInputElement>) => { - const value = e.target.value; - const numericValue = value ? Number(value) : undefined; - if ( - numericValue && - fromBlock && - !isNaN(numericValue) && - !isNaN(Number(fromBlock)) && - numericValue < Number(fromBlock) - ) { - setBlockRangeError("To block must be greater than From block"); - e.target.value = ""; - } else if (numericValue !=undefined && numericValue <= 0 && fromBlock) { - setBlockRangeError("To block must be greater than From block"); - e.target.value = ""; - } else { - setToBlock(numericValue); - setBlockRangeError(null); - } - } - } + onBlur={(e) => handleOnBlur(e,setToBlock,validateToBlock)} /> </div> </div> )} - {blockRangeError && ( + {rangeError && ( <ErrorMessage - message={blockRangeError} - onClose={() => setBlockRangeError(null)} // Close the error message + message={rangeError} + onClose={() => setRangeError(null)} // Close the error message timeout={3000} /> )} diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx index 994422cc8166af4b38ce6a5e78510b2906721cd3..e38f53158b92ed4847d30f59f25e1125b9a5e8d5 100644 --- a/components/ui/toggle.tsx +++ b/components/ui/toggle.tsx @@ -26,13 +26,13 @@ const Toggle: React.FC<ToggleProps> = ({ {leftLabel && <p>{leftLabel}</p>} <div className={cn( - "w-10 h-5 rounded-3xl border-2 invalid border-white relative", + "w-10 h-5 rounded-3xl border-2 invalid relative border-black dark:border-white", { "cursor-pointer": !disabled, "bg-green-600": checked, "bg-transparent": !checked, "border-gray-700": disabled && !checked, - "border-black dark:border-white": !disabled, +//"border-black dark:border-white": !disabled, } )} onClick={!disabled ? onClick : undefined} diff --git a/hooks/common/useSearchRanges.ts b/hooks/common/useSearchRanges.ts index abe0c55fffbf5a7ce5a518d793c523e052c39b14..7b92f2971061d59a40cc016e17d881588956b82b 100644 --- a/hooks/common/useSearchRanges.ts +++ b/hooks/common/useSearchRanges.ts @@ -142,6 +142,26 @@ const useSearchRanges = (defaultSelectKey: string = "lastTime") => { .milliseconds(0) .toDate(); } + + //Validate that payloadStartDate is a valid + if (payloadStartDate && (isNaN(payloadStartDate?.getTime()) || payloadStartDate?.getTime() <= 0)) { + payloadStartDate = undefined; //fallback + } + //Validate that payloadToBlock does not exceed latest headblock number + if (payloadToBlock) { + const currentHeadBlockNumber = await checkTemporaryHeadBlockNumber(); + if (payloadToBlock > currentHeadBlockNumber) { + payloadToBlock = currentHeadBlockNumber; //fallback + } + } + if(payloadFromBlock) + { + const currentHeadBlockNumber = await checkTemporaryHeadBlockNumber(); + if (payloadFromBlock > currentHeadBlockNumber) { + payloadFromBlock = currentHeadBlockNumber; //fallback + } + } + return { payloadFromBlock, payloadToBlock, diff --git a/lib/utils.ts b/lib/utils.ts index a92e97da866a411e04d9b2ab98c7450333708fd2..8650cd413186e62b111ae9d038b8925ac835f2f6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -84,7 +84,7 @@ export const formatPercent = (numberToFormat: number): string => { export const convertOperationResultsToTableOperations = ( operations: Hive.OperationResponse[] ): Explorer.OperationForTable[] => { - return operations.map((operation) => ({ + return operations?.map((operation) => ({ operation: operation.op, blockNumber: operation.block, trxId: operation.trx_id, diff --git a/pages/index.tsx b/pages/index.tsx index 06e2acfdb16f6700308eb24c6e8f1ca5f42634c1..15d89d2724f38664e5635851e1849fc740900e53 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -39,6 +39,11 @@ export default function Home() { useDynamicGlobal(headBlockNum).dynamicGlobalData; const headBlockData = useHeadBlock(headBlockNum).headBlockData; const { blockOperations } = useBlockOperations(headBlockNum || 0); + + + // Filter operations that have a trx_id + const trxOperations = blockOperations?.operations_result.filter((operation) => operation.trx_id); + const opcount = blockOperations?.operations_result?.length || 0; const strokeColor = theme === "dark" ? "#FFF" : "#000"; @@ -51,7 +56,7 @@ export default function Home() { <div className="grid grid-cols-4 text-white px-2 w-full gap-3"> <HeadBlockCard headBlockCardData={dynamicGlobalQueryData} - transactionCount={blockOperations?.operations_result?.length} + transactionCount={ trxOperations?.length} blockDetails={headBlockData} opcount={opcount} /> diff --git a/styles/theme.css b/styles/theme.css index beb7e0802df10c7a7318ce0648d51d3853925007..ad27dfcc9a6de5b91eaa6480dbbf052eaa0b00e8 100644 --- a/styles/theme.css +++ b/styles/theme.css @@ -319,4 +319,32 @@ pre { font-size: 1rem; } -/*End Sync Dialog Styles*/ \ No newline at end of file +/*End Sync Dialog Styles*/ + +/* Data Box Styling */ +.data-box { + background-color: var(--color-extra-light-gray); + border-radius: 12px; + padding: 10px; + margin: 6px 0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + color: var(--color-text); + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: flex-start; + border: 1px solid var(--color-light-gray); /* Fine border */ + transition: all 0.3s ease-in-out; + flex-direction: column; /* Stack elements vertically */ +} + +.data-box:hover { + background-color: var(--color-row-hover); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.15); /* Elevated shadow */ + transform: translateY(-3px); /* Slight lift on hover */ +} + +.data-box span { + font-weight: bold; + font-size: 14px; +} \ No newline at end of file