diff --git a/components/searchRanges/SearchRanges.tsx b/components/searchRanges/SearchRanges.tsx index 70cabf48ae977262dee3adec0826124cdc50f655..81797cf83fa2d198aa162db06afbda5e8996ec8d 100644 --- a/components/searchRanges/SearchRanges.tsx +++ b/components/searchRanges/SearchRanges.tsx @@ -1,20 +1,17 @@ -import React, { useEffect, useState } from "react"; +import React, { useState, useEffect } 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"; + interface SearchRangesProps { rangesProps: SearchRangesResult; safeTimeRangeDisplay?: boolean; } -const SearchRanges: React.FC<SearchRangesProps> = ({ - rangesProps, - safeTimeRangeDisplay, -}) => { +const SearchRanges: React.FC<SearchRangesProps> = ({ rangesProps }) => { const { rangeSelectOptions, timeSelectOptions, @@ -38,79 +35,100 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ 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; + const [localLastBlocks, setLocalLastBlocks] = useState( + lastBlocksValue !== undefined ? String(lastBlocksValue) : "" + ); + const [localLastTimeUnit, setLocalLastTimeUnit] = useState( + lastTimeUnitValue !== undefined ? String(lastTimeUnitValue) : "" + ); + const [localFromBlock, setLocalFromBlock] = useState( + fromBlock !== undefined ? String(fromBlock) : "" + ); + const [localToBlock, setLocalToBlock] = useState( + toBlock !== undefined ? String(toBlock) : "" + ); - // Fetch the latest block number dynamically - let validated = true; - if (validateField) { - validated = validateField(e, numericValue); + useEffect(() => { + setLocalLastBlocks( + lastBlocksValue !== undefined ? String(lastBlocksValue) : "" + ); + setLocalLastTimeUnit( + lastTimeUnitValue !== undefined ? String(lastTimeUnitValue) : "" + ); + setLocalFromBlock(fromBlock !== undefined ? String(fromBlock) : ""); + setLocalToBlock(toBlock !== undefined ? String(toBlock) : ""); + }, [lastBlocksValue, lastTimeUnitValue, fromBlock, toBlock]); + + const sanitizeNumericInput = (value: string, allowDecimal = false) => { + let cleaned = allowDecimal + ? value.replace(/[^0-9.]/g, "") + : value.replace(/[^0-9]/g, ""); + + if (allowDecimal && cleaned.split(".").length > 2) { + const parts = cleaned.split("."); + cleaned = parts.shift() + "." + parts.join(""); } - validated ? fieldSetter(numericValue) : fieldSetter(null); + if (cleaned.length > 15) { + cleaned = cleaned.slice(0, 15); + } + return cleaned; }; - const validateToBlock = ( - e: React.FocusEvent<HTMLInputElement>, - value: number | undefined - ) => { - if (value !== undefined && value <= 0) { + const validateFromBlock = (numVal: number | undefined) => { + if (numVal !== undefined && numVal <= 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 = ""; + if (numVal && toBlock && !isNaN(numVal) && numVal > toBlock) { + setRangeError("From block must be less than To block"); return false; } return true; }; - const validateFromBlock = ( - e: React.FocusEvent<HTMLInputElement>, - value: number | undefined - ) => { - if (value !== undefined && value <= 0) { + const validateToBlock = (numVal: number | undefined) => { + if (numVal !== undefined && numVal <= 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 = ""; + if (numVal && fromBlock && !isNaN(numVal) && numVal < fromBlock) { + setRangeError("To block must be greater than From block"); return false; } return true; }; - const handleNumericInput = ( - e: React.ChangeEvent<HTMLInputElement>, - allowDecimal: boolean = false - ) => { - let cleanedValue = e.target.value; + const handleLastBlocksBlur = () => { + const val = localLastBlocks ? Number(localLastBlocks) : undefined; + setLastBlocksValue(val); + setRangeError(null); + }; - // 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 + const handleLastTimeUnitBlur = () => { + const val = localLastTimeUnit ? Number(localLastTimeUnit) : undefined; + setLastTimeUnitValue(val); + setRangeError(null); + }; - if (allowDecimal && cleanedValue.split(".").length > 2) { - cleanedValue = - cleanedValue.slice(0, cleanedValue.indexOf(".") + 1) + - cleanedValue.split(".").slice(1).join(""); // Remove extra decimals + const handleFromBlockBlur = () => { + const val = localFromBlock ? Number(localFromBlock) : undefined; + if (!validateFromBlock(val)) { + setFromBlock(undefined); + return; } + setFromBlock(val); + setRangeError(null); + }; - if (cleanedValue.length > 15) { - cleanedValue = cleanedValue.slice(0, 15); // Limit to 15 digits + const handleToBlockBlur = () => { + const val = localToBlock ? Number(localToBlock) : undefined; + if (!validateToBlock(val)) { + setToBlock(undefined); + return; } - - e.target.value = cleanedValue; + setToBlock(val); + setRangeError(null); }; useEffect(() => { @@ -130,22 +148,17 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ value={rangeSelectKey} > <SelectTrigger className="w-1/2 border-0 border-b-2 bg-theme text-text"> - { - rangeSelectOptions.find( - (selectOption) => selectOption.key === rangeSelectKey - )?.name - } + {rangeSelectOptions.find((opt) => opt.key === rangeSelectKey)?.name} </SelectTrigger> <SelectContent className="bg-theme text-text rounded-sm max-h-[31rem]"> - {rangeSelectOptions.map((selectOption, index) => ( + {rangeSelectOptions.map((option, idx) => ( <SelectItem className="text-center" - key={index} - value={selectOption.key} - defaultChecked={false} + key={idx} + value={option.key} data-testid="search-select-option" > - {selectOption.name} + {option.name} </SelectItem> ))} </SelectContent> @@ -153,90 +166,91 @@ const SearchRanges: React.FC<SearchRangesProps> = ({ {rangeSelectKey === "lastBlocks" && ( <div className="flex items-center"> - <div className="flex flex-col w-full"> - <Input - className="w-1/2 border-0 border-b-2 bg-theme" - type="text" // Use type="text" to allow custom validation - defaultValue={lastBlocksValue || ""} - onChange={(e) => handleNumericInput(e)} - onBlur={(e) => handleOnBlur(e, setLastBlocksValue, null)} - placeholder={"Last"} - /> - </div> + <Input + className="w-1/2 border-0 border-b-2 bg-theme" + type="text" + value={localLastBlocks} + onChange={(e) => + setLocalLastBlocks(sanitizeNumericInput(e.target.value)) + } + onBlur={handleLastBlocksBlur} + placeholder="Last" + /> </div> )} {rangeSelectKey === "lastTime" && ( - <> - <div className="flex items-center justify-center"> - <div className="flex flex-col w-full mr-2"> - <Input - type="text" - className="bg-theme border-0 border-b-2 text-text" - defaultValue={lastTimeUnitValue || ""} - onChange={(e) => handleNumericInput(e, true)} - onBlur={(e) => handleOnBlur(e, setLastTimeUnitValue, null)} - placeholder={"Last"} - /> - </div> - <Select onValueChange={setTimeUnitSelectKey}> - <SelectTrigger className="pl-2 bg-theme border-0 border-b-2 text-text"> - { - timeSelectOptions.find( - (selectOption) => selectOption.key === timeUnitSelectKey - )?.name - } - </SelectTrigger> - <SelectContent - className="bg-theme text-text rounded-sm max-h-[31rem]" - data-testid="select-time-option-units" - > - {timeSelectOptions.map((selectOption, index) => ( - <SelectItem - className="text-center" - key={index} - value={selectOption.key} - defaultChecked={false} - > - {selectOption.name} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - </> + <div className="flex items-center justify-center"> + <Input + type="text" + className="bg-theme border-0 border-b-2 text-text mr-2" + value={localLastTimeUnit} + onChange={(e) => + setLocalLastTimeUnit(sanitizeNumericInput(e.target.value, true)) + } + onBlur={handleLastTimeUnitBlur} + placeholder="Last" + /> + <Select + onValueChange={setTimeUnitSelectKey} + value={timeUnitSelectKey} + > + <SelectTrigger className="pl-2 bg-theme border-0 border-b-2 text-text"> + { + timeSelectOptions.find((opt) => opt.key === timeUnitSelectKey) + ?.name + } + </SelectTrigger> + <SelectContent className="bg-theme text-text rounded-sm max-h-[31rem]"> + {timeSelectOptions.map((option, index) => ( + <SelectItem + className="text-center" + key={index} + value={option.key} + > + {option.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> )} {rangeSelectKey === "blockRange" && ( <div className="flex items-center"> - <div className="flex flex-col w-full mr-2"> + <div className="mr-2 w-full"> <Input type="text" className="bg-theme border-0 border-b-2" data-testid="from-block-input" - defaultValue={fromBlock || ""} - onChange={(e) => handleNumericInput(e)} - onBlur={(e) => handleOnBlur(e, setFromBlock, validateFromBlock)} + value={localFromBlock} + onChange={(e) => + setLocalFromBlock(sanitizeNumericInput(e.target.value)) + } + onBlur={handleFromBlockBlur} placeholder="From" /> </div> - <div className="flex flex-col w-full"> + <div className="w-full"> <Input className="bg-theme border-0 border-b-2" data-testid="headblock-number" type="text" - defaultValue={toBlock || ""} - onChange={(e) => handleNumericInput(e)} - placeholder={"To"} - onBlur={(e) => handleOnBlur(e, setToBlock, validateToBlock)} + value={localToBlock} + onChange={(e) => + setLocalToBlock(sanitizeNumericInput(e.target.value)) + } + onBlur={handleToBlockBlur} + placeholder="To" /> </div> </div> )} + {rangeError && ( <ErrorMessage message={rangeError} - onClose={() => setRangeError(null)} // Close the error message + onClose={() => setRangeError(null)} timeout={3000} /> )}