"use client"; import { SearchIcon } from "@/app/utils/svgs/searchIcon"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import React, { useEffect, useRef, useState } from "react"; import InputField from "../../reuseableUI/defaultInputField"; import { ArrowUpIcon } from "@/app/utils/svgs/arrowUpIcon"; import { cn, getFullImageUrl } from "@/app/utils/functions"; import { useYmmStore } from "@/store/useYmmStore"; import { partsLogicClient, type PLSearchProductsResponse as PLResp, } from "@/lib/client/partslogic"; import { saleorGlobalSearch, type GlobalSearchProduct, type GlobalSearchCategory, type GlobalSearchProductType, } from "@/lib/client/saleor"; const Search = ({ className }: { className?: string }) => { const router = useRouter(); const pathname = usePathname(); const isYmmActive = useYmmStore((state) => state.isYmmActive); const searchParams = useSearchParams(); const fitmentPairs = searchParams.get("fitment_pairs"); const [term, setTerm] = useState(""); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [products, setProducts] = useState([]); const [categories, setCategories] = useState([]); const [productTypes, setProductTypes] = useState( [] ); // All refs declared together const abortRef = useRef(null); const containerRef = useRef(null); const justSelectedRef = useRef(false); // Debounced fetch useEffect(() => { // Don't open if user just selected an item if (justSelectedRef.current) { justSelectedRef.current = false; return; } // Clear results if search term is too short if (!term || term.trim().length < 2) { setProducts([]); setCategories([]); setProductTypes([]); setLoading(false); return; } setLoading(true); if (term.trim().length >= 2) { setOpen(true); } const controller = new AbortController(); abortRef.current?.abort(); abortRef.current = controller; const t = setTimeout(async () => { try { // If YMM status is OK/Active → Use PartsLogic search products API // If YMM status is NOT OK → Use GraphQL search if (isYmmActive) { const resp: PLResp = await partsLogicClient.searchProducts({ q: term.trim(), page: 1, per_page: 10, fitment_pairs: fitmentPairs ? fitmentPairs : "", }); if (!controller.signal.aborted) { // Transform PartsLogic response to match component's expected format const restProducts = resp.products.map((product) => ({ id: product.id, name: product.name, slug: product.slug, updatedAt: "", // Map category from PartsLogic format or GraphQL format category: product.category || (product.category_name ? { id: product.category_id || "", name: product.category_name, } : null), // Use primary_image from PartsLogic API or thumbnail from GraphQL thumbnail: product.thumbnail || (product.primary_image ? { url: product.primary_image, alt: product.name, } : null), })); const restCategories = resp.facets.categories.map((cat) => ({ id: cat.id, name: cat.value, slug: cat.value.toLowerCase().replace(/\s+/g, "-"), level: 0, parent: null, backgroundImage: cat.media ? { url: cat.media, alt: cat.value } : null, products: { totalCount: cat.count, }, })); const restProductTypes = resp.facets.brands.map((brand) => ({ id: brand.id, name: brand.value, slug: brand.value.toLowerCase().replace(/\s+/g, "-"), hasVariants: false, })); setProducts(restProducts); setCategories(restCategories); setProductTypes(restProductTypes); } } else { const resp = await saleorGlobalSearch({ q: term.trim(), channel: "default-channel", includeProducts: true, includeCategories: true, includeCollections: false, includeProductTypes: true, }); if (!controller.signal.aborted) { setProducts(resp.products?.edges.map((edge) => edge.node) || []); setCategories( resp.categories?.edges.map((edge) => edge.node) || [] ); setProductTypes( resp.productTypes?.edges.map((edge) => edge.node) || [] ); } } } catch (e) { if (!controller.signal.aborted) { setProducts([]); setCategories([]); setProductTypes([]); } } finally { if (!controller.signal.aborted) { setLoading(false); } } }, 300); // Slightly longer debounce return () => { clearTimeout(t); controller.abort(); }; }, [term, isYmmActive, fitmentPairs]); // Close on outside click or Escape useEffect(() => { const onDocClick = (e: MouseEvent) => { if (!open) return; const t = e.target as Node; if (containerRef.current && !containerRef.current.contains(t)) { setOpen(false); } }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("click", onDocClick); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("click", onDocClick); document.removeEventListener("keydown", onKey); }; }, [open]); const goToShop = (params: Record) => { // Navigate to shop page with search query if (pathname === "/products/all") { // If on shop page, replace with new search query const sp = new URLSearchParams(); if (params.q) sp.set("q", params.q); const newUrl = sp.toString() ? `/products/all?${sp.toString()}` : "/products/all"; router.replace(newUrl, { scroll: false }); } else { // If not on shop page, navigate to shop with search query const sp = new URLSearchParams(); if (params.q) sp.set("q", params.q); const url = sp.toString() ? `/products/all?${sp.toString()}` : "/products/all"; router.push(url); } setOpen(false); }; const onSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!term.trim()) return; goToShop({ q: term.trim() }); }; return (
) => setTerm(e.target.value) } onFocus={() => term.trim().length >= 2 && setOpen(true)} /> {open && term?.length > 1 && (
{/* Products Section - Now at the top */} {products.length > 0 && (
Products
{products.slice(0, 8).map((p) => ( ))}
)} {/* Categories Section */} {categories.length > 0 && (
Categories
{categories.slice(0, 4).map((c) => ( ))}
)} {/* Product Types Section */} {productTypes.length > 0 && (
Product Types
{productTypes.slice(0, 3).map((pt) => ( ))}
)} {!loading && !products.length && !categories.length && !productTypes.length && term.trim().length >= 2 && (
No Results found
)} {loading && (
Searching…
)}
)}
); }; export default Search;