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

Issue #388 - Update Autocomplete so that it can do multiple searches

parent 7b3e4fbc
No related branches found
No related tags found
No related merge requests found
Pipeline #110684 failed
...@@ -6,15 +6,19 @@ import useOnClickOutside from "@/hooks/common/useOnClickOutside"; ...@@ -6,15 +6,19 @@ import useOnClickOutside from "@/hooks/common/useOnClickOutside";
import useInputType from "@/hooks/api/common/useInputType"; import useInputType from "@/hooks/api/common/useInputType";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import { trimAccountName } from "@/utils/StringUtils";
import Hive from "@/types/Hive";
import { capitalizeFirst } from "@/utils/StringUtils";
import Router, { useRouter } from "next/router";
interface AutocompleteInputProps { interface AutocompleteInputProps {
value: string; value: string | null;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder: string; placeholder: string;
inputType: string; // The input type (e.g., 'account_name', 'block', 'transaction') inputType: string | string[]; // The input type (e.g., 'account_name', 'block', 'transaction')
className?: string; // Optional custom className for styling className?: string; // Optional custom className for styling
linkResult?: boolean; linkResult?: boolean;
required?: boolean; required?: boolean;
addLabel?: boolean;
} }
const AutocompleteInput: React.FC<AutocompleteInputProps> = ({ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
...@@ -25,41 +29,90 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({ ...@@ -25,41 +29,90 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
className, className,
linkResult = false, linkResult = false,
required = false, // Default value is false required = false, // Default value is false
addLabel = false,
}) => { }) => {
const [inputFocus, setInputFocus] = useState(false); const [inputFocus, setInputFocus] = useState(false);
const [selectedResult, setSelectedResult] = useState(0); const [selectedResult, setSelectedResult] = useState(0);
const searchContainerRef = useRef(null); const searchContainerRef = useRef(null);
const inputRef = useRef<HTMLInputElement>(null); // Ref for input field const inputRef = useRef<HTMLInputElement>(null);
const selectedResultRef = useRef<HTMLDivElement>(null); // Ref for selected result const selectedResultRef = useRef<HTMLDivElement>(null);
const [searchInputType, setSearchInputType] = useState<string>(""); const [searchInputType, setSearchInputType] = useState<string>("");
const { inputTypeData } = useInputType(searchInputType); const { inputTypeData } = useInputType(searchInputType);
const [searchTerm, setSearchTerm] = useState("");
const router = useRouter();
const [isItemSelected, setIsItemSelected] = useState(false);
const updateInput = async (value: string) => {
setSearchInputType(value);
};
// Debounce the search input to avoid making too many requests const debouncedSearch = useDebounce(
const debouncedSearch = useDebounce((value: string) => { (value: string) => updateInput(trimAccountName(value)),
setSearchInputType(value + encodeURI("%")); 600
}, 300); );
const isNumeric = (value: string): boolean => {
return /^\d+$/.test(value);
};
const isHash = (value: string): boolean => {
return /^[a-fA-F0-9]{40}$/.test(value);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value); onChange(e.target.value);
setSearchTerm(e.target.value);
if(!isNumeric(e.target.value) && !isHash(e.target.value))
{
debouncedSearch(e.target.value + encodeURI("%"));
}else
{
debouncedSearch(e.target.value); debouncedSearch(e.target.value);
}
}; };
// Close the search when clicking outside of the container // Close the search when clicking outside of the container
useOnClickOutside(searchContainerRef, () => setInputFocus(false)); useOnClickOutside(searchContainerRef, () => closeSearchBar());
//Reset Search Bar
const resetSearchBar = () => { const resetSearchBar = () => {
setInputFocus(false); setInputFocus(false);
onChange(""); onChange(""); // Clear the input field
setSearchInputType(""); setSearchInputType("");
setSelectedResult(0); setSelectedResult(0);
setIsItemSelected(false);
}; };
const closeSearchBar = () => { const closeSearchBar = () => {
setInputFocus(false); setInputFocus(false);
}; };
//Ensure cleaning the searchbar when navigating away
useEffect(() => {
const handleRouteChange = () => {
resetSearchBar();
};
router.events.on("routeChangeStart", handleRouteChange);
// Cleanup the event listener on unmount
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events]);
//Handle keyboard events
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (inputFocus && inputTypeData?.input_value?.length) { if (inputFocus && inputTypeData?.input_value) {
let selectedAccount;
if (Array.isArray(inputTypeData.input_value)) {
selectedAccount = inputTypeData.input_value[selectedResult];
} else {
selectedAccount = inputTypeData.input_value;
}
if (event.key === "ArrowDown") { if (event.key === "ArrowDown") {
setSelectedResult((prev) => setSelectedResult((prev) =>
prev < inputTypeData.input_value.length - 1 ? prev + 1 : prev prev < inputTypeData.input_value.length - 1 ? prev + 1 : prev
...@@ -68,11 +121,28 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({ ...@@ -68,11 +121,28 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
if (event.key === "ArrowUp") { if (event.key === "ArrowUp") {
setSelectedResult((prev) => (prev > 0 ? prev - 1 : prev)); setSelectedResult((prev) => (prev > 0 ? prev - 1 : prev));
} }
if (event.key === "Enter" || event.key === "Tab") { if (event.key === "Enter") {
if (isItemSelected && linkResult) {
const href =
inputTypeData.input_type === "account_name" || inputTypeData.input_type === "account_name_array"
? `/@${selectedAccount}`
: `/${getResultTypeHeader(inputTypeData)}/${selectedAccount}`;
router.push(href).then(() => {
closeSearchBar();
resetSearchBar();
});
} else if (!linkResult) {
onChange(selectedAccount);
closeSearchBar();
}
}
if (event.key === "Tab") {
event.preventDefault(); event.preventDefault();
const selectedAccount = inputTypeData.input_value[selectedResult]; onChange(selectedAccount);
onChange(selectedAccount); // Update the input field with the selected account setIsItemSelected(true);
closeSearchBar(); setInputFocus(true);
inputRef.current?.focus();
linkResult ? "" : closeSearchBar();
} }
} }
}; };
...@@ -101,77 +171,160 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({ ...@@ -101,77 +171,160 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
}, [selectedResult]); }, [selectedResult]);
// Render search results based on input type // Render search results based on input type
const renderSearchData = (data: any, linkResult: boolean) => { const getResultTypeHeader = (result: Hive.InputTypeResponse) => {
const inputValue = data.input_value; switch (result.input_type) {
case "block_num":
return "block";
case "transaction_hash":
return "transaction";
case "block_hash":
return "block";
default:
return "account";
}
};
// If input_value is an array, we iterate over the array of accounts // Render search results based on input type
if (Array.isArray(inputValue)) { const renderSearchData = (
data: Hive.InputTypeResponse,
linkResult: boolean
) => {
const inputValue = data.input_value;
const inputTypeArray = Array.isArray(inputType) ? inputType : [inputType];
const resultType = getResultTypeHeader(data);
if (data.input_type === "invalid_input") {
return ( return (
<div className="flex flex-col bg-white shadow-lg rounded-lg mt-1"> <div className="px-4 py-2">Account not found: {searchTerm}</div>
{inputValue.map((account, index) => ( );
<div } else if (
key={index} inputTypeArray.includes(data.input_type) ||
ref={selectedResult === index ? selectedResultRef : null} (inputTypeArray.includes("account_name") &&
className={cn( data.input_type === "account_name_array")
"px-2 py-2 text-sm cursor-pointer hover:bg-gray-100 flex items-center justify-between rounded-md", ) {
{ if (
"bg-gray-100": selectedResult === index, data.input_type === "account_name_array" &&
"border-t border-gray-200": index > 0, Array.isArray(inputValue)
} ) {
)} return (
onClick={() => { <div className="autocomplete-result-container scrollbar-autocomplete">
onChange(account); // Update the input field with the selected account {inputValue.map((account, index) => (
inputRef.current?.focus(); // Focus the input field after selection <div
setInputFocus(true); // Reopen the suggestions on click key={index}
setSearchInputType(account); // Update the search type for new suggestions ref={selectedResult === index ? selectedResultRef : null}
}} className={cn(
> "autocomplete-result-item hover:bg-explorer-light-gray",
{
"bg-explorer-light-gray": selectedResult === index,
"autocomplete-result-item": index > 0,
}
)}
onClick={() => {
onChange(account);
inputRef.current?.focus();
setInputFocus(true);
setSearchInputType(account);
}}
>
{linkResult ? (
<>
{addLabel && (
<span className="autocomplete-result-label">
{capitalizeFirst(resultType)}:&nbsp;
</span>
)}
<Link
href={`/@${account}`}
className="autocomplete-result-link"
>
<span className="autocomplete-result-link">
{account}
</span>
</Link>
</>
) : (
<span className="autocomplete-result-link">
{addLabel && (
<span>{capitalizeFirst(resultType)}::&nbsp;</span>
)}
{account}
</span>
)}
{selectedResult === index && (
<Enter className="hidden md:inline" />
)}
</div>
))}
</div>
);
} else {
const href =
resultType === "account"
? `/@${data.input_value}`
: `/${resultType}/${data.input_value}`;
return (
<div className="autocomplete-result-container scrollbar-autocomplete">
<div className="autocomplete-result-item">
{linkResult ? ( {linkResult ? (
<Link href={`/@${account}`} className="w-full"> <>
<span className="text-blue-600">{account}</span> {addLabel && (
</Link> <span className="autocomplete-result-label">
{capitalizeFirst(resultType)}&nbsp;
</span>
)}
<Link href={href} data-testid="">
<span className="autocomplete-result-link">
{data.input_value}
</span>
</Link>
</>
) : ( ) : (
<div className="w-full"> <>
<span className="text-blue-600">{account}</span> {addLabel && (
</div> <span className="autocomplete-result-label">
)} {capitalizeFirst(resultType)}&nbsp;
{selectedResult === index && ( </span>
<Enter className="hidden md:inline" /> )}
<span className="autocomplete-result-highlight">
{data.input_value}
</span>
</>
)} )}
<Enter className="hidden md:inline text-explorer-dark-gray" />
</div> </div>
))} </div>
</div> );
); }
} }
return null;
}; };
return ( return (
<div ref={searchContainerRef} className={cn("relative", className)}> <div ref={searchContainerRef} className={cn("relative", className)}>
<div className="flex items-center pr-2 z-50"> <div className="flex items-center pr-2 z-50">
<Input <Input
ref={inputRef} // Attach ref to input field ref={inputRef}
className="border-0 w-full text-sm" className="border-0 w-full text-sm"
type="text" type="text"
placeholder={required ? `${placeholder} *` : placeholder} // Add asterisk if required placeholder={required ? `${placeholder} *` : placeholder} // Add asterisk if required
value={value} // Ensure the input field reflects the current value value={value || ""}
onChange={handleInputChange} // When the value in the input changes onChange={handleInputChange}
onFocus={() => setInputFocus(true)} // When the input is focused, show suggestions onFocus={() => setInputFocus(true)}
onKeyDown={handleKeyDown} // Handle keyboard events onKeyDown={handleKeyDown}
/> />
{!!value.length ? ( {value && !!value.length ? (
<X className="cursor-pointer" onClick={() => resetSearchBar()} /> <X className="cursor-pointer" onClick={() => resetSearchBar()} />
) : linkResult ? ( ) : linkResult ? (
<Search /> <Search />
) : null} ) : null}
</div> </div>
{inputFocus && value.length > 0 && !!inputTypeData?.input_value && ( {inputFocus &&
<div value &&
className="absolute value.length > 0 &&
bg-white dark:bg-gray-800 w-full max-h-60 overflow-y-auto border border-gray-200 rounded-lg shadow-lg z-50" !!inputTypeData?.input_value && (
> <div className="absolute bg-theme dark:bg-theme w-full max-h-60 border border-gray-200 rounded-lg shadow-lg z-50 overflow-hidden">
{renderSearchData(inputTypeData, linkResult)} {renderSearchData(inputTypeData, linkResult)}
</div> </div>
)} )}
</div> </div>
); );
}; };
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment