fixed some ui, refactored code

This commit is contained in:
Jelaletdin12
2025-12-13 00:05:43 +05:00
parent 5085c0cffd
commit 633a3c9d47
30 changed files with 1274 additions and 923 deletions

View File

@@ -1,497 +0,0 @@
"use client";
import { useEffect, useState, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { SlidersHorizontal, X } from "lucide-react";
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 {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Checkbox } from "@/components/ui/checkbox";
import InfiniteScroll from "react-infinite-scroll-component";
import ProductCard from "@/features/home/components/ProductCard";
import {
useCategories,
useCategoryFilters,
useFilteredCategoryProducts,
} from "@/features/category/hooks/useCategories";
import { useTranslations } from "next-intl";
import type { Category, Product } from "@/lib/types/api";
interface CategoryPageClientProps {
params: { locale: string; slug: string };
}
export default function CategoryPageClient({
params,
}: CategoryPageClientProps) {
const { slug, locale } = params;
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations();
const { data: categoriesData, isLoading: categoriesLoading } =
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]);
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 } = 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);
}
if (priceRange[0] > 0) {
params.min_price = priceRange[0];
}
if (priceRange[1] < 10000) {
params.max_price = priceRange[1];
}
return params;
}, [currentPage, selectedBrands, selectedFilterCategories, priceRange]);
// Fetch filtered products
const {
data: productsData,
isLoading: productsLoading,
isFetching,
} = 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)
);
return [...prev, ...newProducts];
});
}
}, [productsData, currentPage]);
const hasMore = useMemo(() => {
return !!productsData?.pagination?.next_page_url;
}, [productsData]);
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]);
const handleBrandToggle = useCallback((brandId: number) => {
setSelectedBrands((prev) => {
const newSet = new Set(prev);
if (newSet.has(brandId)) {
newSet.delete(brandId);
} else {
newSet.add(brandId);
}
return newSet;
});
setCurrentPage(1);
setAllProducts([]);
}, []);
const handleCategoryToggle = useCallback((categoryId: number) => {
setSelectedFilterCategories((prev) => {
const newSet = new Set(prev);
if (newSet.has(categoryId)) {
newSet.delete(categoryId);
} else {
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 findCategoryById = useCallback(
(categories: Category[] | undefined, id: number): Category | null => {
if (!categories) return null;
for (const category of categories) {
if (category.id === id) return category;
if (category.children) {
const found = findCategoryById(category.children, id);
if (found) return found;
}
}
return null;
},
[]
);
const FiltersContent = useCallback(
() => (
<div className="space-y-6">
{filtersData?.categories && filtersData.categories.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">{t("category")}</h3>
<div className="space-y-2">
{filtersData.categories.map((category) => (
<label
key={category.id}
className="flex items-center gap-2 cursor-pointer"
>
<Checkbox
checked={selectedFilterCategories.has(category.id)}
onCheckedChange={() => handleCategoryToggle(category.id)}
/>
<span className="text-sm">{category.name}</span>
</label>
))}
</div>
</div>
)}
{filtersData?.brands && filtersData.brands.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">{t("brands")}</h3>
<div className="space-y-2">
{filtersData.brands.map((brand) => (
<label
key={brand.id}
className="flex items-center gap-2 cursor-pointer"
>
<Checkbox
checked={selectedBrands.has(brand.id)}
onCheckedChange={() => handleBrandToggle(brand.id)}
/>
<span className="text-sm">{brand.name}</span>
</label>
))}
</div>
</div>
)}
<div>
<h3 className="text-lg font-semibold mb-3">{t("sort")}</h3>
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="sort"
checked={priceSort === "none"}
onChange={() => handlePriceSortChange("none")}
className="w-4 h-4"
/>
<span>{t("default")}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="sort"
checked={priceSort === "lowToHigh"}
onChange={() => handlePriceSortChange("lowToHigh")}
className="w-4 h-4"
/>
<span>{t("price_low_to_high")}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="sort"
checked={priceSort === "highToLow"}
onChange={() => handlePriceSortChange("highToLow")}
className="w-4 h-4"
/>
<span>{t("price_high_to_low")}</span>
</label>
</div>
</div>
<PriceFilter
title={t("price")}
priceRange={priceRange}
onPriceChange={handlePriceChange}
translations={{ from: t("price_from"), to: t("price_to") }}
/>
<Button
variant="outline"
className="w-full rounded-xl"
onClick={resetFilters}
>
{t("reset")}
</Button>
</div>
),
[
filtersData,
selectedFilterCategories,
selectedBrands,
priceSort,
priceRange,
t,
handleCategoryToggle,
handleBrandToggle,
handlePriceSortChange,
handlePriceChange,
resetFilters,
]
);
if (categoriesLoading) return <div>{t("common.loading")}</div>;
if (!selectedCategory)
return <div className="text-center py-8">{t("category_not_found")}</div>;
const totalItems =
productsData?.pagination?.total || sortedProducts.length || 0;
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-lg mb-0 bg-white">{selectedCategory.name}</h2>
<div className="flex gap-4 bg-white rounded-lg">
<div className="hidden sm:block w-[280px] shrink-0 border-r px-4 ">
<ScrollArea className="h-[calc(100vh-200px)] ">
<FiltersContent />
</ScrollArea>
</div>
<div className="flex-1 bg-white rounded-lg">
{sortedProducts.length > 0 ? (
<InfiniteScroll
dataLength={sortedProducts.length}
next={loadMoreData}
hasMore={hasMore}
scrollThreshold={0.8}
style={{ overflow: "visible" }}
loader={
<div className="flex justify-center py-4">
<div>{t("common.loading")}</div>
</div>
}
>
<div className="bg-white rounded-lg grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{sortedProducts.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]}
button={true} />
))}
</div>
</InfiniteScroll>
) : (
<div className="text-center py-8 text-gray-500">
{t("no_results")}
</div>
)}
</div>
</div>
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button
className="sm:hidden fixed bottom-20 right-4 rounded-xl font-bold gap-2 z-10 shadow-lg"
size="lg"
>
{t("filter")}
<SlidersHorizontal className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[290px] p-0">
<SheetHeader className="p-4 border-b">
<SheetTitle>{t("filter")}</SheetTitle>
<button
onClick={() => setIsOpen(false)}
className="absolute top-4 right-4 rounded-md ring-offset-background transition-opacity hover:opacity-100"
>
<X className="h-4 w-4" />
<span className="sr-only">{t("close")}</span>
</button>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-80px)] p-4">
<FiltersContent />
</ScrollArea>
</SheetContent>
</Sheet>
</div>
);
}
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,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-xl" 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="sm:hidden fixed bottom-20 right-4 rounded-xl 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 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,279 @@
"use client";
import { useEffect, useState, useMemo, useCallback } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
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";
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 } =
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 } = 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);
}
if (priceRange[0] > 0) {
params.min_price = priceRange[0];
}
if (priceRange[1] < 10000) {
params.max_price = priceRange[1];
}
return params;
}, [currentPage, selectedBrands, selectedFilterCategories, priceRange]);
// Fetch filtered products
const { data: productsData, isFetching } = 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)
);
return [...prev, ...newProducts];
});
}
}, [productsData, currentPage]);
const hasMore = useMemo(() => {
return !!productsData?.pagination?.next_page_url;
}, [productsData]);
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]
);
if (categoriesLoading) return <div>{t("common.loading")}</div>;
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 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">
<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}
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")}
>
<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,60 @@
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;
translations: {
loading: string;
no_results: string;
};
}
export default function CategoryProductsGrid({
products,
hasMore,
onLoadMore,
translations,
}: CategoryProductsGridProps) {
if (products.length === 0) {
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>{translations.loading}</div>
</div>
}
>
<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>
</InfiniteScroll>
);
}

View File

@@ -47,7 +47,7 @@ export function useCategoryProducts(
{
params: {
page: options?.page || 1,
limit: options?.limit
per_page: options?.limit
},
}
)
@@ -123,7 +123,7 @@ export function useFilteredCategoryProducts(
queryFn: async () => {
const params: Record<string, any> = {
page: filters.page || 1,
limit: filters.limit || 6,
per_page: filters.limit || 6,
}
if (filters.brands && filters.brands.length > 0) {
@@ -166,10 +166,10 @@ export function useAllCategoryProductsPaginated(
}
) {
const page = options?.page || 1
const limit = options?.limit || 6
const per_page = options?.limit || 6
return useQuery({
queryKey: ["category", category?.id, "paginated-products", page, limit],
queryKey: ["category", category?.id, "paginated-products", page, per_page],
queryFn: async () => {
if (!category) {
return {
@@ -186,7 +186,7 @@ export function useAllCategoryProductsPaginated(
category.children.forEach((child) => categoryIds.push(child.id))
}
const perCategoryLimit = Math.ceil(limit / categoryIds.length)
const perCategoryLimit = Math.ceil(per_page / categoryIds.length)
const hasMoreByCategory: Record<number, boolean> = {}
let allPageProducts: Product[] = []
@@ -196,7 +196,7 @@ export function useAllCategoryProductsPaginated(
{
params: {
page,
limit: perCategoryLimit
per_page: perCategoryLimit
}
}
)