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;