filter price + add wishi list or remove wish list

This commit is contained in:
hamid zarghami
2025-09-06 13:00:20 +03:30
parent 477b42cd31
commit 0f77a3208d
12 changed files with 626 additions and 239 deletions
@@ -0,0 +1,42 @@
import { FC } from 'react'
import { Brand } from '@/types/products.types'
interface BrandFilterProps {
brands: Brand[]
selectedBrand: string
onBrandChange: (brandId: string) => void
onBrandClear: () => void
}
const BrandFilter: FC<BrandFilterProps> = ({
brands,
selectedBrand,
onBrandChange,
onBrandClear,
}) => {
return (
<div className='flex flex-col gap-2.5 text-sm text-[#4A4A4A] max-h-48 overflow-y-auto'>
{brands.map((brand) => (
<label key={brand._id} className='flex items-center gap-2.5'>
<input
type='radio'
name='brand'
className='size-4 border-border'
checked={selectedBrand === brand._id}
onChange={() => onBrandChange(brand._id)}
/>
{brand.title_fa}
</label>
))}
<button
type='button'
onClick={onBrandClear}
className='text-xs text-muted-foreground hover:text-foreground text-right mt-1'
>
حذف برند
</button>
</div>
)
}
export default BrandFilter
@@ -0,0 +1,105 @@
import { FC } from 'react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Category, Brand } from '@/types/products.types'
import CategoryTree from './CategoryTree'
import PriceFilter from './PriceFilter'
import StockFilter from './StockFilter'
import BrandFilter from './BrandFilter'
import WholesaleFilter from './WholesaleFilter'
interface FilterSectionProps {
categories: Category[]
brands: Brand[]
filterState: {
selectedCategories: string[]
priceRange: [number, number]
inStockOnly: boolean
selectedBrand: string
wholeSale: boolean
}
onCategoryChange: (categoryId: string | null) => void
onPriceChange: (range: [number, number]) => void
onStockChange: (inStock: boolean) => void
onBrandChange: (brandId: string) => void
onBrandClear: () => void
onWholesaleChange: (wholesale: boolean) => void
categoryUrl: string
priceMinMaxRange: [number, number] // Original min/max from API
}
const FilterSection: FC<FilterSectionProps> = ({
categories,
brands,
filterState,
onCategoryChange,
onPriceChange,
onStockChange,
onBrandChange,
onBrandClear,
onWholesaleChange,
categoryUrl,
priceMinMaxRange,
}) => {
return (
<div className='mt-3.5'>
<Accordion type='multiple' className='w-full'>
<AccordionItem value='category'>
<AccordionTrigger className='text-[#333333]'>دستهبندی ها</AccordionTrigger>
<AccordionContent>
<CategoryTree
categories={categories}
selectedId={filterState.selectedCategories[0] ?? null}
onSelect={onCategoryChange}
categoryUrl={categoryUrl}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value='price'>
<AccordionTrigger className='text-[#333333]'>قیمت</AccordionTrigger>
<AccordionContent>
<PriceFilter
value={filterState.priceRange}
onChange={onPriceChange}
minMaxRange={priceMinMaxRange}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value='status'>
<AccordionTrigger className='text-[#333333]'>وضعیت موجودی</AccordionTrigger>
<AccordionContent>
<StockFilter
checked={filterState.inStockOnly}
onChange={onStockChange}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value='brand'>
<AccordionTrigger className='text-[#333333]'>برند</AccordionTrigger>
<AccordionContent>
<BrandFilter
brands={brands}
selectedBrand={filterState.selectedBrand}
onBrandChange={onBrandChange}
onBrandClear={onBrandClear}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value='wholesale'>
<AccordionTrigger className='text-[#333333]'>عمدهفروشی</AccordionTrigger>
<AccordionContent>
<WholesaleFilter
checked={filterState.wholeSale}
onChange={onWholesaleChange}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)
}
export default FilterSection
+54 -236
View File
@@ -1,109 +1,43 @@
"use client"
import { FC, useMemo, useState, useEffect, useCallback, useRef } from 'react'
import { FC } from 'react'
import { Setting5 } from 'iconsax-react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { useQuery } from '@tanstack/react-query'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import CategoryTree from './CategoryTree'
import Modal from '@/components/Modal'
import * as api from '../service/Service'
import { Brand, Category } from '@/types/products.types'
import { useFiltersData } from '../hooks/useFiltersData'
import FilterSection from './FilterSection'
type FiltersProps = {
interface FiltersProps {
isMobile?: boolean
isOpen?: boolean
onClose?: () => void
categoryUrl?: string
}
const Filters: FC<FiltersProps> = ({ isMobile = false, isOpen = false, onClose, categoryUrl = 'Organic-supermarket' }) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const Filters: FC<FiltersProps> = ({
isMobile = false,
isOpen = false,
onClose,
categoryUrl = 'Organic-supermarket'
}) => {
const {
filterState,
categories,
brands,
isLoading,
priceMinMaxRange,
applyCategoryFilter,
updatePriceRange,
applyStockFilter,
applyBrandFilter,
clearBrandFilter,
applyWholesaleFilter,
clearAllFilters,
} = useFiltersData({ categoryUrl })
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000000])
const [inStockOnly, setInStockOnly] = useState<boolean>(false)
// const [rating, setRating] = useState<number | null>(null)
const [selectedBrand, setSelectedBrand] = useState<string>('')
const [wholeSale, setWholeSale] = useState<boolean>(false)
// ref برای نگهداری timeout ID
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
// تابع اعمال فیلتر قیمت با debounce
const applyPriceFilter = useCallback((range: [number, number]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
timeoutRef.current = setTimeout(() => {
const params = new URLSearchParams(searchParams.toString())
if (range[0] > 0) params.set('minPrice', range[0].toString())
else params.delete('minPrice')
if (range[1] < 1000000) params.set('maxPrice', range[1].toString())
else params.delete('maxPrice')
router.push(`${pathname}?${params.toString()}`)
}, 1000)
}, [searchParams, pathname, router])
// دریافت brands و categories از API محصولات
const { data: productsData } = useQuery({
queryKey: ['products-filters', categoryUrl, searchParams.toString()],
queryFn: () => {
const params = {
page: 1,
limit: 1,
category: searchParams.get('category') || undefined,
brand: searchParams.get('brand') || undefined,
minPrice: searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : undefined,
maxPrice: searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : undefined,
stock: searchParams.get('stock') || undefined,
wholeSale: searchParams.get('wholeSale') || undefined,
rating: searchParams.get('rating') ? Number(searchParams.get('rating')) : undefined,
}
return api.getCategoryProducts(categoryUrl, params)
},
staleTime: 1000 * 60 * 5,
})
const categories: Category[] = useMemo(() => {
return productsData?.results?.category?.children ?? []
}, [productsData])
const brands: Brand[] = useMemo(() => {
return productsData?.results?.brands ?? []
}, [productsData])
useEffect(() => {
const qsCategories = searchParams.get('category')?.split(',').filter(Boolean) ?? []
const qsMin = searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : 0
const qsMax = searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : 1000000
const qsStock = searchParams.get('stock') === '1'
const qsBrand = searchParams.get('brand') ?? ''
const qsWholeSale = searchParams.get('wholeSale') === '1'
setSelectedCategories(qsCategories)
setPriceRange([qsMin, qsMax])
setInStockOnly(qsStock)
setSelectedBrand(qsBrand)
setWholeSale(qsWholeSale)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const clearFilters = () => {
setSelectedCategories([])
setPriceRange([0, 1000000])
setInStockOnly(false)
setSelectedBrand('')
setWholeSale(false)
const params = new URLSearchParams(searchParams.toString())
;['category', 'minPrice', 'maxPrice', 'stock', 'brand', 'wholeSale'].forEach((k) => params.delete(k))
router.push(`${pathname}?${params.toString()}`)
const handleClearFilters = () => {
clearAllFilters()
// Close mobile modal after clearing filters
if (isMobile && onClose) {
@@ -119,157 +53,41 @@ const Filters: FC<FiltersProps> = ({ isMobile = false, isOpen = false, onClose,
<div className='text-[#666666]'>فیلترها</div>
</div>
<div className='mt-3.5'>
<Accordion type='multiple' className='w-full'>
<AccordionItem value='category'>
<AccordionTrigger className='text-[#333333]'>دستهبندی ها</AccordionTrigger>
<AccordionContent>
<CategoryTree
categories={categories}
selectedId={selectedCategories[0] ?? null}
onSelect={(id) => {
const newCategories = id ? [id] : []
setSelectedCategories(newCategories)
// اعمال فیلتر دسته‌بندی بلافاصله
const params = new URLSearchParams(searchParams.toString())
if (newCategories.length > 0) {
params.set('category', newCategories.join(','))
} else {
params.delete('category')
}
router.push(`${pathname}?${params.toString()}`)
}}
categoryUrl={categoryUrl}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value='price'>
<AccordionTrigger className='text-[#333333]'>قیمت</AccordionTrigger>
<AccordionContent>
<div className='space-y-4'>
<div className='px-1'>
<Slider
value={priceRange}
onValueChange={(value) => {
setPriceRange(value as [number, number])
applyPriceFilter(value as [number, number])
}}
min={0}
max={1000000}
step={10000}
className='w-full'
/>
</div>
<div className='flex items-center justify-between text-sm text-[#666666]'>
<span>حداقل: {priceRange[0].toLocaleString('fa-IR')} تومان</span>
<span>حداکثر: {priceRange[1].toLocaleString('fa-IR')} تومان</span>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value='status'>
<AccordionTrigger className='text-[#333333]'>وضعیت موجودی</AccordionTrigger>
<AccordionContent>
<label className='flex items-center gap-2.5 text-sm text-[#4A4A4A]'>
<input
type='checkbox'
className='size-4 rounded border-border'
checked={inStockOnly}
onChange={(e) => {
setInStockOnly(e.target.checked)
// اعمال فیلتر موجودی بلافاصله
const params = new URLSearchParams(searchParams.toString())
if (e.target.checked) {
params.set('stock', '1')
} else {
params.delete('stock')
}
router.push(`${pathname}?${params.toString()}`)
}}
/>
فقط کالاهای موجود
</label>
</AccordionContent>
</AccordionItem>
<AccordionItem value='brand'>
<AccordionTrigger className='text-[#333333]'>برند</AccordionTrigger>
<AccordionContent>
<div className='flex flex-col gap-2.5 text-sm text-[#4A4A4A] max-h-48 overflow-y-auto'>
{brands.map((brand) => (
<label key={brand._id} className='flex items-center gap-2.5'>
<input
type='radio'
name='brand'
className='size-4 border-border'
checked={selectedBrand === brand._id}
onChange={() => {
setSelectedBrand(brand._id)
// اعمال فیلتر برند بلافاصله
const params = new URLSearchParams(searchParams.toString())
params.set('brand', brand._id)
router.push(`${pathname}?${params.toString()}`)
}}
/>
{brand.title_fa}
</label>
))}
<button
type='button'
onClick={() => {
setSelectedBrand('')
// حذف فیلتر برند بلافاصله
const params = new URLSearchParams(searchParams.toString())
params.delete('brand')
router.push(`${pathname}?${params.toString()}`)
}}
className='text-xs text-muted-foreground hover:text-foreground text-right mt-1'
>
حذف برند
</button>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value='wholesale'>
<AccordionTrigger className='text-[#333333]'>عمدهفروشی</AccordionTrigger>
<AccordionContent>
<label className='flex items-center gap-2.5 text-sm text-[#4A4A4A]'>
<input
type='checkbox'
className='size-4 rounded border-border'
checked={wholeSale}
onChange={(e) => {
setWholeSale(e.target.checked)
// اعمال فیلتر عمده‌فروشی بلافاصله
const params = new URLSearchParams(searchParams.toString())
if (e.target.checked) {
params.set('wholeSale', '1')
} else {
params.delete('wholeSale')
}
router.push(`${pathname}?${params.toString()}`)
}}
/>
فقط کالاهای عمدهفروشی
</label>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<FilterSection
categories={categories}
brands={brands}
filterState={filterState}
onCategoryChange={applyCategoryFilter}
onPriceChange={updatePriceRange}
onStockChange={applyStockFilter}
onBrandChange={applyBrandFilter}
onBrandClear={clearBrandFilter}
onWholesaleChange={applyWholesaleFilter}
categoryUrl={categoryUrl}
priceMinMaxRange={priceMinMaxRange}
/>
<div className='mt-4'>
<Button variant='outline' onClick={clearFilters} className='w-full'>حذف همه فیلترها</Button>
<Button
variant='outline'
onClick={handleClearFilters}
className='w-full'
disabled={isLoading}
>
حذف همه فیلترها
</Button>
</div>
</>
)
if (isMobile) {
return (
<Modal open={isOpen} close={onClose || (() => { })} title="فیلترها" isHeader={true}>
<Modal
open={isOpen}
close={onClose || (() => { })}
title="فیلترها"
isHeader={true}
>
<div className='max-h-[70vh] overflow-y-auto'>
<FilterContent />
</div>
@@ -278,7 +96,7 @@ const Filters: FC<FiltersProps> = ({ isMobile = false, isOpen = false, onClose,
}
return (
<div className='w-[270px] hidden lg:block border border-border rounded-[10px] p-5'>
<div className='w-[270px] hidden lg:block border border-border rounded-[10px] p-5 h-fit'>
<FilterContent />
</div>
)
@@ -0,0 +1,62 @@
import { FC, useEffect, useState } from 'react'
import { Slider } from '@/components/ui/slider'
interface PriceFilterProps {
value: [number, number]
onChange: (value: [number, number]) => void
minMaxRange: [number, number] // Original min/max from API
}
const PriceFilter: FC<PriceFilterProps> = ({ value, onChange, minMaxRange }) => {
const [minValue, setMinValue] = useState(value[0])
const [maxValue, setMaxValue] = useState(value[1])
const [hasUserChanged, setHasUserChanged] = useState(false)
const handleChange = (newValue: [number, number]) => {
const [min, max] = newValue
if (min === minValue && max === maxValue) {
return
}
setMinValue(min)
setMaxValue(max)
setHasUserChanged(true)
}
useEffect(() => {
if (hasUserChanged && minValue && maxValue) {
const timeout = setTimeout(() => {
onChange([minValue, maxValue])
}, 2000)
return () => clearTimeout(timeout)
}
}, [minValue, maxValue, onChange, hasUserChanged])
// Update current values when prop changes
useEffect(() => {
setMinValue(value[0])
setMaxValue(value[1])
setHasUserChanged(false)
}, [value])
return (
<div className='space-y-4'>
<div className='px-1'>
<Slider
value={[minValue, maxValue]}
onValueChange={(newValue) => handleChange(newValue as [number, number])}
min={minMaxRange[0]}
max={minMaxRange[1]}
// step={10000}
className='w-full [&_[data-slot=slider-thumb]:first-of-type]:order-2 [&_[data-slot=slider-thumb]:last-of-type]:order-1'
/>
</div>
<div className='flex items-center justify-between text-sm text-[#666666]'>
<span>{maxValue.toLocaleString('fa-IR')} تومان</span>
<span>{minValue.toLocaleString('fa-IR')} تومان</span>
</div>
</div>
)
}
export default PriceFilter
@@ -0,0 +1,22 @@
import { FC } from 'react'
interface StockFilterProps {
checked: boolean
onChange: (checked: boolean) => void
}
const StockFilter: FC<StockFilterProps> = ({ checked, onChange }) => {
return (
<label className='flex items-center gap-2.5 text-sm text-[#4A4A4A]'>
<input
type='checkbox'
className='size-4 rounded border-border'
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
فقط کالاهای موجود
</label>
)
}
export default StockFilter
@@ -0,0 +1,22 @@
import { FC } from 'react'
interface WholesaleFilterProps {
checked: boolean
onChange: (checked: boolean) => void
}
const WholesaleFilter: FC<WholesaleFilterProps> = ({ checked, onChange }) => {
return (
<label className='flex items-center gap-2.5 text-sm text-[#4A4A4A]'>
<input
type='checkbox'
className='size-4 rounded border-border'
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
فقط کالاهای عمدهفروشی
</label>
)
}
export default WholesaleFilter
+262
View File
@@ -0,0 +1,262 @@
import { useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCategoryProductsData } from "./useProductsData";
import { Brand, Category } from "@/types/products.types";
export interface FilterState {
selectedCategories: string[];
priceRange: [number, number];
inStockOnly: boolean;
selectedBrand: string;
wholeSale: boolean;
}
export interface UseFiltersDataProps {
categoryUrl: string;
}
export const useFiltersData = ({ categoryUrl }: UseFiltersDataProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// State management
const [filterState, setFilterState] = useState<FilterState>({
selectedCategories: [],
priceRange: [0, 1000000], // Default values, will be updated from API
inStockOnly: false,
selectedBrand: "",
wholeSale: false,
});
// Ref for debouncing price filter
const priceFilterTimeoutRef = useRef<NodeJS.Timeout>();
// Get filter parameters from URL
const filterParams = useMemo(
() => ({
page: 1,
limit: 1,
category: searchParams.get("category") || undefined,
brand: searchParams.get("brand") || undefined,
minPrice: searchParams.get("minPrice")
? Number(searchParams.get("minPrice"))
: undefined,
maxPrice: searchParams.get("maxPrice")
? Number(searchParams.get("maxPrice"))
: undefined,
stock: searchParams.get("stock") || undefined,
wholeSale: searchParams.get("wholeSale") || undefined,
rating: searchParams.get("rating")
? Number(searchParams.get("rating"))
: undefined,
}),
[searchParams]
);
// Fetch data for filters
const { data: productsData, isLoading } = useCategoryProductsData(
categoryUrl,
filterParams
);
// Extract categories and brands from API response
const categories: Category[] = useMemo(() => {
return productsData?.results?.category?.children ?? [];
}, [productsData]);
const brands: Brand[] = useMemo(() => {
return productsData?.results?.brands ?? [];
}, [productsData]);
// Extract price range from API response
const apiPriceRange = useMemo(() => {
return productsData?.results?.filters?.priceRange;
}, [productsData]);
// Update price range from API when data is available
useEffect(() => {
if (apiPriceRange) {
setFilterState((prev) => ({
...prev,
priceRange: [apiPriceRange.minPrice, apiPriceRange.maxPrice],
}));
}
}, [apiPriceRange]);
// Initialize state from URL parameters
useEffect(() => {
const qsCategories =
searchParams.get("category")?.split(",").filter(Boolean) ?? [];
const qsMin = searchParams.get("minPrice")
? Number(searchParams.get("minPrice"))
: apiPriceRange?.minPrice ?? 0;
const qsMax = searchParams.get("maxPrice")
? Number(searchParams.get("maxPrice"))
: apiPriceRange?.maxPrice ?? 1000000;
const qsStock = searchParams.get("stock") === "1";
const qsBrand = searchParams.get("brand") ?? "";
const qsWholeSale = searchParams.get("wholeSale") === "1";
setFilterState({
selectedCategories: qsCategories,
priceRange: [qsMin, qsMax],
inStockOnly: qsStock,
selectedBrand: qsBrand,
wholeSale: qsWholeSale,
});
}, [searchParams, apiPriceRange]);
// Update URL parameters
const updateUrlParams = useCallback(
(updates: Partial<Record<string, string | null>>) => {
const params = new URLSearchParams(searchParams.toString());
Object.entries(updates).forEach(([key, value]) => {
if (value === null || value === "") {
params.delete(key);
} else {
params.set(key, value);
}
});
router.push(`${pathname}?${params.toString()}`);
},
[searchParams, pathname, router]
);
// Apply price filter with debouncing
const applyPriceFilter = useCallback(
(range: [number, number]) => {
if (priceFilterTimeoutRef.current) {
clearTimeout(priceFilterTimeoutRef.current);
}
priceFilterTimeoutRef.current = setTimeout(() => {
const updates: Partial<Record<string, string | null>> = {};
if (range[0] > 0) {
updates.minPrice = range[0].toString();
} else {
updates.minPrice = null;
}
const maxPrice = apiPriceRange?.maxPrice ?? 1000000;
if (range[1] < maxPrice) {
updates.maxPrice = range[1].toString();
} else {
updates.maxPrice = null;
}
updateUrlParams(updates);
}, 1000);
},
[updateUrlParams, apiPriceRange]
);
// Apply category filter
const applyCategoryFilter = useCallback(
(categoryId: string | null) => {
const newCategories = categoryId ? [categoryId] : [];
setFilterState((prev) => ({
...prev,
selectedCategories: newCategories,
}));
updateUrlParams({
category: newCategories.length > 0 ? newCategories.join(",") : null,
});
},
[updateUrlParams]
);
// Apply stock filter
const applyStockFilter = useCallback(
(inStock: boolean) => {
setFilterState((prev) => ({ ...prev, inStockOnly: inStock }));
updateUrlParams({ stock: inStock ? "1" : null });
},
[updateUrlParams]
);
// Apply brand filter
const applyBrandFilter = useCallback(
(brandId: string) => {
setFilterState((prev) => ({ ...prev, selectedBrand: brandId }));
updateUrlParams({ brand: brandId });
},
[updateUrlParams]
);
// Clear brand filter
const clearBrandFilter = useCallback(() => {
setFilterState((prev) => ({ ...prev, selectedBrand: "" }));
updateUrlParams({ brand: null });
}, [updateUrlParams]);
// Apply wholesale filter
const applyWholesaleFilter = useCallback(
(wholesale: boolean) => {
setFilterState((prev) => ({ ...prev, wholeSale: wholesale }));
updateUrlParams({ wholeSale: wholesale ? "1" : null });
},
[updateUrlParams]
);
// Clear all filters
const clearAllFilters = useCallback(() => {
const defaultMinPrice = apiPriceRange?.minPrice ?? 0;
const defaultMaxPrice = apiPriceRange?.maxPrice ?? 1000000;
setFilterState({
selectedCategories: [],
priceRange: [defaultMinPrice, defaultMaxPrice],
inStockOnly: false,
selectedBrand: "",
wholeSale: false,
});
const params = new URLSearchParams(searchParams.toString());
const filterKeys = [
"category",
"minPrice",
"maxPrice",
"stock",
"brand",
"wholeSale",
];
filterKeys.forEach((key) => params.delete(key));
router.push(`${pathname}?${params.toString()}`);
}, [searchParams, pathname, router, apiPriceRange]);
// Update price range in state
const updatePriceRange = useCallback(
(range: [number, number]) => {
setFilterState((prev) => ({ ...prev, priceRange: range }));
applyPriceFilter(range);
},
[applyPriceFilter]
);
return {
// State
filterState,
categories,
brands,
isLoading,
priceMinMaxRange: apiPriceRange
? ([apiPriceRange.minPrice, apiPriceRange.maxPrice] as [number, number])
: ([0, 1000000] as [number, number]),
// Actions
applyCategoryFilter,
updatePriceRange,
applyStockFilter,
applyBrandFilter,
clearBrandFilter,
applyWholesaleFilter,
clearAllFilters,
};
};
+13 -1
View File
@@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { ProductsResponse } from "@/types/products.types";
@@ -34,3 +34,15 @@ export const useCategoryProductsData = (
queryFn: () => api.getCategoryProducts(categoryUrl, params),
});
};
export const useAddWishlist = () => {
return useMutation({
mutationFn: api.addWishlist,
});
};
export const useRemoveWishlist = () => {
return useMutation({
mutationFn: api.removeWishlist,
});
};
+17
View File
@@ -1,5 +1,6 @@
import axios from "@/config/axios";
import { ProductsResponse } from "@/types/products.types";
import { WishListType } from "../types/Types";
export const getProducts = async (params?: {
page?: number;
@@ -45,3 +46,19 @@ export const getCategoryProducts = async (
});
return data;
};
export const addWishlist = async (params: WishListType) => {
const { data } = await axios.post(
`/product/${params.productId}/wishlist/add`,
params
);
return data;
};
export const removeWishlist = async (params: WishListType) => {
const { data } = await axios.post(
`/product/${params.productId}/wishlist/remove`,
params
);
return data;
};
+4
View File
@@ -0,0 +1,4 @@
export type WishListType = {
productId: string;
variantId: string;
};
+22 -2
View File
@@ -2,7 +2,8 @@ import { IProduct } from '@/types/landing.types'
import { NumberFormat } from '@/config/func'
import { Heart } from 'iconsax-react'
import Image from 'next/image'
import { FC } from 'react'
import { FC, useState } from 'react'
import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData'
type Props = {
item?: IProduct
@@ -10,6 +11,25 @@ type Props = {
const ProductCard: FC<Props> = ({ item }) => {
const [isWished, setIsWished] = useState(item?.isWishlist)
const { mutate: addWishlist } = useAddWishlist()
const { mutate: removeWishlist } = useRemoveWishlist()
const handleWish = () => {
if (!item?._id || !item?.variants[0]?._id) return
if (isWished) {
removeWishlist({ productId: item._id.toString(), variantId: item.variants[0]._id })
setIsWished(false)
} else {
addWishlist({ productId: item._id.toString(), variantId: item.variants[0]._id })
setIsWished(true)
}
}
if (item) return (
<div className='bg-white rounded-[20px] sm:rounded-[40px] p-4 sm:p-6 w-full max-w-[240px] sm:max-w-[280px] mx-auto shadow'>
<div className='flex justify-center'>
@@ -64,7 +84,7 @@ const ProductCard: FC<Props> = ({ item }) => {
</div>
<div className='flex justify-end'>
<Heart size={20} color='#000' />
<Heart onClick={handleWish} variant={isWished ? 'Bold' : 'Outline'} size={20} color='#000' />
</div>
</div>
)
+1
View File
@@ -81,6 +81,7 @@ export interface IProduct {
url: string;
default_variant: IDefaultVariant;
variants: IVariant[];
isWishlist: boolean;
}
export interface ICategory {