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)}
+ />
+
+
+
+
+ {/* محدوده قیمت */}
+
+
+ محدوده قیمت
+
+
+
+ {/* ورودیهای قیمت */}
+
+
+ {/* اسلایدر قیمت */}
+
+ {
+ 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_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) => (
+
+ ))}
+
+
+
+
+ {/* محدوده قیمت */}
+
+
+ محدوده قیمت
+
+
+
+ {/* ورودیهای قیمت */}
+
+
+ {/* اسلایدر قیمت */}
+
+
+
+
+ {/* واحد قیمت */}
+
+ تومان
+
+
+
+
+
+
+ )
+}
+
+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;
+}