From 903d6e1f4ff267c01d84aadef95c2f06398f9ee3 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Sat, 20 Dec 2025 03:34:46 +0500 Subject: [PATCH] added filter to category page --- package-lock.json | 59 +- src/app/api/brandsApi.js | 10 +- src/app/api/categories.js | 35 +- src/app/api/collectionsApi.js | 19 +- src/app/api/filtersApi.js | 77 ++ src/app/store.js | 3 + src/components/BrandsSidebar/index.jsx | 85 +- src/pages/Category/CategoryPage.module.scss | 1 + .../components/CategoryBreadcrumbs.jsx | 59 ++ .../Category/components/CategoryFilters.jsx | 98 ++ src/pages/Category/hooks/useCategoryData.js | 110 ++ .../Category/hooks/useCategoryProducts.js | 316 ++++++ src/pages/Category/index.jsx | 953 +++++------------- 13 files changed, 1041 insertions(+), 784 deletions(-) create mode 100644 src/app/api/filtersApi.js create mode 100644 src/pages/Category/components/CategoryBreadcrumbs.jsx create mode 100644 src/pages/Category/components/CategoryFilters.jsx create mode 100644 src/pages/Category/hooks/useCategoryData.js create mode 100644 src/pages/Category/hooks/useCategoryProducts.js diff --git a/package-lock.json b/package-lock.json index c436174..3a2da35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -173,6 +173,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1084,7 +1085,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1574,17 +1574,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~6.20.0" - } - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1596,6 +1585,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1639,6 +1629,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1935,6 +1926,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1959,8 +1951,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -2088,8 +2079,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/compute-scroll-into-view": { "version": "3.1.0", @@ -2207,7 +2197,8 @@ "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "peer": true }, "node_modules/debug": { "version": "4.4.0", @@ -2527,6 +2518,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3101,6 +3093,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" }, @@ -7320,6 +7313,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7331,6 +7325,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7399,6 +7394,7 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -7467,7 +7463,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8213,7 +8210,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8233,7 +8229,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8417,26 +8412,6 @@ "node": ">=16.0.0" } }, - "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -8569,8 +8544,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/update-browserslist-db": { "version": "1.1.1", @@ -8630,6 +8604,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.4.49", diff --git a/src/app/api/brandsApi.js b/src/app/api/brandsApi.js index 724e770..22b4d4a 100644 --- a/src/app/api/brandsApi.js +++ b/src/app/api/brandsApi.js @@ -11,7 +11,6 @@ export const brandsApi = baseApi.injectEndpoints({ if (params.page) { queryParams.append("page", params.page); } - if (params.limit) { queryParams.append("limit", params.limit); } @@ -29,12 +28,10 @@ export const brandsApi = baseApi.injectEndpoints({ getBrandProducts: builder.query({ query: (params) => { - // Handle both string ID and object with pagination params if (typeof params === 'string' || typeof params === 'number') { return `/brands/${params}/products`; } - // Handle object with pagination const { id, page = 1, limit } = params; let url = `/brands/${id}/products?page=${page}`; @@ -44,9 +41,10 @@ export const brandsApi = baseApi.injectEndpoints({ return url; }, - transformResponse: (response) => { - return response.data || response; - }, + transformResponse: (response) => ({ + data: response.data || response, + pagination: response.pagination || {}, + }), }), }), }); diff --git a/src/app/api/categories.js b/src/app/api/categories.js index 82586f6..dcdfc3e 100644 --- a/src/app/api/categories.js +++ b/src/app/api/categories.js @@ -5,17 +5,24 @@ export const categoriesApi = baseApi.injectEndpoints({ getCategories: builder.query({ query: (type = "tree") => `/categories?type=${type}`, }), + getCategoryProducts: builder.query({ - query: ({ categoryId, page = 1, limit }) => { - let url = `categories/${categoryId}/products?page=${page}`; - if (limit) url += `&limit=${limit}`; - return url; + query: ({ categoryId, page = 1, limit, brands, min_price, max_price }) => { + const params = new URLSearchParams(); + params.append('page', page); + if (limit) params.append('limit', limit); + if (brands) params.append('brands', brands); + if (min_price) params.append('min_price', min_price); + if (max_price) params.append('max_price', max_price); + + return `categories/${categoryId}/products?${params.toString()}`; }, transformResponse: (response) => ({ data: response.data || [], pagination: response.pagination || {}, }), }), + getAllCategoryProducts: builder.query({ async queryFn(category, queryApi, extraOptions, baseQuery) { const fetchProducts = async (categoryId) => { @@ -36,7 +43,7 @@ export const categoriesApi = baseApi.injectEndpoints({ getAllCategoryProductsPaginated: builder.query({ async queryFn( - { category, page = 1, limit = 6 }, + { category, page = 1, limit = 6, brands, min_price, max_price }, queryApi, extraOptions, baseQuery @@ -51,14 +58,20 @@ export const categoriesApi = baseApi.injectEndpoints({ const perCategoryLimit = Math.ceil(limit / categoryIds.length); for (const categoryId of categoryIds) { + const params = new URLSearchParams(); + params.append('page', currentPage); + params.append('limit', perCategoryLimit); + if (brands) params.append('brands', brands); + if (min_price) params.append('min_price', min_price); + if (max_price) params.append('max_price', max_price); + const result = await baseQuery( - `categories/${categoryId}/products?page=${currentPage}&limit=${perCategoryLimit}` + `categories/${categoryId}/products?${params.toString()}` ); + if (result.data && result.data.data) { allPageProducts = [...allPageProducts, ...result.data.data]; - - hasMoreByCategory[categoryId] = - !!result.data.pagination.next_page_url; + hasMoreByCategory[categoryId] = !!result.data.pagination.next_page_url; } } @@ -90,9 +103,11 @@ export const categoriesApi = baseApi.injectEndpoints({ } }, }), + getProductById: builder.query({ query: (productId) => `/products/${productId}`, }), + getRelatedProducts: builder.query({ query: (productId) => `/products/${productId}/related`, }), @@ -107,4 +122,4 @@ export const { useLazyGetAllCategoryProductsPaginatedQuery, useGetProductByIdQuery, useGetRelatedProductsQuery, -} = categoriesApi; +} = categoriesApi; \ No newline at end of file diff --git a/src/app/api/collectionsApi.js b/src/app/api/collectionsApi.js index 8fb2741..2550502 100644 --- a/src/app/api/collectionsApi.js +++ b/src/app/api/collectionsApi.js @@ -5,12 +5,13 @@ export const collectionsApi = baseApi.injectEndpoints({ getCollections: builder.query({ query: () => `/collections`, }), + getCollectionById: builder.query({ query: (collectionId) => `/collections/${collectionId}`, }), + getCollectionProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products`, - // Ürünleri dönüştürerek boş kontrol edilebilir transformResponse: (response) => { return { data: response.data || [], @@ -18,7 +19,7 @@ export const collectionsApi = baseApi.injectEndpoints({ }; }, }), - // Yeni endpoint: Koleksiyonun ürün içerip içermediğini kontrol eder + checkCollectionHasProducts: builder.query({ query: (collectionId) => `/collections/${collectionId}/products?limit=1`, transformResponse: (response) => { @@ -27,10 +28,18 @@ export const collectionsApi = baseApi.injectEndpoints({ }; }, }), - // Sayfalı koleksiyon ürünleri için endpoint + getCollectionProductsPaginated: builder.query({ - query: ({ collectionId, page = 1, limit = 6 }) => - `/collections/${collectionId}/products?page=${page}${limit ? `&limit=${limit}` : ''}`, + query: ({ collectionId, page = 1, limit = 6, brands, min_price, max_price }) => { + const params = new URLSearchParams(); + params.append('page', page); + if (limit) params.append('limit', limit); + if (brands) params.append('brands', brands); + if (min_price) params.append('min_price', min_price); + if (max_price) params.append('max_price', max_price); + + return `/collections/${collectionId}/products?${params.toString()}`; + }, transformResponse: (response) => ({ data: response.data || [], pagination: response.pagination || {}, diff --git a/src/app/api/filtersApi.js b/src/app/api/filtersApi.js new file mode 100644 index 0000000..ba7a110 --- /dev/null +++ b/src/app/api/filtersApi.js @@ -0,0 +1,77 @@ +import { baseApi } from "./baseApi" + +export const filtersApi = baseApi.injectEndpoints({ + endpoints: (builder) => ({ + getFilters: builder.query({ + query: (params) => { + const queryParams = new URLSearchParams() + + if (params?.category_id) { + queryParams.append("category_id", String(params.category_id)) + } + if (params?.collection_id) { + queryParams.append("collection_id", String(params.collection_id)) + } + if (params?.brand_id) { + queryParams.append("brand_id", String(params.brand_id)) + } + + return `/filters?${queryParams.toString()}` + }, + transformResponse: (response) => { + return { + categories: response.data?.categories || [], + brands: response.data?.brands || [], + } + }, + keepUnusedDataFor: 300, + + serializeQueryArgs: ({ queryArgs }) => { + if (!queryArgs) return 'no-params'; + + const parts = []; + + if (queryArgs.category_id) { + parts.push(`cat:${queryArgs.category_id}`); + } + if (queryArgs.collection_id) { + parts.push(`col:${queryArgs.collection_id}`); + } + if (queryArgs.brand_id) { + parts.push(`brd:${queryArgs.brand_id}`); + } + + return parts.length > 0 ? parts.join('|') : 'no-params'; + }, + + merge: (currentCache, newItems) => { + return newItems; + }, + + refetchOnMountOrArgChange: 30, // Refetch if older than 30 seconds + refetchOnFocus: false, + refetchOnReconnect: false, + + providesTags: (result, error, arg) => { + if (!arg) return [{ type: "Filters", id: "no-params" }]; + + const tags = [{ type: "Filters", id: "LIST" }]; + + if (arg.category_id) { + tags.push({ type: "Filters", id: `cat-${arg.category_id}` }); + } + if (arg.collection_id) { + tags.push({ type: "Filters", id: `col-${arg.collection_id}` }); + } + if (arg.brand_id) { + tags.push({ type: "Filters", id: `brd-${arg.brand_id}` }); + } + + return tags; + }, + }), + }), + overrideExisting: true, +}) + +export const { useGetFiltersQuery, useLazyGetFiltersQuery } = filtersApi \ No newline at end of file diff --git a/src/app/store.js b/src/app/store.js index ae27b40..8d09d2b 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -13,6 +13,7 @@ import { mediaApi } from "./api/bannersApi"; import { reviewsApi } from "./api/reviewApi"; import { profileApi } from "./api/myProfileApi"; import { contactApi } from "./api/contactUs"; +import { filtersApi } from "./api/filtersApi"; const store = configureStore({ reducer: { @@ -30,6 +31,7 @@ const store = configureStore({ [reviewsApi.reducerPath]: reviewsApi.reducer, [profileApi.reducerPath]: profileApi.reducer, [contactApi.reducerPath]: contactApi.reducer, + [filtersApi.reducerPath]: filtersApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( @@ -46,6 +48,7 @@ const store = configureStore({ mediaApi.middleware, profileApi.middleware, contactApi.middleware, + filtersApi.middleware ), }); diff --git a/src/components/BrandsSidebar/index.jsx b/src/components/BrandsSidebar/index.jsx index 1db16cf..f4b7103 100644 --- a/src/components/BrandsSidebar/index.jsx +++ b/src/components/BrandsSidebar/index.jsx @@ -1,38 +1,18 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { Drawer, Input, Checkbox } from "antd"; import styles from "./BrandsSidebar.module.scss"; import { useTranslation } from "react-i18next"; import brand from "../../assets/icons/brand.svg"; -const Sidebar = () => { +const BrandSidebar = ({ + brands = [], + selectedBrand = null, + onBrandSelect, + onBrandDeselect +}) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const { t, i18n } = useTranslation(); - const brands = [ - "Abat", - "Altın", - "Arçalyk", - "Aýaz baba", - "Balşeker", - "Bars", - "Belet Film", - "Beýlekiler / Другие", - "Bingo", - "Bold", - "Carte Noire", - "Çaykur", - "Dabara", - "Datmeni", - "Elin", - "Emin Et", - "Enemeli", - "Ermak", - "Eyfel", - "Familia", - "Farmasi", - "Ferrero Rocher", - "Granum", - ]; + const { t } = useTranslation(); const handleToggle = () => { setIsOpen(!isOpen); @@ -42,15 +22,28 @@ const Sidebar = () => { setSearchTerm(e.target.value.toLowerCase()); }; - const filteredBrands = brands.filter((brand) => - brand.toLowerCase().includes(searchTerm) - ); + const filteredBrands = useMemo(() => { + if (!brands || brands.length === 0) return []; + + return brands.filter((brand) => + brand.name.toLowerCase().includes(searchTerm) + ); + }, [brands, searchTerm]); + + const handleBrandChange = (brandId, checked) => { + if (checked) { + onBrandSelect?.(brandId); + } else { + onBrandDeselect?.(); + } + }; return (
{ onChange={handleSearch} className={styles.searchInput} /> -
- {filteredBrands.map((brand, index) => ( -
- {brand} -
- ))} -
+ + {filteredBrands.length > 0 ? ( +
+ {filteredBrands.map((brand) => ( +
+ handleBrandChange(brand.id, e.target.checked)} + > + {brand.name} + +
+ ))} +
+ ) : ( +
+ {brands.length === 0 + ? t("common.noData") + : t("common.noResults")} +
+ )}
); }; -export default Sidebar; +export default BrandSidebar; \ No newline at end of file diff --git a/src/pages/Category/CategoryPage.module.scss b/src/pages/Category/CategoryPage.module.scss index 310cf5b..e3fab58 100644 --- a/src/pages/Category/CategoryPage.module.scss +++ b/src/pages/Category/CategoryPage.module.scss @@ -5,6 +5,7 @@ margin: auto; padding: 20px 1.375rem; box-sizing: border-box; + flex-direction: column; h2 { font-size: 24px; font-weight: 700; diff --git a/src/pages/Category/components/CategoryBreadcrumbs.jsx b/src/pages/Category/components/CategoryBreadcrumbs.jsx new file mode 100644 index 0000000..379b1f0 --- /dev/null +++ b/src/pages/Category/components/CategoryBreadcrumbs.jsx @@ -0,0 +1,59 @@ +import React, { useMemo } from "react"; +import { Breadcrumb } from "antd"; +import { useTranslation } from "react-i18next"; +import styles from "../CategoryPage.module.scss"; + +const CategoryBreadcrumbs = ({ categoriesData, categoryId, onCategoryClick }) => { + const { t } = useTranslation(); + + const breadcrumbs = useMemo(() => { + if (!categoriesData?.data || !categoryId) return []; + + const buildPath = (categories, targetId, path = []) => { + for (const category of categories) { + if (category.id === parseInt(targetId)) { + return [...path, category]; + } + if (category.children) { + const found = buildPath(category.children, targetId, [...path, category]); + if (found) return found; + } + } + return null; + }; + + return buildPath(categoriesData.data, categoryId) || []; + }, [categoriesData, categoryId]); + + if (breadcrumbs.length === 0) return null; + + return ( + + + + {breadcrumbs.map((crumb, index) => { + const isLast = index === breadcrumbs.length - 1; + + return ( + + {isLast ? ( + {crumb.name} + ) : ( + { + e.preventDefault(); + onCategoryClick?.(crumb.id); + }} + style={{ cursor: 'pointer' }} + > + {crumb.name} + + )} + + ); + })} + + ); +}; + +export default CategoryBreadcrumbs; \ No newline at end of file diff --git a/src/pages/Category/components/CategoryFilters.jsx b/src/pages/Category/components/CategoryFilters.jsx new file mode 100644 index 0000000..ad62bdd --- /dev/null +++ b/src/pages/Category/components/CategoryFilters.jsx @@ -0,0 +1,98 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { TiTick } from "react-icons/ti"; +import styles from "../CategoryPage.module.scss"; + +const CategoryFilters = ({ + filtersData, + selectedFilterCategory, + selectedFilterBrand, + brandSearchQuery, + searchQuery, + onCategorySelect, + onCategoryDeselect, + onBrandSelect, + onBrandDeselect, + onBrandSearchChange, +}) => { + const { t } = useTranslation(); + + if (searchQuery) return null; + + return ( + + ); +}; + +export default CategoryFilters; diff --git a/src/pages/Category/hooks/useCategoryData.js b/src/pages/Category/hooks/useCategoryData.js new file mode 100644 index 0000000..aec25b4 --- /dev/null +++ b/src/pages/Category/hooks/useCategoryData.js @@ -0,0 +1,110 @@ +import { useState, useEffect, useMemo } from "react"; +import { useGetCategoriesQuery } from "../../../app/api/categories"; +import { useGetCollectionByIdQuery } from "../../../app/api/collectionsApi"; +import { + useGetFiltersQuery, + useLazyGetFiltersQuery, +} from "../../../app/api/filtersApi"; + +const useCategoryData = ({ + categoryId, + collectionId, + brandId, + selectedFilterCategory, + searchQuery, +}) => { + const [selectedCategory, setSelectedCategory] = useState(null); + + const { data: categoriesData } = useGetCategoriesQuery("tree"); + + const filterParams = useMemo(() => { + if (searchQuery) return null; + if (selectedFilterCategory) return { category_id: selectedFilterCategory }; + if (categoryId) return { category_id: categoryId }; + if (collectionId) return { collection_id: collectionId }; + if (brandId) return { brand_id: brandId }; + return null; + }, [categoryId, collectionId, brandId, selectedFilterCategory, searchQuery]); + + const { + data: filtersData, + isLoading: filtersLoading, + error: filtersError, + } = useGetFiltersQuery(filterParams, { + skip: !filterParams, + }); + + const [fetchFilters, { data: lazyFiltersData }] = useLazyGetFiltersQuery(); + + const { + data: collectionData, + isLoading: collectionLoading, + error: collectionError, + } = useGetCollectionByIdQuery(collectionId, { + skip: !collectionId, + }); + + const isSubCategory = useMemo(() => { + if (!categoriesData?.data || !categoryId) return false; + + const checkIsSubCategory = (categories, targetId) => { + for (const category of categories) { + if (category.children) { + for (const subCategory of category.children) { + if (subCategory.id === parseInt(targetId)) return true; + if (subCategory.children) { + const found = checkIsSubCategory([subCategory], targetId); + if (found) return true; + } + } + } + } + return false; + }; + + return checkIsSubCategory(categoriesData.data, parseInt(categoryId)); + }, [categoriesData, categoryId]); + + const activeFilters = useMemo(() => { + return selectedFilterCategory && lazyFiltersData + ? lazyFiltersData + : filtersData; + }, [selectedFilterCategory, lazyFiltersData, filtersData]); + + useEffect(() => { + if (!categoryId || !categoriesData?.data) { + setSelectedCategory(null); + return; + } + + const findCategory = (categories, targetId) => { + for (const cat of categories) { + if (cat.id === parseInt(targetId)) return cat; + if (cat.children) { + const found = findCategory(cat.children, targetId); + if (found) return found; + } + } + return null; + }; + + const category = findCategory(categoriesData.data, parseInt(categoryId)); + setSelectedCategory(category); + }, [categoryId, categoriesData]); + + const isLoading = filtersLoading || collectionLoading; + const hasError = filtersError || collectionError; + + return { + categoriesData, + selectedCategory, + isSubCategory, + filtersData: activeFilters, + collectionData, + isLoading, + hasError, + fetchFilters, + }; +}; + +export default useCategoryData; diff --git a/src/pages/Category/hooks/useCategoryProducts.js b/src/pages/Category/hooks/useCategoryProducts.js new file mode 100644 index 0000000..c0f8a56 --- /dev/null +++ b/src/pages/Category/hooks/useCategoryProducts.js @@ -0,0 +1,316 @@ +import { useState, useEffect, useMemo, useRef } from "react"; +import { + useGetCategoryProductsQuery, + useLazyGetAllCategoryProductsPaginatedQuery, +} from "../../../app/api/categories"; +import { useLazyGetBrandProductsQuery } from "../../../app/api/brandsApi"; +import { useLazyGetCollectionProductsPaginatedQuery } from "../../../app/api/collectionsApi"; + +const useCategoryProducts = ({ + categoryId, + collectionId, + brandId, + selectedCategory, + isSubCategory, + currentPage, + selectedFilterCategory, + selectedFilterBrand, + minPrice, + maxPrice, + searchQuery, +}) => { + const [products, setProducts] = useState([]); + const [hasMore, setHasMore] = useState(true); + + const isFetchingRef = useRef(false); + const lastFetchKeyRef = useRef(null); + const abortControllerRef = useRef(null); + + const contextId = useMemo(() => { + const parts = [ + selectedFilterCategory && `fcat-${selectedFilterCategory}`, + categoryId && `cat-${categoryId}`, + brandId && `brand-${brandId}`, + collectionId && `col-${collectionId}`, + selectedFilterBrand && `fbrand-${selectedFilterBrand}`, + ].filter(Boolean); + return parts.join("|") || "none"; + }, [ + selectedFilterCategory, + categoryId, + brandId, + collectionId, + selectedFilterBrand, + ]); + + const fetchParams = useMemo( + () => ({ + page: currentPage, + limit: 6, + brands: selectedFilterBrand || undefined, + min_price: minPrice || undefined, + max_price: maxPrice || undefined, + }), + [currentPage, selectedFilterBrand, minPrice, maxPrice] + ); + + const fetchKey = `${contextId}-p${currentPage}`; + + const shouldUseBaseQuery = + categoryId && + !isSubCategory && + !searchQuery && + !selectedFilterCategory && + !selectedFilterBrand && + !brandId && + !collectionId; + + const { + data: paginatedCategoryProducts, + isLoading: categoryLoading, + isFetching: categoryFetching, + } = useGetCategoryProductsQuery( + { + categoryId: categoryId, + page: currentPage, + min_price: minPrice || undefined, + max_price: maxPrice || undefined, + }, + { + skip: !shouldUseBaseQuery, + } + ); + + const [ + fetchCategoryPaginated, + { + data: lazyCategoryProducts, + isLoading: lazyCategoryLoading, + isFetching: lazyCategoryFetching, + reset: resetCategoryPaginated, + }, + ] = useLazyGetAllCategoryProductsPaginatedQuery(); + + const [ + fetchBrandPaginated, + { + data: paginatedBrandProducts, + isLoading: brandPaginatedLoading, + isFetching: brandFetching, + reset: resetBrandPaginated, + }, + ] = useLazyGetBrandProductsQuery(); + + const [ + fetchCollectionPaginated, + { + data: paginatedCollectionProducts, + isLoading: collectionPaginatedLoading, + isFetching: collectionFetching, + reset: resetCollectionPaginated, + }, + ] = useLazyGetCollectionProductsPaginatedQuery(); + + useEffect(() => { + setProducts([]); + setHasMore(true); + + resetCategoryPaginated?.(); + resetBrandPaginated?.(); + resetCollectionPaginated?.(); + + lastFetchKeyRef.current = null; + isFetchingRef.current = false; + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + }, [ + contextId, + resetCategoryPaginated, + resetBrandPaginated, + resetCollectionPaginated, + ]); + + useEffect(() => { + if (searchQuery) return; + + if (lastFetchKeyRef.current === fetchKey) { + return; + } + + if (isFetchingRef.current) { + return; + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + isFetchingRef.current = true; + lastFetchKeyRef.current = fetchKey; + + const executeFetch = async () => { + try { + if (selectedFilterBrand) { + await fetchBrandPaginated({ + id: selectedFilterBrand, + ...fetchParams, + }); + return; + } + + if (selectedFilterCategory) { + await fetchCategoryPaginated({ + category: { + id: selectedFilterCategory, + children: [], + }, + ...fetchParams, + }); + return; + } + + if (categoryId && isSubCategory) { + await fetchCategoryPaginated({ + category: { + id: parseInt(categoryId), + children: [], + }, + ...fetchParams, + }); + return; + } + + if (brandId) { + await fetchBrandPaginated({ + id: brandId, + ...fetchParams, + }); + return; + } + + if (collectionId) { + await fetchCollectionPaginated({ + collectionId, + ...fetchParams, + }); + return; + } + } catch (error) { + if (error.name !== "AbortError") { + console.error("Fetch error:", error); + } + } finally { + isFetchingRef.current = false; + } + }; + + executeFetch(); + + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, [ + fetchKey, + searchQuery, + selectedFilterBrand, + selectedFilterCategory, + categoryId, + isSubCategory, + brandId, + collectionId, + fetchParams, + fetchCategoryPaginated, + fetchBrandPaginated, + fetchCollectionPaginated, + ]); + + useEffect(() => { + const updateProducts = (newData, hasNextPage) => { + if (!newData || newData.length === 0) { + if (currentPage === 1) { + setProducts([]); + setHasMore(false); + } + return; + } + + setProducts((prev) => { + if (currentPage === 1) { + return newData; + } + + const existingIds = new Set(prev.map((p) => p.id)); + const newProducts = newData.filter((p) => !existingIds.has(p.id)); + + return newProducts.length > 0 ? [...prev, ...newProducts] : prev; + }); + + setHasMore(hasNextPage); + }; + + if (paginatedCategoryProducts && shouldUseBaseQuery) { + updateProducts( + paginatedCategoryProducts.data || [], + !!paginatedCategoryProducts.pagination?.next_page_url + ); + return; + } + + if (lazyCategoryProducts) { + updateProducts( + lazyCategoryProducts.data || [], + lazyCategoryProducts.pagination?.hasMorePages || false + ); + return; + } + + // Brand products + if (paginatedBrandProducts) { + updateProducts( + paginatedBrandProducts.data || [], + !!paginatedBrandProducts.pagination?.next_page_url + ); + return; + } + + if (paginatedCollectionProducts) { + updateProducts( + paginatedCollectionProducts.data || [], + !!paginatedCollectionProducts.pagination?.next_page_url + ); + } + }, [ + paginatedCategoryProducts, + lazyCategoryProducts, + paginatedBrandProducts, + paginatedCollectionProducts, + currentPage, + shouldUseBaseQuery, + ]); + + const isLoading = + categoryLoading || + lazyCategoryLoading || + brandPaginatedLoading || + collectionPaginatedLoading || + categoryFetching || + lazyCategoryFetching || + brandFetching || + collectionFetching; + + return { + products, + hasMore, + isLoading, + setProducts, + setHasMore, + }; +}; + +export default useCategoryProducts; diff --git a/src/pages/Category/index.jsx b/src/pages/Category/index.jsx index 23f1d09..d97c17f 100644 --- a/src/pages/Category/index.jsx +++ b/src/pages/Category/index.jsx @@ -1,761 +1,338 @@ -import React, { useEffect, useState, useMemo, useCallback } from "react"; +"use client"; + +import { useEffect, useState, useMemo, useRef } from "react"; +import { useParams, useLocation, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Result, Button } from "antd"; +import InfiniteScroll from "react-infinite-scroll-component"; + import styles from "./CategoryPage.module.scss"; import ProductCard from "../../components/ProductCard/index"; -import { TiTick } from "react-icons/ti"; import BrandSidebar from "../../components/BrandsSidebar/index"; import FilterSidebar from "../../components/FilterSideBar/index"; -import { useTranslation } from "react-i18next"; -import InfiniteScroll from "react-infinite-scroll-component"; -import { - useParams, - useLocation, - useNavigate, - useSearchParams, -} from "react-router-dom"; -import { - useGetCategoriesQuery, - useGetAllCategoryProductsQuery, - useGetCategoryProductsQuery, - useLazyGetAllCategoryProductsPaginatedQuery, -} from "../../app/api/categories"; -import { - useGetCollectionByIdQuery, - useGetCollectionProductsQuery, - useLazyGetCollectionProductsPaginatedQuery, -} from "../../app/api/collectionsApi"; -import { - useGetBrandProductsQuery, - useLazyGetBrandProductsQuery, -} from "../../app/api/brandsApi"; import Loader from "../../components/Loader/index"; -import { Result, Button } from "antd"; + +import CategoryFilters from "./components/CategoryFilters"; +import CategoryBreadcrumbs from "./components/CategoryBreadcrumbs"; +import useCategoryData from "./hooks/useCategoryData"; +import useCategoryProducts from "./hooks/useCategoryProducts"; const CategoryPage = () => { const { t } = useTranslation(); const { categoryId, collectionId, brandId } = useParams(); const location = useLocation(); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - // Get categories data - const { data: categoriesData } = useGetCategoriesQuery("tree"); - const [selectedCategory, setSelectedCategory] = useState(null); + const [pageState, setPageState] = useState({ + currentPage: 1, + minPrice: "", + maxPrice: "", + }); - // Track if the category has subcategories to display - const [hasSubcategories, setHasSubcategories] = useState(false); - const [subcategoriesToShow, setSubcategoriesToShow] = useState([]); + const [filterState, setFilterState] = useState({ + selectedFilterCategory: null, + selectedFilterBrand: null, + brandSearchQuery: "", + }); - // Pagination state - const [currentPage, setCurrentPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [allProducts, setAllProducts] = useState([]); + const routeKey = useMemo( + () => `${categoryId || "x"}-${collectionId || "x"}-${brandId || "x"}`, + [categoryId, collectionId, brandId] + ); - // Price sorting state - const [priceSort, setPriceSort] = useState("none"); + const prevRouteRef = useRef(routeKey); + const isInitialMount = useRef(true); - // Search related data const searchResults = location.state?.searchData?.data || []; const searchQuery = location.state?.searchQuery || null; - // Determine if category is a subcategory - const isSubCategory = useMemo(() => { - if (!categoriesData?.data || !categoryId) return false; - - const checkIsSubCategory = (categories, targetId) => { - for (const category of categories) { - if (category.children) { - for (const subCategory of category.children) { - if (subCategory.id === parseInt(targetId)) { - return true; - } - - // Check deeper nested levels - if (subCategory.children) { - const foundInNested = checkIsSubCategory([subCategory], targetId); - if (foundInNested) return true; - } - } - } - } - return false; - }; - - return checkIsSubCategory(categoriesData.data, parseInt(categoryId)); - }, [categoriesData, categoryId]); - - // Original query for first page data - only run on first page load const { - data: categoryProducts = [], - error: categoryError, - isLoading: categoryLoading, - } = useGetAllCategoryProductsQuery(selectedCategory, { - skip: !selectedCategory || !categoryId || !isSubCategory || currentPage > 1, - }); - - // New lazy query for subcategory pagination - const [ - fetchSubcategoryProducts, - { - data: paginatedSubcategoryProducts, - isLoading: subcategoryProductsLoading, - }, - ] = useLazyGetAllCategoryProductsPaginatedQuery(); - - // Paginated category products query - const { - data: paginatedCategoryProducts, - isLoading: paginatedCategoryLoading, - isFetching: paginatedCategoryFetching, - } = useGetCategoryProductsQuery( - { categoryId, page: currentPage }, - { skip: !categoryId || isSubCategory || searchQuery } - ); - - // Brand products with lazy loading for pagination - const [ - fetchBrandProducts, - { data: paginatedBrandProducts, isLoading: brandProductsFetching }, - ] = useLazyGetBrandProductsQuery(); - - // Regular brand products query for initial data - const { - data: brandProducts = [], - isLoading: brandProductsLoading, - error: brandProductsError, - } = useGetBrandProductsQuery(brandId, { - skip: !brandId || currentPage > 1, - }); - - // Collection data - const { - data: collectionData, - error: collectionError, - isLoading: collectionLoading, - } = useGetCollectionByIdQuery(collectionId, { - skip: !collectionId, - }); - - // New paginated collection products (lazy query) - const [ - fetchCollectionProducts, - { - data: paginatedCollectionProducts, - isLoading: paginatedCollectionLoading, - }, - ] = useLazyGetCollectionProductsPaginatedQuery(); - - // Original collection products query for first page data - const { - data: collectionProducts = { data: [] }, - error: collectionProductsError, - isLoading: collectionProductsLoading, - } = useGetCollectionProductsQuery(collectionId, { - skip: !collectionId || currentPage > 1, - }); - - // Load subcategory products when page changes - useEffect(() => { - if (categoryId && isSubCategory && selectedCategory && currentPage > 1) { - fetchSubcategoryProducts({ - category: selectedCategory, - page: currentPage, - limit: 6, - }); - } - }, [ - categoryId, - isSubCategory, + categoriesData, selectedCategory, - currentPage, - fetchSubcategoryProducts, - ]); - - // Fetch collection products when page changes - useEffect(() => { - if (collectionId && currentPage > 1) { - fetchCollectionProducts({ - collectionId, - page: currentPage, - limit: 6, - }); - } - }, [collectionId, currentPage, fetchCollectionProducts]); - - // Fetch more brand products when page changes - useEffect(() => { - if (brandId && currentPage > 1) { - fetchBrandProducts({ - type: undefined, - page: currentPage, - limit: 6, - id: brandId, - }); - } - }, [brandId, currentPage, fetchBrandProducts]); - - const isProductInList = (list, newProduct) => { - return list.some((product) => product.id === newProduct.id); - }; - - useEffect(() => { - if ( - paginatedCategoryProducts && - categoryId && - !isSubCategory && - !searchQuery - ) { - if ( - paginatedCategoryProducts.data && - paginatedCategoryProducts.data.length > 0 - ) { - setAllProducts((prevProducts) => { - if (currentPage === 1) { - return [...paginatedCategoryProducts.data]; - } - - const newProducts = paginatedCategoryProducts.data.filter( - (newProduct) => !isProductInList(prevProducts, newProduct) - ); - - return [...prevProducts, ...newProducts]; - }); - - setHasMore(!!paginatedCategoryProducts.pagination.next_page_url); - } else if (currentPage === 1) { - setAllProducts([]); - setHasMore(false); - } else { - setHasMore(false); - } - } - - if ( - paginatedSubcategoryProducts && - categoryId && - isSubCategory && - !searchQuery - ) { - if ( - paginatedSubcategoryProducts.data && - paginatedSubcategoryProducts.data.length > 0 - ) { - setAllProducts((prevProducts) => { - if (currentPage === 1) { - return [...paginatedSubcategoryProducts.data]; - } - - const newProducts = paginatedSubcategoryProducts.data.filter( - (newProduct) => !isProductInList(prevProducts, newProduct) - ); - - return [...prevProducts, ...newProducts]; - }); - - setHasMore( - paginatedSubcategoryProducts.pagination?.hasMorePages || false - ); - } else if (currentPage > 1) { - setHasMore(false); - } - } - - if (paginatedBrandProducts && brandId) { - if (paginatedBrandProducts.length > 0) { - setAllProducts((prevProducts) => { - if (currentPage === 1) { - return [...paginatedBrandProducts]; - } - - const newProducts = paginatedBrandProducts.filter( - (newProduct) => !isProductInList(prevProducts, newProduct) - ); - - return [...prevProducts, ...newProducts]; - }); - - setHasMore(paginatedBrandProducts.length === 6); - } else if (currentPage > 1) { - setHasMore(false); - } - } - - if (paginatedCollectionProducts && collectionId) { - if ( - paginatedCollectionProducts.data && - paginatedCollectionProducts.data.length > 0 - ) { - setAllProducts((prevProducts) => { - if (currentPage === 1) { - return [...paginatedCollectionProducts.data]; - } - - const newProducts = paginatedCollectionProducts.data.filter( - (newProduct) => !isProductInList(prevProducts, newProduct) - ); - - return [...prevProducts, ...newProducts]; - }); - - setHasMore(!!paginatedCollectionProducts.pagination?.next_page_url); - } else if (currentPage > 1) { - setHasMore(false); - } - } - }, [ - paginatedCategoryProducts, - paginatedBrandProducts, - paginatedCollectionProducts, - paginatedSubcategoryProducts, - currentPage, - categoryId, - brandId, - collectionId, isSubCategory, + filtersData, + collectionData, + isLoading: dataLoading, + hasError: dataError, + fetchFilters, + } = useCategoryData({ + categoryId, + collectionId, + brandId, + selectedFilterCategory: filterState.selectedFilterCategory, searchQuery, - ]); + }); - const findCategoryById = (categories, id) => { - if (!categories) return null; - - const parsedId = typeof id === "string" ? parseInt(id) : id; - - for (const category of categories) { - if (category.id === parsedId) return category; - if (category.children) { - const found = findCategoryById(category.children, parsedId); - if (found) return found; - } - } - return null; - }; - - // Get parent category for the current subcategory - const getParentCategory = (categories, childId) => { - if (!categories) return null; - - const parsedChildId = - typeof childId === "string" ? parseInt(childId) : childId; - - for (const category of categories) { - if (category.children) { - for (const child of category.children) { - if (child.id === parsedChildId) { - return category; - } - - // Check deeper nested levels - if (child.children) { - const foundInNested = getParentCategory([child], parsedChildId); - if (foundInNested) return child; - } - } - } - } - - return null; - }; - - // Find and setup the selected category and its subcategories - useEffect(() => { - if (categoriesData?.data && categoryId) { - const category = findCategoryById( - categoriesData.data, - parseInt(categoryId) - ); - - if (selectedCategory?.id !== category?.id) { - // Reset product-related states - setAllProducts([]); - setHasMore(true); - setCurrentPage(1); - setSelectedCategory(category); - - // Set subcategories to display in sidebar - // Set subcategories to display in sidebar - if (category) { - if (category.children && category.children.length > 0) { - // If the category has children, show them - setHasSubcategories(true); - setSubcategoriesToShow(category.children); - } else { - // If this is a subcategory without children, don't show any category in sidebar - setHasSubcategories(false); - setSubcategoriesToShow([]); - } - } - } - } - }, [categoriesData, categoryId, selectedCategory]); - - useEffect(() => { - setCurrentPage(1); - setAllProducts([]); - setHasMore(true); - }, [categoryId, brandId, collectionId, searchQuery, location.key]); - - useEffect(() => { - if ( - categoryId && - isSubCategory && - categoryProducts?.length > 0 && - currentPage === 1 - ) { - setAllProducts(categoryProducts); - } else if (brandId && brandProducts?.length > 0 && currentPage === 1) { - setAllProducts(brandProducts); - } else if ( - collectionId && - collectionProducts?.data?.length > 0 && - currentPage === 1 - ) { - setAllProducts(collectionProducts.data); - } - }, [ - categoryId, - categoryProducts, - brandId, - brandProducts, - collectionId, - collectionProducts, - currentPage, - isSubCategory, - ]); - - const loadMoreData = useCallback(() => { - if ( - !hasMore || - paginatedCategoryFetching || - brandProductsFetching || - paginatedCollectionLoading || - subcategoryProductsLoading - ) - return; - - setCurrentPage((prevPage) => prevPage + 1); - }, [ + const { + products: allProducts, hasMore, - paginatedCategoryFetching, - brandProductsFetching, - paginatedCollectionLoading, - subcategoryProductsLoading, - ]); - - const isLoading = - categoryLoading || - collectionLoading || - collectionProductsLoading || - brandProductsLoading || - (paginatedCategoryLoading && currentPage === 1) || - (subcategoryProductsLoading && currentPage === 1) || - (paginatedCollectionLoading && currentPage === 1); - - const hasError = - categoryError || - collectionError || - collectionProductsError || - brandProductsError; - - const products = useMemo(() => { - let productsList = []; - - if (searchQuery) productsList = [...searchResults]; - else productsList = [...allProducts]; - - if (priceSort === "lowToHigh") { - return [...productsList].sort( - (a, b) => (a.price_amount || 0) - (b.price_amount || 0) - ); - } else if (priceSort === "highToLow") { - return [...productsList].sort( - (a, b) => (b.price_amount || 0) - (a.price_amount || 0) - ); - } - - return productsList; - }, [searchQuery, searchResults, priceSort, allProducts]); - - const totalItems = useMemo(() => { - if ( - paginatedCategoryProducts?.pagination && - !isSubCategory && - !searchQuery && - categoryId - ) { - return paginatedCategoryProducts.pagination.total || products.length || 0; - } - return products.length || 0; - }, [ - paginatedCategoryProducts, - products, - isSubCategory, - searchQuery, + isLoading: productsLoading, + setProducts: setAllProducts, + setHasMore, + } = useCategoryProducts({ categoryId, + collectionId, + brandId, + selectedCategory, + isSubCategory, + currentPage: pageState.currentPage, + selectedFilterCategory: filterState.selectedFilterCategory, + selectedFilterBrand: filterState.selectedFilterBrand, + minPrice: pageState.minPrice, + maxPrice: pageState.maxPrice, + searchQuery, + }); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + prevRouteRef.current = routeKey; + return; + } + + if (prevRouteRef.current === routeKey) return; + + prevRouteRef.current = routeKey; + + setAllProducts([]); + setHasMore(true); + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setFilterState({ + selectedFilterCategory: null, + selectedFilterBrand: null, + brandSearchQuery: "", + }); + + if (location.state?.clearFilters) { + navigate(location.pathname, { replace: true, state: {} }); + } + }, [ + routeKey, + location.state?.clearFilters, + navigate, + setAllProducts, + setHasMore, ]); - const handlePriceSortChange = (sortType) => { - setPriceSort(sortType); - }; + const filteredProducts = useMemo(() => { + let list = searchQuery ? searchResults : allProducts; - const handleAddToCart = (product) => { - console.log("Adding to cart:", product); - }; - - const handleToggleFavorite = (product) => { - console.log("Toggling favorite:", product); - }; - - const handleSubCategorySelect = (subCategoryId) => { - // Reset product-related states - setAllProducts([]); - setCurrentPage(1); - setHasMore(true); - setPriceSort("none"); - - // Use the existing category data instead of fetching again - const newCategory = findCategoryById( - categoriesData?.data, - parseInt(subCategoryId) - ); - setSelectedCategory(newCategory); - - // Update URL without full navigation - navigate(`/category/${subCategoryId}`, { replace: true }); - - // Fetch products for new subcategory - if (newCategory) { - fetchSubcategoryProducts({ - category: newCategory, - page: 1, - limit: 6, + if (pageState.minPrice || pageState.maxPrice) { + const min = pageState.minPrice ? parseFloat(pageState.minPrice) : 0; + const max = pageState.maxPrice + ? parseFloat(pageState.maxPrice) + : Infinity; + list = list.filter((p) => { + const price = p.price_amount || 0; + return price >= min && price <= max; }); } - }; - const handleCategoryClick = (categoryId) => { - setAllProducts([]); - setCurrentPage(1); - setHasMore(true); - navigate(`/category/${categoryId}`); - }; + return list; + }, [ + searchQuery, + searchResults, + allProducts, + pageState.minPrice, + pageState.maxPrice, + ]); - const renderBreadcrumbs = () => { - if (!categoriesData?.data || !selectedCategory) return null; + const totalItems = filteredProducts.length; - const breadcrumbs = []; - let currentCategory = selectedCategory; - let parentId = currentCategory.parent_id; - - // Add the current category first - breadcrumbs.unshift(currentCategory); - - // Then add all parent categories - while (parentId) { - const parentCategory = findCategoryById(categoriesData.data, parentId); - if (parentCategory) { - breadcrumbs.unshift(parentCategory); - parentId = parentCategory.parent_id; - } else { - break; - } - } - - return ( -
- {breadcrumbs.map((category, index) => ( - - handleCategoryClick(category.id)} - > - {category.name} - - {index < breadcrumbs.length - 1 && ( - / - )} - - ))} -
- ); - }; - - // Page title based on context const pageTitle = useMemo(() => { if (searchQuery) return `${t("search.resultsFor")}: "${searchQuery}"`; + if (filterState.selectedFilterCategory) { + const cat = findCategoryById( + categoriesData?.data, + filterState.selectedFilterCategory + ); + return cat?.name || "Category"; + } if (brandId) return "Brand Products"; - if (categoryId && selectedCategory) return selectedCategory.name; - if (categoryId) return "Category"; + if (selectedCategory) return selectedCategory.name; if (collectionData?.data?.name) return collectionData.data.name; - return "Collection"; - }, [searchQuery, brandId, categoryId, selectedCategory, collectionData, t]); + return "Products"; + }, [ + searchQuery, + filterState.selectedFilterCategory, + brandId, + selectedCategory, + collectionData, + categoriesData, + t, + ]); - const renderSubCategories = () => { - if (!selectedCategory?.children || searchQuery) return null; + const handleFilterCategorySelect = (categoryId) => { + setFilterState((prev) => ({ + ...prev, + selectedFilterCategory: categoryId, + selectedFilterBrand: null, + })); - return ( -
- {selectedCategory.children.map((subCategory) => ( - - ))} -
- ); + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setAllProducts([]); + setHasMore(true); + + fetchFilters({ category_id: categoryId }); }; - if (isLoading) return ; - if (hasError) + const handleFilterCategoryDeselect = () => { + setFilterState((prev) => ({ + ...prev, + selectedFilterCategory: null, + })); + + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setAllProducts([]); + setHasMore(true); + + if (categoryId) fetchFilters({ category_id: categoryId }); + }; + + const handleFilterBrandSelect = (brandId) => { + setFilterState((prev) => ({ + ...prev, + selectedFilterBrand: brandId, + })); + + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setAllProducts([]); + setHasMore(true); + }; + + const handleFilterBrandDeselect = () => { + setFilterState((prev) => ({ + ...prev, + selectedFilterBrand: null, + })); + + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setAllProducts([]); + setHasMore(true); + }; + + const handleCategoryClick = (targetId) => { + setFilterState({ + selectedFilterCategory: null, + selectedFilterBrand: null, + brandSearchQuery: "", + }); + setPageState({ currentPage: 1, minPrice: "", maxPrice: "" }); + setAllProducts([]); + setHasMore(true); + + navigate(`/category/${targetId}`, { + replace: false, + state: { clearFilters: true, timestamp: Date.now() }, + }); + }; + + const loadMoreData = () => { + if (!hasMore || productsLoading) return; + setPageState((prev) => ({ ...prev, currentPage: prev.currentPage + 1 })); + }; + + if (dataLoading) return ; + if (dataError) { return ( -
- navigate("/")}> - Baş sahypa gidiň - - } - /> -
+ navigate("/")}> + Baş sahypa gidiň + + } + /> ); + } + + const isInitialLoad = + (productsLoading && allProducts.length === 0) || + (filterState.selectedFilterBrand && + allProducts.length === 0 && + productsLoading); return ( -
- {categoryId && renderBreadcrumbs()} +
+ {(categoryId || filterState.selectedFilterCategory) && ( + + )} +

{pageTitle}

{t("category.total")}: {totalItems} {t("category.items")}

+
- - + {/* {}} currentPriceSort="none" /> */}
- {renderSubCategories()} + {selectedCategory?.children && !searchQuery && ( +
+ {selectedCategory.children.map((sub) => ( + + ))} +
+ )}
- + + setFilterState((prev) => ({ ...prev, brandSearchQuery: query })) + } + />
- {products.length > 0 ? ( + {isInitialLoad ? ( +
+ +
+ ) : filteredProducts.length > 0 ? (
} className={styles.productGrid} - key={`scroll-${categoryId || collectionId || brandId}`} > - {products.map((product) => ( + {filteredProducts.map((product) => ( ))} @@ -768,4 +345,16 @@ const CategoryPage = () => { ); }; +function findCategoryById(categories, targetId) { + if (!categories) return null; + for (const cat of categories) { + if (cat.id === targetId) return cat; + if (cat.children) { + const found = findCategoryById(cat.children, targetId); + if (found) return found; + } + } + return null; +} + export default CategoryPage;