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

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

parent cb2e06a3
No related branches found
No related tags found
1 merge request!493Delrifai/#388 search bar changes
......@@ -6,15 +6,19 @@ import useOnClickOutside from "@/hooks/common/useOnClickOutside";
import useInputType from "@/hooks/api/common/useInputType";
import { cn } from "@/lib/utils";
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 {
value: string;
value: string | null;
onChange: (value: string) => void;
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
linkResult?: boolean;
required?: boolean;
addLabel?: boolean;
}
const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
......@@ -25,41 +29,90 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
className,
linkResult = false,
required = false, // Default value is false
addLabel = false,
}) => {
const [inputFocus, setInputFocus] = useState(false);
const [selectedResult, setSelectedResult] = useState(0);
const searchContainerRef = useRef(null);
const inputRef = useRef<HTMLInputElement>(null); // Ref for input field
const selectedResultRef = useRef<HTMLDivElement>(null); // Ref for selected result
const inputRef = useRef<HTMLInputElement>(null);
const selectedResultRef = useRef<HTMLDivElement>(null);
const [searchInputType, setSearchInputType] = useState<string>("");
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((value: string) => {
setSearchInputType(value + encodeURI("%"));
}, 300);
const debouncedSearch = useDebounce(
(value: string) => updateInput(trimAccountName(value)),
600
);
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>) => {
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);
}
};
// Close the search when clicking outside of the container
useOnClickOutside(searchContainerRef, () => setInputFocus(false));
useOnClickOutside(searchContainerRef, () => closeSearchBar());
//Reset Search Bar
const resetSearchBar = () => {
setInputFocus(false);
onChange("");
onChange(""); // Clear the input field
setSearchInputType("");
setSelectedResult(0);
setIsItemSelected(false);
};
const closeSearchBar = () => {
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>) => {
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") {
setSelectedResult((prev) =>
prev < inputTypeData.input_value.length - 1 ? prev + 1 : prev
......@@ -68,11 +121,28 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
if (event.key === "ArrowUp") {
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();
const selectedAccount = inputTypeData.input_value[selectedResult];
onChange(selectedAccount); // Update the input field with the selected account
closeSearchBar();
onChange(selectedAccount);
setIsItemSelected(true);
setInputFocus(true);
inputRef.current?.focus();
linkResult ? "" : closeSearchBar();
}
}
};
......@@ -101,77 +171,160 @@ const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
}, [selectedResult]);
// Render search results based on input type
const renderSearchData = (data: any, linkResult: boolean) => {
const inputValue = data.input_value;
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";
}
};
// If input_value is an array, we iterate over the array of accounts
if (Array.isArray(inputValue)) {
// Render search results based on input type
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 (
<div className="flex flex-col bg-white shadow-lg rounded-lg mt-1">
{inputValue.map((account, index) => (
<div
key={index}
ref={selectedResult === index ? selectedResultRef : null}
className={cn(
"px-2 py-2 text-sm cursor-pointer hover:bg-gray-100 flex items-center justify-between rounded-md",
{
"bg-gray-100": selectedResult === index,
"border-t border-gray-200": index > 0,
}
)}
onClick={() => {
onChange(account); // Update the input field with the selected account
inputRef.current?.focus(); // Focus the input field after selection
setInputFocus(true); // Reopen the suggestions on click
setSearchInputType(account); // Update the search type for new suggestions
}}
>
<div className="px-4 py-2">Account not found: {searchTerm}</div>
);
} else if (
inputTypeArray.includes(data.input_type) ||
(inputTypeArray.includes("account_name") &&
data.input_type === "account_name_array")
) {
if (
data.input_type === "account_name_array" &&
Array.isArray(inputValue)
) {
return (
<div className="autocomplete-result-container scrollbar-autocomplete">
{inputValue.map((account, index) => (
<div
key={index}
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 ? (
<Link href={`/@${account}`} className="w-full">
<span className="text-blue-600">{account}</span>
</Link>
<>
{addLabel && (
<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>
</div>
)}
{selectedResult === index && (
<Enter className="hidden md:inline" />
<>
{addLabel && (
<span className="autocomplete-result-label">
{capitalizeFirst(resultType)}&nbsp;
</span>
)}
<span className="autocomplete-result-highlight">
{data.input_value}
</span>
</>
)}
<Enter className="hidden md:inline text-explorer-dark-gray" />
</div>
))}
</div>
);
</div>
);
}
}
return null;
};
return (
<div ref={searchContainerRef} className={cn("relative", className)}>
<div className="flex items-center pr-2 z-50">
<Input
ref={inputRef} // Attach ref to input field
ref={inputRef}
className="border-0 w-full text-sm"
type="text"
placeholder={required ? `${placeholder} *` : placeholder} // Add asterisk if required
value={value} // Ensure the input field reflects the current value
onChange={handleInputChange} // When the value in the input changes
onFocus={() => setInputFocus(true)} // When the input is focused, show suggestions
onKeyDown={handleKeyDown} // Handle keyboard events
value={value || ""}
onChange={handleInputChange}
onFocus={() => setInputFocus(true)}
onKeyDown={handleKeyDown}
/>
{!!value.length ? (
{value && !!value.length ? (
<X className="cursor-pointer" onClick={() => resetSearchBar()} />
) : linkResult ? (
<Search />
) : null}
</div>
{inputFocus && value.length > 0 && !!inputTypeData?.input_value && (
<div
className="absolute
bg-white dark:bg-gray-800 w-full max-h-60 overflow-y-auto border border-gray-200 rounded-lg shadow-lg z-50"
>
{renderSearchData(inputTypeData, linkResult)}
</div>
)}
{inputFocus &&
value &&
value.length > 0 &&
!!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)}
</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