diff --git a/src/client/components/LinkToGlobalItem.tsx b/src/client/components/LinkToGlobalItem.tsx new file mode 100644 index 0000000..6652f50 --- /dev/null +++ b/src/client/components/LinkToGlobalItem.tsx @@ -0,0 +1,206 @@ +import { Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { + useGlobalItem, + useGlobalItems, + useLinkItem, + useUnlinkItem, +} from "../hooks/useGlobalItems"; + +interface LinkToGlobalItemProps { + itemId: number; + linkedGlobalItemId?: number | null; +} + +export function LinkToGlobalItem({ + itemId, + linkedGlobalItemId, +}: LinkToGlobalItemProps) { + const [isOpen, setIsOpen] = useState(false); + const [searchInput, setSearchInput] = useState(""); + const [debouncedQuery, setDebouncedQuery] = useState(""); + + const linkItem = useLinkItem(); + const unlinkItem = useUnlinkItem(); + const { data: linkedItem } = useGlobalItem(linkedGlobalItemId ?? null); + const { data: searchResults, isLoading: isSearching } = useGlobalItems( + isOpen && debouncedQuery ? debouncedQuery : undefined, + ); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedQuery(searchInput); + }, 300); + return () => clearTimeout(timer); + }, [searchInput]); + + function handleLink(globalItemId: number) { + linkItem.mutate( + { itemId, globalItemId }, + { + onSuccess: () => { + setIsOpen(false); + setSearchInput(""); + setDebouncedQuery(""); + }, + }, + ); + } + + function handleUnlink() { + unlinkItem.mutate(itemId); + } + + // Already linked state + if (linkedGlobalItemId && linkedItem) { + return ( +
+
+
+ + + + + {linkedItem.brand} {linkedItem.model} + +
+ +
+
+ ); + } + + // Collapsed / trigger state + if (!isOpen) { + return ( + + ); + } + + // Open search state + return ( +
+
+
+ + Link to global catalog + + +
+ setSearchInput(e.target.value)} + placeholder="Search by brand or model..." + className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-gray-300" + autoFocus + /> +
+ + {/* Search results */} + {debouncedQuery && ( +
+ {isSearching ? ( +
+ Searching... +
+ ) : searchResults && searchResults.length > 0 ? ( +
+ {searchResults.map((item) => ( + + ))} +
+ ) : ( +
+ No items found +
+ )} +
+ )} +
+ ); +}