"use client"; import { useState, useEffect, Suspense, useCallback, useMemo, useRef, } from "react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; import ReactPaginate from "react-paginate"; import ItemsPerPageSelectClient from "@/app/components/shop/ItemsPerPageSelectClient"; import Breadcrumb from "@/app/components/reuseableUI/breadcrumb"; import HierarchicalCategoryFilter from "@/app/components/search/HierarchicalCategoryFilter"; import { SortDropdown } from "@/app/components/sortDropdown"; import { FiltersCollapsible } from "@/app/components/filtersCollapsible"; import { ListIcon } from "@/app/utils/svgs/listIcon"; import { GridIcon } from "@/app/utils/svgs/GridIcon"; import ListCard from "@/app/components/reuseableUI/listCard"; import { ProductCard } from "@/app/components/reuseableUI/productCard"; import ShopMobileFilters from "@/app/components/shop/ShopMobileFilters"; import { getFullImageUrl } from "@/app/utils/functions"; import { SearchByVehicle } from "../components/reuseableUI/searchByVehicle"; import { useVehicleData } from "@/hooks/useVehicleData"; import { generateItemListSchema } from "@/lib/schema"; import { handleScrollToTop } from "@/hooks/scrollPageTop"; // This page is intentionally client-rendered and `noindex` (see `src/app/search/layout.tsx`). type ViewMode = "grid" | "list"; const ChevronDownIcon = ( ); type ItemsPerPage = 10 | 20 | 50 | 100; interface YMMProduct { id: string; name: string; slug: string; description: string; skus: Array; category: { id: string; name: string; }; productType: { id: string; name: string; slug: string; }; thumbnail: { url: string; alt: string; }; price_min: number | null; price_max: number | null; media: Array<{ id: number; url: string; alt: string; }>; } interface YMMCategory { id: string; value: string; slug: string; count: number; media?: string; level?: number; parent_id?: string; children?: YMMCategory[]; } interface YMMProductType { id: string; value: string; count: number; media?: string; } interface FlatCategory { id: string; name: string; slug: string; products: { totalCount: number }; } interface YMMSearchResponse { products: YMMProduct[]; facets: { brands: YMMProductType[]; categories: YMMCategory[]; price_ranges: Array<{ min: number; max: number; count: number }> | null; years: Array<{ value: string; count: number }>; makes: Array<{ value: string; count: number }>; models: Array<{ value: string; count: number }>; }; pagination: { total: number; page: number; per_page: number; total_pages: number; }; } function SearchPageContent() { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const selectedPairs = searchParams?.get("fitment_pairs") || ""; const { getSelectedNames, isComplete } = useVehicleData(); const [itemsPerPage, setItemsPerPage] = useState(20); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedBrands, setSelectedBrands] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [loading, setLoading] = useState(true); const [searchData, setSearchData] = useState(null); const [viewMode, setViewMode] = useState("grid"); const [sortKey, setSortKey] = useState("name_asc"); const [isInitialized, setIsInitialized] = useState(false); const topRef = useRef(null); const categorySlugToId = useMemo(() => { const flattenCategories = ( categories: YMMCategory[] ): Array<[string, string]> => { const result: Array<[string, string]> = []; categories.forEach((cat) => { result.push([cat.slug, cat.id]); if (cat.children) { result.push(...flattenCategories(cat.children)); } }); return result; }; return new Map( searchData?.facets.categories ? flattenCategories(searchData.facets.categories) : [] ); }, [searchData?.facets.categories]); const brandSlugToId = useMemo( () => new Map( searchData?.facets.brands.map((brand) => [ brand.value.toLowerCase().replace(/\s+/g, "-"), brand.id, ]) || [] ), [searchData?.facets.brands] ); const SORT_OPTIONS = [ { key: "price_min:asc", label: "Price: Low to High" }, { key: "price_max:desc", label: "Price: High to Low" }, ]; const updateURL = useCallback( (categories: string[], brands: string[], sort: string, page: number) => { const params = new URLSearchParams(); // Preserve fitment_pairs parameter if (selectedPairs) params.set("fitment_pairs", selectedPairs); // Add category filters if (categories.length > 0) { categories.forEach((cat) => { params.append("category", cat); }); } // Add brand filters if (brands.length > 0) { brands.forEach((brand) => { params.append("brand", brand); }); } // Add sort parameter if (sort !== "name_asc") { params.set("sort", sort); } // Add page parameter if (page > 1) { params.set("page", page.toString()); } const queryString = params.toString(); const newURL = queryString ? `${pathname}?${queryString}` : pathname; router.replace(newURL, { scroll: false }); }, [pathname, router, selectedPairs] ); const readFiltersFromURL = useCallback(() => { const categoriesParam = searchParams.getAll("categories"); const productTypesParam = searchParams.getAll("productTypes"); const sortParam = searchParams.get("sort"); const pageParam = searchParams.get("page"); return { categories: categoriesParam, brands: productTypesParam, sortKey: sortParam || "name_asc", page: pageParam ? parseInt(pageParam, 10) : 1, }; }, [searchParams]); useEffect(() => { if (!isInitialized) { const urlFilters = readFiltersFromURL(); setSelectedCategories(urlFilters.categories); setSelectedBrands(urlFilters.brands); setSortKey(urlFilters.sortKey); setCurrentPage(urlFilters.page); setIsInitialized(true); } }, [isInitialized, readFiltersFromURL]); useEffect(() => { if (isInitialized) { updateURL(selectedCategories, selectedBrands, sortKey, currentPage); } }, [ isInitialized, selectedCategories, selectedBrands, sortKey, currentPage, updateURL, ]); useEffect(() => { const fetchYMMResults = async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(currentPage), per_page: String(itemsPerPage), }); // Add fitment_pairs if exists if (selectedPairs) { params.set("fitment_pairs", selectedPairs); } // Convert category slugs to IDs if (selectedCategories.length > 0 && categorySlugToId.size > 0) { selectedCategories.forEach((slug) => { // const id = categorySlugToId.get(slug); params.append("category_slug", slug); }); } // Convert brand slugs to IDs if (selectedBrands.length > 0 && brandSlugToId.size > 0) { selectedBrands.forEach((slug) => { // const id = brandSlugToId.get(slug); params.append("brand_slug", slug); }); } // Add sorting if (sortKey) { if (sortKey === "name_asc") { console.log("Get all products"); } else { params.set("sort_by", sortKey); } } const baseUrl = process.env.NEXT_PUBLIC_PARTSLOGIC_URL; const response = await fetch( `${baseUrl}/api/search/products?${params.toString()}`, { headers: { accept: "application/json", }, } ); if (!response.ok) { throw new Error("Failed to fetch products"); } const data: YMMSearchResponse = await response.json(); setSearchData(data); } catch (error) { console.error("Error fetching products:", error); } finally { setLoading(false); } }; fetchYMMResults(); }, [ selectedPairs, currentPage, itemsPerPage, selectedCategories, selectedBrands, sortKey, // categorySlugToId, // brandSlugToId, ]); const productsForGrid = searchData?.products.map((product) => ({ node: { id: product.id, name: product.name, slug: product.slug, media: product.media?.map((m) => ({ id: String(m.id), url: m.url, alt: m.alt || null, })), skus: product.skus || [], category: product.category || null, price_min: product.price_min, price_max: product.price_max, pricing: { onSale: null, priceRange: { start: product.price_min || null, stop: product.price_max || null, }, }, }, })) || []; const breadcrumbItems = [ { text: "HOME", link: "/" }, { text: "SHOP", link: "/products/all" }, { text: "SEARCH RESULTS" }, ]; const totalProducts = searchData?.pagination.total || 0; const totalPages = searchData?.pagination.total_pages || 1; const pageTitle = selectedPairs ? "Search Results" : "All Products"; const handleVehicleSearch = (pairs: string) => { router.push(`/search?fitment_pairs=${pairs}`); }; // Flatten categories for mobile filters const flatCategories = useMemo(() => { const flatten = (categories: YMMCategory[], level = 0): FlatCategory[] => { const result: FlatCategory[] = []; categories.forEach((cat) => { result.push({ id: cat.slug, name: `${" ".repeat(level)}${cat.value}`, slug: cat.slug, products: { totalCount: cat.count }, }); if (cat.children) { result.push(...flatten(cat.children, level + 1)); } }); return result; }; return searchData?.facets.categories ? flatten(searchData.facets.categories) : []; }, [searchData?.facets.categories]); // Generate ItemList schema with category information const itemListSchema = searchData && searchData.products.length > 0 ? generateItemListSchema( searchData.products.map((product) => ({ id: product.id, slug: product.slug, name: product.name, price: product.price_min || 0, currency: "USD", image: product.media?.[0]?.url, category: product.category ? { id: product.category.id, name: product.category.name, } : undefined, })), pageTitle ) : null; const handlePageChange = (selectedItem: { selected: number }) => { setCurrentPage(selectedItem.selected + 1); window.scrollTo({ top: 0, behavior: "smooth", }); }; return (
{/* Schema.org structured data */} {itemListSchema && (