first commit

This commit is contained in:
Jelaletdin12
2026-02-01 20:55:57 +05:00
commit b8c871750a
128 changed files with 23114 additions and 0 deletions

View File

@@ -0,0 +1,234 @@
import { useCallback } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import type { FilterBrand, FilterCategory } from "@/lib/types/api";
interface FiltersData {
categories: FilterCategory[];
brands: FilterBrand[];
}
interface CategoryFiltersProps {
filtersData: FiltersData | undefined;
selectedBrands: Set<number>;
selectedFilterCategories: Set<number>;
priceSort: "none" | "lowToHigh" | "highToLow";
priceRange: [number, number];
onBrandToggle: (brandId: number) => void;
onCategoryToggle: (categoryId: number) => void;
onPriceSortChange: (sortType: "none" | "lowToHigh" | "highToLow") => void;
onPriceChange: (values: number[]) => void;
onReset: () => void;
translations: {
category: string;
brands: string;
sort: string;
default: string;
price_low_to_high: string;
price_high_to_low: string;
price: string;
price_from: string;
price_to: string;
reset: string;
};
}
export default function CategoryFilters({
filtersData,
selectedBrands,
selectedFilterCategories,
priceSort,
priceRange,
onBrandToggle,
onCategoryToggle,
onPriceSortChange,
onPriceChange,
onReset,
translations,
}: CategoryFiltersProps) {
return (
<div className="space-y-6 mb-6">
{filtersData?.categories && filtersData.categories.length > 0 && (
<FilterSection title={translations.category}>
{filtersData.categories.map((category) => (
<CheckboxItem
key={category.id}
checked={selectedFilterCategories.has(category.id)}
onCheckedChange={() => onCategoryToggle(category.id)}
label={category.name}
/>
))}
</FilterSection>
)}
{filtersData?.brands && filtersData.brands.length > 0 && (
<FilterSection title={translations.brands}>
{filtersData.brands.map((brand) => (
<CheckboxItem
key={brand.id}
checked={selectedBrands.has(brand.id)}
onCheckedChange={() => onBrandToggle(brand.id)}
label={brand.name}
/>
))}
</FilterSection>
)}
{/* <FilterSection title={translations.sort}>
<RadioItem
name="sort"
checked={priceSort === "none"}
onChange={() => onPriceSortChange("none")}
label={translations.default}
/>
<RadioItem
name="sort"
checked={priceSort === "lowToHigh"}
onChange={() => onPriceSortChange("lowToHigh")}
label={translations.price_low_to_high}
/>
<RadioItem
name="sort"
checked={priceSort === "highToLow"}
onChange={() => onPriceSortChange("highToLow")}
label={translations.price_high_to_low}
/>
</FilterSection> */}
<PriceFilter
title={translations.price}
priceRange={priceRange}
onPriceChange={onPriceChange}
translations={{
from: translations.price_from,
to: translations.price_to,
}}
/>
<Button variant="outline" className="w-full rounded-lg cursor-pointer mb-6" onClick={onReset}>
{translations.reset}
</Button>
</div>
);
}
function FilterSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<h3 className="text-lg font-semibold mb-3">{title}</h3>
<div className="space-y-2">{children}</div>
</div>
);
}
function CheckboxItem({
checked,
onCheckedChange,
label,
}: {
checked: boolean;
onCheckedChange: () => void;
label: string;
}) {
return (
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
<span className="text-sm">{label}</span>
</label>
);
}
function RadioItem({
name,
checked,
onChange,
label,
}: {
name: string;
checked: boolean;
onChange: () => void;
label: string;
}) {
return (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name={name}
checked={checked}
onChange={onChange}
className="w-4 h-4"
/>
<span>{label}</span>
</label>
);
}
function PriceFilter({
title,
priceRange,
onPriceChange,
translations,
}: {
title: string;
priceRange: [number, number];
onPriceChange: (values: number[]) => void;
translations: { from: string; to: string };
}) {
return (
<div>
<h3 className="text-lg font-semibold mb-3">{title}</h3>
<div className="space-y-4">
<div className="flex gap-2">
<div className="flex-1">
<Label htmlFor="price-from" className="text-xs mb-1">
{translations.from}
</Label>
<Input
id="price-from"
type="number"
value={priceRange[0]}
onChange={(e) =>
onPriceChange([parseInt(e.target.value) || 0, priceRange[1]])
}
className="rounded-lg"
/>
</div>
<div className="flex-1">
<Label htmlFor="price-to" className="text-xs mb-1">
{translations.to}
</Label>
<Input
id="price-to"
type="number"
value={priceRange[1]}
onChange={(e) =>
onPriceChange([
priceRange[0],
parseInt(e.target.value) || 10000,
])
}
className="rounded-lg"
/>
</div>
</div>
<Slider
min={0}
max={99999}
step={100}
value={priceRange}
onValueChange={onPriceChange}
className="mt-2"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { SlidersHorizontal, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
interface CategoryFiltersSheetProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
filterLabel: string;
closeLabel: string;
children: React.ReactNode;
}
export default function CategoryFiltersSheet({
isOpen,
onOpenChange,
filterLabel,
closeLabel,
children,
}: CategoryFiltersSheetProps) {
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetTrigger asChild>
<Button
className="bg-[#005bff] hover:bg-[#0041c4] sm:hidden fixed bottom-20 right-4 rounded-lg cursor-pointer font-bold gap-2 z-10 shadow-lg"
size="lg"
>
{filterLabel}
<SlidersHorizontal className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[290px] p-0">
<SheetHeader className="p-4 border-b">
<SheetTitle>{filterLabel}</SheetTitle>
<button
onClick={() => onOpenChange(false)}
className="absolute top-4 right-4 rounded-md cursor-pointer ring-offset-background transition-opacity hover:opacity-100"
>
<X className="h-4 w-4" />
<span className="sr-only">{closeLabel}</span>
</button>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-80px)] p-4">
{children}
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,395 @@
"use client";
import { useEffect, useState, useMemo, useCallback } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import {
useCategories,
useCategoryFilters,
useFilteredCategoryProducts,
} from "@/features/category/hooks/useCategories";
import { useTranslations } from "next-intl";
import type { Category, Product } from "@/lib/types/api";
import CategoryFilters from "./CategoryFilters";
import CategoryProductsGrid from "./CategoryProductsGrid";
import CategoryFiltersSheet from "./CategoryFiltersSheet";
import ErrorPage from "@/components/ErrorPage";
interface CategoryPageClientProps {
params: { locale: string; slug: string };
}
export default function CategoryPageClient({
params,
}: CategoryPageClientProps) {
const { slug } = params;
const t = useTranslations();
const [isSheetOpen, setIsSheetOpen] = useState(false);
const {
data: categoriesData,
isLoading: categoriesLoading,
isError: categoriesError
} = useCategories();
const selectedCategory = useMemo(() => {
if (!categoriesData || !slug) return null;
const findBySlug = (categories: Category[]): Category | null => {
for (const category of categories) {
if (category.slug === slug) return category;
if (category.children) {
const found = findBySlug(category.children);
if (found) return found;
}
}
return null;
};
return findBySlug(categoriesData);
}, [categoriesData, slug]);
// State management
const [currentPage, setCurrentPage] = useState(1);
const [allProducts, setAllProducts] = useState<Product[]>([]);
const [priceSort, setPriceSort] = useState<
"none" | "lowToHigh" | "highToLow"
>("none");
const [priceRange, setPriceRange] = useState<[number, number]>([0, 10000]);
const [selectedBrands, setSelectedBrands] = useState<Set<number>>(new Set());
const [selectedFilterCategories, setSelectedFilterCategories] = useState<
Set<number>
>(new Set());
// Fetch filters
const {
data: filtersData,
isLoading: filtersLoading,
isError: filtersError
} = useCategoryFilters(selectedCategory?.id, {
enabled: !!selectedCategory,
});
// Build filter params
const filterParams = useMemo(() => {
const params: any = {
page: currentPage,
limit: 6,
};
if (selectedBrands.size > 0) {
params.brands = Array.from(selectedBrands);
}
if (selectedFilterCategories.size > 0) {
params.categories = Array.from(selectedFilterCategories);
}
params.min_price = priceRange[0];
params.max_price = priceRange[1];
return params;
}, [currentPage, selectedBrands, selectedFilterCategories, priceRange]);
// Fetch filtered products
const {
data: productsData,
isFetching,
isError: productsError
} = useFilteredCategoryProducts(
selectedCategory?.id?.toString() || "",
filterParams,
{ enabled: !!selectedCategory }
);
// Reset on category change
useEffect(() => {
if (selectedCategory) {
setAllProducts([]);
setCurrentPage(1);
setSelectedBrands(new Set());
setSelectedFilterCategories(new Set());
setPriceRange([0, 10000]);
setPriceSort("none");
}
}, [selectedCategory?.id]);
// Update products list
useEffect(() => {
if (productsData?.data) {
setAllProducts((prev) => {
if (currentPage === 1) {
return productsData.data;
}
const existingIds = new Set(prev.map((p) => p.id));
const newProducts = productsData.data.filter(
(p: Product) => !existingIds.has(p.id)
);
if (newProducts.length === 0) {
return prev;
}
return [...prev, ...newProducts];
});
}
}, [productsData?.data, currentPage]);
const hasMore = useMemo(() => {
if (!productsData?.pagination) return false;
if (productsData.pagination.next_page_url) return true;
if (
productsData.pagination.current_page &&
productsData.pagination.last_page
) {
return (
productsData.pagination.current_page < productsData.pagination.last_page
);
}
if (productsData.pagination.hasMorePages !== undefined) {
return productsData.pagination.hasMorePages;
}
return false;
}, [productsData?.pagination]);
const loadMoreData = useCallback(() => {
if (!hasMore || isFetching) return;
setCurrentPage((prev) => prev + 1);
}, [hasMore, isFetching]);
const sortedProducts = useMemo(() => {
const products = [...allProducts];
if (priceSort === "lowToHigh") {
return products.sort(
(a, b) =>
parseFloat(a.price_amount || "0") - parseFloat(b.price_amount || "0")
);
}
if (priceSort === "highToLow") {
return products.sort(
(a, b) =>
parseFloat(b.price_amount || "0") - parseFloat(a.price_amount || "0")
);
}
return products;
}, [allProducts, priceSort]);
// Filter handlers
const handleBrandToggle = useCallback((brandId: number) => {
setSelectedBrands((prev) => {
const newSet = new Set(prev);
newSet.has(brandId) ? newSet.delete(brandId) : newSet.add(brandId);
return newSet;
});
setCurrentPage(1);
setAllProducts([]);
}, []);
const handleCategoryToggle = useCallback((categoryId: number) => {
setSelectedFilterCategories((prev) => {
const newSet = new Set(prev);
newSet.has(categoryId)
? newSet.delete(categoryId)
: newSet.add(categoryId);
return newSet;
});
setCurrentPage(1);
setAllProducts([]);
}, []);
const handlePriceChange = useCallback((values: number[]) => {
setPriceRange([values[0], values[1]]);
setCurrentPage(1);
setAllProducts([]);
}, []);
const handlePriceSortChange = useCallback(
(sortType: "none" | "lowToHigh" | "highToLow") => {
setPriceSort(sortType);
},
[]
);
const resetFilters = useCallback(() => {
setSelectedBrands(new Set());
setSelectedFilterCategories(new Set());
setPriceRange([0, 10000]);
setPriceSort("none");
setCurrentPage(1);
setAllProducts([]);
}, []);
const filterTranslations = useMemo(
() => ({
category: t("category"),
brands: t("brands"),
sort: t("sort"),
default: t("default"),
price_low_to_high: t("price_low_to_high"),
price_high_to_low: t("price_high_to_low"),
price: t("price"),
price_from: t("price_from"),
price_to: t("price_to"),
reset: t("reset"),
}),
[t]
);
// ERROR STATE
if (categoriesError || productsError || filtersError) {
return <ErrorPage />;
}
// LOADING STATE
if (categoriesLoading) {
return (
<div className="flex flex-col mx-auto max-w-[1504px] px-2 md:px-4 lg:px-6 pb-12">
{/* Title Skeleton */}
<Skeleton className="h-16 w-full rounded-t-lg mb-0 bg-white" />
<div className="flex p-2 md:p-4 gap-4 bg-white rounded-b-lg mt-0">
{/* Desktop Filters Skeleton */}
<div className="hidden sm:block w-[280px] shrink-0 border-r px-4 space-y-6">
<Skeleton className="h-8 w-32" />
<div className="space-y-2">
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<Skeleton className="h-8 w-32" />
<div className="space-y-2">
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-10 w-full rounded-lg" />
</div>
{/* Products Grid Skeleton */}
<div className="flex-1">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="w-full aspect-square rounded-lg" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-6 w-1/2" />
</div>
))}
</div>
</div>
</div>
</div>
);
}
// CATEGORY NOT FOUND
if (!selectedCategory) {
return <div className="text-center py-8">{t("category_not_found")}</div>;
}
return (
<div className="flex flex-col mx-auto max-w-[1504px] px-2 md:px-4 lg:px-6 pb-12">
<h2 className="p-4 text-3xl font-bold pb-6 rounded-t-lg mb-0 bg-white">
{selectedCategory.name}
</h2>
<div className="flex p-2 md:p-4 gap-4 bg-white rounded-b-lg">
{/* Desktop Filters Sidebar */}
<div className="hidden sm:block w-[280px] shrink-0 border-r px-4">
<ScrollArea className="h-auto">
{filtersLoading ? (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-10 w-full rounded-lg" />
</div>
) : (
<CategoryFilters
filtersData={filtersData}
selectedBrands={selectedBrands}
selectedFilterCategories={selectedFilterCategories}
priceSort={priceSort}
priceRange={priceRange}
onBrandToggle={handleBrandToggle}
onCategoryToggle={handleCategoryToggle}
onPriceSortChange={handlePriceSortChange}
onPriceChange={handlePriceChange}
onReset={resetFilters}
translations={filterTranslations}
/>
)}
</ScrollArea>
</div>
{/* Products Grid */}
<div className="flex-1 bg-white rounded-lg mb-6">
<CategoryProductsGrid
products={sortedProducts}
hasMore={hasMore}
onLoadMore={loadMoreData}
isFetching={isFetching}
translations={{
loading: t("common.loading"),
no_results: t("no_results"),
}}
/>
</div>
</div>
{/* Mobile Filters Sheet */}
<CategoryFiltersSheet
isOpen={isSheetOpen}
onOpenChange={setIsSheetOpen}
filterLabel={t("filter")}
closeLabel={t("close")}
>
{filtersLoading ? (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</div>
<Skeleton className="h-32 w-full" />
<Skeleton className="h-10 w-full rounded-lg" />
</div>
) : (
<CategoryFilters
filtersData={filtersData}
selectedBrands={selectedBrands}
selectedFilterCategories={selectedFilterCategories}
priceSort={priceSort}
priceRange={priceRange}
onBrandToggle={handleBrandToggle}
onCategoryToggle={handleCategoryToggle}
onPriceSortChange={handlePriceSortChange}
onPriceChange={handlePriceChange}
onReset={resetFilters}
translations={filterTranslations}
/>
)}
</CategoryFiltersSheet>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import InfiniteScroll from "react-infinite-scroll-component";
import ProductCard from "@/features/home/components/ProductCard";
import type { Product } from "@/lib/types/api";
interface CategoryProductsGridProps {
products: Product[];
hasMore: boolean;
onLoadMore: () => void;
isFetching?: boolean;
translations: {
loading: string;
no_results: string;
};
}
export default function CategoryProductsGrid({
products,
hasMore,
onLoadMore,
isFetching = false,
translations,
}: CategoryProductsGridProps) {
if (products.length === 0 && !isFetching) {
return (
<div className="text-center py-8 text-gray-500">
{translations.no_results}
</div>
);
}
return (
<InfiniteScroll
dataLength={products.length}
next={onLoadMore}
hasMore={hasMore}
scrollThreshold={0.8}
style={{ overflow: "visible" }}
loader={
<div className="flex justify-center py-4">
<div className="flex items-center gap-2">
<div className="w-5 h-5 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
<span>{translations.loading}</span>
</div>
</div>
}
endMessage={
products.length > 0 && !hasMore ? (
<div className="text-center py-4 text-gray-500 text-sm">
</div>
) : null
}
>
<div className="bg-white rounded-lg grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{products.map((product) => (
<ProductCard
key={product.id}
id={product.id}
name={product.name}
price={
product.price_amount ? parseFloat(product.price_amount) : null
}
struct_price_text={`${product.price_amount} TMT`}
images={[product.media?.[0]?.images_400x400]}
stock={product.stock}
button={true}
/>
))}
</div>
{isFetching && products.length === 0 && (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mt-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-lg mb-2" />
<div className="bg-gray-200 h-4 rounded w-3/4 mb-2" />
<div className="bg-gray-200 h-4 rounded w-1/2" />
</div>
))}
</div>
)}
</InfiniteScroll>
);
}

View File

@@ -0,0 +1,17 @@
// import { Skeleton } from "@/components/ui/skeleton"
// import { Card } from "@/components/ui/card"
// import { CardContent } from "@/components/ui/card"
// export default function CategorySkeleton() {
// return (
// <Card className="overflow-hidden rounded-xl">
// {/* Image */}
// <Skeleton className="w-full h-36 bg-gray-200" />
// {/* Name */}
// <CardContent className="py-2">
// <Skeleton className="h-4 w-3/4 bg-gray-200" />
// </CardContent>
// </Card>
// )
// }

View File

@@ -0,0 +1,222 @@
import { useQuery } from "@tanstack/react-query"
import { apiClient } from "@/lib/api"
import type { Category, Product, PaginatedResponse, FiltersResponse, ProductFilters } from "@/lib/types/api"
// Get all categories as tree
export function useCategories(options?: { enabled?: boolean }) {
return useQuery({
queryKey: ["categories"],
queryFn: async () => {
const response = await apiClient.get<PaginatedResponse<Category>>("/categories", {
params: { type: "tree" },
})
return response.data.data || response.data
},
enabled: options?.enabled !== false,
staleTime: 1000 * 60 * 30, // 30 minutes
})
}
// Get single category by ID
export function useCategory(id: number | string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: ["category", id],
queryFn: async () => {
const response = await apiClient.get<Category>(`/categories/${id}`)
return response.data
},
enabled: options?.enabled !== false && !!id,
staleTime: 1000 * 60 * 15,
})
}
// Get products for a single category with pagination
export function useCategoryProducts(
categoryId: number | string,
options?: {
enabled?: boolean
page?: number
limit?: number
}
) {
return useQuery({
queryKey: ["category", categoryId, "products", options?.page, options?.limit],
queryFn: async () => {
const response = await apiClient.get<PaginatedResponse<Product>>(
`/categories/${categoryId}/products`,
{
params: {
page: options?.page || 1,
per_page: options?.limit
},
}
)
return {
data: response.data.data || [],
pagination: response.data.pagination || {}
}
},
enabled: options?.enabled !== false && !!categoryId,
})
}
// Get ALL products from category and its children - NO pagination (for initial load)
export function useAllCategoryProducts(
category: Category | undefined,
options?: { enabled?: boolean }
) {
return useQuery({
queryKey: ["category", category?.id, "all-products"],
queryFn: async () => {
if (!category) return []
const fetchProducts = async (categoryId: number) => {
const response = await apiClient.get<PaginatedResponse<Product>>(
`/categories/${categoryId}/products`
)
return response.data.data || []
}
let allProducts = await fetchProducts(category.id)
if (category.children && category.children.length > 0) {
for (const child of category.children) {
const childProducts = await fetchProducts(child.id)
allProducts = [...allProducts, ...childProducts]
}
}
return allProducts
},
enabled: options?.enabled !== false && !!category,
})
}
export function useCategoryFilters(
categoryId: number | string | undefined,
options?: { enabled?: boolean }
) {
return useQuery({
queryKey: ["category-filters", categoryId],
queryFn: async () => {
const response = await apiClient.get<FiltersResponse>(
"/filters",
{
params: { category_id: categoryId },
}
)
return response.data.data
},
enabled: options?.enabled !== false && !!categoryId,
staleTime: 1000 * 60 * 15,
})
}
// Get filtered category products
export function useFilteredCategoryProducts(
categoryId: number | string,
filters: ProductFilters,
options?: { enabled?: boolean }
) {
return useQuery({
queryKey: ["category", categoryId, "filtered-products", filters],
queryFn: async () => {
const params: Record<string, any> = {
page: filters.page || 1,
per_page: filters.limit || 6,
}
if (filters.brands && filters.brands.length > 0) {
params.brands = filters.brands.join(',')
}
if (filters.categories && filters.categories.length > 0) {
params.categories = filters.categories.join(',')
}
if (filters.min_price !== undefined) {
params.min_price = filters.min_price
}
if (filters.max_price !== undefined) {
params.max_price = filters.max_price
}
const response = await apiClient.get<PaginatedResponse<Product>>(
`/categories/${categoryId}/products`,
{ params }
)
return {
data: response.data.data || [],
pagination: response.data.pagination || {}
}
},
enabled: options?.enabled !== false && !!categoryId,
})
}
// Get products from category and children WITH pagination (mimics RTK getAllCategoryProductsPaginated)
export function useAllCategoryProductsPaginated(
category: Category | undefined,
options?: {
enabled?: boolean
page?: number
limit?: number
}
) {
const page = options?.page || 1
const per_page = options?.limit || 6
return useQuery({
queryKey: ["category", category?.id, "paginated-products", page, per_page],
queryFn: async () => {
if (!category) {
return {
data: [],
pagination: {
currentPage: page,
hasMorePages: false
}
}
}
const categoryIds = [category.id]
if (category.children && category.children.length > 0) {
category.children.forEach((child) => categoryIds.push(child.id))
}
const perCategoryLimit = Math.ceil(per_page / categoryIds.length)
const hasMoreByCategory: Record<number, boolean> = {}
let allPageProducts: Product[] = []
for (const categoryId of categoryIds) {
const response = await apiClient.get<PaginatedResponse<Product>>(
`/categories/${categoryId}/products`,
{
params: {
page,
per_page: perCategoryLimit
}
}
)
if (response.data.data) {
allPageProducts = [...allPageProducts, ...response.data.data]
hasMoreByCategory[categoryId] = !!response.data.pagination?.next_page_url
}
}
const hasMorePages = Object.values(hasMoreByCategory).some((hasMore) => hasMore)
return {
data: allPageProducts,
pagination: {
currentPage: page,
hasMorePages
}
}
},
enabled: options?.enabled !== false && !!category,
})
}