import { useForm } from "react-hook-form"; import type { FunctionComponent } from "react"; import { Fragment, useState } from "react"; import { onPromise, preventDefault } from "@/helpers"; import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types"; import { CheckIcon, ChevronUpDownIcon, LockClosedIcon, MagnifyingGlassIcon, ArrowPathIcon, } from "@heroicons/react/20/solid"; import { Listbox, ListboxButton, ListboxOptions, ListboxOption, Transition, } from "@headlessui/react"; import clsx from "clsx"; import type { Maybe } from "true-myth"; import { placeholders } from "@/constants"; /** * Props for the LookupInput component. */ type LookupInputProps = { isLoading?: boolean; /** * Callback function called when a type of registry is detected when a user changes their input. * @param type - The detected type of registry. * @returns A promise. */ onRegistry?: (type: TargetType) => Promise; /** * Callback function called when a user hits submit. * @param props - The submit props. * @returns A promise. */ onSubmit?: (props: SubmitProps) => Promise; /** * Callback function called when a user changes their input (text search) or explicitly changes the type of search. * @param target - The target object containing the search target and target type. * @returns Nothing. */ onChange?: (target: { target: string; targetType: TargetType | null; }) => void; detectedType: Maybe; }; const LookupInput: FunctionComponent = ({ isLoading, onSubmit, onChange, detectedType, }: LookupInputProps) => { const { register, handleSubmit, getValues } = useForm({ defaultValues: { target: "", // Not used at this time. followReferral: false, requestJSContact: false, }, }); /** * A mapping of available (simple) target types to their long-form human-readable names. */ const objectNames: Record = { auto: "Autodetect", domain: "Domain", ip: "IP/CIDR", // IPv4/IPv6 are combined into this option tld: "TLD", autnum: "AS Number", entity: "Entity Handle", registrar: "Registrar", url: "URL", json: "JSON", }; /** * Mapping of precise target types to their simplified short-form names. */ const targetShortNames: Record = { domain: "Domain", tld: "TLD", ip4: "IPv4", ip6: "IPv6", autnum: "ASN", entity: "Entity", registrar: "Registrar", url: "URL", json: "JSON", }; /** * Represents the selected value in the LookupInput component. */ const [selected, setSelected] = useState( "auto" ); /** * Retrieves the target type based on the provided value. * @param value - The value to retrieve the target type for. * @returns The target type as ObjectType or null. */ function retrieveTargetType(value?: string | null): TargetType | null { // If the value is null and the selected value is null, return null. if (value == null) value = selected; // 'auto' means 'do whatever' so we return null. if (value == "auto") return null; return value as TargetType; } const searchIcon = ( <> ); const searchInput = ( { if (onChange != undefined) onChange({ target: getValues("target"), // dropdown target will be pulled from state anyways, so no need to provide it here targetType: retrieveTargetType(null), }); }, })} /> ); const dropdown = ( { setSelected(value); if (onChange != undefined) onChange({ target: getValues("target"), // we provide the value as the state will not have updated yet for this context targetType: retrieveTargetType(value), }); }} disabled={isLoading} >
{/* Fetch special text for 'auto' mode, otherwise just use the options. */} {selected == "auto" ? ( // If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future. detectedType.isJust ? ( `Auto (${targetShortNames[detectedType.value]})` ) : ( objectNames["auto"] ) ) : ( <> {objectNames[selected]} )} {Object.entries(objectNames).map(([key, value]) => ( clsx( "relative cursor-default select-none py-2 pl-10 pr-4", focus ? "bg-zinc-800 text-zinc-300" : null ) } value={key} > {({ selected }) => ( <> {value} {selected ? ( ) : ( )} )} ))}
); return (
{searchIcon} {searchInput} {dropdown}
); }; export default LookupInput;