diff --git a/src/app/brand/[id]/page.tsx b/src/app/brand/[id]/page.tsx new file mode 100644 index 0000000..d467e53 --- /dev/null +++ b/src/app/brand/[id]/page.tsx @@ -0,0 +1,229 @@ +'use client' + +import Layout from '@/hoc/Layout' +import { NextPage } from 'next' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb' +import BrandIntro from '../components/BrandIntro' +import BrandFilters from '../components/BrandFilters' +import BrandSorts from '../components/BrandSorts' +import GridWrapper from '@/components/GridWrapper' +import ProductCard from '@/components/ProductCard' +import { Setting5 } from 'iconsax-react' +import { useState, useEffect, useMemo } from 'react' +import InfiniteScroll from 'react-infinite-scroll-component' +import { useInfiniteBrandProductsData } from '../hooks/useInfiniteBrandData' +import { IProduct } from '@/types/landing.types' +import { Product } from '../types/Types' +import { useSearchParams, useParams } from 'next/navigation' + +const BrandPage: NextPage = () => { + const [showMobileFilters, setShowMobileFilters] = useState(false) + const searchParams = useSearchParams() + const params = useParams() + const [filterParams, setFilterParams] = useState({ + page: 1, + limit: 20, + sort: 3 + }) + + // استخراج brandId از URL + const brandId = params.id as string + + // خواندن پارامترها از URL + useEffect(() => { + const urlParams = { + page: parseInt(searchParams.get('page') || '1'), + limit: parseInt(searchParams.get('limit') || '20'), + sort: parseInt(searchParams.get('sort') || '3'), + category: searchParams.get('category') || undefined, + minPrice: searchParams.get('minPrice') ? Number(searchParams.get('minPrice')) : undefined, + maxPrice: searchParams.get('maxPrice') ? Number(searchParams.get('maxPrice')) : undefined, + } + setFilterParams(urlParams) + }, [searchParams]) + + // دریافت داده‌های محصولات از API بر اساس brandId با infinite scroll + const { + data, + isLoading, + error, + fetchNextPage, + hasNextPage + } = useInfiniteBrandProductsData(brandId, filterParams) + + // جمع‌آوری تمام محصولات از صفحات مختلف + const allProducts = useMemo(() => { + if (!data?.pages) return []; + return data.pages.flatMap(page => page.results.products); + }, [data?.pages]); + + // تبدیل Product به IProduct + const convertToIProduct = (product: Product): IProduct => { + // اطمینان از وجود default_variant + const defaultVariant = product.default_variant || product.variants?.[0]; + const price = defaultVariant?.price || { + order_limit: 1, + retailPrice: 0, + is_specialSale: false, + discount_percent: 0, + specialSale_order_limit: null, + specialSale_quantity: null, + specialSale_endDate: null, + selling_price: 0 + }; + + return { + _id: product._id, + title_fa: product.title_fa, + title_en: product.title_en, + seoTitle: product.seoTitle || null, + seoDescription: product.seoDescription || null, + model: '', + description: product.description, + tags: product.tags || [], + shop: defaultVariant?.shop || { + _id: '', + shopName: '', + shopDescription: '', + logo: '' + }, + status: defaultVariant?.market_status || 'active', + imagesUrl: product.imagesUrl, + url: product.url, + isWishlist: false, + default_variant: { + _id: defaultVariant?._id || product._id.toString(), + market_status: defaultVariant?.market_status || 'active', + price: price, + stock: defaultVariant?.stock || 0, + isFreeShip: defaultVariant?.isFreeShip || false, + isWholeSale: defaultVariant?.isWholeSale || false + }, + variants: (product.variants || []).map(variant => ({ + ...variant, + market_status: variant.market_status as "Marketable" | "Unmarketable" | "OutOfStock", + shipmentMethod: variant.shipmentMethod.map(method => ({ + ...method, + deliveryType: method.deliveryType as "Standard" | "SameDay" | "Express" + })) + })) + }; + } + + return ( +
+ {/* معرفی برند */} + {data?.pages?.[0]?.results.brand && ( + + )} + +
+ + + + فروشگاه آناهیتا + + {data?.pages?.[0]?.results.brand && ( + <> + / + + + {data.pages[0].results.brand.title_fa} + + + + )} + + + + {/* Mobile Filter Button */} +
+ +
+ +
+ {/* Desktop Filters */} + + + {/* Mobile Filters Modal */} + setShowMobileFilters(false)} + brandId={brandId} + /> + +
+ + +
+ {isLoading ? ( +
+
در حال بارگذاری...
+
+ ) : error ? ( +
+
خطا در بارگذاری محصولات
+
+ ) : allProducts.length > 0 ? ( + +
در حال بارگذاری محصولات بیشتر...
+
+ } + endMessage={ +
+ {/*
تمام محصولات بارگذاری شد
*/} +
+ } + > + + {allProducts.map((product: Product) => ( + + ))} + + + ) : ( +
+
محصولی یافت نشد
+
+ )} +
+ +
+
+
+ + {/* Breadcrumb */} + + + ) +} + +export default function BrandPageWithLayout() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/brand/components/BrandFilterSection.tsx b/src/app/brand/components/BrandFilterSection.tsx new file mode 100644 index 0000000..9d88498 --- /dev/null +++ b/src/app/brand/components/BrandFilterSection.tsx @@ -0,0 +1,127 @@ +'use client' +import { FC, useState, useCallback } from 'react' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import { Slider } from '@/components/ui/slider' +import Input from '@/components/Input' +import { CategoryVariant } from '../types/Types' +import { NumberFormat } from '@/config/func' +import CategoryTree from '@/app/products/components/CategoryTree' + +interface FilterState { + categories: string[] + priceRange: [number, number] +} + +interface BrandFilterSectionProps { + categories: CategoryVariant[] + filterState: FilterState + onCategoryChange: (categoryId: string) => void + onPriceChange: (range: [number, number]) => void + brandId: string + priceMinMaxRange: { minPrice: number; maxPrice: number } +} + +const BrandFilterSection: FC = ({ + categories, + filterState, + onCategoryChange, + onPriceChange, + priceMinMaxRange, +}) => { + const [priceInputs, setPriceInputs] = useState({ + min: filterState.priceRange?.[0] || priceMinMaxRange.minPrice, + max: filterState.priceRange?.[1] || priceMinMaxRange.maxPrice, + }) + + const handlePriceInputChange = (field: 'min' | 'max', value: string) => { + const numValue = parseInt(value.replace(/,/g, '')) || 0 + setPriceInputs(prev => ({ ...prev, [field]: numValue })) + + if (field === 'min') { + onPriceChange([numValue, priceInputs.max]) + } else { + onPriceChange([priceInputs.min, numValue]) + } + } + + return ( +
+ {/* دسته‌بندی‌ها */} + + + + دسته‌بندی‌ها + + +
+ id && onCategoryChange(id)} + /> +
+
+
+ + {/* محدوده قیمت */} + + + محدوده قیمت + + +
+ {/* ورودی‌های قیمت */} +
+
+ + handlePriceInputChange('min', e.target.value)} + className='text-sm' + placeholder='0' + seprator={true} + /> +
+
+ + handlePriceInputChange('max', e.target.value)} + className='text-sm' + placeholder='8,000,000' + seprator={true} + /> +
+
+ + {/* اسلایدر قیمت */} +
+ { + setPriceInputs({ min: value[0], max: value[1] }) + onPriceChange([value[0], value[1]]) + }} + min={priceMinMaxRange.minPrice} + max={priceMinMaxRange.maxPrice} + step={100000} + className='w-full' + /> +
+ + {/* واحد قیمت */} +
+ تومان +
+
+
+
+
+
+ ) +} + + +export default BrandFilterSection diff --git a/src/app/brand/components/BrandFilters.tsx b/src/app/brand/components/BrandFilters.tsx new file mode 100644 index 0000000..f05c73b --- /dev/null +++ b/src/app/brand/components/BrandFilters.tsx @@ -0,0 +1,93 @@ +"use client" + +import { FC } from 'react' +import { Setting5 } from 'iconsax-react' +import { Button } from '@/components/ui/button' +import Modal from '@/components/Modal' +import BrandFilterSection from './BrandFilterSection' +import { useBrandFiltersData } from '../hooks/useBrandFiltersData' + +interface BrandFiltersProps { + isMobile?: boolean + isOpen?: boolean + onClose?: () => void + brandId: string +} + +const BrandFilters: FC = ({ + isMobile = false, + isOpen = false, + onClose, + brandId +}) => { + const { + filterState, + categories, + isLoading, + priceMinMaxRange, + applyCategoryFilter, + updatePriceRange, + clearAllFilters, + } = useBrandFiltersData({ brandId }) + + const handleClearFilters = () => { + clearAllFilters() + + // Close mobile modal after clearing filters + if (isMobile && onClose) { + onClose() + } + } + + const FilterContent = () => ( + <> +
+ +
فیلترها
+
+ + + +
+ +
+ + ) + + if (isMobile) { + return ( + { })} + title="فیلترها" + isHeader={true} + > +
+ +
+
+ ) + } + + return ( +
+ +
+ ) +} + +export default BrandFilters diff --git a/src/app/brand/components/BrandIntro.tsx b/src/app/brand/components/BrandIntro.tsx new file mode 100644 index 0000000..eff1fed --- /dev/null +++ b/src/app/brand/components/BrandIntro.tsx @@ -0,0 +1,41 @@ +'use client' +import { FC } from 'react' +import Image from 'next/image' +import { Brand } from '../types/Types' + +interface Props { + brand: Brand +} + +const BrandIntro: FC = ({ brand }) => { + return ( +
+
+
+ {brand.title_fa} +
+ +
+

+ {brand.title_en?.toUpperCase()} +

+

+ {brand.title_fa} +

+ +

+ {brand.description} +

+
+
+
+ ) +} + +export default BrandIntro diff --git a/src/app/brand/components/BrandProductsCarousel.tsx b/src/app/brand/components/BrandProductsCarousel.tsx new file mode 100644 index 0000000..fb865a6 --- /dev/null +++ b/src/app/brand/components/BrandProductsCarousel.tsx @@ -0,0 +1,145 @@ +'use client' +import { FC } from 'react' +import { Swiper, SwiperSlide } from 'swiper/react' +import { Pagination, Navigation } from 'swiper/modules' +import ProductCard from '@/components/ProductCard' +import { Product } from '../types/Types' +import { IProduct } from '@/types/landing.types' +import 'swiper/css' +import 'swiper/css/pagination' +import 'swiper/css/navigation' + +interface Props { + products: Product[] + brandName: string +} + +const BrandProductsCarousel: FC = ({ products, brandName }) => { + return ( +
+
+

محصولات برند {brandName}

+
+ نمایش محصولات +
+
+
+ + + {products.map((product) => { + // تبدیل Product به IProduct + const defaultVariant = product.default_variant || product.variants?.[0]; + const price = defaultVariant?.price || { + order_limit: 1, + retailPrice: 0, + is_specialSale: false, + discount_percent: 0, + specialSale_order_limit: null, + specialSale_quantity: null, + specialSale_endDate: null, + selling_price: 0 + }; + + const iProduct: IProduct = { + _id: product._id, + title_fa: product.title_fa, + title_en: product.title_en, + seoTitle: product.seoTitle || null, + seoDescription: product.seoDescription || null, + model: '', + description: product.description, + tags: product.tags || [], + shop: defaultVariant?.shop || { + _id: '', + shopName: '', + shopDescription: '', + logo: '' + }, + status: defaultVariant?.market_status || 'active', + imagesUrl: product.imagesUrl, + url: product.url, + isWishlist: product.isWished || false, + default_variant: { + _id: defaultVariant?._id || product._id.toString(), + market_status: defaultVariant?.market_status || 'active', + price: price, + stock: defaultVariant?.stock || 0, + isFreeShip: defaultVariant?.isFreeShip || false, + isWholeSale: defaultVariant?.isWholeSale || false + }, + variants: (product.variants || []).map(variant => ({ + ...variant, + market_status: variant.market_status as "Marketable" | "Unmarketable" | "OutOfStock", + shipmentMethod: variant.shipmentMethod.map(method => ({ + ...method, + deliveryType: method.deliveryType as "Standard" | "SameDay" | "Express" + })) + })) + }; + + return ( + +
+ +
+
+ ); + })} +
+ + +
+ ) +} + +export default BrandProductsCarousel diff --git a/src/app/brand/components/BrandSorts.tsx b/src/app/brand/components/BrandSorts.tsx new file mode 100644 index 0000000..e10cd2e --- /dev/null +++ b/src/app/brand/components/BrandSorts.tsx @@ -0,0 +1,119 @@ +import { Sort, ArrowDown2 } from 'iconsax-react' +import { FC, useState, useEffect, useRef } from 'react' +import { useRouter, useSearchParams, usePathname } from 'next/navigation' +import { BrandResponse } from '../types/Types' + +const sortOptions = [ + { key: '1', label: 'مرتبط ترین' }, + { key: '2', label: 'پربازدیدترین' }, + { key: '3', label: 'جدیدترین' }, + { key: '4', label: 'پرفروش ترین' }, + { key: '5', label: 'ارزانترین' }, + { key: '6', label: 'گرانترین' }, + { key: '7', label: 'پیشنهاد خریداران' }, +] + +type BrandSortsProps = { + data?: BrandResponse +} + +const BrandSorts: FC = ({ data }) => { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const [selectedSort, setSelectedSort] = useState('3') // default to newest + const [showDropdown, setShowDropdown] = useState(false) + const dropdownRef = useRef(null) + + const selectedOption = sortOptions.find(option => option.key === selectedSort) + + // خواندن sort از URL + useEffect(() => { + const sortFromUrl = searchParams.get('sort') + if (sortFromUrl) { + setSelectedSort(sortFromUrl) + } + }, [searchParams]) + + const handleSortChange = (sortKey: string) => { + setSelectedSort(sortKey) + const params = new URLSearchParams(searchParams.toString()) + params.set('sort', sortKey) + router.push(`${pathname}?${params.toString()}`) + } + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowDropdown(false) + } + } + + if (showDropdown) { + document.addEventListener('mousedown', handleClickOutside) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [showDropdown]) + + return ( +
+ {/* Desktop Version */} +
+
+ +
مرتب سازی:
+
+ + {sortOptions.map((option) => ( +
handleSortChange(option.key)} + > + {option.label} +
+ ))} +
+ + {/* Mobile Version */} +
+ +
مرتب سازی:
+
setShowDropdown(!showDropdown)} + > + {selectedOption?.label} + +
+ + {showDropdown && ( +
+ {sortOptions.map((option) => ( +
{ + handleSortChange(option.key) + setShowDropdown(false) + }} + > + {option.label} +
+ ))} +
+ )} +
+ +
+ {data?.results?.pager?.totalItems || 0} کالا +
+
+ ) +} + +export default BrandSorts diff --git a/src/app/brand/components/Pagination.tsx b/src/app/brand/components/Pagination.tsx new file mode 100644 index 0000000..7c0a554 --- /dev/null +++ b/src/app/brand/components/Pagination.tsx @@ -0,0 +1,90 @@ +'use client' +import { FC } from 'react' +import { ArrowLeft2, ArrowRight2 } from 'iconsax-react' + +interface Props { + currentPage: number + totalPages: number + onPageChange: (page: number) => void +} + +const Pagination: FC = ({ currentPage, totalPages, onPageChange }) => { + const getVisiblePages = () => { + const delta = 2 + const range = [] + const rangeWithDots = [] + + for ( + let i = Math.max(2, currentPage - delta); + i <= Math.min(totalPages - 1, currentPage + delta); + i++ + ) { + range.push(i) + } + + if (currentPage - delta > 2) { + rangeWithDots.push(1, '...') + } else { + rangeWithDots.push(1) + } + + rangeWithDots.push(...range) + + if (currentPage + delta < totalPages - 1) { + rangeWithDots.push('...', totalPages) + } else if (totalPages > 1) { + rangeWithDots.push(totalPages) + } + + return rangeWithDots + } + + const visiblePages = getVisiblePages() + + if (totalPages <= 1) return null + + return ( +
+ {/* دکمه قبلی */} + + + {/* شماره صفحات */} +
+ {visiblePages.map((page, index) => ( +
+ {page === '...' ? ( + ... + ) : ( + + )} +
+ ))} +
+ + {/* دکمه بعدی */} + +
+ ) +} + +export default Pagination diff --git a/src/app/brand/components/ProductFilters.tsx b/src/app/brand/components/ProductFilters.tsx new file mode 100644 index 0000000..08ac001 --- /dev/null +++ b/src/app/brand/components/ProductFilters.tsx @@ -0,0 +1,128 @@ +'use client' +import { FC, useState } from 'react' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' +import { Slider } from '@/components/ui/slider' +import Input from '@/components/Input' +import { CategoryVariant } from '../types/Types' +import { NumberFormat } from '@/config/func' + +interface Props { + categories: CategoryVariant[] + priceRange: { minPrice: number; maxPrice: number } + selectedCategories: string[] + selectedPriceRange: [number, number] + onCategoryChange: (categoryId: string) => void + onPriceRangeChange: (range: [number, number]) => void +} + +const ProductFilters: FC = ({ + categories, + priceRange, + selectedCategories, + selectedPriceRange, + onCategoryChange, + onPriceRangeChange, +}) => { + const [priceInputs, setPriceInputs] = useState({ + min: selectedPriceRange[0], + max: selectedPriceRange[1], + }) + + const handlePriceInputChange = (field: 'min' | 'max', value: string) => { + const numValue = parseInt(value.replace(/,/g, '')) || 0 + setPriceInputs(prev => ({ ...prev, [field]: numValue })) + + if (field === 'min') { + onPriceRangeChange([numValue, selectedPriceRange[1]]) + } else { + onPriceRangeChange([selectedPriceRange[0], numValue]) + } + } + + return ( +
+

فیلترها

+ + {/* دسته‌بندی‌ها */} + + + + دسته‌بندی‌ها + + +
+ {categories.map((category) => ( + + ))} +
+
+
+ + {/* محدوده قیمت */} + + + محدوده قیمت + + +
+ {/* ورودی‌های قیمت */} +
+
+ + handlePriceInputChange('min', e.target.value)} + className='text-sm' + placeholder='0' + /> +
+
+ + handlePriceInputChange('max', e.target.value)} + className='text-sm' + placeholder='8,000,000' + /> +
+
+ + {/* اسلایدر قیمت */} +
+ +
+ + {/* واحد قیمت */} +
+ تومان +
+
+
+
+
+
+ ) +} + +export default ProductFilters diff --git a/src/app/brand/components/ProductGrid.tsx b/src/app/brand/components/ProductGrid.tsx new file mode 100644 index 0000000..79a2fc2 --- /dev/null +++ b/src/app/brand/components/ProductGrid.tsx @@ -0,0 +1,111 @@ +'use client' +import { FC } from 'react' +import ProductCard from '@/components/ProductCard' +import { Product } from '../types/Types' +import { IProduct } from '@/types/landing.types' + +interface Props { + products: Product[] + isLoading?: boolean +} + +const ProductGrid: FC = ({ products, isLoading = false }) => { + if (isLoading) { + return ( +
+
+

محصولات تولید کننده

+
+ +
+ {Array.from({ length: 8 }).map((_, index) => ( +
+
+
+ ))} +
+
+ ) + } + + return ( +
+
+

محصولات تولید کننده

+
+ {products.length} محصول یافت شد +
+
+ + {products.length === 0 ? ( +
+
هیچ محصولی یافت نشد
+
لطفاً فیلترهای خود را تغییر دهید
+
+ ) : ( +
+ {products.map((product) => { + // تبدیل Product به IProduct + const defaultVariant = product.default_variant || product.variants?.[0]; + const price = defaultVariant?.price || { + order_limit: 1, + retailPrice: 0, + is_specialSale: false, + discount_percent: 0, + specialSale_order_limit: null, + specialSale_quantity: null, + specialSale_endDate: null, + selling_price: 0 + }; + + const iProduct: IProduct = { + _id: product._id, + title_fa: product.title_fa, + title_en: product.title_en, + seoTitle: product.seoTitle || null, + seoDescription: product.seoDescription || null, + model: '', + description: product.description, + tags: product.tags || [], + shop: defaultVariant?.shop || { + _id: '', + shopName: '', + shopDescription: '', + logo: '' + }, + status: defaultVariant?.market_status || 'active', + imagesUrl: product.imagesUrl, + url: product.url, + isWishlist: product.isWished || false, + default_variant: { + _id: defaultVariant?._id || product._id.toString(), + market_status: defaultVariant?.market_status || 'active', + price: price, + stock: defaultVariant?.stock || 0, + isFreeShip: defaultVariant?.isFreeShip || false, + isWholeSale: defaultVariant?.isWholeSale || false + }, + variants: (product.variants || []).map(variant => ({ + ...variant, + market_status: variant.market_status as "Marketable" | "Unmarketable" | "OutOfStock", + shipmentMethod: variant.shipmentMethod.map(method => ({ + ...method, + deliveryType: method.deliveryType as "Standard" | "SameDay" | "Express" + })) + })) + }; + + return ( + + ); + })} +
+ )} +
+ ) +} + +export default ProductGrid diff --git a/src/app/brand/hooks/useBrandData.ts b/src/app/brand/hooks/useBrandData.ts new file mode 100644 index 0000000..4d7a4c0 --- /dev/null +++ b/src/app/brand/hooks/useBrandData.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/Service"; +import { BrandProductsParams } from "../types/Types"; + +export const useGetBrandProducts = (params: BrandProductsParams) => { + return useQuery({ + queryKey: ["brand-products", params], + queryFn: () => api.getBrandProducts(params), + enabled: !!params.brandId, + }); +}; diff --git a/src/app/brand/hooks/useBrandFiltersData.ts b/src/app/brand/hooks/useBrandFiltersData.ts new file mode 100644 index 0000000..c3e13b0 --- /dev/null +++ b/src/app/brand/hooks/useBrandFiltersData.ts @@ -0,0 +1,94 @@ +import { useState, useCallback, useEffect } from "react"; +import { useGetBrandProducts } from "./useBrandData"; +import { CategoryVariant } from "../types/Types"; + +interface UseBrandFiltersDataProps { + brandId: string; +} + +export const useBrandFiltersData = ({ brandId }: UseBrandFiltersDataProps) => { + const [filterState, setFilterState] = useState<{ + categories: string[]; + priceRange: [number, number]; + }>({ + categories: [], + priceRange: [0, 8000000], + }); + + const { data, isLoading } = useGetBrandProducts({ + brandId, + page: 1, + limit: 20, + category: + filterState.categories.length > 0 + ? filterState.categories.join(",") + : undefined, + priceMin: + filterState.priceRange[0] > 0 ? filterState.priceRange[0] : undefined, + priceMax: + filterState.priceRange[1] < 8000000 + ? filterState.priceRange[1] + : undefined, + sort: 3, + }); + + // بروزرسانی محدوده قیمت از API + useEffect(() => { + if (data?.results?.filters?.priceRange) { + const { minPrice, maxPrice } = data.results.filters.priceRange; + setFilterState((prev) => ({ + ...prev, + priceRange: [minPrice, maxPrice], + })); + } + }, [data?.results?.filters?.priceRange]); + + // نمایش دسته اصلی برند برای ساختار درختی + const categories: CategoryVariant[] = data?.results?.category + ? [data.results.category as CategoryVariant] + : []; + + const priceMinMaxRange = data?.results?.filters?.priceRange || { + minPrice: 0, + maxPrice: 8000000, + }; + + const applyCategoryFilter = useCallback((categoryId: string) => { + setFilterState((prev) => ({ + ...prev, + categories: prev.categories.includes(categoryId) + ? prev.categories.filter((id) => id !== categoryId) + : [...prev.categories, categoryId], + })); + }, []); + + const updatePriceRange = useCallback((range: [number, number]) => { + setFilterState((prev) => ({ + ...prev, + priceRange: range, + })); + }, []); + + const clearAllFilters = useCallback(() => { + const defaultPriceRange = data?.results?.filters?.priceRange + ? [ + data.results.filters.priceRange.minPrice, + data.results.filters.priceRange.maxPrice, + ] + : [0, 8000000]; + setFilterState({ + categories: [], + priceRange: defaultPriceRange as [number, number], + }); + }, [data?.results?.filters?.priceRange]); + + return { + filterState, + categories, + isLoading, + priceMinMaxRange, + applyCategoryFilter, + updatePriceRange, + clearAllFilters, + }; +}; diff --git a/src/app/brand/hooks/useInfiniteBrandData.ts b/src/app/brand/hooks/useInfiniteBrandData.ts new file mode 100644 index 0000000..bbba703 --- /dev/null +++ b/src/app/brand/hooks/useInfiniteBrandData.ts @@ -0,0 +1,33 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import * as api from "../service/Service"; +import { BrandResponse } from "../types/Types"; + +interface UseInfiniteBrandProductsDataParams { + page?: number; + limit?: number; + category?: string; + minPrice?: number; + maxPrice?: number; + sort?: number; +} + +export const useInfiniteBrandProductsData = ( + brandId: string, + params?: UseInfiniteBrandProductsDataParams +) => { + return useInfiniteQuery({ + queryKey: ["infinite-brand-products", brandId, params], + queryFn: ({ pageParam = 1 }) => + api.getBrandProducts({ + brandId, + ...params, + page: pageParam as number, + }), + getNextPageParam: (lastPage) => { + const pager = lastPage.results.pager; + return pager.nextPage ? pager.page + 1 : undefined; + }, + initialPageParam: 1, + enabled: !!brandId, + }); +}; diff --git a/src/app/brand/service/Service.ts b/src/app/brand/service/Service.ts new file mode 100644 index 0000000..46b085c --- /dev/null +++ b/src/app/brand/service/Service.ts @@ -0,0 +1,31 @@ +import axios from "@/config/axios"; +import { BrandResponse, BrandProductsParams } from "../types/Types"; + +export const getBrandProducts = async ( + params: BrandProductsParams +): Promise => { + const { + brandId, + page = 1, + limit = 10, + sort, + priceMin, + priceMax, + category, + } = params; + + const queryParams = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (sort) queryParams.append("sort", sort.toString()); + if (priceMin) queryParams.append("priceMin", priceMin.toString()); + if (priceMax) queryParams.append("priceMax", priceMax.toString()); + if (category) queryParams.append("category", category); + + const { data } = await axios.get( + `/brand/${brandId}/search?${queryParams.toString()}` + ); + return data; +}; diff --git a/src/app/brand/types/Types.ts b/src/app/brand/types/Types.ts new file mode 100644 index 0000000..e6da0ed --- /dev/null +++ b/src/app/brand/types/Types.ts @@ -0,0 +1,219 @@ +// تایپ‌های اصلی برند +export interface Brand { + _id: string; + title_fa: string; + title_en: string; + category: string; + status: string; + description: string; + logoUrl: string; + images: string[]; + deleted: boolean; + createdAt: string; + updatedAt: string; + url: string; +} + +// تایپ‌های محصول +export interface ProductImage { + cover: string; + list: string[]; +} + +export interface ProductSpecification { + title: string; + values: string[]; +} + +export interface ProductBrand { + _id: string; + status: string; + title_en: string; + title_fa: string; + images: string[]; + logoUrl: string; +} + +export interface ProductCategory { + _id: string; + title_fa: string; + title_en: string; + icon: string; + imageUrl: string; + theme: string; + description: string; + leaf: boolean; + parent: string; +} + +export interface ProductPrice { + order_limit: number; + retailPrice: number; + selling_price: number; + is_specialSale: boolean; + discount_percent: number; + specialSale_order_limit: number | null; + specialSale_quantity: number | null; + specialSale_endDate: string | null; +} + +export interface Shop { + _id: string; + shopName: string; + shopCode: number; + owner: string; + shopDescription: string; + telephoneNumber: string; + isChatActive: boolean; + shopPostalCode: string; + logo: string; +} + +export interface ShipmentMethod { + _id: number; + name: string; + description: string; + deliveryTime: number; + deliveryType: string; +} + +export interface Warranty { + _id: number; + duration: string; + logoUrl: string; + name: string; +} + +export interface Meterage { + _id: number; + value: string; +} + +export interface ProductVariant { + _id: string; + market_status: string; + price: ProductPrice; + stock: number; + postingTime: number; + isFreeShip: boolean; + isWholeSale: boolean; + shop: Shop; + shipmentMethod: ShipmentMethod[]; + warranty: Warranty; + meterage: Meterage; +} + +export interface Product { + _id: number; + url: string; + title_fa: string; + title_en: string; + seoTitle: string; + seoDescription: string | null; + source: string; + description: string; + metaDescription: string; + tags: string[]; + advantages: string[]; + disAdvantages: string[]; + imagesUrl: ProductImage; + isFake: string; + isWished: boolean | null; + specifications: ProductSpecification[]; + brand: ProductBrand; + category: ProductCategory; + default_variant: ProductVariant; + variants: ProductVariant[]; +} + +// تایپ‌های فیلتر +export interface PriceRange { + minPrice: number; + maxPrice: number; +} + +export interface Filters { + priceRange: PriceRange; +} + +// تایپ‌های دسته‌بندی +export interface CategoryVariant { + _id: string; + title_fa: string; + title_en: string; + icon: string; + imageUrl: string; + description: string; + variants: number[]; + hierarchy: string[]; + leaf: boolean; + parent: string; + deleted: boolean; + createdAt: string; + updatedAt: string; + url: string; + children: CategoryVariant[]; +} + +export interface Category { + _id: string; + title_fa: string; + title_en: string; + icon: string; + imageUrl: string; + description: string; + variants: number[]; + hierarchy: string[]; + leaf: boolean; + parent: string | null; + deleted: boolean; + createdAt: string; + updatedAt: string; + url: string; + children: CategoryVariant[]; +} + +// تایپ‌های پاژینیشن +export interface Pager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean; + nextPage: boolean; +} + +// تایپ اصلی پاسخ API +export interface BrandResponse { + status: number; + success: boolean; + results: { + brand: Brand; + products: Product[]; + filters: Filters; + category: Category; + pager: Pager; + }; +} + +// تایپ‌های پارامترهای درخواست +export interface BrandParams { + id: string; + page?: number; + limit?: number; + sort?: number; + priceMin?: number; + priceMax?: number; + category?: string; +} + +// تایپ پارامترهای hook برای دریافت محصولات برند +export interface BrandProductsParams { + brandId: string; + page?: number; + limit?: number; + sort?: number; + priceMin?: number; + priceMax?: number; + category?: string; +}