added filter to category page
This commit is contained in:
@@ -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 || {},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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 || {},
|
||||
|
||||
77
src/app/api/filtersApi.js
Normal file
77
src/app/api/filtersApi.js
Normal file
@@ -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
|
||||
@@ -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
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.sidebarContainer}>
|
||||
<button onClick={handleToggle} className={styles.mobileNavButton}>
|
||||
<img src={brand} alt="" />
|
||||
{t("navbar.brands")}
|
||||
{selectedBrand && <span className={styles.badge}>1</span>}
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
@@ -67,16 +60,30 @@ const Sidebar = () => {
|
||||
onChange={handleSearch}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<div className={styles.brandsList}>
|
||||
{filteredBrands.map((brand, index) => (
|
||||
<div key={index} className={styles.brandItem}>
|
||||
<Checkbox>{brand}</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredBrands.length > 0 ? (
|
||||
<div className={styles.brandsList}>
|
||||
{filteredBrands.map((brand) => (
|
||||
<div key={brand.id} className={styles.brandItem}>
|
||||
<Checkbox
|
||||
checked={selectedBrand === brand.id}
|
||||
onChange={(e) => handleBrandChange(brand.id, e.target.checked)}
|
||||
>
|
||||
{brand.name}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyState}>
|
||||
{brands.length === 0
|
||||
? t("common.noData")
|
||||
: t("common.noResults")}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
export default BrandSidebar;
|
||||
@@ -5,6 +5,7 @@
|
||||
margin: auto;
|
||||
padding: 20px 1.375rem;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
|
||||
59
src/pages/Category/components/CategoryBreadcrumbs.jsx
Normal file
59
src/pages/Category/components/CategoryBreadcrumbs.jsx
Normal file
@@ -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 (
|
||||
<Breadcrumb className={styles.breadcrumb}>
|
||||
|
||||
|
||||
{breadcrumbs.map((crumb, index) => {
|
||||
const isLast = index === breadcrumbs.length - 1;
|
||||
|
||||
return (
|
||||
<Breadcrumb.Item key={crumb.id}>
|
||||
{isLast ? (
|
||||
<span>{crumb.name}</span>
|
||||
) : (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onCategoryClick?.(crumb.id);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{crumb.name}
|
||||
</a>
|
||||
)}
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
})}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryBreadcrumbs;
|
||||
98
src/pages/Category/components/CategoryFilters.jsx
Normal file
98
src/pages/Category/components/CategoryFilters.jsx
Normal file
@@ -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 (
|
||||
<aside className={styles.sidebar}>
|
||||
{filtersData?.categories?.length > 0 && (
|
||||
<div className={styles.filterSection}>
|
||||
<h3>{t("category.subCategories")}</h3>
|
||||
<ul>
|
||||
{filtersData.categories.map((category) => (
|
||||
<li
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
if (selectedFilterCategory === category.id) {
|
||||
onCategoryDeselect();
|
||||
} else {
|
||||
onCategorySelect(category.id);
|
||||
}
|
||||
}}
|
||||
className={
|
||||
selectedFilterCategory === category.id
|
||||
? styles.activeSubcategory
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{category.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filtersData?.brands?.length > 0 && (
|
||||
<div className={styles.filterSection}>
|
||||
<h3>{t("navbar.brands")}</h3>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Gözleg"
|
||||
value={brandSearchQuery}
|
||||
onChange={(e) => onBrandSearchChange(e.target.value)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
|
||||
<ul>
|
||||
{filtersData.brands
|
||||
.filter((brand) =>
|
||||
brand.name
|
||||
.toLowerCase()
|
||||
.includes(brandSearchQuery.toLowerCase())
|
||||
)
|
||||
.map((brand) => (
|
||||
<li key={brand.id}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFilterBrand === brand.id}
|
||||
onChange={() => {
|
||||
if (selectedFilterBrand === brand.id) {
|
||||
onBrandDeselect();
|
||||
} else {
|
||||
onBrandSelect(brand.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className={styles.customCheckbox}>
|
||||
<TiTick className={styles.checkIcon} />
|
||||
</span>
|
||||
{brand.name}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryFilters;
|
||||
110
src/pages/Category/hooks/useCategoryData.js
Normal file
110
src/pages/Category/hooks/useCategoryData.js
Normal file
@@ -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;
|
||||
316
src/pages/Category/hooks/useCategoryProducts.js
Normal file
316
src/pages/Category/hooks/useCategoryProducts.js
Normal file
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user