"use client"; import { ProductCard } from "@/app/components/reuseableUI/productCard"; import HierarchicalCategoryFilter from "@/app/components/search/HierarchicalCategoryFilter"; import ItemsPerPageSelectClient from "@/app/components/shop/ItemsPerPageSelectClient"; import { getFullImageUrl } from "@/app/utils/functions"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Suspense, useCallback, useEffect, useMemo, useState, useRef, } from "react"; import ReactPaginate from "react-paginate"; import Breadcrumb from "../../components/reuseableUI/breadcrumb"; import Heading from "../../components/reuseableUI/heading"; import ListCard from "../../components/reuseableUI/listCard"; import ShopMobileFilters from "../../components/shop/ShopMobileFilters"; import { SortDropdown } from "../../components/sortDropdown"; import { productBreadcrumbItems } from "../../utils/constant"; import { GridIcon } from "../../utils/svgs/GridIcon"; import { ListIcon } from "../../utils/svgs/listIcon"; import { ChevronDownIcon } from "@/app/utils/svgs/chevronDownIcon"; import { FiltersCollapsible } from "@/app/components/filtersCollapsible"; import { handleScrollToTop } from "@/hooks/scrollPageTop"; import { SearchByVehicle } from "@/app/components/reuseableUI/searchByVehicle"; type ViewMode = "grid" | "list"; 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; }; } interface AllProductsClientProps { initialProducts?: YMMProduct[] | null; initialPagination?: YMMSearchResponse["pagination"] | null; initialFacets?: YMMSearchResponse["facets"] | null; } function AllProductsClientInner({ initialProducts, initialPagination, initialFacets, }: AllProductsClientProps) { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const topRef = useRef(null); const skipFirstFetchRef = useRef(false); // Use initial data if provided (from SSR) const hasInitialData = Array.isArray(initialProducts) && initialPagination != null; skipFirstFetchRef.current = skipFirstFetchRef.current || hasInitialData; const selectedPairs = searchParams?.get("fitment_pairs") || ""; const getSearch = searchParams?.get("q") || ""; const [itemsPerPage, setItemsPerPage] = useState( (initialPagination?.per_page as ItemsPerPage) || 20 ); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedBrands, setSelectedBrands] = useState([]); const [currentPage, setCurrentPage] = useState(initialPagination?.page || 1); const [loading, setLoading] = useState(!hasInitialData); const [searchData, setSearchData] = useState( hasInitialData ? { products: initialProducts, facets: initialFacets || { brands: [], categories: [], price_ranges: null, years: [], makes: [], models: [], }, pagination: initialPagination || { total: 0, page: 1, per_page: 20, total_pages: 0, }, } : null ); const [viewMode, setViewMode] = useState("grid"); const [sortKey, setSortKey] = useState("name_asc"); const [isInitialized, setIsInitialized] = useState(false); 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, perPage: number ) => { const params = new URLSearchParams(); // Preserve fitment_pairs parameter if (selectedPairs) params.set("fitment_pairs", selectedPairs); if (getSearch) params.set("q", getSearch); if (perPage !== 20) params.set("per_page", String(perPage)); // Keep URL keys aligned with PartsLogic request keys. categories.forEach((slug) => params.append("category_slug", slug)); brands.forEach((slug) => params.append("brand_slug", slug)); // Add sort parameter if (sort !== "name_asc") { params.set("sort_by", 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 }); }, [getSearch, pathname, router, selectedPairs] ); const readFiltersFromURL = useCallback(() => { const categoriesParam = searchParams.getAll("category_slug"); const productTypesParam = searchParams.getAll("brand_slug"); const sortParam = searchParams.get("sort_by"); const pageParam = searchParams.get("page"); const perPageParam = searchParams.get("per_page"); return { categories: categoriesParam, brands: productTypesParam, sortKey: sortParam || "name_asc", page: pageParam ? parseInt(pageParam, 10) : 1, perPage: perPageParam ? parseInt(perPageParam, 10) : null, }; }, [searchParams]); useEffect(() => { if (!isInitialized) { const urlFilters = readFiltersFromURL(); setSelectedCategories(urlFilters.categories); setSelectedBrands(urlFilters.brands); setSortKey(urlFilters.sortKey); setCurrentPage(urlFilters.page); if (urlFilters.perPage && [10, 20, 50, 100].includes(urlFilters.perPage)) { setItemsPerPage(urlFilters.perPage as ItemsPerPage); } setIsInitialized(true); } }, [isInitialized, readFiltersFromURL]); useEffect(() => { if (isInitialized) { updateURL( selectedCategories, selectedBrands, sortKey, currentPage, itemsPerPage ); } }, [ isInitialized, selectedCategories, selectedBrands, sortKey, currentPage, itemsPerPage, updateURL, ]); useEffect(() => { if (!isInitialized) return; if (skipFirstFetchRef.current) { skipFirstFetchRef.current = false; return; } const fetchYMMResults = async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(currentPage), per_page: String(itemsPerPage), }); if (getSearch) { params.set("q", getSearch); } // Add fitment_pairs if exists if (selectedPairs) { params.set("fitment_pairs", selectedPairs); } // Convert category slugs to IDs if (selectedCategories.length > 0) { selectedCategories.forEach((slug) => { // const id = categorySlugToId.get(slug); params.append("category_slug", slug); }); } // Convert brand slugs to IDs if (selectedBrands.length > 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, getSearch, isInitialized, ]); 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, }, }, }, })) || []; // 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]); const totalPages = searchData?.pagination.total_pages || 1; const handlePageChange = (selectedItem: { selected: number }) => { setCurrentPage(selectedItem.selected + 1); window.scrollTo({ top: 0, behavior: "smooth", }); }; return (
{/* H1 is rendered server-side in `src/app/products/all/page.tsx` for SEO. */}
[0]["categories"] } productTypes={ (searchData?.facets.brands || []).map((pt) => { const slug = pt.value.toLowerCase().replace(/\s+/g, "-"); return { id: slug, name: pt.value, slug: slug, products: { totalCount: pt.count }, }; }) as unknown as Parameters< typeof ShopMobileFilters >[0]["productTypes"] } selectedCategoryIds={selectedCategories} selectedProductTypeIds={selectedBrands} onCategoryChange={setSelectedCategories} onProductTypeChange={setSelectedBrands} filtersLoading={loading} />
{loading && (
)} {!loading && productsForGrid.length === 0 ? (

No products found

Try adjusting your filters or search criteria.

) : !loading ? ( <> {viewMode === "grid" ? (
{productsForGrid.map((product) => { const href = `/product/${encodeURIComponent( product.node.slug )}`; const imageUrl = getFullImageUrl(product.node.media?.[0]?.url) || "/no-image-avail-large.png"; return ( ); })}
) : (
{productsForGrid.map((product) => { const href = `/product/${encodeURIComponent( product.node.slug )}`; const imageUrl = getFullImageUrl(product.node.media?.[0]?.url) || "/no-image-avail-large.png"; return ( ); })}
)} ) : null} {totalPages > 1 && (
{ChevronDownIcon} Prev

} nextLabel={

Next {ChevronDownIcon}

} renderOnZeroPageCount={undefined} containerClassName="flex items-center justify-center gap-2 font-secondary" pageClassName="list-none" pageLinkClassName="px-3 py-2 !text-base cursor-pointer bg-[var(--color-secondary-200)] text-gray-900 hover:opacity-80" previousClassName="list-none" previousLinkClassName="px-3 py-2 !text-base cursor-pointer bg-[var(--color-secondary-200)] text-gray-700 hover:opacity-80 flex items-center gap-1" nextClassName="list-none" nextLinkClassName="px-3 py-2 !text-base cursor-pointer bg-[var(--color-secondary-200)] text-gray-700 hover:opacity-80 flex items-center gap-1" activeClassName="list-none" activeLinkClassName="px-3 py-2 !text-base !bg-[var(--color-primary-600)] text-white border border-[var(--color-primary-600)]" disabledClassName="opacity-50 pointer-events-none" breakLabel={"..."} breakLinkClassName="px-2 !text-base text-gray-500" />
)}
); } export default function AllProductsClient(props: AllProductsClientProps) { return ( ); }