From f396125acf489f67d2f2004b2873e4fc78fb0498 Mon Sep 17 00:00:00 2001 From: "nabijonovdavronbek619@gmail.com" Date: Sat, 7 Mar 2026 21:16:56 +0500 Subject: [PATCH] add additional filter to product page --- app/[locale]/catalog_page/products/page.tsx | 2 +- pages/products/filter/catalog.tsx | 150 ++++++++++++++++++++ pages/products/filter/category.tsx | 87 ++++++++++++ pages/products/filter/filter.tsx | 33 +---- pages/products/filter/useCataloghook.ts | 34 +++++ pages/products/filter/useCategory.ts | 78 ++++++++++ pages/products/product/mianProduct.tsx | 6 +- pages/products/product/products.tsx | 2 +- request/links.ts | 22 ++- zustand/useCatalog.ts | 13 ++ 10 files changed, 393 insertions(+), 34 deletions(-) create mode 100644 pages/products/filter/catalog.tsx create mode 100644 pages/products/filter/category.tsx create mode 100644 pages/products/filter/useCataloghook.ts create mode 100644 pages/products/filter/useCategory.ts create mode 100644 zustand/useCatalog.ts diff --git a/app/[locale]/catalog_page/products/page.tsx b/app/[locale]/catalog_page/products/page.tsx index a2b1ae9..38359ee 100644 --- a/app/[locale]/catalog_page/products/page.tsx +++ b/app/[locale]/catalog_page/products/page.tsx @@ -8,7 +8,7 @@ export default function Page() { return (
-
+
diff --git a/pages/products/filter/catalog.tsx b/pages/products/filter/catalog.tsx new file mode 100644 index 0000000..43745bc --- /dev/null +++ b/pages/products/filter/catalog.tsx @@ -0,0 +1,150 @@ +"use client"; +import { useTranslations } from "next-intl"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { useCatalogHook } from "./useCataloghook"; +import { useCatalog } from "@/zustand/useCatalog"; +import { AnimatePresence, motion } from "framer-motion"; + +export function CatalogSection() { + const t = useTranslations(); + const setParentID = useCatalog((state) => state.setParentID); + const parentID = useCatalog((state) => state.parentID); + const { + catalogSection, + catalogsectionChild, + setParent, + childLoading, + openDropdowns, + setOpenDropdowns, + } = useCatalogHook(); + + return ( +
+ {/* ── Top-level categories ─────────────────────────────────────── */} +
+ {catalogSection?.map((item: any) => ( +
+
{ + setParent(item.id); + setOpenDropdowns((prev) => (prev === item.id ? null : item.id)); + }} + className="flex items-center gap-2" + > +

+ {item.name} +

+ + {item.children.length > 0 && ( + + {/* + * Single icon that rotates — replaces the ChevronUp/Down swap. + * Logic unchanged: openDropdowns === item.id still drives it. + */} + + + )} +
+
+ ))} +
+ + {/* ── Sub-category dropdown — animated open/close ───────────────── */} + {/* + * AnimatePresence watches its children mount/unmount. + * The `key` on the motion.div is the open dropdown's id so that + * when you switch categories, the old panel exits and new one enters. + * All original conditional logic (catalogsectionChild, childLoading, + * .length > 0, t("subcategory_not_found")) is untouched. + */} + + {catalogsectionChild && openDropdowns !== null && ( + + {childLoading ? ( + // Loading skeleton — staggered pulse instead of plain text +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+ ) : catalogsectionChild.length > 0 ? ( + + {catalogsectionChild.map((subItem: any) => ( + setParentID(subItem.id)} + className="border-l px-3 my-2 hover:cursor-pointer hover:text-red-500 text-white flex items-center justify-center" + > +

+ {subItem.name} +

+
+ ))} +
+ ) : ( + + {t("subcategory_not_found")} + + )} +
+ )} +
+
+ ); +} diff --git a/pages/products/filter/category.tsx b/pages/products/filter/category.tsx new file mode 100644 index 0000000..0fcd263 --- /dev/null +++ b/pages/products/filter/category.tsx @@ -0,0 +1,87 @@ +"use client"; +import { useTranslations } from "next-intl"; +import { useCategoryHook } from "./useCategory"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +export function Category() { + const t = useTranslations(); + const { + categoryBack, + handleCategoryClick, + openDropdowns, + category, + subCategory, + handleSubCategoryClick, + subCategoryBack, + subCategoryLoading, + } = useCategoryHook(); + + return ( +
+ {categoryBack?.map((item: any) => ( +
+ {/* Main Category */} +
handleCategoryClick(item)} className=""> +

+ {item.name} +

+ {item.have_sub_category && ( + + {openDropdowns[item.id] ? ( + + ) : ( + + )} + + )} +
+ + {/* ⭐ YANGI: SubCategory Dropdown */} + {item.have_sub_category && openDropdowns[item.id] && ( +
+ {subCategoryLoading ? ( +

Yuklanmoqda...

+ ) : subCategoryBack && subCategoryBack.length > 0 ? ( + subCategoryBack.map((subItem: any) => ( +
handleSubCategoryClick(subItem)} + className="hover:cursor-pointer flex items-center gap-2 hover:bg-gray-600 p-1.5 rounded transition-colors" + > + + {subCategory.id === subItem.id && ( + + )} + +

{subItem.name}

+
+ )) + ) : ( +

+ {t("subcategory_not_found")} +

+ )} +
+ )} +
+ ))} +
+ ); +} diff --git a/pages/products/filter/filter.tsx b/pages/products/filter/filter.tsx index e4d1af8..a3a827e 100644 --- a/pages/products/filter/filter.tsx +++ b/pages/products/filter/filter.tsx @@ -1,34 +1,11 @@ -import { CatalogItem } from "@/lib/types"; -import httpClient from "@/request/api"; -import { endPoints } from "@/request/links"; -import { useQuery } from "@tanstack/react-query"; -import { ChevronDown } from "lucide-react"; +import { Category } from "./category"; +import { CatalogSection } from "./catalog"; export default function Filter() { - const { data } = useQuery({ - queryKey: ["catalogsection"], - queryFn: () => httpClient(endPoints.filter.catalog), - select: (res) => res?.data?.data?.results, - }); - - console.log("filter catalog: ", data); return ( -
-
- {data?.map((item: CatalogItem) => ( -
-
{item.name}
- {item.children.length > 0 && ( - - )} -
- ))} -
+
+ +
); } diff --git a/pages/products/filter/useCataloghook.ts b/pages/products/filter/useCataloghook.ts new file mode 100644 index 0000000..c030a8b --- /dev/null +++ b/pages/products/filter/useCataloghook.ts @@ -0,0 +1,34 @@ +"use client"; +import httpClient from "@/request/api"; +import { endPoints } from "@/request/links"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +export const useCatalogHook = () => { + const [openDropdowns, setOpenDropdowns] = useState(0); + const [parent, setParent] = useState(0); + const { data: catalogsectionChild, isLoading: childLoading } = useQuery({ + queryKey: ["catalogsection/", parent], + queryFn: () => httpClient(endPoints.filter.child({ id: parent || 0 })), + select: (data) => data?.data?.data?.results, + enabled: !!openDropdowns, + }); + + const { data: catalogSection } = useQuery({ + queryKey: ["catalogsection"], + queryFn: () => httpClient(endPoints.filter.catalogSection), + select: (data) => data?.data?.data?.results, + }); + + console.log({ catalogSection }); + console.log({ catalogsectionChild }); + + return { + catalogSection, + catalogsectionChild, + setParent, + openDropdowns, + setOpenDropdowns, + childLoading, + }; +}; diff --git a/pages/products/filter/useCategory.ts b/pages/products/filter/useCategory.ts new file mode 100644 index 0000000..e5a673b --- /dev/null +++ b/pages/products/filter/useCategory.ts @@ -0,0 +1,78 @@ +import httpClient from "@/request/api"; +import { endPoints } from "@/request/links"; +import { useCategory } from "@/zustand/useCategory"; +import { useSubCategory } from "@/zustand/useSubCategory"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +export const useCategoryHook = () => { + const [openDropdowns, setOpenDropdowns] = useState>( + {}, + ); + + const category = useCategory((state) => state.category); + const setCategory = useCategory((state) => state.setCategory); + const subCategory = useSubCategory((state) => state.subCategory); + const setSubCategory = useSubCategory((state) => state.setSubCategory); + const clearSubCategory = useSubCategory((state) => state.clearSubCategory); + + // Category data + const { data: categoryBack } = useQuery({ + queryKey: ["category"], + queryFn: () => httpClient(endPoints.category.all), + select: (data) => data?.data?.results, + }); + + const { data: subCategoryBack, isLoading: subCategoryLoading } = useQuery({ + queryKey: ["subCategory", category.id], + queryFn: () => httpClient(endPoints.subCategory.byId(category.id)), + enabled: + !!category.id && + category.have_sub_category === true && + openDropdowns[category.id] === true, + select: (data) => data?.data?.results, + }); + + const handleCategoryClick = (item: any) => { + if (item.have_sub_category) { + // Agar subCategory bo'lsa, dropdown ochish/yopish + setOpenDropdowns((prev) => ({ + ...prev, + [item.id]: !prev[item.id], + })); + + // Category'ni set qilish (filterlar yangilanishi uchun) + setCategory(item); + + // SubCategory'ni tozalash (yangisini tanlash uchun) + if (!openDropdowns[item.id]) { + clearSubCategory(); + } + } else { + // Agar subCategory bo'lmasa, to'g'ridan-to'g'ri category ni set qilish + setCategory(item); + clearSubCategory(); + + // Barcha dropdown'larni yopish + setOpenDropdowns({}); + } + }; + + const handleSubCategoryClick = (item: any) => { + setSubCategory(item); + }; + + return { + category, + setCategory, + subCategory, + setSubCategory, + clearSubCategory, + categoryBack, + subCategoryBack, + subCategoryLoading, + handleCategoryClick, + handleSubCategoryClick, + openDropdowns, + }; +}; diff --git a/pages/products/product/mianProduct.tsx b/pages/products/product/mianProduct.tsx index 24f4c20..f5cdbbf 100644 --- a/pages/products/product/mianProduct.tsx +++ b/pages/products/product/mianProduct.tsx @@ -10,6 +10,7 @@ import { useProductPageInfo } from "@/zustand/useProduct"; import { useSubCategory } from "@/zustand/useSubCategory"; import { useTranslations } from "next-intl"; import PaginationLite from "@/components/paginationUI"; +import { useCatalog } from "@/zustand/useCatalog"; export default function MainProduct() { const t = useTranslations(); @@ -18,6 +19,7 @@ export default function MainProduct() { const filter = useFilter((s) => s.filter); const getFiltersByType = useFilter((s) => s.getFiltersByType); const setProduct = useProductPageInfo((s) => s.setProducts); + const parentID = useCatalog((state) => state.parentID); const [currentPage, setCurrentPage] = useState(1); @@ -34,7 +36,9 @@ export default function MainProduct() { const requestLink = useMemo(() => { const baseLink = category.have_sub_category ? endPoints.product.bySubCategory({ id: subCategory.id, currentPage }) - : endPoints.product.byCategory({ id: category.id, currentPage }); + : parentID + ? endPoints.product.byCatalogSection({ id: parentID, currentPage }) + : endPoints.product.byCategory({ id: category.id, currentPage }); return `${baseLink}${queryParams}`; }, [ category.id, diff --git a/pages/products/product/products.tsx b/pages/products/product/products.tsx index 84ce45f..86702ad 100644 --- a/pages/products/product/products.tsx +++ b/pages/products/product/products.tsx @@ -5,7 +5,7 @@ export function Products() { return (
-
+
{/* filter part */} diff --git a/request/links.ts b/request/links.ts index 80ab424..7d195c1 100644 --- a/request/links.ts +++ b/request/links.ts @@ -29,6 +29,13 @@ export const endPoints = { return link; }, + byCatalogSection: ({ id, currentPage }: ProductTypes) => { + let link = "product"; + if (id) link += `?catalog_section=${id}`; + if (currentPage) link += `&page=${currentPage}`; + + return link; + }, detail: (id: number) => `product/${id}/`, }, faq: "faq/", @@ -41,9 +48,18 @@ export const endPoints = { normative: "document/?type=normative", guides: "guide/", filter: { - catalog: "catalogsection/?page_size=500", - child: (parenId: number) => - `catalogsection/?page_size=500&parent=${parenId}`, + size: "size/", + sizePageItems: "size/?page_size=500", + sizeCategoryId: (id: number) => `size/?category=${id}&page_size=500`, + catalog: "catalog/", + catalogPageItems: "catalog/?page_size=500", + catalogCategoryId: (id: number) => `catalog/?category=${id}&page_size=500`, + child: ({ id }: { id?: number }) => { + const link = "catalogsection/?page_size=500"; + if (id) return `${link}&parent=${id}`; + return link; + }, + catalogSection: "catalogsection/?page_size=500", }, post: { sendNumber: "callBack/", diff --git a/zustand/useCatalog.ts b/zustand/useCatalog.ts new file mode 100644 index 0000000..cc84c6b --- /dev/null +++ b/zustand/useCatalog.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +type CatalogType = { + parentID: number; + setParentID: (id: number) => void; +}; + +export const useCatalog = create((set) => { + return { + parentID: 0, + setParentID: (id: number) => set({ parentID: id }), + }; +});